diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 3ba4f00..9073658 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1,168 +1,170 @@ array( 'CommandException' => 'future/exec', 'ConduitClient' => 'conduit/client', 'ConduitClientException' => 'conduit/client', 'ConduitFuture' => 'conduit/client', 'ExecFuture' => 'future/exec', 'FileFinder' => 'filesystem/filefinder', 'FileList' => 'filesystem/filelist', 'Filesystem' => 'filesystem', 'FilesystemException' => 'filesystem', 'Future' => 'future', 'FutureIterator' => 'future', 'FutureProxy' => 'future/proxy', 'HTTPFuture' => 'future/http', 'HTTPSFuture' => 'future/https', 'LinesOfALargeFile' => 'filesystem/linesofalargefile', 'MFilterTestHelper' => 'utils/__tests__', 'PhutilConsoleFormatter' => 'console', 'PhutilDaemon' => 'daemon/base', 'PhutilDaemonOverseer' => 'daemon/overseer', 'PhutilDefaultSyntaxHighlighterEngine' => 'markup/syntax/engine/default', 'PhutilDocblockParser' => 'parser/docblock', 'PhutilDocblockParserTestCase' => 'parser/docblock/__tests__', 'PhutilErrorHandler' => 'error', + 'PhutilExcessiveServiceCallsDaemon' => 'daemon/torture/excessiveservicecalls', 'PhutilFatalDaemon' => 'daemon/torture/fatal', 'PhutilHangForeverDaemon' => 'daemon/torture/hangforever', 'PhutilInteractiveEditor' => 'console/editor', 'PhutilMarkupEngine' => 'markup/engine', 'PhutilMissingSymbolException' => 'symbols/exception/missing', 'PhutilNiceDaemon' => 'daemon/torture/nice', 'PhutilProcessGroupDaemon' => 'daemon/torture/processgroup', 'PhutilReadableSerializer' => 'readableserializer', 'PhutilRemarkupBlockStorage' => 'markup/engine/remarkup/blockstorage', 'PhutilRemarkupEngine' => 'markup/engine/remarkup', 'PhutilRemarkupEngineBlockRule' => 'markup/engine/remarkup/blockrule/base', 'PhutilRemarkupEngineRemarkupCodeBlockRule' => 'markup/engine/remarkup/blockrule/remarkupcode', 'PhutilRemarkupEngineRemarkupDefaultBlockRule' => 'markup/engine/remarkup/blockrule/remarkupdefault', 'PhutilRemarkupEngineRemarkupHeaderBlockRule' => 'markup/engine/remarkup/blockrule/remarkupheader', 'PhutilRemarkupEngineRemarkupInlineBlockRule' => 'markup/engine/remarkup/blockrule/remarkupinline', 'PhutilRemarkupEngineRemarkupListBlockRule' => 'markup/engine/remarkup/blockrule/remarkuplist', 'PhutilRemarkupEngineRemarkupNoteBlockRule' => 'markup/engine/remarkup/blockrule/remarkupnote', 'PhutilRemarkupEngineRemarkupQuotesBlockRule' => 'markup/engine/remarkup/blockrule/remarkupquotes', 'PhutilRemarkupEngineTestCase' => 'markup/engine/remarkup/__tests__', 'PhutilRemarkupRule' => 'markup/engine/remarkup/markuprule/base', 'PhutilRemarkupRuleBold' => 'markup/engine/remarkup/markuprule/bold', 'PhutilRemarkupRuleEscapeHTML' => 'markup/engine/remarkup/markuprule/escapehtml', 'PhutilRemarkupRuleEscapeRemarkup' => 'markup/engine/remarkup/markuprule/escaperemarkup', 'PhutilRemarkupRuleHyperlink' => 'markup/engine/remarkup/markuprule/hyperlink', 'PhutilRemarkupRuleItalic' => 'markup/engine/remarkup/markuprule/italics', 'PhutilRemarkupRuleLinebreaks' => 'markup/engine/remarkup/markuprule/linebreaks', 'PhutilRemarkupRuleMonospace' => 'markup/engine/remarkup/markuprule/monospace', 'PhutilSaturateStdoutDaemon' => 'daemon/torture/saturatestdout', 'PhutilServiceProfiler' => 'serviceprofiler', 'PhutilSymbolLoader' => 'symbols', 'PhutilSyntaxHighlighter' => 'markup/syntax/highlighter/base', 'PhutilSyntaxHighlighterEngine' => 'markup/syntax/engine/base', 'PhutilTortureTestDaemon' => 'daemon/torture/base', 'PhutilURI' => 'parser/uri', 'PhutilUtilsTestCase' => 'utils/__tests__', 'PhutilXHPASTSyntaxHighlighter' => 'markup/syntax/highlighter/xhpast', 'TempFile' => 'filesystem/tempfile', 'XHPASTNode' => 'parser/xhpast/api/node', 'XHPASTNodeList' => 'parser/xhpast/api/list', 'XHPASTSyntaxErrorException' => 'parser/xhpast/api/exception', 'XHPASTToken' => 'parser/xhpast/api/token', 'XHPASTTree' => 'parser/xhpast/api/tree', 'XHPASTTreeTestCase' => 'parser/xhpast/api/tree/__tests__', ), 'function' => array( 'Futures' => 'future', 'array_mergev' => 'utils', 'array_select_keys' => 'utils', 'coalesce' => 'utils', 'csprintf' => 'xsprintf/csprintf', 'exec_manual' => 'future/exec', 'execx' => 'future/exec', 'head' => 'utils', 'id' => 'utils', 'idx' => 'utils', 'ifilter' => 'utils', 'igroup' => 'utils', 'ipull' => 'utils', 'isort' => 'utils', 'jsprintf' => 'xsprintf/jsprintf', 'mfilter' => 'utils', 'mgroup' => 'utils', 'mpull' => 'utils', 'msort' => 'utils', 'newv' => 'utils', 'nonempty' => 'utils', 'phlog' => 'error', 'phutil_autoload_class' => 'autoload', 'phutil_console_confirm' => 'console', 'phutil_console_format' => 'console', 'phutil_console_prompt' => 'console', 'phutil_console_wrap' => 'console', 'phutil_deprecated' => 'moduleutils', 'phutil_error_listener_example' => 'error', 'phutil_escape_html' => 'markup', 'phutil_escape_uri' => 'markup', 'phutil_get_library_name_for_root' => 'moduleutils', 'phutil_get_library_root' => 'moduleutils', 'phutil_get_library_root_for_path' => 'moduleutils', 'phutil_is_utf8' => 'utils', 'phutil_passthru' => 'future/exec', 'phutil_render_tag' => 'markup', 'phutil_utf8ize' => 'utils', 'vcsprintf' => 'xsprintf/csprintf', 'vjsprintf' => 'xsprintf/jsprintf', 'xhp_parser_node_constants' => 'parser/xhpast/constants', 'xhpast_get_binary_path' => 'parser/xhpast/bin', 'xhpast_get_build_instructions' => 'parser/xhpast/bin', 'xhpast_get_parser_future' => 'parser/xhpast/bin', 'xhpast_is_available' => 'parser/xhpast/bin', 'xhpast_parser_token_constants' => 'parser/xhpast/constants', 'xsprintf' => 'xsprintf', 'xsprintf_callback_example' => 'xsprintf', 'xsprintf_command' => 'xsprintf/csprintf', 'xsprintf_javascript' => 'xsprintf/jsprintf', ), 'requires_class' => array( 'ConduitFuture' => 'FutureProxy', 'ExecFuture' => 'Future', 'FutureProxy' => 'Future', 'HTTPFuture' => 'Future', 'HTTPSFuture' => 'Future', 'PhutilDefaultSyntaxHighlighterEngine' => 'PhutilSyntaxHighlighterEngine', 'PhutilDocblockParserTestCase' => 'ArcanistPhutilTestCase', + 'PhutilExcessiveServiceCallsDaemon' => 'PhutilTortureTestDaemon', 'PhutilFatalDaemon' => 'PhutilTortureTestDaemon', 'PhutilHangForeverDaemon' => 'PhutilTortureTestDaemon', 'PhutilNiceDaemon' => 'PhutilTortureTestDaemon', 'PhutilProcessGroupDaemon' => 'PhutilTortureTestDaemon', 'PhutilRemarkupEngine' => 'PhutilMarkupEngine', 'PhutilRemarkupEngineRemarkupCodeBlockRule' => 'PhutilRemarkupEngineBlockRule', 'PhutilRemarkupEngineRemarkupDefaultBlockRule' => 'PhutilRemarkupEngineBlockRule', 'PhutilRemarkupEngineRemarkupHeaderBlockRule' => 'PhutilRemarkupEngineBlockRule', 'PhutilRemarkupEngineRemarkupInlineBlockRule' => 'PhutilRemarkupEngineBlockRule', 'PhutilRemarkupEngineRemarkupListBlockRule' => 'PhutilRemarkupEngineBlockRule', 'PhutilRemarkupEngineRemarkupNoteBlockRule' => 'PhutilRemarkupEngineBlockRule', 'PhutilRemarkupEngineRemarkupQuotesBlockRule' => 'PhutilRemarkupEngineBlockRule', 'PhutilRemarkupEngineTestCase' => 'ArcanistPhutilTestCase', 'PhutilRemarkupRuleBold' => 'PhutilRemarkupRule', 'PhutilRemarkupRuleEscapeHTML' => 'PhutilRemarkupRule', 'PhutilRemarkupRuleEscapeRemarkup' => 'PhutilRemarkupRule', 'PhutilRemarkupRuleHyperlink' => 'PhutilRemarkupRule', 'PhutilRemarkupRuleItalic' => 'PhutilRemarkupRule', 'PhutilRemarkupRuleLinebreaks' => 'PhutilRemarkupRule', 'PhutilRemarkupRuleMonospace' => 'PhutilRemarkupRule', 'PhutilSaturateStdoutDaemon' => 'PhutilTortureTestDaemon', 'PhutilTortureTestDaemon' => 'PhutilDaemon', 'PhutilUtilsTestCase' => 'ArcanistPhutilTestCase', 'XHPASTTreeTestCase' => 'ArcanistPhutilTestCase', ), 'requires_interface' => array( ), )); diff --git a/src/conduit/client/ConduitClient.php b/src/conduit/client/ConduitClient.php index 076044e..06be056 100644 --- a/src/conduit/client/ConduitClient.php +++ b/src/conduit/client/ConduitClient.php @@ -1,127 +1,120 @@ connectionID; } public function __construct($uri) { $this->protocol = parse_url($uri, PHP_URL_SCHEME); $this->host = parse_url($uri, PHP_URL_HOST); $this->path = parse_url($uri, PHP_URL_PATH); if (!$this->host) { throw new Exception("Conduit URI '{$uri}' must include a valid host."); } $this->path = trim($this->path, '/').'/'; } public function callMethodSynchronous($method, array $params) { return $this->callMethod($method, $params)->resolve(); } public function didReceiveResponse($method, $data) { if ($this->profilerCallID !== null) { $profiler = PhutilServiceProfiler::getInstance(); $profiler->endServiceCall( $this->profilerCallID, array()); } if ($method == 'conduit.connect') { $this->sessionKey = idx($data, 'sessionKey'); $this->connectionID = idx($data, 'connectionID'); } return $data; } public function callMethod($method, array $params) { $meta = array(); if ($this->sessionKey) { $meta['sessionKey'] = $this->sessionKey; } if ($this->connectionID) { $meta['connectionID'] = $this->connectionID; } if ($method == 'conduit.connect') { $certificate = idx($params, 'certificate'); if ($certificate) { $token = time(); $params['authToken'] = $token; $params['authSignature'] = sha1($token.$certificate); } unset($params['certificate']); } if ($meta) { $params['__conduit__'] = $meta; } $uri = $this->protocol.'://'.$this->host.'/'.$this->path.$method; $data = array( 'params' => json_encode($params), 'output' => 'json', ); if ($this->protocol == 'https') { $core_future = new HTTPSFuture($uri, $data); } else { $core_future = new HTTPFuture($uri, $data); $core_future->setMethod('POST'); } $profiler = PhutilServiceProfiler::getInstance(); $this->profilerCallID = $profiler->beginServiceCall( array( 'type' => 'conduit', 'method' => $method, )); $conduit_future = new ConduitFuture($core_future); $conduit_future->setClient($this, $method); $conduit_future->isReady(); return $conduit_future; } - public function setTraceMode($mode) { - phutil_deprecated( - 'ConduitClient::setTraceMode()', - 'Use PhutilServiceProfiler.'); - return $this; - } - } diff --git a/src/conduit/client/ConduitFuture.php b/src/conduit/client/ConduitFuture.php index 606069e..a0c05f2 100644 --- a/src/conduit/client/ConduitFuture.php +++ b/src/conduit/client/ConduitFuture.php @@ -1,98 +1,69 @@ client = $client; $this->conduitMethod = $method; return $this; } - public function setTraceMode($trace_mode) { - $this->traceMode = $trace_mode; - return $this; - } - - public function setTraceName($trace_name) { - $this->traceName = $trace_name; - return $this; - } - - public function setStartTime($time) { - $this->startTime = $time; - return $this; - } - protected function didReceiveResult($result) { - if (empty($this->endTime)) { - $this->endTime = microtime(true); - $time = (int)(1000 * ($this->endTime - $this->startTime)); - $time = number_format($time).' ms'; - if ($this->traceMode) { - echo "[Conduit] <<< Completed {$this->traceName} in {$time}.\n"; - } - } - if ($result[0] !== 200) { throw new Exception( "Host returned an HTTP error response #{$result[0]} in response ". "to a Conduit method call."); } $raw = $result[1]; $shield = 'for(;;);'; if (!strncmp($raw, $shield, strlen($shield))) { $raw = substr($raw, strlen($shield)); } $data = json_decode($raw, true); if (!is_array($data)) { throw new Exception( "Host returned HTTP/200, but invalid JSON data in response to ". "a Conduit method call:\n{$raw}"); } if ($data['error_code']) { throw new ConduitClientException( $data['error_code'], $data['error_info']); } $result = $data['result']; $result = $this->client->didReceiveResponse( $this->conduitMethod, $result); return $result; } } diff --git a/src/conduit/client/__init__.php b/src/conduit/client/__init__.php index 688c45b..4066ef2 100644 --- a/src/conduit/client/__init__.php +++ b/src/conduit/client/__init__.php @@ -1,19 +1,18 @@ argv = $argv; if (!self::$sighandlerInstalled) { self::$sighandlerInstalled = true; pcntl_signal(SIGINT, __CLASS__.'::exitOnSignal'); pcntl_signal(SIGTERM, __CLASS__.'::exitOnSignal'); } + + // Without discard mode, this consumes unbounded amounts of memory. Keep + // memory bounded. + PhutilServiceProfiler::getInstance()->enableDiscardMode(); } final public function stillWorking() { if (!posix_isatty(STDOUT)) { posix_kill(posix_getppid(), SIGUSR1); } if ($this->traceMemory) { $memuse = number_format(memory_get_usage() / 1024, 1); $daemon = get_class($this); fprintf(STDERR, " {$daemon} Memory Usage: {$memuse} KB\n"); } } final protected function sleep($duration) { $this->stillWorking(); while ($duration > 0) { sleep(min($duration, 60)); $duration -= 60; $this->stillWorking(); } } public static function exitOnSignal($signo) { // Normally, PHP doesn't invoke destructors when existing in response to // a signal. This forces it to do so, so we have a fighting chance of // releasing any locks, leases or resources on our way out. exit(128 + $signo); } final protected function getArgv() { return $this->argv; } final public function execute() { $this->willRun(); $this->run(); } abstract protected function run(); final public function setTraceMemory() { $this->traceMemory = true; return $this; } final public function getTraceMemory() { return $this->traceMemory; } final public function setTraceMode() { $this->traceMode = true; PhutilServiceProfiler::installEchoListener(); $this->didSetTraceMode(); } final public function getTraceMode() { return $this->traceMode; } protected function willRun() { // This is a hook for subclasses. } protected function didSetTraceMode() { // This is a hook for subclasses. } } diff --git a/src/daemon/torture/excessiveservicecalls/PhutilExcessiveServiceCallsDaemon.php b/src/daemon/torture/excessiveservicecalls/PhutilExcessiveServiceCallsDaemon.php new file mode 100644 index 0000000..91d829f --- /dev/null +++ b/src/daemon/torture/excessiveservicecalls/PhutilExcessiveServiceCallsDaemon.php @@ -0,0 +1,33 @@ +stillWorking(); + } + } + +} diff --git a/src/daemon/torture/excessiveservicecalls/__init__.php b/src/daemon/torture/excessiveservicecalls/__init__.php new file mode 100644 index 0000000..136ad7f --- /dev/null +++ b/src/daemon/torture/excessiveservicecalls/__init__.php @@ -0,0 +1,13 @@ + array('pipe', 'r'), // stdin 1 => array('pipe', 'w'), // stdout 2 => array('pipe', 'w'), // stderr ); - public static function pushEchoMode($mode) { - phutil_deprecated( - 'ExecFuture::pushEchoMode()', - 'Use PhutilServiceProfiler.'); - } - - public static function popEchoMode() { - phutil_deprecated( - 'ExecFuture::popEchoMode()', - 'Use PhutilServiceProfiler.'); - } - - public static function peekEchoMode() { - phutil_deprecated( - 'ExecFuture::peekEchoMode()', - 'Use PhutilServiceProfiler.'); - } - /* -( Creating ExecFutures )----------------------------------------------- */ /** * Create a new ExecFuture. * * $future = new ExecFuture('wc -l %s', $file_path); * * @param string ##sprintf()##-style command string which will be passed * through @{function:csprintf} with the rest of the arguments. * @param ... Zero or more additional arguments for @{function:csprintf}. * @return ExecFuture ExecFuture for running the specified command. * @task create */ public function __construct($command) { $argv = func_get_args(); $this->command = call_user_func_array('csprintf', $argv); } /* -( Configuring Execution )---------------------------------------------- */ /** * Retrieve the raw command to be executed. * * @return string Raw command. * @task info */ public function getCommand() { return $this->command; } /** * Retrieve the byte limit for the stderr buffer. * * @return int Maximum buffer size, in bytes. * @task info */ public function getStderrSizeLimit() { return $this->stderrSizeLimit; } /** * Retrieve the byte limit for the stdout buffer. * * @return int Maximum buffer size, in bytes. * @task info */ public function getStdoutSizeLimit() { return $this->stdoutSizeLimit; } /** * Get the process's pid. This only works after execution is initiated, e.g. * by a call to start(). * * @return int Process ID of the executing process. * @task info */ public function getPID() { $status = $this->procGetStatus(); return $status['pid']; } /* -( Configuring Execution )---------------------------------------------- */ /** * Set a maximum size for the stdout read buffer. To limit stderr, see * @{method:setStderrSizeLimit}. The major use of these methods is to use less * memory if you are running a command which sometimes produces huge volumes * of output that you don't really care about. * * NOTE: Setting this to 0 means "no buffer", not "unlimited buffer". * * @param int Maximum size of the stdout read buffer. * @return this * @task config */ public function setStdoutSizeLimit($limit) { $this->stdoutSizeLimit = $limit; } /** * Set a maximum size for the stderr read buffer. * See @{method:setStdoutSizeLimit} for discussion. * * @param int Maximum size of the stderr read buffer. * @return this * @task config */ public function setStderrSizeLimit($limit) { $this->stderrSizeLimit = $limit; return $this; } /** * Set the current working directory to use when executing the command. * * @param string Directory to set as CWD before executing the command. * @return this * @task config */ public function setCWD($cwd) { $this->cwd = $cwd; return $this; } /* -( Interacting With Commands )------------------------------------------ */ /** * Read and return output from stdout and stderr, if any is available. This * method keeps a read cursor on each stream, but the entire streams are * still returned when the future resolves. You can call read() again after * resolving the future to retrieve only the parts of the streams you did not * previously read: * * $future = new ExecFuture('...'); * // ... * list($stdout) = $future->read(); // Returns output so far * list($stdout) = $future->read(); // Returns new output since first call * // ... * list($stdout) = $future->resolvex(); // Returns ALL output * list($stdout) = $future->read(); // Returns unread output * * NOTE: If you set a limit with @{method:setStdoutSizeLimit} or * @{method:setStderrSizeLimit}, this method will not be able to read data * past the limit. * * NOTE: If you call @{method:discardBuffers}, all the stdout/stderr data * will be thrown away and the cursors will be reset. * * @return pair <$stdout, $stderr> pair with new output since the last call * to this method. * @task interact */ public function read() { if ($this->start) { $this->isReady(); // Sync } $result = array( (string)substr($this->stdout, $this->stdoutPos), (string)substr($this->stderr, $this->stderrPos), ); $this->stdoutPos = strlen($this->stdout); $this->stderrPos = strlen($this->stderr); return $result; } /** * Write data to stdin of the command. * * @param string Data to write. * @param bool If true, keep the pipe open for writing. By default, the pipe * will be closed after the write completes so that commands which * listen for EOF will execute. * @return this * @task interact */ public function write($data, $keep_pipe = false) { $this->stdin .= $data; if (!$keep_pipe) { $this->closePipe = true; } if ($this->start) { $this->isReady(); // Sync } return $this; } /** * Permanently discard the stdout and stderr buffers and reset the read * cursors. This is basically useful only if you are streaming a large amount * of data from some process: * * $future = new ExecFuture('zcat huge_file.gz'); * do { * $done = $future->resolve(0.1); // Every 100ms, * list($stdout) = $future->read(); // read output... * echo $stdout; // send it somewhere... * $future->discardBuffers(); // and then free the buffers. * } while ($done === null); * * Conceivably you might also need to do this if you're writing a client using * ExecFuture and ##netcat##, but you probably should not do that. * * NOTE: This completely discards the data. It won't be available when the * future resolves. This is almost certainly only useful if you need the * buffer memory for some reason. * * @return this * @task interact */ public function discardBuffers() { $this->stdout = ''; $this->stderr = ''; $this->stdoutPos = 0; $this->stderrPos = 0; return $this; } /* -( Configuring Execution )---------------------------------------------- */ /** * Set a hard limit on execution time. If the command runs longer, it will * be killed and the future will resolve with error code * ##ExecFuture::TIMED_OUT_EXIT_CODE##. * * @param int Maximum number of seconds this command may execute for. * @return this * @task config */ public function setTimeout($seconds) { $this->timeout = $seconds; return $this; } /* -( Resolving Execution )------------------------------------------------ */ /** * Resolve a command, returning its exit code, stdout, and stderr. See also * @{function:exec_manual}. For stronger error-checking behavior, see * @{method:resolvex} and @{method:resolveJSON}. * * list($err, $stdout, $stderr) = $future->resolve(); * * @param float Optional timeout after which resolution will pause and * execution will return to the caller. * @return list <$err, $stdout, $stderr> list. * @task resolve */ public function resolve($timeout = null) { if (null === $timeout) { $timeout = $this->timeout; } return parent::resolve($timeout); } /** * Resolve a command you expect to exit with return code 0. Works like * @{method:resolve}, but throws if $err is nonempty. Returns only * $stdout and $stderr. See also @{function:execx}. * * list($stdout, $stderr) = $future->resolvex(); * * @param float Optional timeout after which resolution will pause and * execution will return to the caller. * @return pair <$stdout, $stderr> pair. * @task resolve */ public function resolvex($timeout = null) { list($err, $stdout, $stderr) = $this->resolve($timeout); if ($err) { $cmd = $this->command; throw new CommandException( "Command '{$cmd}' failed with error #{$err}:\n". "stdout:\n{$stdout}\n". "stderr:\n{$stderr}\n", $cmd, $err, $stdout, $stderr); } return array($stdout, $stderr); } /** * Resolve a command you expect to return valid JSON. Works like * @{method:resolvex}, but also throws if stderr is nonempty, or stdout is not * valid JSON. Returns a PHP array, decoded from the JSON command output. * * @param float Optional timeout after which resolution will pause and * execution will return to the caller. * @return array PHP array, decoded from JSON command output. * @task resolve */ public function resolveJSON($timeout = null) { list($stdout, $stderr) = $this->resolvex($timeout); if (strlen($stderr)) { $cmd = $this->command; throw new CommandException( "JSON command '{$cmd}' emitted text to stderr when none was expected: ". $stderr, $cmd, 0, $stdout, $stderr); } $object = json_decode($stdout, true); if (!is_array($object)) { $cmd = $this->command; throw new CommandException( "JSON command '{$cmd}' did not produce a valid JSON object on stdout: ". $stdout, $cmd, 0, $stdout, $stderr); } return $object; } /* -( Internals )---------------------------------------------------------- */ /** * Provides read sockets to the future core. * * @return list List of read sockets. * @task internal */ public function getReadSockets() { list($stdin, $stdout, $stderr) = $this->pipes; $sockets = array(); if (isset($stdout) && !feof($stdout)) { $sockets[] = $stdout; } if (isset($stderr) && !feof($stderr)) { $sockets[] = $stderr; } return $sockets; } /** * Provides write sockets to the future core. * * @return list List of write sockets. * @task internal */ public function getWriteSockets() { list($stdin, $stdout, $stderr) = $this->pipes; $sockets = array(); if (isset($stdin) && strlen($this->stdin) && !feof($stdin)) { $sockets[] = $stdin; } return $sockets; } /** * Reads some bytes from a stream, discarding output once a certain amount * has been accumulated. * * @param resource Stream to read from. * @param int Maximum number of bytes to return from $stream. If * additional bytes are available, they will be read and * discarded. * @param string Human-readable description of stream, for exception * message. * @return string The data read from the stream. * @task internal */ protected function readAndDiscard($stream, $limit, $description) { $output = ''; do { $data = fread($stream, 4096); if (false === $data) { throw new Exception('Failed to read from '.$description); } $read_bytes = strlen($data); if ($read_bytes > 0 && $limit > 0) { if ($read_bytes > $limit) { $data = substr($data, 0, $limit); } $output .= $data; $limit -= strlen($data); } } while ($read_bytes > 0); return $output; } /** * Begin or continue command execution. * * @return bool True if future has resolved. * @task internal */ public function isReady() { if (!$this->pipes) { $profiler = PhutilServiceProfiler::getInstance(); $this->profilerCallID = $profiler->beginServiceCall( array( 'type' => 'exec', 'command' => $this->command, )); $pipes = array(); $proc = proc_open( $this->command, self::$descriptorSpec, $pipes, $this->cwd); if (!is_resource($proc)) { throw new Exception('Failed to open process.'); } $this->start = time(); $this->pipes = $pipes; $this->proc = $proc; list($stdin, $stdout, $stderr) = $pipes; if ((!stream_set_blocking($stdout, false)) || (!stream_set_blocking($stderr, false)) || (!stream_set_blocking($stdin, false))) { $this->__destruct(); throw new Exception('Failed to set streams nonblocking.'); } return false; } if (!$this->proc) { return true; } list($stdin, $stdout, $stderr) = $this->pipes; if (isset($this->stdin) && strlen($this->stdin)) { $bytes = fwrite($stdin, $this->stdin); if ($bytes === false) { throw new Exception('Unable to write to stdin!'); } else if ($bytes) { $this->stdin = substr($this->stdin, $bytes); } if (!strlen($this->stdin) && $this->closePipe) { @fclose($stdin); $this->pipes[0] = null; } } else { if ($this->closePipe && $stdin) { @fclose($stdin); } $this->pipes[0] = null; } // Read status before reading pipes so that we can never miss data that // arrives between our last read and the process exiting. $status = $this->procGetStatus(); $this->stdout .= $this->readAndDiscard( $stdout, $this->getStdoutSizeLimit() - strlen($this->stdout), 'stdout'); $this->stderr .= $this->readAndDiscard( $stderr, $this->getStderrSizeLimit() - strlen($this->stderr), 'stderr'); if (!$status['running']) { $this->result = array( $status['exitcode'], $this->stdout, $this->stderr, ); $this->__destruct(); $this->endProfile(); return true; } if ($this->timeout && ((time() - $this->start) >= $this->timeout)) { if (defined('SIGKILL')) { $signal = SIGKILL; } else { $signal = 9; } proc_terminate($this->proc, $signal); $this->result = array( self::TIMED_OUT_EXIT_CODE, $this->stdout, $this->stderr."\n". "(This process was prematurely terminated by timeout.)"); $this->__destruct(); $this->endProfile(); return true; } } /** * Close and free resources if necessary. * * @return void * @task internal */ public function __destruct() { foreach ($this->pipes as $pipe) { if (isset($pipe)) { @fclose($pipe); } } $this->pipes = array(null, null, null); if ($this->proc) { @proc_close($this->proc); $this->proc = null; } $this->stdin = null; } /** * End the service call profiler for this command. * * @return void * @task internal */ private function endProfile() { if ($this->profilerCallID !== null) { $profiler = PhutilServiceProfiler::getInstance(); $profiler->endServiceCall( $this->profilerCallID, array( 'err' => $this->result ? idx($this->result, 0) : null, )); } } /** * Execute proc_get_status(), but avoid pitfalls. * * @return dict Process status. * @task internal */ private function procGetStatus() { // After the process exits, we only get one chance to read proc_get_status() // before it starts returning garbage. Make sure we don't throw away the // last good read. if ($this->procStatus) { if (!$this->procStatus['running']) { return $this->procStatus; } } $this->procStatus = proc_get_status($this->proc); return $this->procStatus; } } diff --git a/src/future/exec/__init__.php b/src/future/exec/__init__.php index 7254b2c..a264bca 100644 --- a/src/future/exec/__init__.php +++ b/src/future/exec/__init__.php @@ -1,18 +1,17 @@ discardMode = true; } public static function getInstance() { if (empty(self::$instance)) { self::$instance = new PhutilServiceProfiler(); } return self::$instance; } public function beginServiceCall(array $data) { $data['begin'] = microtime(true); $id = $this->logSize++; $this->events[$id] = $data; foreach ($this->listeners as $listener) { call_user_func($listener, 'begin', $id, $data); } return $id; } public function endServiceCall($call_id, array $data) { $data = ($this->events[$call_id] + $data); $data['end'] = microtime(true); $data['duration'] = ($data['end'] - $data['begin']); $this->events[$call_id] = $data; foreach ($this->listeners as $listener) { call_user_func($listener, 'end', $call_id, $data); } if ($this->discardMode) { unset($this->events[$call_id]); } } public function getServiceCallLog() { return $this->events; } public function addListener($callback) { $this->listeners[] = $callback; } public static function installEchoListener() { $instance = PhutilServiceProfiler::getInstance(); $instance->addListener(array('PhutilServiceProfiler', 'echoListener')); } public static function echoListener($type, $id, $data) { $is_begin = false; $is_end = false; switch ($type) { case 'begin': $is_begin = true; echo '>>> '; break; case 'end': $is_end = true; echo '<<< '; break; default: echo '??? '; break; } echo '['.$id.'] '; $type = idx($data, 'type', 'mystery'); echo '<'.$type.'> '; if ($is_begin) { switch ($type) { case 'exec': echo '$ '.$data['command']; break; + case 'conduit': + echo $data['method'].'()'; + break; } } if ($is_end) { echo number_format((int)(1000000 * $data['duration'])).' us'; } echo "\n"; } }