diff --git a/scripts/daemon/phabricator_daemon_launcher.php b/scripts/daemon/phabricator_daemon_launcher.php index a67a8ace57..106023ab44 100755 --- a/scripts/daemon/phabricator_daemon_launcher.php +++ b/scripts/daemon/phabricator_daemon_launcher.php @@ -4,183 +4,26 @@ $root = dirname(dirname(dirname(__FILE__))); require_once $root.'/scripts/__init_script__.php'; -$control = new PhabricatorDaemonControl(); +PhabricatorDaemonManagementWorkflow::requireExtensions(); -must_have_extension('pcntl'); -must_have_extension('posix'); +$args = new PhutilArgumentParser($argv); +$args->setTagline('manage daemons'); +$args->setSynopsis(<<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); - } - } -} - -$command = isset($argv[1]) ? $argv[1] : 'help'; -switch ($command) { - case 'list': - $err = $control->executeListCommand(); - exit($err); - - case 'status': - $err = $control->executeStatusCommand(); - exit($err); - - case 'stop': - $pass_argv = array_slice($argv, 2); - $err = $control->executeStopCommand($pass_argv); - exit($err); - - case 'restart': - $err = $control->executeStopCommand(array()); - if ($err) { - exit($err); - } - /* Fall Through */ - case 'start': - $running = $control->loadRunningDaemons(); - // "running" might not mean actually running so much as was running at - // some point. ergo, do a quick grouping and only barf if daemons are - // *actually* running. - $running_dict = mgroup($running, 'isRunning'); - if (!empty($running_dict[true])) { - echo phutil_console_wrap( - "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.\n"); - exit(1); - } - - $daemons = array( - array('PhabricatorRepositoryPullLocalDaemon', array()), - array('PhabricatorGarbageCollectorDaemon', array()), - ); - - $taskmasters = PhabricatorEnv::getEnvConfig('phd.start-taskmasters'); - for ($ii = 0; $ii < $taskmasters; $ii++) { - $daemons[] = array('PhabricatorTaskmasterDaemon', array()); - } - - will_launch($control); - foreach ($daemons as $spec) { - list($name, $argv) = $spec; - echo "Launching '{$name}'...\n"; - $control->launchDaemon($name, $argv); - } - - echo "Done.\n"; - break; - - case 'launch': - case 'debug': - $is_debug = ($argv[1] == 'debug'); - - $daemon = idx($argv, 2); - if (!$daemon) { - throw new Exception("Daemon name required!"); - } - - $pass_argv = array_slice($argv, 3); - - $n = 1; - if (!$is_debug) { - if (is_numeric($daemon)) { - $n = $daemon; - if ($n < 1) { - throw new Exception("Count must be at least 1!"); - } - $daemon = idx($argv, 3); - if (!$daemon) { - throw new Exception("Daemon name required!"); - } - $pass_argv = array_slice($argv, 4); - } - } - - $loader = new PhutilSymbolLoader(); - $symbols = $loader - ->setAncestorClass('PhutilDaemon') - ->selectSymbolsWithoutLoading(); - - $symbols = ipull($symbols, 'name'); - $match = array(); - foreach ($symbols as $symbol) { - if (stripos($symbol, $daemon) !== false) { - if (strtolower($symbol) == strtolower($daemon)) { - $match = array($symbol); - break; - } else { - $match[] = $symbol; - } - } - } - - if (count($match) == 0) { - throw new Exception( - "No daemons match! Use 'phd list' for a list of daemons."); - } else if (count($match) > 1) { - throw new Exception( - "Which of these daemons did you mean?\n". - " ".implode("\n ", $match)); - } else { - $daemon = reset($match); - } - - $with_logs = true; - if ($is_debug) { - // In debug mode, we emit errors straight to stdout, so nothing useful - // will show up in the logs. Don't echo the message about stuff showing - // up in them, since it would be confusing. - $with_logs = false; - } - - will_launch($control, $with_logs); - - if ($is_debug) { - echo "Launching {$daemon} in debug mode (nondaemonized)...\n"; - } else { - echo "Launching {$n} x {$daemon}"; - } - - for ($ii = 0; $ii < $n; $ii++) { - $control->launchDaemon($daemon, $pass_argv, $is_debug); - if (!$is_debug) { - echo "."; - } - } - - echo "\n"; - echo "Done.\n"; - - break; - - case '--help': - case 'help': - default: - $err = $control->executeHelpCommand(); - exit($err); -} - -function will_launch($control, $with_logs = true) { - echo "Staging launch...\n"; - $control->pingConduit(); - if ($with_logs) { - $log_dir = $control->getLogDirectory().'/daemons.log'; - echo "NOTE: Logs will appear in '{$log_dir}'.\n\n"; - } -} +EOSYNOPSIS + ); +$args->parseStandardArguments(); +$workflows = array( + new PhabricatorDaemonManagementListWorkflow(), + new PhabricatorDaemonManagementStatusWorkflow(), + new PhabricatorDaemonManagementStartWorkflow(), + new PhabricatorDaemonManagementStopWorkflow(), + new PhabricatorDaemonManagementRestartWorkflow(), + new PhabricatorDaemonManagementLaunchWorkflow(), + new PhabricatorDaemonManagementDebugWorkflow(), + new PhutilHelpArgumentWorkflow(), +); +$args->parseWorkflows($workflows); diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 275a9afd03..920ebee561 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1016,7 +1016,6 @@ phutil_register_library_map(array( 'PhabricatorDaemon' => 'infrastructure/daemon/PhabricatorDaemon.php', 'PhabricatorDaemonCombinedLogController' => 'applications/daemon/controller/PhabricatorDaemonCombinedLogController.php', 'PhabricatorDaemonConsoleController' => 'applications/daemon/controller/PhabricatorDaemonConsoleController.php', - 'PhabricatorDaemonControl' => 'infrastructure/daemon/PhabricatorDaemonControl.php', 'PhabricatorDaemonController' => 'applications/daemon/controller/PhabricatorDaemonController.php', 'PhabricatorDaemonDAO' => 'infrastructure/daemon/storage/PhabricatorDaemonDAO.php', 'PhabricatorDaemonLog' => 'infrastructure/daemon/storage/PhabricatorDaemonLog.php', @@ -1025,6 +1024,14 @@ phutil_register_library_map(array( 'PhabricatorDaemonLogListController' => 'applications/daemon/controller/PhabricatorDaemonLogListController.php', 'PhabricatorDaemonLogListView' => 'applications/daemon/view/PhabricatorDaemonLogListView.php', 'PhabricatorDaemonLogViewController' => 'applications/daemon/controller/PhabricatorDaemonLogViewController.php', + 'PhabricatorDaemonManagementDebugWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementDebugWorkflow.php', + 'PhabricatorDaemonManagementLaunchWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementLaunchWorkflow.php', + 'PhabricatorDaemonManagementListWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementListWorkflow.php', + 'PhabricatorDaemonManagementRestartWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementRestartWorkflow.php', + 'PhabricatorDaemonManagementStartWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementStartWorkflow.php', + 'PhabricatorDaemonManagementStatusWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementStatusWorkflow.php', + 'PhabricatorDaemonManagementStopWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementStopWorkflow.php', + 'PhabricatorDaemonManagementWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementWorkflow.php', 'PhabricatorDaemonReference' => 'infrastructure/daemon/control/PhabricatorDaemonReference.php', 'PhabricatorDebugController' => 'applications/system/PhabricatorDebugController.php', 'PhabricatorDefaultFileStorageEngineSelector' => 'applications/files/engineselector/PhabricatorDefaultFileStorageEngineSelector.php', @@ -2991,6 +2998,14 @@ phutil_register_library_map(array( 'PhabricatorDaemonLogListController' => 'PhabricatorDaemonController', 'PhabricatorDaemonLogListView' => 'AphrontView', 'PhabricatorDaemonLogViewController' => 'PhabricatorDaemonController', + 'PhabricatorDaemonManagementDebugWorkflow' => 'PhabricatorDaemonManagementWorkflow', + 'PhabricatorDaemonManagementLaunchWorkflow' => 'PhabricatorDaemonManagementWorkflow', + 'PhabricatorDaemonManagementListWorkflow' => 'PhabricatorDaemonManagementWorkflow', + 'PhabricatorDaemonManagementRestartWorkflow' => 'PhabricatorDaemonManagementWorkflow', + 'PhabricatorDaemonManagementStartWorkflow' => 'PhabricatorDaemonManagementWorkflow', + 'PhabricatorDaemonManagementStatusWorkflow' => 'PhabricatorDaemonManagementWorkflow', + 'PhabricatorDaemonManagementStopWorkflow' => 'PhabricatorDaemonManagementWorkflow', + 'PhabricatorDaemonManagementWorkflow' => 'PhutilArgumentWorkflow', 'PhabricatorDebugController' => 'PhabricatorController', 'PhabricatorDefaultFileStorageEngineSelector' => 'PhabricatorFileStorageEngineSelector', 'PhabricatorDefaultSearchEngineSelector' => 'PhabricatorSearchEngineSelector', diff --git a/src/applications/daemon/management/PhabricatorDaemonManagementDebugWorkflow.php b/src/applications/daemon/management/PhabricatorDaemonManagementDebugWorkflow.php new file mode 100644 index 0000000000..7b661bd5c8 --- /dev/null +++ b/src/applications/daemon/management/PhabricatorDaemonManagementDebugWorkflow.php @@ -0,0 +1,35 @@ +setName('debug') + ->setSynopsis(pht('Show a list of available daemons.')) + ->setArguments( + array( + array( + 'name' => 'argv', + 'wildcard' => true, + ), + )); + } + + public function execute(PhutilArgumentParser $args) { + $argv = $args->getArg('argv'); + + 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); + } + +} diff --git a/src/applications/daemon/management/PhabricatorDaemonManagementLaunchWorkflow.php b/src/applications/daemon/management/PhabricatorDaemonManagementLaunchWorkflow.php new file mode 100644 index 0000000000..5991739e7b --- /dev/null +++ b/src/applications/daemon/management/PhabricatorDaemonManagementLaunchWorkflow.php @@ -0,0 +1,54 @@ +setName('launch') + ->setSynopsis(pht('Show a list of available daemons.')) + ->setArguments( + array( + array( + 'name' => 'argv', + 'wildcard' => true, + ), + )); + } + + public function execute(PhutilArgumentParser $args) { + $argv = $args->getArg('argv'); + + $daemon_count = 1; + if ($argv) { + if (is_numeric(head($argv))) { + $daemon_count = array_shift($argv); + } + + if ($daemon_count < 1) { + throw new PhutilArgumentUsageException( + pht('You must launch at least one daemon.')); + } + } + + if (!$argv) { + throw new PhutilArgumentUsageException( + pht('You must specify which daemon to launch.')); + } + + $daemon_class = array_shift($argv); + + $this->willLaunchDaemons(); + + for ($ii = 0; $ii < $daemon_count; $ii++) { + $this->launchDaemon($daemon_class, $argv, $is_debug = false); + } + + return 0; + } + +} diff --git a/src/applications/daemon/management/PhabricatorDaemonManagementListWorkflow.php b/src/applications/daemon/management/PhabricatorDaemonManagementListWorkflow.php new file mode 100644 index 0000000000..56263f32c4 --- /dev/null +++ b/src/applications/daemon/management/PhabricatorDaemonManagementListWorkflow.php @@ -0,0 +1,31 @@ +setName('list') + ->setSynopsis(pht('Show a list of available daemons.')) + ->setArguments(array()); + } + + public function execute(PhutilArgumentParser $args) { + $console = PhutilConsole::getConsole(); + + $symbols = $this->loadAvailableDaemonClasses(); + $symbols = igroup($symbols, 'library'); + + foreach ($symbols as $library => $symbol_list) { + $console->writeOut(pht("Daemons in library __%s__:", $library)."\n"); + foreach ($symbol_list as $symbol) { + $console->writeOut(" %s\n", $symbol['name']); + } + $console->writeOut("\n"); + } + + return 0; + } + + +} diff --git a/src/applications/daemon/management/PhabricatorDaemonManagementRestartWorkflow.php b/src/applications/daemon/management/PhabricatorDaemonManagementRestartWorkflow.php new file mode 100644 index 0000000000..052770a12f --- /dev/null +++ b/src/applications/daemon/management/PhabricatorDaemonManagementRestartWorkflow.php @@ -0,0 +1,23 @@ +setName('restart') + ->setSynopsis( + pht( + 'Stop, then start the standard daemon loadout.')) + ->setArguments(array()); + } + + public function execute(PhutilArgumentParser $args) { + $err = $this->executeStopCommand(array()); + if ($err) { + return $err; + } + return $this->executeStartCommand(); + } + +} diff --git a/src/applications/daemon/management/PhabricatorDaemonManagementStartWorkflow.php b/src/applications/daemon/management/PhabricatorDaemonManagementStartWorkflow.php new file mode 100644 index 0000000000..a76989aeba --- /dev/null +++ b/src/applications/daemon/management/PhabricatorDaemonManagementStartWorkflow.php @@ -0,0 +1,21 @@ +setName('start') + ->setSynopsis( + pht( + 'Start the standard configured collection of Phabricator daemons. '. + 'This is appropriate for most installs. Use **phd launch** to '. + 'customize which daemons are launched.')) + ->setArguments(array()); + } + + public function execute(PhutilArgumentParser $args) { + return $this->executeStartCommand(); + } + +} diff --git a/src/applications/daemon/management/PhabricatorDaemonManagementStatusWorkflow.php b/src/applications/daemon/management/PhabricatorDaemonManagementStatusWorkflow.php new file mode 100644 index 0000000000..8ba3e9777b --- /dev/null +++ b/src/applications/daemon/management/PhabricatorDaemonManagementStatusWorkflow.php @@ -0,0 +1,50 @@ +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; + printf( + "%-5s\t%-24s\t%s\n", + "PID", + "Started", + "Daemon"); + foreach ($daemons as $daemon) { + $name = $daemon->getName(); + if (!$daemon->isRunning()) { + $daemon->updateStatus(PhabricatorDaemonLog::STATUS_DEAD); + $status = 2; + $name = ' '.$name; + } + printf( + "%5s\t%-24s\t%s\n", + $daemon->getPID(), + $daemon->getEpochStarted() + ? date('M j Y, g:i:s A', $daemon->getEpochStarted()) + : null, + $name); + } + + return $status; + } + + +} diff --git a/src/applications/daemon/management/PhabricatorDaemonManagementStopWorkflow.php b/src/applications/daemon/management/PhabricatorDaemonManagementStopWorkflow.php new file mode 100644 index 0000000000..6b96664b8b --- /dev/null +++ b/src/applications/daemon/management/PhabricatorDaemonManagementStopWorkflow.php @@ -0,0 +1,27 @@ +setName('stop') + ->setSynopsis( + pht( + 'Stop all running daemons, or specific daemons identified by PIDs. '. + 'Use **phd status** to find PIDs.')) + ->setArguments( + array( + array( + 'name' => 'pids', + 'wildcard' => true, + ), + )); + } + + public function execute(PhutilArgumentParser $args) { + $pids = $args->getArg('pids'); + return $this->executeStopCommand($pids); + } + +} diff --git a/src/infrastructure/daemon/PhabricatorDaemonControl.php b/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php similarity index 52% rename from src/infrastructure/daemon/PhabricatorDaemonControl.php rename to src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php index 91f28f6bcc..67eaacb756 100644 --- a/src/infrastructure/daemon/PhabricatorDaemonControl.php +++ b/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php @@ -1,196 +1,119 @@ setAncestorClass('PhutilDaemon') + ->setConcreteOnly(true) + ->selectSymbolsWithoutLoading(); + } + + public function getPIDDirectory() { + $path = PhabricatorEnv::getEnvConfig('phd.pid-directory'); + return $this->getControlDirectory($path); + } + + 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; + } + + public function loadRunningDaemons() { + $results = array(); + + $pid_dir = $this->getPIDDirectory(); + $pid_files = Filesystem::listDirectory($pid_dir); + if (!$pid_files) { + return $results; + } + + foreach ($pid_files as $pid_file) { + $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; + } + + return $results; + } + + private function findDaemonClass($substring) { $symbols = $this->loadAvailableDaemonClasses(); - $symbols = igroup($symbols, 'library'); - - echo "\n"; - foreach ($symbols as $library => $symbol_list) { - echo phutil_console_format("Daemons in library __%s__:\n", $library); - foreach ($symbol_list as $symbol) { - echo " ".$symbol['name']."\n"; - } - echo "\n"; - } - - return 0; - } - - public function executeStatusCommand() { - $daemons = $this->loadRunningDaemons(); - - if (!$daemons) { - echo "There are no running Phabricator daemons.\n"; - return 1; - } - - $status = 0; - printf( - "%-5s\t%-24s\t%s\n", - "PID", - "Started", - "Daemon"); - foreach ($daemons as $daemon) { - $name = $daemon->getName(); - if (!$daemon->isRunning()) { - $daemon->updateStatus(PhabricatorDaemonLog::STATUS_DEAD); - $status = 2; - $name = ' '.$name; - } - printf( - "%5s\t%-24s\t%s\n", - $daemon->getPID(), - $daemon->getEpochStarted() - ? date('M j Y, g:i:s A', $daemon->getEpochStarted()) - : null, - $name); - } - - return $status; - } - - public function executeStopCommand($pids = null) { - $daemons = $this->loadRunningDaemons(); - if (!$daemons) { - echo "There are no running Phabricator daemons.\n"; - return 0; - } - - $daemons = mpull($daemons, null, 'getPID'); - - $running = array(); - if ($pids == null) { - $running = $daemons; - } else { - // We were given a PID or set of PIDs to kill. - foreach ($pids as $key => $pid) { - if (!preg_match('/^\d+$/', $pid)) { - echo "'{$pid}' is not a valid PID.\n"; - continue; - } else if (empty($daemons[$pid])) { - echo "'{$pid}' is not Phabricator-controlled PID. Not killing.\n"; - continue; + $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 { - $running[] = $daemons[$pid]; + $match[] = $symbol; } } } - if (empty($running)) { - echo "No daemons to kill.\n"; - return 0; - } - - $all_daemons = $running; - - foreach ($running as $key => $daemon) { - $pid = $daemon->getPID(); - $name = $daemon->getName(); - - echo "Stopping daemon '{$name}' ({$pid})...\n"; - if (!$daemon->isRunning()) { - echo "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()) { - echo "Daemon {$pid} exited normally.\n"; - unset($running[$key]); - } - } - if (empty($running)) { - break; - } - usleep(100000); - } while (time() < $start + 15); - - foreach ($running as $key => $daemon) { - $pid = $daemon->getPID(); - echo "KILLing daemon {$pid}.\n"; - posix_kill($pid, SIGKILL); - } - - foreach ($all_daemons as $daemon) { - if ($daemon->getPIDFile()) { - Filesystem::remove($daemon->getPIDFile()); - } + if (count($match) == 0) { + throw new PhutilArgumentUsageException( + pht( + "No daemons match '%s'! Use 'phd list' for a list of avialable ". + "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); } - public function executeHelpCommand() { - echo phutil_console_format(<<findDaemonClass($class); + $console = PhutilConsole::getConsole(); - **start** - Start the normal collection of daemons that Phabricator uses. This - is appropriate for most installs. If you want to customize what - is launched, you can use **launch** for fine-grained control. - - **restart** - Stop all running daemons, then start a standard loadout. - - **stop** [PID ...] - Stop all running daemons if no PIDs are given, or a particular - PID or set of PIDs, if they are supplied. - - **launch** [__n__] __daemon__ [argv ...] - **debug** __daemon__ [argv ...] - Start a daemon (or n copies of a daemon). - With **debug**, do not daemonize. Use this if you're having trouble - getting daemons working. - - **list** - List available daemons. - - **status** - List running daemons. This command will exit with a non-zero exit - status if any daemons are not running. - - **help** - Show this help. - -EOHELP - ); - return 1; - } - - public function pingConduit() { - // It's fairly common to have issues here, e.g. because Phabricator isn't - // running, isn't accessible, you put the domain in your hostsfile but it - // isn't available on the production host, etc. If any of this doesn't work, - // conduit will throw. - - $conduit = new ConduitClient(PhabricatorEnv::getURI('/api/')); - $conduit->callMethodSynchronous('conduit.ping', array()); - } - - public function launchDaemon($daemon, array $argv, $debug = false) { - $symbols = $this->loadAvailableDaemonClasses(); - $symbols = ipull($symbols, 'name', 'name'); - if (empty($symbols[$daemon])) { - throw new Exception( - "Daemon '{$daemon}' is not loaded, misspelled or abstract."); + if ($debug) { + $console->writeOut( + pht( + 'Launching daemon "%s" in debug mode (not daemonized).', + $daemon)."\n"); + } else { + $console->writeOut( + pht( + 'Launching daemon "%s".', + $daemon)."\n"); } - $libphutil_root = dirname(phutil_get_library_root('phutil')); - $launch_daemon = $libphutil_root.'/scripts/daemon/'; - foreach ($argv as $key => $arg) { $argv[$key] = escapeshellarg($arg); } @@ -242,20 +165,23 @@ EOHELP implode(' ', $flags), implode(' ', $argv)); + $libphutil_root = dirname(phutil_get_library_root('phutil')); + $daemon_script_dir = $libphutil_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('PhabricatorDaemonControl', 'ignoreSignal')); + array(__CLASS__, 'ignoreSignal')); echo "\n libphutil/scripts/daemon/ \$ {$command}\n\n"; - phutil_passthru('(cd %s && exec %C)', $launch_daemon, $command); + 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($launch_daemon); + $future->setCWD($daemon_script_dir); $future->resolvex(); } } @@ -264,64 +190,167 @@ EOHELP return; } - 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."); + 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); } } - return $path; } - public function getPIDDirectory() { - $path = PhabricatorEnv::getEnvConfig('phd.pid-directory'); - return $this->getControlDirectory($path); + 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"); } - public function getLogDirectory() { - $path = PhabricatorEnv::getEnvConfig('phd.log-directory'); - return $this->getControlDirectory($path); - } - protected function loadAvailableDaemonClasses() { - $loader = new PhutilSymbolLoader(); - return $loader - ->setAncestorClass('PhutilDaemon') - ->setConcreteOnly(true) - ->selectSymbolsWithoutLoading(); - } +/* -( Commands )----------------------------------------------------------- */ - public function loadRunningDaemons() { - $results = array(); - $pid_dir = $this->getPIDDirectory(); - $pid_files = Filesystem::listDirectory($pid_dir); - if (!$pid_files) { - return $results; - } + protected function executeStartCommand() { + $console = PhutilConsole::getConsole(); - foreach ($pid_files as $pid_file) { - $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(); + $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); } - $ref = PhabricatorDaemonReference::newFromDictionary($dict); - $ref->setPIDFile($pid_dir.'/'.$pid_file); - $results[] = $ref; } - return $results; + $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 function killDaemon(PhabricatorDaemonReference $ref) { + + 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; } } diff --git a/src/applications/repository/controller/PhabricatorRepositoryController.php b/src/applications/repository/controller/PhabricatorRepositoryController.php index cf4f986d60..20b33dc924 100644 --- a/src/applications/repository/controller/PhabricatorRepositoryController.php +++ b/src/applications/repository/controller/PhabricatorRepositoryController.php @@ -22,7 +22,8 @@ abstract class PhabricatorRepositoryController extends PhabricatorController { } private function isPullDaemonRunning() { - $control = new PhabricatorDaemonControl(); + // TODO: This is yuck, fix it. + $control = new PhabricatorDaemonManagementListWorkflow(); $daemons = $control->loadRunningDaemons(); foreach ($daemons as $daemon) { if ($daemon->isRunning() &&