codysnider /
tt-rss
| 1 | #!/usr/bin/env php |
||
| 2 | <?php |
||
| 3 | set_include_path(dirname(__FILE__).DIRECTORY_SEPARATOR."include".PATH_SEPARATOR. |
||
| 4 | get_include_path()); |
||
| 5 | |||
| 6 | declare(ticks=1); |
||
| 7 | chdir(dirname(__FILE__)); |
||
| 8 | |||
| 9 | define('DISABLE_SESSIONS', true); |
||
| 10 | |||
| 11 | require_once "autoload.php"; |
||
| 12 | require_once "functions.php"; |
||
| 13 | require_once "config.php"; |
||
| 14 | |||
| 15 | // defaults |
||
| 16 | define_default('PURGE_INTERVAL', 3600); // seconds |
||
| 17 | define_default('MAX_CHILD_RUNTIME', 1800); // seconds |
||
| 18 | define_default('MAX_JOBS', 2); |
||
| 19 | define_default('SPAWN_INTERVAL', DAEMON_SLEEP_INTERVAL); // seconds |
||
| 20 | |||
| 21 | require_once "sanity_check.php"; |
||
| 22 | require_once "db.php"; |
||
| 23 | require_once "db-prefs.php"; |
||
| 24 | |||
| 25 | if (!function_exists('pcntl_fork')) { |
||
| 26 | die("error: This script requires PHP compiled with PCNTL module.\n"); |
||
| 27 | } |
||
| 28 | |||
| 29 | $options = getopt(""); |
||
| 30 | |||
| 31 | if (!is_array($options)) { |
||
| 32 | die("error: getopt() failed. ". |
||
| 33 | "Most probably you are using PHP CGI to run this script ". |
||
| 34 | "instead of required PHP CLI. Check tt-rss wiki page on updating feeds for ". |
||
| 35 | "additional information.\n"); |
||
| 36 | } |
||
| 37 | |||
| 38 | |||
| 39 | $master_handlers_installed = false; |
||
| 40 | |||
| 41 | $children = array(); |
||
| 42 | $ctimes = array(); |
||
| 43 | |||
| 44 | $last_checkpoint = -1; |
||
| 45 | |||
| 46 | function reap_children() { |
||
| 47 | global $children; |
||
| 48 | global $ctimes; |
||
| 49 | |||
| 50 | $tmp = array(); |
||
| 51 | |||
| 52 | foreach ($children as $pid) { |
||
| 53 | if (pcntl_waitpid($pid, $status, WNOHANG) != $pid) { |
||
| 54 | |||
| 55 | if (file_is_locked("update_daemon-$pid.lock")) { |
||
| 56 | array_push($tmp, $pid); |
||
| 57 | } else { |
||
| 58 | Debug::log("[reap_children] child $pid seems active but lockfile is unlocked."); |
||
| 59 | unset($ctimes[$pid]); |
||
| 60 | |||
| 61 | } |
||
| 62 | } else { |
||
| 63 | Debug::log("[reap_children] child $pid reaped."); |
||
| 64 | unset($ctimes[$pid]); |
||
| 65 | } |
||
| 66 | } |
||
| 67 | |||
| 68 | $children = $tmp; |
||
| 69 | |||
| 70 | return count($tmp); |
||
| 71 | } |
||
| 72 | |||
| 73 | function check_ctimes() { |
||
| 74 | global $ctimes; |
||
| 75 | |||
| 76 | foreach (array_keys($ctimes) as $pid) { |
||
| 77 | $started = $ctimes[$pid]; |
||
| 78 | |||
| 79 | if (time() - $started > MAX_CHILD_RUNTIME) { |
||
| 80 | Debug::log("[MASTER] child process $pid seems to be stuck, aborting..."); |
||
| 81 | posix_kill($pid, SIGKILL); |
||
| 82 | } |
||
| 83 | } |
||
| 84 | } |
||
| 85 | |||
| 86 | function sigchld_handler($signal) { |
||
| 87 | $running_jobs = reap_children(); |
||
| 88 | |||
| 89 | Debug::log("[SIGCHLD] jobs left: $running_jobs"); |
||
| 90 | |||
| 91 | pcntl_waitpid(-1, $status, WNOHANG); |
||
| 92 | } |
||
| 93 | |||
| 94 | function shutdown($caller_pid) { |
||
| 95 | if ($caller_pid == posix_getpid()) { |
||
| 96 | if (file_exists(LOCK_DIRECTORY."/update_daemon.lock")) { |
||
| 97 | Debug::log("removing lockfile (master)..."); |
||
| 98 | unlink(LOCK_DIRECTORY."/update_daemon.lock"); |
||
| 99 | } |
||
| 100 | } |
||
| 101 | } |
||
| 102 | |||
| 103 | function task_shutdown() { |
||
| 104 | $pid = posix_getpid(); |
||
| 105 | |||
| 106 | if (file_exists(LOCK_DIRECTORY."/update_daemon-$pid.lock")) { |
||
| 107 | Debug::log("removing lockfile ($pid)..."); |
||
| 108 | unlink(LOCK_DIRECTORY."/update_daemon-$pid.lock"); |
||
| 109 | } |
||
| 110 | } |
||
| 111 | |||
| 112 | function sigint_handler() { |
||
| 113 | Debug::log("[MASTER] SIG_INT received.\n"); |
||
| 114 | shutdown(posix_getpid()); |
||
| 115 | die; |
||
|
0 ignored issues
–
show
|
|||
| 116 | } |
||
| 117 | |||
| 118 | function task_sigint_handler() { |
||
| 119 | Debug::log("[TASK] SIG_INT received.\n"); |
||
| 120 | task_shutdown(); |
||
| 121 | die; |
||
|
0 ignored issues
–
show
|
|||
| 122 | } |
||
| 123 | |||
| 124 | pcntl_signal(SIGCHLD, 'sigchld_handler'); |
||
| 125 | |||
| 126 | $longopts = array( |
||
| 127 | "log:", |
||
| 128 | "log-level:", |
||
| 129 | "tasks:", |
||
| 130 | "interval:", |
||
| 131 | "quiet", |
||
| 132 | "help" |
||
| 133 | ); |
||
| 134 | |||
| 135 | $options = getopt("", $longopts); |
||
| 136 | |||
| 137 | if (isset($options["help"])) { |
||
| 138 | print "Tiny Tiny RSS update daemon.\n\n"; |
||
| 139 | print "Options:\n"; |
||
| 140 | print " --log FILE - log messages to FILE\n"; |
||
| 141 | print " --log-level N - log verbosity level\n"; |
||
| 142 | print " --tasks N - amount of update tasks to spawn\n"; |
||
| 143 | print " default: ".MAX_JOBS."\n"; |
||
| 144 | print " --interval N - task spawn interval\n"; |
||
| 145 | print " default: ".SPAWN_INTERVAL." seconds.\n"; |
||
| 146 | print " --quiet - don't output messages to stdout\n"; |
||
| 147 | return; |
||
| 148 | } |
||
| 149 | |||
| 150 | Debug::set_enabled(true); |
||
| 151 | |||
| 152 | if (isset($options["log-level"])) { |
||
| 153 | Debug::set_loglevel((int) $options["log-level"]); |
||
| 154 | } |
||
| 155 | |||
| 156 | if (isset($options["log"])) { |
||
| 157 | Debug::set_quiet(isset($options['quiet'])); |
||
| 158 | Debug::set_logfile($options["log"]); |
||
| 159 | Debug::log("Logging to ".$options["log"]); |
||
| 160 | } else { |
||
| 161 | if (isset($options['quiet'])) { |
||
| 162 | Debug::set_loglevel(Debug::$LOG_DISABLED); |
||
| 163 | } |
||
| 164 | } |
||
| 165 | |||
| 166 | if (isset($options["tasks"])) { |
||
| 167 | Debug::log("Set to spawn ".$options["tasks"]." children."); |
||
| 168 | $max_jobs = $options["tasks"]; |
||
| 169 | } else { |
||
| 170 | $max_jobs = MAX_JOBS; |
||
| 171 | } |
||
| 172 | |||
| 173 | if (isset($options["interval"])) { |
||
| 174 | Debug::log("Spawn interval: ".$options["interval"]." seconds."); |
||
| 175 | $spawn_interval = $options["interval"]; |
||
| 176 | } else { |
||
| 177 | $spawn_interval = SPAWN_INTERVAL; |
||
| 178 | } |
||
| 179 | |||
| 180 | // let's enforce a minimum spawn interval as to not forkbomb the host |
||
| 181 | $spawn_interval = max(60, $spawn_interval); |
||
| 182 | Debug::log("Spawn interval: $spawn_interval sec"); |
||
| 183 | |||
| 184 | if (file_is_locked("update_daemon.lock")) { |
||
| 185 | die("error: Can't create lockfile. ". |
||
| 186 | "Maybe another daemon is already running.\n"); |
||
| 187 | } |
||
| 188 | |||
| 189 | // Try to lock a file in order to avoid concurrent update. |
||
| 190 | $lock_handle = make_lockfile("update_daemon.lock"); |
||
| 191 | |||
| 192 | if (!$lock_handle) { |
||
| 193 | die("error: Can't create lockfile. ". |
||
| 194 | "Maybe another daemon is already running.\n"); |
||
| 195 | } |
||
| 196 | |||
| 197 | $schema_version = get_schema_version(); |
||
| 198 | |||
| 199 | if ($schema_version != SCHEMA_VERSION) { |
||
| 200 | die("Schema version is wrong, please upgrade the database.\n"); |
||
| 201 | } |
||
| 202 | |||
| 203 | // Protip: children close shared database handle when terminating, it's a bad idea to |
||
| 204 | // do database stuff on main process from now on. |
||
| 205 | |||
| 206 | while (true) { |
||
| 207 | |||
| 208 | // Since sleep is interupted by SIGCHLD, we need another way to |
||
| 209 | // respect the spawn interval |
||
| 210 | $next_spawn = $last_checkpoint + $spawn_interval - time(); |
||
| 211 | |||
| 212 | if ($next_spawn % 60 == 0) { |
||
| 213 | $running_jobs = count($children); |
||
| 214 | Debug::log("[MASTER] active jobs: $running_jobs, next spawn at $next_spawn sec."); |
||
| 215 | } |
||
| 216 | |||
| 217 | if ($last_checkpoint + $spawn_interval < time()) { |
||
| 218 | check_ctimes(); |
||
| 219 | reap_children(); |
||
| 220 | |||
| 221 | for ($j = count($children); $j < $max_jobs; $j++) { |
||
| 222 | $pid = pcntl_fork(); |
||
| 223 | if ($pid == -1) { |
||
| 224 | die("fork failed!\n"); |
||
| 225 | } else if ($pid) { |
||
| 226 | |||
| 227 | if (!$master_handlers_installed) { |
||
| 228 | Debug::log("[MASTER] installing shutdown handlers"); |
||
| 229 | pcntl_signal(SIGINT, 'sigint_handler'); |
||
| 230 | pcntl_signal(SIGTERM, 'sigint_handler'); |
||
| 231 | register_shutdown_function('shutdown', posix_getpid()); |
||
| 232 | $master_handlers_installed = true; |
||
| 233 | } |
||
| 234 | |||
| 235 | Debug::log("[MASTER] spawned client $j [PID:$pid]..."); |
||
| 236 | array_push($children, $pid); |
||
| 237 | $ctimes[$pid] = time(); |
||
| 238 | } else { |
||
| 239 | pcntl_signal(SIGCHLD, SIG_IGN); |
||
| 240 | pcntl_signal(SIGINT, 'task_sigint_handler'); |
||
| 241 | |||
| 242 | register_shutdown_function('task_shutdown'); |
||
| 243 | |||
| 244 | $quiet = (isset($options["quiet"])) ? "--quiet" : ""; |
||
| 245 | $log = function_exists("flock") && isset($options['log']) ? '--log '.$options['log'] : ''; |
||
| 246 | |||
| 247 | $my_pid = posix_getpid(); |
||
| 248 | |||
| 249 | passthru(PHP_EXECUTABLE." update.php --daemon-loop $quiet $log --task $j --pidlock $my_pid"); |
||
| 250 | |||
| 251 | sleep(1); |
||
| 252 | |||
| 253 | // We exit in order to avoid fork bombing. |
||
| 254 | exit(0); |
||
| 255 | } |
||
| 256 | } |
||
| 257 | $last_checkpoint = time(); |
||
| 258 | } |
||
| 259 | sleep(1); |
||
| 260 | } |
||
| 261 |
In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.