mirror of
https://we.phorge.it/source/arcanist.git
synced 2024-12-01 19:22:41 +01:00
[Wilds] Handle SIGINT (^C) in ArcanistRuntime in a more formal way
Summary: Ref T13098. Add ^C handling and some small bits: - Update `arc weld`. - Test that `arc weld filen<tab>` completes `filename` (it does). - Add a "workflow stack" -- I plan to make it easier for `arc diff` to call `arc unit` / `arc lint` as formal sub-workflows, etc., and make "workflow X delegates to workflow Y" a more formal thing. On interrupts: - Workflows can do something when you press ^C. - If they do, press ^C twice quickly to exit. - Otherwise, we exit on the first ^C. The major thing I'd like to do in the short-ish term is to make `phage` report status on interrupt, but some other workflows might make sense to have interrupt handlers (maybe long-running stuff like `arc upload` / `arc download`) and third parties may find creative uses for them. Test Plan: - Added some `sleep(...)` to WeldWorkflow. - Interrupted, got program exit. - Added interrupt handlers and interrupted, got interrupt handling. - With interrupt handlers, interrupted twice. Got program exit. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13098 Differential Revision: https://secure.phabricator.com/D19703
This commit is contained in:
parent
afcaeea9c3
commit
a62c1d70db
4 changed files with 141 additions and 19 deletions
|
@ -718,7 +718,6 @@ phutil_register_library_map(array(
|
|||
'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 @@ phutil_register_library_map(array(
|
|||
'PhutilExecPassthru' => 'PhutilExecutableFuture',
|
||||
'PhutilExecutableFuture' => 'Future',
|
||||
'PhutilExecutionEnvironment' => 'Phobject',
|
||||
'PhutilExtensionsTestCase' => 'PhutilTestCase',
|
||||
'PhutilFacebookAuthAdapter' => 'PhutilOAuthAuthAdapter',
|
||||
'PhutilFatalDaemon' => 'PhutilTortureTestDaemon',
|
||||
'PhutilFileLock' => 'PhutilLock',
|
||||
|
|
|
@ -129,9 +129,13 @@ abstract class ArcanistWorkflow extends Phobject {
|
|||
}
|
||||
|
||||
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 @@ abstract class ArcanistWorkflow extends Phobject {
|
|||
phlog($ex);
|
||||
}
|
||||
|
||||
$runtime->popWorkflow();
|
||||
|
||||
if ($caught) {
|
||||
throw $caught;
|
||||
}
|
||||
|
@ -189,4 +195,12 @@ abstract class ArcanistWorkflow extends Phobject {
|
|||
return $this->getRuntime()->getLogEngine();
|
||||
}
|
||||
|
||||
public function canHandleSignal($signo) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function handleSignal($signo) {
|
||||
throw new PhutilMethodNotImplementedException();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,36 +1,38 @@
|
|||
<?php
|
||||
|
||||
final class ArcanistWeldWorkflow extends ArcanistWorkflow {
|
||||
final class ArcanistWeldWorkflow
|
||||
extends ArcanistWorkflow {
|
||||
|
||||
public function getWorkflowName() {
|
||||
return 'weld';
|
||||
}
|
||||
|
||||
public function getCommandSynopses() {
|
||||
return phutil_console_format(<<<EOTEXT
|
||||
**weld** [options] __file__ __file__ ...
|
||||
public function getWorkflowInformation() {
|
||||
$help = pht(<<<EOTEXT
|
||||
Robustly fuse two or more files together. The resulting joint is much stronger
|
||||
than the one created by tools like __cat__.
|
||||
EOTEXT
|
||||
);
|
||||
);
|
||||
|
||||
return $this->newWorkflowInformation()
|
||||
->addExample(pht('**weld** [__options__] __file__ __file__ ...'))
|
||||
->setHelp($help);
|
||||
}
|
||||
|
||||
public function getCommandHelp() {
|
||||
return phutil_console_format(<<<EOTEXT
|
||||
Robustly fuse two or more files together. The resulting joint is
|
||||
much stronger than the one created by tools like __cat__.
|
||||
EOTEXT
|
||||
);
|
||||
}
|
||||
|
||||
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.'));
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,9 @@ final class ArcanistRuntime {
|
|||
|
||||
private $workflows;
|
||||
private $logEngine;
|
||||
private $lastInterruptTime;
|
||||
|
||||
private $stack = array();
|
||||
|
||||
public function execute(array $argv) {
|
||||
|
||||
|
@ -68,6 +71,12 @@ final class ArcanistRuntime {
|
|||
|
||||
$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 @@ final class ArcanistRuntime {
|
|||
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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue