Index: src/__phutil_library_map__.php =================================================================== --- src/__phutil_library_map__.php +++ src/__phutil_library_map__.php @@ -718,7 +718,6 @@ 'PhutilExecPassthru' => 'future/exec/PhutilExecPassthru.php', 'PhutilExecutableFuture' => 'future/exec/PhutilExecutableFuture.php', 'PhutilExecutionEnvironment' => 'utils/PhutilExecutionEnvironment.php', - 'PhutilExtensionsTestCase' => 'moduleutils/__tests__/PhutilExtensionsTestCase.php', 'PhutilFacebookAuthAdapter' => 'auth/PhutilFacebookAuthAdapter.php', 'PhutilFatalDaemon' => 'daemon/torture/PhutilFatalDaemon.php', 'PhutilFileLock' => 'filesystem/PhutilFileLock.php', @@ -1841,7 +1840,6 @@ 'PhutilExecPassthru' => 'PhutilExecutableFuture', 'PhutilExecutableFuture' => 'Future', 'PhutilExecutionEnvironment' => 'Phobject', - 'PhutilExtensionsTestCase' => 'PhutilTestCase', 'PhutilFacebookAuthAdapter' => 'PhutilOAuthAuthAdapter', 'PhutilFatalDaemon' => 'PhutilTortureTestDaemon', 'PhutilFileLock' => 'PhutilLock', Index: src/toolset/ArcanistWorkflow.php =================================================================== --- src/toolset/ArcanistWorkflow.php +++ src/toolset/ArcanistWorkflow.php @@ -129,9 +129,13 @@ } final public function executeWorkflow(PhutilArgumentParser $args) { + $runtime = $this->getRuntime(); + $this->arguments = $args; $caught = null; + $runtime->pushWorkflow($this); + try { $err = $this->runWorkflow($args); } catch (Exception $ex) { @@ -144,6 +148,8 @@ phlog($ex); } + $runtime->popWorkflow(); + if ($caught) { throw $caught; } @@ -189,4 +195,12 @@ return $this->getRuntime()->getLogEngine(); } + public function canHandleSignal($signo) { + return false; + } + + public function handleSignal($signo) { + throw new PhutilMethodNotImplementedException(); + } + } Index: src/workflow/ArcanistWeldWorkflow.php =================================================================== --- src/workflow/ArcanistWeldWorkflow.php +++ src/workflow/ArcanistWeldWorkflow.php @@ -1,36 +1,38 @@ newWorkflowInformation() + ->addExample(pht('**weld** [__options__] __file__ __file__ ...')) + ->setHelp($help); } - public function getArguments() { + public function getWorkflowArguments() { return array( - '*' => 'files', + $this->newWorkflowArgument('files') + ->setIsPathArgument(true) + ->setWildcard(true), ); } - public function run() { + + public function runWorkflow() { $files = $this->getArgument('files'); + if (count($files) < 2) { - throw new ArcanistUsageException( + throw new PhutilArgumentUsageException( pht('Specify two or more files to weld together.')); } Index: support/ArcanistRuntime.php =================================================================== --- support/ArcanistRuntime.php +++ support/ArcanistRuntime.php @@ -4,6 +4,9 @@ private $workflows; private $logEngine; + private $lastInterruptTime; + + private $stack = array(); public function execute(array $argv) { @@ -68,6 +71,12 @@ $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); @@ -514,4 +523,103 @@ 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; + } + } + + 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; + } + + }