1<?php 2 3/** 4 * Provides access to the command-line console. Instead of reading from or 5 * writing to stdin/stdout/stderr directly, this class provides a richer API 6 * including support for ANSI color and formatting, convenience methods for 7 * prompting the user, and the ability to interact with stdin/stdout/stderr 8 * in some other process instead of this one. 9 * 10 * @task construct Construction 11 * @task interface Interfacing with the User 12 * @task internal Internals 13 */ 14final class PhutilConsole extends Phobject { 15 16 private static $console; 17 18 private $server; 19 private $channel; 20 private $messages = array(); 21 22 private $flushing = false; 23 private $disabledTypes; 24 25 26/* -( Console Construction )----------------------------------------------- */ 27 28 29 /** 30 * Use @{method:newLocalConsole} or @{method:newRemoteConsole} to construct 31 * new consoles. 32 * 33 * @task construct 34 */ 35 private function __construct() { 36 $this->disabledTypes = new PhutilArrayWithDefaultValue(); 37 } 38 39 40 /** 41 * Get the current console. If there's no active console, a new local console 42 * is created (see @{method:newLocalConsole} for details). You can change the 43 * active console with @{method:setConsole}. 44 * 45 * @return PhutilConsole Active console. 46 * @task construct 47 */ 48 public static function getConsole() { 49 if (empty(self::$console)) { 50 self::setConsole(self::newLocalConsole()); 51 } 52 return self::$console; 53 } 54 55 56 /** 57 * Set the active console. 58 * 59 * @param PhutilConsole 60 * @return void 61 * @task construct 62 */ 63 public static function setConsole(PhutilConsole $console) { 64 self::$console = $console; 65 } 66 67 68 /** 69 * Create a new console attached to stdin/stdout/stderr of this process. 70 * This is how consoles normally work -- for instance, writing output with 71 * @{method:writeOut} prints directly to stdout. If you don't create a 72 * console explicitly, a new local console is created for you. 73 * 74 * @return PhutilConsole A new console which operates on the pipes of this 75 * process. 76 * @task construct 77 */ 78 public static function newLocalConsole() { 79 return self::newConsoleForServer(new PhutilConsoleServer()); 80 } 81 82 83 public static function newConsoleForServer(PhutilConsoleServer $server) { 84 $console = new PhutilConsole(); 85 $console->server = $server; 86 return $console; 87 } 88 89 90 public static function newRemoteConsole() { 91 $io_channel = new PhutilSocketChannel( 92 fopen('php://stdin', 'r'), 93 fopen('php://stdout', 'w')); 94 $protocol_channel = new PhutilPHPObjectProtocolChannel($io_channel); 95 96 $console = new PhutilConsole(); 97 $console->channel = $protocol_channel; 98 99 return $console; 100 } 101 102 103/* -( Interfacing with the User )------------------------------------------ */ 104 105 106 public function confirm($prompt, $default = false) { 107 $message = id(new PhutilConsoleMessage()) 108 ->setType(PhutilConsoleMessage::TYPE_CONFIRM) 109 ->setData( 110 array( 111 'prompt' => $prompt, 112 'default' => $default, 113 )); 114 115 $this->writeMessage($message); 116 $response = $this->waitForMessage(); 117 118 return $response->getData(); 119 } 120 121 public function prompt($prompt, $history = '') { 122 $message = id(new PhutilConsoleMessage()) 123 ->setType(PhutilConsoleMessage::TYPE_PROMPT) 124 ->setData( 125 array( 126 'prompt' => $prompt, 127 'history' => $history, 128 )); 129 130 $this->writeMessage($message); 131 $response = $this->waitForMessage(); 132 133 return $response->getData(); 134 } 135 136 public function sendMessage($data) { 137 $message = id(new PhutilConsoleMessage())->setData($data); 138 return $this->writeMessage($message); 139 } 140 141 public function writeOut($pattern /* , ... */) { 142 $args = func_get_args(); 143 return $this->writeTextMessage(PhutilConsoleMessage::TYPE_OUT, $args); 144 } 145 146 public function writeErr($pattern /* , ... */) { 147 $args = func_get_args(); 148 return $this->writeTextMessage(PhutilConsoleMessage::TYPE_ERR, $args); 149 } 150 151 public function writeLog($pattern /* , ... */) { 152 $args = func_get_args(); 153 return $this->writeTextMessage(PhutilConsoleMessage::TYPE_LOG, $args); 154 } 155 156 public function beginRedirectOut() { 157 // We need as small buffer as possible. 0 means infinite, 1 means 4096 in 158 // PHP < 5.4.0. 159 ob_start(array($this, 'redirectOutCallback'), 2); 160 $this->flushing = true; 161 } 162 163 public function endRedirectOut() { 164 $this->flushing = false; 165 ob_end_flush(); 166 } 167 168 169/* -( Internals )---------------------------------------------------------- */ 170 171 // Must be public because it is called from output buffering. 172 public function redirectOutCallback($string) { 173 if (strlen($string)) { 174 $this->flushing = false; 175 $this->writeOut('%s', $string); 176 $this->flushing = true; 177 } 178 return ''; 179 } 180 181 private function writeTextMessage($type, array $argv) { 182 183 $message = id(new PhutilConsoleMessage()) 184 ->setType($type) 185 ->setData($argv); 186 187 $this->writeMessage($message); 188 189 return $this; 190 } 191 192 private function writeMessage(PhutilConsoleMessage $message) { 193 if ($this->disabledTypes[$message->getType()]) { 194 return $this; 195 } 196 197 if ($this->flushing) { 198 ob_flush(); 199 } 200 if ($this->channel) { 201 $this->channel->write($message); 202 $this->channel->flush(); 203 } else { 204 $response = $this->server->handleMessage($message); 205 if ($response) { 206 $this->messages[] = $response; 207 } 208 } 209 return $this; 210 } 211 212 private function waitForMessage() { 213 if ($this->channel) { 214 $message = $this->channel->waitForMessage(); 215 } else if ($this->messages) { 216 $message = array_shift($this->messages); 217 } else { 218 throw new Exception( 219 pht( 220 '%s called with no messages!', 221 __FUNCTION__.'()')); 222 } 223 224 return $message; 225 } 226 227 public function getServer() { 228 return $this->server; 229 } 230 231 private function disableMessageType($type) { 232 $this->disabledTypes[$type] += 1; 233 return $this; 234 } 235 236 private function enableMessageType($type) { 237 if ($this->disabledTypes[$type] == 0) { 238 throw new Exception(pht("Message type '%s' is already enabled!", $type)); 239 } 240 $this->disabledTypes[$type] -= 1; 241 return $this; 242 } 243 244 public function disableOut() { 245 return $this->disableMessageType(PhutilConsoleMessage::TYPE_OUT); 246 } 247 248 public function enableOut() { 249 return $this->enableMessageType(PhutilConsoleMessage::TYPE_OUT); 250 } 251 252 public function isLogEnabled() { 253 $message = id(new PhutilConsoleMessage()) 254 ->setType(PhutilConsoleMessage::TYPE_ENABLED) 255 ->setData( 256 array( 257 'which' => PhutilConsoleMessage::TYPE_LOG, 258 )); 259 260 $this->writeMessage($message); 261 $response = $this->waitForMessage(); 262 263 return $response->getData(); 264 } 265 266 public function isErrATTY() { 267 $message = id(new PhutilConsoleMessage()) 268 ->setType(PhutilConsoleMessage::TYPE_TTY) 269 ->setData( 270 array( 271 'which' => PhutilConsoleMessage::TYPE_ERR, 272 )); 273 274 $this->writeMessage($message); 275 $response = $this->waitForMessage(); 276 277 return $response->getData(); 278 } 279 280 public function getErrCols() { 281 $message = id(new PhutilConsoleMessage()) 282 ->setType(PhutilConsoleMessage::TYPE_COLS) 283 ->setData( 284 array( 285 'which' => PhutilConsoleMessage::TYPE_ERR, 286 )); 287 288 $this->writeMessage($message); 289 $response = $this->waitForMessage(); 290 291 return $response->getData(); 292 } 293 294 295} 296