diff --git a/resources/sql/autopatches/20150223.daemon.1.id.sql b/resources/sql/autopatches/20150223.daemon.1.id.sql new file mode 100644 index 0000000000..cd757cb817 --- /dev/null +++ b/resources/sql/autopatches/20150223.daemon.1.id.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_daemon.daemon_log + ADD daemonID VARCHAR(64) NOT NULL COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20150223.daemon.2.idlegacy.sql b/resources/sql/autopatches/20150223.daemon.2.idlegacy.sql new file mode 100644 index 0000000000..34583ad96f --- /dev/null +++ b/resources/sql/autopatches/20150223.daemon.2.idlegacy.sql @@ -0,0 +1,2 @@ +UPDATE {$NAMESPACE}_daemon.daemon_log + SET daemonID = CONCAT('legacy-', id) WHERE daemonID = ''; diff --git a/resources/sql/autopatches/20150223.daemon.3.idkey.sql b/resources/sql/autopatches/20150223.daemon.3.idkey.sql new file mode 100644 index 0000000000..b9ad068291 --- /dev/null +++ b/resources/sql/autopatches/20150223.daemon.3.idkey.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_daemon.daemon_log + ADD UNIQUE KEY `key_daemonID` (daemonID); diff --git a/src/applications/daemon/event/PhabricatorDaemonEventListener.php b/src/applications/daemon/event/PhabricatorDaemonEventListener.php index ada6ce92d3..db6d9a2eff 100644 --- a/src/applications/daemon/event/PhabricatorDaemonEventListener.php +++ b/src/applications/daemon/event/PhabricatorDaemonEventListener.php @@ -1,120 +1,121 @@ listen(PhutilDaemonHandle::EVENT_DID_LAUNCH); $this->listen(PhutilDaemonHandle::EVENT_DID_LOG); $this->listen(PhutilDaemonHandle::EVENT_DID_HEARTBEAT); $this->listen(PhutilDaemonHandle::EVENT_WILL_GRACEFUL); $this->listen(PhutilDaemonHandle::EVENT_WILL_EXIT); } public function handleEvent(PhutilEvent $event) { switch ($event->getType()) { case PhutilDaemonHandle::EVENT_DID_LAUNCH: $this->handleLaunchEvent($event); break; case PhutilDaemonHandle::EVENT_DID_HEARTBEAT: $this->handleHeartbeatEvent($event); break; case PhutilDaemonHandle::EVENT_DID_LOG: $this->handleLogEvent($event); break; case PhutilDaemonHandle::EVENT_WILL_GRACEFUL: $this->handleGracefulEvent($event); break; case PhutilDaemonHandle::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()) + ->setDaemonID($id) ->setDaemon($event->getValue('daemonClass')) ->setHost(php_uname('n')) ->setPID(getmypid()) ->setRunningAsUser($current_user['name']) ->setEnvHash(PhabricatorEnv::calculateEnvironmentHash()) ->setEnvInfo(PhabricatorEnv::calculateEnvironmentInfo()) ->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}'!"); + throw new Exception(pht('No such daemon "%s"!', $id)); } } diff --git a/src/applications/daemon/management/PhabricatorDaemonManagementStatusWorkflow.php b/src/applications/daemon/management/PhabricatorDaemonManagementStatusWorkflow.php index 5af583b468..9fc3cc016e 100644 --- a/src/applications/daemon/management/PhabricatorDaemonManagementStatusWorkflow.php +++ b/src/applications/daemon/management/PhabricatorDaemonManagementStatusWorkflow.php @@ -1,99 +1,106 @@ setName('status') ->setSynopsis(pht('Show status of running daemons.')) ->setArguments( array( array( 'name' => 'local', 'help' => pht('Show only local daemons.'), ), )); } public function execute(PhutilArgumentParser $args) { $console = PhutilConsole::getConsole(); if ($args->getArg('local')) { $daemons = $this->loadRunningDaemons(); } else { $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', + 'title' => pht('Log'), + ), + 'daemonID' => array( + 'title' => pht('Daemon'), ), 'host' => array( - 'title' => 'Host', + 'title' => pht('Host'), ), 'pid' => array( - 'title' => 'PID', + 'title' => pht('Overseer'), ), 'started' => array( - 'title' => 'Started', + 'title' => pht('Started'), ), 'daemon' => array( - 'title' => 'Daemon', + 'title' => pht('Class'), ), 'argv' => array( - 'title' => 'Arguments', + 'title' => pht('Arguments'), ), )); foreach ($daemons as $daemon) { if ($daemon instanceof PhabricatorDaemonLog) { $table->addRow(array( 'id' => $daemon->getID(), + 'daemonID' => $daemon->getDaemonID(), 'host' => $daemon->getHost(), 'pid' => $daemon->getPID(), 'started' => date('M j Y, g:i:s A', $daemon->getDateCreated()), 'daemon' => $daemon->getDaemon(), 'argv' => csprintf('%LR', $daemon->getExplicitArgv()), )); } else if ($daemon instanceof PhabricatorDaemonReference) { $name = $daemon->getName(); if (!$daemon->isRunning()) { $daemon->updateStatus(PhabricatorDaemonLog::STATUS_DEAD); $status = 2; $name = ' '.$name; } $daemon_log = $daemon->getDaemonLog(); $id = null; + $daemon_id = null; if ($daemon_log) { $id = $daemon_log->getID(); + $daemon_id = $daemon_log->getDaemonID(); } $table->addRow(array( 'id' => $id, + 'daemonID' => $daemon_id, 'host' => 'localhost', 'pid' => $daemon->getPID(), 'started' => $daemon->getEpochStarted() ? date('M j Y, g:i:s A', $daemon->getEpochStarted()) : null, 'daemon' => $name, 'argv' => csprintf('%LR', $daemon->getArgv()), )); } } $table->draw(); } } diff --git a/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php b/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php index aa89f9f738..5e719532c1 100644 --- a/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php +++ b/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php @@ -1,590 +1,594 @@ 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) { $path = $pid_dir.'/'.$pid_file; $daemons[] = PhabricatorDaemonReference::loadReferencesFromFile($path); } return array_mergev($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()) + $daemon_query = id(new PhabricatorDaemonLogQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withoutIDs($local_ids) - ->withStatus(PhabricatorDaemonLogQuery::STATUS_ALIVE) - ->execute(); + ->withStatus(PhabricatorDaemonLogQuery::STATUS_ALIVE); + + if ($local_ids) { + $daemon_query->withoutIDs($local_ids); + } + + $remote_daemons = $daemon_query->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 launchDaemons( array $daemons, $debug, $run_as_current_user = false) { // Convert any shorthand classnames like "taskmaster" into proper class // names. foreach ($daemons as $key => $daemon) { $class = $this->findDaemonClass($daemon['class']); $daemons[$key]['class'] = $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"); } } } $this->printLaunchingDaemons($daemons, $debug); $flags = array(); if ($debug || PhabricatorEnv::getEnvConfig('phd.trace')) { $flags[] = '--trace'; } if ($debug || PhabricatorEnv::getEnvConfig('phd.verbose')) { $flags[] = '--verbose'; } $instance = PhabricatorEnv::getEnvConfig('cluster.instance'); if ($instance) { $flags[] = '-l'; $flags[] = $instance; } $config = array(); if (!$debug) { $config['daemonize'] = true; } if (!$debug) { $config['log'] = $this->getLogDirectory().'/daemons.log'; } $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); $config['piddir'] = $pid_dir; $config['daemons'] = $daemons; $command = csprintf('./phd-daemon %Ls', $flags); $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"; $tempfile = new TempFile('daemon.config'); Filesystem::writeFile($tempfile, json_encode($config)); phutil_passthru( '(cd %s && exec %C < %s)', $daemon_script_dir, $command, $tempfile); } else { try { $this->executeDaemonLaunchCommand( $command, $daemon_script_dir, $config, $this->runDaemonsAsUser); } catch (Exception $e) { // Retry without sudo $console->writeOut(pht( "sudo command failed. Starting daemon as current user\n")); $this->executeDaemonLaunchCommand( $command, $daemon_script_dir, $config); } } } private function executeDaemonLaunchCommand( $command, $daemon_script_dir, array $config, $run_as_user = null) { $is_sudo = false; 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); $is_sudo = true; } $future = new ExecFuture('exec %C', $command); // Play games to keep 'ps' looking reasonable. $future->setCWD($daemon_script_dir); $future->write(json_encode($config)); list($stdout, $stderr) = $future->resolvex(); if ($is_sudo) { // On OSX, `sudo -n` exits 0 when the user does not have permission to // switch accounts without a password. This is not consistent with // sudo on Linux, and seems buggy/broken. Check for this by string // matching the output. if (preg_match('/sudo: a password is required/', $stderr)) { throw new Exception( pht( 'sudo exited with a zero exit code, but emitted output '. 'consistent with failure under OSX.')); } } } 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); } } } /* -( Commands )----------------------------------------------------------- */ protected final function executeStartCommand($keep_leases, $force) { $console = PhutilConsole::getConsole(); if (!$force) { $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\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.\n". "You can force daemons to start anyway with --force."); $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( 'class' => 'PhabricatorRepositoryPullLocalDaemon', ), array( 'class' => 'PhabricatorGarbageCollectorDaemon', ), array( 'class' => 'PhabricatorTriggerDaemon', ), ); $taskmasters = PhabricatorEnv::getEnvConfig('phd.start-taskmasters'); for ($ii = 0; $ii < $taskmasters; $ii++) { $daemons[] = array( 'class' => 'PhabricatorTaskmasterDaemon', ); } $this->launchDaemons($daemons, $is_debug = false); $console->writeErr(pht('Done.')."\n"); return 0; } protected final function executeStopCommand( array $pids, array $options) { $console = PhutilConsole::getConsole(); $grace_period = idx($options, 'graceful', 15); $force = idx($options, 'force'); $gently = idx($options, 'gently'); if ($gently && $force) { throw new PhutilArgumentUsageException( pht( 'You can not specify conflicting options --gently and --force '. 'together.')); } $daemons = $this->loadRunningDaemons(); if (!$daemons) { $survivors = array(); if (!$pids && !$gently) { $survivors = $this->processRogueDaemons( $grace_period, $warn = true, $force); } if (!$survivors) { $console->writeErr(pht( 'There are no running Phabricator daemons.')."\n"); } return 0; } $running_pids = array_fuse(mpull($daemons, 'getPID')); if (!$pids) { $stop_pids = $running_pids; } else { // We were given a PID or set of PIDs to kill. $stop_pids = array(); 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($running_pids[$pid])) { $console->writeErr( pht( 'PID "%d" is not a known Phabricator daemon PID. It will not '. 'be killed.', $pid)."\n"); continue; } else { $stop_pids[$pid] = $pid; } } } if (!$stop_pids) { $console->writeErr(pht('No daemons to kill.')."\n"); return 0; } $survivors = $this->sendStopSignals($stop_pids, $grace_period); // Try to clean up PID files for daemons we killed. $remove = array(); foreach ($daemons as $daemon) { $pid = $daemon->getPID(); if (empty($stop_pids[$pid])) { // We did not try to stop this overseer. continue; } if (isset($survivors[$pid])) { // We weren't able to stop this overseer. continue; } if (!$daemon->getPIDFile()) { // We don't know where the PID file is. continue; } $remove[] = $daemon->getPIDFile(); } foreach (array_unique($remove) as $remove_file) { Filesystem::remove($remove_file); } if (!$gently) { $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) { $rogue_pids = ipull($rogue_daemons, 'pid'); $survivors = $this->sendStopSignals($rogue_pids, $grace_period); 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 sendStopSignals($pids, $grace_period) { // If we're doing a graceful shutdown, try SIGINT first. if ($grace_period) { $pids = $this->sendSignal($pids, SIGINT, $grace_period); } // If we still have daemons, SIGTERM them. if ($pids) { $pids = $this->sendSignal($pids, SIGTERM, 15); } // If the overseer is still alive, SIGKILL it. if ($pids) { $pids = $this->sendSignal($pids, SIGKILL, 0); } return $pids; } private function sendSignal(array $pids, $signo, $wait) { $console = PhutilConsole::getConsole(); $pids = array_fuse($pids); foreach ($pids as $key => $pid) { if (!$pid) { // NOTE: We must have a PID to signal a daemon, since sending a signal // to PID 0 kills this process. unset($pids[$key]); continue; } switch ($signo) { case SIGINT: $message = pht('Interrupting process %d...', $pid); break; case SIGTERM: $message = pht('Terminating process %d...', $pid); break; case SIGKILL: $message = pht('Killing process %d...', $pid); break; } $console->writeOut("%s\n", $message); posix_kill($pid, $signo); } if ($wait) { $start = PhabricatorTime::getNow(); do { foreach ($pids as $key => $pid) { if (!PhabricatorDaemonReference::isProcessRunning($pid)) { $console->writeOut(pht('Process %d exited.', $pid)."\n"); unset($pids[$key]); } } if (empty($pids)) { break; } usleep(100000); } while (PhabricatorTime::getNow() < $start + $wait); } return $pids; } 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(); } private function printLaunchingDaemons(array $daemons, $debug) { $console = PhutilConsole::getConsole(); if ($debug) { $console->writeOut(pht('Launching daemons (in debug mode):')); } else { $console->writeOut(pht('Launching daemons:')); } $log_dir = $this->getLogDirectory().'/daemons.log'; $console->writeOut( "\n%s\n\n", pht('(Logs will appear in "%s".)', $log_dir)); foreach ($daemons as $daemon) { $is_autoscale = isset($daemon['autoscale']['group']); if ($is_autoscale) { $autoscale = pht('(Autoscaling)'); } else { $autoscale = pht('(Static)'); } $console->writeOut( " %s %s\n", $daemon['class'], $autoscale, implode(' ', idx($daemon, 'argv', array()))); } $console->writeOut("\n"); } } diff --git a/src/applications/daemon/query/PhabricatorDaemonLogQuery.php b/src/applications/daemon/query/PhabricatorDaemonLogQuery.php index 7f411b27db..58e76e3462 100644 --- a/src/applications/daemon/query/PhabricatorDaemonLogQuery.php +++ b/src/applications/daemon/query/PhabricatorDaemonLogQuery.php @@ -1,176 +1,189 @@ ids = $ids; return $this; } public function withoutIDs(array $ids) { $this->notIDs = $ids; return $this; } public function withStatus($status) { $this->status = $status; return $this; } public function withDaemonClasses(array $classes) { $this->daemonClasses = $classes; return $this; } public function setAllowStatusWrites($allow) { $this->allowStatusWrites = $allow; return $this; } + public function withDaemonIDs(array $daemon_ids) { + $this->daemonIDs = $daemon_ids; + return $this; + } + protected function loadPage() { $table = new PhabricatorDaemonLog(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($data); } protected function willFilterPage(array $daemons) { $unknown_delay = PhabricatorDaemonLogQuery::getTimeUntilUnknown(); $dead_delay = PhabricatorDaemonLogQuery::getTimeUntilDead(); $status_running = PhabricatorDaemonLog::STATUS_RUNNING; $status_unknown = PhabricatorDaemonLog::STATUS_UNKNOWN; $status_wait = PhabricatorDaemonLog::STATUS_WAIT; $status_exiting = PhabricatorDaemonLog::STATUS_EXITING; $status_exited = PhabricatorDaemonLog::STATUS_EXITED; $status_dead = PhabricatorDaemonLog::STATUS_DEAD; $filter = array_fuse($this->getStatusConstants()); foreach ($daemons as $key => $daemon) { $status = $daemon->getStatus(); $seen = $daemon->getDateModified(); $is_running = ($status == $status_running) || ($status == $status_wait) || ($status == $status_exiting); // If we haven't seen the daemon recently, downgrade its status to // unknown. $unknown_time = ($seen + $unknown_delay); if ($is_running && ($unknown_time < time())) { $status = $status_unknown; } // If the daemon hasn't been seen in quite a while, assume it is dead. $dead_time = ($seen + $dead_delay); if (($status == $status_unknown) && ($dead_time < time())) { $status = $status_dead; } // If we changed the daemon's status, adjust it. if ($status != $daemon->getStatus()) { $daemon->setStatus($status); // ...and write it, if we're in a context where that's reasonable. if ($this->allowStatusWrites) { $guard = AphrontWriteGuard::beginScopedUnguardedWrites(); $daemon->save(); unset($guard); } } // If the daemon no longer matches the filter, get rid of it. if ($filter) { if (empty($filter[$daemon->getStatus()])) { unset($daemons[$key]); } } } return $daemons; } private function buildWhereClause(AphrontDatabaseConnection $conn_r) { $where = array(); - if ($this->ids) { + if ($this->ids !== null) { $where[] = qsprintf( $conn_r, 'id IN (%Ld)', $this->ids); } - if ($this->notIDs) { + if ($this->notIDs !== null) { $where[] = qsprintf( $conn_r, 'id NOT IN (%Ld)', $this->notIDs); } if ($this->getStatusConstants()) { $where[] = qsprintf( $conn_r, 'status IN (%Ls)', $this->getStatusConstants()); } - if ($this->daemonClasses) { + if ($this->daemonClasses !== null) { $where[] = qsprintf( $conn_r, 'daemon IN (%Ls)', $this->daemonClasses); } + if ($this->daemonIDs !== null) { + $where[] = qsprintf( + $conn_r, + 'daemonID IN (%Ls)', + $this->daemonIDs); + } + $where[] = $this->buildPagingClause($conn_r); return $this->formatWhereClause($where); } private function getStatusConstants() { $status = $this->status; switch ($status) { case self::STATUS_ALL: return array(); case self::STATUS_ALIVE: return array( PhabricatorDaemonLog::STATUS_UNKNOWN, PhabricatorDaemonLog::STATUS_RUNNING, PhabricatorDaemonLog::STATUS_WAIT, PhabricatorDaemonLog::STATUS_EXITING, ); default: - throw new Exception("Unknown status '{$status}'!"); + throw new Exception(pht('Unknown status "%s"!', $status)); } } public function getQueryApplicationClass() { return 'PhabricatorDaemonsApplication'; } } diff --git a/src/applications/daemon/storage/PhabricatorDaemonLog.php b/src/applications/daemon/storage/PhabricatorDaemonLog.php index 0e4f1ea838..6573ea8e4c 100644 --- a/src/applications/daemon/storage/PhabricatorDaemonLog.php +++ b/src/applications/daemon/storage/PhabricatorDaemonLog.php @@ -1,82 +1,88 @@ array( 'argv' => self::SERIALIZATION_JSON, 'explicitArgv' => self::SERIALIZATION_JSON, 'envInfo' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'daemon' => 'text255', 'host' => 'text255', 'pid' => 'uint32', 'runningAsUser' => 'text255?', 'envHash' => 'bytes40', 'status' => 'text8', + 'daemonID' => 'text64', ), self::CONFIG_KEY_SCHEMA => array( 'status' => array( 'columns' => array('status'), ), 'dateCreated' => array( 'columns' => array('dateCreated'), ), + 'key_daemonID' => array( + 'columns' => array('daemonID'), + 'unique' => true, + ), ), ) + 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; } } diff --git a/src/infrastructure/daemon/control/PhabricatorDaemonReference.php b/src/infrastructure/daemon/control/PhabricatorDaemonReference.php index 89001622ef..4b6fe1182f 100644 --- a/src/infrastructure/daemon/control/PhabricatorDaemonReference.php +++ b/src/infrastructure/daemon/control/PhabricatorDaemonReference.php @@ -1,133 +1,148 @@ setViewer(PhabricatorUser::getOmnipotentUser()) + ->withDaemonIDs($daemon_ids) + ->execute(); + } catch (AphrontQueryException $ex) { + // Ignore any issues here; getting this information only allows us + // to provide a more complete picture of daemon status, and we want + // these commands to work if the database is inaccessible. + } + + $logs = mpull($logs, null, 'getDaemonID'); + } + foreach ($daemons as $daemon) { $ref = new PhabricatorDaemonReference(); // NOTE: This is the overseer PID, not the actual daemon process PID. // This is correct for checking status and sending signals (the only // things we do with it), but might be confusing. $daemon['pid'] has // the daemon PID, and we could expose that if we had some use for it. $ref->pid = idx($dict, 'pid'); $ref->start = idx($dict, 'start'); - $ref->name = idx($daemon, 'class'); - $ref->argv = idx($daemon, 'argv', array()); - + $config = idx($daemon, 'config', array()); + $ref->name = idx($config, 'class'); + $ref->argv = idx($config, 'argv', array()); - // TODO: We previously identified daemon logs by using a tuple, but now all daemons under a single overseer will share - // that identifier. We can uniquely identify daemons by $daemon['id'], - // but that isn't currently written into the daemon logs. We should - // start writing it, then load the logs here. This would give us a - // slightly greater ability to keep the web UI in sync when daemons - // get killed forcefully and clean up `phd status` a bit. + $log = idx($logs, idx($daemon, 'id')); + if ($log) { + $ref->daemonLog = $log; + } $ref->pidFile = $path; $refs[] = $ref; } return $refs; } public function updateStatus($new_status) { if (!$this->daemonLog) { return; } try { $this->daemonLog ->setStatus($new_status) ->save(); } catch (AphrontQueryException $ex) { // Ignore anything that goes wrong here. } } 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 getDaemonLog() { return $this->daemonLog; } 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(); } }