Changeset View
Changeset View
Standalone View
Standalone View
src/console/PhutilConsole.php
- This file was added.
<?php | |||||
/** | |||||
* Provides access to the command-line console. Instead of reading from or | |||||
* writing to stdin/stdout/stderr directly, this class provides a richer API | |||||
* including support for ANSI color and formatting, convenience methods for | |||||
* prompting the user, and the ability to interact with stdin/stdout/stderr | |||||
* in some other process instead of this one. | |||||
* | |||||
* @task construct Construction | |||||
* @task interface Interfacing with the User | |||||
* @task internal Internals | |||||
*/ | |||||
final class PhutilConsole extends Phobject { | |||||
private static $console; | |||||
private $server; | |||||
private $channel; | |||||
private $messages = array(); | |||||
private $flushing = false; | |||||
private $disabledTypes; | |||||
/* -( Console Construction )----------------------------------------------- */ | |||||
/** | |||||
* Use @{method:newLocalConsole} or @{method:newRemoteConsole} to construct | |||||
* new consoles. | |||||
* | |||||
* @task construct | |||||
*/ | |||||
private function __construct() { | |||||
$this->disabledTypes = new PhutilArrayWithDefaultValue(); | |||||
} | |||||
/** | |||||
* Get the current console. If there's no active console, a new local console | |||||
* is created (see @{method:newLocalConsole} for details). You can change the | |||||
* active console with @{method:setConsole}. | |||||
* | |||||
* @return PhutilConsole Active console. | |||||
* @task construct | |||||
*/ | |||||
public static function getConsole() { | |||||
if (empty(self::$console)) { | |||||
self::setConsole(self::newLocalConsole()); | |||||
} | |||||
return self::$console; | |||||
} | |||||
/** | |||||
* Set the active console. | |||||
* | |||||
* @param PhutilConsole | |||||
* @return void | |||||
* @task construct | |||||
*/ | |||||
public static function setConsole(PhutilConsole $console) { | |||||
self::$console = $console; | |||||
} | |||||
/** | |||||
* Create a new console attached to stdin/stdout/stderr of this process. | |||||
* This is how consoles normally work -- for instance, writing output with | |||||
* @{method:writeOut} prints directly to stdout. If you don't create a | |||||
* console explicitly, a new local console is created for you. | |||||
* | |||||
* @return PhutilConsole A new console which operates on the pipes of this | |||||
* process. | |||||
* @task construct | |||||
*/ | |||||
public static function newLocalConsole() { | |||||
return self::newConsoleForServer(new PhutilConsoleServer()); | |||||
} | |||||
public static function newConsoleForServer(PhutilConsoleServer $server) { | |||||
$console = new PhutilConsole(); | |||||
$console->server = $server; | |||||
return $console; | |||||
} | |||||
public static function newRemoteConsole() { | |||||
$io_channel = new PhutilSocketChannel( | |||||
fopen('php://stdin', 'r'), | |||||
fopen('php://stdout', 'w')); | |||||
$protocol_channel = new PhutilPHPObjectProtocolChannel($io_channel); | |||||
$console = new PhutilConsole(); | |||||
$console->channel = $protocol_channel; | |||||
return $console; | |||||
} | |||||
/* -( Interfacing with the User )------------------------------------------ */ | |||||
public function confirm($prompt, $default = false) { | |||||
$message = id(new PhutilConsoleMessage()) | |||||
->setType(PhutilConsoleMessage::TYPE_CONFIRM) | |||||
->setData( | |||||
array( | |||||
'prompt' => $prompt, | |||||
'default' => $default, | |||||
)); | |||||
$this->writeMessage($message); | |||||
$response = $this->waitForMessage(); | |||||
return $response->getData(); | |||||
} | |||||
public function prompt($prompt, $history = '') { | |||||
$message = id(new PhutilConsoleMessage()) | |||||
->setType(PhutilConsoleMessage::TYPE_PROMPT) | |||||
->setData( | |||||
array( | |||||
'prompt' => $prompt, | |||||
'history' => $history, | |||||
)); | |||||
$this->writeMessage($message); | |||||
$response = $this->waitForMessage(); | |||||
return $response->getData(); | |||||
} | |||||
public function sendMessage($data) { | |||||
$message = id(new PhutilConsoleMessage())->setData($data); | |||||
return $this->writeMessage($message); | |||||
} | |||||
public function writeOut($pattern /* , ... */) { | |||||
$args = func_get_args(); | |||||
return $this->writeTextMessage(PhutilConsoleMessage::TYPE_OUT, $args); | |||||
} | |||||
public function writeErr($pattern /* , ... */) { | |||||
$args = func_get_args(); | |||||
return $this->writeTextMessage(PhutilConsoleMessage::TYPE_ERR, $args); | |||||
} | |||||
public function writeLog($pattern /* , ... */) { | |||||
$args = func_get_args(); | |||||
return $this->writeTextMessage(PhutilConsoleMessage::TYPE_LOG, $args); | |||||
} | |||||
public function beginRedirectOut() { | |||||
// We need as small buffer as possible. 0 means infinite, 1 means 4096 in | |||||
// PHP < 5.4.0. | |||||
ob_start(array($this, 'redirectOutCallback'), 2); | |||||
$this->flushing = true; | |||||
} | |||||
public function endRedirectOut() { | |||||
$this->flushing = false; | |||||
ob_end_flush(); | |||||
} | |||||
/* -( Internals )---------------------------------------------------------- */ | |||||
// Must be public because it is called from output buffering. | |||||
public function redirectOutCallback($string) { | |||||
if (strlen($string)) { | |||||
$this->flushing = false; | |||||
$this->writeOut('%s', $string); | |||||
$this->flushing = true; | |||||
} | |||||
return ''; | |||||
} | |||||
private function writeTextMessage($type, array $argv) { | |||||
$message = id(new PhutilConsoleMessage()) | |||||
->setType($type) | |||||
->setData($argv); | |||||
$this->writeMessage($message); | |||||
return $this; | |||||
} | |||||
private function writeMessage(PhutilConsoleMessage $message) { | |||||
if ($this->disabledTypes[$message->getType()]) { | |||||
return $this; | |||||
} | |||||
if ($this->flushing) { | |||||
ob_flush(); | |||||
} | |||||
if ($this->channel) { | |||||
$this->channel->write($message); | |||||
$this->channel->flush(); | |||||
} else { | |||||
$response = $this->server->handleMessage($message); | |||||
if ($response) { | |||||
$this->messages[] = $response; | |||||
} | |||||
} | |||||
return $this; | |||||
} | |||||
private function waitForMessage() { | |||||
if ($this->channel) { | |||||
$message = $this->channel->waitForMessage(); | |||||
} else if ($this->messages) { | |||||
$message = array_shift($this->messages); | |||||
} else { | |||||
throw new Exception( | |||||
pht( | |||||
'%s called with no messages!', | |||||
__FUNCTION__.'()')); | |||||
} | |||||
return $message; | |||||
} | |||||
public function getServer() { | |||||
return $this->server; | |||||
} | |||||
private function disableMessageType($type) { | |||||
$this->disabledTypes[$type] += 1; | |||||
return $this; | |||||
} | |||||
private function enableMessageType($type) { | |||||
if ($this->disabledTypes[$type] == 0) { | |||||
throw new Exception(pht("Message type '%s' is already enabled!", $type)); | |||||
} | |||||
$this->disabledTypes[$type] -= 1; | |||||
return $this; | |||||
} | |||||
public function disableOut() { | |||||
return $this->disableMessageType(PhutilConsoleMessage::TYPE_OUT); | |||||
} | |||||
public function enableOut() { | |||||
return $this->enableMessageType(PhutilConsoleMessage::TYPE_OUT); | |||||
} | |||||
public function isLogEnabled() { | |||||
$message = id(new PhutilConsoleMessage()) | |||||
->setType(PhutilConsoleMessage::TYPE_ENABLED) | |||||
->setData( | |||||
array( | |||||
'which' => PhutilConsoleMessage::TYPE_LOG, | |||||
)); | |||||
$this->writeMessage($message); | |||||
$response = $this->waitForMessage(); | |||||
return $response->getData(); | |||||
} | |||||
public function isErrATTY() { | |||||
$message = id(new PhutilConsoleMessage()) | |||||
->setType(PhutilConsoleMessage::TYPE_TTY) | |||||
->setData( | |||||
array( | |||||
'which' => PhutilConsoleMessage::TYPE_ERR, | |||||
)); | |||||
$this->writeMessage($message); | |||||
$response = $this->waitForMessage(); | |||||
return $response->getData(); | |||||
} | |||||
public function getErrCols() { | |||||
$message = id(new PhutilConsoleMessage()) | |||||
->setType(PhutilConsoleMessage::TYPE_COLS) | |||||
->setData( | |||||
array( | |||||
'which' => PhutilConsoleMessage::TYPE_ERR, | |||||
)); | |||||
$this->writeMessage($message); | |||||
$response = $this->waitForMessage(); | |||||
return $response->getData(); | |||||
} | |||||
} |