diff --git a/resources/sql/autopatches/20141223.daemonloguser.sql b/resources/sql/autopatches/20141223.daemonloguser.sql new file mode 100644 index 0000000000..ee1f14ad0a --- /dev/null +++ b/resources/sql/autopatches/20141223.daemonloguser.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_daemon.daemon_log + ADD runningAsUser VARCHAR(255) COLLATE {$COLLATE_TEXT}; diff --git a/src/applications/config/check/PhabricatorSetupCheckDaemons.php b/src/applications/config/check/PhabricatorSetupCheckDaemons.php index 71dfc4598f..b7443fbd1d 100644 --- a/src/applications/config/check/PhabricatorSetupCheckDaemons.php +++ b/src/applications/config/check/PhabricatorSetupCheckDaemons.php @@ -1,129 +1,164 @@ setViewer(PhabricatorUser::getOmnipotentUser()) ->withStatus(PhabricatorDaemonLogQuery::STATUS_ALIVE) ->withDaemonClasses(array('PhabricatorTaskmasterDaemon')) ->setLimit(1) ->execute(); if (!$task_daemon) { $doc_href = PhabricatorEnv::getDocLink( 'Managing Daemons with phd'); $summary = pht( 'You must start the Phabricator daemons to send email, rebuild '. 'search indexes, and do other background processing.'); $message = pht( 'The Phabricator daemons are not running, so Phabricator will not '. 'be able to perform background processing (including sending email, '. 'rebuilding search indexes, importing commits, cleaning up old data, '. 'and running builds).'. "\n\n". 'Use %s to start daemons. See %s for more information.', phutil_tag('tt', array(), 'bin/phd start'), phutil_tag( 'a', array( 'href' => $doc_href, 'target' => '_blank', ), pht('Managing Daemons with phd'))); $this->newIssue('daemons.not-running') ->setShortName(pht('Daemons Not Running')) ->setName(pht('Phabricator Daemons Are Not Running')) ->setSummary($summary) ->setMessage($message) ->addCommand('phabricator/ $ ./bin/phd start'); } + $phd_user = PhabricatorEnv::getEnvConfig('phd.user'); $environment_hash = PhabricatorEnv::calculateEnvironmentHash(); $all_daemons = id(new PhabricatorDaemonLogQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withStatus(PhabricatorDaemonLogQuery::STATUS_ALIVE) ->execute(); foreach ($all_daemons as $daemon) { + + if ($phd_user) { + if ($daemon->getRunningAsUser() != $phd_user) { + $doc_href = PhabricatorEnv::getDocLink( + 'Managing Daemons with phd'); + + $summary = pht( + 'At least one daemon is currently running as a different '. + 'user than configured in the Phabricator phd.user setting'); + + $message = pht( + 'A daemon is running as user %s while the Phabricator config '. + 'specifies phd.user to be %s.'. + "\n\n". + 'Either adjust phd.user to match %s or start '. + 'the daemons as the correct user. '. + "\n\n". + 'phd Daemons will try to '. + 'use sudo to start as the configured user. '. + 'Make sure that the user who starts phd has the correct '. + 'sudo permissions to start phd daemons as %s', + phutil_tag('tt', array(), $daemon->getRunningAsUser()), + phutil_tag('tt', array(), $phd_user), + phutil_tag('tt', array(), $daemon->getRunningAsUser()), + phutil_tag('tt', array(), $phd_user)); + + $this->newIssue('daemons.run-as-different-user') + ->setName(pht('Daemons are running as the wrong user')) + ->setSummary($summary) + ->setMessage($message) + ->addCommand('phabricator/ $ ./bin/phd restart'); + } + } + if ($daemon->getEnvHash() != $environment_hash) { $doc_href = PhabricatorEnv::getDocLink( 'Managing Daemons with phd'); $summary = pht( 'At least one daemon is currently running with different '. 'configuration than the Phabricator web application.'); $message = pht( 'At least one daemon is currently running with a different '. 'configuration (config checksum %s) than the web application '. '(config checksum %s).'. "\n\n". 'This usually means that you have just made a configuration change '. 'from the web UI, but have not yet restarted the daemons. You '. 'need to restart the daemons after making configuration changes '. 'so they will pick up the new values: until you do, they will '. 'continue operating with the old settings.'. "\n\n". '(If you plan to make more changes, you can restart the daemons '. 'once after you finish making all of your changes.)'. "\n\n". 'Use %s to restart daemons. You can find a list of running daemons '. 'in the %s, which will also help you identify which daemon (or '. 'daemons) have divergent configuration. For more information about '. 'managing the daemons, see %s in the documentation.'. "\n\n". 'This can also happen if you use the %s environmental variable to '. 'choose a configuration file, but the daemons run with a different '. 'value than the web application. If restarting the daemons does '. 'not resolve this issue and you use %s to select configuration, '. 'check that it is set consistently.'. "\n\n". 'A third possible cause is that you run several machines, and '. 'the %s configuration file differs between them. This file is '. 'updated when you edit configuration from the CLI with %s. If '. 'restarting the daemons does not resolve this issue and you '. 'run multiple machines, check that all machines have identical '. '%s configuration files.'. "\n\n". 'This issue is not severe, but usually indicates that something '. 'is not configured the way you expect, and may cause the daemons '. 'to exhibit different behavior than the web application does.', phutil_tag('tt', array(), substr($daemon->getEnvHash(), 0, 12)), phutil_tag('tt', array(), substr($environment_hash, 0, 12)), phutil_tag('tt', array(), 'bin/phd restart'), phutil_tag( 'a', array( 'href' => '/daemon/', 'target' => '_blank', ), pht('Daemon Console')), phutil_tag( 'a', array( 'href' => $doc_href, 'target' => '_blank', ), pht('Managing Daemons with phd')), phutil_tag('tt', array(), 'PHABRICATOR_ENV'), phutil_tag('tt', array(), 'PHABRICATOR_ENV'), phutil_tag('tt', array(), 'phabricator/conf/local/local.json'), phutil_tag('tt', array(), 'bin/config'), phutil_tag('tt', array(), 'phabricator/conf/local/local.json')); $this->newIssue('daemons.need-restarting') ->setName(pht('Daemons and Web Have Different Config')) ->setSummary($summary) ->setMessage($message) ->addCommand('phabricator/ $ ./bin/phd restart'); break; } } } } diff --git a/src/applications/daemon/controller/PhabricatorDaemonLogViewController.php b/src/applications/daemon/controller/PhabricatorDaemonLogViewController.php index 17ad33e1bc..fa473bed8b 100644 --- a/src/applications/daemon/controller/PhabricatorDaemonLogViewController.php +++ b/src/applications/daemon/controller/PhabricatorDaemonLogViewController.php @@ -1,199 +1,200 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $log = id(new PhabricatorDaemonLogQuery()) ->setViewer($user) ->withIDs(array($this->id)) ->setAllowStatusWrites(true) ->executeOne(); if (!$log) { return new Aphront404Response(); } $events = id(new PhabricatorDaemonLogEvent())->loadAllWhere( 'logID = %d ORDER BY id DESC LIMIT 1000', $log->getID()); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Daemon %s', $log->getID())); $header = id(new PHUIHeaderView()) ->setHeader($log->getDaemon()); $tag = id(new PHUITagView()) ->setType(PHUITagView::TYPE_STATE); $status = $log->getStatus(); switch ($status) { case PhabricatorDaemonLog::STATUS_UNKNOWN: $tag->setBackgroundColor(PHUITagView::COLOR_ORANGE); $tag->setName(pht('Unknown')); break; case PhabricatorDaemonLog::STATUS_RUNNING: $tag->setBackgroundColor(PHUITagView::COLOR_GREEN); $tag->setName(pht('Running')); break; case PhabricatorDaemonLog::STATUS_DEAD: $tag->setBackgroundColor(PHUITagView::COLOR_RED); $tag->setName(pht('Dead')); break; case PhabricatorDaemonLog::STATUS_WAIT: $tag->setBackgroundColor(PHUITagView::COLOR_BLUE); $tag->setName(pht('Waiting')); break; case PhabricatorDaemonLog::STATUS_EXITING: $tag->setBackgroundColor(PHUITagView::COLOR_YELLOW); $tag->setName(pht('Exiting')); break; case PhabricatorDaemonLog::STATUS_EXITED: $tag->setBackgroundColor(PHUITagView::COLOR_GREY); $tag->setName(pht('Exited')); break; } $header->addTag($tag); $env_hash = PhabricatorEnv::calculateEnvironmentHash(); if ($log->getEnvHash() != $env_hash) { $tag = id(new PHUITagView()) ->setType(PHUITagView::TYPE_STATE) ->setBackgroundColor(PHUITagView::COLOR_YELLOW) ->setName(pht('Stale Config')); $header->addTag($tag); } $properties = $this->buildPropertyListView($log); $event_view = id(new PhabricatorDaemonLogEventsView()) ->setUser($user) ->setEvents($events); $event_panel = new AphrontPanelView(); $event_panel->setNoBackground(); $event_panel->appendChild($event_view); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); return $this->buildApplicationPage( array( $crumbs, $object_box, $event_panel, ), array( 'title' => pht('Daemon Log'), 'device' => false, )); } private function buildPropertyListView(PhabricatorDaemonLog $daemon) { $request = $this->getRequest(); $viewer = $request->getUser(); $view = id(new PHUIPropertyListView()) ->setUser($viewer); $id = $daemon->getID(); $c_epoch = $daemon->getDateCreated(); $u_epoch = $daemon->getDateModified(); $unknown_time = PhabricatorDaemonLogQuery::getTimeUntilUnknown(); $dead_time = PhabricatorDaemonLogQuery::getTimeUntilDead(); $wait_time = PhutilDaemonOverseer::RESTART_WAIT; $details = null; $status = $daemon->getStatus(); switch ($status) { case PhabricatorDaemonLog::STATUS_RUNNING: $details = pht( 'This daemon is running normally and reported a status update '. 'recently (within %s).', phutil_format_relative_time($unknown_time)); break; case PhabricatorDaemonLog::STATUS_UNKNOWN: $details = pht( 'This daemon has not reported a status update recently (within %s). '. 'It may have exited abruptly. After %s, it will be presumed dead.', phutil_format_relative_time($unknown_time), phutil_format_relative_time($dead_time)); break; case PhabricatorDaemonLog::STATUS_DEAD: $details = pht( 'This daemon did not report a status update for %s. It is '. 'presumed dead. Usually, this indicates that the daemon was '. 'killed or otherwise exited abruptly with an error. You may '. 'need to restart it.', phutil_format_relative_time($dead_time)); break; case PhabricatorDaemonLog::STATUS_WAIT: $details = pht( 'This daemon is running normally and reported a status update '. 'recently (within %s). However, it encountered an error while '. 'doing work and is waiting a little while (%s) to resume '. 'processing. After encountering an error, daemons wait before '. 'resuming work to avoid overloading services.', phutil_format_relative_time($unknown_time), phutil_format_relative_time($wait_time)); break; case PhabricatorDaemonLog::STATUS_EXITING: $details = pht( 'This daemon is shutting down gracefully.'); break; case PhabricatorDaemonLog::STATUS_EXITED: $details = pht( 'This daemon exited normally and is no longer running.'); break; } $view->addProperty(pht('Status Details'), $details); $view->addProperty(pht('Daemon Class'), $daemon->getDaemon()); $view->addProperty(pht('Host'), $daemon->getHost()); $view->addProperty(pht('PID'), $daemon->getPID()); + $view->addProperty(pht('Running as'), $daemon->getRunningAsUser()); $view->addProperty(pht('Started'), phabricator_datetime($c_epoch, $viewer)); $view->addProperty( pht('Seen'), pht( '%s ago (%s)', phutil_format_relative_time(time() - $u_epoch), phabricator_datetime($u_epoch, $viewer))); $argv = $daemon->getArgv(); if (is_array($argv)) { $argv = implode("\n", $argv); } $view->addProperty( pht('Argv'), phutil_tag( 'textarea', array( 'style' => 'width: 100%; height: 12em;', ), $argv)); $view->addProperty( pht('View Full Logs'), phutil_tag( 'tt', array(), "phabricator/ $ ./bin/phd log --id {$id}")); return $view; } } diff --git a/src/applications/daemon/event/PhabricatorDaemonEventListener.php b/src/applications/daemon/event/PhabricatorDaemonEventListener.php index a17b14338f..2ed1540790 100644 --- a/src/applications/daemon/event/PhabricatorDaemonEventListener.php +++ b/src/applications/daemon/event/PhabricatorDaemonEventListener.php @@ -1,117 +1,119 @@ listen(PhutilDaemonOverseer::EVENT_DID_LAUNCH); $this->listen(PhutilDaemonOverseer::EVENT_DID_LOG); $this->listen(PhutilDaemonOverseer::EVENT_DID_HEARTBEAT); $this->listen(PhutilDaemonOverseer::EVENT_WILL_GRACEFUL); $this->listen(PhutilDaemonOverseer::EVENT_WILL_EXIT); } public function handleEvent(PhutilEvent $event) { switch ($event->getType()) { case PhutilDaemonOverseer::EVENT_DID_LAUNCH: $this->handleLaunchEvent($event); break; case PhutilDaemonOverseer::EVENT_DID_HEARTBEAT: $this->handleHeartbeatEvent($event); break; case PhutilDaemonOverseer::EVENT_DID_LOG: $this->handleLogEvent($event); break; case PhutilDaemonOverseer::EVENT_WILL_GRACEFUL: $this->handleGracefulEvent($event); break; case PhutilDaemonOverseer::EVENT_WILL_EXIT: $this->handleExitEvent($event); break; } } private function handleLaunchEvent(PhutilEvent $event) { $id = $event->getValue('id'); + $current_user = posix_getpwuid(posix_geteuid()); $daemon = id(new PhabricatorDaemonLog()) ->setDaemon($event->getValue('daemonClass')) ->setHost(php_uname('n')) ->setPID(getmypid()) + ->setRunningAsUser($current_user['name']) ->setEnvHash(PhabricatorEnv::calculateEnvironmentHash()) ->setStatus(PhabricatorDaemonLog::STATUS_RUNNING) ->setArgv($event->getValue('argv')) ->setExplicitArgv($event->getValue('explicitArgv')) ->save(); $this->daemons[$id] = $daemon; } private function handleHeartbeatEvent(PhutilEvent $event) { $daemon = $this->getDaemon($event->getValue('id')); // Just update the timestamp. $daemon->save(); } private function handleLogEvent(PhutilEvent $event) { $daemon = $this->getDaemon($event->getValue('id')); // TODO: This is a bit awkward for historical reasons, clean it up after // removing Conduit. $message = $event->getValue('message'); $context = $event->getValue('context'); if (strlen($context) && $context !== $message) { $message = "({$context}) {$message}"; } $type = $event->getValue('type'); $message = phutil_utf8ize($message); id(new PhabricatorDaemonLogEvent()) ->setLogID($daemon->getID()) ->setLogType($type) ->setMessage((string)$message) ->setEpoch(time()) ->save(); switch ($type) { case 'WAIT': $current_status = PhabricatorDaemonLog::STATUS_WAIT; break; default: $current_status = PhabricatorDaemonLog::STATUS_RUNNING; break; } if ($current_status !== $daemon->getStatus()) { $daemon->setStatus($current_status)->save(); } } private function handleGracefulEvent(PhutilEvent $event) { $id = $event->getValue('id'); $daemon = $this->getDaemon($id); $daemon->setStatus(PhabricatorDaemonLog::STATUS_EXITING)->save(); } private function handleExitEvent(PhutilEvent $event) { $id = $event->getValue('id'); $daemon = $this->getDaemon($id); $daemon->setStatus(PhabricatorDaemonLog::STATUS_EXITED)->save(); unset($this->daemons[$id]); } private function getDaemon($id) { if (isset($this->daemons[$id])) { return $this->daemons[$id]; } throw new Exception("No such daemon '{$id}'!"); } } diff --git a/src/applications/daemon/management/PhabricatorDaemonManagementDebugWorkflow.php b/src/applications/daemon/management/PhabricatorDaemonManagementDebugWorkflow.php index 0c72ad94af..ffc3b10938 100644 --- a/src/applications/daemon/management/PhabricatorDaemonManagementDebugWorkflow.php +++ b/src/applications/daemon/management/PhabricatorDaemonManagementDebugWorkflow.php @@ -1,39 +1,49 @@ setName('debug') ->setExamples('**debug** __daemon__') ->setSynopsis( pht( 'Start __daemon__ in the foreground and print large volumes of '. 'diagnostic information to the console.')) ->setArguments( array( array( 'name' => 'argv', 'wildcard' => true, ), + array( + 'name' => 'as-current-user', + 'help' => 'Run the daemon as the current user '. + 'instead of the configured phd.user', + ), )); } public function execute(PhutilArgumentParser $args) { $argv = $args->getArg('argv'); + $run_as_current_user = $args->getArg('as-current-user'); if (!$argv) { throw new PhutilArgumentUsageException( pht('You must specify which daemon to debug.')); } $daemon_class = array_shift($argv); - return $this->launchDaemon($daemon_class, $argv, $is_debug = true); + return $this->launchDaemon( + $daemon_class, + $argv, + $is_debug = true, + $run_as_current_user); } } diff --git a/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php b/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php index 0d33d776ed..ce688cfafe 100644 --- a/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php +++ b/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php @@ -1,479 +1,536 @@ setAncestorClass('PhutilDaemon') ->setConcreteOnly(true) ->selectSymbolsWithoutLoading(); } protected final function getPIDDirectory() { $path = PhabricatorEnv::getEnvConfig('phd.pid-directory'); return $this->getControlDirectory($path); } protected final function getLogDirectory() { $path = PhabricatorEnv::getEnvConfig('phd.log-directory'); return $this->getControlDirectory($path); } private function getControlDirectory($path) { if (!Filesystem::pathExists($path)) { list($err) = exec_manual('mkdir -p %s', $path); if ($err) { throw new Exception( "phd requires the directory '{$path}' to exist, but it does not ". "exist and could not be created. Create this directory or update ". "'phd.pid-directory' / 'phd.log-directory' in your configuration ". "to point to an existing directory."); } } return $path; } protected final function loadRunningDaemons() { $daemons = array(); $pid_dir = $this->getPIDDirectory(); $pid_files = Filesystem::listDirectory($pid_dir); foreach ($pid_files as $pid_file) { $daemons[] = PhabricatorDaemonReference::newFromFile( $pid_dir.'/'.$pid_file); } return $daemons; } protected final function loadAllRunningDaemons() { $local_daemons = $this->loadRunningDaemons(); $local_ids = array(); foreach ($local_daemons as $daemon) { $daemon_log = $daemon->getDaemonLog(); if ($daemon_log) { $local_ids[] = $daemon_log->getID(); } } $remote_daemons = id(new PhabricatorDaemonLogQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withoutIDs($local_ids) ->withStatus(PhabricatorDaemonLogQuery::STATUS_ALIVE) ->execute(); return array_merge($local_daemons, $remote_daemons); } private function findDaemonClass($substring) { $symbols = $this->loadAvailableDaemonClasses(); $symbols = ipull($symbols, 'name'); $match = array(); foreach ($symbols as $symbol) { if (stripos($symbol, $substring) !== false) { if (strtolower($symbol) == strtolower($substring)) { $match = array($symbol); break; } else { $match[] = $symbol; } } } if (count($match) == 0) { throw new PhutilArgumentUsageException( pht( "No daemons match '%s'! Use 'phd list' for a list of available ". "daemons.", $substring)); } else if (count($match) > 1) { throw new PhutilArgumentUsageException( pht( "Specify a daemon unambiguously. Multiple daemons match '%s': %s.", $substring, implode(', ', $match))); } return head($match); } - protected final function launchDaemon($class, array $argv, $debug) { + protected final function launchDaemon( + $class, + array $argv, + $debug, + $run_as_current_user = false) { + $daemon = $this->findDaemonClass($class); $console = PhutilConsole::getConsole(); + if (!$run_as_current_user) { + // Check if the script is started as the correct user + $phd_user = PhabricatorEnv::getEnvConfig('phd.user'); + $current_user = posix_getpwuid(posix_geteuid()); + $current_user = $current_user['name']; + if ($phd_user && $phd_user != $current_user) { + if ($debug) { + throw new PhutilArgumentUsageException(pht( + 'You are trying to run a daemon as a nonstandard user, '. + 'and `phd` was not able to `sudo` to the correct user. '."\n". + 'Phabricator is configured to run daemons as "%s", '. + 'but the current user is "%s". '."\n". + 'Use `sudo` to run as a different user, pass `--as-current-user` '. + 'to ignore this warning, or edit `phd.user` '. + 'to change the configuration.', $phd_user, $current_user)); + } else { + $this->runDaemonsAsUser = $phd_user; + $console->writeOut(pht('Starting daemons as %s', $phd_user)."\n"); + } + } + } + if ($debug) { if ($argv) { $console->writeOut( pht( "Launching daemon \"%s\" in debug mode (not daemonized) ". "with arguments %s.\n", $daemon, csprintf('%LR', $argv))); } else { $console->writeOut( pht( "Launching daemon \"%s\" in debug mode (not daemonized).\n", $daemon)); } } else { if ($argv) { $console->writeOut( pht( "Launching daemon \"%s\" with arguments %s.\n", $daemon, csprintf('%LR', $argv))); } else { $console->writeOut( pht( "Launching daemon \"%s\".\n", $daemon)); } } foreach ($argv as $key => $arg) { $argv[$key] = escapeshellarg($arg); } $flags = array(); if ($debug || PhabricatorEnv::getEnvConfig('phd.trace')) { $flags[] = '--trace'; } if ($debug || PhabricatorEnv::getEnvConfig('phd.verbose')) { $flags[] = '--verbose'; } if (!$debug) { $flags[] = '--daemonize'; } if (!$debug) { $log_file = $this->getLogDirectory().'/daemons.log'; $flags[] = csprintf('--log=%s', $log_file); } $pid_dir = $this->getPIDDirectory(); // TODO: This should be a much better user experience. Filesystem::assertExists($pid_dir); Filesystem::assertIsDirectory($pid_dir); Filesystem::assertWritable($pid_dir); $flags[] = csprintf('--phd=%s', $pid_dir); $command = csprintf( './phd-daemon %s %C %C', $daemon, implode(' ', $flags), implode(' ', $argv)); $phabricator_root = dirname(phutil_get_library_root('phabricator')); $daemon_script_dir = $phabricator_root.'/scripts/daemon/'; if ($debug) { // Don't terminate when the user sends ^C; it will be sent to the // subprocess which will terminate normally. pcntl_signal( SIGINT, array(__CLASS__, 'ignoreSignal')); echo "\n phabricator/scripts/daemon/ \$ {$command}\n\n"; phutil_passthru('(cd %s && exec %C)', $daemon_script_dir, $command); } else { - $future = new ExecFuture('exec %C', $command); - // Play games to keep 'ps' looking reasonable. - $future->setCWD($daemon_script_dir); - $future->resolvex(); + try { + $this->executeDaemonLaunchCommand( + $command, + $daemon_script_dir, + $this->runDaemonsAsUser); + } catch (CommandException $e) { + // Retry without sudo + $console->writeOut(pht( + "sudo command failed. Starting daemon as current user\n")); + $this->executeDaemonLaunchCommand( + $command, + $daemon_script_dir); + } + } + } + + private function executeDaemonLaunchCommand( + $command, + $daemon_script_dir, + $run_as_user = null) { + + if ($run_as_user) { + // If anything else besides sudo should be + // supported then insert it here (runuser, su, ...) + $command = csprintf( + 'sudo -En -u %s -- %C', + $run_as_user, + $command); } + $future = new ExecFuture('exec %C', $command); + // Play games to keep 'ps' looking reasonable. + $future->setCWD($daemon_script_dir); + $future->resolvex(); } public static function ignoreSignal($signo) { return; } public static function requireExtensions() { self::mustHaveExtension('pcntl'); self::mustHaveExtension('posix'); } private static function mustHaveExtension($ext) { if (!extension_loaded($ext)) { echo "ERROR: The PHP extension '{$ext}' is not installed. You must ". "install it to run daemons on this machine.\n"; exit(1); } $extension = new ReflectionExtension($ext); foreach ($extension->getFunctions() as $function) { $function = $function->name; if (!function_exists($function)) { echo "ERROR: The PHP function {$function}() is disabled. You must ". "enable it to run daemons on this machine.\n"; exit(1); } } } protected final function willLaunchDaemons() { $console = PhutilConsole::getConsole(); $console->writeErr(pht('Preparing to launch daemons.')."\n"); $log_dir = $this->getLogDirectory().'/daemons.log'; $console->writeErr(pht("NOTE: Logs will appear in '%s'.", $log_dir)."\n\n"); } /* -( Commands )----------------------------------------------------------- */ protected final function executeStartCommand($keep_leases = false) { $console = PhutilConsole::getConsole(); $running = $this->loadRunningDaemons(); // This may include daemons which were launched but which are no longer // running; check that we actually have active daemons before failing. foreach ($running as $daemon) { if ($daemon->isRunning()) { $message = pht( "phd start: Unable to start daemons because daemons are already ". "running.\n". "You can view running daemons with 'phd status'.\n". "You can stop running daemons with 'phd stop'.\n". "You can use 'phd restart' to stop all daemons before starting new ". "daemons."); $console->writeErr("%s\n", $message); exit(1); } } if ($keep_leases) { $console->writeErr("%s\n", pht('Not touching active task queue leases.')); } else { $console->writeErr("%s\n", pht('Freeing active task leases...')); $count = $this->freeActiveLeases(); $console->writeErr( "%s\n", pht('Freed %s task lease(s).', new PhutilNumber($count))); } $daemons = array( array('PhabricatorRepositoryPullLocalDaemon', array()), array('PhabricatorGarbageCollectorDaemon', array()), ); $taskmasters = PhabricatorEnv::getEnvConfig('phd.start-taskmasters'); for ($ii = 0; $ii < $taskmasters; $ii++) { $daemons[] = array('PhabricatorTaskmasterDaemon', array()); } $this->willLaunchDaemons(); foreach ($daemons as $spec) { list($name, $argv) = $spec; $this->launchDaemon($name, $argv, $is_debug = false); } $console->writeErr(pht('Done.')."\n"); return 0; } protected final function executeStopCommand( array $pids, $grace_period, $force) { $console = PhutilConsole::getConsole(); $daemons = $this->loadRunningDaemons(); if (!$daemons) { $survivors = array(); if (!$pids) { $survivors = $this->processRogueDaemons( $grace_period, $warn = true, $force); } if (!$survivors) { $console->writeErr(pht( 'There are no running Phabricator daemons.')."\n"); } return 0; } $daemons = mpull($daemons, null, 'getPID'); $running = array(); if (!$pids) { $running = $daemons; } else { // We were given a PID or set of PIDs to kill. foreach ($pids as $key => $pid) { if (!preg_match('/^\d+$/', $pid)) { $console->writeErr(pht("PID '%s' is not a valid PID.", $pid)."\n"); continue; } else if (empty($daemons[$pid])) { $console->writeErr( pht( "PID '%s' is not a Phabricator daemon PID. It will not ". "be killed.", $pid)."\n"); continue; } else { $running[] = $daemons[$pid]; } } } if (empty($running)) { $console->writeErr(pht('No daemons to kill.')."\n"); return 0; } $all_daemons = $running; // don't specify force here as that's about rogue daemons $this->sendStopSignals($running, $grace_period); foreach ($all_daemons as $daemon) { if ($daemon->getPIDFile()) { Filesystem::remove($daemon->getPIDFile()); } } $this->processRogueDaemons($grace_period, !$pids, $force); return 0; } private function processRogueDaemons($grace_period, $warn, $force_stop) { $console = PhutilConsole::getConsole(); $rogue_daemons = PhutilDaemonOverseer::findRunningDaemons(); if ($rogue_daemons) { if ($force_stop) { $stop_rogue_daemons = $this->buildRogueDaemons($rogue_daemons); $survivors = $this->sendStopSignals( $stop_rogue_daemons, $grace_period, $force_stop); if ($survivors) { $console->writeErr(pht( 'Unable to stop processes running without pid files. Try running '. 'this command again with sudo.'."\n")); } } else if ($warn) { $console->writeErr($this->getForceStopHint($rogue_daemons)."\n"); } } return $rogue_daemons; } private function getForceStopHint($rogue_daemons) { $debug_output = ''; foreach ($rogue_daemons as $rogue) { $debug_output .= $rogue['pid'].' '.$rogue['command']."\n"; } return pht( 'There are processes running that look like Phabricator daemons but '. 'have no corresponding PID files:'."\n\n".'%s'."\n\n". 'Stop these processes by re-running this command with the --force '. 'parameter.', $debug_output); } private function buildRogueDaemons(array $daemons) { $rogue_daemons = array(); foreach ($daemons as $pid => $data) { $rogue_daemons[] = PhabricatorDaemonReference::newFromRogueDictionary($data); } return $rogue_daemons; } private function sendStopSignals($daemons, $grace_period, $force = false) { // If we're doing a graceful shutdown, try SIGINT first. if ($grace_period) { $daemons = $this->sendSignal($daemons, SIGINT, $grace_period, $force); } // If we still have daemons, SIGTERM them. if ($daemons) { $daemons = $this->sendSignal($daemons, SIGTERM, 15, $force); } // If the overseer is still alive, SIGKILL it. if ($daemons) { $daemons = $this->sendSignal($daemons, SIGKILL, 0, $force); } return $daemons; } private function sendSignal(array $daemons, $signo, $wait, $force = false) { $console = PhutilConsole::getConsole(); foreach ($daemons as $key => $daemon) { $pid = $daemon->getPID(); $name = $daemon->getName(); if (!$pid && !$force) { $console->writeOut("%s\n", pht("Daemon '%s' has no PID!", $name)); unset($daemons[$key]); continue; } switch ($signo) { case SIGINT: $message = pht("Interrupting daemon '%s' (%s)...", $name, $pid); break; case SIGTERM: $message = pht("Terminating daemon '%s' (%s)...", $name, $pid); break; case SIGKILL: $message = pht("Killing daemon '%s' (%s)...", $name, $pid); break; } $console->writeOut("%s\n", $message); posix_kill($pid, $signo); } if ($wait) { $start = PhabricatorTime::getNow(); do { foreach ($daemons as $key => $daemon) { $pid = $daemon->getPID(); if (!$daemon->isRunning()) { $console->writeOut(pht('Daemon %s exited.', $pid)."\n"); unset($daemons[$key]); } } if (empty($daemons)) { break; } usleep(100000); } while (PhabricatorTime::getNow() < $start + $wait); } return $daemons; } private function freeActiveLeases() { $task_table = id(new PhabricatorWorkerActiveTask()); $conn_w = $task_table->establishConnection('w'); queryfx( $conn_w, 'UPDATE %T SET leaseExpires = UNIX_TIMESTAMP() WHERE leaseExpires > UNIX_TIMESTAMP()', $task_table->getTableName()); return $conn_w->getAffectedRows(); } } diff --git a/src/applications/daemon/storage/PhabricatorDaemonLog.php b/src/applications/daemon/storage/PhabricatorDaemonLog.php index 641ab3aabd..7af4228310 100644 --- a/src/applications/daemon/storage/PhabricatorDaemonLog.php +++ b/src/applications/daemon/storage/PhabricatorDaemonLog.php @@ -1,78 +1,80 @@ array( 'argv' => self::SERIALIZATION_JSON, 'explicitArgv' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'daemon' => 'text255', 'host' => 'text255', 'pid' => 'uint32', + 'runningAsUser' => 'text255?', 'envHash' => 'bytes40', 'status' => 'text8', ), self::CONFIG_KEY_SCHEMA => array( 'status' => array( 'columns' => array('status'), ), 'dateCreated' => array( 'columns' => array('dateCreated'), ), ), ) + parent::getConfiguration(); } public function getExplicitArgv() { $argv = $this->explicitArgv; if (!is_array($argv)) { return array(); } return $argv; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getPHID() { return null; } public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { return PhabricatorPolicies::POLICY_ADMIN; } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { return null; } }