Index: src/__phutil_library_map__.php =================================================================== --- src/__phutil_library_map__.php +++ src/__phutil_library_map__.php @@ -373,6 +373,7 @@ 'phutil_safe_html' => 'markup/render.php', 'phutil_split_lines' => 'utils/utils.php', 'phutil_tag' => 'markup/render.php', + 'phutil_tag_div' => 'markup/render.php', 'phutil_unescape_uri_path_component' => 'markup/render.php', 'phutil_utf8_console_strlen' => 'utils/utf8.php', 'phutil_utf8_convert' => 'utils/utf8.php', @@ -400,6 +401,7 @@ 'vqsprintf' => 'xsprintf/qsprintf.php', 'vqueryfx' => 'xsprintf/queryfx.php', 'vqueryfx_all' => 'xsprintf/queryfx.php', + 'vurisprintf' => 'xsprintf/urisprintf.php', 'xhp_parser_node_constants' => 'parser/xhpast/parser_nodes.php', 'xhpast_get_binary_path' => 'parser/xhpast/bin/xhpast_parse.php', 'xhpast_get_build_instructions' => 'parser/xhpast/bin/xhpast_parse.php', Index: src/channel/PhutilChannel.php =================================================================== --- src/channel/PhutilChannel.php +++ src/channel/PhutilChannel.php @@ -231,6 +231,14 @@ /** + * Close the channel for writing. + * + * @return void + * @task impl + */ + abstract public function closeWriteChannel(); + + /** * Test if the channel is open for reading. * * @return bool True if the channel is open for reading. Index: src/channel/PhutilChannelChannel.php =================================================================== --- src/channel/PhutilChannelChannel.php +++ src/channel/PhutilChannelChannel.php @@ -40,6 +40,10 @@ return $this->channel->isOpen(); } + public function closeWriteChannel() { + return $this->channel->closeWriteChannel(); + } + public function isOpenForReading() { return $this->channel->isOpenForReading(); } Index: src/channel/PhutilExecChannel.php =================================================================== --- src/channel/PhutilExecChannel.php +++ src/channel/PhutilExecChannel.php @@ -108,6 +108,10 @@ $this->future->write($bytes, $keep_pipe = true); } + public function closeWriteChannel() { + $this->future->write('', $keep_pipe = false); + } + protected function writeBytes($bytes) { throw new Exception("ExecFuture can not write bytes directly!"); } Index: src/channel/PhutilProtocolChannel.php =================================================================== --- src/channel/PhutilProtocolChannel.php +++ src/channel/PhutilProtocolChannel.php @@ -121,11 +121,17 @@ * @task wait */ public function waitForMessage() { - while ($this->update()) { + while (true) { + $is_open = $this->update(); $message = $this->read(); if ($message !== null) { return $message; } + + if (!$is_open) { + break; + } + self::waitForAny(array($this)); } Index: src/channel/PhutilSocketChannel.php =================================================================== --- src/channel/PhutilSocketChannel.php +++ src/channel/PhutilSocketChannel.php @@ -173,6 +173,10 @@ } } + public function closeWriteChannel() { + $this->closeWriteSocket(); + } + private function closeOneSocket($socket) { if (!$socket) { return; Index: src/channel/__tests__/PhutilPHPObjectProtocolChannelTestCase.php =================================================================== --- src/channel/__tests__/PhutilPHPObjectProtocolChannelTestCase.php +++ src/channel/__tests__/PhutilPHPObjectProtocolChannelTestCase.php @@ -26,4 +26,41 @@ "Objects are not the same."); } + public function testCloseSocketWriteChannel() { + list($x, $y) = PhutilSocketChannel::newChannelPair(); + $xp = new PhutilPHPObjectProtocolChannel($x); + $yp = new PhutilPHPObjectProtocolChannel($y); + + $yp->closeWriteChannel(); + $yp->update(); + + // NOTE: This test is more broad than the implementation needs to be. A + // better test would be to verify that this throws an exception: + // + // $xp->waitForMessage(); + // + // However, if the test breaks, that method will hang forever instead of + // returning, which would be hard to diagnose. Since the current + // implementation shuts down the entire channel, just test for that. + + $this->assertEqual(false, $xp->update(), 'Expected channel to close.'); + } + + public function testCloseExecWriteChannel() { + $future = new ExecFuture('cat'); + + // If this test breaks, we want to explode, not hang forever. + $future->setTimeout(5); + + $exec_channel = new PhutilExecChannel($future); + $exec_channel->write("quack"); + $exec_channel->closeWriteChannel(); + + // If `closeWriteChannel()` did what it is supposed to, this will just + // echo "quack" and exit with no error code. If the channel did not close, + // this will time out after 5 seconds and throw. + $future->resolvex(); + } + + }