Changeset View
Changeset View
Standalone View
Standalone View
src/channel/PhutilExecChannel.php
- This file was added.
| <?php | |||||
| /** | |||||
| * Channel on an underlying @{class:ExecFuture}. For a description of channels, | |||||
| * see @{class:PhutilChannel}. | |||||
| * | |||||
| * For example, you can open a channel on `nc` like this: | |||||
| * | |||||
| * $future = new ExecFuture('nc example.com 80'); | |||||
| * $channel = new PhutilExecChannel($future); | |||||
| * | |||||
| * $channel->write("GET / HTTP/1.0\n\n"); | |||||
| * while (true) { | |||||
| * echo $channel->read(); | |||||
| * | |||||
| * PhutilChannel::waitForAny(array($channel)); | |||||
| * if (!$channel->update()) { | |||||
| * // Break out of the loop when the channel closes. | |||||
| * break; | |||||
| * } | |||||
| * } | |||||
| * | |||||
| * This script makes an HTTP request to "example.com". This example is heavily | |||||
| * contrived. In most cases, @{class:ExecFuture} and other futures constructs | |||||
| * offer a much easier way to solve problems which involve system commands, and | |||||
| * @{class:HTTPFuture} and other HTTP constructs offer a much easier way to | |||||
| * solve problems which involve HTTP. | |||||
| * | |||||
| * @{class:PhutilExecChannel} is generally useful only when a program acts like | |||||
| * a server but performs I/O on stdin/stdout, and you need to act like a client | |||||
| * or interact with the program at the same time as you manage traditional | |||||
| * socket connections. Examples are Mercurial operating in "cmdserve" mode, git | |||||
| * operating in "receive-pack" mode, etc. It is unlikely that any reasonable | |||||
| * use of this class is concise enough to make a short example out of, so you | |||||
| * get a contrived one instead. | |||||
| * | |||||
| * See also @{class:PhutilSocketChannel}, for a similar channel that uses | |||||
| * sockets for I/O. | |||||
| * | |||||
| * Since @{class:ExecFuture} already supports buffered I/O and socket selection, | |||||
| * the implementation of this class is fairly straightforward. | |||||
| * | |||||
| * @task construct Construction | |||||
| */ | |||||
| final class PhutilExecChannel extends PhutilChannel { | |||||
| private $future; | |||||
| private $stderrHandler; | |||||
| /* -( Construction )------------------------------------------------------- */ | |||||
| /** | |||||
| * Construct an exec channel from a @{class:ExecFuture}. The future should | |||||
| * **NOT** have been started yet (e.g., with `isReady()` or `start()`), | |||||
| * because @{class:ExecFuture} closes stdin by default when futures start. | |||||
| * If stdin has been closed, you will be unable to write on the channel. | |||||
| * | |||||
| * @param ExecFuture Future to use as an underlying I/O source. | |||||
| * @task construct | |||||
| */ | |||||
| public function __construct(ExecFuture $future) { | |||||
| parent::__construct(); | |||||
| // Make an empty write to keep the stdin pipe open. By default, futures | |||||
| // close this pipe when they start. | |||||
| $future->write('', $keep_pipe = true); | |||||
| // Start the future so that reads and writes work immediately. | |||||
| $future->isReady(); | |||||
| $this->future = $future; | |||||
| } | |||||
| public function __destruct() { | |||||
| if (!$this->future->isReady()) { | |||||
| $this->future->resolveKill(); | |||||
| } | |||||
| } | |||||
| public function update() { | |||||
| $this->future->isReady(); | |||||
| return parent::update(); | |||||
| } | |||||
| public function isOpen() { | |||||
| return !$this->future->isReady(); | |||||
| } | |||||
| protected function readBytes($length) { | |||||
| list($stdout, $stderr) = $this->future->read(); | |||||
| $this->future->discardBuffers(); | |||||
| if (strlen($stderr)) { | |||||
| if ($this->stderrHandler) { | |||||
| call_user_func($this->stderrHandler, $this, $stderr); | |||||
| } else { | |||||
| throw new Exception( | |||||
| pht('Unexpected output to stderr on exec channel: %s', $stderr)); | |||||
| } | |||||
| } | |||||
| return $stdout; | |||||
| } | |||||
| public function write($bytes) { | |||||
| $this->future->write($bytes, $keep_pipe = true); | |||||
| } | |||||
| public function closeWriteChannel() { | |||||
| $this->future->write('', $keep_pipe = false); | |||||
| } | |||||
| protected function writeBytes($bytes) { | |||||
| throw new Exception(pht('%s can not write bytes directly!', 'ExecFuture')); | |||||
| } | |||||
| protected function getReadSockets() { | |||||
| return $this->future->getReadSockets(); | |||||
| } | |||||
| protected function getWriteSockets() { | |||||
| return $this->future->getWriteSockets(); | |||||
| } | |||||
| public function isReadBufferEmpty() { | |||||
| // Check both the channel and future read buffers, since either could have | |||||
| // data. | |||||
| return parent::isReadBufferEmpty() && $this->future->isReadBufferEmpty(); | |||||
| } | |||||
| public function setReadBufferSize($size) { | |||||
| // NOTE: We may end up using 2x the buffer size here, one inside | |||||
| // ExecFuture and one inside the Channel. We could tune this eventually, but | |||||
| // it should be fine for now. | |||||
| parent::setReadBufferSize($size); | |||||
| $this->future->setReadBufferSize($size); | |||||
| return $this; | |||||
| } | |||||
| public function isWriteBufferEmpty() { | |||||
| return $this->future->isWriteBufferEmpty(); | |||||
| } | |||||
| public function getWriteBufferSize() { | |||||
| return $this->future->getWriteBufferSize(); | |||||
| } | |||||
| /** | |||||
| * If the wrapped @{class:ExecFuture} outputs data to stderr, we normally | |||||
| * throw an exception. Instead, you can provide a callback handler that will | |||||
| * be invoked and passed the data. It should have this signature: | |||||
| * | |||||
| * function f(PhutilExecChannel $channel, $stderr) { | |||||
| * // ... | |||||
| * } | |||||
| * | |||||
| * The `$channel` will be this channel object, and `$stderr` will be a string | |||||
| * with bytes received over stderr. | |||||
| * | |||||
| * You can set a handler which does nothing to effectively ignore and discard | |||||
| * any output on stderr. | |||||
| * | |||||
| * @param callable Handler to invoke when stderr data is received. | |||||
| * @return this | |||||
| */ | |||||
| public function setStderrHandler($handler) { | |||||
| $this->stderrHandler = $handler; | |||||
| return $this; | |||||
| } | |||||
| } | |||||