1
0
Fork 0
mirror of https://we.phorge.it/source/arcanist.git synced 2025-01-15 17:21:09 +01:00

[Wilds] Provide a skeleton for prompt behaviors

Summary:
Ref T13098. Ref T13198. Ref T12996. The major ideas here are:

Workflows must define a list of all the prompts they can raise, so that these prompts can be enumerated with `arc prompts <workflow>`.

Prompts themselves should respond properly to ^C (abort immediately) and we should be able to make them nonblocking in the future (particularly, we'd like to be able to continue reading bytes from subprocess buffers while the prompt is shown on screen).

This doesn't have a lot of fancy features yet (non-confirm prompts, default yes, prompts which don't abort on "N", etc) but those should be easy to add later.

In the future, you'll be able to configure a default answer to prompts either in a config file or at runtime with `--config prompts=x.y.z=N` or similar.

This removes the history/readline mode for prompts, where you could use the up arrow to cycle through older responses. I believe this was only really useful for "excuse" prompts and intend to remove those.

Test Plan:
Forced `arc shell-complete` to always prompt, then:

  - Got prompted, answered "y", "n", "N", "", "quack". Got sensible behavior in all cases.
  - Ran `echo | arc shell-complete`, got a TTY error.
  - Ran `arc prompts`, `arc prompts shell-complete`.

Reviewers: amckinley

Reviewed By: amckinley

Maniphest Tasks: T13098, T13198, T12996

Differential Revision: https://secure.phabricator.com/D19706
This commit is contained in:
epriestley 2018-09-25 09:07:16 -07:00
parent a62c1d70db
commit df2c1ba912
7 changed files with 326 additions and 8 deletions

View file

