diff --git a/conf/aphlict/aphlict.default.json b/conf/aphlict/aphlict.default.json index 306e32014f..1f1bafc3ea 100644 --- a/conf/aphlict/aphlict.default.json +++ b/conf/aphlict/aphlict.default.json @@ -1,18 +1,24 @@ { "servers": [ { "type": "client", "port": 22280, "listen": "0.0.0.0", "ssl.key": null, "ssl.cert": null }, { "type": "admin", "port": 22281, "listen": "127.0.0.1", "ssl.key": null, "ssl.cert": null } - ] + ], + "logs": [ + { + "path": "/var/log/aphlict.log" + } + ], + "pidfile": "/var/tmp/aphlict/pid/aphlict.pid" } diff --git a/src/applications/aphlict/management/PhabricatorAphlictManagementWorkflow.php b/src/applications/aphlict/management/PhabricatorAphlictManagementWorkflow.php index 6f6014dafb..60d34b89eb 100644 --- a/src/applications/aphlict/management/PhabricatorAphlictManagementWorkflow.php +++ b/src/applications/aphlict/management/PhabricatorAphlictManagementWorkflow.php @@ -1,444 +1,454 @@ debug = $debug; return $this; } protected function getLaunchArguments() { return array( array( 'name' => 'config', 'param' => 'file', 'help' => pht( 'Use a specific configuration file instead of the default '. 'configuration.'), ), ); } protected function parseLaunchArguments(PhutilArgumentParser $args) { $config_file = $args->getArg('config'); if ($config_file) { $full_path = Filesystem::resolvePath($config_file); $show_path = $full_path; } else { $root = dirname(dirname(phutil_get_library_root('phabricator'))); $try = array( 'phabricator/conf/aphlict/aphlict.custom.json', 'phabricator/conf/aphlict/aphlict.default.json', ); foreach ($try as $config) { $full_path = $root.'/'.$config; $show_path = $config; if (Filesystem::pathExists($full_path)) { break; } } } echo tsprintf( "%s\n", pht( 'Reading configuration from: %s', $show_path)); try { $data = Filesystem::readFile($full_path); } catch (Exception $ex) { throw new PhutilArgumentUsageException( pht( 'Failed to read configuration file. %s', $ex->getMessage())); } try { $data = phutil_json_decode($data); } catch (Exception $ex) { throw new PhutilArgumentUsageException( pht( 'Configuration file is not properly formatted JSON. %s', $ex->getMessage())); } try { PhutilTypeSpec::checkMap( $data, array( 'servers' => 'list', + 'logs' => 'optional list', + 'pidfile' => 'string', )); } catch (Exception $ex) { throw new PhutilArgumentUsageException( pht( 'Configuration file has improper configuration keys at top '. 'level. %s', $ex->getMessage())); } $servers = $data['servers']; $has_client = false; $has_admin = false; $port_map = array(); foreach ($servers as $index => $server) { PhutilTypeSpec::checkMap( $server, array( 'type' => 'string', 'port' => 'int', 'listen' => 'optional string|null', 'ssl.key' => 'optional string|null', 'ssl.cert' => 'optional string|null', )); $port = $server['port']; if (!isset($port_map[$port])) { $port_map[$port] = $index; } else { throw new PhutilArgumentUsageException( pht( 'Two servers (at indexes "%s" and "%s") both bind to the same '. 'port ("%s"). Each server must bind to a unique port.', $port_map[$port], $index, $port)); } $type = $server['type']; switch ($type) { case 'admin': $has_admin = true; break; case 'client': $has_client = true; break; default: throw new PhutilArgumentUsageException( pht( 'A specified server (at index "%s", on port "%s") has an '. 'invalid type ("%s"). Valid types are: admin, client.', $index, $port, $type)); } $ssl_key = idx($server, 'ssl.key'); $ssl_cert = idx($server, 'ssl.cert'); if (($ssl_key && !$ssl_cert) || ($ssl_cert && !$ssl_key)) { throw new PhutilArgumentUsageException( pht( 'A specified server (at index "%s", on port "%s") specifies '. 'only one of "%s" and "%s". Each server must specify neither '. '(to disable SSL) or specify both (to enable it).', $index, $port, 'ssl.key', 'ssl.cert')); } } if (!$servers) { throw new PhutilArgumentUsageException( pht( 'Configuration file does not specify any servers. This service '. 'will not be able to interact with the outside world if it does '. 'not listen on any ports. You must specify at least one "%s" '. 'server and at least one "%s" server.', 'admin', 'client')); } if (!$has_client) { throw new PhutilArgumentUsageException( pht( 'Configuration file does not specify any client servers. This '. 'service will be unable to transmit any notifications without a '. 'client server. You must specify at least one server with '. 'type "%s".', 'client')); } if (!$has_admin) { throw new PhutilArgumentUsageException( pht( 'Configuration file does not specify any administrative '. 'servers. This service will be unable to receive messages. '. 'You must specify at least one server with type "%s".', 'admin')); } - $this->configPath = $full_path; - } + $logs = $data['logs']; + foreach ($logs as $index => $log) { + PhutilTypeSpec::checkMap( + $log, + array( + 'path' => 'string', + )); - final public function getPIDPath() { - $path = PhabricatorEnv::getEnvConfig('notification.pidfile'); + $path = $log['path']; - try { - $dir = dirname($path); - if (!Filesystem::pathExists($dir)) { - Filesystem::createDirectory($dir, 0755, true); + try { + $dir = dirname($path); + if (!Filesystem::pathExists($dir)) { + Filesystem::createDirectory($dir, 0755, true); + } + } catch (FilesystemException $ex) { + throw new PhutilArgumentUsageException( + pht( + 'Failed to create directory "%s" for specified log file (with '. + 'index "%s"). You should manually create this directory or '. + 'choose a different logfile location. %s', + $dir, + $ex->getMessage())); } - } catch (FilesystemException $ex) { - throw new Exception( - pht( - "Failed to create '%s'. You should manually create this directory.", - $dir)); } - return $path; - } - - final public function getLogPath() { - $path = PhabricatorEnv::getEnvConfig('notification.log'); + $this->configData = $data; + $this->configPath = $full_path; + $pid_path = $this->getPIDPath(); try { $dir = dirname($path); if (!Filesystem::pathExists($dir)) { Filesystem::createDirectory($dir, 0755, true); } } catch (FilesystemException $ex) { - throw new Exception( + throw new PhutilArgumentUsageException( pht( - "Failed to create '%s'. You should manually create this directory.", - $dir)); + 'Failed to create directory "%s" for specified PID file. You '. + 'should manually create this directory or choose a different '. + 'PID file location. %s', + $dir, + $ex->getMessage())); } + } - return $path; + final public function getPIDPath() { + return $this->configData['pidfile']; } final public function getPID() { $pid = null; if (Filesystem::pathExists($this->getPIDPath())) { $pid = (int)Filesystem::readFile($this->getPIDPath()); } return $pid; } final public function cleanup($signo = '?') { global $g_future; if ($g_future) { $g_future->resolveKill(); $g_future = null; } Filesystem::remove($this->getPIDPath()); exit(1); } public static function requireExtensions() { self::mustHaveExtension('pcntl'); self::mustHaveExtension('posix'); } private static function mustHaveExtension($ext) { if (!extension_loaded($ext)) { echo pht( "ERROR: The PHP extension '%s' is not installed. You must ". "install it to run Aphlict on this machine.", $ext)."\n"; exit(1); } $extension = new ReflectionExtension($ext); foreach ($extension->getFunctions() as $function) { $function = $function->name; if (!function_exists($function)) { echo pht( 'ERROR: The PHP function %s is disabled. You must '. 'enable it to run Aphlict on this machine.', $function.'()')."\n"; exit(1); } } } final protected function willLaunch() { $console = PhutilConsole::getConsole(); $pid = $this->getPID(); if ($pid) { throw new PhutilArgumentUsageException( pht( 'Unable to start notifications server because it is already '. 'running. Use `%s` to restart it.', 'aphlict restart')); } if (posix_getuid() == 0) { throw new PhutilArgumentUsageException( pht('The notification server should not be run as root.')); } // Make sure we can write to the PID file. if (!$this->debug) { Filesystem::writeFile($this->getPIDPath(), ''); } // First, start the server in configuration test mode with --test. This // will let us error explicitly if there are missing modules, before we // fork and lose access to the console. $test_argv = $this->getServerArgv(); $test_argv[] = '--test=true'; execx('%C', $this->getStartCommand($test_argv)); } private function getServerArgv() { - $log = $this->getLogPath(); - $server_argv = array(); $server_argv[] = '--config='.$this->configPath; - $server_argv[] = '--log='.$log; - return $server_argv; } final protected function launch() { $console = PhutilConsole::getConsole(); if ($this->debug) { $console->writeOut( "%s\n", pht('Starting Aphlict server in foreground...')); } else { Filesystem::writeFile($this->getPIDPath(), getmypid()); } $command = $this->getStartCommand($this->getServerArgv()); if (!$this->debug) { declare(ticks = 1); pcntl_signal(SIGINT, array($this, 'cleanup')); pcntl_signal(SIGTERM, array($this, 'cleanup')); } register_shutdown_function(array($this, 'cleanup')); if ($this->debug) { $console->writeOut( "%s\n\n $ %s\n\n", pht('Launching server:'), $command); $err = phutil_passthru('%C', $command); $console->writeOut(">>> %s\n", pht('Server exited!')); exit($err); } else { while (true) { global $g_future; $g_future = new ExecFuture('exec %C', $command); $g_future->resolve(); // If the server exited, wait a couple of seconds and restart it. unset($g_future); sleep(2); } } } /* -( Commands )----------------------------------------------------------- */ final protected function executeStartCommand() { $console = PhutilConsole::getConsole(); $this->willLaunch(); $pid = pcntl_fork(); if ($pid < 0) { throw new Exception( pht( 'Failed to %s!', 'fork()')); } else if ($pid) { $console->writeErr("%s\n", pht('Aphlict Server started.')); exit(0); } // When we fork, the child process will inherit its parent's set of open // file descriptors. If the parent process of bin/aphlict is waiting for // bin/aphlict's file descriptors to close, it will be stuck waiting on // the daemonized process. (This happens if e.g. bin/aphlict is started // in another script using passthru().) fclose(STDOUT); fclose(STDERR); $this->launch(); return 0; } final protected function executeStopCommand() { $console = PhutilConsole::getConsole(); $pid = $this->getPID(); if (!$pid) { $console->writeErr("%s\n", pht('Aphlict is not running.')); return 0; } $console->writeErr("%s\n", pht('Stopping Aphlict Server (%s)...', $pid)); posix_kill($pid, SIGINT); $start = time(); do { if (!PhabricatorDaemonReference::isProcessRunning($pid)) { $console->writeOut( "%s\n", pht('Aphlict Server (%s) exited normally.', $pid)); $pid = null; break; } usleep(100000); } while (time() < $start + 5); if ($pid) { $console->writeErr("%s\n", pht('Sending %s a SIGKILL.', $pid)); posix_kill($pid, SIGKILL); unset($pid); } Filesystem::remove($this->getPIDPath()); return 0; } private function getNodeBinary() { if (Filesystem::binaryExists('nodejs')) { return 'nodejs'; } if (Filesystem::binaryExists('node')) { return 'node'; } throw new PhutilArgumentUsageException( pht( 'No `%s` or `%s` binary was found in %s. You must install '. 'Node.js to start the Aphlict server.', 'nodejs', 'node', '$PATH')); } private function getAphlictScriptPath() { $root = dirname(phutil_get_library_root('phabricator')); return $root.'/support/aphlict/server/aphlict_server.js'; } private function getStartCommand(array $server_argv) { return csprintf( '%s %s %Ls', $this->getNodeBinary(), $this->getAphlictScriptPath(), $server_argv); } } diff --git a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php index 9afc23a454..e054918c1d 100644 --- a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php +++ b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php @@ -1,378 +1,380 @@ newIssue('config.unknown.'.$key) ->setShortName($short) ->setName($name) ->setSummary($summary); $stack = PhabricatorEnv::getConfigSourceStack(); $stack = $stack->getStack(); $found = array(); $found_local = false; $found_database = false; foreach ($stack as $source_key => $source) { $value = $source->getKeys(array($key)); if ($value) { $found[] = $source->getName(); if ($source instanceof PhabricatorConfigDatabaseSource) { $found_database = true; } if ($source instanceof PhabricatorConfigLocalSource) { $found_local = true; } } } $message = $message."\n\n".pht( 'This configuration value is defined in these %d '. 'configuration source(s): %s.', count($found), implode(', ', $found)); $issue->setMessage($message); if ($found_local) { $command = csprintf('phabricator/ $ ./bin/config delete %s', $key); $issue->addCommand($command); } if ($found_database) { $issue->addPhabricatorConfig($key); } } $this->executeManiphestFieldChecks(); } /** * Return a map of deleted config options. Keys are option keys; values are * explanations of what happened to the option. */ public static function getAncientConfig() { $reason_auth = pht( 'This option has been migrated to the "Auth" application. Your old '. 'configuration is still in effect, but now stored in "Auth" instead of '. 'configuration. Going forward, you can manage authentication from '. 'the web UI.'); $auth_config = array( 'controller.oauth-registration', 'auth.password-auth-enabled', 'facebook.auth-enabled', 'facebook.registration-enabled', 'facebook.auth-permanent', 'facebook.application-id', 'facebook.application-secret', 'facebook.require-https-auth', 'github.auth-enabled', 'github.registration-enabled', 'github.auth-permanent', 'github.application-id', 'github.application-secret', 'google.auth-enabled', 'google.registration-enabled', 'google.auth-permanent', 'google.application-id', 'google.application-secret', 'ldap.auth-enabled', 'ldap.hostname', 'ldap.port', 'ldap.base_dn', 'ldap.search_attribute', 'ldap.search-first', 'ldap.username-attribute', 'ldap.real_name_attributes', 'ldap.activedirectory_domain', 'ldap.version', 'ldap.referrals', 'ldap.anonymous-user-name', 'ldap.anonymous-user-password', 'ldap.start-tls', 'disqus.auth-enabled', 'disqus.registration-enabled', 'disqus.auth-permanent', 'disqus.application-id', 'disqus.application-secret', 'phabricator.oauth-uri', 'phabricator.auth-enabled', 'phabricator.registration-enabled', 'phabricator.auth-permanent', 'phabricator.application-id', 'phabricator.application-secret', ); $ancient_config = array_fill_keys($auth_config, $reason_auth); $markup_reason = pht( 'Custom remarkup rules are now added by subclassing '. '%s or %s.', 'PhabricatorRemarkupCustomInlineRule', 'PhabricatorRemarkupCustomBlockRule'); $session_reason = pht( 'Sessions now expire and are garbage collected rather than having an '. 'arbitrary concurrency limit.'); $differential_field_reason = pht( 'All Differential fields are now managed through the configuration '. 'option "%s". Use that option to configure which fields are shown.', 'differential.fields'); $reply_domain_reason = pht( 'Individual application reply handler domains have been removed. '. 'Configure a reply domain with "%s".', 'metamta.reply-handler-domain'); $reply_handler_reason = pht( 'Reply handlers can no longer be overridden with configuration.'); $monospace_reason = pht( 'Phabricator no longer supports global customization of monospaced '. 'fonts.'); $public_mail_reason = pht( 'Inbound mail addresses are now configured for each application '. 'in the Applications tool.'); $gc_reason = pht( 'Garbage collectors are now configured with "%s".', 'bin/garbage set-policy'); $aphlict_reason = pht( 'Configuration of the notification server has changed substantially. '. 'For discussion, see T10794.'); $ancient_config += array( 'phid.external-loaders' => pht( 'External loaders have been replaced. Extend `%s` '. 'to implement new PHID and handle types.', 'PhabricatorPHIDType'), 'maniphest.custom-task-extensions-class' => pht( 'Maniphest fields are now loaded automatically. '. 'You can configure them with `%s`.', 'maniphest.fields'), 'maniphest.custom-fields' => pht( 'Maniphest fields are now defined in `%s`. '. 'Existing definitions have been migrated.', 'maniphest.custom-field-definitions'), 'differential.custom-remarkup-rules' => $markup_reason, 'differential.custom-remarkup-block-rules' => $markup_reason, 'auth.sshkeys.enabled' => pht( 'SSH keys are now actually useful, so they are always enabled.'), 'differential.anonymous-access' => pht( 'Phabricator now has meaningful global access controls. See `%s`.', 'policy.allow-public'), 'celerity.resource-path' => pht( 'An alternate resource map is no longer supported. Instead, use '. 'multiple maps. See T4222.'), 'metamta.send-immediately' => pht( 'Mail is now always delivered by the daemons.'), 'auth.sessions.conduit' => $session_reason, 'auth.sessions.web' => $session_reason, 'tokenizer.ondemand' => pht( 'Phabricator now manages typeahead strategies automatically.'), 'differential.revision-custom-detail-renderer' => pht( 'Obsolete; use standard rendering events instead.'), 'differential.show-host-field' => $differential_field_reason, 'differential.show-test-plan-field' => $differential_field_reason, 'differential.field-selector' => $differential_field_reason, 'phabricator.show-beta-applications' => pht( 'This option has been renamed to `%s` to emphasize the '. 'unfinished nature of many prototype applications. '. 'Your existing setting has been migrated.', 'phabricator.show-prototypes'), 'notification.user' => pht( 'The notification server no longer requires root permissions. Start '. 'the server as the user you want it to run under.'), 'notification.debug' => pht( 'Notifications no longer have a dedicated debugging mode.'), 'translation.provider' => pht( 'The translation implementation has changed and providers are no '. 'longer used or supported.'), 'config.mask' => pht( 'Use `%s` instead of this option.', 'config.hide'), 'phd.start-taskmasters' => pht( 'Taskmasters now use an autoscaling pool. You can configure the '. 'pool size with `%s`.', 'phd.taskmasters'), 'storage.engine-selector' => pht( 'Phabricator now automatically discovers available storage engines '. 'at runtime.'), 'storage.upload-size-limit' => pht( 'Phabricator now supports arbitrarily large files. Consult the '. 'documentation for configuration details.'), 'security.allow-outbound-http' => pht( 'This option has been replaced with the more granular option `%s`.', 'security.outbound-blacklist'), 'metamta.reply.show-hints' => pht( 'Phabricator no longer shows reply hints in mail.'), 'metamta.differential.reply-handler-domain' => $reply_domain_reason, 'metamta.diffusion.reply-handler-domain' => $reply_domain_reason, 'metamta.macro.reply-handler-domain' => $reply_domain_reason, 'metamta.maniphest.reply-handler-domain' => $reply_domain_reason, 'metamta.pholio.reply-handler-domain' => $reply_domain_reason, 'metamta.diffusion.reply-handler' => $reply_handler_reason, 'metamta.differential.reply-handler' => $reply_handler_reason, 'metamta.maniphest.reply-handler' => $reply_handler_reason, 'metamta.package.reply-handler' => $reply_handler_reason, 'metamta.precedence-bulk' => pht( 'Phabricator now always sends transaction mail with '. '"Precedence: bulk" to improve deliverability.'), 'style.monospace' => $monospace_reason, 'style.monospace.windows' => $monospace_reason, 'search.engine-selector' => pht( 'Phabricator now automatically discovers available search engines '. 'at runtime.'), 'metamta.files.public-create-email' => $public_mail_reason, 'metamta.maniphest.public-create-email' => $public_mail_reason, 'metamta.maniphest.default-public-author' => $public_mail_reason, 'metamta.paste.public-create-email' => $public_mail_reason, 'security.allow-conduit-act-as-user' => pht( 'Impersonating users over the API is no longer supported.'), 'feed.public' => pht('The framable public feed is no longer supported.'), 'auth.login-message' => pht( 'This configuration option has been replaced with a modular '. 'handler. See T9346.'), 'gcdaemon.ttl.herald-transcripts' => $gc_reason, 'gcdaemon.ttl.daemon-logs' => $gc_reason, 'gcdaemon.ttl.differential-parse-cache' => $gc_reason, 'gcdaemon.ttl.markup-cache' => $gc_reason, 'gcdaemon.ttl.task-archive' => $gc_reason, 'gcdaemon.ttl.general-cache' => $gc_reason, 'gcdaemon.ttl.conduit-logs' => $gc_reason, 'phd.variant-config' => pht( 'This configuration is no longer relevant because daemons '. 'restart automatically on configuration changes.'), 'notification.ssl-cert' => $aphlict_reason, 'notification.ssl-key' => $aphlict_reason, + 'notification.pidfile' => $aphlict_reason, + 'notification.log' => $aphlict_reason, ); return $ancient_config; } private function executeManiphestFieldChecks() { $maniphest_appclass = 'PhabricatorManiphestApplication'; if (!PhabricatorApplication::isClassInstalled($maniphest_appclass)) { return; } $capabilities = array( ManiphestEditAssignCapability::CAPABILITY, ManiphestEditPoliciesCapability::CAPABILITY, ManiphestEditPriorityCapability::CAPABILITY, ManiphestEditProjectsCapability::CAPABILITY, ManiphestEditStatusCapability::CAPABILITY, ); // Check for any of these capabilities set to anything other than // "All Users". $any_set = false; $app = new PhabricatorManiphestApplication(); foreach ($capabilities as $capability) { $setting = $app->getPolicy($capability); if ($setting != PhabricatorPolicies::POLICY_USER) { $any_set = true; break; } } if (!$any_set) { return; } $issue_summary = pht( 'Maniphest is currently configured with deprecated policy settings '. 'which will be removed in a future version of Phabricator.'); $message = pht( 'Some policy settings in Maniphest are now deprecated and will be '. 'removed in a future version of Phabricator. You are currently using '. 'at least one of these settings.'. "\n\n". 'The deprecated settings are "Can Assign Tasks", '. '"Can Edit Task Policies", "Can Prioritize Tasks", '. '"Can Edit Task Projects", and "Can Edit Task Status". You can '. 'find these settings in Applications, or follow the link below.'. "\n\n". 'You can find discussion of this change (including rationale and '. 'recommendations on how to configure similar features) in the upstream, '. 'at the link below.'. "\n\n". 'To resolve this issue, set all of these policies to "All Users" after '. 'making any necessary form customization changes.'); $more_href = 'https://secure.phabricator.com/T10003'; $edit_href = '/applications/view/PhabricatorManiphestApplication/'; $issue = $this->newIssue('maniphest.T10003-per-field-policies') ->setShortName(pht('Deprecated Policies')) ->setName(pht('Deprecated Maniphest Field Policies')) ->setSummary($issue_summary) ->setMessage($message) ->addLink($more_href, pht('Learn More: Upstream Discussion')) ->addLink($edit_href, pht('Edit These Settings')); } } diff --git a/src/applications/config/option/PhabricatorNotificationConfigOptions.php b/src/applications/config/option/PhabricatorNotificationConfigOptions.php index 4f9bfc2869..5855aafb60 100644 --- a/src/applications/config/option/PhabricatorNotificationConfigOptions.php +++ b/src/applications/config/option/PhabricatorNotificationConfigOptions.php @@ -1,57 +1,50 @@ newOption('notification.enabled', 'bool', false) ->setBoolOptions( array( pht('Enable Real-Time Notifications'), pht('Disable Real-Time Notifications'), )) ->setSummary(pht('Enable real-time notifications.')) ->setDescription( pht( "Enable real-time notifications. You must also run a Node.js ". "based notification server for this to work. Consult the ". "documentation in 'Notifications User Guide: Setup and ". "Configuration' for instructions.")), $this->newOption( 'notification.client-uri', 'string', 'http://localhost:22280/') ->setDescription(pht('Location of the client server.')), $this->newOption( 'notification.server-uri', 'string', 'http://localhost:22281/') ->setDescription(pht('Location of the notification receiver server.')), - $this->newOption('notification.log', 'string', '/var/log/aphlict.log') - ->setDescription(pht('Location of the server log file.')), - $this->newOption( - 'notification.pidfile', - 'string', - '/var/tmp/aphlict/pid/aphlict.pid') - ->setDescription(pht('Location of the server PID file.')), ); } } diff --git a/src/docs/user/configuration/notifications.diviner b/src/docs/user/configuration/notifications.diviner index 20e7afd3b5..0b828f7072 100644 --- a/src/docs/user/configuration/notifications.diviner +++ b/src/docs/user/configuration/notifications.diviner @@ -1,195 +1,198 @@ @title Notifications User Guide: Setup and Configuration @group config Guide to setting up notifications. Overview ======== By default, Phabricator delivers information about events (like users creating tasks or commenting on code reviews) through email and in-application notifications. Phabricator can also be configured to deliver notifications in real time, by popping up a message in any open browser windows if something has happened or an object has been updated. To enable real-time notifications: - Set `notification.enabled` in your configuration to true. - Run the notification server, as described below. This document describes the process in detail. Supported Browsers ================== Notifications are supported for browsers which support WebSockets. This covers most modern browsers (like Chrome, Firefox, Safari, and recent versions of Internet Explorer) and many mobile browsers. IE8 and IE9 do not support WebSockets, so real-time notifications won't work in those browsers. Installing Node and Modules =========================== The notification server uses Node.js, so you'll need to install it first. To install Node.js, follow the instructions on [[ http://nodejs.org | nodejs.org ]]. You will also need to install the `ws` module for Node. This needs to be installed into the notification server directory: phabricator/ $ cd support/aphlict/server/ phabricator/support/aphlict/server/ $ npm install ws Once Node.js and the `ws` module are installed, you're ready to start the server. Running the Aphlict Server ========================== After installing Node.js, you can control the notification server with the `bin/aphlict` command. To start the server: phabricator/ $ bin/aphlict start By default, the server must be able to listen on port `22280`. If you're using a host firewall (like a security group in EC2), make sure traffic can reach the server. The server configuration is controlled by a configuration file, which is separate from Phabricator's configuration settings. The default file can be found at `phabricator/conf/aphlict/aphlict.default.json`. To make adjustments to the default configuration, either copy this file to create `aphlict.custom.json` in the same directory (this file will be used if it exists) or specify a configuration file explicitly with the `--config` flag: phabricator/ $ bin/aphlict start --config path/to/config.json The configuration file has these settings: - - `servers`: A list of servers to start. + - `servers`: //Required list.// A list of servers to start. + - `logs`: //Optional list.// A list of logs to write to. + - `pidfile`: //Required string.// Path to a PID file. Each server in the `servers` list should be an object with these keys: - `type`: //Required string.// The type of server to start. Options are `admin` or `client`. Normally, you should run one of each. - `port`: //Required int.// The port this server should listen on. - `listen`: //Optional string.// Which interface to bind to. By default, the `admin` server is bound to localhost (so only other services on the local machine can connect to it), while the `client` server is bound to `0.0.0.0` (so any client can connect. - `ssl.key`: //Optional string.// If you want to use SSL on this port, the path to an SSL key. - `ssl.cert`: //Optional string.// If you want to use SSL on this port, the path to an SSL certificate. +Each log in the `logs` list should be an object with these keys: + + - `path`: //Required string.// Path to the log file. + The defaults are appropriate for simple cases, but you may need to adjust them if you are running a more complex configuration. Configuring Phabricator ======================= You may also want to adjust these settings: - `notification.client-uri` Externally-facing host and port that browsers will connect to in order to listen for notifications. - `notification.server-uri` Internally-facing host and port that Phabricator will connect to in order to publish notifications. - - `notification.log` Log file location for the server. - - `notification.pidfile` Pidfile location used to stop any running server when - aphlict is restarted. Verifying Server Status ======================= Access `/notification/status/` to verify the server is operational. You should see a table showing stats like "uptime" and connection/message counts if the server is working. If it isn't working, you should see an error. You can also send a test notification by clicking the button in the upper right corner of this screen. Troubleshooting =============== You can run `aphlict` in the foreground to get output to your console: phabricator/ $ ./bin/aphlict debug Because the notification server uses WebSockets, your browser error console may also have information that is useful in figuring out what's wrong. The server also generates a log, by default in `/var/log/aphlict.log`. You can change this location by changing `notification.log` in your configuration. The log may contain information useful in resolving issues. Advanced Usage ============== It is possible to route the WebSockets traffic for Aphlict through a reverse proxy such as `nginx` (see @{article:Configuration Guide} for instructions on configuring `nginx`). In order to do this with `nginx`, you will require at least version 1.3. You can read some more information about using `nginx` with WebSockets at http://nginx.com/blog/websocket-nginx/. There are a few benefits of this approach: - SSL is terminated at the `nginx` layer and consequently there is no need to configure `notificaton.ssl-cert` and `notification.ssl-key` (in fact, with this approach you should //not// configure these options because otherwise the Aphlict server will not accept HTTP traffic). - You don't have to open up a separate port on the server. - Clients don't need to be able to connect to Aphlict over a non-standard port which may be blocked by a firewall or anti-virus software. The following files show an example `nginx` configuration. Note that this is an example only and you may need to adjust this to suit your own setup. ```lang=nginx, name=/etc/nginx/conf.d/connection_upgrade.conf map $http_upgrade $connection_upgrade { default upgrade; '' close; } ``` ```lang=nginx, name=/etc/nginx/conf.d/websocket_pool.conf upstream websocket_pool { ip_hash; server 127.0.0.1:22280; } ``` ```lang=nginx, name=/etc/nginx/sites-enabled/phabricator.example.com.conf server { server_name phabricator.example.com; root /path/to/phabricator/webroot; // ... location = /ws/ { proxy_pass http://websocket_pool; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 999999999; } } ``` With this approach, you should set `notification.client-uri` to `http://localhost/ws/`. Additionally, there is no need for the Aphlict server to bind to `0.0.0.0` anymore (which is the default behavior), so you could start the Aphlict server with `./bin/aphlict start --client-host=localhost` instead. diff --git a/support/aphlict/server/aphlict_server.js b/support/aphlict/server/aphlict_server.js index 4037f4f3d6..7244ee7593 100644 --- a/support/aphlict/server/aphlict_server.js +++ b/support/aphlict/server/aphlict_server.js @@ -1,166 +1,170 @@ 'use strict'; var JX = require('./lib/javelin').JX; var http = require('http'); var https = require('https'); var util = require('util'); var fs = require('fs'); function parse_command_line_arguments(argv) { var args = { - log: '/var/log/aphlict.log', test: false, config: null }; for (var ii = 2; ii < argv.length; ii++) { var arg = argv[ii]; var matches = arg.match(/^--([^=]+)=(.*)$/); if (!matches) { throw new Error('Unknown argument "' + arg + '"!'); } if (!(matches[1] in args)) { throw new Error('Unknown argument "' + matches[1] + '"!'); } args[matches[1]] = matches[2]; } return args; } function parse_config(args) { var data = fs.readFileSync(args.config); return JSON.parse(data); } require('./lib/AphlictLog'); var debug = new JX.AphlictLog() .addConsole(console); var args = parse_command_line_arguments(process.argv); var config = parse_config(args); function set_exit_code(code) { process.on('exit', function() { process.exit(code); }); } process.on('uncaughtException', function(err) { var context = null; - if (err.code == 'EACCES' && err.path == args.log) { + if (err.code == 'EACCES') { context = util.format( - 'Unable to open logfile ("%s"). Check that permissions are set ' + + 'Unable to open file ("%s"). Check that permissions are set ' + 'correctly.', err.path); } var message = [ '\n<<< UNCAUGHT EXCEPTION! >>>', ]; if (context) { message.push(context); } message.push(err.stack); debug.log(message.join('\n\n')); set_exit_code(1); }); -// Add the logfile so we'll fail if we can't write to it. -if (args.log) { - debug.addLog(args.log); -} - try { require('ws'); } catch (ex) { throw new Error( 'You need to install the Node.js "ws" module for websocket support. ' + 'See "Notifications User Guide: Setup and Configuration" in the ' + 'documentation for instructions. ' + ex.toString()); } // NOTE: Require these only after checking for the "ws" module, since they // depend on it. require('./lib/AphlictAdminServer'); require('./lib/AphlictClientServer'); var ii; + +var logs = config.logs || []; +for (ii = 0; ii < logs.length; ii++) { + debug.addLog(logs[ii].path); +} + var servers = []; for (ii = 0; ii < config.servers.length; ii++) { var spec = config.servers[ii]; spec.listen = spec.listen || '0.0.0.0'; if (spec['ssl.key']) { spec['ssl.key'] = fs.readFileSync(spec['ssl.key']); } if (spec['ssl.cert']){ spec['ssl.cert'] = fs.readFileSync(spec['ssl.cert']); } servers.push(spec); } // If we're just doing a configuration test, exit here before starting any // servers. if (args.test) { debug.log('Configuration test OK.'); set_exit_code(0); return; } debug.log('Starting servers (service PID %d).', process.pid); +for (ii = 0; ii < logs.length; ii++) { + debug.log('Logging to "%s".', logs[ii].path); +} + var aphlict_servers = []; var aphlict_clients = []; var aphlict_admins = []; for (ii = 0; ii < servers.length; ii++) { var server = servers[ii]; var is_client = (server.type == 'client'); var http_server; if (server['ssl.key']) { var https_config = { key: server['ssl.key'], cert: server['ssl.cert'] }; http_server = https.createServer(https_config); } else { http_server = http.createServer(); } var aphlict_server; if (is_client) { aphlict_server = new JX.AphlictClientServer(http_server); } else { aphlict_server = new JX.AphlictAdminServer(http_server); } aphlict_server.setLogger(debug); aphlict_server.listen(server.port, server.listen); debug.log( 'Started %s server (Port %d, %s).', server.type, server.port, server['ssl.key'] ? 'With SSL' : 'No SSL'); aphlict_servers.push(aphlict_server); if (is_client) { aphlict_clients.push(aphlict_server); } else { aphlict_admins.push(aphlict_server); } } for (ii = 0; ii < aphlict_admins.length; ii++) { var admin_server = aphlict_admins[ii]; admin_server.setClientServers(aphlict_clients); }