PageRenderTime 53ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php

http://github.com/facebook/phabricator
PHP | 611 lines | 487 code | 100 blank | 24 comment | 61 complexity | 1be5f88f442ec96a273e05b2f58ebacf MD5 | raw file
Possible License(s): JSON, MPL-2.0-no-copyleft-exception, Apache-2.0, BSD-3-Clause, LGPL-2.0, MIT, LGPL-2.1, LGPL-3.0
  1. <?php
  2. abstract class PhabricatorDaemonManagementWorkflow
  3. extends PhabricatorManagementWorkflow {
  4. private $runDaemonsAsUser = null;
  5. final protected function loadAvailableDaemonClasses() {
  6. return id(new PhutilSymbolLoader())
  7. ->setAncestorClass('PhutilDaemon')
  8. ->setConcreteOnly(true)
  9. ->selectSymbolsWithoutLoading();
  10. }
  11. final protected function getLogDirectory() {
  12. $path = PhabricatorEnv::getEnvConfig('phd.log-directory');
  13. return $this->getControlDirectory($path);
  14. }
  15. private function getControlDirectory($path) {
  16. if (!Filesystem::pathExists($path)) {
  17. list($err) = exec_manual('mkdir -p %s', $path);
  18. if ($err) {
  19. throw new Exception(
  20. pht(
  21. "%s requires the directory '%s' to exist, but it does not exist ".
  22. "and could not be created. Create this directory or update ".
  23. "'%s' in your configuration to point to an existing ".
  24. "directory.",
  25. 'phd',
  26. $path,
  27. 'phd.log-directory'));
  28. }
  29. }
  30. return $path;
  31. }
  32. private function findDaemonClass($substring) {
  33. $symbols = $this->loadAvailableDaemonClasses();
  34. $symbols = ipull($symbols, 'name');
  35. $match = array();
  36. foreach ($symbols as $symbol) {
  37. if (stripos($symbol, $substring) !== false) {
  38. if (strtolower($symbol) == strtolower($substring)) {
  39. $match = array($symbol);
  40. break;
  41. } else {
  42. $match[] = $symbol;
  43. }
  44. }
  45. }
  46. if (count($match) == 0) {
  47. throw new PhutilArgumentUsageException(
  48. pht(
  49. "No daemons match '%s'! Use '%s' for a list of available daemons.",
  50. $substring,
  51. 'phd list'));
  52. } else if (count($match) > 1) {
  53. throw new PhutilArgumentUsageException(
  54. pht(
  55. "Specify a daemon unambiguously. Multiple daemons match '%s': %s.",
  56. $substring,
  57. implode(', ', $match)));
  58. }
  59. return head($match);
  60. }
  61. final protected function launchDaemons(
  62. array $daemons,
  63. $debug,
  64. $run_as_current_user = false) {
  65. // Convert any shorthand classnames like "taskmaster" into proper class
  66. // names.
  67. foreach ($daemons as $key => $daemon) {
  68. $class = $this->findDaemonClass($daemon['class']);
  69. $daemons[$key]['class'] = $class;
  70. }
  71. $console = PhutilConsole::getConsole();
  72. if (!$run_as_current_user) {
  73. // Check if the script is started as the correct user
  74. $phd_user = PhabricatorEnv::getEnvConfig('phd.user');
  75. $current_user = posix_getpwuid(posix_geteuid());
  76. $current_user = $current_user['name'];
  77. if ($phd_user && $phd_user != $current_user) {
  78. if ($debug) {
  79. throw new PhutilArgumentUsageException(
  80. pht(
  81. "You are trying to run a daemon as a nonstandard user, ".
  82. "and `%s` was not able to `%s` to the correct user. \n".
  83. 'Phabricator is configured to run daemons as "%s", '.
  84. 'but the current user is "%s". '."\n".
  85. 'Use `%s` to run as a different user, pass `%s` to ignore this '.
  86. 'warning, or edit `%s` to change the configuration.',
  87. 'phd',
  88. 'sudo',
  89. $phd_user,
  90. $current_user,
  91. 'sudo',
  92. '--as-current-user',
  93. 'phd.user'));
  94. } else {
  95. $this->runDaemonsAsUser = $phd_user;
  96. $console->writeOut(pht('Starting daemons as %s', $phd_user)."\n");
  97. }
  98. }
  99. }
  100. $this->printLaunchingDaemons($daemons, $debug);
  101. $trace = PhutilArgumentParser::isTraceModeEnabled();
  102. $flags = array();
  103. if ($trace || PhabricatorEnv::getEnvConfig('phd.trace')) {
  104. $flags[] = '--trace';
  105. }
  106. if ($debug || PhabricatorEnv::getEnvConfig('phd.verbose')) {
  107. $flags[] = '--verbose';
  108. }
  109. $instance = $this->getInstance();
  110. if ($instance) {
  111. $flags[] = '-l';
  112. $flags[] = $instance;
  113. }
  114. $config = array();
  115. if (!$debug) {
  116. $config['daemonize'] = true;
  117. }
  118. if (!$debug) {
  119. $config['log'] = $this->getLogDirectory().'/daemons.log';
  120. }
  121. $config['daemons'] = $daemons;
  122. $command = csprintf('./phd-daemon %Ls', $flags);
  123. $phabricator_root = dirname(phutil_get_library_root('phabricator'));
  124. $daemon_script_dir = $phabricator_root.'/scripts/daemon/';
  125. if ($debug) {
  126. // Don't terminate when the user sends ^C; it will be sent to the
  127. // subprocess which will terminate normally.
  128. pcntl_signal(
  129. SIGINT,
  130. array(__CLASS__, 'ignoreSignal'));
  131. echo "\n phabricator/scripts/daemon/ \$ {$command}\n\n";
  132. $tempfile = new TempFile('daemon.config');
  133. Filesystem::writeFile($tempfile, json_encode($config));
  134. phutil_passthru(
  135. '(cd %s && exec %C < %s)',
  136. $daemon_script_dir,
  137. $command,
  138. $tempfile);
  139. } else {
  140. try {
  141. $this->executeDaemonLaunchCommand(
  142. $command,
  143. $daemon_script_dir,
  144. $config,
  145. $this->runDaemonsAsUser);
  146. } catch (Exception $ex) {
  147. throw new PhutilArgumentUsageException(
  148. pht(
  149. 'Daemons are configured to run as user "%s" in configuration '.
  150. 'option `%s`, but the current user is "%s" and `phd` was unable '.
  151. 'to switch to the correct user with `sudo`. Command output:'.
  152. "\n\n".
  153. '%s',
  154. $phd_user,
  155. 'phd.user',
  156. $current_user,
  157. $ex->getMessage()));
  158. }
  159. }
  160. }
  161. private function executeDaemonLaunchCommand(
  162. $command,
  163. $daemon_script_dir,
  164. array $config,
  165. $run_as_user = null) {
  166. $is_sudo = false;
  167. if ($run_as_user) {
  168. // If anything else besides sudo should be
  169. // supported then insert it here (runuser, su, ...)
  170. $command = csprintf(
  171. 'sudo -En -u %s -- %C',
  172. $run_as_user,
  173. $command);
  174. $is_sudo = true;
  175. }
  176. $future = new ExecFuture('exec %C', $command);
  177. // Play games to keep 'ps' looking reasonable.
  178. $future->setCWD($daemon_script_dir);
  179. $future->write(json_encode($config));
  180. list($stdout, $stderr) = $future->resolvex();
  181. if ($is_sudo) {
  182. // On OSX, `sudo -n` exits 0 when the user does not have permission to
  183. // switch accounts without a password. This is not consistent with
  184. // sudo on Linux, and seems buggy/broken. Check for this by string
  185. // matching the output.
  186. if (preg_match('/sudo: a password is required/', $stderr)) {
  187. throw new Exception(
  188. pht(
  189. '%s exited with a zero exit code, but emitted output '.
  190. 'consistent with failure under OSX.',
  191. 'sudo'));
  192. }
  193. }
  194. }
  195. public static function ignoreSignal($signo) {
  196. return;
  197. }
  198. public static function requireExtensions() {
  199. self::mustHaveExtension('pcntl');
  200. self::mustHaveExtension('posix');
  201. }
  202. private static function mustHaveExtension($ext) {
  203. if (!extension_loaded($ext)) {
  204. echo pht(
  205. "ERROR: The PHP extension '%s' is not installed. You must ".
  206. "install it to run daemons on this machine.\n",
  207. $ext);
  208. exit(1);
  209. }
  210. $extension = new ReflectionExtension($ext);
  211. foreach ($extension->getFunctions() as $function) {
  212. $function = $function->name;
  213. if (!function_exists($function)) {
  214. echo pht(
  215. "ERROR: The PHP function %s is disabled. You must ".
  216. "enable it to run daemons on this machine.\n",
  217. $function.'()');
  218. exit(1);
  219. }
  220. }
  221. }
  222. /* -( Commands )----------------------------------------------------------- */
  223. final protected function executeStartCommand(array $options) {
  224. PhutilTypeSpec::checkMap(
  225. $options,
  226. array(
  227. 'keep-leases' => 'optional bool',
  228. 'force' => 'optional bool',
  229. 'reserve' => 'optional float',
  230. ));
  231. $console = PhutilConsole::getConsole();
  232. if (!idx($options, 'force')) {
  233. $process_refs = $this->getOverseerProcessRefs();
  234. if ($process_refs) {
  235. $this->logWarn(
  236. pht('RUNNING DAEMONS'),
  237. pht('Daemons are already running:'));
  238. fprintf(STDERR, '%s', "\n");
  239. foreach ($process_refs as $process_ref) {
  240. fprintf(
  241. STDERR,
  242. '%s',
  243. tsprintf(
  244. " %s %s\n",
  245. $process_ref->getPID(),
  246. $process_ref->getCommand()));
  247. }
  248. fprintf(STDERR, '%s', "\n");
  249. $this->logFail(
  250. pht('RUNNING DAEMONS'),
  251. pht(
  252. 'Use "phd stop" to stop daemons, "phd restart" to restart '.
  253. 'daemons, or "phd start --force" to ignore running processes.'));
  254. exit(1);
  255. }
  256. }
  257. if (idx($options, 'keep-leases')) {
  258. $console->writeErr("%s\n", pht('Not touching active task queue leases.'));
  259. } else {
  260. $console->writeErr("%s\n", pht('Freeing active task leases...'));
  261. $count = $this->freeActiveLeases();
  262. $console->writeErr(
  263. "%s\n",
  264. pht('Freed %s task lease(s).', new PhutilNumber($count)));
  265. }
  266. $daemons = array(
  267. array(
  268. 'class' => 'PhabricatorRepositoryPullLocalDaemon',
  269. 'label' => 'pull',
  270. ),
  271. array(
  272. 'class' => 'PhabricatorTriggerDaemon',
  273. 'label' => 'trigger',
  274. ),
  275. array(
  276. 'class' => 'PhabricatorFactDaemon',
  277. 'label' => 'fact',
  278. ),
  279. array(
  280. 'class' => 'PhabricatorTaskmasterDaemon',
  281. 'label' => 'task',
  282. 'pool' => PhabricatorEnv::getEnvConfig('phd.taskmasters'),
  283. 'reserve' => idx($options, 'reserve', 0),
  284. ),
  285. );
  286. $this->launchDaemons($daemons, $is_debug = false);
  287. $console->writeErr("%s\n", pht('Done.'));
  288. return 0;
  289. }
  290. final protected function executeStopCommand(array $options) {
  291. $grace_period = idx($options, 'graceful', 15);
  292. $force = idx($options, 'force');
  293. $query = id(new PhutilProcessQuery())
  294. ->withIsOverseer(true);
  295. $instance = $this->getInstance();
  296. if ($instance !== null && !$force) {
  297. $query->withInstances(array($instance));
  298. }
  299. try {
  300. $process_refs = $query->execute();
  301. } catch (Exception $ex) {
  302. // See T13321. If this fails for some reason, just continue for now so
  303. // that daemon management still works. In the long run, we don't expect
  304. // this to fail, but I don't want to break this workflow while we iron
  305. // bugs out.
  306. // See T12827. Particularly, this is likely to fail on Solaris.
  307. phlog($ex);
  308. $process_refs = array();
  309. }
  310. if (!$process_refs) {
  311. if ($instance !== null && !$force) {
  312. $this->logInfo(
  313. pht('NO DAEMONS'),
  314. pht(
  315. 'There are no running daemons for the current instance ("%s"). '.
  316. 'Use "--force" to stop daemons for all instances.',
  317. $instance));
  318. } else {
  319. $this->logInfo(
  320. pht('NO DAEMONS'),
  321. pht('There are no running daemons.'));
  322. }
  323. return 0;
  324. }
  325. $process_refs = mpull($process_refs, null, 'getPID');
  326. $stop_pids = array_keys($process_refs);
  327. $live_pids = $this->sendStopSignals($stop_pids, $grace_period);
  328. $stop_pids = array_fuse($stop_pids);
  329. $live_pids = array_fuse($live_pids);
  330. $dead_pids = array_diff_key($stop_pids, $live_pids);
  331. foreach ($dead_pids as $dead_pid) {
  332. $dead_ref = $process_refs[$dead_pid];
  333. $this->logOkay(
  334. pht('STOP'),
  335. pht(
  336. 'Stopped PID %d ("%s")',
  337. $dead_pid,
  338. $dead_ref->getCommand()));
  339. }
  340. foreach ($live_pids as $live_pid) {
  341. $live_ref = $process_refs[$live_pid];
  342. $this->logFail(
  343. pht('SURVIVED'),
  344. pht(
  345. 'Unable to stop PID %d ("%s").',
  346. $live_pid,
  347. $live_ref->getCommand()));
  348. }
  349. if ($live_pids) {
  350. $this->logWarn(
  351. pht('SURVIVORS'),
  352. pht(
  353. 'Unable to stop all daemon processes. You may need to run this '.
  354. 'command as root with "sudo".'));
  355. }
  356. return 0;
  357. }
  358. final protected function executeReloadCommand(array $pids) {
  359. $process_refs = $this->getOverseerProcessRefs();
  360. if (!$process_refs) {
  361. $this->logInfo(
  362. pht('NO DAEMONS'),
  363. pht('There are no running daemon processes to reload.'));
  364. return 0;
  365. }
  366. foreach ($process_refs as $process_ref) {
  367. $pid = $process_ref->getPID();
  368. $this->logInfo(
  369. pht('RELOAD'),
  370. pht('Reloading process %d...', $pid));
  371. posix_kill($pid, SIGHUP);
  372. }
  373. return 0;
  374. }
  375. private function sendStopSignals($pids, $grace_period) {
  376. // If we're doing a graceful shutdown, try SIGINT first.
  377. if ($grace_period) {
  378. $pids = $this->sendSignal($pids, SIGINT, $grace_period);
  379. }
  380. // If we still have daemons, SIGTERM them.
  381. if ($pids) {
  382. $pids = $this->sendSignal($pids, SIGTERM, 15);
  383. }
  384. // If the overseer is still alive, SIGKILL it.
  385. if ($pids) {
  386. $pids = $this->sendSignal($pids, SIGKILL, 0);
  387. }
  388. return $pids;
  389. }
  390. private function sendSignal(array $pids, $signo, $wait) {
  391. $console = PhutilConsole::getConsole();
  392. $pids = array_fuse($pids);
  393. foreach ($pids as $key => $pid) {
  394. if (!$pid) {
  395. // NOTE: We must have a PID to signal a daemon, since sending a signal
  396. // to PID 0 kills this process.
  397. unset($pids[$key]);
  398. continue;
  399. }
  400. switch ($signo) {
  401. case SIGINT:
  402. $message = pht('Interrupting process %d...', $pid);
  403. break;
  404. case SIGTERM:
  405. $message = pht('Terminating process %d...', $pid);
  406. break;
  407. case SIGKILL:
  408. $message = pht('Killing process %d...', $pid);
  409. break;
  410. }
  411. $console->writeOut("%s\n", $message);
  412. posix_kill($pid, $signo);
  413. }
  414. if ($wait) {
  415. $start = PhabricatorTime::getNow();
  416. do {
  417. foreach ($pids as $key => $pid) {
  418. if (!PhabricatorDaemonReference::isProcessRunning($pid)) {
  419. $console->writeOut(pht('Process %d exited.', $pid)."\n");
  420. unset($pids[$key]);
  421. }
  422. }
  423. if (empty($pids)) {
  424. break;
  425. }
  426. usleep(100000);
  427. } while (PhabricatorTime::getNow() < $start + $wait);
  428. }
  429. return $pids;
  430. }
  431. private function freeActiveLeases() {
  432. $task_table = id(new PhabricatorWorkerActiveTask());
  433. $conn_w = $task_table->establishConnection('w');
  434. queryfx(
  435. $conn_w,
  436. 'UPDATE %T SET leaseExpires = UNIX_TIMESTAMP()
  437. WHERE leaseExpires > UNIX_TIMESTAMP()',
  438. $task_table->getTableName());
  439. return $conn_w->getAffectedRows();
  440. }
  441. private function printLaunchingDaemons(array $daemons, $debug) {
  442. $console = PhutilConsole::getConsole();
  443. if ($debug) {
  444. $console->writeOut(pht('Launching daemons (in debug mode):'));
  445. } else {
  446. $console->writeOut(pht('Launching daemons:'));
  447. }
  448. $log_dir = $this->getLogDirectory().'/daemons.log';
  449. $console->writeOut(
  450. "\n%s\n\n",
  451. pht('(Logs will appear in "%s".)', $log_dir));
  452. foreach ($daemons as $daemon) {
  453. $pool_size = pht('(Pool: %s)', idx($daemon, 'pool', 1));
  454. $console->writeOut(
  455. " %s %s\n",
  456. $pool_size,
  457. $daemon['class'],
  458. implode(' ', idx($daemon, 'argv', array())));
  459. }
  460. $console->writeOut("\n");
  461. }
  462. protected function getAutoscaleReserveArgument() {
  463. return array(
  464. 'name' => 'autoscale-reserve',
  465. 'param' => 'ratio',
  466. 'help' => pht(
  467. 'Specify a proportion of machine memory which must be free '.
  468. 'before autoscale pools will grow. For example, a value of 0.25 '.
  469. 'means that pools will not grow unless the machine has at least '.
  470. '25%%%% of its RAM free.'),
  471. );
  472. }
  473. private function selectDaemonPIDs(array $daemons, array $pids) {
  474. $console = PhutilConsole::getConsole();
  475. $running_pids = array_fuse(mpull($daemons, 'getPID'));
  476. if (!$pids) {
  477. $select_pids = $running_pids;
  478. } else {
  479. // We were given a PID or set of PIDs to kill.
  480. $select_pids = array();
  481. foreach ($pids as $key => $pid) {
  482. if (!preg_match('/^\d+$/', $pid)) {
  483. $console->writeErr(pht("PID '%s' is not a valid PID.", $pid)."\n");
  484. continue;
  485. } else if (empty($running_pids[$pid])) {
  486. $console->writeErr(
  487. "%s\n",
  488. pht(
  489. 'PID "%d" is not a known Phabricator daemon PID.',
  490. $pid));
  491. continue;
  492. } else {
  493. $select_pids[$pid] = $pid;
  494. }
  495. }
  496. }
  497. return $select_pids;
  498. }
  499. protected function getOverseerProcessRefs() {
  500. $query = id(new PhutilProcessQuery())
  501. ->withIsOverseer(true);
  502. $instance = PhabricatorEnv::getEnvConfig('cluster.instance');
  503. if ($instance !== null) {
  504. $query->withInstances(array($instance));
  505. }
  506. return $query->execute();
  507. }
  508. protected function getInstance() {
  509. return PhabricatorEnv::getEnvConfig('cluster.instance');
  510. }
  511. }