@ -385,6 +385,8 @@ phutil_register_library_map(array(
'ArcanistPregQuoteMisuseXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistPregQuoteMisuseXHPASTLinterRule.php',
'ArcanistPregQuoteMisuseXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistPregQuoteMisuseXHPASTLinterRuleTestCase.php',
'ArcanistProjectConfigurationSource' => 'config/source/ArcanistProjectConfigurationSource.php',
'ArcanistPrompt' => 'toolset/ArcanistPrompt.php',
'ArcanistPromptsWorkflow' => 'toolset/workflow/ArcanistPromptsWorkflow.php',
'ArcanistPublicPropertyXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistPublicPropertyXHPASTLinterRule.php',
'ArcanistPublicPropertyXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistPublicPropertyXHPASTLinterRuleTestCase.php',
'ArcanistPuppetLintLinter' => 'lint/linter/ArcanistPuppetLintLinter.php',
@ -1491,6 +1493,8 @@ phutil_register_library_map(array(
'ArcanistPregQuoteMisuseXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistPregQuoteMisuseXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
'ArcanistProjectConfigurationSource' => 'ArcanistWorkingCopyConfigurationSource',
'ArcanistPrompt' => 'Phobject',
'ArcanistPromptsWorkflow' => 'ArcanistWorkflow',
'ArcanistPublicPropertyXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistPublicPropertyXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
'ArcanistPuppetLintLinter' => 'ArcanistExternalLinter',

View file

@ -18,12 +18,19 @@ final class ArcanistLogEngine
return new ArcanistLogMessage();
}
private function writeBytes($bytes) {
fprintf(STDERR, '%s', $bytes);
return $this;
}
public function writeNewline() {
return $this->writeBytes("\n");
}
public function writeMessage(ArcanistLogMessage $message) {
$color = $message->getColor();
fprintf(
STDERR,
'%s',
$this->writeBytes(
tsprintf(
"**<bg:".$color."> %s </bg>** %s\n",
$message->getLabel(),

View file

@ -0,0 +1,152 @@
<?php
final class ArcanistPrompt
extends Phobject {
private $key;
private $workflow;
private $description;
private $query;
public function setKey($key) {
$this->key = $key;
return $this;
}
public function getKey() {
return $this->key;
}
public function setWorkflow(ArcanistWorkflow $workflow) {
$this->workflow = $workflow;
return $this;
}
public function getWorkflow() {
return $this->workflow;
}
public function setDescription($description) {
$this->description = $description;
return $this;
}
public function getDescription() {
return $this->description;
}
public function setQuery($query) {
$this->query = $query;
return $this;
}
public function getQuery() {
return $this->query;
}
public function execute() {
$workflow = $this->getWorkflow();
if ($workflow) {
$workflow_ok = $workflow->hasPrompt($this->getKey());
} else {
$workflow_ok = false;
}
if (!$workflow_ok) {
throw new Exception(
pht(
'Prompt ("%s") is executing, but it is not properly bound to the '.
'invoking workflow. You may have called "newPrompt()" to execute a '.
'prompt instead of "getPrompt()". Use "newPrompt()" when defining '.
'prompts and "getPrompt()" when executing them.',
$this->getKey()));
}
$query = $this->getQuery();
if (!strlen($query)) {
throw new Exception(
pht(
'Prompt ("%s") has no query text!',
$this->getKey()));
}
$options = '[y/N]';
$default = 'N';
try {
phutil_console_require_tty();
} catch (PhutilConsoleStdinNotInteractiveException $ex) {
// TOOLSETS: Clean this up to provide more details to the user about how
// they can configure prompts to be answered.
// Throw after echoing the prompt so the user has some idea what happened.
echo $query."\n";
throw $ex;
}
// NOTE: We're making stdin nonblocking so that we can respond to signals
// immediately. If we don't, and you ^C during a prompt, the program does
// not handle the signal until fgets() returns.
$stdin = fopen('php://stdin', 'r');
if (!$stdin) {
throw new Exception(pht('Failed to open stdin for reading.'));
}
$ok = stream_set_blocking($stdin, false);
if (!$ok) {
throw new Exception(pht('Unable to set stdin nonblocking.'));
}
echo "\n";
$result = null;
while (true) {
echo tsprintf(
'**<bg:cyan> %s </bg>** %s %s ',
'>>>',
$query,
$options);
while (true) {
$read = array($stdin);
$write = array();
$except = array();
$ok = stream_select($read, $write, $except, 1);
if ($ok === false) {
throw new Exception(pht('stream_select() failed!'));
}
$response = fgets($stdin);
if (!strlen($response)) {
continue;
}
break;
}
$response = trim($response);
if (!strlen($response)) {
$response = $default;
}
if (phutil_utf8_strtolower($response) == 'y') {
$result = true;
break;
}
if (phutil_utf8_strtolower($response) == 'n') {
$result = false;
break;
}
}
if (!$result) {
throw new ArcanistUserAbortException();
}
}
}

View file

@ -8,6 +8,7 @@ abstract class ArcanistWorkflow extends Phobject {
private $configurationEngine;
private $configurationSourceList;
private $conduitEngine;
private $promptMap;
/**
* Return the command used to invoke this workflow from the command like,
@ -203,4 +204,63 @@ abstract class ArcanistWorkflow extends Phobject {
throw new PhutilMethodNotImplementedException();
}
protected function newPrompts() {
return array();
}
protected function newPrompt($key) {
return id(new ArcanistPrompt())
->setWorkflow($this)
->setKey($key);
}
public function hasPrompt($key) {
$map = $this->getPromptMap();
return isset($map[$key]);
}
public function getPromptMap() {
if ($this->promptMap === null) {
$prompts = $this->newPrompts();
assert_instances_of($prompts, 'ArcanistPrompt');
$map = array();
foreach ($prompts as $prompt) {
$key = $prompt->getKey();
if (isset($map[$key])) {
throw new Exception(
pht(
'Workflow ("%s") generates two prompts with the same '.
'key ("%s"). Each prompt a workflow generates must have a '.
'unique key.',
get_class($this),
$key));
}
$map[$key] = $prompt;
}
$this->promptMap = $map;
}
return $this->promptMap;
}
protected function getPrompt($key) {
$map = $this->getPromptMap();
$prompt = idx($map, $key);
if (!$prompt) {
throw new Exception(
pht(
'Workflow ("%s") is requesting a prompt ("%s") but it did not '.
'generate any prompt with that name in "newPrompts()".',
get_class($this),
$key));
}
return clone $prompt;
}
}

View file

@ -0,0 +1,80 @@
<?php
final class ArcanistPromptsWorkflow extends ArcanistWorkflow {
public function getWorkflowName() {
return 'prompts';
}
public function supportsToolset(ArcanistToolset $toolset) {
return true;
}
public function getWorkflowInformation() {
$help = pht(<<<EOTEXT
Show information about prompts a workflow may execute and configure default
responses.
**Show Prompts**
To show possible prompts a workflow may execute, run:
$ arc prompts <workflow>
EOTEXT
);
return $this->newWorkflowInformation()
->addExample(pht('**prompts** __workflow__'))
->setHelp($help);
}
public function getWorkflowArguments() {
return array(
$this->newWorkflowArgument('argv')
->setWildcard(true),
);
}
public function runWorkflow() {
$argv = $this->getArgument('argv');
if (!$argv) {
throw new PhutilArgumentUsageException(
pht('Provide a workflow to list prompts for.'));
}
$runtime = $this->getRuntime();
$workflows = $runtime->getWorkflows();
$workflow_key = array_shift($argv);
$workflow = idx($workflows, $workflow_key);
if (!$workflow) {
throw new PhutilArgumentUsageException(
pht(
'Workflow "%s" is unknown. Supported workflows are: %s.',
$workflow_key,
implode(', ', array_keys($workflows))));
}
$prompts = $workflow->getPromptMap();
if (!$prompts) {
echo tsprintf(
"%s\n",
pht('This workflow can not prompt.'));
return 0;
}
foreach ($prompts as $prompt) {
echo tsprintf(
"**%s**\n",
$prompt->getKey());
echo tsprintf(
"%s\n",
$prompt->getDescription());
}
return 0;
}
}

View file

@ -155,6 +155,16 @@ EOTEXT
$this->runAutocomplete();
}
protected function newPrompts() {
return array(
$this->newPrompt('arc.shell-complete.install')
->setDescription(
pht(
'Confirms writing to to "~/.profile" (or another similar file) '.
'to install shell completion.')),
);
}
private function runInstall() {
$log = $this->getLogEngine();
@ -281,11 +291,9 @@ EOTEXT
}
}
// TOOLSETS: Generalize prompting.
if (!phutil_console_confirm($prompt, false)) {
throw new PhutilArgumentUsageException(pht('Aborted.'));
}
$this->getPrompt('arc.shell-complete.install')
->setQuery($prompt)
->execute();
Filesystem::writeFile($file_path, $new_data);

View file

@ -32,6 +32,8 @@ final class ArcanistRuntime {
$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;
@ -590,6 +592,11 @@ final class ArcanistRuntime {
}
}
// 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'),