1
0
Fork 0
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:
epriestley 2018-09-24 08:14:44 -07:00
parent afcaeea9c3
commit a62c1d70db
4 changed files with 141 additions and 19 deletions

View file

@ -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',

View file

@ -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();
}
}

View file

@ -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.'));
}

View file

@ -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;
}
}