diff --git a/scripts/daemon/phabricator_daemon_launcher.php b/scripts/daemon/phabricator_daemon_launcher.php index 671be82fe..2d1b528ef 100755 --- a/scripts/daemon/phabricator_daemon_launcher.php +++ b/scripts/daemon/phabricator_daemon_launcher.php @@ -1,201 +1,212 @@ #!/usr/bin/env php <?php /* * Copyright 2011 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ $root = dirname(dirname(dirname(__FILE__))); require_once $root.'/scripts/__init_script__.php'; require_once $root.'/scripts/__init_env__.php'; phutil_require_module('phabricator', 'infrastructure/daemon/control'); $control = new PhabricatorDaemonControl(); switch (isset($argv[1]) ? $argv[1] : 'help') { case 'list': $err = $control->executeListCommand(); exit($err); case 'status': $err = $control->executeStatusCommand(); exit($err); case 'stop': $err = $control->executeStopCommand(); exit($err); case 'repository-launch-readonly': $need_launch = phd_load_tracked_repositories_of_type('git'); if (!$need_launch) { echo "There are no repositories with tracking enabled.\n"; } else { foreach ($need_launch as $repository) { $name = $repository->getName(); $callsign = $repository->getCallsign(); $desc = "'{$name}' ({$callsign})"; $phid = $repository->getPHID(); echo "Launching 'git fetch' daemon on the {$desc} repository...\n"; $control->launchDaemon( 'PhabricatorRepositoryGitFetchDaemon', array( $phid, )); } } break; case 'repository-launch-master': $need_launch = phd_load_tracked_repositories(); if (!$need_launch) { echo "There are no repositories with tracking enabled.\n"; } else { foreach ($need_launch as $repository) { $name = $repository->getName(); $callsign = $repository->getCallsign(); $desc = "'{$name}' ({$callsign})"; $phid = $repository->getPHID(); switch ($repository->getVersionControlSystem()) { case 'git': echo "Launching 'git fetch' daemon on the {$desc} repository...\n"; $control->launchDaemon( 'PhabricatorRepositoryGitFetchDaemon', array( $phid, )); echo "Launching discovery daemon on the {$desc} repository...\n"; $control->launchDaemon( 'PhabricatorRepositoryGitCommitDiscoveryDaemon', array( $phid, )); break; case 'svn': echo "Launching discovery daemon on the {$desc} repository...\n"; $control->launchDaemon( 'PhabricatorRepositorySvnCommitDiscoveryDaemon', array( $phid, )); break; } } echo "Launching CommitTask daemon...\n"; $control->launchDaemon( 'PhabricatorRepositoryCommitTaskDaemon', array()); 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_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!"); + 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); } - $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); } - echo "Launching {$n} x {$daemon}"; + 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); - echo "."; + $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 phd_load_tracked_repositories_of_type($type) { $repositories = phd_load_tracked_repositories(); foreach ($repositories as $key => $repository) { if ($repository->getVersionControlSystem() != $type) { unset($repositories[$key]); } } return $repositories; } function phd_load_tracked_repositories() { phutil_require_module( 'phabricator', 'applications/repository/storage/repository'); $repositories = id(new PhabricatorRepository())->loadAll(); foreach ($repositories as $key => $repository) { if (!$repository->getDetail('tracking-enabled')) { unset($repositories[$key]); } } return $repositories; } diff --git a/src/docs/managing_daemons.diviner b/src/docs/managing_daemons.diviner index 5fc2cec87..92c7d2ae7 100644 --- a/src/docs/managing_daemons.diviner +++ b/src/docs/managing_daemons.diviner @@ -1,67 +1,72 @@ @title Managing Daemons with phd @group config Explains Phabricator daemons and the daemon control program ##phd##. = Overview = Phabricator uses daemons (background processing scripts) to handle a number of tasks, like: - tracking repositories and discovering new commits; - sending mail; - updating objects in the search index; and - custom tasks you define. Daemons are started and stopped with **phd** (the **Ph**abricator **D**aemon launcher). Daemons can be monitored via a web console. You do not need to run daemons for most parts of Phabricator to work, but a few features (principally, repository tracking with Diffusion) require them and several features will benefit in performance or stability if you configure daemons. = phd = **phd** is a command-line script (located at ##phabricator/bin/phd##). To get a list of commands, run ##phd help##: phabricator/ $ ./bin/phd help NAME phd - phabricator daemon launcher ... Generally, you will use: - **phd launch** to launch daemons; + - **phd debug** to debug problems with daemons; - **phd status** to get a list of running daemons; and - **phd stop** to stop all daemons. NOTE: When you upgrade Phabricator or change configuration, you should restart the daemons by stopping and relaunching them. NOTE: When you **launch** a daemon, you can type any unique substring of its name, so **phd launch metamta** will work correctly. = Daemon Console = You can view status and debugging information for daemons in the Daemon Console via the web interface. Go to ##/daemon/## in your install or click **Daemon Console** from the homepage. The Daemon Console shows a list of all the daemons that have ever launched, and allows you to view log information for them. If you have issues with daemons, you may be able to find error information that will help you resolve the problem in the console. +NOTE: The easiest way to figure out what's wrong with a daemon is usually to use +**phd debug** to launch it instead of **phd launch**. This will run it without +daemonizing it, so you can see output in your console. + = Available Daemons = You can get a list of launchable daemons with **phd list**: - **libphutil test daemons** are not generally useful unless you are developing daemon infrastructure or debugging a daemon problem; - **PhabricatorMetaMTADaemon** sends mail in the background, see @{article:Configuring Outbound Email} for details; - **PhabricatorTaskmasterDaemon** runs a generic task queue; and - **PhabricatorRepository** daemons track repositories, descriptions are available in the @{article:Diffusion User Guide}. diff --git a/src/infrastructure/daemon/control/PhabricatorDaemonControl.php b/src/infrastructure/daemon/control/PhabricatorDaemonControl.php index cff0abf72..4fab8ddb4 100644 --- a/src/infrastructure/daemon/control/PhabricatorDaemonControl.php +++ b/src/infrastructure/daemon/control/PhabricatorDaemonControl.php @@ -1,270 +1,289 @@ <?php /* * Copyright 2011 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ final class PhabricatorDaemonControl { public function executeListCommand() { $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 0; } printf( "%-5s\t%-24s\t%s\n", "PID", "Started", "Daemon"); foreach ($daemons as $daemon) { $name = $daemon->getName(); if (!$daemon->isRunning()) { $name = '<DEAD> '.$name; if ($daemon->getPIDFile()) { Filesystem::remove($daemon->getPIDFile()); } } printf( "%5s\t%-24s\t%s\n", $daemon->getPID(), $daemon->getEpochStarted() ? date('M j Y, g:i:s A', $daemon->getEpochStarted()) : null, $name); } return 0; } public function executeStopCommand() { $daemons = $this->loadRunningDaemons(); if (!$daemons) { echo "There are no running Phabricator daemons.\n"; return 0; } $running = $daemons; 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]); } 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 ($daemons as $daemon) { if ($daemon->getPIDFile()) { Filesystem::remove($daemon->getPIDFile()); } } } public function executeHelpCommand() { echo phutil_console_format(<<<EOHELP **NAME** **phd** - phabricator daemon launcher **COMMAND REFERENCE** **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. **stop** Stop all daemons. **status** List running daemons. **stop** Stop all running daemons. **help** Show this help. **repository-launch-master** Launches daemons to update and parse all tracked repositories. You must also launch Taskmaster daemons, either on the same machine or elsewhere. You should launch a master only one machine. For other machines, launch a 'readonly'. **repository-launch-readonly** Launches daemons to 'git pull' tracked git repositories so they stay up to date. EOHELP ); return 1; } - public function launchDaemon($daemon, array $argv) { + public function launchDaemon($daemon, array $argv, $debug) { $symbols = $this->loadAvailableDaemonClasses(); $symbols = ipull($symbols, 'name', 'name'); if (empty($symbols[$daemon])) { throw new Exception("Daemon '{$daemon}' is not known."); } $pid_dir = $this->getControlDirectory('pid'); $libphutil_root = dirname(phutil_get_library_root('phutil')); $launch_daemon = $libphutil_root.'/scripts/daemon/'; // TODO: This should be a much better user experience. Filesystem::assertExists($pid_dir); Filesystem::assertIsDirectory($pid_dir); Filesystem::assertWritable($pid_dir); foreach ($argv as $key => $arg) { $argv[$key] = escapeshellarg($arg); } $bootloader = PhutilBootloader::getInstance(); $all_libraries = $bootloader->getAllLibraries(); $non_default_libraries = array_diff( $all_libraries, array('phutil', 'phabricator')); $extra_libraries = array(); foreach ($non_default_libraries as $library) { $extra_libraries[] = csprintf( '--load-phutil-library=%s', phutil_get_library_root($library)); } - $future = new ExecFuture( + $command = csprintf( "./launch_daemon.php ". "%s ". "--load-phutil-library=%s ". implode(' ', $extra_libraries)." ". "--conduit-uri=%s ". - "--daemonize ". "--phd=%s ". + ($debug ? '--trace ' : '--daemonize '). implode(' ', $argv), $daemon, phutil_get_library_root('phabricator'), PhabricatorEnv::getURI('/api/'), $pid_dir); - // Play games to keep 'ps' looking reasonable. - $future->setCWD($launch_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')); + + echo "\n libphutil/scripts/daemon/ \$ {$command}\n\n"; + + phutil_passthru('(cd %s && exec %C)', $launch_daemon, $command); + } else { + $future = new ExecFuture('exec %C', $command); + // Play games to keep 'ps' looking reasonable. + $future->setCWD($launch_daemon); + $future->resolvex(); + } + } - $future->resolvex(); + public static function ignoreSignal($signo) { + return; } protected function getControlDirectory($dir) { $path = PhabricatorEnv::getEnvConfig('phd.pid-directory').'/'.$dir; 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' in your configuration to point to an existing ". "directory."); } } return $path; } protected function loadAvailableDaemonClasses() { $loader = new PhutilSymbolLoader(); return $loader ->setAncestorClass('PhutilDaemon') ->selectSymbolsWithoutLoading(); } protected function loadRunningDaemons() { $results = array(); $pid_dir = $this->getControlDirectory('pid'); $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; } protected function killDaemon(PhabricatorDaemonReference $ref) { } }