diff --git a/src/applications/daemon/management/PhabricatorDaemonManagementStatusWorkflow.php b/src/applications/daemon/management/PhabricatorDaemonManagementStatusWorkflow.php index 18132ab15e..dbccce3dc5 100644 --- a/src/applications/daemon/management/PhabricatorDaemonManagementStatusWorkflow.php +++ b/src/applications/daemon/management/PhabricatorDaemonManagementStatusWorkflow.php @@ -1,111 +1,52 @@ setName('status') ->setSynopsis(pht('Show status of running daemons.')) ->setArguments(array()); } public function execute(PhutilArgumentParser $args) { $console = PhutilConsole::getConsole(); $daemons = $this->loadRunningDaemons(); if (!$daemons) { $console->writeErr( "%s\n", pht('There are no running Phabricator daemons.')); return 1; } $status = 0; - $table = id(new PhutilConsoleTable()) - ->addColumns(array( - 'pid' => array( - 'title' => 'PID', - ), - 'started' => array( - 'title' => 'Started', - ), - 'daemon' => array( - 'title' => 'Daemon', - ), - 'argv' => array( - 'title' => 'Arguments', - ), - )); - + printf( + "%-5s\t%-24s\t%-50s%s\n", + 'PID', + 'Started', + 'Daemon', + 'Arguments'); foreach ($daemons as $daemon) { $name = $daemon->getName(); if (!$daemon->isRunning()) { $daemon->updateStatus(PhabricatorDaemonLog::STATUS_DEAD); $status = 2; $name = ' '.$name; } - - $table->addRow(array( - 'pid' => $daemon->getPID(), - 'started' => $daemon->getEpochStarted() + printf( + "%5s\t%-24s\t%-50s%s\n", + $daemon->getPID(), + $daemon->getEpochStarted() ? date('M j Y, g:i:s A', $daemon->getEpochStarted()) : null, - 'daemon' => $name, - 'argv' => csprintf('%LR', $daemon->getArgv()), - )); + $name, + csprintf('%LR', $daemon->getArgv())); } - $table->draw(); + return $status; } - protected function executeGlobal() { - $console = PhutilConsole::getConsole(); - $daemons = $this->loadAllRunningDaemons(); - - if (!$daemons) { - $console->writeErr( - "%s\n", - pht('There are no running Phabricator daemons.')); - return 1; - } - - $status = 0; - - $table = id(new PhutilConsoleTable()) - ->addColumns(array( - 'id' => array( - 'title' => 'ID', - ), - 'host' => array( - 'title' => 'Host', - ), - 'pid' => array( - 'title' => 'PID', - ), - 'started' => array( - 'title' => 'Started', - ), - 'daemon' => array( - 'title' => 'Daemon', - ), - 'argv' => array( - 'title' => 'Arguments', - ), - )); - - foreach ($daemons as $daemon) { - $table->addRow(array( - 'id' => $daemon->getID(), - 'host' => $daemon->getHost(), - 'pid' => $daemon->getPID(), - 'started' => date('M j Y, g:i:s A', $daemon->getDateCreated()), - 'daemon' => $daemon->getDaemon(), - 'argv' => csprintf('%LR', array() /* $daemon->getArgv() */), - )); - } - - $table->draw(); - } } diff --git a/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php b/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php index 6a7e3bd435..a0e1cf575e 100644 --- a/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php +++ b/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php @@ -1,373 +1,376 @@ setAncestorClass('PhutilDaemon') ->setConcreteOnly(true) ->selectSymbolsWithoutLoading(); } - protected final function getPIDDirectory() { + public function getPIDDirectory() { $path = PhabricatorEnv::getEnvConfig('phd.pid-directory'); return $this->getControlDirectory($path); } - protected final function getLogDirectory() { + public 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() { + public function loadRunningDaemons() { $results = array(); - $ids = array(); $pid_dir = $this->getPIDDirectory(); $pid_files = Filesystem::listDirectory($pid_dir); if (!$pid_files) { return $results; } foreach ($pid_files as $pid_file) { - $results[] = PhabricatorDaemonReference::newFromDictionary( - $pid_dir.'/'.$pid_file); - $ids[] = $ref->getDaemonLog()->getID(); + $pid_data = Filesystem::readFile($pid_dir.'/'.$pid_file); + $dict = json_decode($pid_data, true); + if (!is_array($dict)) { + // Just return a hanging reference, since control code needs to be + // robust against unusual system states. + $dict = array(); + } + $ref = PhabricatorDaemonReference::newFromDictionary($dict); + $ref->setPIDFile($pid_dir.'/'.$pid_file); + $results[] = $ref; } - $other = id(new PhabricatorDaemonLogQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withStatus(PhabricatorDaemonLogQuery::STATUS_ALIVE) - ->execute(); - - + return $results; } 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 function launchDaemon($class, array $argv, $debug) { $daemon = $this->findDaemonClass($class); $console = PhutilConsole::getConsole(); 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(); } } 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() { + protected 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) { + protected 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) { + + protected function executeStopCommand(array $pids) { $console = PhutilConsole::getConsole(); $daemons = $this->loadRunningDaemons(); if (!$daemons) { $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; foreach ($running as $key => $daemon) { $pid = $daemon->getPID(); $name = $daemon->getName(); $console->writeErr(pht("Stopping daemon '%s' (%s)...", $name, $pid)."\n"); if (!$daemon->isRunning()) { $console->writeErr(pht('Daemon is not running.')."\n"); unset($running[$key]); $daemon->updateStatus(PhabricatorDaemonLog::STATUS_EXITED); } else { posix_kill($pid, SIGINT); } } $start = time(); do { foreach ($running as $key => $daemon) { $pid = $daemon->getPID(); if (!$daemon->isRunning()) { $console->writeOut(pht('Daemon %s exited normally.', $pid)."\n"); unset($running[$key]); } } if (empty($running)) { break; } usleep(100000); } while (time() < $start + 15); foreach ($running as $key => $daemon) { $pid = $daemon->getPID(); $console->writeErr(pht('Sending daemon %s a SIGKILL.', $pid)."\n"); posix_kill($pid, SIGKILL); } foreach ($all_daemons as $daemon) { if ($daemon->getPIDFile()) { Filesystem::remove($daemon->getPIDFile()); } } return 0; } 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/infrastructure/daemon/control/PhabricatorDaemonReference.php b/src/infrastructure/daemon/control/PhabricatorDaemonReference.php index a2a71228a7..679680a62e 100644 --- a/src/infrastructure/daemon/control/PhabricatorDaemonReference.php +++ b/src/infrastructure/daemon/control/PhabricatorDaemonReference.php @@ -1,136 +1,118 @@ pidFile = $path; - return $ref; - } - public static function newFromDictionary(array $dict) { $ref = new PhabricatorDaemonReference(); $ref->name = idx($dict, 'name', 'Unknown'); $ref->argv = idx($dict, 'argv', array()); $ref->pid = idx($dict, 'pid'); $ref->start = idx($dict, 'start'); - $this->daemonLog = id(new PhabricatorDaemonLog())->loadOneWhere( - 'daemon = %s AND pid = %d AND dateCreated = %d', - $this->name, - $this->pid, - $this->start); - return $ref; } public function updateStatus($new_status) { try { if (!$this->daemonLog) { $this->daemonLog = id(new PhabricatorDaemonLog())->loadOneWhere( 'daemon = %s AND pid = %d AND dateCreated = %d', $this->name, $this->pid, $this->start); } if ($this->daemonLog) { $this->daemonLog ->setStatus($new_status) ->save(); } } catch (AphrontQueryException $ex) { // Ignore anything that goes wrong here. We anticipate at least two // specific failure modes: // // - Upgrade scripts which run `git pull`, then `phd stop`, then // `bin/storage upgrade` will fail when trying to update the `status` // column, as it does not exist yet. // - Daemons running on machines which do not have access to MySQL // (like an IRC bot) will not be able to load or save the log. // // } } public function getPID() { return $this->pid; } public function getName() { return $this->name; } public function getArgv() { return $this->argv; } public function getEpochStarted() { return $this->start; } - public function getPIDFile() { - return $this->pidFile; + public function setPIDFile($pid_file) { + $this->pidFile = $pid_file; + return $this; } - public function getDaemonLog() { - return $this->daemonLog; + public function getPIDFile() { + return $this->pidFile; } public function isRunning() { return self::isProcessRunning($this->getPID()); } public static function isProcessRunning($pid) { if (!$pid) { return false; } if (function_exists('posix_kill')) { // This may fail if we can't signal the process because we are running as // a different user (for example, we are 'apache' and the process is some // other user's, or we are a normal user and the process is root's), but // we can check the error code to figure out if the process exists. $is_running = posix_kill($pid, 0); if (posix_get_last_error() == 1) { // "Operation Not Permitted", indicates that the PID exists. If it // doesn't, we'll get an error 3 ("No such process") instead. $is_running = true; } } else { // If we don't have the posix extension, just exec. list($err) = exec_manual('ps %s', $pid); $is_running = ($err == 0); } return $is_running; } public function waitForExit($seconds) { $start = time(); while (time() < $start + $seconds) { usleep(100000); if (!$this->isRunning()) { return true; } } return !$this->isRunning(); } }