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) { |
||
0 ignored issues
–
show
|
|||
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; |
||
116 | } |
||
117 | |||
118 | function task_sigint_handler() { |
||
119 | Debug::log("[TASK] SIG_INT received.\n"); |
||
120 | task_shutdown(); |
||
121 | die; |
||
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 |
This check looks for parameters that have been defined for a function or method, but which are not used in the method body.