Changeset View
Changeset View
Standalone View
Standalone View
src/runtime/ArcanistRuntime.php
- This file was added.
| <?php | |||||
| final class ArcanistRuntime { | |||||
| private $workflows; | |||||
| private $logEngine; | |||||
| private $lastInterruptTime; | |||||
| private $stack = array(); | |||||
| public function execute(array $argv) { | |||||
| try { | |||||
| $this->checkEnvironment(); | |||||
| } catch (Exception $ex) { | |||||
| echo "CONFIGURATION ERROR\n\n"; | |||||
| echo $ex->getMessage(); | |||||
| echo "\n\n"; | |||||
| return 1; | |||||
| } | |||||
| PhutilTranslator::getInstance() | |||||
| ->setLocale(PhutilLocale::loadLocale('en_US')) | |||||
| ->setTranslations(PhutilTranslation::getTranslationMapForLocale('en_US')); | |||||
| $log = new ArcanistLogEngine(); | |||||
| $this->logEngine = $log; | |||||
| try { | |||||
| return $this->executeCore($argv); | |||||
| } catch (ArcanistConduitException $ex) { | |||||
| $log->writeError(pht('CONDUIT'), $ex->getMessage()); | |||||
| } catch (PhutilArgumentUsageException $ex) { | |||||
| $log->writeError(pht('USAGE EXCEPTION'), $ex->getMessage()); | |||||
| } catch (ArcanistUserAbortException $ex) { | |||||
| $log->writeError(pht('---'), $ex->getMessage()); | |||||
| } | |||||
| return 1; | |||||
| } | |||||
| private function executeCore(array $argv) { | |||||
| $log = $this->getLogEngine(); | |||||
| $config_args = array( | |||||
| array( | |||||
| 'name' => 'library', | |||||
| 'param' => 'path', | |||||
| 'help' => pht('Load a library.'), | |||||
| 'repeat' => true, | |||||
| ), | |||||
| array( | |||||
| 'name' => 'config', | |||||
| 'param' => 'key=value', | |||||
| 'repeat' => true, | |||||
| 'help' => pht('Specify a runtime configuration value.'), | |||||
| ), | |||||
| array( | |||||
| 'name' => 'config-file', | |||||
| 'param' => 'path', | |||||
| 'repeat' => true, | |||||
| 'help' => pht( | |||||
| 'Load one or more configuration files. If this flag is provided, '. | |||||
| 'the system and user configuration files are ignored.'), | |||||
| ), | |||||
| ); | |||||
| $args = id(new PhutilArgumentParser($argv)) | |||||
| ->parseStandardArguments(); | |||||
| $is_trace = $args->getArg('trace'); | |||||
| $log->setShowTraceMessages($is_trace); | |||||
| $log->writeTrace(pht('ARGV'), csprintf('%Ls', $argv)); | |||||
| // We're installing the signal handler after parsing "--trace" so that it | |||||
| // can emit debugging messages. This means there's a very small window at | |||||
| // startup where signals have no special handling, but we couldn't really | |||||
| // route them or do anything interesting with them anyway. | |||||
| $this->installSignalHandler(); | |||||
| $args->parsePartial($config_args, true); | |||||
| $config_engine = $this->loadConfiguration($args); | |||||
| $config = $config_engine->newConfigurationSourceList(); | |||||
| $this->loadLibraries($args, $config); | |||||
| // Now that we've loaded libraries, we can validate configuration. | |||||
| // Do this before continuing since configuration can impact other | |||||
| // behaviors immediately and we want to catch any issues right away. | |||||
| $config->setConfigOptions($config_engine->newConfigOptionsMap()); | |||||
| $config->validateConfiguration($this); | |||||
| $toolset = $this->newToolset($argv); | |||||
| $args->parsePartial($toolset->getToolsetArguments()); | |||||
| $workflows = $this->newWorkflows($toolset); | |||||
| $this->workflows = $workflows; | |||||
| $phutil_workflows = array(); | |||||
| foreach ($workflows as $key => $workflow) { | |||||
| $phutil_workflows[$key] = $workflow->newPhutilWorkflow(); | |||||
| $workflow | |||||
| ->setRuntime($this) | |||||
| ->setConfigurationEngine($config_engine) | |||||
| ->setConfigurationSourceList($config); | |||||
| } | |||||
| $unconsumed_argv = $args->getUnconsumedArgumentVector(); | |||||
| if (!$unconsumed_argv) { | |||||
| // TOOLSETS: This means the user just ran "arc" or some other top-level | |||||
| // toolset without any workflow argument. We should give them a summary | |||||
| // of the toolset, a list of workflows, and a pointer to "arc help" for | |||||
| // more details. | |||||
| // A possible exception is "arc --help", which should perhaps pass | |||||
| // through and act like "arc help". | |||||
| throw new PhutilArgumentUsageException(pht('Choose a workflow!')); | |||||
| } | |||||
| $alias_effects = id(new ArcanistAliasEngine()) | |||||
| ->setRuntime($this) | |||||
| ->setToolset($toolset) | |||||
| ->setWorkflows($workflows) | |||||
| ->setConfigurationSourceList($config) | |||||
| ->resolveAliases($unconsumed_argv); | |||||
| $result_argv = $this->applyAliasEffects($alias_effects, $unconsumed_argv); | |||||
| $args->setUnconsumedArgumentVector($result_argv); | |||||
| // TOOLSETS: Some day, stop falling through to the old "arc" runtime. | |||||
| try { | |||||
| return $args->parseWorkflowsFull($phutil_workflows); | |||||
| } catch (PhutilArgumentUsageException $usage_exception) { | |||||
| $log->writeHint( | |||||
| pht('(::)'), | |||||
| pht( | |||||
| 'Workflow is unrecognized by modern "arc", falling through '. | |||||
| 'to classic mode.')); | |||||
| } | |||||
| $arcanist_root = phutil_get_library_root('arcanist'); | |||||
| $arcanist_root = dirname($arcanist_root); | |||||
| $bin = $arcanist_root.'/scripts/arcanist.php'; | |||||
| $err = phutil_passthru( | |||||
| 'php -f %R -- %Ls', | |||||
| $bin, | |||||
| array_slice($argv, 1)); | |||||
| return $err; | |||||
| } | |||||
| /** | |||||
| * Perform some sanity checks against the possible diversity of PHP builds in | |||||
| * the wild, like very old versions and builds that were compiled with flags | |||||
| * that exclude core functionality. | |||||
| */ | |||||
| private function checkEnvironment() { | |||||
| // NOTE: We don't have phutil_is_windows() yet here. | |||||
| $is_windows = (DIRECTORY_SEPARATOR != '/'); | |||||
| // We use stream_socket_pair() which is not available on Windows earlier. | |||||
| $min_version = ($is_windows ? '5.3.0' : '5.2.3'); | |||||
| $cur_version = phpversion(); | |||||
| if (version_compare($cur_version, $min_version, '<')) { | |||||
| $message = sprintf( | |||||
| 'You are running a version of PHP ("%s"), which is older than the '. | |||||
| 'minimum supported version ("%s"). Update PHP to continue.', | |||||
| $cur_version, | |||||
| $min_version); | |||||
| throw new Exception($message); | |||||
| } | |||||
| if ($is_windows) { | |||||
| $need_functions = array( | |||||
| 'curl_init' => array('builtin-dll', 'php_curl.dll'), | |||||
| ); | |||||
| } else { | |||||
| $need_functions = array( | |||||
| 'curl_init' => array( | |||||
| 'text', | |||||
| "You need to install the cURL PHP extension, maybe with ". | |||||
| "'apt-get install php5-curl' or 'yum install php53-curl' or ". | |||||
| "something similar.", | |||||
| ), | |||||
| 'json_decode' => array('flag', '--without-json'), | |||||
| ); | |||||
| } | |||||
| $problems = array(); | |||||
| $config = null; | |||||
| $show_config = false; | |||||
| foreach ($need_functions as $fname => $resolution) { | |||||
| if (function_exists($fname)) { | |||||
| continue; | |||||
| } | |||||
| static $info; | |||||
| if ($info === null) { | |||||
| ob_start(); | |||||
| phpinfo(INFO_GENERAL); | |||||
| $info = ob_get_clean(); | |||||
| $matches = null; | |||||
| if (preg_match('/^Configure Command =>\s*(.*?)$/m', $info, $matches)) { | |||||
| $config = $matches[1]; | |||||
| } | |||||
| } | |||||
| list($what, $which) = $resolution; | |||||
| if ($what == 'flag' && strpos($config, $which) !== false) { | |||||
| $show_config = true; | |||||
| $problems[] = sprintf( | |||||
| 'The build of PHP you are running was compiled with the configure '. | |||||
| 'flag "%s", which means it does not support the function "%s()". '. | |||||
| 'This function is required for Arcanist to run. Install a standard '. | |||||
| 'build of PHP or rebuild it without this flag. You may also be '. | |||||
| 'able to build or install the relevant extension separately.', | |||||
| $which, | |||||
| $fname); | |||||
| continue; | |||||
| } | |||||
| if ($what == 'builtin-dll') { | |||||
| $problems[] = sprintf( | |||||
| 'The build of PHP you are running does not have the "%s" extension '. | |||||
| 'enabled. Edit your php.ini file and uncomment the line which '. | |||||
| 'reads "extension=%s".', | |||||
| $which, | |||||
| $which); | |||||
| continue; | |||||
| } | |||||
| if ($what == 'text') { | |||||
| $problems[] = $which; | |||||
| continue; | |||||
| } | |||||
| $problems[] = sprintf( | |||||
| 'The build of PHP you are running is missing the required function '. | |||||
| '"%s()". Rebuild PHP or install the extension which provides "%s()".', | |||||
| $fname, | |||||
| $fname); | |||||
| } | |||||
| if ($problems) { | |||||
| if ($show_config) { | |||||
| $problems[] = "PHP was built with this configure command:\n\n{$config}"; | |||||
| } | |||||
| $problems = implode("\n\n", $problems); | |||||
| throw new Exception($problems); | |||||
| } | |||||
| } | |||||
| private function loadConfiguration(PhutilArgumentParser $args) { | |||||
| $engine = id(new ArcanistConfigurationEngine()) | |||||
| ->setArguments($args); | |||||
| $working_copy = ArcanistWorkingCopy::newFromWorkingDirectory(getcwd()); | |||||
| if ($working_copy) { | |||||
| $engine->setWorkingCopy($working_copy); | |||||
| } | |||||
| return $engine; | |||||
| } | |||||
| private function loadLibraries( | |||||
| PhutilArgumentParser $args, | |||||
| ArcanistConfigurationSourceList $config) { | |||||
| // TOOLSETS: Make this work again -- or replace it entirely with package | |||||
| // management? | |||||
| return; | |||||
| $is_trace = $args->getArg('trace'); | |||||
| $load = array(); | |||||
| $working_copy = $this->getWorkingCopy(); | |||||
| $cli_libraries = $args->getArg('library'); | |||||
| if ($cli_libraries) { | |||||
| $load[] = array( | |||||
| '--library', | |||||
| $cli_libraries, | |||||
| ); | |||||
| } else { | |||||
| $system_config = $config->readSystemArcConfig(); | |||||
| $load[] = array( | |||||
| $config->getSystemArcConfigLocation(), | |||||
| idx($system_config, 'load', array()), | |||||
| ); | |||||
| $global_config = $config->readUserArcConfig(); | |||||
| $load[] = array( | |||||
| $config->getUserConfigurationFileLocation(), | |||||
| idx($global_config, 'load', array()), | |||||
| ); | |||||
| $load[] = array( | |||||
| '.arcconfig', | |||||
| $working_copy->getProjectConfig('load'), | |||||
| ); | |||||
| $load[] = array( | |||||
| // TODO: We could explain exactly where this is coming from more | |||||
| // clearly. | |||||
| './.../arc/config', | |||||
| $working_copy->getLocalConfig('load'), | |||||
| ); | |||||
| $load[] = array( | |||||
| '--config load=...', | |||||
| $config->getRuntimeConfig('load', array()), | |||||
| ); | |||||
| } | |||||
| foreach ($load as $spec) { | |||||
| list($source, $libraries) = $spec; | |||||
| if ($is_trace) { | |||||
| $this->logTrace( | |||||
| pht('LOAD'), | |||||
| pht( | |||||
| 'Loading libraries from "%s"...', | |||||
| $source)); | |||||
| } | |||||
| if (!$libraries) { | |||||
| if ($is_trace) { | |||||
| $this->logTrace(pht('NONE'), pht('Nothing to load.')); | |||||
| } | |||||
| continue; | |||||
| } | |||||
| if (!is_array($libraries)) { | |||||
| throw new PhutilArgumentUsageException( | |||||
| pht( | |||||
| 'Libraries specified by "%s" are not formatted correctly. '. | |||||
| 'Expected a list of paths. Check your configuration.', | |||||
| $source)); | |||||
| } | |||||
| foreach ($libraries as $library) { | |||||
| $this->loadLibrary($source, $library, $working_copy, $is_trace); | |||||
| } | |||||
| } | |||||
| } | |||||
| private function loadLibrary( | |||||
| $source, | |||||
| $location, | |||||
| ArcanistWorkingCopyIdentity $working_copy, | |||||
| $is_trace) { | |||||
| // Try to resolve the library location. We look in several places, in | |||||
| // order: | |||||
| // | |||||
| // 1. Inside the working copy. This is for phutil libraries within the | |||||
| // project. For instance "library/src" will resolve to | |||||
| // "./library/src" if it exists. | |||||
| // 2. In the same directory as the working copy. This allows you to | |||||
| // check out a library alongside a working copy and reference it. | |||||
| // If we haven't resolved yet, "library/src" will try to resolve to | |||||
| // "../library/src" if it exists. | |||||
| // 3. Using normal libphutil resolution rules. Generally, this means | |||||
| // that it checks for libraries next to libphutil, then libraries | |||||
| // in the PHP include_path. | |||||
| // | |||||
| // Note that absolute paths will just resolve absolutely through rule (1). | |||||
| $resolved = false; | |||||
| // Check inside the working copy. This also checks absolute paths, since | |||||
| // they'll resolve absolute and just ignore the project root. | |||||
| $resolved_location = Filesystem::resolvePath( | |||||
| $location, | |||||
| $working_copy->getProjectRoot()); | |||||
| if (Filesystem::pathExists($resolved_location)) { | |||||
| $location = $resolved_location; | |||||
| $resolved = true; | |||||
| } | |||||
| // If we didn't find anything, check alongside the working copy. | |||||
| if (!$resolved) { | |||||
| $resolved_location = Filesystem::resolvePath( | |||||
| $location, | |||||
| dirname($working_copy->getProjectRoot())); | |||||
| if (Filesystem::pathExists($resolved_location)) { | |||||
| $location = $resolved_location; | |||||
| $resolved = true; | |||||
| } | |||||
| } | |||||
| if ($is_trace) { | |||||
| $this->logTrace( | |||||
| pht('LOAD'), | |||||
| pht('Loading phutil library from "%s"...', $location)); | |||||
| } | |||||
| $error = null; | |||||
| try { | |||||
| phutil_load_library($location); | |||||
| } catch (PhutilBootloaderException $ex) { | |||||
| fwrite( | |||||
| STDERR, | |||||
| '%s', | |||||
| tsprintf( | |||||
| "**<bg:red> %s </bg>** %s\n", | |||||
| pht( | |||||
| 'Failed to load phutil library at location "%s". This library '. | |||||
| 'is specified by "%s". Check that the setting is correct and '. | |||||
| 'the library is located in the right place.', | |||||
| $location, | |||||
| $source))); | |||||
| $prompt = pht('Continue without loading library?'); | |||||
| if (!phutil_console_confirm($prompt)) { | |||||
| throw $ex; | |||||
| } | |||||
| } catch (PhutilLibraryConflictException $ex) { | |||||
| if ($ex->getLibrary() != 'arcanist') { | |||||
| throw $ex; | |||||
| } | |||||
| // NOTE: If you are running `arc` against itself, we ignore the library | |||||
| // conflict created by loading the local `arc` library (in the current | |||||
| // working directory) and continue without loading it. | |||||
| // This means we only execute code in the `arcanist/` directory which is | |||||
| // associated with the binary you are running, whereas we would normally | |||||
| // execute local code. | |||||
| // This can make `arc` development slightly confusing if your setup is | |||||
| // especially bizarre, but it allows `arc` to be used in automation | |||||
| // workflows more easily. For some context, see PHI13. | |||||
| $executing_directory = dirname(dirname(__FILE__)); | |||||
| $working_directory = dirname($location); | |||||
| fwrite( | |||||
| STDERR, | |||||
| tsprintf( | |||||
| "**<bg:yellow> %s </bg>** %s\n", | |||||
| pht('VERY META'), | |||||
| pht( | |||||
| 'You are running one copy of Arcanist (at path "%s") against '. | |||||
| 'another copy of Arcanist (at path "%s"). Code in the current '. | |||||
| 'working directory will not be loaded or executed.', | |||||
| $executing_directory, | |||||
| $working_directory))); | |||||
| } | |||||
| } | |||||
| private function newToolset(array $argv) { | |||||
| $binary = basename($argv[0]); | |||||
| $toolsets = ArcanistToolset::newToolsetMap(); | |||||
| if (!isset($toolsets[$binary])) { | |||||
| throw new PhutilArgumentUsageException( | |||||
| pht( | |||||
| 'Arcanist toolset "%s" is unknown. The Arcanist binary should '. | |||||
| 'be executed so that "argv[0]" identifies a supported toolset. '. | |||||
| 'Rename the binary or install the library that provides the '. | |||||
| 'desired toolset. Current available toolsets: %s.', | |||||
| $binary, | |||||
| implode(', ', array_keys($toolsets)))); | |||||
| } | |||||
| return $toolsets[$binary]; | |||||
| } | |||||
| private function newWorkflows(ArcanistToolset $toolset) { | |||||
| $workflows = id(new PhutilClassMapQuery()) | |||||
| ->setAncestorClass('ArcanistWorkflow') | |||||
| ->execute(); | |||||
| foreach ($workflows as $key => $workflow) { | |||||
| if (!$workflow->supportsToolset($toolset)) { | |||||
| unset($workflows[$key]); | |||||
| } | |||||
| } | |||||
| $map = array(); | |||||
| foreach ($workflows as $workflow) { | |||||
| $key = $workflow->getWorkflowName(); | |||||
| if (isset($map[$key])) { | |||||
| throw new Exception( | |||||
| pht( | |||||
| 'Two workflows ("%s" and "%s") both have the same name ("%s") '. | |||||
| 'and both support the current toolset ("%s", "%s"). Each '. | |||||
| 'workflow in a given toolset must have a unique name.', | |||||
| get_class($workflow), | |||||
| get_class($map[$key]), | |||||
| get_class($toolset), | |||||
| $toolset->getToolsetKey())); | |||||
| } | |||||
| $map[$key] = id(clone $workflow) | |||||
| ->setToolset($toolset); | |||||
| } | |||||
| return $map; | |||||
| } | |||||
| public function getWorkflows() { | |||||
| return $this->workflows; | |||||
| } | |||||
| public function getLogEngine() { | |||||
| return $this->logEngine; | |||||
| } | |||||
| private function applyAliasEffects(array $effects, array $argv) { | |||||
| assert_instances_of($effects, 'ArcanistAliasEffect'); | |||||
| $log = $this->getLogEngine(); | |||||
| $command = null; | |||||
| $arguments = null; | |||||
| foreach ($effects as $effect) { | |||||
| $message = $effect->getMessage(); | |||||
| if ($message !== null) { | |||||
| $log->writeInfo(pht('ALIAS'), $message); | |||||
| } | |||||
| if ($effect->getCommand()) { | |||||
| $command = $effect->getCommand(); | |||||
| $arguments = $effect->getArguments(); | |||||
| } | |||||
| } | |||||
| if ($command !== null) { | |||||
| $argv = array_merge(array($command), $arguments); | |||||
| } | |||||
| return $argv; | |||||
| } | |||||
| private function installSignalHandler() { | |||||
| $log = $this->getLogEngine(); | |||||
| if (!function_exists('pcntl_signal')) { | |||||
| $log->writeTrace( | |||||
| pht('PCNTL'), | |||||
| pht( | |||||
| 'Unable to install signal handler, pcntl_signal() unavailable. '. | |||||
| 'Continuing without signal handling.')); | |||||
| return; | |||||
| } | |||||
| // NOTE: SIGHUP, SIGTERM and SIGWINCH are handled by "PhutilSignalRouter". | |||||
| // This logic is largely similar to the logic there, but more specific to | |||||
| // Arcanist workflows. | |||||
| pcntl_signal(SIGINT, array($this, 'routeSignal')); | |||||
| } | |||||
| public function routeSignal($signo) { | |||||
| switch ($signo) { | |||||
| case SIGINT: | |||||
| $this->routeInterruptSignal($signo); | |||||
| break; | |||||
| } | |||||
| } | |||||
| private function routeInterruptSignal($signo) { | |||||
| $log = $this->getLogEngine(); | |||||
| $last_interrupt = $this->lastInterruptTime; | |||||
| $now = microtime(true); | |||||
| $this->lastInterruptTime = $now; | |||||
| $should_exit = false; | |||||
| // If we received another SIGINT recently, always exit. This implements | |||||
| // "press ^C twice in quick succession to exit" regardless of what the | |||||
| // workflow may decide to do. | |||||
| $interval = 2; | |||||
| if ($last_interrupt !== null) { | |||||
| if ($now - $last_interrupt < $interval) { | |||||
| $should_exit = true; | |||||
| } | |||||
| } | |||||
| $handler = null; | |||||
| if (!$should_exit) { | |||||
| // Look for an interrupt handler in the current workflow stack. | |||||
| $stack = $this->getWorkflowStack(); | |||||
| foreach ($stack as $workflow) { | |||||
| if ($workflow->canHandleSignal($signo)) { | |||||
| $handler = $workflow; | |||||
| break; | |||||
| } | |||||
| } | |||||
| // If no workflow in the current execution stack can handle an interrupt | |||||
| // signal, just exit on the first interrupt. | |||||
| if (!$handler) { | |||||
| $should_exit = true; | |||||
| } | |||||
| } | |||||
| // It's common for users to ^C on prompts. Write a newline before writing | |||||
| // a response to the interrupt so the behavior is a little cleaner. This | |||||
| // also avoids lines that read "^C [ INTERRUPT ] ...". | |||||
| $log->writeNewline(); | |||||
| if ($should_exit) { | |||||
| $log->writeHint( | |||||
| pht('INTERRUPT'), | |||||
| pht('Interrupted by SIGINT (^C).')); | |||||
| exit(128 + $signo); | |||||
| } | |||||
| $log->writeHint( | |||||
| pht('INTERRUPT'), | |||||
| pht('Press ^C again to exit.')); | |||||
| $handler->handleSignal($signo); | |||||
| } | |||||
| public function pushWorkflow(ArcanistWorkflow $workflow) { | |||||
| $this->stack[] = $workflow; | |||||
| return $this; | |||||
| } | |||||
| public function popWorkflow() { | |||||
| if (!$this->stack) { | |||||
| throw new Exception(pht('Trying to pop an empty workflow stack!')); | |||||
| } | |||||
| return array_pop($this->stack); | |||||
| } | |||||
| public function getWorkflowStack() { | |||||
| return $this->stack; | |||||
| } | |||||
| } | |||||