Path: blob/master/src/applications/aphlict/management/PhabricatorAphlictManagementWorkflow.php
12256 views
<?php12abstract class PhabricatorAphlictManagementWorkflow3extends PhabricatorManagementWorkflow {45private $debug = false;6private $configData;7private $configPath;89final protected function setDebug($debug) {10$this->debug = $debug;11return $this;12}1314protected function getLaunchArguments() {15return array(16array(17'name' => 'config',18'param' => 'file',19'help' => pht(20'Use a specific configuration file instead of the default '.21'configuration.'),22),23);24}2526protected function parseLaunchArguments(PhutilArgumentParser $args) {27$config_file = $args->getArg('config');28if ($config_file) {29$full_path = Filesystem::resolvePath($config_file);30$show_path = $full_path;31} else {32$root = dirname(dirname(phutil_get_library_root('phabricator')));3334$try = array(35'phabricator/conf/aphlict/aphlict.custom.json',36'phabricator/conf/aphlict/aphlict.default.json',37);3839foreach ($try as $config) {40$full_path = $root.'/'.$config;41$show_path = $config;42if (Filesystem::pathExists($full_path)) {43break;44}45}46}4748echo tsprintf(49"%s\n",50pht(51'Reading configuration from: %s',52$show_path));5354try {55$data = Filesystem::readFile($full_path);56} catch (Exception $ex) {57throw new PhutilArgumentUsageException(58pht(59'Failed to read configuration file. %s',60$ex->getMessage()));61}6263try {64$data = phutil_json_decode($data);65} catch (Exception $ex) {66throw new PhutilArgumentUsageException(67pht(68'Configuration file is not properly formatted JSON. %s',69$ex->getMessage()));70}7172try {73PhutilTypeSpec::checkMap(74$data,75array(76'servers' => 'list<wild>',77'logs' => 'optional list<wild>',78'cluster' => 'optional list<wild>',79'pidfile' => 'string',80'memory.hint' => 'optional int',81));82} catch (Exception $ex) {83throw new PhutilArgumentUsageException(84pht(85'Configuration file has improper configuration keys at top '.86'level. %s',87$ex->getMessage()));88}8990$servers = $data['servers'];91$has_client = false;92$has_admin = false;93$port_map = array();94foreach ($servers as $index => $server) {95PhutilTypeSpec::checkMap(96$server,97array(98'type' => 'string',99'port' => 'int',100'listen' => 'optional string|null',101'ssl.key' => 'optional string|null',102'ssl.cert' => 'optional string|null',103'ssl.chain' => 'optional string|null',104));105106$port = $server['port'];107if (!isset($port_map[$port])) {108$port_map[$port] = $index;109} else {110throw new PhutilArgumentUsageException(111pht(112'Two servers (at indexes "%s" and "%s") both bind to the same '.113'port ("%s"). Each server must bind to a unique port.',114$port_map[$port],115$index,116$port));117}118119$type = $server['type'];120switch ($type) {121case 'admin':122$has_admin = true;123break;124case 'client':125$has_client = true;126break;127default:128throw new PhutilArgumentUsageException(129pht(130'A specified server (at index "%s", on port "%s") has an '.131'invalid type ("%s"). Valid types are: admin, client.',132$index,133$port,134$type));135}136137$ssl_key = idx($server, 'ssl.key');138$ssl_cert = idx($server, 'ssl.cert');139if (($ssl_key && !$ssl_cert) || ($ssl_cert && !$ssl_key)) {140throw new PhutilArgumentUsageException(141pht(142'A specified server (at index "%s", on port "%s") specifies '.143'only one of "%s" and "%s". Each server must specify neither '.144'(to disable SSL) or specify both (to enable it).',145$index,146$port,147'ssl.key',148'ssl.cert'));149}150151$ssl_chain = idx($server, 'ssl.chain');152if ($ssl_chain && (!$ssl_key && !$ssl_cert)) {153throw new PhutilArgumentUsageException(154pht(155'A specified server (at index "%s", on port "%s") specifies '.156'a value for "%s", but no value for "%s" or "%s". Servers '.157'should only provide an SSL chain if they also provide an SSL '.158'key and SSL certificate.',159$index,160$port,161'ssl.chain',162'ssl.key',163'ssl.cert'));164}165}166167if (!$servers) {168throw new PhutilArgumentUsageException(169pht(170'Configuration file does not specify any servers. This service '.171'will not be able to interact with the outside world if it does '.172'not listen on any ports. You must specify at least one "%s" '.173'server and at least one "%s" server.',174'admin',175'client'));176}177178if (!$has_client) {179throw new PhutilArgumentUsageException(180pht(181'Configuration file does not specify any client servers. This '.182'service will be unable to transmit any notifications without a '.183'client server. You must specify at least one server with '.184'type "%s".',185'client'));186}187188if (!$has_admin) {189throw new PhutilArgumentUsageException(190pht(191'Configuration file does not specify any administrative '.192'servers. This service will be unable to receive messages. '.193'You must specify at least one server with type "%s".',194'admin'));195}196197$logs = idx($data, 'logs', array());198foreach ($logs as $index => $log) {199PhutilTypeSpec::checkMap(200$log,201array(202'path' => 'string',203));204205$path = $log['path'];206207try {208$dir = dirname($path);209if (!Filesystem::pathExists($dir)) {210Filesystem::createDirectory($dir, 0755, true);211}212} catch (FilesystemException $ex) {213throw new PhutilArgumentUsageException(214pht(215'Failed to create directory "%s" for specified log file (with '.216'index "%s"). You should manually create this directory or '.217'choose a different logfile location. %s',218$dir,219$index,220$ex->getMessage()));221}222}223224$peer_map = array();225226$cluster = idx($data, 'cluster', array());227foreach ($cluster as $index => $peer) {228PhutilTypeSpec::checkMap(229$peer,230array(231'host' => 'string',232'port' => 'int',233'protocol' => 'string',234));235236$host = $peer['host'];237$port = $peer['port'];238$protocol = $peer['protocol'];239240switch ($protocol) {241case 'http':242case 'https':243break;244default:245throw new PhutilArgumentUsageException(246pht(247'Configuration file specifies cluster peer ("%s", at index '.248'"%s") with an invalid protocol, "%s". Valid protocols are '.249'"%s" or "%s".',250$host,251$index,252$protocol,253'http',254'https'));255}256257$peer_key = "{$host}:{$port}";258if (!isset($peer_map[$peer_key])) {259$peer_map[$peer_key] = $index;260} else {261throw new PhutilArgumentUsageException(262pht(263'Configuration file specifies cluster peer "%s" more than '.264'once (at indexes "%s" and "%s"). Each peer must have a '.265'unique host and port combination.',266$peer_key,267$peer_map[$peer_key],268$index));269}270}271272$this->configData = $data;273$this->configPath = $full_path;274275$pid_path = $this->getPIDPath();276try {277$dir = dirname($pid_path);278if (!Filesystem::pathExists($dir)) {279Filesystem::createDirectory($dir, 0755, true);280}281} catch (FilesystemException $ex) {282throw new PhutilArgumentUsageException(283pht(284'Failed to create directory "%s" for specified PID file. You '.285'should manually create this directory or choose a different '.286'PID file location. %s',287$dir,288$ex->getMessage()));289}290}291292final public function getPIDPath() {293return $this->configData['pidfile'];294}295296final public function getPID() {297$pid = null;298if (Filesystem::pathExists($this->getPIDPath())) {299$pid = (int)Filesystem::readFile($this->getPIDPath());300}301return $pid;302}303304final public function cleanup($signo = null) {305global $g_future;306if ($g_future) {307$g_future->resolveKill();308$g_future = null;309}310311Filesystem::remove($this->getPIDPath());312313if ($signo !== null) {314$signame = phutil_get_signal_name($signo);315error_log("Caught signal {$signame}, exiting.");316}317318exit(1);319}320321public static function requireExtensions() {322self::mustHaveExtension('pcntl');323self::mustHaveExtension('posix');324}325326private static function mustHaveExtension($ext) {327if (!extension_loaded($ext)) {328echo pht(329"ERROR: The PHP extension '%s' is not installed. You must ".330"install it to run Aphlict on this machine.",331$ext)."\n";332exit(1);333}334335$extension = new ReflectionExtension($ext);336foreach ($extension->getFunctions() as $function) {337$function = $function->name;338if (!function_exists($function)) {339echo pht(340'ERROR: The PHP function %s is disabled. You must '.341'enable it to run Aphlict on this machine.',342$function.'()')."\n";343exit(1);344}345}346}347348final protected function willLaunch() {349$console = PhutilConsole::getConsole();350351$pid = $this->getPID();352if ($pid) {353throw new PhutilArgumentUsageException(354pht(355'Unable to start notifications server because it is already '.356'running. Use `%s` to restart it.',357'aphlict restart'));358}359360if (posix_getuid() == 0) {361throw new PhutilArgumentUsageException(362pht('The notification server should not be run as root.'));363}364365// Make sure we can write to the PID file.366if (!$this->debug) {367Filesystem::writeFile($this->getPIDPath(), '');368}369370// First, start the server in configuration test mode with --test. This371// will let us error explicitly if there are missing modules, before we372// fork and lose access to the console.373$test_argv = $this->getServerArgv();374$test_argv[] = '--test=true';375376377execx('%C', $this->getStartCommand($test_argv));378}379380private function getServerArgv() {381$server_argv = array();382$server_argv[] = '--config='.$this->configPath;383return $server_argv;384}385386final protected function launch() {387$console = PhutilConsole::getConsole();388389if ($this->debug) {390$console->writeOut(391"%s\n",392pht('Starting Aphlict server in foreground...'));393} else {394Filesystem::writeFile($this->getPIDPath(), getmypid());395}396397$command = $this->getStartCommand($this->getServerArgv());398399if (!$this->debug) {400declare(ticks = 1);401pcntl_signal(SIGINT, array($this, 'cleanup'));402pcntl_signal(SIGTERM, array($this, 'cleanup'));403}404register_shutdown_function(array($this, 'cleanup'));405406if ($this->debug) {407$console->writeOut(408"%s\n\n $ %s\n\n",409pht('Launching server:'),410$command);411412$err = phutil_passthru('%C', $command);413$console->writeOut(">>> %s\n", pht('Server exited!'));414exit($err);415} else {416while (true) {417global $g_future;418$g_future = new ExecFuture('exec %C', $command);419420// Discard all output the subprocess produces: it writes to the log on421// disk, so we don't need to send the output anywhere and can just422// throw it away.423$g_future424->setStdoutSizeLimit(0)425->setStderrSizeLimit(0);426427$g_future->resolve();428429// If the server exited, wait a couple of seconds and restart it.430unset($g_future);431sleep(2);432}433}434}435436437/* -( Commands )----------------------------------------------------------- */438439440final protected function executeStartCommand() {441$console = PhutilConsole::getConsole();442$this->willLaunch();443444$log = $this->getOverseerLogPath();445if ($log !== null) {446echo tsprintf(447"%s\n",448pht(449'Writing logs to: %s',450$log));451}452453$pid = pcntl_fork();454if ($pid < 0) {455throw new Exception(456pht(457'Failed to %s!',458'fork()'));459} else if ($pid) {460$console->writeErr("%s\n", pht('Aphlict Server started.'));461exit(0);462}463464// Redirect process errors to the error log. If we do not do this, any465// error the `aphlict` process itself encounters vanishes into thin air.466if ($log !== null) {467ini_set('error_log', $log);468}469470// When we fork, the child process will inherit its parent's set of open471// file descriptors. If the parent process of bin/aphlict is waiting for472// bin/aphlict's file descriptors to close, it will be stuck waiting on473// the daemonized process. (This happens if e.g. bin/aphlict is started474// in another script using passthru().)475fclose(STDOUT);476fclose(STDERR);477478$this->launch();479return 0;480}481482483final protected function executeStopCommand() {484$console = PhutilConsole::getConsole();485486$pid = $this->getPID();487if (!$pid) {488$console->writeErr("%s\n", pht('Aphlict is not running.'));489return 0;490}491492$console->writeErr("%s\n", pht('Stopping Aphlict Server (%s)...', $pid));493posix_kill($pid, SIGINT);494495$start = time();496do {497if (!PhabricatorDaemonReference::isProcessRunning($pid)) {498$console->writeOut(499"%s\n",500pht('Aphlict Server (%s) exited normally.', $pid));501$pid = null;502break;503}504usleep(100000);505} while (time() < $start + 5);506507if ($pid) {508$console->writeErr("%s\n", pht('Sending %s a SIGKILL.', $pid));509posix_kill($pid, SIGKILL);510unset($pid);511}512513Filesystem::remove($this->getPIDPath());514return 0;515}516517private function getNodeBinary() {518if (Filesystem::binaryExists('nodejs')) {519return 'nodejs';520}521522if (Filesystem::binaryExists('node')) {523return 'node';524}525526throw new PhutilArgumentUsageException(527pht(528'No `%s` or `%s` binary was found in %s. You must install '.529'Node.js to start the Aphlict server.',530'nodejs',531'node',532'$PATH'));533}534535private function getAphlictScriptPath() {536$root = dirname(phutil_get_library_root('phabricator'));537return $root.'/support/aphlict/server/aphlict_server.js';538}539540private function getNodeArgv() {541$argv = array();542543$hint = idx($this->configData, 'memory.hint');544$hint = nonempty($hint, 256);545546$argv[] = sprintf('--max-old-space-size=%d', $hint);547548return $argv;549}550551private function getStartCommand(array $server_argv) {552$launch_argv = array();553554if ($this->debug) {555$launch_argv[] = '--debug=1';556}557558return csprintf(559'%R %Ls -- %s %Ls %Ls',560$this->getNodeBinary(),561$this->getNodeArgv(),562$this->getAphlictScriptPath(),563$launch_argv,564$server_argv);565}566567private function getOverseerLogPath() {568// For now, just return the first log. We could refine this eventually.569$logs = idx($this->configData, 'logs', array());570571foreach ($logs as $log) {572return $log['path'];573}574575return null;576}577578}579580581