diff --git a/src/applications/config/check/PhabricatorDaemonsSetupCheck.php b/src/applications/config/check/PhabricatorDaemonsSetupCheck.php index 331a4f0908..7ef44dc384 100644 --- a/src/applications/config/check/PhabricatorDaemonsSetupCheck.php +++ b/src/applications/config/check/PhabricatorDaemonsSetupCheck.php @@ -1,94 +1,101 @@ setViewer(PhabricatorUser::getOmnipotentUser()) - ->withStatus(PhabricatorDaemonLogQuery::STATUS_ALIVE) - ->withDaemonClasses(array('PhabricatorTaskmasterDaemon')) - ->setLimit(1) - ->execute(); - - if (!$task_daemon) { + try { + $task_daemons = id(new PhabricatorDaemonLogQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withStatus(PhabricatorDaemonLogQuery::STATUS_ALIVE) + ->withDaemonClasses(array('PhabricatorTaskmasterDaemon')) + ->setLimit(1) + ->execute(); + + $no_daemons = !$task_daemons; + } catch (Exception $ex) { + // Just skip this warning if the query fails for some reason. + $no_daemons = false; + } + + if ($no_daemons) { $doc_href = PhabricatorEnv::getDoclink('Managing Daemons with phd'); $summary = pht( 'You must start the Phabricator daemons to send email, rebuild '. 'search indexes, and do other background processing.'); $message = pht( 'The Phabricator daemons are not running, so Phabricator will not '. 'be able to perform background processing (including sending email, '. 'rebuilding search indexes, importing commits, cleaning up old data, '. 'and running builds).'. "\n\n". 'Use %s to start daemons. See %s for more information.', phutil_tag('tt', array(), 'bin/phd start'), phutil_tag( 'a', array( 'href' => $doc_href, 'target' => '_blank', ), pht('Managing Daemons with phd'))); $this->newIssue('daemons.not-running') ->setShortName(pht('Daemons Not Running')) ->setName(pht('Phabricator Daemons Are Not Running')) ->setSummary($summary) ->setMessage($message) ->addCommand('phabricator/ $ ./bin/phd start'); } $expect_user = PhabricatorEnv::getEnvConfig('phd.user'); if (strlen($expect_user)) { try { $all_daemons = id(new PhabricatorDaemonLogQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withStatus(PhabricatorDaemonLogQuery::STATUS_ALIVE) ->execute(); } catch (Exception $ex) { // If this query fails for some reason, just skip this check. $all_daemons = array(); } foreach ($all_daemons as $daemon) { $actual_user = $daemon->getRunningAsUser(); if ($actual_user == $expect_user) { continue; } $summary = pht( 'At least one daemon is currently running as the wrong user.'); $message = pht( 'A daemon is running as user %s, but daemons should be '. 'running as %s.'. "\n\n". 'Either adjust the configuration setting %s or restart the '. 'daemons. Daemons should attempt to run as the proper user when '. 'restarted.', phutil_tag('tt', array(), $actual_user), phutil_tag('tt', array(), $expect_user), phutil_tag('tt', array(), 'phd.user')); $this->newIssue('daemons.run-as-different-user') ->setName(pht('Daemon Running as Wrong User')) ->setSummary($summary) ->setMessage($message) ->addPhabricatorConfig('phd.user') ->addCommand('phabricator/ $ ./bin/phd restart'); break; } } } } diff --git a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php index d863c928b7..1de39f3468 100644 --- a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php +++ b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php @@ -1,547 +1,553 @@ getStack(); foreach ($all_keys as $key) { if (isset($defined_keys[$key])) { continue; } if (isset($ancient_config[$key])) { $summary = pht( 'This option has been removed. You may delete it at your '. 'convenience.'); $message = pht( "The configuration option '%s' has been removed. You may delete ". "it at your convenience.". "\n\n%s", $key, $ancient_config[$key]); $short = pht('Obsolete Config'); $name = pht('Obsolete Configuration Option "%s"', $key); } else { $summary = pht('This option is not recognized. It may be misspelled.'); $message = pht( "The configuration option '%s' is not recognized. It may be ". "misspelled, or it might have existed in an older version of ". "Phabricator. It has no effect, and should be corrected or deleted.", $key); $short = pht('Unknown Config'); $name = pht('Unknown Configuration Option "%s"', $key); } $issue = $this->newIssue('config.unknown.'.$key) ->setShortName($short) ->setName($name) ->setSummary($summary); $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); } } $options = PhabricatorApplicationConfigOptions::loadAllOptions(); foreach ($defined_keys as $key => $value) { $option = idx($options, $key); if (!$option) { continue; } if (!$option->getLocked()) { continue; } $found_database = false; foreach ($stack as $source_key => $source) { $value = $source->getKeys(array($key)); if ($value) { if ($source instanceof PhabricatorConfigDatabaseSource) { $found_database = true; break; } } } if (!$found_database) { continue; } // NOTE: These are values which we don't let you edit directly, but edit // via other UI workflows. For now, don't raise this warning about them. // In the future, before we stop reading database configuration for // locked values, we either need to add a flag which lets these values // continue reading from the database or move them to some other storage // mechanism. $soft_locks = array( 'phabricator.uninstalled-applications', 'phabricator.application-settings', 'config.ignore-issues', 'auth.lock-config', ); $soft_locks = array_fuse($soft_locks); if (isset($soft_locks[$key])) { continue; } $doc_name = 'Configuration Guide: Locked and Hidden Configuration'; $doc_href = PhabricatorEnv::getDoclink($doc_name); $set_command = phutil_tag( 'tt', array(), csprintf( 'bin/config set %R ', $key)); $summary = pht( 'Configuration value "%s" is locked, but has a value in the database.', $key); $message = pht( 'The configuration value "%s" is locked (so it can not be edited '. 'from the web UI), but has a database value. Usually, this means '. 'that it was previously not locked, you set it using the web UI, '. 'and it later became locked.'. "\n\n". 'You should copy this configuration value to a local configuration '. 'source (usually by using %s) and then remove it from the database '. 'with the command below.'. "\n\n". 'For more information on locked and hidden configuration, including '. 'details about this setup issue, see %s.'. "\n\n". 'This database value is currently respected, but a future version '. 'of Phabricator will stop respecting database values for locked '. 'configuration options.', $key, $set_command, phutil_tag( 'a', array( 'href' => $doc_href, 'target' => '_blank', ), $doc_name)); $command = csprintf( 'phabricator/ $ ./bin/config delete --database %R', $key); $this->newIssue('config.locked.'.$key) ->setShortName(pht('Deprecated Config Source')) ->setName( pht( 'Locked Configuration Option "%s" Has Database Value', $key)) ->setSummary($summary) ->setMessage($message) ->addCommand($command) ->addPhabricatorConfig($key); } if (PhabricatorEnv::getEnvConfig('feed.http-hooks')) { $this->newIssue('config.deprecated.feed.http-hooks') ->setShortName(pht('Feed Hooks Deprecated')) ->setName(pht('Migrate From "feed.http-hooks" to Webhooks')) ->addPhabricatorConfig('feed.http-hooks') ->setMessage( pht( 'The "feed.http-hooks" option is deprecated in favor of '. 'Webhooks. This option will be removed in a future version '. 'of Phabricator.'. "\n\n". 'You can configure Webhooks in Herald.'. "\n\n". 'To resolve this issue, remove all URIs from "feed.http-hooks".')); } } /** * 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.'); $stale_reason = pht( 'The Differential revision list view age UI elements have been removed '. 'to simplify the interface.'); $global_settings_reason = pht( 'The "Re: Prefix" and "Vary Subjects" settings are now configured '. 'in global settings.'); $dashboard_reason = pht( 'This option has been removed, you can use Dashboards to provide '. 'homepage customization. See T11533 for more details.'); $elastic_reason = pht( 'Elasticsearch is now configured with "%s".', 'cluster.search'); $mailers_reason = pht( 'Inbound and outbound mail is now configured with "cluster.mailers".'); $prefix_reason = pht( 'Per-application mail subject prefix customization is no longer '. 'directly supported. Prefixes and other strings may be customized with '. '"translation.override".'); + $phd_reason = pht( + 'Use "bin/phd debug ..." to get a detailed daemon execution log.'); + $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, 'notification.enabled' => $aphlict_reason, 'notification.client-uri' => $aphlict_reason, 'notification.server-uri' => $aphlict_reason, 'metamta.differential.unified-comment-context' => pht( 'Inline comments are now always rendered with a limited amount '. 'of context.'), 'differential.days-fresh' => $stale_reason, 'differential.days-stale' => $stale_reason, 'metamta.re-prefix' => $global_settings_reason, 'metamta.vary-subjects' => $global_settings_reason, 'ui.custom-header' => pht( 'This option has been replaced with `ui.logo`, which provides more '. 'flexible configuration options.'), 'welcome.html' => $dashboard_reason, 'maniphest.priorities.unbreak-now' => $dashboard_reason, 'maniphest.priorities.needs-triage' => $dashboard_reason, 'mysql.implementation' => pht( 'Phabricator now automatically selects the best available '. 'MySQL implementation.'), 'mysql.configuration-provider' => pht( 'Phabricator now has application-level management of partitioning '. 'and replicas.'), 'search.elastic.host' => $elastic_reason, 'search.elastic.namespace' => $elastic_reason, 'metamta.mail-adapter' => $mailers_reason, 'amazon-ses.access-key' => $mailers_reason, 'amazon-ses.secret-key' => $mailers_reason, 'amazon-ses.endpoint' => $mailers_reason, 'mailgun.domain' => $mailers_reason, 'mailgun.api-key' => $mailers_reason, 'phpmailer.mailer' => $mailers_reason, 'phpmailer.smtp-host' => $mailers_reason, 'phpmailer.smtp-port' => $mailers_reason, 'phpmailer.smtp-protocol' => $mailers_reason, 'phpmailer.smtp-user' => $mailers_reason, 'phpmailer.smtp-password' => $mailers_reason, 'phpmailer.smtp-encoding' => $mailers_reason, 'sendgrid.api-user' => $mailers_reason, 'sendgrid.api-key' => $mailers_reason, 'celerity.resource-hash' => pht( 'This option generally did not prove useful. Resource hash keys '. 'are now managed automatically.'), 'celerity.enable-deflate' => pht( 'Resource deflation is now managed automatically.'), 'celerity.minify' => pht( 'Resource minification is now managed automatically.'), 'metamta.domain' => pht( 'Mail thread IDs are now generated automatically.'), 'metamta.placeholder-to-recipient' => pht( 'Placeholder recipients are now generated automatically.'), 'metamta.mail-key' => pht( 'Mail object address hash keys are now generated automatically.'), 'phabricator.csrf-key' => pht( 'CSRF HMAC keys are now managed automatically.'), 'metamta.insecure-auth-with-reply-to' => pht( 'Authenticating users based on "Reply-To" is no longer supported.'), 'phabricator.allow-email-users' => pht( 'Public email is now accepted if the associated address has a '. 'default author, and rejected otherwise.'), 'metamta.conpherence.subject-prefix' => $prefix_reason, 'metamta.differential.subject-prefix' => $prefix_reason, 'metamta.diffusion.subject-prefix' => $prefix_reason, 'metamta.files.subject-prefix' => $prefix_reason, 'metamta.legalpad.subject-prefix' => $prefix_reason, 'metamta.macro.subject-prefix' => $prefix_reason, 'metamta.maniphest.subject-prefix' => $prefix_reason, 'metamta.package.subject-prefix' => $prefix_reason, 'metamta.paste.subject-prefix' => $prefix_reason, 'metamta.pholio.subject-prefix' => $prefix_reason, 'metamta.phriction.subject-prefix' => $prefix_reason, 'aphront.default-application-configuration-class' => pht( 'This ancient extension point has been replaced with other '. 'mechanisms, including "AphrontSite".'), 'differential.whitespace-matters' => pht( 'Whitespace rendering is now handled automatically.'), 'phd.pid-directory' => pht( 'Phabricator daemons no longer use PID files.'), + + 'phd.trace' => $phd_reason, + 'phd.verbose' => $phd_reason, ); return $ancient_config; } } diff --git a/src/applications/config/option/PhabricatorPHDConfigOptions.php b/src/applications/config/option/PhabricatorPHDConfigOptions.php index 7a1d39e617..259660d562 100644 --- a/src/applications/config/option/PhabricatorPHDConfigOptions.php +++ b/src/applications/config/option/PhabricatorPHDConfigOptions.php @@ -1,101 +1,69 @@ newOption('phd.log-directory', 'string', '/var/tmp/phd/log') ->setLocked(true) ->setDescription( pht('Directory that the daemons should use to store log files.')), $this->newOption('phd.taskmasters', 'int', 4) ->setLocked(true) ->setSummary(pht('Maximum taskmaster daemon pool size.')) ->setDescription( pht( "Maximum number of taskmaster daemons to run at once. Raising ". "this can increase the maximum throughput of the task queue. The ". "pool will automatically scale down when unutilized.". "\n\n". "If you are running a cluster, this limit applies separately ". "to each instance of `phd`. For example, if this limit is set ". "to `4` and you have three hosts running daemons, the effective ". "global limit will be 12.". "\n\n". "After changing this value, you must restart the daemons. Most ". "configuration changes are picked up by the daemons ". "automatically, but pool sizes can not be changed without a ". "restart.")), - $this->newOption('phd.verbose', 'bool', false) - ->setLocked(true) - ->setBoolOptions( - array( - pht('Verbose mode'), - pht('Normal mode'), - )) - ->setSummary(pht("Launch daemons in 'verbose' mode by default.")) - ->setDescription( - pht( - "Launch daemons in 'verbose' mode by default. This creates a lot ". - "of output, but can help debug issues. Daemons launched in debug ". - "mode with '%s' are always launched in verbose mode. ". - "See also '%s'.", - 'phd debug', - 'phd.trace')), $this->newOption('phd.user', 'string', null) ->setLocked(true) ->setSummary(pht('System user to run daemons as.')) ->setDescription( pht( 'Specify a system user to run the daemons as. Primarily, this '. 'user will own the working copies of any repositories that '. 'Phabricator imports or manages. This option is new and '. 'experimental.')), - $this->newOption('phd.trace', 'bool', false) - ->setLocked(true) - ->setBoolOptions( - array( - pht('Trace mode'), - pht('Normal mode'), - )) - ->setSummary(pht("Launch daemons in 'trace' mode by default.")) - ->setDescription( - pht( - "Launch daemons in 'trace' mode by default. This creates an ". - "ENORMOUS amount of output, but can help debug issues. Daemons ". - "launched in debug mode with '%s' are always launched in ". - "trace mode. See also '%s'.", - 'phd debug', - 'phd.verbose')), $this->newOption('phd.garbage-collection', 'wild', array()) ->setLocked(true) ->setLockedMessage( pht( 'This option can not be edited from the web UI. Use %s to adjust '. 'garbage collector policies.', phutil_tag('tt', array(), 'bin/garbage set-policy'))) ->setSummary(pht('Retention policies for garbage collection.')) ->setDescription( pht( 'Customizes retention policies for garbage collectors.')), ); } } diff --git a/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php b/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php index 05d94e218d..b9645323c2 100644 --- a/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php +++ b/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php @@ -1,611 +1,611 @@ setAncestorClass('PhutilDaemon') ->setConcreteOnly(true) ->selectSymbolsWithoutLoading(); } final protected function getLogDirectory() { $path = PhabricatorEnv::getEnvConfig('phd.log-directory'); return $this->getControlDirectory($path); } private function getControlDirectory($path) { if (!Filesystem::pathExists($path)) { list($err) = exec_manual('mkdir -p %s', $path); if ($err) { throw new Exception( pht( "%s requires the directory '%s' to exist, but it does not exist ". "and could not be created. Create this directory or update ". "'%s' in your configuration to point to an existing ". "directory.", 'phd', $path, 'phd.log-directory')); } } return $path; } private function findDaemonClass($substring) { $symbols = $this->loadAvailableDaemonClasses(); $symbols = ipull($symbols, 'name'); $match = array(); foreach ($symbols as $symbol) { if (stripos($symbol, $substring) !== false) { if (strtolower($symbol) == strtolower($substring)) { $match = array($symbol); break; } else { $match[] = $symbol; } } } if (count($match) == 0) { throw new PhutilArgumentUsageException( pht( "No daemons match '%s'! Use '%s' for a list of available daemons.", $substring, 'phd list')); } else if (count($match) > 1) { throw new PhutilArgumentUsageException( pht( "Specify a daemon unambiguously. Multiple daemons match '%s': %s.", $substring, implode(', ', $match))); } return head($match); } final protected function launchDaemons( array $daemons, $debug, $run_as_current_user = false) { // Convert any shorthand classnames like "taskmaster" into proper class // names. foreach ($daemons as $key => $daemon) { $class = $this->findDaemonClass($daemon['class']); $daemons[$key]['class'] = $class; } $console = PhutilConsole::getConsole(); if (!$run_as_current_user) { // Check if the script is started as the correct user $phd_user = PhabricatorEnv::getEnvConfig('phd.user'); $current_user = posix_getpwuid(posix_geteuid()); $current_user = $current_user['name']; if ($phd_user && $phd_user != $current_user) { if ($debug) { throw new PhutilArgumentUsageException( pht( "You are trying to run a daemon as a nonstandard user, ". "and `%s` was not able to `%s` to the correct user. \n". 'Phabricator is configured to run daemons as "%s", '. 'but the current user is "%s". '."\n". 'Use `%s` to run as a different user, pass `%s` to ignore this '. 'warning, or edit `%s` to change the configuration.', 'phd', 'sudo', $phd_user, $current_user, 'sudo', '--as-current-user', 'phd.user')); } else { $this->runDaemonsAsUser = $phd_user; $console->writeOut(pht('Starting daemons as %s', $phd_user)."\n"); } } } $this->printLaunchingDaemons($daemons, $debug); $trace = PhutilArgumentParser::isTraceModeEnabled(); $flags = array(); - if ($trace || PhabricatorEnv::getEnvConfig('phd.trace')) { + if ($trace) { $flags[] = '--trace'; } - if ($debug || PhabricatorEnv::getEnvConfig('phd.verbose')) { + if ($debug) { $flags[] = '--verbose'; } $instance = $this->getInstance(); if ($instance) { $flags[] = '-l'; $flags[] = $instance; } $config = array(); if (!$debug) { $config['daemonize'] = true; } if (!$debug) { $config['log'] = $this->getLogDirectory().'/daemons.log'; } $config['daemons'] = $daemons; $command = csprintf('./phd-daemon %Ls', $flags); $phabricator_root = dirname(phutil_get_library_root('phabricator')); $daemon_script_dir = $phabricator_root.'/scripts/daemon/'; if ($debug) { // Don't terminate when the user sends ^C; it will be sent to the // subprocess which will terminate normally. pcntl_signal( SIGINT, array(__CLASS__, 'ignoreSignal')); echo "\n phabricator/scripts/daemon/ \$ {$command}\n\n"; $tempfile = new TempFile('daemon.config'); Filesystem::writeFile($tempfile, json_encode($config)); phutil_passthru( '(cd %s && exec %C < %s)', $daemon_script_dir, $command, $tempfile); } else { try { $this->executeDaemonLaunchCommand( $command, $daemon_script_dir, $config, $this->runDaemonsAsUser); } catch (Exception $ex) { throw new PhutilArgumentUsageException( pht( 'Daemons are configured to run as user "%s" in configuration '. 'option `%s`, but the current user is "%s" and `phd` was unable '. 'to switch to the correct user with `sudo`. Command output:'. "\n\n". '%s', $phd_user, 'phd.user', $current_user, $ex->getMessage())); } } } private function executeDaemonLaunchCommand( $command, $daemon_script_dir, array $config, $run_as_user = null) { $is_sudo = false; if ($run_as_user) { // If anything else besides sudo should be // supported then insert it here (runuser, su, ...) $command = csprintf( 'sudo -En -u %s -- %C', $run_as_user, $command); $is_sudo = true; } $future = new ExecFuture('exec %C', $command); // Play games to keep 'ps' looking reasonable. $future->setCWD($daemon_script_dir); $future->write(json_encode($config)); list($stdout, $stderr) = $future->resolvex(); if ($is_sudo) { // On OSX, `sudo -n` exits 0 when the user does not have permission to // switch accounts without a password. This is not consistent with // sudo on Linux, and seems buggy/broken. Check for this by string // matching the output. if (preg_match('/sudo: a password is required/', $stderr)) { throw new Exception( pht( '%s exited with a zero exit code, but emitted output '. 'consistent with failure under OSX.', 'sudo')); } } } public static function ignoreSignal($signo) { return; } 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 daemons on this machine.\n", $ext); 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 daemons on this machine.\n", $function.'()'); exit(1); } } } /* -( Commands )----------------------------------------------------------- */ final protected function executeStartCommand(array $options) { PhutilTypeSpec::checkMap( $options, array( 'keep-leases' => 'optional bool', 'force' => 'optional bool', 'reserve' => 'optional float', )); $console = PhutilConsole::getConsole(); if (!idx($options, 'force')) { $process_refs = $this->getOverseerProcessRefs(); if ($process_refs) { $this->logWarn( pht('RUNNING DAEMONS'), pht('Daemons are already running:')); fprintf(STDERR, '%s', "\n"); foreach ($process_refs as $process_ref) { fprintf( STDERR, '%s', tsprintf( " %s %s\n", $process_ref->getPID(), $process_ref->getCommand())); } fprintf(STDERR, '%s', "\n"); $this->logFail( pht('RUNNING DAEMONS'), pht( 'Use "phd stop" to stop daemons, "phd restart" to restart '. 'daemons, or "phd start --force" to ignore running processes.')); exit(1); } } if (idx($options, 'keep-leases')) { $console->writeErr("%s\n", pht('Not touching active task queue leases.')); } else { $console->writeErr("%s\n", pht('Freeing active task leases...')); $count = $this->freeActiveLeases(); $console->writeErr( "%s\n", pht('Freed %s task lease(s).', new PhutilNumber($count))); } $daemons = array( array( 'class' => 'PhabricatorRepositoryPullLocalDaemon', 'label' => 'pull', ), array( 'class' => 'PhabricatorTriggerDaemon', 'label' => 'trigger', ), array( 'class' => 'PhabricatorFactDaemon', 'label' => 'fact', ), array( 'class' => 'PhabricatorTaskmasterDaemon', 'label' => 'task', 'pool' => PhabricatorEnv::getEnvConfig('phd.taskmasters'), 'reserve' => idx($options, 'reserve', 0), ), ); $this->launchDaemons($daemons, $is_debug = false); $console->writeErr("%s\n", pht('Done.')); return 0; } final protected function executeStopCommand(array $options) { $grace_period = idx($options, 'graceful', 15); $force = idx($options, 'force'); $query = id(new PhutilProcessQuery()) ->withIsOverseer(true); $instance = $this->getInstance(); if ($instance !== null && !$force) { $query->withInstances(array($instance)); } try { $process_refs = $query->execute(); } catch (Exception $ex) { // See T13321. If this fails for some reason, just continue for now so // that daemon management still works. In the long run, we don't expect // this to fail, but I don't want to break this workflow while we iron // bugs out. // See T12827. Particularly, this is likely to fail on Solaris. phlog($ex); $process_refs = array(); } if (!$process_refs) { if ($instance !== null && !$force) { $this->logInfo( pht('NO DAEMONS'), pht( 'There are no running daemons for the current instance ("%s"). '. 'Use "--force" to stop daemons for all instances.', $instance)); } else { $this->logInfo( pht('NO DAEMONS'), pht('There are no running daemons.')); } return 0; } $process_refs = mpull($process_refs, null, 'getPID'); $stop_pids = array_keys($process_refs); $live_pids = $this->sendStopSignals($stop_pids, $grace_period); $stop_pids = array_fuse($stop_pids); $live_pids = array_fuse($live_pids); $dead_pids = array_diff_key($stop_pids, $live_pids); foreach ($dead_pids as $dead_pid) { $dead_ref = $process_refs[$dead_pid]; $this->logOkay( pht('STOP'), pht( 'Stopped PID %d ("%s")', $dead_pid, $dead_ref->getCommand())); } foreach ($live_pids as $live_pid) { $live_ref = $process_refs[$live_pid]; $this->logFail( pht('SURVIVED'), pht( 'Unable to stop PID %d ("%s").', $live_pid, $live_ref->getCommand())); } if ($live_pids) { $this->logWarn( pht('SURVIVORS'), pht( 'Unable to stop all daemon processes. You may need to run this '. 'command as root with "sudo".')); } return 0; } final protected function executeReloadCommand(array $pids) { $process_refs = $this->getOverseerProcessRefs(); if (!$process_refs) { $this->logInfo( pht('NO DAEMONS'), pht('There are no running daemon processes to reload.')); return 0; } foreach ($process_refs as $process_ref) { $pid = $process_ref->getPID(); $this->logInfo( pht('RELOAD'), pht('Reloading process %d...', $pid)); posix_kill($pid, SIGHUP); } return 0; } private function sendStopSignals($pids, $grace_period) { // If we're doing a graceful shutdown, try SIGINT first. if ($grace_period) { $pids = $this->sendSignal($pids, SIGINT, $grace_period); } // If we still have daemons, SIGTERM them. if ($pids) { $pids = $this->sendSignal($pids, SIGTERM, 15); } // If the overseer is still alive, SIGKILL it. if ($pids) { $pids = $this->sendSignal($pids, SIGKILL, 0); } return $pids; } private function sendSignal(array $pids, $signo, $wait) { $console = PhutilConsole::getConsole(); $pids = array_fuse($pids); foreach ($pids as $key => $pid) { if (!$pid) { // NOTE: We must have a PID to signal a daemon, since sending a signal // to PID 0 kills this process. unset($pids[$key]); continue; } switch ($signo) { case SIGINT: $message = pht('Interrupting process %d...', $pid); break; case SIGTERM: $message = pht('Terminating process %d...', $pid); break; case SIGKILL: $message = pht('Killing process %d...', $pid); break; } $console->writeOut("%s\n", $message); posix_kill($pid, $signo); } if ($wait) { $start = PhabricatorTime::getNow(); do { foreach ($pids as $key => $pid) { if (!PhabricatorDaemonReference::isProcessRunning($pid)) { $console->writeOut(pht('Process %d exited.', $pid)."\n"); unset($pids[$key]); } } if (empty($pids)) { break; } usleep(100000); } while (PhabricatorTime::getNow() < $start + $wait); } return $pids; } private function freeActiveLeases() { $task_table = id(new PhabricatorWorkerActiveTask()); $conn_w = $task_table->establishConnection('w'); queryfx( $conn_w, 'UPDATE %T SET leaseExpires = UNIX_TIMESTAMP() WHERE leaseExpires > UNIX_TIMESTAMP()', $task_table->getTableName()); return $conn_w->getAffectedRows(); } private function printLaunchingDaemons(array $daemons, $debug) { $console = PhutilConsole::getConsole(); if ($debug) { $console->writeOut(pht('Launching daemons (in debug mode):')); } else { $console->writeOut(pht('Launching daemons:')); } $log_dir = $this->getLogDirectory().'/daemons.log'; $console->writeOut( "\n%s\n\n", pht('(Logs will appear in "%s".)', $log_dir)); foreach ($daemons as $daemon) { $pool_size = pht('(Pool: %s)', idx($daemon, 'pool', 1)); $console->writeOut( " %s %s\n", $pool_size, $daemon['class'], implode(' ', idx($daemon, 'argv', array()))); } $console->writeOut("\n"); } protected function getAutoscaleReserveArgument() { return array( 'name' => 'autoscale-reserve', 'param' => 'ratio', 'help' => pht( 'Specify a proportion of machine memory which must be free '. 'before autoscale pools will grow. For example, a value of 0.25 '. 'means that pools will not grow unless the machine has at least '. '25%%%% of its RAM free.'), ); } private function selectDaemonPIDs(array $daemons, array $pids) { $console = PhutilConsole::getConsole(); $running_pids = array_fuse(mpull($daemons, 'getPID')); if (!$pids) { $select_pids = $running_pids; } else { // We were given a PID or set of PIDs to kill. $select_pids = array(); foreach ($pids as $key => $pid) { if (!preg_match('/^\d+$/', $pid)) { $console->writeErr(pht("PID '%s' is not a valid PID.", $pid)."\n"); continue; } else if (empty($running_pids[$pid])) { $console->writeErr( "%s\n", pht( 'PID "%d" is not a known Phabricator daemon PID.', $pid)); continue; } else { $select_pids[$pid] = $pid; } } } return $select_pids; } protected function getOverseerProcessRefs() { $query = id(new PhutilProcessQuery()) ->withIsOverseer(true); $instance = PhabricatorEnv::getEnvConfig('cluster.instance'); if ($instance !== null) { $query->withInstances(array($instance)); } return $query->execute(); } protected function getInstance() { return PhabricatorEnv::getEnvConfig('cluster.instance'); } } diff --git a/src/infrastructure/daemon/workers/PhabricatorTaskmasterDaemon.php b/src/infrastructure/daemon/workers/PhabricatorTaskmasterDaemon.php index c2276843db..12b06131d8 100644 --- a/src/infrastructure/daemon/workers/PhabricatorTaskmasterDaemon.php +++ b/src/infrastructure/daemon/workers/PhabricatorTaskmasterDaemon.php @@ -1,69 +1,69 @@ setLimit(1) ->execute(); if ($tasks) { $this->willBeginWork(); foreach ($tasks as $task) { $id = $task->getID(); $class = $task->getTaskClass(); $this->log(pht('Working on task %d (%s)...', $id, $class)); $task = $task->executeTask(); $ex = $task->getExecutionException(); if ($ex) { if ($ex instanceof PhabricatorWorkerPermanentFailureException) { // NOTE: Make sure these reach the daemon log, even when not - // running in "phd.verbose" mode. See T12803 for discussion. + // running in verbose mode. See T12803 for discussion. $log_exception = new PhutilProxyException( pht( 'Task "%s" encountered a permanent failure and was '. 'cancelled.', $id), $ex); phlog($log_exception); } else if ($ex instanceof PhabricatorWorkerYieldException) { $this->log(pht('Task %s yielded.', $id)); } else { $this->log(pht('Task %d failed!', $id)); throw new PhutilProxyException( pht('Error while executing Task ID %d.', $id), $ex); } } else { $this->log(pht('Task %s complete! Moved to archive.', $id)); } } $sleep = 0; } else { if ($this->getIdleDuration() > 15) { $hibernate_duration = phutil_units('3 minutes in seconds'); if ($this->shouldHibernate($hibernate_duration)) { break; } } // When there's no work, sleep for one second. The pool will // autoscale down if we're continuously idle for an extended period // of time. $this->willBeginIdle(); $sleep = 1; } $this->sleep($sleep); } while (!$this->shouldExit()); } }