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