diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index e01a8d3..c61ee3e 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1,316 +1,320 @@ 2, 'class' => array( 'AASTNode' => 'parser/aast/api/AASTNode.php', 'AASTNodeList' => 'parser/aast/api/AASTNodeList.php', 'AASTToken' => 'parser/aast/api/AASTToken.php', 'AASTTree' => 'parser/aast/api/AASTTree.php', 'AbstractDirectedGraph' => 'utils/AbstractDirectedGraph.php', 'AbstractDirectedGraphTestCase' => 'utils/__tests__/AbstractDirectedGraphTestCase.php', 'BaseHTTPFuture' => 'future/http/BaseHTTPFuture.php', 'CommandException' => 'future/CommandException.php', 'ConduitClient' => 'conduit/ConduitClient.php', 'ConduitClientException' => 'conduit/ConduitClientException.php', 'ConduitFuture' => 'conduit/ConduitFuture.php', 'ExecFuture' => 'future/ExecFuture.php', 'ExecFutureTestCase' => 'future/__tests__/ExecFutureTestCase.php', 'FileFinder' => 'filesystem/FileFinder.php', 'FileList' => 'filesystem/FileList.php', 'Filesystem' => 'filesystem/Filesystem.php', 'FilesystemException' => 'filesystem/FilesystemException.php', 'Future' => 'future/Future.php', 'FutureIterator' => 'future/FutureIterator.php', 'FutureProxy' => 'future/FutureProxy.php', 'HTTPFuture' => 'future/http/HTTPFuture.php', 'HTTPFutureResponseStatus' => 'future/http/status/HTTPFutureResponseStatus.php', 'HTTPFutureResponseStatusCURL' => 'future/http/status/HTTPFutureResponseStatusCURL.php', 'HTTPFutureResponseStatusHTTP' => 'future/http/status/HTTPFutureResponseStatusHTTP.php', 'HTTPFutureResponseStatusParse' => 'future/http/status/HTTPFutureResponseStatusParse.php', 'HTTPFutureResponseStatusTransport' => 'future/http/status/HTTPFutureResponseStatusTransport.php', 'HTTPSFuture' => 'future/http/HTTPSFuture.php', 'ImmediateFuture' => 'future/ImmediateFuture.php', 'LinesOfALarge' => 'filesystem/linesofalarge/LinesOfALarge.php', 'LinesOfALargeExecFuture' => 'filesystem/linesofalarge/LinesOfALargeExecFuture.php', 'LinesOfALargeExecFutureTestCase' => 'filesystem/linesofalarge/__tests__/LinesOfALargeExecFutureTestCase.php', 'LinesOfALargeFile' => 'filesystem/linesofalarge/LinesOfALargeFile.php', 'LinesOfALargeFileTestCase' => 'filesystem/linesofalarge/__tests__/LinesOfALargeFileTestCase.php', 'MFilterTestHelper' => 'utils/__tests__/MFilterTestHelper.php', 'PhutilAWSEC2Future' => 'future/aws/PhutilAWSEC2Future.php', 'PhutilAWSException' => 'future/aws/PhutilAWSException.php', 'PhutilAWSFuture' => 'future/aws/PhutilAWSFuture.php', 'PhutilAggregateException' => 'error/PhutilAggregateException.php', 'PhutilArgumentParser' => 'parser/argument/PhutilArgumentParser.php', 'PhutilArgumentParserException' => 'parser/argument/exception/PhutilArgumentParserException.php', 'PhutilArgumentParserTestCase' => 'parser/argument/__tests__/PhutilArgumentParserTestCase.php', 'PhutilArgumentSpecification' => 'parser/argument/PhutilArgumentSpecification.php', 'PhutilArgumentSpecificationException' => 'parser/argument/exception/PhutilArgumentSpecificationException.php', 'PhutilArgumentSpecificationTestCase' => 'parser/argument/__tests__/PhutilArgumentSpecificationTestCase.php', 'PhutilArgumentUsageException' => 'parser/argument/exception/PhutilArgumentUsageException.php', 'PhutilArgumentWorkflow' => 'parser/argument/workflow/PhutilArgumentWorkflow.php', 'PhutilChannel' => 'channel/PhutilChannel.php', 'PhutilChannelChannel' => 'channel/PhutilChannelChannel.php', 'PhutilConsoleFormatter' => 'console/PhutilConsoleFormatter.php', 'PhutilConsoleStdinNotInteractiveException' => 'console/PhutilConsoleStdinNotInteractiveException.php', 'PhutilConsoleSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilConsoleSyntaxHighlighter.php', 'PhutilConsoleWrapTestCase' => 'console/__tests__/PhutilConsoleWrapTestCase.php', 'PhutilDaemon' => 'daemon/PhutilDaemon.php', 'PhutilDaemonOverseer' => 'daemon/PhutilDaemonOverseer.php', 'PhutilDefaultSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilDefaultSyntaxHighlighter.php', 'PhutilDefaultSyntaxHighlighterEngine' => 'markup/syntax/engine/PhutilDefaultSyntaxHighlighterEngine.php', 'PhutilDefaultSyntaxHighlighterEnginePygmentsFuture' => 'markup/syntax/highlighter/pygments/PhutilDefaultSyntaxHighlighterEnginePygmentsFuture.php', 'PhutilDefaultSyntaxHighlighterEngineTestCase' => 'markup/syntax/engine/__tests__/PhutilDefaultSyntaxHighlighterEngineTestCase.php', 'PhutilDeferredLog' => 'filesystem/PhutilDeferredLog.php', 'PhutilDeferredLogTestCase' => 'filesystem/__tests__/PhutilDeferredLogTestCase.php', 'PhutilDivinerSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilDivinerSyntaxHighlighter.php', 'PhutilDocblockParser' => 'parser/PhutilDocblockParser.php', 'PhutilDocblockParserTestCase' => 'parser/__tests__/PhutilDocblockParserTestCase.php', 'PhutilEmailAddress' => 'parser/PhutilEmailAddress.php', 'PhutilEmailAddressTestCase' => 'parser/__tests__/PhutilEmailAddressTestCase.php', 'PhutilErrorHandler' => 'error/PhutilErrorHandler.php', 'PhutilEvent' => 'events/PhutilEvent.php', 'PhutilEventConstants' => 'events/constant/PhutilEventConstants.php', 'PhutilEventEngine' => 'events/PhutilEventEngine.php', 'PhutilEventListener' => 'events/PhutilEventListener.php', 'PhutilEventType' => 'events/constant/PhutilEventType.php', 'PhutilExcessiveServiceCallsDaemon' => 'daemon/torture/PhutilExcessiveServiceCallsDaemon.php', 'PhutilExecChannel' => 'channel/PhutilExecChannel.php', 'PhutilFatalDaemon' => 'daemon/torture/PhutilFatalDaemon.php', 'PhutilHangForeverDaemon' => 'daemon/torture/PhutilHangForeverDaemon.php', 'PhutilHelpArgumentWorkflow' => 'parser/argument/workflow/PhutilHelpArgumentWorkflow.php', 'PhutilInteractiveEditor' => 'console/PhutilInteractiveEditor.php', 'PhutilJSON' => 'parser/PhutilJSON.php', 'PhutilJSONTestCase' => 'parser/__tests__/PhutilJSONTestCase.php', 'PhutilLanguageGuesser' => 'parser/PhutilLanguageGuesser.php', 'PhutilLanguageGuesserTestCase' => 'parser/__tests__/PhutilLanguageGuesserTestCase.php', 'PhutilMarkupEngine' => 'markup/PhutilMarkupEngine.php', 'PhutilMarkupTestCase' => 'markup/__tests__/PhutilMarkupTestCase.php', 'PhutilMissingSymbolException' => 'symbols/exception/PhutilMissingSymbolException.php', 'PhutilNiceDaemon' => 'daemon/torture/PhutilNiceDaemon.php', + 'PhutilPHPObjectProtocolChannel' => 'channel/PhutilPHPObjectProtocolChannel.php', + 'PhutilPHPObjectProtocolChannelTestCase' => 'channel/__tests__/PhutilPHPObjectProtocolChannelTestCase.php', 'PhutilPHTTestCase' => 'internationalization/__tests__/PhutilPHTTestCase.php', 'PhutilPerson' => 'internationalization/PhutilPerson.php', 'PhutilPersonTest' => 'internationalization/__tests__/PhutilPersonTest.php', 'PhutilProcessGroupDaemon' => 'daemon/torture/PhutilProcessGroupDaemon.php', 'PhutilProtocolChannel' => 'channel/PhutilProtocolChannel.php', 'PhutilPygmentsSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilPygmentsSyntaxHighlighter.php', 'PhutilRainbowSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilRainbowSyntaxHighlighter.php', 'PhutilReadableSerializer' => 'readableserializer/PhutilReadableSerializer.php', 'PhutilRemarkupBlockStorage' => 'markup/engine/remarkup/PhutilRemarkupBlockStorage.php', 'PhutilRemarkupEngine' => 'markup/engine/PhutilRemarkupEngine.php', 'PhutilRemarkupEngineBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupEngineBlockRule.php', 'PhutilRemarkupEngineRemarkupCodeBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupCodeBlockRule.php', 'PhutilRemarkupEngineRemarkupDefaultBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupDefaultBlockRule.php', 'PhutilRemarkupEngineRemarkupHeaderBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupHeaderBlockRule.php', 'PhutilRemarkupEngineRemarkupInlineBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupInlineBlockRule.php', 'PhutilRemarkupEngineRemarkupListBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupListBlockRule.php', 'PhutilRemarkupEngineRemarkupLiteralBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupLiteralBlockRule.php', 'PhutilRemarkupEngineRemarkupNoteBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupNoteBlockRule.php', 'PhutilRemarkupEngineRemarkupQuotesBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupQuotesBlockRule.php', 'PhutilRemarkupEngineTestCase' => 'markup/engine/__tests__/PhutilRemarkupEngineTestCase.php', 'PhutilRemarkupRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupRule.php', 'PhutilRemarkupRuleBold' => 'markup/engine/remarkup/markuprule/PhutilRemarkupRuleBold.php', 'PhutilRemarkupRuleDel' => 'markup/engine/remarkup/markuprule/PhutilRemarkupRuleDel.php', 'PhutilRemarkupRuleEscapeHTML' => 'markup/engine/remarkup/markuprule/PhutilRemarkupRuleEscapeHTML.php', 'PhutilRemarkupRuleEscapeRemarkup' => 'markup/engine/remarkup/markuprule/PhutilRemarkupRuleEscapeRemarkup.php', 'PhutilRemarkupRuleHyperlink' => 'markup/engine/remarkup/markuprule/PhutilRemarkupRuleHyperlink.php', 'PhutilRemarkupRuleItalic' => 'markup/engine/remarkup/markuprule/PhutilRemarkupRuleItalic.php', 'PhutilRemarkupRuleLinebreaks' => 'markup/engine/remarkup/markuprule/PhutilRemarkupRuleLinebreaks.php', 'PhutilRemarkupRuleMonospace' => 'markup/engine/remarkup/markuprule/PhutilRemarkupRuleMonospace.php', 'PhutilSaturateStdoutDaemon' => 'daemon/torture/PhutilSaturateStdoutDaemon.php', 'PhutilServiceProfiler' => 'serviceprofiler/PhutilServiceProfiler.php', 'PhutilSimpleOptions' => 'parser/PhutilSimpleOptions.php', 'PhutilSimpleOptionsTestCase' => 'parser/__tests__/PhutilSimpleOptionsTestCase.php', 'PhutilSocketChannel' => 'channel/PhutilSocketChannel.php', 'PhutilSymbolLoader' => 'symbols/PhutilSymbolLoader.php', 'PhutilSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilSyntaxHighlighter.php', 'PhutilSyntaxHighlighterEngine' => 'markup/syntax/engine/PhutilSyntaxHighlighterEngine.php', 'PhutilSyntaxHighlighterException' => 'markup/syntax/highlighter/PhutilSyntaxHighlighterException.php', 'PhutilTortureTestDaemon' => 'daemon/torture/PhutilTortureTestDaemon.php', 'PhutilTranslator' => 'internationalization/PhutilTranslator.php', 'PhutilTranslatorTestCase' => 'internationalization/__tests__/PhutilTranslatorTestCase.php', 'PhutilURI' => 'parser/PhutilURI.php', 'PhutilURITestCase' => 'parser/__tests__/PhutilURITestCase.php', 'PhutilUTF8TestCase' => 'utils/__tests__/PhutilUTF8TestCase.php', 'PhutilUtilsTestCase' => 'utils/__tests__/PhutilUtilsTestCase.php', 'PhutilXHPASTSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilXHPASTSyntaxHighlighter.php', 'PhutilXHPASTSyntaxHighlighterTestCase' => 'markup/syntax/highlighter/__tests__/PhutilXHPASTSyntaxHighlighterTestCase.php', 'TempFile' => 'filesystem/TempFile.php', 'TestAbstractDirectedGraph' => 'utils/__tests__/TestAbstractDirectedGraph.php', 'XHPASTNode' => 'parser/xhpast/api/XHPASTNode.php', 'XHPASTSyntaxErrorException' => 'parser/xhpast/api/XHPASTSyntaxErrorException.php', 'XHPASTToken' => 'parser/xhpast/api/XHPASTToken.php', 'XHPASTTree' => 'parser/xhpast/api/XHPASTTree.php', 'XHPASTTreeTestCase' => 'parser/xhpast/api/__tests__/XHPASTTreeTestCase.php', ), 'function' => array( 'Futures' => 'future/functions.php', 'array_mergev' => 'utils/utils.php', 'array_select_keys' => 'utils/utils.php', 'assert_instances_of' => 'utils/utils.php', 'coalesce' => 'utils/utils.php', 'csprintf' => 'xsprintf/csprintf.php', 'exec_manual' => 'future/execx.php', 'execx' => 'future/execx.php', 'head' => 'utils/utils.php', 'head_key' => 'utils/utils.php', 'hsprintf' => 'markup/render.php', 'id' => 'utils/utils.php', 'idx' => 'utils/utils.php', 'ifilter' => 'utils/utils.php', 'igroup' => 'utils/utils.php', 'ipull' => 'utils/utils.php', 'isort' => 'utils/utils.php', 'jsprintf' => 'xsprintf/jsprintf.php', 'last' => 'utils/utils.php', 'last_key' => 'utils/utils.php', 'mfilter' => 'utils/utils.php', 'mgroup' => 'utils/utils.php', 'mpull' => 'utils/utils.php', 'msort' => 'utils/utils.php', 'newv' => 'utils/utils.php', 'nonempty' => 'utils/utils.php', 'phlog' => 'error/phlog.php', 'pht' => 'internationalization/pht.php', 'phutil_console_confirm' => 'console/format.php', 'phutil_console_format' => 'console/format.php', 'phutil_console_prompt' => 'console/format.php', 'phutil_console_require_tty' => 'console/format.php', 'phutil_console_wrap' => 'console/format.php', 'phutil_deprecated' => 'moduleutils/moduleutils.php', 'phutil_error_listener_example' => 'error/phlog.php', 'phutil_escape_html' => 'markup/render.php', 'phutil_escape_uri' => 'markup/render.php', 'phutil_escape_uri_path_component' => 'markup/render.php', 'phutil_get_library_name_for_root' => 'moduleutils/moduleutils.php', 'phutil_get_library_root' => 'moduleutils/moduleutils.php', 'phutil_get_library_root_for_path' => 'moduleutils/moduleutils.php', 'phutil_is_utf8' => 'utils/utf8.php', 'phutil_passthru' => 'future/execx.php', 'phutil_render_tag' => 'markup/render.php', 'phutil_unescape_uri_path_component' => 'markup/render.php', 'phutil_utf8_hard_wrap_html' => 'utils/utf8.php', 'phutil_utf8_shorten' => 'utils/utf8.php', 'phutil_utf8_strlen' => 'utils/utf8.php', 'phutil_utf8ize' => 'utils/utf8.php', 'phutil_utf8v' => 'utils/utf8.php', 'vcsprintf' => 'xsprintf/csprintf.php', 'vjsprintf' => 'xsprintf/jsprintf.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', 'xhpast_get_parser_future' => 'parser/xhpast/bin/xhpast_parse.php', 'xhpast_is_available' => 'parser/xhpast/bin/xhpast_parse.php', 'xhpast_parser_token_constants' => 'parser/xhpast/parser_tokens.php', 'xsprintf' => 'xsprintf/xsprintf.php', 'xsprintf_callback_example' => 'xsprintf/xsprintf.php', 'xsprintf_command' => 'xsprintf/csprintf.php', 'xsprintf_javascript' => 'xsprintf/jsprintf.php', ), 'xmap' => array( 'AASTNodeList' => array( 0 => 'Iterator', 1 => 'Countable', ), 'AbstractDirectedGraphTestCase' => 'ArcanistPhutilTestCase', 'BaseHTTPFuture' => 'Future', 'CommandException' => 'Exception', 'ConduitClientException' => 'Exception', 'ConduitFuture' => 'FutureProxy', 'ExecFuture' => 'Future', 'ExecFutureTestCase' => 'ArcanistPhutilTestCase', 'FilesystemException' => 'Exception', 'FutureIterator' => 'Iterator', 'FutureProxy' => 'Future', 'HTTPFuture' => 'BaseHTTPFuture', 'HTTPFutureResponseStatus' => 'Exception', 'HTTPFutureResponseStatusCURL' => 'HTTPFutureResponseStatus', 'HTTPFutureResponseStatusHTTP' => 'HTTPFutureResponseStatus', 'HTTPFutureResponseStatusParse' => 'HTTPFutureResponseStatus', 'HTTPFutureResponseStatusTransport' => 'HTTPFutureResponseStatus', 'HTTPSFuture' => 'BaseHTTPFuture', 'ImmediateFuture' => 'Future', 'LinesOfALarge' => 'Iterator', 'LinesOfALargeExecFuture' => 'LinesOfALarge', 'LinesOfALargeExecFutureTestCase' => 'ArcanistPhutilTestCase', 'LinesOfALargeFile' => 'LinesOfALarge', 'LinesOfALargeFileTestCase' => 'ArcanistPhutilTestCase', 'PhutilAWSEC2Future' => 'PhutilAWSFuture', 'PhutilAWSException' => 'Exception', 'PhutilAWSFuture' => 'FutureProxy', 'PhutilAggregateException' => 'Exception', 'PhutilArgumentParserException' => 'Exception', 'PhutilArgumentParserTestCase' => 'ArcanistPhutilTestCase', 'PhutilArgumentSpecificationException' => 'PhutilArgumentParserException', 'PhutilArgumentSpecificationTestCase' => 'ArcanistPhutilTestCase', 'PhutilArgumentUsageException' => 'PhutilArgumentParserException', 'PhutilChannelChannel' => 'PhutilChannel', 'PhutilConsoleStdinNotInteractiveException' => 'Exception', 'PhutilConsoleWrapTestCase' => 'ArcanistPhutilTestCase', 'PhutilDefaultSyntaxHighlighterEngine' => 'PhutilSyntaxHighlighterEngine', 'PhutilDefaultSyntaxHighlighterEnginePygmentsFuture' => 'FutureProxy', 'PhutilDefaultSyntaxHighlighterEngineTestCase' => 'ArcanistPhutilTestCase', 'PhutilDeferredLogTestCase' => 'ArcanistPhutilTestCase', 'PhutilDocblockParserTestCase' => 'ArcanistPhutilTestCase', 'PhutilEmailAddressTestCase' => 'ArcanistPhutilTestCase', 'PhutilEventType' => 'PhutilEventConstants', 'PhutilExcessiveServiceCallsDaemon' => 'PhutilTortureTestDaemon', 'PhutilExecChannel' => 'PhutilChannel', 'PhutilFatalDaemon' => 'PhutilTortureTestDaemon', 'PhutilHangForeverDaemon' => 'PhutilTortureTestDaemon', 'PhutilHelpArgumentWorkflow' => 'PhutilArgumentWorkflow', 'PhutilJSONTestCase' => 'ArcanistPhutilTestCase', 'PhutilLanguageGuesserTestCase' => 'ArcanistPhutilTestCase', 'PhutilMarkupTestCase' => 'ArcanistPhutilTestCase', 'PhutilMissingSymbolException' => 'Exception', 'PhutilNiceDaemon' => 'PhutilTortureTestDaemon', + 'PhutilPHPObjectProtocolChannel' => 'PhutilProtocolChannel', + 'PhutilPHPObjectProtocolChannelTestCase' => 'ArcanistPhutilTestCase', 'PhutilPHTTestCase' => 'ArcanistPhutilTestCase', 'PhutilPersonTest' => 'PhutilPerson', 'PhutilProcessGroupDaemon' => 'PhutilTortureTestDaemon', 'PhutilProtocolChannel' => 'PhutilChannelChannel', 'PhutilRemarkupEngine' => 'PhutilMarkupEngine', 'PhutilRemarkupEngineRemarkupCodeBlockRule' => 'PhutilRemarkupEngineBlockRule', 'PhutilRemarkupEngineRemarkupDefaultBlockRule' => 'PhutilRemarkupEngineBlockRule', 'PhutilRemarkupEngineRemarkupHeaderBlockRule' => 'PhutilRemarkupEngineBlockRule', 'PhutilRemarkupEngineRemarkupInlineBlockRule' => 'PhutilRemarkupEngineBlockRule', 'PhutilRemarkupEngineRemarkupListBlockRule' => 'PhutilRemarkupEngineBlockRule', 'PhutilRemarkupEngineRemarkupLiteralBlockRule' => 'PhutilRemarkupEngineBlockRule', 'PhutilRemarkupEngineRemarkupNoteBlockRule' => 'PhutilRemarkupEngineBlockRule', 'PhutilRemarkupEngineRemarkupQuotesBlockRule' => 'PhutilRemarkupEngineBlockRule', 'PhutilRemarkupEngineTestCase' => 'ArcanistPhutilTestCase', 'PhutilRemarkupRuleBold' => 'PhutilRemarkupRule', 'PhutilRemarkupRuleDel' => 'PhutilRemarkupRule', 'PhutilRemarkupRuleEscapeHTML' => 'PhutilRemarkupRule', 'PhutilRemarkupRuleEscapeRemarkup' => 'PhutilRemarkupRule', 'PhutilRemarkupRuleHyperlink' => 'PhutilRemarkupRule', 'PhutilRemarkupRuleItalic' => 'PhutilRemarkupRule', 'PhutilRemarkupRuleLinebreaks' => 'PhutilRemarkupRule', 'PhutilRemarkupRuleMonospace' => 'PhutilRemarkupRule', 'PhutilSaturateStdoutDaemon' => 'PhutilTortureTestDaemon', 'PhutilSimpleOptionsTestCase' => 'ArcanistPhutilTestCase', 'PhutilSocketChannel' => 'PhutilChannel', 'PhutilSyntaxHighlighterException' => 'Exception', 'PhutilTortureTestDaemon' => 'PhutilDaemon', 'PhutilTranslatorTestCase' => 'ArcanistPhutilTestCase', 'PhutilURITestCase' => 'ArcanistPhutilTestCase', 'PhutilUTF8TestCase' => 'ArcanistPhutilTestCase', 'PhutilUtilsTestCase' => 'ArcanistPhutilTestCase', 'PhutilXHPASTSyntaxHighlighterTestCase' => 'ArcanistPhutilTestCase', 'TestAbstractDirectedGraph' => 'AbstractDirectedGraph', 'XHPASTNode' => 'AASTNode', 'XHPASTSyntaxErrorException' => 'Exception', 'XHPASTToken' => 'AASTToken', 'XHPASTTree' => 'AASTTree', 'XHPASTTreeTestCase' => 'ArcanistPhutilTestCase', ), )); diff --git a/src/channel/PhutilChannel.php b/src/channel/PhutilChannel.php index 9ab62cf..eac6ac3 100644 --- a/src/channel/PhutilChannel.php +++ b/src/channel/PhutilChannel.php @@ -1,317 +1,334 @@ ibuf; $this->ibuf = ''; return $result; } /** * Write to the channel. A channel defines what data format it accepts, * so this method may take strings, objects, or anything else. * * The default implementation accepts bytes. * * @param wild Data to write to the channel, normally bytes. * @return this * * @task io */ public function write($bytes) { if (!is_scalar($bytes)) { throw new Exception("PhutilChannel->write() may only write strings!"); } $this->obuf .= $bytes; return $this; } /* -( Waiting for Activity )----------------------------------------------- */ /** * Wait (using select()) for activity on any channel. This method blocks until * some channel is ready to be updated. * * It does not provide a way to determine which channels are ready to be * updated. The expectation is that you'll just update every channel. This * might change eventually. * * Available options are: * * - 'read' (list) Additional streams to select for read. * - 'write' (list) Additional streams to select for write. * - 'except' (list) Additional streams to select for except. * - 'timeout' (float) Select timeout, defaults to 1. * * NOTE: Extra streams must be //streams//, not //sockets//, because this * method uses `stream_select()`, not `socket_select()`. * * @param list A list of channels to wait for. * @param dict Options, see above. * @return void * * @task wait */ public static function waitForAny(array $channels, array $options = array()) { assert_instances_of($channels, 'PhutilChannel'); $read = idx($options, 'read', array()); $write = idx($options, 'write', array()); $except = idx($options, 'except', array()); $wait = idx($options, 'timeout', 1); foreach ($channels as $channel) { // If any of the channels have data in read buffers, return immediately. // If we don't, we risk running select() on a bunch of sockets which won't // become readable because the data the application expects is already // in a read buffer. if (!$channel->isReadBufferEmpty()) { return; } $r_sockets = $channel->getReadSockets(); $w_sockets = $channel->getWriteSockets(); // If any channel has no read sockets and no write sockets, assume it // isn't selectable and return immediately (effectively degrading to a // busy wait). if (!$r_sockets && !$w_sockets) { return; } foreach ($r_sockets as $socket) { $read[] = $socket; $except[] = $socket; } foreach ($w_sockets as $socket) { $write[] = $socket; $except[] = $socket; } } if (!$read && !$write && !$except) { return false; } $wait_sec = (int)$wait; $wait_usec = 1000000 * ($wait - $wait_sec); @stream_select($read, $write, $except, $wait_sec, $wait_usec); } /* -( Responding to Activity )--------------------------------------------- */ /** * Updates the channel, filling input buffers and flushing output buffers. * Returns false if the channel has closed. * * @return bool True if the channel is still open. * * @task update */ public function update() { while (true) { $in = $this->readBytes(); if (!strlen($in)) { // Reading is blocked for now. break; } $this->ibuf .= $in; } while (strlen($this->obuf)) { $len = $this->writeBytes($this->obuf); if (!$len) { // Writing is blocked for now. break; } $this->obuf = substr($this->obuf, $len); } return $this->isOpen(); } /* -( Channel Implementation )--------------------------------------------- */ /** * Set a channel name. This is primarily intended to allow you to debug * channel code more easily, by naming channels something meaningful. * * @param string Channel name. * @return this * * @task impl */ public function setName($name) { $this->name = $name; return $this; } /** * Get the channel name, as set by @{method:setName}. * * @return string Name of the channel. * * @task impl */ public function getName() { return coalesce($this->name, get_class($this)); } /** * Test if the channel is open: active, can be read from and written to, etc. * * @return bool True if the channel is open. * * @task impl */ abstract public function isOpen(); /** * Read from the channel's underlying I/O. * * @return string Bytes, if available. * * @task impl */ abstract protected function readBytes(); /** * Write to the channel's underlying I/O. * * @param string Bytes to write. * @return int Number of bytes written. * * @task impl */ abstract protected function writeBytes($bytes); /** * Get sockets to select for reading. * * @return list Read sockets. * * @task impl */ protected function getReadSockets() { return array(); } /** * Get sockets to select for writing. * * @return list Write sockets. * * @task impl */ protected function getWriteSockets() { return array(); } /** * Test state of the read buffer. * * @return bool True if the read buffer is empty. * * @task impl */ protected function isReadBufferEmpty() { return (strlen($this->ibuf) == 0); } /** * Test state of the write buffer. * * @return bool True if the write buffer is empty. * * @task impl */ protected function isWriteBufferEmpty() { return (strlen($this->obuf) == 0); } + + /** + * Wait for any buffered writes to complete. This is a blocking call. When + * the call returns, the write buffer will be empty. + * + * @task impl + */ + public function flush() { + while (!$this->isWriteBufferEmpty()) { + self::waitForAny(array($this)); + if (!$this->update()) { + throw new Exception("Channel closed while flushing output!"); + } + } + return $this; + } + } diff --git a/src/channel/PhutilExecChannel.php b/src/channel/PhutilExecChannel.php index 790922e..e8a09d4 100644 --- a/src/channel/PhutilExecChannel.php +++ b/src/channel/PhutilExecChannel.php @@ -1,133 +1,134 @@ 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 reasonble * 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 * * @group channel */ final class PhutilExecChannel extends PhutilChannel { private $future; /* -( 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) { // 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() { list($stdout, $stderr) = $this->future->read(); $this->future->discardBuffers(); if (strlen($stderr)) { - throw new Exception("Unexpected output to stderr on Exec channel."); + throw new Exception( + "Unexpected output to stderr on exec channel: {$stderr}"); } return $stdout; } public function write($bytes) { $this->future->write($bytes, $keep_pipe = true); } protected function writeBytes($bytes) { throw new Exception("ExecFuture can not write bytes directly!"); } protected function getReadSockets() { return $this->future->getReadSockets(); } protected function getWriteSockets() { return $this->future->getWriteSockets(); } } diff --git a/src/channel/PhutilPHPObjectProtocolChannel.php b/src/channel/PhutilPHPObjectProtocolChannel.php new file mode 100644 index 0000000..7f663ed --- /dev/null +++ b/src/channel/PhutilPHPObjectProtocolChannel.php @@ -0,0 +1,101 @@ + + * + * ...where is a 4-byte unsigned big-endian integer. + * + * @task protocol + */ + protected function encodeMessage($message) { + $message = serialize($message); + $len = pack('N', strlen($message)); + return "{$len}{$message}"; + } + + + /** + * Decode a message received from the other end of the channel. + * + * @task protocol + */ + protected function decodeStream($data) { + $this->buf .= $data; + + $objects = array(); + while (strlen($this->buf) >= $this->byteLengthOfNextChunk) { + switch ($this->mode) { + case self::MODE_LENGTH: + $len = substr($this->buf, 0, 4); + $this->buf = substr($this->buf, 4); + + $this->mode = self::MODE_OBJECT; + $this->byteLengthOfNextChunk = head(unpack('N', $len)); + break; + case self::MODE_OBJECT: + $data = substr($this->buf, 0, $this->byteLengthOfNextChunk); + $this->buf = substr($this->buf, $this->byteLengthOfNextChunk); + + $obj = @unserialize($data); + if ($obj === false) { + throw new Exception("Failed to unserialize object: {$data}"); + } else { + $objects[] = $obj; + } + + $this->mode = self::MODE_LENGTH; + $this->byteLengthOfNextChunk = 4; + break; + } + } + + return $objects; + } + +} diff --git a/src/channel/PhutilSocketChannel.php b/src/channel/PhutilSocketChannel.php index ec8cfa8..d6c9602 100644 --- a/src/channel/PhutilSocketChannel.php +++ b/src/channel/PhutilSocketChannel.php @@ -1,111 +1,167 @@ socket = $socket; + $this->readSocket = $read_socket; + $this->writeSocket = $write_socket; } public function __destruct() { - $this->closeSocket(); + $this->closeSockets(); + } + + + /** + * Creates a pair of socket channels that are connected to each other. This + * is mostly useful for writing unit tests of, e.g., protocol channels. + * + * list($x, $y) = PhutilSocketChannel::newChannelPair(); + * + * @task construct + */ + public static function newChannelPair() { + $sockets = null; + + $domain = phutil_is_windows() ? STREAM_PF_INET : STREAM_PF_UNIX; + $pair = stream_socket_pair($domain, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP); + if (!$pair) { + throw new Exception("socket_create_pair() failed!"); + } + + $x = new PhutilSocketChannel($pair[0]); + $y = new PhutilSocketChannel($pair[1]); + + return array($x, $y); } public function isOpen() { - return (bool)$this->socket; + return (bool)$this->readSocket; } protected function readBytes() { - $data = @fread($this->socket, 4096); + $data = @fread($this->readSocket, 4096); if ($data === false) { - $this->closeSocket(); + $this->closeSockets(); $data = ''; } // NOTE: fread() continues returning empty string after the socket is // closed, we need to check for EOF explicitly. if ($data === '') { - if (feof($this->socket)) { - $this->closeSocket(); + if (feof($this->readSocket)) { + $this->closeSockets(); } } return $data; } protected function writeBytes($bytes) { - $len = @fwrite($this->socket, $bytes); + $socket = $this->writeSocket; + if (!$socket) { + $socket = $this->readSocket; + } + + $len = @fwrite($socket, $bytes); if ($len === false) { - $this->closeSocket(); + $this->closeSockets(); return 0; } return $len; } protected function getReadSockets() { - return array($this->socket); + return array($this->readSocket); } protected function getWriteSockets() { if ($this->isWriteBufferEmpty()) { return array(); + } else if ($this->writeSocket) { + return array($this->writeSocket); } else { - return array($this->socket); + return array($this->readSocket); } } - private function closeSocket() { - if ($this->socket) { - @stream_socket_shutdown($this->socket, STREAM_SHUT_RDWR); - @fclose($this->socket); - $this->socket = null; + private function closeSockets() { + foreach (array($this->readSocket, $this->writeSocket) as $socket) { + if (!$socket) { + continue; + } + + @stream_socket_shutdown($socket, STREAM_SHUT_RDWR); + @fclose($socket); } + $this->readSocket = null; + $this->writeSocket = null; } } diff --git a/src/channel/__tests__/PhutilPHPObjectProtocolChannelTestCase.php b/src/channel/__tests__/PhutilPHPObjectProtocolChannelTestCase.php new file mode 100644 index 0000000..3c6ec52 --- /dev/null +++ b/src/channel/__tests__/PhutilPHPObjectProtocolChannelTestCase.php @@ -0,0 +1,46 @@ + mt_rand(), + ); + + $xp->write($object); + $xp->flush(); + $result = $yp->waitForMessage(); + + $this->assertEqual( + true, + (array)$object === (array)$result, + "Values are identical."); + + $this->assertEqual( + false, + $object === $result, + "Objects are not the same."); + } + +}