diff --git a/src/filesystem/FileFinder.php b/src/filesystem/FileFinder.php index 87e38fb..98acef1 100644 --- a/src/filesystem/FileFinder.php +++ b/src/filesystem/FileFinder.php @@ -1,287 +1,294 @@ withType('f') * ->withSuffix('php') * ->find(); * * @task create Creating a File Query * @task config Configuring File Queries * @task exec Executing the File Query * @task internal Internal */ final class FileFinder extends Phobject { private $root; private $exclude = array(); private $paths = array(); private $name = array(); private $suffix = array(); private $type; private $generateChecksums = false; private $followSymlinks; private $forceMode; /** * Create a new FileFinder. * * @param string Root directory to find files beneath. * @return this * @task create */ public function __construct($root) { $this->root = rtrim($root, '/'); } /** * @task config */ public function excludePath($path) { $this->exclude[] = $path; return $this; } /** * @task config */ public function withName($name) { $this->name[] = $name; return $this; } /** * @task config */ public function withSuffix($suffix) { $this->suffix[] = '*.'.$suffix; return $this; } /** * @task config */ public function withPath($path) { $this->paths[] = $path; return $this; } /** * @task config */ public function withType($type) { $this->type = $type; return $this; } /** * @task config */ public function withFollowSymlinks($follow) { $this->followSymlinks = $follow; return $this; } /** * @task config */ public function setGenerateChecksums($generate) { $this->generateChecksums = $generate; return $this; } /** * @task config * @param string Either "php", "shell", or the empty string. */ public function setForceMode($mode) { $this->forceMode = $mode; return $this; } /** * @task internal */ public function validateFile($file) { $matches = !count($this->name) && !count($this->suffix); foreach ($this->name as $curr_name) { if (basename($file) === $curr_name) { $matches = true; break; } } foreach ($this->suffix as $curr_suffix) { if (fnmatch($curr_suffix, $file)) { $matches = true; break; } } if (!$matches) { return false; } $matches = (count($this->paths) == 0); foreach ($this->paths as $path) { if (fnmatch($path, $this->root.'/'.$file)) { $matches = true; break; } } $fullpath = $this->root.'/'.ltrim($file, '/'); if (($this->type == 'f' && is_dir($fullpath)) || ($this->type == 'd' && !is_dir($fullpath))) { $matches = false; } return $matches; } /** * @task internal */ private function getFiles($dir) { $found = Filesystem::listDirectory($this->root.'/'.$dir, true); $files = array(); if (strlen($dir) > 0) { $dir = rtrim($dir, '/').'/'; } foreach ($found as $filename) { // Only exclude files whose names match relative to the root. if ($dir == '') { $matches = true; foreach ($this->exclude as $exclude_path) { if (fnmatch(ltrim($exclude_path, './'), $dir.$filename)) { $matches = false; break; } } if (!$matches) { continue; } } if ($this->validateFile($dir.$filename)) { $files[] = $dir.$filename; } if (is_dir($this->root.'/'.$dir.$filename)) { foreach ($this->getFiles($dir.$filename) as $file) { $files[] = $file; } } } return $files; } /** * @task exec */ public function find() { $files = array(); if (!is_dir($this->root) || !is_readable($this->root)) { throw new Exception( pht( "Invalid %s root directory specified ('%s'). Root directory ". "must be a directory, be readable, and be specified with an ". "absolute path.", __CLASS__, $this->root)); } if ($this->forceMode == 'shell') { $php_mode = false; } else if ($this->forceMode == 'php') { $php_mode = true; } else { $php_mode = (phutil_is_windows() || !Filesystem::binaryExists('find')); } if ($php_mode) { $files = $this->getFiles(''); } else { $args = array(); $command = array(); $command[] = 'find'; if ($this->followSymlinks) { $command[] = '-L'; } $command[] = '.'; if ($this->exclude) { $command[] = $this->generateList('path', $this->exclude).' -prune'; $command[] = '-o'; } if ($this->type) { $command[] = '-type %s'; $args[] = $this->type; } if ($this->name || $this->suffix) { $command[] = $this->generateList('name', array_merge( $this->name, $this->suffix)); } if ($this->paths) { $command[] = $this->generateList('path', $this->paths); } $command[] = '-print0'; array_unshift($args, implode(' ', $command)); list($stdout) = newv('ExecFuture', $args) ->setCWD($this->root) ->resolvex(); $stdout = trim($stdout); if (!strlen($stdout)) { return array(); } $files = explode("\0", $stdout); // On OSX/BSD, find prepends a './' to each file. - for ($i = 0; $i < count($files); $i++) { - if (substr($files[$i], 0, 2) == './') { - $files[$i] = substr($files[$i], 2); + foreach ($files as $key => $file) { + // When matching directories, we can get "." back in the result set, + // but this isn't an interesting result. + if ($file == '.') { + unset($files[$key]); + continue; + } + + if (substr($files[$key], 0, 2) == './') { + $files[$key] = substr($files[$key], 2); } } } if (!$this->generateChecksums) { return $files; } else { $map = array(); foreach ($files as $line) { $fullpath = $this->root.'/'.ltrim($line, '/'); if (is_dir($fullpath)) { $map[$line] = null; } else { $map[$line] = md5_file($fullpath); } } return $map; } } /** * @task internal */ private function generateList($flag, array $items) { $items = array_map('escapeshellarg', $items); foreach ($items as $key => $item) { $items[$key] = '-'.$flag.' '.$item; } $items = implode(' -o ', $items); return '"(" '.$items.' ")"'; } } diff --git a/src/filesystem/__tests__/FileFinderTestCase.php b/src/filesystem/__tests__/FileFinderTestCase.php index ff47d44..03947c8 100644 --- a/src/filesystem/__tests__/FileFinderTestCase.php +++ b/src/filesystem/__tests__/FileFinderTestCase.php @@ -1,145 +1,165 @@ excludePath('./exclude') ->excludePath('subdir.txt'); return $finder; } public function testFinderWithChecksums() { foreach (array('php', 'shell') as $mode) { $files = $this->getFinder() ->setGenerateChecksums(true) ->withType('f') ->withPath('*') ->withSuffix('txt') ->setForceMode($mode) ->find(); // Test whether correct files were found. $this->assertTrue(array_key_exists('test.txt', $files)); $this->assertTrue(array_key_exists('file.txt', $files)); $this->assertTrue( array_key_exists( 'include_dir.txt/subdir.txt/alsoinclude.txt', $files)); $this->assertFalse(array_key_exists('test', $files)); $this->assertTrue(array_key_exists('.hidden.txt', $files)); $this->assertFalse(array_key_exists('exclude/file.txt', $files)); $this->assertFalse(array_key_exists('include_dir.txt', $files)); foreach ($files as $file => $checksum) { $this->assertFalse(is_dir($file)); } // Test checksums. $this->assertEqual( $files['test.txt'], 'aea46212fa8b8d0e0e6aa34a15c9e2f5'); $this->assertEqual( $files['file.txt'], '725130ba6441eadb4e5d807898e0beae'); $this->assertEqual( $files['.hidden.txt'], 'b6cfc9ce9afe12b258ee1c19c235aa27'); $this->assertEqual( $files['include_dir.txt/subdir.txt/alsoinclude.txt'], '91e5c1ad76ff229c6456ac92e74e1d9f'); } } public function testFinderWithoutChecksums() { foreach (array('php', 'shell') as $mode) { $files = $this->getFinder() ->withType('f') ->withPath('*') ->withSuffix('txt') ->setForceMode($mode) ->find(); // Test whether correct files were found. $this->assertTrue(in_array('test.txt', $files)); $this->assertTrue(in_array('file.txt', $files)); $this->assertTrue(in_array('.hidden.txt', $files)); $this->assertTrue( in_array('include_dir.txt/subdir.txt/alsoinclude.txt', $files)); $this->assertFalse(in_array('test', $files)); $this->assertFalse(in_array('exclude/file.txt', $files)); $this->assertFalse(in_array('include_dir.txt', $files)); foreach ($files as $file => $checksum) { $this->assertFalse(is_dir($file)); } } } - public function testFinderWithDirectories() { + public function testFinderWithFilesAndDirectories() { foreach (array('php', 'shell') as $mode) { $files = $this->getFinder() ->setGenerateChecksums(true) ->withPath('*') ->withSuffix('txt') ->setForceMode($mode) ->find(); // Test whether the correct files were found. $this->assertTrue(array_key_exists('test.txt', $files)); $this->assertTrue(array_key_exists('file.txt', $files)); $this->assertTrue( array_key_exists( 'include_dir.txt/subdir.txt/alsoinclude.txt', $files)); $this->assertFalse(array_key_exists('test', $files)); $this->assertTrue(array_key_exists('.hidden.txt', $files)); $this->assertFalse(array_key_exists('exclude/file.txt', $files)); $this->assertTrue(array_key_exists('include_dir.txt', $files)); // Test checksums. $this->assertEqual($files['test.txt'], 'aea46212fa8b8d0e0e6aa34a15c9e2f5'); $this->assertEqual($files['include_dir.txt'], null); } } + public function testFinderWithDirectories() { + foreach (array('php', 'shell') as $mode) { + $directories = $this->getFinder() + ->withType('d') + ->setForceMode($mode) + ->find(); + + sort($directories); + $directories = array_values($directories); + + $this->assertEqual( + array( + 'include_dir.txt', + 'include_dir.txt/subdir.txt', + ), + $directories, + pht('FileFinder of directories in "%s" mode', $mode)); + } + } + public function testFinderWithPath() { foreach (array('php', 'shell') as $mode) { $files = $this->getFinder() ->setGenerateChecksums(true) ->withType('f') ->withPath('*/include_dir.txt/subdir.txt/alsoinclude.txt') ->withSuffix('txt') ->setForceMode($mode) ->find(); // Test whether the correct files were found. $this->assertTrue( array_key_exists( 'include_dir.txt/subdir.txt/alsoinclude.txt', $files)); // Ensure that only the one file was found. $this->assertEqual(1, count($files)); } } public function testFinderWithNames() { foreach (array('php', 'shell') as $mode) { $files = $this->getFinder() ->withType('f') ->withPath('*') ->withName('test') ->setForceMode($mode) ->find(); // Test whether the correct files were found. $this->assertTrue(in_array('test', $files)); $this->assertFalse(in_array('exclude/test', $files)); $this->assertTrue(in_array('include_dir.txt/test', $files)); $this->assertTrue(in_array('include_dir.txt/subdir.txt/test', $files)); $this->assertEqual(3, count($files)); } } } diff --git a/src/parser/xhpast/api/XHPASTNode.php b/src/parser/xhpast/api/XHPASTNode.php index f3aafcf..0f0f991 100644 --- a/src/parser/xhpast/api/XHPASTNode.php +++ b/src/parser/xhpast/api/XHPASTNode.php @@ -1,301 +1,301 @@ getTypeName(), array( 'n_STRING_SCALAR', 'n_NUMERIC_SCALAR', )); } public function getDocblockToken() { if ($this->l == -1) { return null; } $tokens = $this->tree->getRawTokenStream(); for ($ii = $this->l - 1; $ii >= 0; $ii--) { if ($tokens[$ii]->getTypeName() == 'T_DOC_COMMENT') { return $tokens[$ii]; } if (!$tokens[$ii]->isAnyWhitespace()) { return null; } } return null; } public function evalStatic() { switch ($this->getTypeName()) { case 'n_STATEMENT': return $this->getChildByIndex(0)->evalStatic(); break; case 'n_STRING_SCALAR': return (string)$this->getStringLiteralValue(); case 'n_NUMERIC_SCALAR': $value = $this->getSemanticString(); if (preg_match('/^0x/i', $value)) { // Hex $value = base_convert(substr($value, 2), 16, 10); } else if (preg_match('/^0\d+$/i', $value)) { // Octal $value = base_convert(substr($value, 1), 8, 10); } return +$value; case 'n_SYMBOL_NAME': $value = $this->getSemanticString(); if ($value == 'INF') { return INF; } switch (strtolower($value)) { case 'true': return true; case 'false': return false; case 'null': return null; default: throw new Exception(pht('Unrecognized symbol name.')); } break; case 'n_UNARY_PREFIX_EXPRESSION': $operator = $this->getChildOfType(0, 'n_OPERATOR'); $operand = $this->getChildByIndex(1); switch ($operator->getSemanticString()) { case '-': return -$operand->evalStatic(); break; case '+': return $operand->evalStatic(); break; default: throw new Exception( pht('Unexpected operator in static expression.')); } break; case 'n_ARRAY_LITERAL': $result = array(); $values = $this->getChildOfType(0, 'n_ARRAY_VALUE_LIST'); foreach ($values->getChildren() as $child) { $key = $child->getChildByIndex(0); $val = $child->getChildByIndex(1); if ($key->getTypeName() == 'n_EMPTY') { $result[] = $val->evalStatic(); } else { $result[$key->evalStatic()] = $val->evalStatic(); } } return $result; case 'n_CONCATENATION_LIST': $result = ''; foreach ($this->getChildren() as $child) { if ($child->getTypeName() == 'n_OPERATOR') { continue; } $result .= $child->evalStatic(); } return $result; default: throw new Exception( pht( 'Unexpected node during static evaluation, of type: %s', $this->getTypeName())); } } public function isConstantString() { return $this->checkIsConstantString(); } public function isConstantStringWithMagicConstants() { return $this->checkIsConstantString(array('n_MAGIC_SCALAR')); } private function checkIsConstantString(array $additional_types = array()) { switch ($this->getTypeName()) { case 'n_HEREDOC': case 'n_STRING_SCALAR': return !$this->getStringVariables(); case 'n_CONCATENATION_LIST': foreach ($this->getChildren() as $child) { if ($child->getTypeName() == 'n_OPERATOR') { continue; } if (!$child->checkIsConstantString($additional_types)) { return false; } } return true; default: if (in_array($this->getTypeName(), $additional_types)) { return true; } return false; } } public function getStringVariables() { $value = $this->getConcreteString(); switch ($this->getTypeName()) { case 'n_HEREDOC': if (preg_match("/^<<<\s*'/", $value)) { // Nowdoc: <<<'EOT' return array(); } break; case 'n_STRING_SCALAR': if ($value[0] == "'") { return array(); } break; default: throw new Exception(pht('Unexpected type %s.', $this->getTypeName())); } // We extract just the variable names and ignore properties and array keys. $re = '/\\\\.|(\$|\{\$|\${)([a-z_\x7F-\xFF][a-z0-9_\x7F-\xFF]*)/i'; $matches = null; preg_match_all($re, $value, $matches, PREG_OFFSET_CAPTURE); return ipull(array_filter($matches[2]), 0, 1); } public function getStringLiteralValue() { if ($this->getTypeName() != 'n_STRING_SCALAR') { return null; } $value = $this->getSemanticString(); $type = $value[0]; $value = preg_replace('/^b?[\'"]|[\'"]$/i', '', $value); $esc = false; $len = strlen($value); $out = ''; if ($type == "'") { // Single quoted strings treat everything as a literal except "\\" and // "\'". return str_replace( array('\\\\', '\\\''), array('\\', "'"), $value); } // Double quoted strings treat "\X" as a literal if X isn't specifically // a character which needs to be escaped -- e.g., "\q" and "\'" are // literally "\q" and "\'". stripcslashes() is too aggressive, so find // all these under-escaped backslashes and escape them. for ($ii = 0; $ii < $len; $ii++) { $c = $value[$ii]; if ($esc) { $esc = false; switch ($c) { case 'x': $u = isset($value[$ii + 1]) ? $value[$ii + 1] : null; if (!preg_match('/^[a-f0-9]/i', $u)) { // PHP treats \x followed by anything which is not a hex digit // as a literal \x. $out .= '\\\\'.$c; break; } /* fallthrough */ case 'n': case 'r': case 'f': case 'v': case '"': case '$': case 't': case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': $out .= '\\'.$c; break; case 'e': // Since PHP 5.4.0, this means "esc". However, stripcslashes() does // not perform this conversion. $out .= chr(27); break; default: $out .= '\\\\'.$c; break; } } else if ($c == '\\') { $esc = true; } else { $out .= $c; } } return stripcslashes($out); } /** * Determines the parent namespace for a node. * * Traverses the AST upwards from a given node in order to determine the * namespace in which the node is declared. * * To prevent any possible ambiguity, the returned namespace will always be * prefixed with the namespace separator. * * @param XHPASTNode The input node. * @return string|null The namespace which contains the input node, or * `null` if no such node exists. */ public function getNamespace() { $namespaces = $this ->getTree() ->getRootNode() ->selectDescendantsOfType('n_NAMESPACE') ->getRawNodes(); foreach (array_reverse($namespaces) as $namespace) { if ($namespace->isAfter($this)) { continue; } $body = $namespace->getChildByIndex(1); if ($body->getTypeName() != 'n_EMPTY') { - if ($body->containsDescendant($this)) { - return self::getNamespaceName($namespace); + if (!$body->containsDescendant($this)) { + continue; } } return $namespace->getNamespaceName(); } return null; } /** * Returns the namespace name from a node of type `n_NAMESPACE`. * * @return string|null */ private function getNamespaceName() { if ($this->getTypeName() != 'n_NAMESPACE') { return null; } $namespace_name = $this->getChildByIndex(0); if ($namespace_name->getTypeName() == 'n_EMPTY') { return null; } return '\\'.$namespace_name->getConcreteString(); } } diff --git a/src/phage/__tests__/PhageAgentTestCase.php b/src/phage/__tests__/PhageAgentTestCase.php index 368ea8a..4973081 100644 --- a/src/phage/__tests__/PhageAgentTestCase.php +++ b/src/phage/__tests__/PhageAgentTestCase.php @@ -1,47 +1,49 @@ runBootloaderTests(new PhagePHPAgentBootloader()); } private function runBootloaderTests(PhageAgentBootloader $boot) { $name = get_class($boot); $exec = new ExecFuture('%C', $boot->getBootCommand()); $exec->write($boot->getBootSequence(), $keep_open = true); $exec_channel = new PhutilExecChannel($exec); $agent = new PhutilJSONProtocolChannel($exec_channel); $agent->write( array( 'type' => 'EXEC', 'key' => 1, 'command' => 'echo phage', + 'timeout' => null, )); $this->agentExpect( $agent, array( 'type' => 'RSLV', 'key' => 1, 'err' => 0, 'stdout' => "phage\n", 'stderr' => '', + 'timeout' => false, ), pht("'%s' for %s", 'echo phage', $name)); $agent->write( array( 'type' => 'EXIT', )); } private function agentExpect(PhutilChannel $agent, $expect, $what) { $message = $agent->waitForMessage(); $this->assertEqual($expect, $message, $what); } } diff --git a/src/phage/action/PhageAgentAction.php b/src/phage/action/PhageAgentAction.php index 1e996a4..9a84b47 100644 --- a/src/phage/action/PhageAgentAction.php +++ b/src/phage/action/PhageAgentAction.php @@ -1,272 +1,274 @@ isExiting) { throw new Exception( pht( 'You can not add new actions to an exiting agent.')); } $key = 'command/'.$this->commandKey++; $this->commands[$key] = array( 'key' => $key, 'command' => $action, ); $this->queued[$key] = $key; } public function isActiveAgent() { return $this->isActive; } final public function setLimit($limit) { $this->limit = $limit; return $this; } final public function getLimit() { return $this->limit; } final public function setThrottle($throttle) { $this->throttle = $throttle; return $this; } final public function getThrottle() { return $this->throttle; } abstract protected function newAgentFuture(PhutilCommandString $command); protected function getAllWaitingChannels() { $channels = array(); if ($this->isActiveAgent()) { $channels[] = $this->channel; } return $channels; } public function startAgent() { $bootloader = new PhagePHPAgentBootloader(); $future = $this->newAgentFuture($bootloader->getBootCommand()); $future->write($bootloader->getBootSequence(), $keep_open = true); $channel = new PhutilExecChannel($future); $channel->setStderrHandler(array($this, 'didReadAgentStderr')); $channel = new PhutilJSONProtocolChannel($channel); $this->future = $future; $this->channel = $channel; $this->isActive = true; } private function updateQueue() { // If we don't have anything waiting in queue, we have nothing to do. if (!$this->queued) { return false; } $now = microtime(true); // If we're throttling commands and recently started one, don't start // another one yet. $throttle = $this->getThrottle(); if ($throttle) { if ($this->waitUntil && ($now < $this->waitUntil)) { return false; } } // If we're limiting parallelism and the active list is full, don't // start anything else yet. $limit = $this->getLimit(); if ($limit) { if (count($this->active) >= $limit) { return false; } } // Move a command out of the queue and tell the agent to execute it. $key = head($this->queued); unset($this->queued[$key]); $this->active[$key] = $key; $command = $this->commands[$key]['command']; $channel = $this->getChannel(); $channel->write( array( 'type' => 'EXEC', 'key' => $key, 'command' => $command->getCommand()->getUnmaskedString(), + 'timeout' => $command->getTimeout(), )); if ($throttle) { $this->waitUntil = ($now + $throttle); } return true; } private function getChannel() { return $this->channel; } public function updateAgent() { if (!$this->isActiveAgent()) { return; } $channel = $this->channel; while (true) { do { $did_update = $this->updateQueue(); } while ($did_update); $is_open = $channel->update(); $message = $channel->read(); if ($message !== null) { switch ($message['type']) { case 'TEXT': $key = $message['key']; $this->writeOutput($key, $message['kind'], $message['text']); break; case 'RSLV': $key = $message['key']; $command = $this->commands[$key]['command']; $this->writeOutput($key, 'stdout', $message['stdout']); $this->writeOutput($key, 'stderr', $message['stderr']); $exit_code = $message['err']; $command->setExitCode($exit_code); + $command->setDidTimeout((bool)idx($message, 'timeout')); if ($exit_code != 0) { $exit_code = $this->formatOutput( pht( 'Command ("%s") exited nonzero ("%s")!', $command->getCommand(), $exit_code), $command->getLabel()); fprintf(STDOUT, '%s', $exit_code); } unset($this->active[$key]); if (!$this->active && !$this->queued) { $channel->write( array( 'type' => 'EXIT', 'key' => 'exit', )); $this->isExiting = true; break; } } } if (!$is_open) { if ($this->isExiting) { $this->isActive = false; break; } else { throw new Exception(pht('Channel closed unexpectedly!')); } } if ($message === null) { break; } } } private function writeOutput($key, $kind, $text) { if (!strlen($text)) { return; } switch ($kind) { case 'stdout': $target = STDOUT; break; case 'stderr': $target = STDERR; break; default: throw new Exception(pht('Unknown output kind "%s".', $kind)); } $command = $this->commands[$key]['command']; $label = $command->getLabel(); if (!strlen($label)) { $label = pht('Unknown Command'); } $text = $this->formatOutput($text, $label); fprintf($target, '%s', $text); } private function formatOutput($output, $context) { $output = phutil_split_lines($output, false); foreach ($output as $key => $line) { $output[$key] = tsprintf("[%s] %R\n", $context, $line); } $output = implode('', $output); return $output; } public function didReadAgentStderr($channel, $stderr) { throw new Exception( pht( 'Unexpected output on agent stderr: %s.', $stderr)); } } diff --git a/src/phage/action/PhageExecuteAction.php b/src/phage/action/PhageExecuteAction.php index fa477c2..811b289 100644 --- a/src/phage/action/PhageExecuteAction.php +++ b/src/phage/action/PhageExecuteAction.php @@ -1,41 +1,62 @@ command = $command; return $this; } public function getCommand() { return $this->command; } public function setLabel($label) { $this->label = $label; return $this; } public function getLabel() { return $this->label; } + public function setTimeout($timeout) { + $this->timeout = $timeout; + return $this; + } + + public function getTimeout() { + return $this->timeout; + } + public function setExitCode($exit_code) { $this->exitCode = $exit_code; return $this; } public function getExitCode() { return $this->exitCode; } + public function setDidTimeout($did_timeout) { + $this->didTimeout = $did_timeout; + return $this; + } + + public function getDidTimeout() { + return $this->didTimeout; + } + } diff --git a/src/phage/agent/PhagePHPAgent.php b/src/phage/agent/PhagePHPAgent.php index 6ac27bf..e3beebc 100644 --- a/src/phage/agent/PhagePHPAgent.php +++ b/src/phage/agent/PhagePHPAgent.php @@ -1,138 +1,145 @@ stdin = $stdin; } public function execute() { while (true) { if ($this->exec) { $iterator = new FutureIterator($this->exec); $iterator->setUpdateInterval(0.050); foreach ($iterator as $key => $future) { if ($future === null) { foreach ($this->exec as $read_key => $read_future) { $this->readFuture($read_key, $read_future); } break; } else { $this->resolveFuture($key, $future); } } } else { PhutilChannel::waitForAny(array($this->getMaster())); } $this->processInput(); } } private function getMaster() { if (!$this->master) { $raw_channel = new PhutilSocketChannel( $this->stdin, fopen('php://stdout', 'w')); $json_channel = new PhutilJSONProtocolChannel($raw_channel); $this->master = $json_channel; } return $this->master; } private function processInput() { $channel = $this->getMaster(); $open = $channel->update(); if (!$open) { throw new Exception(pht('Channel closed!')); } while (true) { $command = $channel->read(); if ($command === null) { break; } $this->processCommand($command); } } private function processCommand(array $spec) { switch ($spec['type']) { case 'EXEC': $key = $spec['key']; $cmd = $spec['command']; $future = new ExecFuture('%C', $cmd); + + $timeout = $spec['timeout']; + if ($timeout) { + $future->setTimeout(ceil($timeout)); + } + $future->isReady(); $this->exec[$key] = $future; break; case 'EXIT': $this->terminateAgent(); break; } } private function readFuture($key, ExecFuture $future) { $master = $this->getMaster(); list($stdout, $stderr) = $future->read(); $future->discardBuffers(); if (strlen($stdout)) { $master->write( array( 'type' => 'TEXT', 'key' => $key, 'kind' => 'stdout', 'text' => $stdout, )); } if (strlen($stderr)) { $master->write( array( 'type' => 'TEXT', 'key' => $key, 'kind' => 'stderr', 'text' => $stderr, )); } } private function resolveFuture($key, ExecFuture $future) { $result = $future->resolve(); $master = $this->getMaster(); $master->write( array( 'type' => 'RSLV', 'key' => $key, 'err' => $result[0], 'stdout' => $result[1], 'stderr' => $result[2], + 'timeout' => (bool)$future->getWasKilledByTimeout(), )); unset($this->exec[$key]); } public function __destruct() { $this->terminateAgent(); } private function terminateAgent() { foreach ($this->exec as $key => $future) { $future->resolveKill(); } exit(0); } }