diff --git a/src/init/lib/PhutilBootloader.php b/src/init/lib/PhutilBootloader.php index 7a8759e8..ab6bd587 100644 --- a/src/init/lib/PhutilBootloader.php +++ b/src/init/lib/PhutilBootloader.php @@ -1,332 +1,336 @@ registerLibrary($name, $path); } public static function getInstance() { if (!self::$instance) { self::$instance = new PhutilBootloader(); } return self::$instance; } private function __construct() { // This method intentionally left blank. } public function getClassTree() { return $this->classTree; } public function registerInMemoryLibrary($name, $map) { $this->registeredLibraries[$name] = "memory:$name"; $this->inMemoryMaps[$name] = $map; $this->getLibraryMap($name); } public function registerLibrary($name, $path) { // Detect attempts to load the same library multiple times from different // locations. This might mean you're doing something silly like trying to // include two different versions of something, or it might mean you're // doing something subtle like running a different version of 'arc' on a // working copy of Arcanist. if (isset($this->registeredLibraries[$name])) { $old_path = $this->registeredLibraries[$name]; if ($old_path != $path) { throw new PhutilLibraryConflictException($name, $old_path, $path); } } $this->registeredLibraries[$name] = $path; // If we're loading libphutil itself, load the utility functions first so // we can safely call functions like "id()" when handling errors. In // particular, this improves error behavior when "utils.php" itself can // not load. if ($name === 'arcanist') { $root = $this->getLibraryRoot('arcanist'); $this->executeInclude($root.'/utils/utils.php'); } // For libphutil v2 libraries, load all functions when we load the library. if (!class_exists('PhutilSymbolLoader', false)) { $root = $this->getLibraryRoot('arcanist'); $this->executeInclude($root.'/symbols/PhutilSymbolLoader.php'); } $loader = new PhutilSymbolLoader(); $loader ->setLibrary($name) ->setType('function'); try { $loader->selectAndLoadSymbols(); } catch (PhutilBootloaderException $ex) { // Ignore this, it happens if a global function's file is removed or // similar. Worst case is that we fatal when calling the function, which // is no worse than fataling here. } catch (PhutilMissingSymbolException $ex) { // Ignore this, it happens if a global function is removed. Everything // else loaded so proceed forward: worst case is a fatal when we // hit a function call to a function which no longer exists, which is // no worse than fataling here. } if (empty($_SERVER['PHUTIL_DISABLE_RUNTIME_EXTENSIONS'])) { $extdir = $path.DIRECTORY_SEPARATOR.'extensions'; if (Filesystem::pathExists($extdir)) { $extensions = id(new FileFinder($extdir)) ->withSuffix('php') ->withType('f') ->withFollowSymlinks(true) ->setForceMode('php') ->find(); foreach ($extensions as $extension) { $this->loadExtension( $name, $path, $extdir.DIRECTORY_SEPARATOR.$extension); } } } return $this; } public function registerLibraryMap(array $map) { $this->libraryMaps[$this->currentLibrary] = $map; return $this; } public function getLibraryMap($name) { if (isset($this->extendedMaps[$name])) { return $this->extendedMaps[$name]; } if (empty($this->libraryMaps[$name])) { $root = $this->getLibraryRoot($name); $this->currentLibrary = $name; if (isset($this->inMemoryMaps[$name])) { $this->libraryMaps[$name] = $this->inMemoryMaps[$name]; } else { $okay = include $root.'/__phutil_library_map__.php'; if (!$okay) { throw new PhutilBootloaderException( "Include of '{$root}/__phutil_library_map__.php' failed!"); } } $map = $this->libraryMaps[$name]; $version = isset($map['__library_version__']) ? $map['__library_version__'] : 1; switch ($version) { case 1: throw new Exception( 'libphutil v1 libraries are no longer supported.'); case 2: // NOTE: In version 2 of the library format, all parents (both // classes and interfaces) are stored in the 'xmap'. The value is // either a string for a single parent (the common case) or an array // for multiple parents. foreach ($map['xmap'] as $child => $parents) { foreach ((array)$parents as $parent) { $this->classTree[$parent][] = $child; } } break; default: throw new Exception("Unsupported library version '{$version}'!"); } } $map = $this->libraryMaps[$name]; // If there's an extension map for this library, merge the maps. if (isset($this->extensionMaps[$name])) { $emap = $this->extensionMaps[$name]; foreach (array('function', 'class', 'xmap') as $dict_key) { if (!isset($emap[$dict_key])) { continue; } $map[$dict_key] += $emap[$dict_key]; } } $this->extendedMaps[$name] = $map; return $map; } public function getLibraryMapWithoutExtensions($name) { // This just does all the checks to make sure the library is valid, then // we throw away the result. $this->getLibraryMap($name); return $this->libraryMaps[$name]; } public function getLibraryRoot($name) { if (empty($this->registeredLibraries[$name])) { throw new PhutilBootloaderException( "The phutil library '{$name}' has not been loaded!"); } return $this->registeredLibraries[$name]; } public function getAllLibraries() { return array_keys($this->registeredLibraries); } public function loadLibrarySource($library, $source) { $path = $this->getLibraryRoot($library).'/'.$source; $this->executeInclude($path); } + public function loadLibrary($path) { + $this->executeInclude($path.'/__phutil_library_init__.php'); + } + private function executeInclude($path) { // Include the source using `include_once`, but convert any warnings or // recoverable errors into exceptions. // Some messages, including "Declaration of X should be compatible with Y", // do not cause `include_once` to return an error code. Use // error_get_last() to make sure we're catching everything in every PHP // version. // (Also, the severity of some messages changed between versions of PHP.) // Note that we may enter this method after some earlier, unrelated error. // In this case, error_get_last() will return information for that error. // In PHP7 and later we could use error_clear_last() to clear that error, // but the function does not exist in earlier versions of PHP. Instead, // check if the value has changed. // Some parser-like errors, including "class must implement all abstract // methods", cause PHP to fatal immediately with an E_ERROR. In these // cases, include_once() does not throw and never returns. We leave // reporting enabled for these errors since we don't have a way to do // anything more graceful. // Likewise, some errors, including "cannot redeclare Class::method()" // cause PHP to fatal immediately with E_COMPILE_ERROR. Treat these like // the similar errors which raise E_ERROR. // See also T12190. $old_last = error_get_last(); try { $old = error_reporting(E_ERROR | E_COMPILE_ERROR); $okay = include_once $path; error_reporting($old); } catch (Exception $ex) { throw $ex; } catch (ParseError $throwable) { // NOTE: As of PHP7, syntax errors may raise a ParseError (which is a // Throwable, not an Exception) with a useless message (like "syntax // error, unexpected ':'") and a trace which ends a level above this. // Treating this object normally results in an unusable message which // does not identify where the syntax error occurred. Converting it to // a string and taking the first line gives us something reasonable, // however. $message = (string)$throwable; $message = preg_split("/\n/", $message); $message = reset($message); throw new Exception($message); } if (!$okay) { throw new Exception("Source file \"{$path}\" failed to load."); } $new_last = error_get_last(); if ($new_last !== null) { if ($new_last !== $old_last) { $message = $new_last['message']; throw new Exception( "Error while loading file \"{$path}\": {$message}"); } } } private function loadExtension($library, $root, $path) { $old_functions = get_defined_functions(); $old_functions = array_fill_keys($old_functions['user'], true); $old_classes = array_fill_keys(get_declared_classes(), true); $old_interfaces = array_fill_keys(get_declared_interfaces(), true); $this->executeInclude($path); $new_functions = get_defined_functions(); $new_functions = array_fill_keys($new_functions['user'], true); $new_classes = array_fill_keys(get_declared_classes(), true); $new_interfaces = array_fill_keys(get_declared_interfaces(), true); $add_functions = array_diff_key($new_functions, $old_functions); $add_classes = array_diff_key($new_classes, $old_classes); $add_interfaces = array_diff_key($new_interfaces, $old_interfaces); // NOTE: We can't trust the path we loaded to be the location of these // symbols, because it might have loaded other paths. foreach ($add_functions as $func => $ignored) { $rfunc = new ReflectionFunction($func); $fpath = Filesystem::resolvePath($rfunc->getFileName(), $root); $this->extensionMaps[$library]['function'][$func] = $fpath; } foreach ($add_classes + $add_interfaces as $class => $ignored) { $rclass = new ReflectionClass($class); $cpath = Filesystem::resolvePath($rclass->getFileName(), $root); $this->extensionMaps[$library]['class'][$class] = $cpath; $xmap = $rclass->getInterfaceNames(); $parent = $rclass->getParentClass(); if ($parent) { $xmap[] = $parent->getName(); } if ($xmap) { foreach ($xmap as $parent_class) { $this->classTree[$parent_class][] = $class; } if (count($xmap) == 1) { $xmap = head($xmap); } $this->extensionMaps[$library]['xmap'][$class] = $xmap; } } // Clear the extended library cache (should one exist) so we know that // we need to rebuild it. unset($this->extendedMaps[$library]); } } diff --git a/src/init/lib/moduleutils.php b/src/init/lib/moduleutils.php index ad447cff..aba95f5b 100644 --- a/src/init/lib/moduleutils.php +++ b/src/init/lib/moduleutils.php @@ -1,53 +1,53 @@ getLibraryRoot($library); } function phutil_get_library_root_for_path($path) { foreach (Filesystem::walkToRoot($path) as $dir) { if (Filesystem::pathExists($dir.'/__phutil_library_init__.php')) { return $dir; } } return null; } function phutil_get_library_name_for_root($path) { $path = rtrim(Filesystem::resolvePath($path), '/'); $bootloader = PhutilBootloader::getInstance(); $libraries = $bootloader->getAllLibraries(); foreach ($libraries as $library) { $root = $bootloader->getLibraryRoot($library); if (rtrim(Filesystem::resolvePath($root), '/') == $path) { return $library; } } return null; } function phutil_get_current_library_name() { $caller = head(debug_backtrace(false)); $root = phutil_get_library_root_for_path($caller['file']); return phutil_get_library_name_for_root($root); } /** * Warns about use of deprecated behavior. */ function phutil_deprecated($what, $why) { PhutilErrorHandler::dispatchErrorMessage( PhutilErrorHandler::DEPRECATED, $what, array( 'why' => $why, )); } function phutil_load_library($path) { - require_once $path.'/__phutil_library_init__.php'; + PhutilBootloader::getInstance()->loadLibrary($path); } diff --git a/src/runtime/ArcanistRuntime.php b/src/runtime/ArcanistRuntime.php index 06262390..66624724 100644 --- a/src/runtime/ArcanistRuntime.php +++ b/src/runtime/ArcanistRuntime.php @@ -1,670 +1,676 @@ 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(); // If we can test whether STDIN is a TTY, and it isn't, require that "--" // appear in the argument list. This is intended to make it very hard to // write unsafe scripts on top of Arcanist. if (phutil_is_noninteractive()) { $args->setRequireArgumentTerminator(true); } $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($config_engine, $config, $args); // 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 (ArcanistMissingArgumentTerminatorException $terminator_exception) { $log->writeHint( pht('USAGE'), pht( '"%s" is being run noninteractively, but the argument list is '. 'missing "--" to indicate end of flags.', $toolset->getToolsetKey())); $log->writeHint( pht('USAGE'), pht( 'When running noninteractively, you MUST provide "--" to all '. 'commands (even if they take no arguments).')); $log->writeHint( pht('USAGE'), tsprintf( '%s <__%s__>', pht('Learn More:'), 'https://phurl.io/u/noninteractive')); throw new PhutilArgumentUsageException( pht('Missing required "--" in argument list.')); } catch (PhutilArgumentUsageException $usage_exception) { // TODO: This is very, very hacky; we're trying to let errors like // "you passed the wrong arguments" through but fall back to classic // mode if the workflow itself doesn't exist. if (!preg_match('/invalid command/i', $usage_exception->getMessage())) { throw $usage_exception; } } $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 != '/'); // NOTE: There's a hard PHP version check earlier, in "init-script.php". 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( ArcanistConfigurationEngine $engine, ArcanistConfigurationSourceList $config, PhutilArgumentParser $args) { $sources = array(); $cli_libraries = $args->getArg('library'); if ($cli_libraries) { $sources = array(); foreach ($cli_libraries as $cli_library) { $sources[] = array( 'type' => 'flag', 'library-source' => $cli_library, ); } } else { $items = $config->getStorageValueList('load'); foreach ($items as $item) { foreach ($item->getValue() as $library_path) { $sources[] = array( 'type' => 'config', 'config-source' => $item->getConfigurationSource(), 'library-source' => $library_path, ); } } } foreach ($sources as $spec) { $library_source = $spec['library-source']; switch ($spec['type']) { case 'flag': $description = pht('runtime --library flag'); break; case 'config': $config_source = $spec['config-source']; $description = pht( 'Configuration (%s)', $config_source->getSourceDisplayName()); break; } $this->loadLibrary($engine, $library_source, $description); } } private function loadLibrary( ArcanistConfigurationEngine $engine, $location, $description) { // TODO: This is a legacy system that should be replaced with package // management. $log = $this->getLogEngine(); $working_copy = $engine->getWorkingCopy(); if ($working_copy) { $working_copy_root = $working_copy->getPath(); $working_directory = $working_copy->getWorkingDirectory(); } else { $working_copy_root = null; $working_directory = getcwd(); } // 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. if ($working_copy_root !== null) { $resolved_location = Filesystem::resolvePath( $location, $working_copy_root); 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_root)); if (Filesystem::pathExists($resolved_location)) { $location = $resolved_location; $resolved = true; } } } // Look beside "arcanist/". This is rule (3) above. if (!$resolved) { $arcanist_root = phutil_get_library_root('arcanist'); - $arcanist_root = dirname($arcanist_root); + $arcanist_root = dirname(dirname($arcanist_root)); $resolved_location = Filesystem::resolvePath( $location, $arcanist_root); if (Filesystem::pathExists($resolved_location)) { $location = $resolved_location; $resolved = true; } } $log->writeTrace( pht('LOAD'), pht('Loading library from "%s"...', $location)); $error = null; try { phutil_load_library($location); - } catch (PhutilBootloaderException $ex) { - fwrite( - STDERR, - '%s', - tsprintf( - "** %s ** %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, - $description))); - - $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( - "** %s ** %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))); + $log->writeWarn( + 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)); + } catch (PhutilBootloaderException $ex) { + $log->writeError( + pht('LIBRARY ERROR'), + pht( + 'Failed to load library at location "%s". This library '. + 'is specified by "%s". Check that the library is up to date.', + $location, + $description)); + + $prompt = pht('Continue without loading library?'); + if (!phutil_console_confirm($prompt)) { + throw $ex; + } + } catch (Exception $ex) { + $log->writeError( + pht('LOAD ERROR'), + pht( + 'Failed to load 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, + $description)); + + $prompt = pht('Continue without loading library?'); + if (!phutil_console_confirm($prompt)) { + throw $ex; + } } } 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') ->setContinueOnFailure(true) ->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->writeHint(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; } }