diff --git a/src/toolset/ArcanistPrompt.php b/src/toolset/ArcanistPrompt.php index 24523596..caff32c6 100644 --- a/src/toolset/ArcanistPrompt.php +++ b/src/toolset/ArcanistPrompt.php @@ -1,312 +1,320 @@ 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'; $saved_response = $this->getSavedResponse(); 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.')); + // 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. + + // On Windows, we skip this because stdin can not be made nonblocking. + + if (!phutil_is_windows()) { + $ok = stream_set_blocking($stdin, false); + if (!$ok) { + throw new Exception(pht('Unable to set stdin nonblocking.')); + } } echo "\n"; $result = null; $is_saved = false; while (true) { if ($saved_response !== null) { $is_saved = true; $response = $saved_response; $saved_response = null; } else { echo tsprintf( '** %s ** %s %s ', '>>>', $query, $options); - while (true) { - $is_saved = false; + $is_saved = false; - $read = array($stdin); - $write = array(); - $except = array(); + if (phutil_is_windows()) { + $response = fgets($stdin); + } else { + while (true) { + $read = array($stdin); + $write = array(); + $except = array(); + + $ok = @stream_select($read, $write, $except, 1); + if ($ok === false) { + // NOTE: We may be interrupted by a system call, particularly if + // the window is resized while a prompt is shown and the terminal + // sends SIGWINCH. + + // If we are, just continue below and try to read from stdin. If + // we were interrupted, we should read nothing and continue + // normally. If the pipe is broken, the read should fail. + } - $ok = @stream_select($read, $write, $except, 1); - if ($ok === false) { - // NOTE: We may be interrupted by a system call, particularly if - // the window is resized while a prompt is shown and the terminal - // sends SIGWINCH. + $response = ''; + while (true) { + $bytes = fread($stdin, 8192); + if ($bytes === false) { + throw new Exception( + pht('fread() from stdin failed with an error.')); + } - // If we are, just continue below and try to read from stdin. If - // we were interrupted, we should read nothing and continue - // normally. If the pipe is broken, the read should fail. - } + if (!strlen($bytes)) { + break; + } - $response = ''; - while (true) { - $bytes = fread($stdin, 8192); - if ($bytes === false) { - throw new Exception( - pht('fread() from stdin failed with an error.')); + $response .= $bytes; } - if (!strlen($bytes)) { - break; + if (!strlen($response)) { + continue; } - $response .= $bytes; - } - - if (!strlen($response)) { - continue; + break; } - - break; } $response = trim($response); if (!strlen($response)) { $response = $default; } } $save_scope = null; if (!$is_saved) { $matches = null; if (preg_match('(^(.*)([!*])\z)', $response, $matches)) { $response = $matches[1]; if ($matches[2] === '*') { $save_scope = ArcanistConfigurationSource::SCOPE_USER; } else { $save_scope = ArcanistConfigurationSource::SCOPE_WORKING_COPY; } } } if (phutil_utf8_strtolower($response) == 'y') { $result = true; break; } if (phutil_utf8_strtolower($response) == 'n') { $result = false; break; } if (phutil_utf8_strtolower($response) == '?') { echo tsprintf( "\n** %s ** **%s**\n\n", pht('PROMPT'), $this->getKey()); echo tsprintf( "%s\n", $this->getDescription()); echo tsprintf("\n"); echo tsprintf( "%s\n", pht( 'The default response to this prompt is "%s".', $default)); echo tsprintf("\n"); echo tsprintf( "%?\n", pht( 'Use "*" after a response to save it in user configuration.')); echo tsprintf( "%?\n", pht( 'Use "!" after a response to save it in working copy '. 'configuration.')); echo tsprintf( "%?\n", pht( 'Run "arc help prompts" for detailed help on configuring '. 'responses.')); echo tsprintf("\n"); continue; } } if ($save_scope !== null) { $this->saveResponse($save_scope, $response); } if ($is_saved) { echo tsprintf( "** %s ** %s **<%s>**\n". "** %s ** (%s)\n\n", '>>>', $query, $response, '>>>', pht( 'Using saved response to prompt "%s".', $this->getKey())); } if (!$result) { throw new ArcanistUserAbortException(); } } private function getSavedResponse() { $config_key = ArcanistArcConfigurationEngineExtension::KEY_PROMPTS; $workflow = $this->getWorkflow(); $config = $workflow->getConfig($config_key); $prompt_key = $this->getKey(); $prompt_response = null; foreach ($config as $response) { if ($response->getPrompt() === $prompt_key) { $prompt_response = $response; } } if ($prompt_response === null) { return null; } return $prompt_response->getResponse(); } private function saveResponse($scope, $response_value) { $config_key = ArcanistArcConfigurationEngineExtension::KEY_PROMPTS; $workflow = $this->getWorkflow(); echo tsprintf( "** %s ** %s\n", pht('SAVE PROMPT'), pht( 'Saving response "%s" to prompt "%s".', $response_value, $this->getKey())); $source_list = $workflow->getConfigurationSourceList(); $source = $source_list->getWritableSourceFromScope($scope); $response_list = $source_list->getConfigFromScopes( $config_key, array($scope)); foreach ($response_list as $key => $response) { if ($response->getPrompt() === $this->getKey()) { unset($response_list[$key]); } } if ($response_value !== null) { $response_list[] = id(new ArcanistPromptResponse()) ->setPrompt($this->getKey()) ->setResponse($response_value); } $option = $source_list->getConfigOption($config_key); $option->writeValue($source, $response_list); } }