Generate file attachment transactions for explicit Remarkup attachments on common...
[phabricator.git] / src / applications / daemon / management / PhabricatorDaemonManagementWorkflow.php
blob853a797448cf75d7cbef746c879c630d1ace4078
1 <?php
3 abstract class PhabricatorDaemonManagementWorkflow
4 extends PhabricatorManagementWorkflow {
6 private $runDaemonsAsUser = null;
8 final protected function loadAvailableDaemonClasses() {
9 return id(new PhutilSymbolLoader())
10 ->setAncestorClass('PhutilDaemon')
11 ->setConcreteOnly(true)
12 ->selectSymbolsWithoutLoading();
15 final protected function getLogDirectory() {
16 $path = PhabricatorEnv::getEnvConfig('phd.log-directory');
17 return $this->getControlDirectory($path);
20 private function getControlDirectory($path) {
21 if (!Filesystem::pathExists($path)) {
22 list($err) = exec_manual('mkdir -p %s', $path);
23 if ($err) {
24 throw new Exception(
25 pht(
26 "%s requires the directory '%s' to exist, but it does not exist ".
27 "and could not be created. Create this directory or update ".
28 "'%s' in your configuration to point to an existing ".
29 "directory.",
30 'phd',
31 $path,
32 'phd.log-directory'));
35 return $path;
38 private function findDaemonClass($substring) {
39 $symbols = $this->loadAvailableDaemonClasses();
41 $symbols = ipull($symbols, 'name');
42 $match = array();
43 foreach ($symbols as $symbol) {
44 if (stripos($symbol, $substring) !== false) {
45 if (strtolower($symbol) == strtolower($substring)) {
46 $match = array($symbol);
47 break;
48 } else {
49 $match[] = $symbol;
54 if (count($match) == 0) {
55 throw new PhutilArgumentUsageException(
56 pht(
57 "No daemons match '%s'! Use '%s' for a list of available daemons.",
58 $substring,
59 'phd list'));
60 } else if (count($match) > 1) {
61 throw new PhutilArgumentUsageException(
62 pht(
63 "Specify a daemon unambiguously. Multiple daemons match '%s': %s.",
64 $substring,
65 implode(', ', $match)));
68 return head($match);
71 final protected function launchDaemons(
72 array $daemons,
73 $debug,
74 $run_as_current_user = false) {
76 // Convert any shorthand classnames like "taskmaster" into proper class
77 // names.
78 foreach ($daemons as $key => $daemon) {
79 $class = $this->findDaemonClass($daemon['class']);
80 $daemons[$key]['class'] = $class;
83 $console = PhutilConsole::getConsole();
85 if (!$run_as_current_user) {
86 // Check if the script is started as the correct user
87 $phd_user = PhabricatorEnv::getEnvConfig('phd.user');
88 $current_user = posix_getpwuid(posix_geteuid());
89 $current_user = $current_user['name'];
90 if ($phd_user && $phd_user != $current_user) {
91 if ($debug) {
92 throw new PhutilArgumentUsageException(
93 pht(
94 "You are trying to run a daemon as a nonstandard user, ".
95 "and `%s` was not able to `%s` to the correct user. \n".
96 'The daemons are configured to run as "%s", '.
97 'but the current user is "%s". '."\n".
98 'Use `%s` to run as a different user, pass `%s` to ignore this '.
99 'warning, or edit `%s` to change the configuration.',
100 'phd',
101 'sudo',
102 $phd_user,
103 $current_user,
104 'sudo',
105 '--as-current-user',
106 'phd.user'));
107 } else {
108 $this->runDaemonsAsUser = $phd_user;
109 $console->writeOut(pht('Starting daemons as %s', $phd_user)."\n");
114 $this->printLaunchingDaemons($daemons, $debug);
116 $trace = PhutilArgumentParser::isTraceModeEnabled();
118 $flags = array();
119 if ($trace) {
120 $flags[] = '--trace';
123 if ($debug) {
124 $flags[] = '--verbose';
127 $instance = $this->getInstance();
128 if ($instance) {
129 $flags[] = '-l';
130 $flags[] = $instance;
133 $config = array();
135 if (!$debug) {
136 $config['daemonize'] = true;
139 if (!$debug) {
140 $config['log'] = $this->getLogDirectory().'/daemons.log';
143 $config['daemons'] = $daemons;
145 $command = csprintf('./phd-daemon %Ls', $flags);
147 $phabricator_root = dirname(phutil_get_library_root('phabricator'));
148 $daemon_script_dir = $phabricator_root.'/scripts/daemon/';
150 if ($debug) {
151 // Don't terminate when the user sends ^C; it will be sent to the
152 // subprocess which will terminate normally.
153 pcntl_signal(
154 SIGINT,
155 array(__CLASS__, 'ignoreSignal'));
157 echo "\n scripts/daemon/ \$ {$command}\n\n";
159 $tempfile = new TempFile('daemon.config');
160 Filesystem::writeFile($tempfile, json_encode($config));
162 phutil_passthru(
163 '(cd %s && exec %C < %s)',
164 $daemon_script_dir,
165 $command,
166 $tempfile);
167 } else {
168 try {
169 $this->executeDaemonLaunchCommand(
170 $command,
171 $daemon_script_dir,
172 $config,
173 $this->runDaemonsAsUser);
174 } catch (Exception $ex) {
175 throw new PhutilArgumentUsageException(
176 pht(
177 'Daemons are configured to run as user "%s" in configuration '.
178 'option `%s`, but the current user is "%s" and `phd` was unable '.
179 'to switch to the correct user with `sudo`. Command output:'.
180 "\n\n".
181 '%s',
182 $phd_user,
183 'phd.user',
184 $current_user,
185 $ex->getMessage()));
190 private function executeDaemonLaunchCommand(
191 $command,
192 $daemon_script_dir,
193 array $config,
194 $run_as_user = null) {
196 $is_sudo = false;
197 if ($run_as_user) {
198 // If anything else besides sudo should be
199 // supported then insert it here (runuser, su, ...)
200 $command = csprintf(
201 'sudo -En -u %s -- %C',
202 $run_as_user,
203 $command);
204 $is_sudo = true;
206 $future = new ExecFuture('exec %C', $command);
207 // Play games to keep 'ps' looking reasonable.
208 $future->setCWD($daemon_script_dir);
209 $future->write(json_encode($config));
210 list($stdout, $stderr) = $future->resolvex();
212 if ($is_sudo) {
213 // On OSX, `sudo -n` exits 0 when the user does not have permission to
214 // switch accounts without a password. This is not consistent with
215 // sudo on Linux, and seems buggy/broken. Check for this by string
216 // matching the output.
217 if (preg_match('/sudo: a password is required/', $stderr)) {
218 throw new Exception(
219 pht(
220 '%s exited with a zero exit code, but emitted output '.
221 'consistent with failure under OSX.',
222 'sudo'));
227 public static function ignoreSignal($signo) {
228 return;
231 public static function requireExtensions() {
232 self::mustHaveExtension('pcntl');
233 self::mustHaveExtension('posix');
236 private static function mustHaveExtension($ext) {
237 if (!extension_loaded($ext)) {
238 echo pht(
239 "ERROR: The PHP extension '%s' is not installed. You must ".
240 "install it to run daemons on this machine.\n",
241 $ext);
242 exit(1);
245 $extension = new ReflectionExtension($ext);
246 foreach ($extension->getFunctions() as $function) {
247 $function = $function->name;
248 if (!function_exists($function)) {
249 echo pht(
250 "ERROR: The PHP function %s is disabled. You must ".
251 "enable it to run daemons on this machine.\n",
252 $function.'()');
253 exit(1);
259 /* -( Commands )----------------------------------------------------------- */
262 final protected function executeStartCommand(array $options) {
263 PhutilTypeSpec::checkMap(
264 $options,
265 array(
266 'keep-leases' => 'optional bool',
267 'force' => 'optional bool',
268 'reserve' => 'optional float',
271 $console = PhutilConsole::getConsole();
273 if (!idx($options, 'force')) {
274 $process_refs = $this->getOverseerProcessRefs();
275 if ($process_refs) {
276 $this->logWarn(
277 pht('RUNNING DAEMONS'),
278 pht('Daemons are already running:'));
280 fprintf(STDERR, '%s', "\n");
281 foreach ($process_refs as $process_ref) {
282 fprintf(
283 STDERR,
284 '%s',
285 tsprintf(
286 " %s %s\n",
287 $process_ref->getPID(),
288 $process_ref->getCommand()));
290 fprintf(STDERR, '%s', "\n");
292 $this->logFail(
293 pht('RUNNING DAEMONS'),
294 pht(
295 'Use "phd stop" to stop daemons, "phd restart" to restart '.
296 'daemons, or "phd start --force" to ignore running processes.'));
298 exit(1);
302 if (idx($options, 'keep-leases')) {
303 $console->writeErr("%s\n", pht('Not touching active task queue leases.'));
304 } else {
305 $console->writeErr("%s\n", pht('Freeing active task leases...'));
306 $count = $this->freeActiveLeases();
307 $console->writeErr(
308 "%s\n",
309 pht('Freed %s task lease(s).', new PhutilNumber($count)));
312 $daemons = array(
313 array(
314 'class' => 'PhabricatorRepositoryPullLocalDaemon',
315 'label' => 'pull',
317 array(
318 'class' => 'PhabricatorTriggerDaemon',
319 'label' => 'trigger',
321 array(
322 'class' => 'PhabricatorFactDaemon',
323 'label' => 'fact',
325 array(
326 'class' => 'PhabricatorTaskmasterDaemon',
327 'label' => 'task',
328 'pool' => PhabricatorEnv::getEnvConfig('phd.taskmasters'),
329 'reserve' => idx($options, 'reserve', 0),
333 $this->launchDaemons($daemons, $is_debug = false);
335 $console->writeErr("%s\n", pht('Done.'));
336 return 0;
339 final protected function executeStopCommand(array $options) {
340 $grace_period = idx($options, 'graceful', 15);
341 $force = idx($options, 'force');
343 $query = id(new PhutilProcessQuery())
344 ->withIsOverseer(true);
346 $instance = $this->getInstance();
347 if ($instance !== null && !$force) {
348 $query->withInstances(array($instance));
351 try {
352 $process_refs = $query->execute();
353 } catch (Exception $ex) {
354 // See T13321. If this fails for some reason, just continue for now so
355 // that daemon management still works. In the long run, we don't expect
356 // this to fail, but I don't want to break this workflow while we iron
357 // bugs out.
359 // See T12827. Particularly, this is likely to fail on Solaris.
361 phlog($ex);
363 $process_refs = array();
366 if (!$process_refs) {
367 if ($instance !== null && !$force) {
368 $this->logInfo(
369 pht('NO DAEMONS'),
370 pht(
371 'There are no running daemons for the current instance ("%s"). '.
372 'Use "--force" to stop daemons for all instances.',
373 $instance));
374 } else {
375 $this->logInfo(
376 pht('NO DAEMONS'),
377 pht('There are no running daemons.'));
380 return 0;
383 $process_refs = mpull($process_refs, null, 'getPID');
385 $stop_pids = array_keys($process_refs);
386 $live_pids = $this->sendStopSignals($stop_pids, $grace_period);
388 $stop_pids = array_fuse($stop_pids);
389 $live_pids = array_fuse($live_pids);
391 $dead_pids = array_diff_key($stop_pids, $live_pids);
393 foreach ($dead_pids as $dead_pid) {
394 $dead_ref = $process_refs[$dead_pid];
395 $this->logOkay(
396 pht('STOP'),
397 pht(
398 'Stopped PID %d ("%s")',
399 $dead_pid,
400 $dead_ref->getCommand()));
403 foreach ($live_pids as $live_pid) {
404 $live_ref = $process_refs[$live_pid];
405 $this->logFail(
406 pht('SURVIVED'),
407 pht(
408 'Unable to stop PID %d ("%s").',
409 $live_pid,
410 $live_ref->getCommand()));
413 if ($live_pids) {
414 $this->logWarn(
415 pht('SURVIVORS'),
416 pht(
417 'Unable to stop all daemon processes. You may need to run this '.
418 'command as root with "sudo".'));
421 return 0;
424 final protected function executeReloadCommand(array $pids) {
425 $process_refs = $this->getOverseerProcessRefs();
427 if (!$process_refs) {
428 $this->logInfo(
429 pht('NO DAEMONS'),
430 pht('There are no running daemon processes to reload.'));
432 return 0;
435 foreach ($process_refs as $process_ref) {
436 $pid = $process_ref->getPID();
438 $this->logInfo(
439 pht('RELOAD'),
440 pht('Reloading process %d...', $pid));
442 posix_kill($pid, SIGHUP);
445 return 0;
448 private function sendStopSignals($pids, $grace_period) {
449 // If we're doing a graceful shutdown, try SIGINT first.
450 if ($grace_period) {
451 $pids = $this->sendSignal($pids, SIGINT, $grace_period);
454 // If we still have daemons, SIGTERM them.
455 if ($pids) {
456 $pids = $this->sendSignal($pids, SIGTERM, 15);
459 // If the overseer is still alive, SIGKILL it.
460 if ($pids) {
461 $pids = $this->sendSignal($pids, SIGKILL, 0);
464 return $pids;
467 private function sendSignal(array $pids, $signo, $wait) {
468 $console = PhutilConsole::getConsole();
470 $pids = array_fuse($pids);
472 foreach ($pids as $key => $pid) {
473 if (!$pid) {
474 // NOTE: We must have a PID to signal a daemon, since sending a signal
475 // to PID 0 kills this process.
476 unset($pids[$key]);
477 continue;
480 switch ($signo) {
481 case SIGINT:
482 $message = pht('Interrupting process %d...', $pid);
483 break;
484 case SIGTERM:
485 $message = pht('Terminating process %d...', $pid);
486 break;
487 case SIGKILL:
488 $message = pht('Killing process %d...', $pid);
489 break;
492 $console->writeOut("%s\n", $message);
493 posix_kill($pid, $signo);
496 if ($wait) {
497 $start = PhabricatorTime::getNow();
498 do {
499 foreach ($pids as $key => $pid) {
500 if (!PhabricatorDaemonReference::isProcessRunning($pid)) {
501 $console->writeOut(pht('Process %d exited.', $pid)."\n");
502 unset($pids[$key]);
505 if (empty($pids)) {
506 break;
508 usleep(100000);
509 } while (PhabricatorTime::getNow() < $start + $wait);
512 return $pids;
515 private function freeActiveLeases() {
516 $task_table = id(new PhabricatorWorkerActiveTask());
517 $conn_w = $task_table->establishConnection('w');
518 queryfx(
519 $conn_w,
520 'UPDATE %T SET leaseExpires = UNIX_TIMESTAMP()
521 WHERE leaseExpires > UNIX_TIMESTAMP()',
522 $task_table->getTableName());
523 return $conn_w->getAffectedRows();
527 private function printLaunchingDaemons(array $daemons, $debug) {
528 $console = PhutilConsole::getConsole();
530 if ($debug) {
531 $console->writeOut(pht('Launching daemons (in debug mode):'));
532 } else {
533 $console->writeOut(pht('Launching daemons:'));
536 $log_dir = $this->getLogDirectory().'/daemons.log';
537 $console->writeOut(
538 "\n%s\n\n",
539 pht('(Logs will appear in "%s".)', $log_dir));
541 foreach ($daemons as $daemon) {
542 $pool_size = pht('(Pool: %s)', idx($daemon, 'pool', 1));
544 $console->writeOut(
545 " %s %s\n",
546 $pool_size,
547 $daemon['class'],
548 implode(' ', idx($daemon, 'argv', array())));
550 $console->writeOut("\n");
553 protected function getAutoscaleReserveArgument() {
554 return array(
555 'name' => 'autoscale-reserve',
556 'param' => 'ratio',
557 'help' => pht(
558 'Specify a proportion of machine memory which must be free '.
559 'before autoscale pools will grow. For example, a value of 0.25 '.
560 'means that pools will not grow unless the machine has at least '.
561 '25%%%% of its RAM free.'),
565 private function selectDaemonPIDs(array $daemons, array $pids) {
566 $console = PhutilConsole::getConsole();
568 $running_pids = array_fuse(mpull($daemons, 'getPID'));
569 if (!$pids) {
570 $select_pids = $running_pids;
571 } else {
572 // We were given a PID or set of PIDs to kill.
573 $select_pids = array();
574 foreach ($pids as $key => $pid) {
575 if (!preg_match('/^\d+$/', $pid)) {
576 $console->writeErr(pht("PID '%s' is not a valid PID.", $pid)."\n");
577 continue;
578 } else if (empty($running_pids[$pid])) {
579 $console->writeErr(
580 "%s\n",
581 pht(
582 'PID "%d" is not a known daemon PID.',
583 $pid));
584 continue;
585 } else {
586 $select_pids[$pid] = $pid;
591 return $select_pids;
594 protected function getOverseerProcessRefs() {
595 $query = id(new PhutilProcessQuery())
596 ->withIsOverseer(true);
598 $instance = PhabricatorEnv::getEnvConfig('cluster.instance');
599 if ($instance !== null) {
600 $query->withInstances(array($instance));
603 return $query->execute();
606 protected function getInstance() {
607 return PhabricatorEnv::getEnvConfig('cluster.instance');