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