@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 579 lines 16 kB view raw
1<?php 2 3abstract class PhabricatorAphlictManagementWorkflow 4 extends PhabricatorManagementWorkflow { 5 6 private $debug = false; 7 private $configData; 8 private $configPath; 9 10 final protected function setDebug($debug) { 11 $this->debug = $debug; 12 return $this; 13 } 14 15 protected function getLaunchArguments() { 16 return array( 17 array( 18 'name' => 'config', 19 'param' => 'file', 20 'help' => pht( 21 'Use a specific configuration file instead of the default '. 22 'configuration.'), 23 ), 24 ); 25 } 26 27 protected function parseLaunchArguments(PhutilArgumentParser $args) { 28 $config_file = $args->getArg('config'); 29 if ($config_file) { 30 $full_path = Filesystem::resolvePath($config_file); 31 $show_path = $full_path; 32 } else { 33 $root = dirname(phutil_get_library_root('phorge')); 34 35 $try = array( 36 'conf/aphlict/aphlict.custom.json', 37 'conf/aphlict/aphlict.default.json', 38 ); 39 40 foreach ($try as $config) { 41 $full_path = $root.'/'.$config; 42 $show_path = '<phorge>/'.$config; 43 if (Filesystem::pathExists($full_path)) { 44 break; 45 } 46 } 47 } 48 49 echo tsprintf( 50 "%s\n", 51 pht( 52 'Reading configuration from: %s', 53 $show_path)); 54 55 try { 56 $data = Filesystem::readFile($full_path); 57 } catch (Exception $ex) { 58 throw new PhutilArgumentUsageException( 59 pht( 60 'Failed to read configuration file. %s', 61 $ex->getMessage())); 62 } 63 64 try { 65 $data = phutil_json_decode($data); 66 } catch (Exception $ex) { 67 throw new PhutilArgumentUsageException( 68 pht( 69 'Configuration file is not properly formatted JSON. %s', 70 $ex->getMessage())); 71 } 72 73 try { 74 PhutilTypeSpec::checkMap( 75 $data, 76 array( 77 'servers' => 'list<wild>', 78 'logs' => 'optional list<wild>', 79 'cluster' => 'optional list<wild>', 80 'pidfile' => 'string', 81 'memory.hint' => 'optional int', 82 )); 83 } catch (Exception $ex) { 84 throw new PhutilArgumentUsageException( 85 pht( 86 'Configuration file has improper configuration keys at top '. 87 'level. %s', 88 $ex->getMessage())); 89 } 90 91 $servers = $data['servers']; 92 $has_client = false; 93 $has_admin = false; 94 $port_map = array(); 95 foreach ($servers as $index => $server) { 96 PhutilTypeSpec::checkMap( 97 $server, 98 array( 99 'type' => 'string', 100 'port' => 'int', 101 'listen' => 'optional string|null', 102 'ssl.key' => 'optional string|null', 103 'ssl.cert' => 'optional string|null', 104 'ssl.chain' => 'optional string|null', 105 )); 106 107 $port = $server['port']; 108 if (!isset($port_map[$port])) { 109 $port_map[$port] = $index; 110 } else { 111 throw new PhutilArgumentUsageException( 112 pht( 113 'Two servers (at indexes "%s" and "%s") both bind to the same '. 114 'port ("%s"). Each server must bind to a unique port.', 115 $port_map[$port], 116 $index, 117 $port)); 118 } 119 120 $type = $server['type']; 121 switch ($type) { 122 case 'admin': 123 $has_admin = true; 124 break; 125 case 'client': 126 $has_client = true; 127 break; 128 default: 129 throw new PhutilArgumentUsageException( 130 pht( 131 'A specified server (at index "%s", on port "%s") has an '. 132 'invalid type ("%s"). Valid types are: admin, client.', 133 $index, 134 $port, 135 $type)); 136 } 137 138 $ssl_key = idx($server, 'ssl.key'); 139 $ssl_cert = idx($server, 'ssl.cert'); 140 if (($ssl_key && !$ssl_cert) || ($ssl_cert && !$ssl_key)) { 141 throw new PhutilArgumentUsageException( 142 pht( 143 'A specified server (at index "%s", on port "%s") specifies '. 144 'only one of "%s" and "%s". Each server must specify neither '. 145 '(to disable SSL) or specify both (to enable it).', 146 $index, 147 $port, 148 'ssl.key', 149 'ssl.cert')); 150 } 151 152 $ssl_chain = idx($server, 'ssl.chain'); 153 if ($ssl_chain && (!$ssl_key && !$ssl_cert)) { 154 throw new PhutilArgumentUsageException( 155 pht( 156 'A specified server (at index "%s", on port "%s") specifies '. 157 'a value for "%s", but no value for "%s" or "%s". Servers '. 158 'should only provide an SSL chain if they also provide an SSL '. 159 'key and SSL certificate.', 160 $index, 161 $port, 162 'ssl.chain', 163 'ssl.key', 164 'ssl.cert')); 165 } 166 } 167 168 if (!$servers) { 169 throw new PhutilArgumentUsageException( 170 pht( 171 'Configuration file does not specify any servers. This service '. 172 'will not be able to interact with the outside world if it does '. 173 'not listen on any ports. You must specify at least one "%s" '. 174 'server and at least one "%s" server.', 175 'admin', 176 'client')); 177 } 178 179 if (!$has_client) { 180 throw new PhutilArgumentUsageException( 181 pht( 182 'Configuration file does not specify any client servers. This '. 183 'service will be unable to transmit any notifications without a '. 184 'client server. You must specify at least one server with '. 185 'type "%s".', 186 'client')); 187 } 188 189 if (!$has_admin) { 190 throw new PhutilArgumentUsageException( 191 pht( 192 'Configuration file does not specify any administrative '. 193 'servers. This service will be unable to receive messages. '. 194 'You must specify at least one server with type "%s".', 195 'admin')); 196 } 197 198 $logs = idx($data, 'logs', array()); 199 foreach ($logs as $index => $log) { 200 PhutilTypeSpec::checkMap( 201 $log, 202 array( 203 'path' => 'string', 204 )); 205 206 $path = $log['path']; 207 208 try { 209 $dir = dirname($path); 210 if (!Filesystem::pathExists($dir)) { 211 Filesystem::createDirectory($dir, 0755, true); 212 } 213 } catch (FilesystemException $ex) { 214 throw new PhutilArgumentUsageException( 215 pht( 216 'Failed to create directory "%s" for specified log file (with '. 217 'index "%s"). You should manually create this directory or '. 218 'choose a different logfile location. %s', 219 $dir, 220 $index, 221 $ex->getMessage())); 222 } 223 } 224 225 $peer_map = array(); 226 227 $cluster = idx($data, 'cluster', array()); 228 foreach ($cluster as $index => $peer) { 229 PhutilTypeSpec::checkMap( 230 $peer, 231 array( 232 'host' => 'string', 233 'port' => 'int', 234 'protocol' => 'string', 235 )); 236 237 $host = $peer['host']; 238 $port = $peer['port']; 239 $protocol = $peer['protocol']; 240 241 switch ($protocol) { 242 case 'http': 243 case 'https': 244 break; 245 default: 246 throw new PhutilArgumentUsageException( 247 pht( 248 'Configuration file specifies cluster peer ("%s", at index '. 249 '"%s") with an invalid protocol, "%s". Valid protocols are '. 250 '"%s" or "%s".', 251 $host, 252 $index, 253 $protocol, 254 'http', 255 'https')); 256 } 257 258 $peer_key = "{$host}:{$port}"; 259 if (!isset($peer_map[$peer_key])) { 260 $peer_map[$peer_key] = $index; 261 } else { 262 throw new PhutilArgumentUsageException( 263 pht( 264 'Configuration file specifies cluster peer "%s" more than '. 265 'once (at indexes "%s" and "%s"). Each peer must have a '. 266 'unique host and port combination.', 267 $peer_key, 268 $peer_map[$peer_key], 269 $index)); 270 } 271 } 272 273 $this->configData = $data; 274 $this->configPath = $full_path; 275 276 $pid_path = $this->getPIDPath(); 277 try { 278 $dir = dirname($pid_path); 279 if (!Filesystem::pathExists($dir)) { 280 Filesystem::createDirectory($dir, 0755, true); 281 } 282 } catch (FilesystemException $ex) { 283 throw new PhutilArgumentUsageException( 284 pht( 285 'Failed to create directory "%s" for specified PID file. You '. 286 'should manually create this directory or choose a different '. 287 'PID file location. %s', 288 $dir, 289 $ex->getMessage())); 290 } 291 } 292 293 final public function getPIDPath() { 294 return $this->configData['pidfile']; 295 } 296 297 final public function getPID() { 298 $pid = null; 299 if (Filesystem::pathExists($this->getPIDPath())) { 300 $pid = (int)Filesystem::readFile($this->getPIDPath()); 301 } 302 return $pid; 303 } 304 305 final public function cleanup($signo = null) { 306 global $g_future; 307 if ($g_future) { 308 $g_future->resolveKill(); 309 $g_future = null; 310 } 311 312 Filesystem::remove($this->getPIDPath()); 313 314 if ($signo !== null) { 315 $signame = phutil_get_signal_name($signo); 316 error_log("Caught signal {$signame}, exiting."); 317 } 318 319 exit(1); 320 } 321 322 public static function requireExtensions() { 323 self::mustHaveExtension('pcntl'); 324 self::mustHaveExtension('posix'); 325 } 326 327 private static function mustHaveExtension($ext) { 328 if (!extension_loaded($ext)) { 329 echo pht( 330 "ERROR: The PHP extension '%s' is not installed. You must ". 331 "install it to run Aphlict on this machine.", 332 $ext)."\n"; 333 exit(1); 334 } 335 336 $extension = new ReflectionExtension($ext); 337 foreach ($extension->getFunctions() as $function) { 338 $function = $function->name; 339 if (!function_exists($function)) { 340 echo pht( 341 'ERROR: The PHP function %s is disabled. You must '. 342 'enable it to run Aphlict on this machine.', 343 $function.'()')."\n"; 344 exit(1); 345 } 346 } 347 } 348 349 final protected function willLaunch() { 350 $console = PhutilConsole::getConsole(); 351 352 $pid = $this->getPID(); 353 if ($pid) { 354 throw new PhutilArgumentUsageException( 355 pht( 356 'Unable to start notifications server because it is already '. 357 'running. Use `%s` to restart it.', 358 'aphlict restart')); 359 } 360 361 if (posix_getuid() == 0) { 362 throw new PhutilArgumentUsageException( 363 pht('The notification server should not be run as root.')); 364 } 365 366 // Make sure we can write to the PID file. 367 if (!$this->debug) { 368 Filesystem::writeFile($this->getPIDPath(), ''); 369 } 370 371 // First, start the server in configuration test mode with --test. This 372 // will let us error explicitly if there are missing modules, before we 373 // fork and lose access to the console. 374 $test_argv = $this->getServerArgv(); 375 $test_argv[] = '--test=true'; 376 377 378 execx('%C', $this->getStartCommand($test_argv)); 379 } 380 381 private function getServerArgv() { 382 $server_argv = array(); 383 $server_argv[] = '--config='.$this->configPath; 384 return $server_argv; 385 } 386 387 final protected function launch() { 388 $console = PhutilConsole::getConsole(); 389 390 if ($this->debug) { 391 $console->writeOut( 392 "%s\n", 393 pht('Starting Aphlict server in foreground...')); 394 } else { 395 Filesystem::writeFile($this->getPIDPath(), (string)getmypid()); 396 } 397 398 $command = $this->getStartCommand($this->getServerArgv()); 399 400 if (!$this->debug) { 401 declare(ticks = 1); 402 pcntl_signal(SIGINT, array($this, 'cleanup')); 403 pcntl_signal(SIGTERM, array($this, 'cleanup')); 404 } 405 register_shutdown_function(array($this, 'cleanup')); 406 407 if ($this->debug) { 408 $console->writeOut( 409 "%s\n\n $ %s\n\n", 410 pht('Launching server:'), 411 $command); 412 413 $err = phutil_passthru('%C', $command); 414 $console->writeOut(">>> %s\n", pht('Server exited!')); 415 exit($err); 416 } else { 417 while (true) { 418 global $g_future; 419 $g_future = new ExecFuture('exec %C', $command); 420 421 // Discard all output the subprocess produces: it writes to the log on 422 // disk, so we don't need to send the output anywhere and can just 423 // throw it away. 424 $g_future 425 ->setStdoutSizeLimit(0) 426 ->setStderrSizeLimit(0); 427 428 $g_future->resolve(); 429 430 // If the server exited, wait a couple of seconds and restart it. 431 unset($g_future); 432 sleep(2); 433 } 434 } 435 } 436 437 438/* -( Commands )----------------------------------------------------------- */ 439 440 441 final protected function executeStartCommand() { 442 $console = PhutilConsole::getConsole(); 443 $this->willLaunch(); 444 445 $log = $this->getOverseerLogPath(); 446 if ($log !== null) { 447 echo tsprintf( 448 "%s\n", 449 pht( 450 'Writing logs to: %s', 451 $log)); 452 } 453 454 $pid = pcntl_fork(); 455 if ($pid < 0) { 456 throw new Exception( 457 pht( 458 'Failed to %s!', 459 'fork()')); 460 } else if ($pid) { 461 $console->writeErr("%s\n", pht('Aphlict Server started.')); 462 exit(0); 463 } 464 465 // Redirect process errors to the error log. If we do not do this, any 466 // error the `aphlict` process itself encounters vanishes into thin air. 467 if ($log !== null) { 468 ini_set('error_log', $log); 469 } 470 471 // When we fork, the child process will inherit its parent's set of open 472 // file descriptors. If the parent process of bin/aphlict is waiting for 473 // bin/aphlict's file descriptors to close, it will be stuck waiting on 474 // the daemonized process. (This happens if e.g. bin/aphlict is started 475 // in another script using passthru().) 476 fclose(STDOUT); 477 fclose(STDERR); 478 479 $this->launch(); 480 return 0; 481 } 482 483 484 final protected function executeStopCommand() { 485 $console = PhutilConsole::getConsole(); 486 487 $pid = $this->getPID(); 488 if (!$pid) { 489 $console->writeErr("%s\n", pht('Aphlict is not running.')); 490 return 0; 491 } 492 493 $console->writeErr("%s\n", pht('Stopping Aphlict Server (%s)...', $pid)); 494 posix_kill($pid, SIGINT); 495 496 $start = time(); 497 do { 498 if (!PhabricatorDaemonReference::isProcessRunning($pid)) { 499 $console->writeOut( 500 "%s\n", 501 pht('Aphlict Server (%s) exited normally.', $pid)); 502 $pid = null; 503 break; 504 } 505 usleep(100000); 506 } while (time() < $start + 5); 507 508 if ($pid) { 509 $console->writeErr("%s\n", pht('Sending %s a SIGKILL.', $pid)); 510 posix_kill($pid, SIGKILL); 511 unset($pid); 512 } 513 514 Filesystem::remove($this->getPIDPath()); 515 return 0; 516 } 517 518 private function getNodeBinary() { 519 if (Filesystem::binaryExists('nodejs')) { 520 return 'nodejs'; 521 } 522 523 if (Filesystem::binaryExists('node')) { 524 return 'node'; 525 } 526 527 throw new PhutilArgumentUsageException( 528 pht( 529 'No `%s` or `%s` binary was found in %s. You must install '. 530 'Node.js to start the Aphlict server.', 531 'nodejs', 532 'node', 533 '$PATH')); 534 } 535 536 private function getAphlictScriptPath() { 537 $root = dirname(phutil_get_library_root('phabricator')); 538 return $root.'/support/aphlict/server/aphlict_server.js'; 539 } 540 541 private function getNodeArgv() { 542 $argv = array(); 543 544 $hint = idx($this->configData, 'memory.hint'); 545 $hint = nonempty($hint, 256); 546 547 $argv[] = sprintf('--max-old-space-size=%d', $hint); 548 549 return $argv; 550 } 551 552 private function getStartCommand(array $server_argv) { 553 $launch_argv = array(); 554 555 if ($this->debug) { 556 $launch_argv[] = '--debug=1'; 557 } 558 559 return csprintf( 560 '%R %Ls -- %s %Ls %Ls', 561 $this->getNodeBinary(), 562 $this->getNodeArgv(), 563 $this->getAphlictScriptPath(), 564 $launch_argv, 565 $server_argv); 566 } 567 568 private function getOverseerLogPath() { 569 // For now, just return the first log. We could refine this eventually. 570 $logs = idx($this->configData, 'logs', array()); 571 572 foreach ($logs as $log) { 573 return $log['path']; 574 } 575 576 return null; 577 } 578 579}