@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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