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);
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 ".
32 'phd.log-directory'));
38 private function findDaemonClass($substring) {
39 $symbols = $this->loadAvailableDaemonClasses();
41 $symbols = ipull($symbols, 'name');
43 foreach ($symbols as $symbol) {
44 if (stripos($symbol, $substring) !== false) {
45 if (strtolower($symbol) == strtolower($substring)) {
46 $match = array($symbol);
54 if (count($match) == 0) {
55 throw new PhutilArgumentUsageException(
57 "No daemons match '%s'! Use '%s' for a list of available daemons.",
60 } else if (count($match) > 1) {
61 throw new PhutilArgumentUsageException(
63 "Specify a daemon unambiguously. Multiple daemons match '%s': %s.",
65 implode(', ', $match)));
71 final protected function launchDaemons(
74 $run_as_current_user = false) {
76 // Convert any shorthand classnames like "taskmaster" into proper class
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) {
92 throw new PhutilArgumentUsageException(
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.',
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();
120 $flags[] = '--trace';
124 $flags[] = '--verbose';
127 $instance = $this->getInstance();
130 $flags[] = $instance;
136 $config['daemonize'] = true;
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/';
151 // Don't terminate when the user sends ^C; it will be sent to the
152 // subprocess which will terminate normally.
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));
163 '(cd %s && exec %C < %s)',
169 $this->executeDaemonLaunchCommand(
173 $this->runDaemonsAsUser
);
174 } catch (Exception
$ex) {
175 throw new PhutilArgumentUsageException(
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:'.
190 private function executeDaemonLaunchCommand(
194 $run_as_user = null) {
198 // If anything else besides sudo should be
199 // supported then insert it here (runuser, su, ...)
201 'sudo -En -u %s -- %C',
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();
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)) {
220 '%s exited with a zero exit code, but emitted output '.
221 'consistent with failure under OSX.',
227 public static function ignoreSignal($signo) {
231 public static function requireExtensions() {
232 self
::mustHaveExtension('pcntl');
233 self
::mustHaveExtension('posix');
236 private static function mustHaveExtension($ext) {
237 if (!extension_loaded($ext)) {
239 "ERROR: The PHP extension '%s' is not installed. You must ".
240 "install it to run daemons on this machine.\n",
245 $extension = new ReflectionExtension($ext);
246 foreach ($extension->getFunctions() as $function) {
247 $function = $function->name
;
248 if (!function_exists($function)) {
250 "ERROR: The PHP function %s is disabled. You must ".
251 "enable it to run daemons on this machine.\n",
259 /* -( Commands )----------------------------------------------------------- */
262 final protected function executeStartCommand(array $options) {
263 PhutilTypeSpec
::checkMap(
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();
277 pht('RUNNING DAEMONS'),
278 pht('Daemons are already running:'));
280 fprintf(STDERR
, '%s', "\n");
281 foreach ($process_refs as $process_ref) {
287 $process_ref->getPID(),
288 $process_ref->getCommand()));
290 fprintf(STDERR
, '%s', "\n");
293 pht('RUNNING DAEMONS'),
295 'Use "phd stop" to stop daemons, "phd restart" to restart '.
296 'daemons, or "phd start --force" to ignore running processes.'));
302 if (idx($options, 'keep-leases')) {
303 $console->writeErr("%s\n", pht('Not touching active task queue leases.'));
305 $console->writeErr("%s\n", pht('Freeing active task leases...'));
306 $count = $this->freeActiveLeases();
309 pht('Freed %s task lease(s).', new PhutilNumber($count)));
314 'class' => 'PhabricatorRepositoryPullLocalDaemon',
318 'class' => 'PhabricatorTriggerDaemon',
319 'label' => 'trigger',
322 'class' => 'PhabricatorFactDaemon',
326 'class' => 'PhabricatorTaskmasterDaemon',
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.'));
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));
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
359 // See T12827. Particularly, this is likely to fail on Solaris.
363 $process_refs = array();
366 if (!$process_refs) {
367 if ($instance !== null && !$force) {
371 'There are no running daemons for the current instance ("%s"). '.
372 'Use "--force" to stop daemons for all instances.',
377 pht('There are no running daemons.'));
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];
398 'Stopped PID %d ("%s")',
400 $dead_ref->getCommand()));
403 foreach ($live_pids as $live_pid) {
404 $live_ref = $process_refs[$live_pid];
408 'Unable to stop PID %d ("%s").',
410 $live_ref->getCommand()));
417 'Unable to stop all daemon processes. You may need to run this '.
418 'command as root with "sudo".'));
424 final protected function executeReloadCommand(array $pids) {
425 $process_refs = $this->getOverseerProcessRefs();
427 if (!$process_refs) {
430 pht('There are no running daemon processes to reload.'));
435 foreach ($process_refs as $process_ref) {
436 $pid = $process_ref->getPID();
440 pht('Reloading process %d...', $pid));
442 posix_kill($pid, SIGHUP
);
448 private function sendStopSignals($pids, $grace_period) {
449 // If we're doing a graceful shutdown, try SIGINT first.
451 $pids = $this->sendSignal($pids, SIGINT
, $grace_period);
454 // If we still have daemons, SIGTERM them.
456 $pids = $this->sendSignal($pids, SIGTERM
, 15);
459 // If the overseer is still alive, SIGKILL it.
461 $pids = $this->sendSignal($pids, SIGKILL
, 0);
467 private function sendSignal(array $pids, $signo, $wait) {
468 $console = PhutilConsole
::getConsole();
470 $pids = array_fuse($pids);
472 foreach ($pids as $key => $pid) {
474 // NOTE: We must have a PID to signal a daemon, since sending a signal
475 // to PID 0 kills this process.
482 $message = pht('Interrupting process %d...', $pid);
485 $message = pht('Terminating process %d...', $pid);
488 $message = pht('Killing process %d...', $pid);
492 $console->writeOut("%s\n", $message);
493 posix_kill($pid, $signo);
497 $start = PhabricatorTime
::getNow();
499 foreach ($pids as $key => $pid) {
500 if (!PhabricatorDaemonReference
::isProcessRunning($pid)) {
501 $console->writeOut(pht('Process %d exited.', $pid)."\n");
509 } while (PhabricatorTime
::getNow() < $start +
$wait);
515 private function freeActiveLeases() {
516 $task_table = id(new PhabricatorWorkerActiveTask());
517 $conn_w = $task_table->establishConnection('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();
531 $console->writeOut(pht('Launching daemons (in debug mode):'));
533 $console->writeOut(pht('Launching daemons:'));
536 $log_dir = $this->getLogDirectory().'/daemons.log';
539 pht('(Logs will appear in "%s".)', $log_dir));
541 foreach ($daemons as $daemon) {
542 $pool_size = pht('(Pool: %s)', idx($daemon, 'pool', 1));
548 implode(' ', idx($daemon, 'argv', array())));
550 $console->writeOut("\n");
553 protected function getAutoscaleReserveArgument() {
555 'name' => 'autoscale-reserve',
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'));
570 $select_pids = $running_pids;
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");
578 } else if (empty($running_pids[$pid])) {
582 'PID "%d" is not a known daemon PID.',
586 $select_pids[$pid] = $pid;
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');