diff --git a/scripts/daemon/exec/exec_daemon.php b/scripts/daemon/exec/exec_daemon.php index 41da2c0..af53131 100755 --- a/scripts/daemon/exec/exec_daemon.php +++ b/scripts/daemon/exec/exec_daemon.php @@ -1,131 +1,131 @@ #!/usr/bin/env php setTagline(pht('daemon executor')); $args->setSynopsis(<<parse( array( array( 'name' => 'trace', 'help' => pht('Enable debug tracing.'), ), array( 'name' => 'trace-memory', 'help' => pht('Enable debug memory tracing.'), ), array( 'name' => 'verbose', 'help' => pht('Enable verbose activity logging.'), ), array( 'name' => 'label', 'short' => 'l', 'param' => 'label', 'help' => pht( 'Optional process label. Makes "%s" nicer, no behavioral effects.', 'ps'), ), array( 'name' => 'daemon', 'wildcard' => true, ), )); $trace_memory = $args->getArg('trace-memory'); $trace_mode = $args->getArg('trace') || $trace_memory; $verbose = $args->getArg('verbose'); if (function_exists('posix_isatty') && posix_isatty(STDIN)) { fprintf(STDERR, pht('Reading daemon configuration from stdin...')."\n"); } $config = @file_get_contents('php://stdin'); $config = id(new PhutilJSONParser())->parse($config); PhutilTypeSpec::checkMap( $config, array( 'log' => 'optional string|null', 'argv' => 'optional list', 'load' => 'optional list', - 'autoscale' => 'optional wild', + 'down' => 'optional int', )); $log = idx($config, 'log'); if ($log) { ini_set('error_log', $log); PhutilErrorHandler::setErrorListener(array('PhutilDaemon', 'errorListener')); } $load = idx($config, 'load', array()); foreach ($load as $library) { $library = Filesystem::resolvePath($library); phutil_load_library($library); } PhutilErrorHandler::initialize(); $daemon = $args->getArg('daemon'); if (!$daemon) { throw new PhutilArgumentUsageException( pht('Specify which class of daemon to start.')); } else if (count($daemon) > 1) { throw new PhutilArgumentUsageException( pht('Specify exactly one daemon to start.')); } else { $daemon = head($daemon); if (!class_exists($daemon)) { throw new PhutilArgumentUsageException( pht( 'No class "%s" exists in any known library.', $daemon)); } else if (!is_subclass_of($daemon, 'PhutilDaemon')) { throw new PhutilArgumentUsageException( pht( 'Class "%s" is not a subclass of "%s".', $daemon, 'PhutilDaemon')); } } $argv = idx($config, 'argv', array()); $daemon = newv($daemon, array($argv)); if ($trace_mode) { $daemon->setTraceMode(); } if ($trace_memory) { $daemon->setTraceMemory(); } if ($verbose) { $daemon->setVerbose(true); } -$autoscale = idx($config, 'autoscale'); -if ($autoscale) { - $daemon->setAutoscaleProperties($autoscale); +$down_duration = idx($config, 'down'); +if ($down_duration) { + $daemon->setScaledownDuration($down_duration); } $daemon->execute(); diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 66dd81b..76d8676 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1,1078 +1,1080 @@ 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', 'AphrontAccessDeniedQueryException' => 'aphront/storage/exception/AphrontAccessDeniedQueryException.php', 'AphrontBaseMySQLDatabaseConnection' => 'aphront/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php', 'AphrontCharacterSetQueryException' => 'aphront/storage/exception/AphrontCharacterSetQueryException.php', 'AphrontConnectionLostQueryException' => 'aphront/storage/exception/AphrontConnectionLostQueryException.php', 'AphrontConnectionQueryException' => 'aphront/storage/exception/AphrontConnectionQueryException.php', 'AphrontCountQueryException' => 'aphront/storage/exception/AphrontCountQueryException.php', 'AphrontDatabaseConnection' => 'aphront/storage/connection/AphrontDatabaseConnection.php', 'AphrontDatabaseTransactionState' => 'aphront/storage/connection/AphrontDatabaseTransactionState.php', 'AphrontDeadlockQueryException' => 'aphront/storage/exception/AphrontDeadlockQueryException.php', 'AphrontDuplicateKeyQueryException' => 'aphront/storage/exception/AphrontDuplicateKeyQueryException.php', 'AphrontInvalidCredentialsQueryException' => 'aphront/storage/exception/AphrontInvalidCredentialsQueryException.php', 'AphrontIsolatedDatabaseConnection' => 'aphront/storage/connection/AphrontIsolatedDatabaseConnection.php', 'AphrontLockTimeoutQueryException' => 'aphront/storage/exception/AphrontLockTimeoutQueryException.php', 'AphrontMySQLDatabaseConnection' => 'aphront/storage/connection/mysql/AphrontMySQLDatabaseConnection.php', 'AphrontMySQLiDatabaseConnection' => 'aphront/storage/connection/mysql/AphrontMySQLiDatabaseConnection.php', 'AphrontNotSupportedQueryException' => 'aphront/storage/exception/AphrontNotSupportedQueryException.php', 'AphrontObjectMissingQueryException' => 'aphront/storage/exception/AphrontObjectMissingQueryException.php', 'AphrontParameterQueryException' => 'aphront/storage/exception/AphrontParameterQueryException.php', 'AphrontQueryException' => 'aphront/storage/exception/AphrontQueryException.php', 'AphrontQueryTimeoutQueryException' => 'aphront/storage/exception/AphrontQueryTimeoutQueryException.php', 'AphrontRecoverableQueryException' => 'aphront/storage/exception/AphrontRecoverableQueryException.php', 'AphrontRequestStream' => 'aphront/requeststream/AphrontRequestStream.php', 'AphrontSchemaQueryException' => 'aphront/storage/exception/AphrontSchemaQueryException.php', 'AphrontScopedUnguardedWriteCapability' => 'aphront/writeguard/AphrontScopedUnguardedWriteCapability.php', 'AphrontWriteGuard' => 'aphront/writeguard/AphrontWriteGuard.php', 'BaseHTTPFuture' => 'future/http/BaseHTTPFuture.php', 'CaseInsensitiveArray' => 'utils/CaseInsensitiveArray.php', 'CaseInsensitiveArrayTestCase' => 'utils/__tests__/CaseInsensitiveArrayTestCase.php', 'CommandException' => 'future/exec/CommandException.php', 'ConduitClient' => 'conduit/ConduitClient.php', 'ConduitClientException' => 'conduit/ConduitClientException.php', 'ConduitClientTestCase' => 'conduit/__tests__/ConduitClientTestCase.php', 'ConduitFuture' => 'conduit/ConduitFuture.php', 'ExecFuture' => 'future/exec/ExecFuture.php', 'ExecFutureTestCase' => 'future/exec/__tests__/ExecFutureTestCase.php', 'ExecPassthruTestCase' => 'future/exec/__tests__/ExecPassthruTestCase.php', 'FileFinder' => 'filesystem/FileFinder.php', 'FileFinderTestCase' => 'filesystem/__tests__/FileFinderTestCase.php', 'FileList' => 'filesystem/FileList.php', 'Filesystem' => 'filesystem/Filesystem.php', 'FilesystemException' => 'filesystem/FilesystemException.php', 'FilesystemTestCase' => 'filesystem/__tests__/FilesystemTestCase.php', 'Future' => 'future/Future.php', 'FutureIterator' => 'future/FutureIterator.php', 'FutureIteratorTestCase' => 'future/__tests__/FutureIteratorTestCase.php', 'FutureProxy' => 'future/FutureProxy.php', 'HTTPFuture' => 'future/http/HTTPFuture.php', 'HTTPFutureCURLResponseStatus' => 'future/http/status/HTTPFutureCURLResponseStatus.php', 'HTTPFutureCertificateResponseStatus' => 'future/http/status/HTTPFutureCertificateResponseStatus.php', 'HTTPFutureHTTPResponseStatus' => 'future/http/status/HTTPFutureHTTPResponseStatus.php', 'HTTPFutureParseResponseStatus' => 'future/http/status/HTTPFutureParseResponseStatus.php', 'HTTPFutureResponseStatus' => 'future/http/status/HTTPFutureResponseStatus.php', 'HTTPFutureTransportResponseStatus' => 'future/http/status/HTTPFutureTransportResponseStatus.php', 'HTTPSFuture' => 'future/http/HTTPSFuture.php', 'ImmediateFuture' => 'future/ImmediateFuture.php', 'LibphutilUSEnglishTranslation' => 'internationalization/translation/LibphutilUSEnglishTranslation.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', 'PHPASTParserTestCase' => 'parser/xhpast/__tests__/PHPASTParserTestCase.php', 'PhageAction' => 'phage/action/PhageAction.php', 'PhageAgentAction' => 'phage/action/PhageAgentAction.php', 'PhageAgentBootloader' => 'phage/bootloader/PhageAgentBootloader.php', 'PhageAgentTestCase' => 'phage/__tests__/PhageAgentTestCase.php', 'PhageExecuteAction' => 'phage/action/PhageExecuteAction.php', 'PhageLocalAction' => 'phage/action/PhageLocalAction.php', 'PhagePHPAgent' => 'phage/agent/PhagePHPAgent.php', 'PhagePHPAgentBootloader' => 'phage/bootloader/PhagePHPAgentBootloader.php', 'PhagePlanAction' => 'phage/action/PhagePlanAction.php', 'Phobject' => 'object/Phobject.php', 'PhobjectTestCase' => 'object/__tests__/PhobjectTestCase.php', 'PhutilAPCKeyValueCache' => 'cache/PhutilAPCKeyValueCache.php', 'PhutilAWSEC2Future' => 'future/aws/PhutilAWSEC2Future.php', 'PhutilAWSException' => 'future/aws/PhutilAWSException.php', 'PhutilAWSFuture' => 'future/aws/PhutilAWSFuture.php', 'PhutilAWSManagementWorkflow' => 'future/aws/management/PhutilAWSManagementWorkflow.php', 'PhutilAWSS3DeleteManagementWorkflow' => 'future/aws/management/PhutilAWSS3DeleteManagementWorkflow.php', 'PhutilAWSS3Future' => 'future/aws/PhutilAWSS3Future.php', 'PhutilAWSS3GetManagementWorkflow' => 'future/aws/management/PhutilAWSS3GetManagementWorkflow.php', 'PhutilAWSS3ManagementWorkflow' => 'future/aws/management/PhutilAWSS3ManagementWorkflow.php', 'PhutilAWSS3PutManagementWorkflow' => 'future/aws/management/PhutilAWSS3PutManagementWorkflow.php', 'PhutilAWSv4Signature' => 'future/aws/PhutilAWSv4Signature.php', 'PhutilAWSv4SignatureTestCase' => 'future/aws/__tests__/PhutilAWSv4SignatureTestCase.php', 'PhutilAggregateException' => 'error/PhutilAggregateException.php', 'PhutilAllCapsEnglishLocale' => 'internationalization/locales/PhutilAllCapsEnglishLocale.php', 'PhutilAmazonAuthAdapter' => 'auth/PhutilAmazonAuthAdapter.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', 'PhutilArgumentSpellingCorrector' => 'parser/argument/PhutilArgumentSpellingCorrector.php', 'PhutilArgumentSpellingCorrectorTestCase' => 'parser/argument/__tests__/PhutilArgumentSpellingCorrectorTestCase.php', 'PhutilArgumentUsageException' => 'parser/argument/exception/PhutilArgumentUsageException.php', 'PhutilArgumentWorkflow' => 'parser/argument/workflow/PhutilArgumentWorkflow.php', 'PhutilArray' => 'utils/PhutilArray.php', 'PhutilArrayTestCase' => 'utils/__tests__/PhutilArrayTestCase.php', 'PhutilArrayWithDefaultValue' => 'utils/PhutilArrayWithDefaultValue.php', 'PhutilAsanaAuthAdapter' => 'auth/PhutilAsanaAuthAdapter.php', 'PhutilAsanaFuture' => 'future/asana/PhutilAsanaFuture.php', 'PhutilAuthAdapter' => 'auth/PhutilAuthAdapter.php', 'PhutilAuthConfigurationException' => 'auth/exception/PhutilAuthConfigurationException.php', 'PhutilAuthCredentialException' => 'auth/exception/PhutilAuthCredentialException.php', 'PhutilAuthException' => 'auth/exception/PhutilAuthException.php', 'PhutilAuthUserAbortedException' => 'auth/exception/PhutilAuthUserAbortedException.php', 'PhutilBacktraceSignalHandler' => 'future/exec/PhutilBacktraceSignalHandler.php', 'PhutilBallOfPHP' => 'phage/util/PhutilBallOfPHP.php', 'PhutilBitbucketAuthAdapter' => 'auth/PhutilBitbucketAuthAdapter.php', 'PhutilBootloader' => 'moduleutils/PhutilBootloader.php', 'PhutilBootloaderException' => 'moduleutils/PhutilBootloaderException.php', 'PhutilBritishEnglishLocale' => 'internationalization/locales/PhutilBritishEnglishLocale.php', 'PhutilBufferedIterator' => 'utils/PhutilBufferedIterator.php', 'PhutilBufferedIteratorTestCase' => 'utils/__tests__/PhutilBufferedIteratorTestCase.php', 'PhutilBugtraqParser' => 'parser/PhutilBugtraqParser.php', 'PhutilBugtraqParserTestCase' => 'parser/__tests__/PhutilBugtraqParserTestCase.php', 'PhutilCIDRBlock' => 'ip/PhutilCIDRBlock.php', 'PhutilCIDRList' => 'ip/PhutilCIDRList.php', 'PhutilCLikeCodeSnippetContextFreeGrammar' => 'grammar/code/PhutilCLikeCodeSnippetContextFreeGrammar.php', 'PhutilCalendarAbsoluteDateTime' => 'parser/calendar/data/PhutilCalendarAbsoluteDateTime.php', 'PhutilCalendarContainerNode' => 'parser/calendar/data/PhutilCalendarContainerNode.php', 'PhutilCalendarDateTime' => 'parser/calendar/data/PhutilCalendarDateTime.php', 'PhutilCalendarDateTimeTestCase' => 'parser/calendar/data/__tests__/PhutilCalendarDateTimeTestCase.php', 'PhutilCalendarDocumentNode' => 'parser/calendar/data/PhutilCalendarDocumentNode.php', 'PhutilCalendarDuration' => 'parser/calendar/data/PhutilCalendarDuration.php', 'PhutilCalendarEventNode' => 'parser/calendar/data/PhutilCalendarEventNode.php', 'PhutilCalendarNode' => 'parser/calendar/data/PhutilCalendarNode.php', 'PhutilCalendarProxyDateTime' => 'parser/calendar/data/PhutilCalendarProxyDateTime.php', 'PhutilCalendarRawNode' => 'parser/calendar/data/PhutilCalendarRawNode.php', 'PhutilCalendarRecurrenceList' => 'parser/calendar/data/PhutilCalendarRecurrenceList.php', 'PhutilCalendarRecurrenceRule' => 'parser/calendar/data/PhutilCalendarRecurrenceRule.php', 'PhutilCalendarRecurrenceRuleTestCase' => 'parser/calendar/data/__tests__/PhutilCalendarRecurrenceRuleTestCase.php', 'PhutilCalendarRecurrenceSet' => 'parser/calendar/data/PhutilCalendarRecurrenceSet.php', 'PhutilCalendarRecurrenceSource' => 'parser/calendar/data/PhutilCalendarRecurrenceSource.php', 'PhutilCalendarRecurrenceTestCase' => 'parser/calendar/data/__tests__/PhutilCalendarRecurrenceTestCase.php', 'PhutilCalendarRelativeDateTime' => 'parser/calendar/data/PhutilCalendarRelativeDateTime.php', 'PhutilCalendarRootNode' => 'parser/calendar/data/PhutilCalendarRootNode.php', 'PhutilCalendarUserNode' => 'parser/calendar/data/PhutilCalendarUserNode.php', 'PhutilCallbackFilterIterator' => 'utils/PhutilCallbackFilterIterator.php', 'PhutilCallbackSignalHandler' => 'future/exec/PhutilCallbackSignalHandler.php', 'PhutilChannel' => 'channel/PhutilChannel.php', 'PhutilChannelChannel' => 'channel/PhutilChannelChannel.php', 'PhutilChannelTestCase' => 'channel/__tests__/PhutilChannelTestCase.php', 'PhutilChunkedIterator' => 'utils/PhutilChunkedIterator.php', 'PhutilChunkedIteratorTestCase' => 'utils/__tests__/PhutilChunkedIteratorTestCase.php', 'PhutilClassMapQuery' => 'symbols/PhutilClassMapQuery.php', 'PhutilCodeSnippetContextFreeGrammar' => 'grammar/code/PhutilCodeSnippetContextFreeGrammar.php', 'PhutilCommandString' => 'xsprintf/PhutilCommandString.php', 'PhutilConsole' => 'console/PhutilConsole.php', 'PhutilConsoleBlock' => 'console/view/PhutilConsoleBlock.php', 'PhutilConsoleFormatter' => 'console/PhutilConsoleFormatter.php', 'PhutilConsoleList' => 'console/view/PhutilConsoleList.php', 'PhutilConsoleMessage' => 'console/PhutilConsoleMessage.php', 'PhutilConsoleProgressBar' => 'console/PhutilConsoleProgressBar.php', 'PhutilConsoleServer' => 'console/PhutilConsoleServer.php', 'PhutilConsoleServerChannel' => 'console/PhutilConsoleServerChannel.php', 'PhutilConsoleStdinNotInteractiveException' => 'console/PhutilConsoleStdinNotInteractiveException.php', 'PhutilConsoleSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilConsoleSyntaxHighlighter.php', 'PhutilConsoleTable' => 'console/view/PhutilConsoleTable.php', 'PhutilConsoleView' => 'console/view/PhutilConsoleView.php', 'PhutilConsoleWrapTestCase' => 'console/__tests__/PhutilConsoleWrapTestCase.php', 'PhutilContextFreeGrammar' => 'grammar/PhutilContextFreeGrammar.php', 'PhutilCowsay' => 'utils/PhutilCowsay.php', 'PhutilCowsayTestCase' => 'utils/__tests__/PhutilCowsayTestCase.php', 'PhutilCsprintfTestCase' => 'xsprintf/__tests__/PhutilCsprintfTestCase.php', 'PhutilCzechLocale' => 'internationalization/locales/PhutilCzechLocale.php', 'PhutilDaemon' => 'daemon/PhutilDaemon.php', 'PhutilDaemonHandle' => 'daemon/PhutilDaemonHandle.php', 'PhutilDaemonOverseer' => 'daemon/PhutilDaemonOverseer.php', 'PhutilDaemonOverseerModule' => 'daemon/PhutilDaemonOverseerModule.php', + 'PhutilDaemonPool' => 'daemon/PhutilDaemonPool.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', 'PhutilDirectedScalarGraph' => 'utils/PhutilDirectedScalarGraph.php', 'PhutilDirectoryFixture' => 'filesystem/PhutilDirectoryFixture.php', 'PhutilDirectoryKeyValueCache' => 'cache/PhutilDirectoryKeyValueCache.php', 'PhutilDisqusAuthAdapter' => 'auth/PhutilDisqusAuthAdapter.php', 'PhutilDivinerSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilDivinerSyntaxHighlighter.php', 'PhutilDocblockParser' => 'parser/PhutilDocblockParser.php', 'PhutilDocblockParserTestCase' => 'parser/__tests__/PhutilDocblockParserTestCase.php', 'PhutilEditDistanceMatrix' => 'utils/PhutilEditDistanceMatrix.php', 'PhutilEditDistanceMatrixTestCase' => 'utils/__tests__/PhutilEditDistanceMatrixTestCase.php', 'PhutilEditorConfig' => 'parser/PhutilEditorConfig.php', 'PhutilEditorConfigTestCase' => 'parser/__tests__/PhutilEditorConfigTestCase.php', 'PhutilEmailAddress' => 'parser/PhutilEmailAddress.php', 'PhutilEmailAddressTestCase' => 'parser/__tests__/PhutilEmailAddressTestCase.php', 'PhutilEmojiLocale' => 'internationalization/locales/PhutilEmojiLocale.php', 'PhutilEmptyAuthAdapter' => 'auth/PhutilEmptyAuthAdapter.php', 'PhutilEnglishCanadaLocale' => 'internationalization/locales/PhutilEnglishCanadaLocale.php', 'PhutilErrorHandler' => 'error/PhutilErrorHandler.php', 'PhutilErrorHandlerTestCase' => 'error/__tests__/PhutilErrorHandlerTestCase.php', 'PhutilErrorTrap' => 'error/PhutilErrorTrap.php', 'PhutilEvent' => 'events/PhutilEvent.php', 'PhutilEventConstants' => 'events/constant/PhutilEventConstants.php', 'PhutilEventEngine' => 'events/PhutilEventEngine.php', 'PhutilEventListener' => 'events/PhutilEventListener.php', 'PhutilEventType' => 'events/constant/PhutilEventType.php', 'PhutilExampleBufferedIterator' => 'utils/PhutilExampleBufferedIterator.php', 'PhutilExcessiveServiceCallsDaemon' => 'daemon/torture/PhutilExcessiveServiceCallsDaemon.php', 'PhutilExecChannel' => 'channel/PhutilExecChannel.php', 'PhutilExecPassthru' => 'future/exec/PhutilExecPassthru.php', 'PhutilExecutableFuture' => 'future/exec/PhutilExecutableFuture.php', 'PhutilExecutionEnvironment' => 'utils/PhutilExecutionEnvironment.php', 'PhutilExtensionsTestCase' => 'moduleutils/__tests__/PhutilExtensionsTestCase.php', 'PhutilFacebookAuthAdapter' => 'auth/PhutilFacebookAuthAdapter.php', 'PhutilFatalDaemon' => 'daemon/torture/PhutilFatalDaemon.php', 'PhutilFileLock' => 'filesystem/PhutilFileLock.php', 'PhutilFileLockTestCase' => 'filesystem/__tests__/PhutilFileLockTestCase.php', 'PhutilFileTree' => 'filesystem/PhutilFileTree.php', 'PhutilFrenchLocale' => 'internationalization/locales/PhutilFrenchLocale.php', 'PhutilGermanLocale' => 'internationalization/locales/PhutilGermanLocale.php', 'PhutilGitHubAuthAdapter' => 'auth/PhutilGitHubAuthAdapter.php', 'PhutilGitHubFuture' => 'future/github/PhutilGitHubFuture.php', 'PhutilGitHubResponse' => 'future/github/PhutilGitHubResponse.php', 'PhutilGitURI' => 'parser/PhutilGitURI.php', 'PhutilGitURITestCase' => 'parser/__tests__/PhutilGitURITestCase.php', 'PhutilGoogleAuthAdapter' => 'auth/PhutilGoogleAuthAdapter.php', 'PhutilHTTPEngineExtension' => 'future/http/PhutilHTTPEngineExtension.php', 'PhutilHangForeverDaemon' => 'daemon/torture/PhutilHangForeverDaemon.php', 'PhutilHashingIterator' => 'utils/PhutilHashingIterator.php', 'PhutilHashingIteratorTestCase' => 'utils/__tests__/PhutilHashingIteratorTestCase.php', 'PhutilHelpArgumentWorkflow' => 'parser/argument/workflow/PhutilHelpArgumentWorkflow.php', 'PhutilHgsprintfTestCase' => 'xsprintf/__tests__/PhutilHgsprintfTestCase.php', 'PhutilHighIntensityIntervalDaemon' => 'daemon/torture/PhutilHighIntensityIntervalDaemon.php', 'PhutilICSParser' => 'parser/calendar/ics/PhutilICSParser.php', 'PhutilICSParserException' => 'parser/calendar/ics/PhutilICSParserException.php', 'PhutilICSParserTestCase' => 'parser/calendar/ics/__tests__/PhutilICSParserTestCase.php', 'PhutilICSWriter' => 'parser/calendar/ics/PhutilICSWriter.php', 'PhutilICSWriterTestCase' => 'parser/calendar/ics/__tests__/PhutilICSWriterTestCase.php', 'PhutilINIParserException' => 'parser/exception/PhutilINIParserException.php', 'PhutilIPAddress' => 'ip/PhutilIPAddress.php', 'PhutilIPAddressTestCase' => 'ip/__tests__/PhutilIPAddressTestCase.php', 'PhutilIPv4Address' => 'ip/PhutilIPv4Address.php', 'PhutilIPv6Address' => 'ip/PhutilIPv6Address.php', 'PhutilInRequestKeyValueCache' => 'cache/PhutilInRequestKeyValueCache.php', 'PhutilInteractiveEditor' => 'console/PhutilInteractiveEditor.php', 'PhutilInvalidRuleParserGeneratorException' => 'parser/generator/exception/PhutilInvalidRuleParserGeneratorException.php', 'PhutilInvalidStateException' => 'exception/PhutilInvalidStateException.php', 'PhutilInvalidStateExceptionTestCase' => 'exception/__tests__/PhutilInvalidStateExceptionTestCase.php', 'PhutilInvisibleSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilInvisibleSyntaxHighlighter.php', 'PhutilIrreducibleRuleParserGeneratorException' => 'parser/generator/exception/PhutilIrreducibleRuleParserGeneratorException.php', 'PhutilJIRAAuthAdapter' => 'auth/PhutilJIRAAuthAdapter.php', 'PhutilJSON' => 'parser/PhutilJSON.php', 'PhutilJSONFragmentLexer' => 'lexer/PhutilJSONFragmentLexer.php', 'PhutilJSONFragmentLexerHighlighterTestCase' => 'markup/syntax/highlighter/__tests__/PhutilJSONFragmentLexerHighlighterTestCase.php', 'PhutilJSONParser' => 'parser/PhutilJSONParser.php', 'PhutilJSONParserException' => 'parser/exception/PhutilJSONParserException.php', 'PhutilJSONParserTestCase' => 'parser/__tests__/PhutilJSONParserTestCase.php', 'PhutilJSONProtocolChannel' => 'channel/PhutilJSONProtocolChannel.php', 'PhutilJSONProtocolChannelTestCase' => 'channel/__tests__/PhutilJSONProtocolChannelTestCase.php', 'PhutilJSONTestCase' => 'parser/__tests__/PhutilJSONTestCase.php', 'PhutilJavaCodeSnippetContextFreeGrammar' => 'grammar/code/PhutilJavaCodeSnippetContextFreeGrammar.php', 'PhutilKeyValueCache' => 'cache/PhutilKeyValueCache.php', 'PhutilKeyValueCacheNamespace' => 'cache/PhutilKeyValueCacheNamespace.php', 'PhutilKeyValueCacheProfiler' => 'cache/PhutilKeyValueCacheProfiler.php', 'PhutilKeyValueCacheProxy' => 'cache/PhutilKeyValueCacheProxy.php', 'PhutilKeyValueCacheStack' => 'cache/PhutilKeyValueCacheStack.php', 'PhutilKeyValueCacheTestCase' => 'cache/__tests__/PhutilKeyValueCacheTestCase.php', 'PhutilKoreanLocale' => 'internationalization/locales/PhutilKoreanLocale.php', 'PhutilLDAPAuthAdapter' => 'auth/PhutilLDAPAuthAdapter.php', 'PhutilLanguageGuesser' => 'parser/PhutilLanguageGuesser.php', 'PhutilLanguageGuesserTestCase' => 'parser/__tests__/PhutilLanguageGuesserTestCase.php', 'PhutilLexer' => 'lexer/PhutilLexer.php', 'PhutilLexerSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilLexerSyntaxHighlighter.php', 'PhutilLibraryConflictException' => 'moduleutils/PhutilLibraryConflictException.php', 'PhutilLibraryMapBuilder' => 'moduleutils/PhutilLibraryMapBuilder.php', 'PhutilLibraryTestCase' => '__tests__/PhutilLibraryTestCase.php', 'PhutilLipsumContextFreeGrammar' => 'grammar/PhutilLipsumContextFreeGrammar.php', 'PhutilLocale' => 'internationalization/PhutilLocale.php', 'PhutilLocaleTestCase' => 'internationalization/__tests__/PhutilLocaleTestCase.php', 'PhutilLock' => 'filesystem/PhutilLock.php', 'PhutilLockException' => 'filesystem/PhutilLockException.php', 'PhutilLogFileChannel' => 'channel/PhutilLogFileChannel.php', 'PhutilLunarPhase' => 'utils/PhutilLunarPhase.php', 'PhutilLunarPhaseTestCase' => 'utils/__tests__/PhutilLunarPhaseTestCase.php', 'PhutilMarkupEngine' => 'markup/PhutilMarkupEngine.php', 'PhutilMarkupTestCase' => 'markup/__tests__/PhutilMarkupTestCase.php', 'PhutilMemcacheKeyValueCache' => 'cache/PhutilMemcacheKeyValueCache.php', 'PhutilMethodNotImplementedException' => 'error/PhutilMethodNotImplementedException.php', 'PhutilMetricsChannel' => 'channel/PhutilMetricsChannel.php', 'PhutilMissingSymbolException' => 'symbols/exception/PhutilMissingSymbolException.php', 'PhutilModuleUtilsTestCase' => 'moduleutils/__tests__/PhutilModuleUtilsTestCase.php', 'PhutilNiceDaemon' => 'daemon/torture/PhutilNiceDaemon.php', 'PhutilNumber' => 'internationalization/PhutilNumber.php', 'PhutilOAuth1AuthAdapter' => 'auth/PhutilOAuth1AuthAdapter.php', 'PhutilOAuth1Future' => 'future/oauth/PhutilOAuth1Future.php', 'PhutilOAuth1FutureTestCase' => 'future/oauth/__tests__/PhutilOAuth1FutureTestCase.php', 'PhutilOAuthAuthAdapter' => 'auth/PhutilOAuthAuthAdapter.php', 'PhutilOnDiskKeyValueCache' => 'cache/PhutilOnDiskKeyValueCache.php', 'PhutilOpaqueEnvelope' => 'error/PhutilOpaqueEnvelope.php', 'PhutilOpaqueEnvelopeKey' => 'error/PhutilOpaqueEnvelopeKey.php', 'PhutilOpaqueEnvelopeTestCase' => 'error/__tests__/PhutilOpaqueEnvelopeTestCase.php', 'PhutilPHPCodeSnippetContextFreeGrammar' => 'grammar/code/PhutilPHPCodeSnippetContextFreeGrammar.php', 'PhutilPHPFragmentLexer' => 'lexer/PhutilPHPFragmentLexer.php', 'PhutilPHPFragmentLexerHighlighterTestCase' => 'markup/syntax/highlighter/__tests__/PhutilPHPFragmentLexerHighlighterTestCase.php', 'PhutilPHPFragmentLexerTestCase' => 'lexer/__tests__/PhutilPHPFragmentLexerTestCase.php', 'PhutilPHPObjectProtocolChannel' => 'channel/PhutilPHPObjectProtocolChannel.php', 'PhutilPHPObjectProtocolChannelTestCase' => 'channel/__tests__/PhutilPHPObjectProtocolChannelTestCase.php', 'PhutilParserGenerator' => 'parser/PhutilParserGenerator.php', 'PhutilParserGeneratorException' => 'parser/generator/exception/PhutilParserGeneratorException.php', 'PhutilParserGeneratorTestCase' => 'parser/__tests__/PhutilParserGeneratorTestCase.php', 'PhutilPayPalAPIFuture' => 'future/paypal/PhutilPayPalAPIFuture.php', 'PhutilPerson' => 'internationalization/PhutilPerson.php', 'PhutilPersonTest' => 'internationalization/__tests__/PhutilPersonTest.php', 'PhutilPhabricatorAuthAdapter' => 'auth/PhutilPhabricatorAuthAdapter.php', 'PhutilPhtTestCase' => 'internationalization/__tests__/PhutilPhtTestCase.php', 'PhutilPirateEnglishLocale' => 'internationalization/locales/PhutilPirateEnglishLocale.php', 'PhutilPortugueseBrazilLocale' => 'internationalization/locales/PhutilPortugueseBrazilLocale.php', 'PhutilPortuguesePortugalLocale' => 'internationalization/locales/PhutilPortuguesePortugalLocale.php', 'PhutilPregsprintfTestCase' => 'xsprintf/__tests__/PhutilPregsprintfTestCase.php', 'PhutilProcessGroupDaemon' => 'daemon/torture/PhutilProcessGroupDaemon.php', 'PhutilProseDiff' => 'utils/PhutilProseDiff.php', 'PhutilProseDiffTestCase' => 'utils/__tests__/PhutilProseDiffTestCase.php', 'PhutilProseDifferenceEngine' => 'utils/PhutilProseDifferenceEngine.php', 'PhutilProtocolChannel' => 'channel/PhutilProtocolChannel.php', 'PhutilProxyException' => 'error/PhutilProxyException.php', 'PhutilProxyIterator' => 'utils/PhutilProxyIterator.php', 'PhutilPygmentizeParser' => 'parser/PhutilPygmentizeParser.php', 'PhutilPygmentizeParserTestCase' => 'parser/__tests__/PhutilPygmentizeParserTestCase.php', 'PhutilPygmentsSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilPygmentsSyntaxHighlighter.php', 'PhutilPythonFragmentLexer' => 'lexer/PhutilPythonFragmentLexer.php', 'PhutilQsprintfInterface' => 'xsprintf/PhutilQsprintfInterface.php', 'PhutilQueryStringParser' => 'parser/PhutilQueryStringParser.php', 'PhutilQueryStringParserTestCase' => 'parser/__tests__/PhutilQueryStringParserTestCase.php', 'PhutilRainbowSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilRainbowSyntaxHighlighter.php', 'PhutilRawEnglishLocale' => 'internationalization/locales/PhutilRawEnglishLocale.php', 'PhutilReadableSerializer' => 'readableserializer/PhutilReadableSerializer.php', 'PhutilReadableSerializerTestCase' => 'readableserializer/__tests__/PhutilReadableSerializerTestCase.php', 'PhutilRealNameContextFreeGrammar' => 'grammar/PhutilRealNameContextFreeGrammar.php', 'PhutilRemarkupBlockInterpreter' => 'markup/engine/remarkup/blockrule/PhutilRemarkupBlockInterpreter.php', 'PhutilRemarkupBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupBlockRule.php', 'PhutilRemarkupBlockStorage' => 'markup/engine/remarkup/PhutilRemarkupBlockStorage.php', 'PhutilRemarkupBoldRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupBoldRule.php', 'PhutilRemarkupCodeBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupCodeBlockRule.php', 'PhutilRemarkupDefaultBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupDefaultBlockRule.php', 'PhutilRemarkupDelRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupDelRule.php', 'PhutilRemarkupDocumentLinkRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupDocumentLinkRule.php', 'PhutilRemarkupEngine' => 'markup/engine/PhutilRemarkupEngine.php', 'PhutilRemarkupEngineTestCase' => 'markup/engine/__tests__/PhutilRemarkupEngineTestCase.php', 'PhutilRemarkupEscapeRemarkupRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupEscapeRemarkupRule.php', 'PhutilRemarkupHeaderBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupHeaderBlockRule.php', 'PhutilRemarkupHighlightRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupHighlightRule.php', 'PhutilRemarkupHorizontalRuleBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupHorizontalRuleBlockRule.php', 'PhutilRemarkupHyperlinkRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupHyperlinkRule.php', 'PhutilRemarkupInlineBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupInlineBlockRule.php', 'PhutilRemarkupInterpreterBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupInterpreterBlockRule.php', 'PhutilRemarkupItalicRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupItalicRule.php', 'PhutilRemarkupLinebreaksRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupLinebreaksRule.php', 'PhutilRemarkupListBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupListBlockRule.php', 'PhutilRemarkupLiteralBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupLiteralBlockRule.php', 'PhutilRemarkupMonospaceRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupMonospaceRule.php', 'PhutilRemarkupNoteBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupNoteBlockRule.php', 'PhutilRemarkupQuotesBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupQuotesBlockRule.php', 'PhutilRemarkupReplyBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupReplyBlockRule.php', 'PhutilRemarkupRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupRule.php', 'PhutilRemarkupSimpleTableBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupSimpleTableBlockRule.php', 'PhutilRemarkupTableBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupTableBlockRule.php', 'PhutilRemarkupTestInterpreterRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupTestInterpreterRule.php', 'PhutilRemarkupUnderlineRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupUnderlineRule.php', 'PhutilRope' => 'utils/PhutilRope.php', 'PhutilRopeTestCase' => 'utils/__tests__/PhutilRopeTestCase.php', 'PhutilSafeHTML' => 'markup/PhutilSafeHTML.php', 'PhutilSafeHTMLProducerInterface' => 'markup/PhutilSafeHTMLProducerInterface.php', 'PhutilSafeHTMLTestCase' => 'markup/__tests__/PhutilSafeHTMLTestCase.php', 'PhutilSaturateStdoutDaemon' => 'daemon/torture/PhutilSaturateStdoutDaemon.php', 'PhutilSearchQueryCompiler' => 'search/PhutilSearchQueryCompiler.php', 'PhutilSearchQueryCompilerSyntaxException' => 'search/PhutilSearchQueryCompilerSyntaxException.php', 'PhutilSearchQueryCompilerTestCase' => 'search/__tests__/PhutilSearchQueryCompilerTestCase.php', 'PhutilSearchStemmer' => 'search/PhutilSearchStemmer.php', 'PhutilSearchStemmerTestCase' => 'search/__tests__/PhutilSearchStemmerTestCase.php', 'PhutilServiceProfiler' => 'serviceprofiler/PhutilServiceProfiler.php', 'PhutilShellLexer' => 'lexer/PhutilShellLexer.php', 'PhutilShellLexerTestCase' => 'lexer/__tests__/PhutilShellLexerTestCase.php', 'PhutilSignalHandler' => 'future/exec/PhutilSignalHandler.php', 'PhutilSignalRouter' => 'future/exec/PhutilSignalRouter.php', 'PhutilSimpleOptions' => 'parser/PhutilSimpleOptions.php', 'PhutilSimpleOptionsLexer' => 'lexer/PhutilSimpleOptionsLexer.php', 'PhutilSimpleOptionsLexerTestCase' => 'lexer/__tests__/PhutilSimpleOptionsLexerTestCase.php', 'PhutilSimpleOptionsTestCase' => 'parser/__tests__/PhutilSimpleOptionsTestCase.php', 'PhutilSimplifiedChineseLocale' => 'internationalization/locales/PhutilSimplifiedChineseLocale.php', 'PhutilSlackAuthAdapter' => 'auth/PhutilSlackAuthAdapter.php', 'PhutilSlackFuture' => 'future/slack/PhutilSlackFuture.php', 'PhutilSocketChannel' => 'channel/PhutilSocketChannel.php', 'PhutilSortVector' => 'utils/PhutilSortVector.php', 'PhutilSpanishSpainLocale' => 'internationalization/locales/PhutilSpanishSpainLocale.php', 'PhutilSprite' => 'sprites/PhutilSprite.php', 'PhutilSpriteSheet' => 'sprites/PhutilSpriteSheet.php', 'PhutilStreamIterator' => 'utils/PhutilStreamIterator.php', 'PhutilSymbolLoader' => 'symbols/PhutilSymbolLoader.php', 'PhutilSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilSyntaxHighlighter.php', 'PhutilSyntaxHighlighterEngine' => 'markup/syntax/engine/PhutilSyntaxHighlighterEngine.php', 'PhutilSyntaxHighlighterException' => 'markup/syntax/highlighter/PhutilSyntaxHighlighterException.php', 'PhutilSystem' => 'utils/PhutilSystem.php', 'PhutilSystemTestCase' => 'utils/__tests__/PhutilSystemTestCase.php', 'PhutilTerminalString' => 'xsprintf/PhutilTerminalString.php', 'PhutilTestPhobject' => 'object/__tests__/PhutilTestPhobject.php', 'PhutilTortureTestDaemon' => 'daemon/torture/PhutilTortureTestDaemon.php', 'PhutilTraditionalChineseLocale' => 'internationalization/locales/PhutilTraditionalChineseLocale.php', 'PhutilTranslation' => 'internationalization/PhutilTranslation.php', 'PhutilTranslationTestCase' => 'internationalization/__tests__/PhutilTranslationTestCase.php', 'PhutilTranslator' => 'internationalization/PhutilTranslator.php', 'PhutilTranslatorTestCase' => 'internationalization/__tests__/PhutilTranslatorTestCase.php', 'PhutilTsprintfTestCase' => 'xsprintf/__tests__/PhutilTsprintfTestCase.php', 'PhutilTwitchAuthAdapter' => 'auth/PhutilTwitchAuthAdapter.php', 'PhutilTwitchFuture' => 'future/twitch/PhutilTwitchFuture.php', 'PhutilTwitterAuthAdapter' => 'auth/PhutilTwitterAuthAdapter.php', 'PhutilTypeCheckException' => 'parser/exception/PhutilTypeCheckException.php', 'PhutilTypeExtraParametersException' => 'parser/exception/PhutilTypeExtraParametersException.php', 'PhutilTypeLexer' => 'lexer/PhutilTypeLexer.php', 'PhutilTypeMissingParametersException' => 'parser/exception/PhutilTypeMissingParametersException.php', 'PhutilTypeSpec' => 'parser/PhutilTypeSpec.php', 'PhutilTypeSpecTestCase' => 'parser/__tests__/PhutilTypeSpecTestCase.php', 'PhutilURI' => 'parser/PhutilURI.php', 'PhutilURITestCase' => 'parser/__tests__/PhutilURITestCase.php', 'PhutilUSEnglishLocale' => 'internationalization/locales/PhutilUSEnglishLocale.php', 'PhutilUTF8StringTruncator' => 'utils/PhutilUTF8StringTruncator.php', 'PhutilUTF8TestCase' => 'utils/__tests__/PhutilUTF8TestCase.php', 'PhutilUnknownSymbolParserGeneratorException' => 'parser/generator/exception/PhutilUnknownSymbolParserGeneratorException.php', 'PhutilUnreachableRuleParserGeneratorException' => 'parser/generator/exception/PhutilUnreachableRuleParserGeneratorException.php', 'PhutilUnreachableTerminalParserGeneratorException' => 'parser/generator/exception/PhutilUnreachableTerminalParserGeneratorException.php', 'PhutilUrisprintfTestCase' => 'xsprintf/__tests__/PhutilUrisprintfTestCase.php', 'PhutilUtilsTestCase' => 'utils/__tests__/PhutilUtilsTestCase.php', 'PhutilVeryWowEnglishLocale' => 'internationalization/locales/PhutilVeryWowEnglishLocale.php', 'PhutilWordPressAuthAdapter' => 'auth/PhutilWordPressAuthAdapter.php', 'PhutilWordPressFuture' => 'future/wordpress/PhutilWordPressFuture.php', 'PhutilXHPASTBinary' => 'parser/xhpast/bin/PhutilXHPASTBinary.php', 'PhutilXHPASTSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilXHPASTSyntaxHighlighter.php', 'PhutilXHPASTSyntaxHighlighterFuture' => 'markup/syntax/highlighter/xhpast/PhutilXHPASTSyntaxHighlighterFuture.php', 'PhutilXHPASTSyntaxHighlighterTestCase' => 'markup/syntax/highlighter/__tests__/PhutilXHPASTSyntaxHighlighterTestCase.php', 'QueryFuture' => 'future/query/QueryFuture.php', 'TempFile' => 'filesystem/TempFile.php', 'TestAbstractDirectedGraph' => 'utils/__tests__/TestAbstractDirectedGraph.php', 'XHPASTNode' => 'parser/xhpast/api/XHPASTNode.php', 'XHPASTNodeTestCase' => 'parser/xhpast/api/__tests__/XHPASTNodeTestCase.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', 'XsprintfUnknownConversionException' => 'xsprintf/exception/XsprintfUnknownConversionException.php', ), 'function' => array( 'array_fuse' => 'utils/utils.php', 'array_interleave' => 'utils/utils.php', 'array_mergev' => 'utils/utils.php', 'array_select_keys' => 'utils/utils.php', 'assert_instances_of' => 'utils/utils.php', 'assert_stringlike' => 'utils/utils.php', 'coalesce' => 'utils/utils.php', 'csprintf' => 'xsprintf/csprintf.php', 'exec_manual' => 'future/exec/execx.php', 'execx' => 'future/exec/execx.php', 'head' => 'utils/utils.php', 'head_key' => 'utils/utils.php', 'hgsprintf' => 'xsprintf/hgsprintf.php', 'hsprintf' => 'markup/render.php', 'id' => 'utils/utils.php', 'idx' => 'utils/utils.php', 'idxv' => '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', 'ldap_sprintf' => 'xsprintf/ldapsprintf.php', 'mfilter' => 'utils/utils.php', 'mgroup' => 'utils/utils.php', 'mpull' => 'utils/utils.php', 'msort' => 'utils/utils.php', 'msortv' => 'utils/utils.php', 'newv' => 'utils/utils.php', 'nonempty' => 'utils/utils.php', 'phlog' => 'error/phlog.php', 'pht' => 'internationalization/pht.php', 'phutil_censor_credentials' => 'utils/utils.php', 'phutil_console_confirm' => 'console/format.php', 'phutil_console_format' => 'console/format.php', 'phutil_console_get_terminal_width' => 'console/format.php', 'phutil_console_prompt' => 'console/format.php', 'phutil_console_require_tty' => 'console/format.php', 'phutil_console_select' => 'console/format.php', 'phutil_console_wrap' => 'console/format.php', 'phutil_count' => 'internationalization/pht.php', 'phutil_date_format' => 'utils/viewutils.php', 'phutil_deprecated' => 'moduleutils/moduleutils.php', 'phutil_error_listener_example' => 'error/phlog.php', 'phutil_escape_html' => 'markup/render.php', 'phutil_escape_html_newlines' => 'markup/render.php', 'phutil_escape_uri' => 'markup/render.php', 'phutil_escape_uri_path_component' => 'markup/render.php', 'phutil_fnmatch' => 'utils/utils.php', 'phutil_format_bytes' => 'utils/viewutils.php', 'phutil_format_relative_time' => 'utils/viewutils.php', 'phutil_format_relative_time_detailed' => 'utils/viewutils.php', 'phutil_format_units_generic' => 'utils/viewutils.php', 'phutil_fwrite_nonblocking_stream' => 'utils/utils.php', 'phutil_get_current_library_name' => 'moduleutils/moduleutils.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_get_signal_name' => 'future/exec/execx.php', 'phutil_hashes_are_identical' => 'utils/utils.php', 'phutil_implode_html' => 'markup/render.php', 'phutil_ini_decode' => 'utils/utils.php', 'phutil_is_hiphop_runtime' => 'utils/utils.php', 'phutil_is_utf8' => 'utils/utf8.php', 'phutil_is_utf8_slowly' => 'utils/utf8.php', 'phutil_is_utf8_with_only_bmp_characters' => 'utils/utf8.php', 'phutil_is_windows' => 'utils/utils.php', 'phutil_json_decode' => 'utils/utils.php', 'phutil_json_encode' => 'utils/utils.php', 'phutil_load_library' => 'moduleutils/core.php', 'phutil_loggable_string' => 'utils/utils.php', 'phutil_parse_bytes' => 'utils/viewutils.php', 'phutil_passthru' => 'future/exec/execx.php', 'phutil_person' => 'internationalization/pht.php', 'phutil_register_library' => 'moduleutils/core.php', 'phutil_register_library_map' => 'moduleutils/core.php', '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_units' => 'utils/utils.php', 'phutil_utf8_console_strlen' => 'utils/utf8.php', 'phutil_utf8_convert' => 'utils/utf8.php', 'phutil_utf8_encode_codepoint' => 'utils/utf8.php', 'phutil_utf8_hard_wrap' => 'utils/utf8.php', 'phutil_utf8_hard_wrap_html' => 'utils/utf8.php', 'phutil_utf8_is_combining_character' => 'utils/utf8.php', 'phutil_utf8_strlen' => 'utils/utf8.php', 'phutil_utf8_strtolower' => 'utils/utf8.php', 'phutil_utf8_strtoupper' => 'utils/utf8.php', 'phutil_utf8_strtr' => 'utils/utf8.php', 'phutil_utf8_ucwords' => 'utils/utf8.php', 'phutil_utf8ize' => 'utils/utf8.php', 'phutil_utf8v' => 'utils/utf8.php', 'phutil_utf8v_codepoints' => 'utils/utf8.php', 'phutil_utf8v_combine_characters' => 'utils/utf8.php', 'phutil_utf8v_combined' => 'utils/utf8.php', 'phutil_validate_json' => 'utils/utils.php', 'phutil_var_export' => 'utils/utils.php', 'ppull' => 'utils/utils.php', 'pregsprintf' => 'xsprintf/pregsprintf.php', 'qsprintf' => 'xsprintf/qsprintf.php', 'qsprintf_check_scalar_type' => 'xsprintf/qsprintf.php', 'qsprintf_check_type' => 'xsprintf/qsprintf.php', 'queryfx' => 'xsprintf/queryfx.php', 'queryfx_all' => 'xsprintf/queryfx.php', 'queryfx_one' => 'xsprintf/queryfx.php', 'tsprintf' => 'xsprintf/tsprintf.php', 'urisprintf' => 'xsprintf/urisprintf.php', 'vcsprintf' => 'xsprintf/csprintf.php', 'vjsprintf' => 'xsprintf/jsprintf.php', 'vqsprintf' => 'xsprintf/qsprintf.php', 'vurisprintf' => 'xsprintf/urisprintf.php', 'xhp_parser_node_constants' => 'parser/xhpast/parser_nodes.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', 'xsprintf_ldap' => 'xsprintf/ldapsprintf.php', 'xsprintf_mercurial' => 'xsprintf/hgsprintf.php', 'xsprintf_query' => 'xsprintf/qsprintf.php', 'xsprintf_regex' => 'xsprintf/pregsprintf.php', 'xsprintf_terminal' => 'xsprintf/tsprintf.php', 'xsprintf_uri' => 'xsprintf/urisprintf.php', ), 'xmap' => array( 'AASTNode' => 'Phobject', 'AASTNodeList' => array( 'Phobject', 'Countable', 'Iterator', ), 'AASTToken' => 'Phobject', 'AASTTree' => 'Phobject', 'AbstractDirectedGraph' => 'Phobject', 'AbstractDirectedGraphTestCase' => 'PhutilTestCase', 'AphrontAccessDeniedQueryException' => 'AphrontQueryException', 'AphrontBaseMySQLDatabaseConnection' => 'AphrontDatabaseConnection', 'AphrontCharacterSetQueryException' => 'AphrontQueryException', 'AphrontConnectionLostQueryException' => 'AphrontRecoverableQueryException', 'AphrontConnectionQueryException' => 'AphrontQueryException', 'AphrontCountQueryException' => 'AphrontQueryException', 'AphrontDatabaseConnection' => array( 'Phobject', 'PhutilQsprintfInterface', ), 'AphrontDatabaseTransactionState' => 'Phobject', 'AphrontDeadlockQueryException' => 'AphrontRecoverableQueryException', 'AphrontDuplicateKeyQueryException' => 'AphrontQueryException', 'AphrontInvalidCredentialsQueryException' => 'AphrontQueryException', 'AphrontIsolatedDatabaseConnection' => 'AphrontDatabaseConnection', 'AphrontLockTimeoutQueryException' => 'AphrontRecoverableQueryException', 'AphrontMySQLDatabaseConnection' => 'AphrontBaseMySQLDatabaseConnection', 'AphrontMySQLiDatabaseConnection' => 'AphrontBaseMySQLDatabaseConnection', 'AphrontNotSupportedQueryException' => 'AphrontQueryException', 'AphrontObjectMissingQueryException' => 'AphrontQueryException', 'AphrontParameterQueryException' => 'AphrontQueryException', 'AphrontQueryException' => 'Exception', 'AphrontQueryTimeoutQueryException' => 'AphrontRecoverableQueryException', 'AphrontRecoverableQueryException' => 'AphrontQueryException', 'AphrontRequestStream' => 'Phobject', 'AphrontSchemaQueryException' => 'AphrontQueryException', 'AphrontScopedUnguardedWriteCapability' => 'Phobject', 'AphrontWriteGuard' => 'Phobject', 'BaseHTTPFuture' => 'Future', 'CaseInsensitiveArray' => 'PhutilArray', 'CaseInsensitiveArrayTestCase' => 'PhutilTestCase', 'CommandException' => 'Exception', 'ConduitClient' => 'Phobject', 'ConduitClientException' => 'Exception', 'ConduitClientTestCase' => 'PhutilTestCase', 'ConduitFuture' => 'FutureProxy', 'ExecFuture' => 'PhutilExecutableFuture', 'ExecFutureTestCase' => 'PhutilTestCase', 'ExecPassthruTestCase' => 'PhutilTestCase', 'FileFinder' => 'Phobject', 'FileFinderTestCase' => 'PhutilTestCase', 'FileList' => 'Phobject', 'Filesystem' => 'Phobject', 'FilesystemException' => 'Exception', 'FilesystemTestCase' => 'PhutilTestCase', 'Future' => 'Phobject', 'FutureIterator' => array( 'Phobject', 'Iterator', ), 'FutureIteratorTestCase' => 'PhutilTestCase', 'FutureProxy' => 'Future', 'HTTPFuture' => 'BaseHTTPFuture', 'HTTPFutureCURLResponseStatus' => 'HTTPFutureResponseStatus', 'HTTPFutureCertificateResponseStatus' => 'HTTPFutureResponseStatus', 'HTTPFutureHTTPResponseStatus' => 'HTTPFutureResponseStatus', 'HTTPFutureParseResponseStatus' => 'HTTPFutureResponseStatus', 'HTTPFutureResponseStatus' => 'Exception', 'HTTPFutureTransportResponseStatus' => 'HTTPFutureResponseStatus', 'HTTPSFuture' => 'BaseHTTPFuture', 'ImmediateFuture' => 'Future', 'LibphutilUSEnglishTranslation' => 'PhutilTranslation', 'LinesOfALarge' => array( 'Phobject', 'Iterator', ), 'LinesOfALargeExecFuture' => 'LinesOfALarge', 'LinesOfALargeExecFutureTestCase' => 'PhutilTestCase', 'LinesOfALargeFile' => 'LinesOfALarge', 'LinesOfALargeFileTestCase' => 'PhutilTestCase', 'MFilterTestHelper' => 'Phobject', 'PHPASTParserTestCase' => 'PhutilTestCase', 'PhageAction' => 'Phobject', 'PhageAgentAction' => 'PhageAction', 'PhageAgentBootloader' => 'Phobject', 'PhageAgentTestCase' => 'PhutilTestCase', 'PhageExecuteAction' => 'PhageAction', 'PhageLocalAction' => 'PhageAgentAction', 'PhagePHPAgent' => 'Phobject', 'PhagePHPAgentBootloader' => 'PhageAgentBootloader', 'PhagePlanAction' => 'PhageAction', 'Phobject' => 'Iterator', 'PhobjectTestCase' => 'PhutilTestCase', 'PhutilAPCKeyValueCache' => 'PhutilKeyValueCache', 'PhutilAWSEC2Future' => 'PhutilAWSFuture', 'PhutilAWSException' => 'Exception', 'PhutilAWSFuture' => 'FutureProxy', 'PhutilAWSManagementWorkflow' => 'PhutilArgumentWorkflow', 'PhutilAWSS3DeleteManagementWorkflow' => 'PhutilAWSS3ManagementWorkflow', 'PhutilAWSS3Future' => 'PhutilAWSFuture', 'PhutilAWSS3GetManagementWorkflow' => 'PhutilAWSS3ManagementWorkflow', 'PhutilAWSS3ManagementWorkflow' => 'PhutilAWSManagementWorkflow', 'PhutilAWSS3PutManagementWorkflow' => 'PhutilAWSS3ManagementWorkflow', 'PhutilAWSv4Signature' => 'Phobject', 'PhutilAWSv4SignatureTestCase' => 'PhutilTestCase', 'PhutilAggregateException' => 'Exception', 'PhutilAllCapsEnglishLocale' => 'PhutilLocale', 'PhutilAmazonAuthAdapter' => 'PhutilOAuthAuthAdapter', 'PhutilArgumentParser' => 'Phobject', 'PhutilArgumentParserException' => 'Exception', 'PhutilArgumentParserTestCase' => 'PhutilTestCase', 'PhutilArgumentSpecification' => 'Phobject', 'PhutilArgumentSpecificationException' => 'PhutilArgumentParserException', 'PhutilArgumentSpecificationTestCase' => 'PhutilTestCase', 'PhutilArgumentSpellingCorrector' => 'Phobject', 'PhutilArgumentSpellingCorrectorTestCase' => 'PhutilTestCase', 'PhutilArgumentUsageException' => 'PhutilArgumentParserException', 'PhutilArgumentWorkflow' => 'Phobject', 'PhutilArray' => array( 'Phobject', 'Countable', 'ArrayAccess', 'Iterator', ), 'PhutilArrayTestCase' => 'PhutilTestCase', 'PhutilArrayWithDefaultValue' => 'PhutilArray', 'PhutilAsanaAuthAdapter' => 'PhutilOAuthAuthAdapter', 'PhutilAsanaFuture' => 'FutureProxy', 'PhutilAuthAdapter' => 'Phobject', 'PhutilAuthConfigurationException' => 'PhutilAuthException', 'PhutilAuthCredentialException' => 'PhutilAuthException', 'PhutilAuthException' => 'Exception', 'PhutilAuthUserAbortedException' => 'PhutilAuthException', 'PhutilBacktraceSignalHandler' => 'PhutilSignalHandler', 'PhutilBallOfPHP' => 'Phobject', 'PhutilBitbucketAuthAdapter' => 'PhutilOAuth1AuthAdapter', 'PhutilBootloaderException' => 'Exception', 'PhutilBritishEnglishLocale' => 'PhutilLocale', 'PhutilBufferedIterator' => array( 'Phobject', 'Iterator', ), 'PhutilBufferedIteratorTestCase' => 'PhutilTestCase', 'PhutilBugtraqParser' => 'Phobject', 'PhutilBugtraqParserTestCase' => 'PhutilTestCase', 'PhutilCIDRBlock' => 'Phobject', 'PhutilCIDRList' => 'Phobject', 'PhutilCLikeCodeSnippetContextFreeGrammar' => 'PhutilCodeSnippetContextFreeGrammar', 'PhutilCalendarAbsoluteDateTime' => 'PhutilCalendarDateTime', 'PhutilCalendarContainerNode' => 'PhutilCalendarNode', 'PhutilCalendarDateTime' => 'Phobject', 'PhutilCalendarDateTimeTestCase' => 'PhutilTestCase', 'PhutilCalendarDocumentNode' => 'PhutilCalendarContainerNode', 'PhutilCalendarDuration' => 'Phobject', 'PhutilCalendarEventNode' => 'PhutilCalendarContainerNode', 'PhutilCalendarNode' => 'Phobject', 'PhutilCalendarProxyDateTime' => 'PhutilCalendarDateTime', 'PhutilCalendarRawNode' => 'PhutilCalendarContainerNode', 'PhutilCalendarRecurrenceList' => 'PhutilCalendarRecurrenceSource', 'PhutilCalendarRecurrenceRule' => 'PhutilCalendarRecurrenceSource', 'PhutilCalendarRecurrenceRuleTestCase' => 'PhutilTestCase', 'PhutilCalendarRecurrenceSet' => 'Phobject', 'PhutilCalendarRecurrenceSource' => 'Phobject', 'PhutilCalendarRecurrenceTestCase' => 'PhutilTestCase', 'PhutilCalendarRelativeDateTime' => 'PhutilCalendarProxyDateTime', 'PhutilCalendarRootNode' => 'PhutilCalendarContainerNode', 'PhutilCalendarUserNode' => 'PhutilCalendarNode', 'PhutilCallbackFilterIterator' => 'FilterIterator', 'PhutilCallbackSignalHandler' => 'PhutilSignalHandler', 'PhutilChannel' => 'Phobject', 'PhutilChannelChannel' => 'PhutilChannel', 'PhutilChannelTestCase' => 'PhutilTestCase', 'PhutilChunkedIterator' => array( 'Phobject', 'Iterator', ), 'PhutilChunkedIteratorTestCase' => 'PhutilTestCase', 'PhutilClassMapQuery' => 'Phobject', 'PhutilCodeSnippetContextFreeGrammar' => 'PhutilContextFreeGrammar', 'PhutilCommandString' => 'Phobject', 'PhutilConsole' => 'Phobject', 'PhutilConsoleBlock' => 'PhutilConsoleView', 'PhutilConsoleFormatter' => 'Phobject', 'PhutilConsoleList' => 'PhutilConsoleView', 'PhutilConsoleMessage' => 'Phobject', 'PhutilConsoleProgressBar' => 'Phobject', 'PhutilConsoleServer' => 'Phobject', 'PhutilConsoleServerChannel' => 'PhutilChannelChannel', 'PhutilConsoleStdinNotInteractiveException' => 'Exception', 'PhutilConsoleSyntaxHighlighter' => 'Phobject', 'PhutilConsoleTable' => 'PhutilConsoleView', 'PhutilConsoleView' => 'Phobject', 'PhutilConsoleWrapTestCase' => 'PhutilTestCase', 'PhutilContextFreeGrammar' => 'Phobject', 'PhutilCowsay' => 'Phobject', 'PhutilCowsayTestCase' => 'PhutilTestCase', 'PhutilCsprintfTestCase' => 'PhutilTestCase', 'PhutilCzechLocale' => 'PhutilLocale', 'PhutilDaemon' => 'Phobject', 'PhutilDaemonHandle' => 'Phobject', 'PhutilDaemonOverseer' => 'Phobject', 'PhutilDaemonOverseerModule' => 'Phobject', + 'PhutilDaemonPool' => 'Phobject', 'PhutilDefaultSyntaxHighlighter' => 'Phobject', 'PhutilDefaultSyntaxHighlighterEngine' => 'PhutilSyntaxHighlighterEngine', 'PhutilDefaultSyntaxHighlighterEnginePygmentsFuture' => 'FutureProxy', 'PhutilDefaultSyntaxHighlighterEngineTestCase' => 'PhutilTestCase', 'PhutilDeferredLog' => 'Phobject', 'PhutilDeferredLogTestCase' => 'PhutilTestCase', 'PhutilDirectedScalarGraph' => 'AbstractDirectedGraph', 'PhutilDirectoryFixture' => 'Phobject', 'PhutilDirectoryKeyValueCache' => 'PhutilKeyValueCache', 'PhutilDisqusAuthAdapter' => 'PhutilOAuthAuthAdapter', 'PhutilDivinerSyntaxHighlighter' => 'Phobject', 'PhutilDocblockParser' => 'Phobject', 'PhutilDocblockParserTestCase' => 'PhutilTestCase', 'PhutilEditDistanceMatrix' => 'Phobject', 'PhutilEditDistanceMatrixTestCase' => 'PhutilTestCase', 'PhutilEditorConfig' => 'Phobject', 'PhutilEditorConfigTestCase' => 'PhutilTestCase', 'PhutilEmailAddress' => 'Phobject', 'PhutilEmailAddressTestCase' => 'PhutilTestCase', 'PhutilEmojiLocale' => 'PhutilLocale', 'PhutilEmptyAuthAdapter' => 'PhutilAuthAdapter', 'PhutilEnglishCanadaLocale' => 'PhutilLocale', 'PhutilErrorHandler' => 'Phobject', 'PhutilErrorHandlerTestCase' => 'PhutilTestCase', 'PhutilErrorTrap' => 'Phobject', 'PhutilEvent' => 'Phobject', 'PhutilEventConstants' => 'Phobject', 'PhutilEventEngine' => 'Phobject', 'PhutilEventListener' => 'Phobject', 'PhutilEventType' => 'PhutilEventConstants', 'PhutilExampleBufferedIterator' => 'PhutilBufferedIterator', 'PhutilExcessiveServiceCallsDaemon' => 'PhutilTortureTestDaemon', 'PhutilExecChannel' => 'PhutilChannel', 'PhutilExecPassthru' => 'PhutilExecutableFuture', 'PhutilExecutableFuture' => 'Future', 'PhutilExecutionEnvironment' => 'Phobject', 'PhutilExtensionsTestCase' => 'PhutilTestCase', 'PhutilFacebookAuthAdapter' => 'PhutilOAuthAuthAdapter', 'PhutilFatalDaemon' => 'PhutilTortureTestDaemon', 'PhutilFileLock' => 'PhutilLock', 'PhutilFileLockTestCase' => 'PhutilTestCase', 'PhutilFileTree' => 'Phobject', 'PhutilFrenchLocale' => 'PhutilLocale', 'PhutilGermanLocale' => 'PhutilLocale', 'PhutilGitHubAuthAdapter' => 'PhutilOAuthAuthAdapter', 'PhutilGitHubFuture' => 'FutureProxy', 'PhutilGitHubResponse' => 'Phobject', 'PhutilGitURI' => 'Phobject', 'PhutilGitURITestCase' => 'PhutilTestCase', 'PhutilGoogleAuthAdapter' => 'PhutilOAuthAuthAdapter', 'PhutilHTTPEngineExtension' => 'Phobject', 'PhutilHangForeverDaemon' => 'PhutilTortureTestDaemon', 'PhutilHashingIterator' => array( 'PhutilProxyIterator', 'Iterator', ), 'PhutilHashingIteratorTestCase' => 'PhutilTestCase', 'PhutilHelpArgumentWorkflow' => 'PhutilArgumentWorkflow', 'PhutilHgsprintfTestCase' => 'PhutilTestCase', 'PhutilHighIntensityIntervalDaemon' => 'PhutilTortureTestDaemon', 'PhutilICSParser' => 'Phobject', 'PhutilICSParserException' => 'Exception', 'PhutilICSParserTestCase' => 'PhutilTestCase', 'PhutilICSWriter' => 'Phobject', 'PhutilICSWriterTestCase' => 'PhutilTestCase', 'PhutilINIParserException' => 'Exception', 'PhutilIPAddress' => 'Phobject', 'PhutilIPAddressTestCase' => 'PhutilTestCase', 'PhutilIPv4Address' => 'PhutilIPAddress', 'PhutilIPv6Address' => 'PhutilIPAddress', 'PhutilInRequestKeyValueCache' => 'PhutilKeyValueCache', 'PhutilInteractiveEditor' => 'Phobject', 'PhutilInvalidRuleParserGeneratorException' => 'PhutilParserGeneratorException', 'PhutilInvalidStateException' => 'Exception', 'PhutilInvalidStateExceptionTestCase' => 'PhutilTestCase', 'PhutilInvisibleSyntaxHighlighter' => 'Phobject', 'PhutilIrreducibleRuleParserGeneratorException' => 'PhutilParserGeneratorException', 'PhutilJIRAAuthAdapter' => 'PhutilOAuth1AuthAdapter', 'PhutilJSON' => 'Phobject', 'PhutilJSONFragmentLexer' => 'PhutilLexer', 'PhutilJSONFragmentLexerHighlighterTestCase' => 'PhutilTestCase', 'PhutilJSONParser' => 'Phobject', 'PhutilJSONParserException' => 'Exception', 'PhutilJSONParserTestCase' => 'PhutilTestCase', 'PhutilJSONProtocolChannel' => 'PhutilProtocolChannel', 'PhutilJSONProtocolChannelTestCase' => 'PhutilTestCase', 'PhutilJSONTestCase' => 'PhutilTestCase', 'PhutilJavaCodeSnippetContextFreeGrammar' => 'PhutilCLikeCodeSnippetContextFreeGrammar', 'PhutilKeyValueCache' => 'Phobject', 'PhutilKeyValueCacheNamespace' => 'PhutilKeyValueCacheProxy', 'PhutilKeyValueCacheProfiler' => 'PhutilKeyValueCacheProxy', 'PhutilKeyValueCacheProxy' => 'PhutilKeyValueCache', 'PhutilKeyValueCacheStack' => 'PhutilKeyValueCache', 'PhutilKeyValueCacheTestCase' => 'PhutilTestCase', 'PhutilKoreanLocale' => 'PhutilLocale', 'PhutilLDAPAuthAdapter' => 'PhutilAuthAdapter', 'PhutilLanguageGuesser' => 'Phobject', 'PhutilLanguageGuesserTestCase' => 'PhutilTestCase', 'PhutilLexer' => 'Phobject', 'PhutilLexerSyntaxHighlighter' => 'PhutilSyntaxHighlighter', 'PhutilLibraryConflictException' => 'Exception', 'PhutilLibraryMapBuilder' => 'Phobject', 'PhutilLibraryTestCase' => 'PhutilTestCase', 'PhutilLipsumContextFreeGrammar' => 'PhutilContextFreeGrammar', 'PhutilLocale' => 'Phobject', 'PhutilLocaleTestCase' => 'PhutilTestCase', 'PhutilLock' => 'Phobject', 'PhutilLockException' => 'Exception', 'PhutilLogFileChannel' => 'PhutilChannelChannel', 'PhutilLunarPhase' => 'Phobject', 'PhutilLunarPhaseTestCase' => 'PhutilTestCase', 'PhutilMarkupEngine' => 'Phobject', 'PhutilMarkupTestCase' => 'PhutilTestCase', 'PhutilMemcacheKeyValueCache' => 'PhutilKeyValueCache', 'PhutilMethodNotImplementedException' => 'Exception', 'PhutilMetricsChannel' => 'PhutilChannelChannel', 'PhutilMissingSymbolException' => 'Exception', 'PhutilModuleUtilsTestCase' => 'PhutilTestCase', 'PhutilNiceDaemon' => 'PhutilTortureTestDaemon', 'PhutilNumber' => 'Phobject', 'PhutilOAuth1AuthAdapter' => 'PhutilAuthAdapter', 'PhutilOAuth1Future' => 'FutureProxy', 'PhutilOAuth1FutureTestCase' => 'PhutilTestCase', 'PhutilOAuthAuthAdapter' => 'PhutilAuthAdapter', 'PhutilOnDiskKeyValueCache' => 'PhutilKeyValueCache', 'PhutilOpaqueEnvelope' => 'Phobject', 'PhutilOpaqueEnvelopeKey' => 'Phobject', 'PhutilOpaqueEnvelopeTestCase' => 'PhutilTestCase', 'PhutilPHPCodeSnippetContextFreeGrammar' => 'PhutilCLikeCodeSnippetContextFreeGrammar', 'PhutilPHPFragmentLexer' => 'PhutilLexer', 'PhutilPHPFragmentLexerHighlighterTestCase' => 'PhutilTestCase', 'PhutilPHPFragmentLexerTestCase' => 'PhutilTestCase', 'PhutilPHPObjectProtocolChannel' => 'PhutilProtocolChannel', 'PhutilPHPObjectProtocolChannelTestCase' => 'PhutilTestCase', 'PhutilParserGenerator' => 'Phobject', 'PhutilParserGeneratorException' => 'Exception', 'PhutilParserGeneratorTestCase' => 'PhutilTestCase', 'PhutilPayPalAPIFuture' => 'FutureProxy', 'PhutilPersonTest' => array( 'Phobject', 'PhutilPerson', ), 'PhutilPhabricatorAuthAdapter' => 'PhutilOAuthAuthAdapter', 'PhutilPhtTestCase' => 'PhutilTestCase', 'PhutilPirateEnglishLocale' => 'PhutilLocale', 'PhutilPortugueseBrazilLocale' => 'PhutilLocale', 'PhutilPortuguesePortugalLocale' => 'PhutilLocale', 'PhutilPregsprintfTestCase' => 'PhutilTestCase', 'PhutilProcessGroupDaemon' => 'PhutilTortureTestDaemon', 'PhutilProseDiff' => 'Phobject', 'PhutilProseDiffTestCase' => 'PhutilTestCase', 'PhutilProseDifferenceEngine' => 'Phobject', 'PhutilProtocolChannel' => 'PhutilChannelChannel', 'PhutilProxyException' => 'Exception', 'PhutilProxyIterator' => array( 'Phobject', 'Iterator', ), 'PhutilPygmentizeParser' => 'Phobject', 'PhutilPygmentizeParserTestCase' => 'PhutilTestCase', 'PhutilPygmentsSyntaxHighlighter' => 'Phobject', 'PhutilPythonFragmentLexer' => 'PhutilLexer', 'PhutilQueryStringParser' => 'Phobject', 'PhutilQueryStringParserTestCase' => 'PhutilTestCase', 'PhutilRainbowSyntaxHighlighter' => 'Phobject', 'PhutilRawEnglishLocale' => 'PhutilLocale', 'PhutilReadableSerializer' => 'Phobject', 'PhutilReadableSerializerTestCase' => 'PhutilTestCase', 'PhutilRealNameContextFreeGrammar' => 'PhutilContextFreeGrammar', 'PhutilRemarkupBlockInterpreter' => 'Phobject', 'PhutilRemarkupBlockRule' => 'Phobject', 'PhutilRemarkupBlockStorage' => 'Phobject', 'PhutilRemarkupBoldRule' => 'PhutilRemarkupRule', 'PhutilRemarkupCodeBlockRule' => 'PhutilRemarkupBlockRule', 'PhutilRemarkupDefaultBlockRule' => 'PhutilRemarkupBlockRule', 'PhutilRemarkupDelRule' => 'PhutilRemarkupRule', 'PhutilRemarkupDocumentLinkRule' => 'PhutilRemarkupRule', 'PhutilRemarkupEngine' => 'PhutilMarkupEngine', 'PhutilRemarkupEngineTestCase' => 'PhutilTestCase', 'PhutilRemarkupEscapeRemarkupRule' => 'PhutilRemarkupRule', 'PhutilRemarkupHeaderBlockRule' => 'PhutilRemarkupBlockRule', 'PhutilRemarkupHighlightRule' => 'PhutilRemarkupRule', 'PhutilRemarkupHorizontalRuleBlockRule' => 'PhutilRemarkupBlockRule', 'PhutilRemarkupHyperlinkRule' => 'PhutilRemarkupRule', 'PhutilRemarkupInlineBlockRule' => 'PhutilRemarkupBlockRule', 'PhutilRemarkupInterpreterBlockRule' => 'PhutilRemarkupBlockRule', 'PhutilRemarkupItalicRule' => 'PhutilRemarkupRule', 'PhutilRemarkupLinebreaksRule' => 'PhutilRemarkupRule', 'PhutilRemarkupListBlockRule' => 'PhutilRemarkupBlockRule', 'PhutilRemarkupLiteralBlockRule' => 'PhutilRemarkupBlockRule', 'PhutilRemarkupMonospaceRule' => 'PhutilRemarkupRule', 'PhutilRemarkupNoteBlockRule' => 'PhutilRemarkupBlockRule', 'PhutilRemarkupQuotesBlockRule' => 'PhutilRemarkupBlockRule', 'PhutilRemarkupReplyBlockRule' => 'PhutilRemarkupBlockRule', 'PhutilRemarkupRule' => 'Phobject', 'PhutilRemarkupSimpleTableBlockRule' => 'PhutilRemarkupBlockRule', 'PhutilRemarkupTableBlockRule' => 'PhutilRemarkupBlockRule', 'PhutilRemarkupTestInterpreterRule' => 'PhutilRemarkupBlockInterpreter', 'PhutilRemarkupUnderlineRule' => 'PhutilRemarkupRule', 'PhutilRope' => 'Phobject', 'PhutilRopeTestCase' => 'PhutilTestCase', 'PhutilSafeHTML' => 'Phobject', 'PhutilSafeHTMLTestCase' => 'PhutilTestCase', 'PhutilSaturateStdoutDaemon' => 'PhutilTortureTestDaemon', 'PhutilSearchQueryCompiler' => 'Phobject', 'PhutilSearchQueryCompilerSyntaxException' => 'Exception', 'PhutilSearchQueryCompilerTestCase' => 'PhutilTestCase', 'PhutilSearchStemmer' => 'Phobject', 'PhutilSearchStemmerTestCase' => 'PhutilTestCase', 'PhutilServiceProfiler' => 'Phobject', 'PhutilShellLexer' => 'PhutilLexer', 'PhutilShellLexerTestCase' => 'PhutilTestCase', 'PhutilSignalHandler' => 'Phobject', 'PhutilSignalRouter' => 'Phobject', 'PhutilSimpleOptions' => 'Phobject', 'PhutilSimpleOptionsLexer' => 'PhutilLexer', 'PhutilSimpleOptionsLexerTestCase' => 'PhutilTestCase', 'PhutilSimpleOptionsTestCase' => 'PhutilTestCase', 'PhutilSimplifiedChineseLocale' => 'PhutilLocale', 'PhutilSlackAuthAdapter' => 'PhutilOAuthAuthAdapter', 'PhutilSlackFuture' => 'FutureProxy', 'PhutilSocketChannel' => 'PhutilChannel', 'PhutilSortVector' => 'Phobject', 'PhutilSpanishSpainLocale' => 'PhutilLocale', 'PhutilSprite' => 'Phobject', 'PhutilSpriteSheet' => 'Phobject', 'PhutilStreamIterator' => array( 'Phobject', 'Iterator', ), 'PhutilSyntaxHighlighter' => 'Phobject', 'PhutilSyntaxHighlighterEngine' => 'Phobject', 'PhutilSyntaxHighlighterException' => 'Exception', 'PhutilSystem' => 'Phobject', 'PhutilSystemTestCase' => 'PhutilTestCase', 'PhutilTerminalString' => 'Phobject', 'PhutilTestPhobject' => 'Phobject', 'PhutilTortureTestDaemon' => 'PhutilDaemon', 'PhutilTraditionalChineseLocale' => 'PhutilLocale', 'PhutilTranslation' => 'Phobject', 'PhutilTranslationTestCase' => 'PhutilTestCase', 'PhutilTranslator' => 'Phobject', 'PhutilTranslatorTestCase' => 'PhutilTestCase', 'PhutilTsprintfTestCase' => 'PhutilTestCase', 'PhutilTwitchAuthAdapter' => 'PhutilOAuthAuthAdapter', 'PhutilTwitchFuture' => 'FutureProxy', 'PhutilTwitterAuthAdapter' => 'PhutilOAuth1AuthAdapter', 'PhutilTypeCheckException' => 'Exception', 'PhutilTypeExtraParametersException' => 'Exception', 'PhutilTypeLexer' => 'PhutilLexer', 'PhutilTypeMissingParametersException' => 'Exception', 'PhutilTypeSpec' => 'Phobject', 'PhutilTypeSpecTestCase' => 'PhutilTestCase', 'PhutilURI' => 'Phobject', 'PhutilURITestCase' => 'PhutilTestCase', 'PhutilUSEnglishLocale' => 'PhutilLocale', 'PhutilUTF8StringTruncator' => 'Phobject', 'PhutilUTF8TestCase' => 'PhutilTestCase', 'PhutilUnknownSymbolParserGeneratorException' => 'PhutilParserGeneratorException', 'PhutilUnreachableRuleParserGeneratorException' => 'PhutilParserGeneratorException', 'PhutilUnreachableTerminalParserGeneratorException' => 'PhutilParserGeneratorException', 'PhutilUrisprintfTestCase' => 'PhutilTestCase', 'PhutilUtilsTestCase' => 'PhutilTestCase', 'PhutilVeryWowEnglishLocale' => 'PhutilLocale', 'PhutilWordPressAuthAdapter' => 'PhutilOAuthAuthAdapter', 'PhutilWordPressFuture' => 'FutureProxy', 'PhutilXHPASTBinary' => 'Phobject', 'PhutilXHPASTSyntaxHighlighter' => 'Phobject', 'PhutilXHPASTSyntaxHighlighterFuture' => 'FutureProxy', 'PhutilXHPASTSyntaxHighlighterTestCase' => 'PhutilTestCase', 'QueryFuture' => 'Future', 'TempFile' => 'Phobject', 'TestAbstractDirectedGraph' => 'AbstractDirectedGraph', 'XHPASTNode' => 'AASTNode', 'XHPASTNodeTestCase' => 'PhutilTestCase', 'XHPASTSyntaxErrorException' => 'Exception', 'XHPASTToken' => 'AASTToken', 'XHPASTTree' => 'AASTTree', 'XHPASTTreeTestCase' => 'PhutilTestCase', 'XsprintfUnknownConversionException' => 'InvalidArgumentException', ), )); diff --git a/src/daemon/PhutilDaemon.php b/src/daemon/PhutilDaemon.php index 629a567..082421f 100644 --- a/src/daemon/PhutilDaemon.php +++ b/src/daemon/PhutilDaemon.php @@ -1,407 +1,383 @@ shouldExit()) { * if (work_available()) { * $this->willBeginWork(); * do_work(); * $this->sleep(0); * } else { * $this->willBeginIdle(); * $this->sleep(1); * } * } * * In particular, call @{method:willBeginWork} before becoming busy, and * @{method:willBeginIdle} when no work is available. If the daemon is launched * into an autoscale pool, this will cause the pool to automatically scale up * when busy and down when idle. * * See @{class:PhutilHighIntensityIntervalDaemon} for an example of a simple * autoscaling daemon. * * Launching a daemon which does not make these callbacks into an autoscale * pool will have no effect. * * @task overseer Communicating With the Overseer * @task autoscale Autoscaling Daemon Pools */ abstract class PhutilDaemon extends Phobject { const MESSAGETYPE_STDOUT = 'stdout'; const MESSAGETYPE_HEARTBEAT = 'heartbeat'; const MESSAGETYPE_BUSY = 'busy'; const MESSAGETYPE_IDLE = 'idle'; const MESSAGETYPE_DOWN = 'down'; + const MESSAGETYPE_HIBERNATE = 'hibernate'; const WORKSTATE_BUSY = 'busy'; const WORKSTATE_IDLE = 'idle'; private $argv; private $traceMode; private $traceMemory; private $verbose; private $notifyReceived; private $inGracefulShutdown; private $workState = null; private $idleSince = null; - private $autoscaleProperties = array(); + private $scaledownDuration; final public function setVerbose($verbose) { $this->verbose = $verbose; return $this; } final public function getVerbose() { return $this->verbose; } + final public function setScaledownDuration($scaledown_duration) { + $this->scaledownDuration = $scaledown_duration; + return $this; + } + + final public function getScaledownDuration() { + return $this->scaledownDuration; + } + final public function __construct(array $argv) { $this->argv = $argv; $router = PhutilSignalRouter::getRouter(); $handler_key = 'daemon.term'; if (!$router->getHandler($handler_key)) { $handler = new PhutilCallbackSignalHandler( SIGTERM, __CLASS__.'::onTermSignal'); $router->installHandler($handler_key, $handler); } pcntl_signal(SIGINT, array($this, 'onGracefulSignal')); pcntl_signal(SIGUSR2, array($this, 'onNotifySignal')); // Without discard mode, this consumes unbounded amounts of memory. Keep // memory bounded. PhutilServiceProfiler::getInstance()->enableDiscardMode(); $this->beginStdoutCapture(); } final public function __destruct() { $this->endStdoutCapture(); } final public function stillWorking() { $this->emitOverseerMessage(self::MESSAGETYPE_HEARTBEAT, null); if ($this->traceMemory) { $daemon = get_class($this); fprintf( STDERR, "%s %s %s\n", '', $daemon, pht( 'Memory Usage: %s KB', new PhutilNumber(memory_get_usage() / 1024, 1))); } } final public function shouldExit() { return $this->inGracefulShutdown; } + final protected function shouldHibernate($duration) { + // Don't hibernate if we don't have very long to sleep. + if ($duration < 5) { + return false; + } + + // Never hibernate if we're part of a pool and could scale down instead. + // We only hibernate the last process to drop the pool size to zero. + if ($this->getScaledownDuration()) { + return false; + } + + // Don't hibernate for too long. + $duration = max($duration, phutil_units('3 minutes in seconds')); + + $this->emitOverseerMessage( + self::MESSAGETYPE_HIBERNATE, + array( + 'duration' => $duration, + )); + + $this->log( + pht( + 'Preparing to hibernate for %s second(s).', + new PhutilNumber($duration))); + + return true; + } + final protected function sleep($duration) { $this->notifyReceived = false; $this->willSleep($duration); $this->stillWorking(); - $is_autoscale = $this->isClonedAutoscaleDaemon(); - $scale_down = $this->getAutoscaleDownDuration(); + $scale_down = $this->getScaledownDuration(); $max_sleep = 60; - if ($is_autoscale) { + if ($scale_down) { $max_sleep = min($max_sleep, $scale_down); } - if ($is_autoscale) { + if ($scale_down) { if ($this->workState == self::WORKSTATE_IDLE) { $dur = (time() - $this->idleSince); $this->log(pht('Idle for %s seconds.', $dur)); } } while ($duration > 0 && !$this->notifyReceived && !$this->shouldExit()) { // If this is an autoscaling clone and we've been idle for too long, // we're going to scale the pool down by exiting and not restarting. The // DOWN message tells the overseer that we don't want to be restarted. - if ($is_autoscale) { + if ($scale_down) { if ($this->workState == self::WORKSTATE_IDLE) { if ($this->idleSince && ($this->idleSince + $scale_down < time())) { $this->inGracefulShutdown = true; $this->emitOverseerMessage(self::MESSAGETYPE_DOWN, null); $this->log( pht( 'Daemon was idle for more than %s second(s), '. 'scaling pool down.', new PhutilNumber($scale_down))); break; } } } sleep(min($duration, $max_sleep)); $duration -= $max_sleep; $this->stillWorking(); } } protected function willSleep($duration) { return; } public static function onTermSignal($signo) { self::didCatchSignal($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(); PhutilConsole::getConsole()->getServer()->setEnableLog(true); $this->didSetTraceMode(); return $this; } final public function getTraceMode() { return $this->traceMode; } final public function onGracefulSignal($signo) { self::didCatchSignal($signo); $this->inGracefulShutdown = true; } final public function onNotifySignal($signo) { self::didCatchSignal($signo); $this->notifyReceived = true; $this->onNotify($signo); } protected function onNotify($signo) { // This is a hook for subclasses. } protected function willRun() { // This is a hook for subclasses. } protected function didSetTraceMode() { // This is a hook for subclasses. } final protected function log($message) { if ($this->verbose) { $daemon = get_class($this); fprintf(STDERR, "%s %s %s\n", '', $daemon, $message); } } private static function didCatchSignal($signo) { $signame = phutil_get_signal_name($signo); fprintf( STDERR, "%s Caught signal %s (%s).\n", '', $signo, $signame); } /* -( Communicating With the Overseer )------------------------------------ */ private function beginStdoutCapture() { ob_start(array($this, 'didReceiveStdout'), 2); } private function endStdoutCapture() { ob_end_flush(); } public function didReceiveStdout($data) { if (!strlen($data)) { return ''; } return $this->encodeOverseerMessage(self::MESSAGETYPE_STDOUT, $data); } private function encodeOverseerMessage($type, $data) { $structure = array($type); if ($data !== null) { $structure[] = $data; } return json_encode($structure)."\n"; } private function emitOverseerMessage($type, $data) { $this->endStdoutCapture(); echo $this->encodeOverseerMessage($type, $data); $this->beginStdoutCapture(); } public static function errorListener($event, $value, array $metadata) { // If the caller has redirected the error log to a file, PHP won't output // messages to stderr, so the overseer can't capture them. Install a // listener which just echoes errors to stderr, so the overseer is always // aware of errors. $console = PhutilConsole::getConsole(); $message = idx($metadata, 'default_message'); if ($message) { $console->writeErr("%s\n", $message); } if (idx($metadata, 'trace')) { $trace = PhutilErrorHandler::formatStacktrace($metadata['trace']); $console->writeErr("%s\n", $trace); } } /* -( Autoscaling )-------------------------------------------------------- */ /** * Prepare to become busy. This may autoscale the pool up. * * This notifies the overseer that the daemon has become busy. If daemons * that are part of an autoscale pool are continuously busy for a prolonged * period of time, the overseer may scale up the pool. * * @return this * @task autoscale */ protected function willBeginWork() { if ($this->workState != self::WORKSTATE_BUSY) { $this->workState = self::WORKSTATE_BUSY; $this->idleSince = null; $this->emitOverseerMessage(self::MESSAGETYPE_BUSY, null); } return $this; } /** * Prepare to idle. This may autoscale the pool down. * * This notifies the overseer that the daemon is no longer busy. If daemons - * that are part of an autoscale pool are idle for a prolonged period of time, - * they may exit to scale the pool down. + * that are part of an autoscale pool are idle for a prolonged period of + * time, they may exit to scale the pool down. * * @return this * @task autoscale */ protected function willBeginIdle() { if ($this->workState != self::WORKSTATE_IDLE) { $this->workState = self::WORKSTATE_IDLE; $this->idleSince = time(); $this->emitOverseerMessage(self::MESSAGETYPE_IDLE, null); } return $this; } - - - /** - * Determine if this is a clone or the original daemon. - * - * @return bool True if this is an cloned autoscaling daemon. - * @task autoscale - */ - private function isClonedAutoscaleDaemon() { - return (bool)$this->getAutoscaleProperty('clone', false); - } - - - /** - * Get the duration (in seconds) which a daemon must be continuously idle - * for before it should exit to scale the pool down. - * - * @return int Duration, in seconds. - * @task autoscale - */ - private function getAutoscaleDownDuration() { - return $this->getAutoscaleProperty('down', 15); - } - - - /** - * Configure autoscaling for this daemon. - * - * @param map Map of autoscale properties. - * @return this - * @task autoscale - */ - public function setAutoscaleProperties(array $autoscale_properties) { - PhutilTypeSpec::checkMap( - $autoscale_properties, - array( - 'group' => 'optional string', - 'up' => 'optional int', - 'down' => 'optional int', - 'pool' => 'optional int', - 'clone' => 'optional bool', - 'reserve' => 'optional int|float', - )); - - $this->autoscaleProperties = $autoscale_properties; - - return $this; - } - - - /** - * Read autoscaling configuration for this daemon. - * - * @param string Property to read. - * @param wild Default value to return if the property is not set. - * @return wild Property value, or `$default` if one is not set. - * @task autoscale - */ - private function getAutoscaleProperty($key, $default = null) { - return idx($this->autoscaleProperties, $key, $default); - } - } diff --git a/src/daemon/PhutilDaemonHandle.php b/src/daemon/PhutilDaemonHandle.php index e6ff223..6d4e5b1 100644 --- a/src/daemon/PhutilDaemonHandle.php +++ b/src/daemon/PhutilDaemonHandle.php @@ -1,409 +1,478 @@ overseer = $overseer; - $this->daemonClass = $daemon_class; - $this->argv = $argv; - $this->config = $config; + + private function __construct() { + // + } + + public static function newFromConfig(array $config) { + PhutilTypeSpec::checkMap( + $config, + array( + 'class' => 'string', + 'argv' => 'optional list', + 'load' => 'optional list', + 'log' => 'optional string|null', + 'down' => 'optional int', + )); + + $config = $config + array( + 'argv' => array(), + 'load' => array(), + 'log' => null, + 'down' => 15, + ); + + $daemon = new self(); + $daemon->properties = $config; + $daemon->daemonID = $daemon->generateDaemonID(); + + return $daemon; + } + + public function setDaemonPool(PhutilDaemonPool $daemon_pool) { + $this->pool = $daemon_pool; + return $this; + } + + public function getDaemonPool() { + return $this->pool; + } + + public function getBusyEpoch() { + return $this->busyEpoch; + } + + public function getDaemonClass() { + return $this->getProperty('class'); + } + + private function getProperty($key) { + return idx($this->properties, $key); + } + + public function setCommandLineArguments(array $arguments) { + $this->argv = $arguments; + return $this; + } + + public function getCommandLineArguments() { + return $this->argv; + } + + public function getDaemonArguments() { + return $this->getProperty('argv'); + } + + public function didLaunch() { $this->restartAt = time(); - $this->daemonID = $this->generateDaemonID(); $this->dispatchEvent( self::EVENT_DID_LAUNCH, array( - 'argv' => $this->argv, - 'explicitArgv' => idx($this->config, 'argv'), + 'argv' => $this->getCommandLineArguments(), + 'explicitArgv' => $this->getDaemonArguments(), )); + + return $this; } public function isRunning() { return (bool)$this->future; } public function isDone() { return (!$this->shouldRestart && !$this->isRunning()); } public function getFuture() { return $this->future; } - public function setTraceMemory($trace_memory) { - $this->traceMemory = $trace_memory; - return $this; - } - - public function getTraceMemory() { - return $this->traceMemory; - } - public function update() { - $this->updateMemory(); - if (!$this->isRunning()) { if (!$this->shouldRestart) { return; } if (!$this->restartAt || (time() < $this->restartAt)) { return; } if ($this->shouldShutdown) { return; } $this->startDaemonProcess(); } $future = $this->future; $result = null; if ($future->isReady()) { $result = $future->resolve(); } list($stdout, $stderr) = $future->read(); $future->discardBuffers(); if (strlen($stdout)) { $this->didReadStdout($stdout); } $stderr = trim($stderr); if (strlen($stderr)) { foreach (phutil_split_lines($stderr, false) as $line) { $this->logMessage('STDE', $line); } } if ($result !== null) { list($err) = $result; if ($err) { $this->logMessage('FAIL', pht('Process exited with error %s.', $err)); } else { $this->logMessage('DONE', pht('Process exited normally.')); } $this->future = null; if ($this->shouldShutdown) { $this->restartAt = null; + $this->dispatchEvent(self::EVENT_WILL_EXIT); } else { $this->scheduleRestart(); } } $this->updateHeartbeatEvent(); $this->updateHangDetection(); } private function updateHeartbeatEvent() { if ($this->heartbeat > time()) { return; } $this->heartbeat = time() + $this->getHeartbeatEventFrequency(); $this->dispatchEvent(self::EVENT_DID_HEARTBEAT); } private function updateHangDetection() { if (!$this->isRunning()) { return; } if (time() > $this->deadline) { $this->logMessage('HANG', pht('Hang detected. Restarting process.')); $this->annihilateProcessGroup(); $this->scheduleRestart(); } } private function scheduleRestart() { - $this->logMessage('WAIT', pht('Waiting to restart process.')); - $this->restartAt = time() + self::getWaitBeforeRestart(); + // Wait a minimum of a few sceconds before restarting, but we may wait + // longer if the daemon has initiated hibernation. + $default_restart = time() + self::getWaitBeforeRestart(); + if ($default_restart >= $this->restartAt) { + $this->restartAt = $default_restart; + } + + $this->logMessage( + 'WAIT', + pht( + 'Waiting %s second(s) to restart process.', + new PhutilNumber($this->restartAt - time()))); } /** * Generate a unique ID for this daemon. * * @return string A unique daemon ID. */ private function generateDaemonID() { return substr(getmypid().':'.Filesystem::readRandomCharacters(12), 0, 12); } public function getDaemonID() { return $this->daemonID; } public function getPID() { return $this->pid; } private function getCaptureBufferSize() { return 65535; } private function getRequiredHeartbeatFrequency() { return 86400; } public static function getWaitBeforeRestart() { return 5; } public static function getHeartbeatEventFrequency() { return 120; } private function getKillDelay() { return 3; } private function getDaemonCWD() { $root = dirname(phutil_get_library_root('phutil')); return $root.'/scripts/daemon/exec/'; } private function newExecFuture() { - $class = $this->daemonClass; - $argv = $this->argv; + $class = $this->getDaemonClass(); + $argv = $this->getCommandLineArguments(); $buffer_size = $this->getCaptureBufferSize(); // NOTE: PHP implements proc_open() by running 'sh -c'. On most systems this // is bash, but on Ubuntu it's dash. When you proc_open() using bash, you // get one new process (the command you ran). When you proc_open() using // dash, you get two new processes: the command you ran and a parent // "dash -c" (or "sh -c") process. This means that the child process's PID // is actually the 'dash' PID, not the command's PID. To avoid this, use // 'exec' to replace the shell process with the real process; without this, // the child will call posix_getppid(), be given the pid of the 'sh -c' // process, and send it SIGUSR1 to keepalive which will terminate it // immediately. We also won't be able to do process group management because // the shell process won't properly posix_setsid() so the pgid of the child // won't be meaningful. + $config = $this->properties; + unset($config['class']); + $config = phutil_json_encode($config); + return id(new ExecFuture('exec ./exec_daemon.php %s %Ls', $class, $argv)) ->setCWD($this->getDaemonCWD()) ->setStdoutSizeLimit($buffer_size) ->setStderrSizeLimit($buffer_size) - ->write(json_encode($this->config)); + ->write($config); } /** * Dispatch an event to event listeners. * * @param string Event type. * @param dict Event parameters. * @return void */ private function dispatchEvent($type, array $params = array()) { $data = array( - 'id' => $this->daemonID, - 'daemonClass' => $this->daemonClass, - 'childPID' => $this->pid, + 'id' => $this->getDaemonID(), + 'daemonClass' => $this->getDaemonClass(), + 'childPID' => $this->getPID(), ) + $params; $event = new PhutilEvent($type, $data); try { PhutilEventEngine::dispatchEvent($event); } catch (Exception $ex) { phlog($ex); } } private function annihilateProcessGroup() { - $pid = $this->pid; + $pid = $this->getPID(); + $pgid = posix_getpgid($pid); if ($pid && $pgid) { posix_kill(-$pgid, SIGTERM); sleep($this->getKillDelay()); posix_kill(-$pgid, SIGKILL); $this->pid = null; } } - private function updateMemory() { - if ($this->traceMemory) { - $this->logMessage( - 'RAMS', - pht( - 'Overseer Memory Usage: %s KB', - new PhutilNumber(memory_get_usage() / 1024, 1))); - } - } - private function startDaemonProcess() { $this->logMessage('INIT', pht('Starting process.')); $this->deadline = time() + $this->getRequiredHeartbeatFrequency(); $this->heartbeat = time() + self::getHeartbeatEventFrequency(); $this->stdoutBuffer = ''; $this->future = $this->newExecFuture(); $this->future->start(); $this->pid = $this->future->getPID(); } private function didReadStdout($data) { $this->stdoutBuffer .= $data; while (true) { $pos = strpos($this->stdoutBuffer, "\n"); if ($pos === false) { break; } $message = substr($this->stdoutBuffer, 0, $pos); $this->stdoutBuffer = substr($this->stdoutBuffer, $pos + 1); try { $structure = phutil_json_decode($message); } catch (PhutilJSONParserException $ex) { $structure = array(); } switch (idx($structure, 0)) { case PhutilDaemon::MESSAGETYPE_STDOUT: $this->logMessage('STDO', idx($structure, 1)); break; case PhutilDaemon::MESSAGETYPE_HEARTBEAT: $this->deadline = time() + $this->getRequiredHeartbeatFrequency(); break; case PhutilDaemon::MESSAGETYPE_BUSY: - $this->overseer->didBeginWork($this); + if (!$this->busyEpoch) { + $this->busyEpoch = time(); + } break; case PhutilDaemon::MESSAGETYPE_IDLE: - $this->overseer->didBeginIdle($this); + $this->busyEpoch = null; break; case PhutilDaemon::MESSAGETYPE_DOWN: // The daemon is exiting because it doesn't have enough work and it // is trying to scale the pool down. We should not restart it. $this->shouldRestart = false; $this->shouldShutdown = true; break; + case PhutilDaemon::MESSAGETYPE_HIBERNATE: + $config = idx($structure, 1); + $duration = (int)idx($config, 'duration', 0); + $this->restartAt = time() + $duration; + $this->logMessage( + 'ZZZZ', + pht( + 'Process is preparing to hibernate for %s second(s).', + new PhutilNumber($duration))); + break; default: // If we can't parse this or it isn't a message we understand, just // emit the raw message. $this->logMessage('STDO', pht(' %s', $message)); break; } } } public function didReceiveNotifySignal($signo) { - $pid = $this->pid; + $pid = $this->getPID(); if ($pid) { posix_kill($pid, $signo); } } public function didReceiveReloadSignal($signo) { $signame = phutil_get_signal_name($signo); if ($signame) { $sigmsg = pht( 'Reloading in response to signal %d (%s).', $signo, $signame); } else { $sigmsg = pht( 'Reloading in response to signal %d.', $signo); } $this->logMessage('RELO', $sigmsg, $signo); // This signal means "stop the current process gracefully, then launch // a new identical process once it exits". This can be used to update // daemons after code changes (the new processes will run the new code) // without aborting any running tasks. // We SIGINT the daemon but don't set the shutdown flag, so it will // naturally be restarted after it exits, as though it had exited after an // unhandled exception. - posix_kill($this->pid, SIGINT); + posix_kill($this->getPID(), SIGINT); } public function didReceiveGracefulSignal($signo) { $this->shouldShutdown = true; $this->shouldRestart = false; $signame = phutil_get_signal_name($signo); if ($signame) { $sigmsg = pht( 'Graceful shutdown in response to signal %d (%s).', $signo, $signame); } else { $sigmsg = pht( 'Graceful shutdown in response to signal %d.', $signo); } $this->logMessage('DONE', $sigmsg, $signo); - posix_kill($this->pid, SIGINT); + posix_kill($this->getPID(), SIGINT); } - public function didReceiveTerminalSignal($signo) { + public function didReceiveTerminateSignal($signo) { $this->shouldShutdown = true; $this->shouldRestart = false; $signame = phutil_get_signal_name($signo); if ($signame) { $sigmsg = pht( 'Shutting down in response to signal %s (%s).', $signo, $signame); } else { $sigmsg = pht('Shutting down in response to signal %s.', $signo); } $this->logMessage('EXIT', $sigmsg, $signo); $this->annihilateProcessGroup(); } private function logMessage($type, $message, $context = null) { - $this->overseer->logMessage($type, $message, $context); + $this->getDaemonPool()->logMessage($type, $message, $context); + $this->dispatchEvent( self::EVENT_DID_LOG, array( 'type' => $type, 'message' => $message, 'context' => $context, )); } - public function didRemoveDaemon() { - $this->dispatchEvent(self::EVENT_WILL_EXIT); + public function toDictionary() { + return array( + 'pid' => $this->getPID(), + 'id' => $this->getDaemonID(), + 'config' => $this->properties, + ); } + } diff --git a/src/daemon/PhutilDaemonOverseer.php b/src/daemon/PhutilDaemonOverseer.php index 14d9b3f..fc5bbc6 100644 --- a/src/daemon/PhutilDaemonOverseer.php +++ b/src/daemon/PhutilDaemonOverseer.php @@ -1,536 +1,453 @@ enableDiscardMode(); $args = new PhutilArgumentParser($argv); $args->setTagline(pht('daemon overseer')); $args->setSynopsis(<<parseStandardArguments(); $args->parse( array( array( 'name' => 'trace-memory', 'help' => pht('Enable debug memory tracing.'), ), array( 'name' => 'verbose', 'help' => pht('Enable verbose activity logging.'), ), array( 'name' => 'label', 'short' => 'l', 'param' => 'label', 'help' => pht( 'Optional process label. Makes "%s" nicer, no behavioral effects.', 'ps'), ), )); $argv = array(); if ($args->getArg('trace')) { $this->traceMode = true; $argv[] = '--trace'; } if ($args->getArg('trace-memory')) { $this->traceMode = true; $this->traceMemory = true; $argv[] = '--trace-memory'; } $verbose = $args->getArg('verbose'); if ($verbose) { $this->verbose = true; $argv[] = '--verbose'; } $label = $args->getArg('label'); if ($label) { $argv[] = '-l'; $argv[] = $label; } $this->argv = $argv; if (function_exists('posix_isatty') && posix_isatty(STDIN)) { fprintf(STDERR, pht('Reading daemon configuration from stdin...')."\n"); } $config = @file_get_contents('php://stdin'); $config = id(new PhutilJSONParser())->parse($config); $this->libraries = idx($config, 'load'); $this->log = idx($config, 'log'); $this->daemonize = idx($config, 'daemonize'); $this->piddir = idx($config, 'piddir'); $this->config = $config; if (self::$instance) { throw new Exception( pht('You may not instantiate more than one Overseer per process.')); } self::$instance = $this; $this->startEpoch = time(); // Check this before we daemonize, since if it's an issue the child will // exit immediately. if ($this->piddir) { $dir = $this->piddir; try { Filesystem::assertWritable($dir); } catch (Exception $ex) { throw new Exception( pht( "Specified daemon PID directory ('%s') does not exist or is ". "not writable by the daemon user!", $dir)); } } if (!idx($config, 'daemons')) { throw new PhutilArgumentUsageException( pht('You must specify at least one daemon to start!')); } if ($this->log) { // NOTE: Now that we're committed to daemonizing, redirect the error // log if we have a `--log` parameter. Do this at the last moment // so as many setup issues as possible are surfaced. ini_set('error_log', $this->log); } if ($this->daemonize) { // We need to get rid of these or the daemon will hang when we TERM it // waiting for something to read the buffers. TODO: Learn how unix works. fclose(STDOUT); fclose(STDERR); ob_start(); $pid = pcntl_fork(); if ($pid === -1) { throw new Exception(pht('Unable to fork!')); } else if ($pid) { exit(0); } } $this->modules = PhutilDaemonOverseerModule::getAllModules(); - pcntl_signal(SIGUSR2, array($this, 'didReceiveNotifySignal')); - - pcntl_signal(SIGHUP, array($this, 'didReceiveReloadSignal')); - pcntl_signal(SIGINT, array($this, 'didReceiveGracefulSignal')); - pcntl_signal(SIGTERM, array($this, 'didReceiveTerminalSignal')); + $this->installSignalHandlers(); } public function addLibrary($library) { $this->libraries[] = $library; return $this; } public function run() { - $this->daemons = array(); - - foreach ($this->config['daemons'] as $config) { - $config += array( - 'argv' => array(), - 'autoscale' => array(), - ); - - $daemon = new PhutilDaemonHandle( - $this, - $config['class'], - $this->argv, - array( - 'log' => $this->log, - 'argv' => $config['argv'], - 'load' => $this->libraries, - 'autoscale' => $config['autoscale'], - )); - - $daemon->setTraceMemory($this->traceMemory); - - $this->addDaemon($daemon, $config); - - $group = idx($config['autoscale'], 'group'); - if (strlen($group)) { - if (isset($this->autoscaleConfig[$group])) { - throw new Exception( - pht( - 'Two daemons are part of the same autoscale group ("%s"). '. - 'Each daemon autoscale group must be unique.', - $group)); - } - $this->autoscaleConfig[$group] = $config; - } - } - - $should_reload = false; + $this->createDaemonPools(); while (true) { - foreach ($this->modules as $module) { - try { - if ($module->shouldReloadDaemons()) { - $this->logMessage( - 'RELO', - pht( - 'Reloading daemons (triggered by overseer module "%s").', - get_class($module))); - $should_reload = true; - } - } catch (Exception $ex) { - phlog($ex); - } - } - - if ($should_reload) { - $this->didReceiveReloadSignal(SIGHUP); - $should_reload = false; + if ($this->shouldReloadDaemons()) { + $this->didReceiveSignal(SIGHUP); } $futures = array(); - foreach ($this->getDaemonHandles() as $daemon) { - $daemon->update(); - if ($daemon->isRunning()) { - $futures[] = $daemon->getFuture(); - } + foreach ($this->getDaemonPools() as $pool) { + $pool->updatePool(); - if ($daemon->isDone()) { - $this->removeDaemon($daemon); + foreach ($pool->getFutures() as $future) { + $futures[] = $future; } } $this->updatePidfile(); - $this->updateAutoscale(); + $this->updateMemory(); - if ($futures) { - $iter = id(new FutureIterator($futures)) - ->setUpdateInterval(1); - foreach ($iter as $future) { - break; - } - } else { + $this->waitForDaemonFutures($futures); + + if (!$futures) { if ($this->inGracefulShutdown) { break; } - sleep(1); } } exit($this->err); } - private function addDaemon(PhutilDaemonHandle $daemon, array $config) { - $id = $daemon->getDaemonID(); - $this->daemons[$id] = array( - 'handle' => $daemon, - 'config' => $config, - ); - $autoscale_group = $this->getAutoscaleGroup($daemon); - if ($autoscale_group) { - $this->autoscale[$autoscale_group][$id] = true; - } - - return $this; - } - - private function removeDaemon(PhutilDaemonHandle $daemon) { - $id = $daemon->getDaemonID(); - - $autoscale_group = $this->getAutoscaleGroup($daemon); - if ($autoscale_group) { - unset($this->autoscale[$autoscale_group][$id]); - } + private function waitForDaemonFutures(array $futures) { + assert_instances_of($futures, 'ExecFuture'); - unset($this->daemons[$id]); - - $daemon->didRemoveDaemon(); - - return $this; - } - - private function getAutoscaleGroup(PhutilDaemonHandle $daemon) { - $id = $daemon->getDaemonID(); - $autoscale = $this->daemons[$id]['config']['autoscale']; - return idx($autoscale, 'group'); - } - - private function getAutoscaleProperty($group_key, $key, $default = null) { - $config = $this->autoscaleConfig[$group_key]['autoscale']; - return idx($config, $key, $default); - } - - public function didBeginWork(PhutilDaemonHandle $daemon) { - $id = $daemon->getDaemonID(); - $busy = idx($this->daemons[$daemon->getDaemonID()], 'busy'); - if (!$busy) { - $this->daemons[$id]['busy'] = time(); - } - } - - public function didBeginIdle(PhutilDaemonHandle $daemon) { - $id = $daemon->getDaemonID(); - unset($this->daemons[$id]['busy']); - } - - public function updateAutoscale() { - if ($this->inGracefulShutdown) { - return; - } - - foreach ($this->autoscale as $group => $daemons) { - $scaleup_duration = $this->getAutoscaleProperty($group, 'up', 2); - $max_pool_size = $this->getAutoscaleProperty($group, 'pool', 8); - $reserve = $this->getAutoscaleProperty($group, 'reserve', 0); - - // Don't scale a group if it is already at the maximum pool size. - if (count($daemons) >= $max_pool_size) { - continue; + if ($futures) { + // TODO: This only wakes if any daemons actually exit. It would be a bit + // cleaner to wait on any I/O with Channels. + $iter = id(new FutureIterator($futures)) + ->setUpdateInterval(1); + foreach ($iter as $future) { + break; } - - $should_scale = true; - foreach ($daemons as $daemon_id => $ignored) { - $busy = idx($this->daemons[$daemon_id], 'busy'); - if (!$busy) { - // At least one daemon in the group hasn't reported that it has - // started work. - $should_scale = false; - break; - } - - if ((time() - $busy) < $scaleup_duration) { - // At least one daemon in the group was idle recently, so we have - // not fullly - $should_scale = false; - break; - } - } - - // If we have a configured memory reserve for this pool, it tells us that - // we should not scale up unless there's at least that much memory left - // on the system (for example, a reserve of 0.25 means that 25% of system - // memory must be free to autoscale). - if ($should_scale && $reserve) { - // On some systems this may be slightly more expensive than other - // checks, so only do it once we're prepared to scale up. - $memory = PhutilSystem::getSystemMemoryInformation(); - $free_ratio = ($memory['free'] / $memory['total']); - - // If we don't have enough free memory, don't scale. - if ($free_ratio <= $reserve) { - continue; - } - } - - if ($should_scale) { - $config = $this->autoscaleConfig[$group]; - - $config['autoscale']['clone'] = true; - - $clone = new PhutilDaemonHandle( - $this, - $config['class'], - $this->argv, - array( - 'log' => $this->log, - 'argv' => $config['argv'], - 'load' => $this->libraries, - 'autoscale' => $config['autoscale'], - )); - - $this->logMessage( - 'AUTO', - pht( - 'Scaling pool "%s" up to %s daemon(s).', - $group, - new PhutilNumber(count($daemons) + 1))); - - $this->addDaemon($clone, $config); - - // Don't scale more than one pool up per iteration. Otherwise, we could - // break the memory barrier if we have a lot of pools and scale them - // all up at once. - return; + } else { + if (!$this->inGracefulShutdown) { + sleep(1); } } } - public function didReceiveNotifySignal($signo) { - foreach ($this->getDaemonHandles() as $daemon) { - $daemon->didReceiveNotifySignal($signo); - } - } + private function createDaemonPools() { + $configs = $this->config['daemons']; - public function didReceiveReloadSignal($signo) { - foreach ($this->getDaemonHandles() as $daemon) { - $daemon->didReceiveReloadSignal($signo); - } - } + $forced_options = array( + 'load' => $this->libraries, + 'log' => $this->log, + ); - public function didReceiveGracefulSignal($signo) { - // If we receive SIGINT more than once, interpret it like SIGTERM. - if ($this->inGracefulShutdown) { - return $this->didReceiveTerminalSignal($signo); - } - $this->inGracefulShutdown = true; + foreach ($configs as $config) { + $config = $forced_options + $config; - foreach ($this->getDaemonHandles() as $daemon) { - $daemon->didReceiveGracefulSignal($signo); - } - } + $pool = PhutilDaemonPool::newFromConfig($config) + ->setOverseer($this) + ->setCommandLineArguments($this->argv); - public function didReceiveTerminalSignal($signo) { - $this->err = 128 + $signo; - if ($this->inAbruptShutdown) { - exit($this->err); - } - $this->inAbruptShutdown = true; - - foreach ($this->getDaemonHandles() as $daemon) { - $daemon->didReceiveTerminalSignal($signo); + $this->pools[] = $pool; } } - private function getDaemonHandles() { - return ipull($this->daemons, 'handle'); + private function getDaemonPools() { + return $this->pools; } + /** * Identify running daemons by examining the process table. This isn't * completely reliable, but can be used as a fallback if the pid files fail * or we end up with stray daemons by other means. * * Example output (array keys are process IDs): * * array( * 12345 => array( * 'type' => 'overseer', * 'command' => 'php launch_daemon.php --daemonize ...', * 'pid' => 12345, * ), * 12346 => array( * 'type' => 'daemon', * 'command' => 'php exec_daemon.php ...', * 'pid' => 12346, * ), * ); * * @return dict Map of PIDs to process information, identifying running * daemon processes. */ public static function findRunningDaemons() { $results = array(); list($err, $processes) = exec_manual('ps -o pid,command -a -x -w -w -w'); if ($err) { return $results; } $processes = array_filter(explode("\n", trim($processes))); foreach ($processes as $process) { list($pid, $command) = preg_split('/\s+/', trim($process), 2); $pattern = '/((launch|exec)_daemon.php|phd-daemon)/'; $matches = null; if (!preg_match($pattern, $command, $matches)) { continue; } switch ($matches[1]) { case 'exec_daemon.php': $type = 'daemon'; break; case 'launch_daemon.php': case 'phd-daemon': default: $type = 'overseer'; break; } $results[(int)$pid] = array( 'type' => $type, 'command' => $command, 'pid' => (int)$pid, ); } return $results; } private function updatePidfile() { if (!$this->piddir) { return; } - $daemons = array(); + $pidfile = $this->toDictionary(); - foreach ($this->daemons as $daemon) { - $handle = $daemon['handle']; - $config = $daemon['config']; + if ($pidfile !== $this->lastPidfile) { + $this->lastPidfile = $pidfile; + $pidfile_path = $this->piddir.'/daemon.'.getmypid(); + Filesystem::writeFile($pidfile_path, phutil_json_encode($pidfile)); + } + } - if (!$handle->isRunning()) { - continue; - } + public function toDictionary() { + $daemons = array(); + foreach ($this->getDaemonPools() as $pool) { + foreach ($pool->getDaemons() as $daemon) { + if (!$daemon->isRunning()) { + continue; + } - $daemons[] = array( - 'pid' => $handle->getPID(), - 'id' => $handle->getDaemonID(), - 'config' => $config, - ); + $daemons[] = $daemon->toDictionary(); + } } - $pidfile = array( + return array( 'pid' => getmypid(), 'start' => $this->startEpoch, 'config' => $this->config, 'daemons' => $daemons, ); + } - if ($pidfile !== $this->lastPidfile) { - $this->lastPidfile = $pidfile; - $pidfile_path = $this->piddir.'/daemon.'.getmypid(); - Filesystem::writeFile($pidfile_path, json_encode($pidfile)); + private function updateMemory() { + if (!$this->traceMemory) { + return; } + + $this->logMessage( + 'RAMS', + pht( + 'Overseer Memory Usage: %s KB', + new PhutilNumber(memory_get_usage() / 1024, 1))); } public function logMessage($type, $message, $context = null) { if ($this->traceMode || $this->verbose) { error_log(date('Y-m-d g:i:s A').' ['.$type.'] '.$message); } } + +/* -( Signal Handling )---------------------------------------------------- */ + + + /** + * @task signals + */ + private function installSignalHandlers() { + $signals = array( + SIGUSR2, + SIGHUP, + SIGINT, + SIGTERM, + ); + + foreach ($signals as $signal) { + pcntl_signal($signal, array($this, 'didReceiveSignal')); + } + } + + + /** + * @task signals + */ + public function didReceiveSignal($signo) { + switch ($signo) { + case SIGUSR2: + $signal_type = self::SIGNAL_NOTIFY; + break; + case SIGHUP: + $signal_type = self::SIGNAL_RELOAD; + break; + case SIGINT: + // If we receive SIGINT more than once, interpret it like SIGTERM. + if ($this->inGracefulShutdown) { + return $this->didReceiveSignal(SIGTERM); + } + + $this->inGracefulShutdown = true; + $signal_type = self::SIGNAL_GRACEFUL; + break; + case SIGTERM: + // If we receive SIGTERM more than once, terminate abruptly. + $this->err = 128 + $signo; + if ($this->inAbruptShutdown) { + exit($this->err); + } + + $this->inAbruptShutdown = true; + $signal_type = self::SIGNAL_TERMINATE; + break; + default: + throw new Exception( + pht( + 'Signal handler called with unknown signal type ("%d")!', + $signo)); + } + + foreach ($this->getDaemonPools() as $pool) { + $pool->didReceiveSignal($signal_type, $signo); + } + } + + +/* -( Daemon Modules )----------------------------------------------------- */ + + + private function getModules() { + return $this->modules; + } + + private function shouldReloadDaemons() { + $modules = $this->getModules(); + + $should_reload = false; + foreach ($modules as $module) { + try { + // NOTE: Even if one module tells us to reload, we call the method on + // each module anyway to make calls a little more predictable. + + if ($module->shouldReloadDaemons()) { + $this->logMessage( + 'RELO', + pht( + 'Reloading daemons (triggered by overseer module "%s").', + get_class($module))); + $should_reload = true; + } + } catch (Exception $ex) { + phlog($ex); + } + } + + return $should_reload; + } + + } diff --git a/src/daemon/PhutilDaemonPool.php b/src/daemon/PhutilDaemonPool.php new file mode 100644 index 0000000..e6080c5 --- /dev/null +++ b/src/daemon/PhutilDaemonPool.php @@ -0,0 +1,245 @@ + + } + + public static function newFromConfig(array $config) { + PhutilTypeSpec::checkMap( + $config, + array( + 'class' => 'string', + 'label' => 'string', + 'argv' => 'optional list', + 'load' => 'optional list', + 'log' => 'optional string|null', + 'pool' => 'optional int', + 'up' => 'optional int', + 'down' => 'optional int', + 'reserve' => 'optional int|float', + )); + + $config = $config + array( + 'argv' => array(), + 'load' => array(), + 'log' => null, + 'pool' => 1, + 'up' => 2, + 'down' => 15, + 'reserve' => 0, + ); + + $pool = new self(); + $pool->properties = $config; + + return $pool; + } + + public function setOverseer(PhutilDaemonOverseer $overseer) { + $this->overseer = $overseer; + return $this; + } + + public function getOverseer() { + return $this->overseer; + } + + public function setCommandLineArguments(array $arguments) { + $this->commandLineArguments = $arguments; + return $this; + } + + public function getCommandLineArguments() { + return $this->commandLineArguments; + } + + private function newDaemon() { + $config = $this->properties; + + if (count($this->daemons)) { + $down_duration = $this->getPoolScaledownDuration(); + } else { + // TODO: For now, never scale pools down to 0. + $down_duration = 0; + } + + $forced_config = array( + 'down' => $down_duration, + ); + + $config = $forced_config + $config; + + $config = array_select_keys( + $config, + array( + 'class', + 'log', + 'load', + 'argv', + 'down', + )); + + $daemon = PhutilDaemonHandle::newFromConfig($config) + ->setDaemonPool($this) + ->setCommandLineArguments($this->getCommandLineArguments()); + + $daemon_id = $daemon->getDaemonID(); + $this->daemons[$daemon_id] = $daemon; + + $daemon->didLaunch(); + + return $daemon; + } + + public function getDaemons() { + return $this->daemons; + } + + public function getFutures() { + $futures = array(); + foreach ($this->getDaemons() as $daemon) { + $future = $daemon->getFuture(); + if ($future) { + $futures[] = $future; + } + } + + return $futures; + } + + public function didReceiveSignal($signal, $signo) { + foreach ($this->getDaemons() as $daemon) { + switch ($signal) { + case PhutilDaemonOverseer::SIGNAL_NOTIFY: + $daemon->didReceiveNotifySignal($signo); + break; + case PhutilDaemonOverseer::SIGNAL_RELOAD: + $daemon->didReceiveReloadSignal($signo); + break; + case PhutilDaemonOverseer::SIGNAL_GRACEFUL: + $daemon->didReceiveGracefulSignal($signo); + break; + case PhutilDaemonOverseer::SIGNAL_TERMINATE: + $daemon->didReceiveTerminateSignal($signo); + break; + default: + throw new Exception( + pht( + 'Unknown signal "%s" ("%d").', + $signal, + $signo)); + } + } + } + + public function getPoolLabel() { + return $this->getPoolProperty('label'); + } + + public function getPoolMaximumSize() { + return $this->getPoolProperty('pool'); + } + + public function getPoolScaleupDuration() { + return $this->getPoolProperty('up'); + } + + public function getPoolScaledownDuration() { + return $this->getPoolProperty('down'); + } + + public function getPoolMemoryReserve() { + return $this->getPoolProperty('reserve'); + } + + public function getPoolDaemonClass() { + return $this->getPoolProperty('class'); + } + + private function getPoolProperty($key) { + return idx($this->properties, $key); + } + + public function updatePool() { + $daemons = $this->getDaemons(); + + foreach ($daemons as $key => $daemon) { + $daemon->update(); + + if ($daemon->isDone()) { + unset($daemons[$key]); + } + } + + $this->updateAutoscale(); + } + + private function updateAutoscale() { + $daemons = $this->getDaemons(); + + // If this pool is already at the maximum size, we can't launch any new + // daemons. + $max_size = $this->getPoolMaximumSize(); + if (count($daemons) >= $max_size) { + return; + } + + $now = time(); + $scaleup_duration = $this->getPoolScaleupDuration(); + + foreach ($daemons as $daemon) { + $busy_epoch = $daemon->getBusyEpoch(); + // If any daemons haven't started work yet, don't scale the pool up. + if (!$busy_epoch) { + return; + } + + // If any daemons started work very recently, wait a little while + // to scale the pool up. + $busy_for = ($now - $busy_epoch); + if ($busy_for < $scaleup_duration) { + return; + } + } + + // If we have a configured memory reserve for this pool, it tells us that + // we should not scale up unless there's at least that much memory left + // on the system (for example, a reserve of 0.25 means that 25% of system + // memory must be free to autoscale). + $reserve = $this->getPoolMemoryReserve(); + if ($reserve) { + // On some systems this may be slightly more expensive than other checks, + // so we only do it once we're prepared to scale up. + $memory = PhutilSystem::getSystemMemoryInformation(); + $free_ratio = ($memory['free'] / $memory['total']); + + // If we don't have enough free memory, don't scale. + if ($free_ratio <= $reserve) { + return; + } + } + + $this->logMessage( + 'AUTO', + pht( + 'Scaling pool "%s" up to %s daemon(s).', + $this->getPoolLabel(), + new PhutilNumber(count($daemons) + 1))); + + $this->newDaemon(); + } + + public function logMessage($type, $message, $context = null) { + return $this->getOverseer()->logMessage($type, $message, $context); + } + +} diff --git a/src/parser/argument/PhutilArgumentParser.php b/src/parser/argument/PhutilArgumentParser.php index f554d65..177e800 100644 --- a/src/parser/argument/PhutilArgumentParser.php +++ b/src/parser/argument/PhutilArgumentParser.php @@ -1,911 +1,914 @@ setTagline('make an new dog') * $args->setSynopsis(<<parse( * array( * array( * 'name' => 'name', * 'param' => 'dogname', * 'default' => 'Rover', * 'help' => 'Set the dog\'s name. By default, the dog will be '. * 'named "Rover".', * ), * array( * 'name' => 'big', * 'short' => 'b', * 'help' => 'If set, create a large dog.', * ), * )); * * $dog_name = $args->getArg('name'); * $dog_size = $args->getArg('big') ? 'big' : 'small'; * * // ... etc ... * * (For detailed documentation on supported keys in argument specifications, * see @{class:PhutilArgumentSpecification}.) * * This will handle argument parsing, and generate appropriate usage help if * the user provides an unsupported flag. @{class:PhutilArgumentParser} also * supports some builtin "standard" arguments: * * $args->parseStandardArguments(); * * See @{method:parseStandardArguments} for details. Notably, this includes * a "--help" flag, and an "--xprofile" flag for profiling command-line scripts. * * Normally, when the parser encounters an unknown flag, it will exit with * an error. However, you can use @{method:parsePartial} to consume only a * set of flags: * * $args->parsePartial($spec_list); * * This allows you to parse some flags before making decisions about other * parsing, or share some flags across scripts. The builtin standard arguments * are implemented in this way. * * There is also builtin support for "workflows", which allow you to build a * script that operates in several modes (e.g., by accepting commands like * `install`, `upgrade`, etc), like `arc` does. For detailed documentation on * workflows, see @{class:PhutilArgumentWorkflow}. * * @task parse Parsing Arguments * @task read Reading Arguments * @task help Command Help * @task internal Internals */ final class PhutilArgumentParser extends Phobject { private $bin; private $argv; private $specs = array(); private $results = array(); private $parsed; private $tagline; private $synopsis; private $workflows; private $showHelp; const PARSE_ERROR_CODE = 77; private static $traceModeEnabled = false; /* -( Parsing Arguments )-------------------------------------------------- */ /** * Build a new parser. Generally, you start a script with: * * $args = new PhutilArgumentParser($argv); * * @param list Argument vector to parse, generally the $argv global. * @task parse */ public function __construct(array $argv) { $this->bin = $argv[0]; $this->argv = array_slice($argv, 1); } /** * Parse and consume a list of arguments, removing them from the argument * vector but leaving unparsed arguments for later consumption. You can * retrieve unconsumed arguments directly with * @{method:getUnconsumedArgumentVector}. Doing a partial parse can make it * easier to share common flags across scripts or workflows. * * @param list List of argument specs, see * @{class:PhutilArgumentSpecification}. * @return this * @task parse */ public function parsePartial(array $specs) { return $this->parseInternal($specs, false); } + /** + * @return this + */ private function parseInternal(array $specs, $correct_spelling) { $specs = PhutilArgumentSpecification::newSpecsFromList($specs); $this->mergeSpecs($specs); $specs_by_name = mpull($specs, null, 'getName'); $specs_by_short = mpull($specs, null, 'getShortAlias'); unset($specs_by_short[null]); $argv = $this->argv; $len = count($argv); for ($ii = 0; $ii < $len; $ii++) { $arg = $argv[$ii]; $map = null; $options = null; if (!is_string($arg)) { // Non-string argument; pass it through as-is. } else if ($arg == '--') { // This indicates "end of flags". break; } else if ($arg == '-') { // This is a normal argument (e.g., stdin). continue; } else if (!strncmp('--', $arg, 2)) { $pre = '--'; $arg = substr($arg, 2); $map = $specs_by_name; $options = array_keys($specs_by_name); } else if (!strncmp('-', $arg, 1) && strlen($arg) > 1) { $pre = '-'; $arg = substr($arg, 1); $map = $specs_by_short; } if ($map) { $val = null; $parts = explode('=', $arg, 2); if (count($parts) == 2) { list($arg, $val) = $parts; } // Try to correct flag spelling for full flags, to allow users to make // minor mistakes. if ($correct_spelling && $options && !isset($map[$arg])) { $corrections = PhutilArgumentSpellingCorrector::newFlagCorrector() ->correctSpelling($arg, $options); if (count($corrections) == 1) { $corrected = head($corrections); $this->logMessage( tsprintf( "%s\n", pht( '(Assuming "%s" is the British spelling of "%s".)', $pre.$arg, $pre.$corrected))); $arg = $corrected; } } if (isset($map[$arg])) { $spec = $map[$arg]; unset($argv[$ii]); $param_name = $spec->getParamName(); if ($val !== null) { if ($param_name === null) { throw new PhutilArgumentUsageException( pht( "Argument '%s' does not take a parameter.", "{$pre}{$arg}")); } } else { if ($param_name !== null) { if ($ii + 1 < $len) { $val = $argv[$ii + 1]; unset($argv[$ii + 1]); $ii++; } else { throw new PhutilArgumentUsageException( pht( "Argument '%s' requires a parameter.", "{$pre}{$arg}")); } } else { $val = true; } } if (!$spec->getRepeatable()) { if (array_key_exists($spec->getName(), $this->results)) { throw new PhutilArgumentUsageException( pht( "Argument '%s' was provided twice.", "{$pre}{$arg}")); } } $conflicts = $spec->getConflicts(); foreach ($conflicts as $conflict => $reason) { if (array_key_exists($conflict, $this->results)) { if (!is_string($reason) || !strlen($reason)) { $reason = '.'; } else { $reason = ': '.$reason.'.'; } throw new PhutilArgumentUsageException( pht( "Argument '%s' conflicts with argument '%s'%s", "{$pre}{$arg}", "--{$conflict}", $reason)); } } if ($spec->getRepeatable()) { if ($spec->getParamName() === null) { if (empty($this->results[$spec->getName()])) { $this->results[$spec->getName()] = 0; } $this->results[$spec->getName()]++; } else { $this->results[$spec->getName()][] = $val; } } else { $this->results[$spec->getName()] = $val; } } } } foreach ($specs as $spec) { if ($spec->getWildcard()) { $this->results[$spec->getName()] = $this->filterWildcardArgv($argv); $argv = array(); break; } } $this->argv = array_values($argv); return $this; } /** * Parse and consume a list of arguments, throwing an exception if there is * anything left unconsumed. This is like @{method:parsePartial}, but raises * a {class:PhutilArgumentUsageException} if there are leftovers. * * Normally, you would call @{method:parse} instead, which emits a * user-friendly error. You can also use @{method:printUsageException} to * render the exception in a user-friendly way. * * @param list List of argument specs, see * @{class:PhutilArgumentSpecification}. * @return this * @task parse */ public function parseFull(array $specs) { $this->parseInternal($specs, true); if (count($this->argv)) { $arg = head($this->argv); throw new PhutilArgumentUsageException( pht("Unrecognized argument '%s'.", $arg)); } if ($this->showHelp) { $this->printHelpAndExit(); } return $this; } /** * Parse and consume a list of arguments, raising a user-friendly error if * anything remains. See also @{method:parseFull} and @{method:parsePartial}. * * @param list List of argument specs, see * @{class:PhutilArgumentSpecification}. * @return this * @task parse */ public function parse(array $specs) { try { return $this->parseFull($specs); } catch (PhutilArgumentUsageException $ex) { $this->printUsageException($ex); exit(self::PARSE_ERROR_CODE); } } /** * Parse and execute workflows, raising a user-friendly error if anything * remains. See also @{method:parseWorkflowsFull}. * * See @{class:PhutilArgumentWorkflow} for details on using workflows. * * @param list List of argument specs, see * @{class:PhutilArgumentSpecification}. * @return this * @task parse */ public function parseWorkflows(array $workflows) { try { return $this->parseWorkflowsFull($workflows); } catch (PhutilArgumentUsageException $ex) { $this->printUsageException($ex); exit(self::PARSE_ERROR_CODE); } } /** * Select a workflow. For commands that may operate in several modes, like * `arc`, the modes can be split into "workflows". Each workflow specifies * the arguments it accepts. This method takes a list of workflows, selects * the chosen workflow, parses its arguments, and either executes it (if it * is executable) or returns it for handling. * * See @{class:PhutilArgumentWorkflow} for details on using workflows. * * @param list List of @{class:PhutilArgumentWorkflow}s. * @return PhutilArgumentWorkflow|no Returns the chosen workflow if it is * not executable, or executes it and * exits with a return code if it is. * @task parse */ public function parseWorkflowsFull(array $workflows) { assert_instances_of($workflows, 'PhutilArgumentWorkflow'); // Clear out existing workflows. We need to do this to permit the // construction of sub-workflows. $this->workflows = array(); foreach ($workflows as $workflow) { $name = $workflow->getName(); if ($name === null) { throw new PhutilArgumentSpecificationException( pht('Workflow has no name!')); } if (isset($this->workflows[$name])) { throw new PhutilArgumentSpecificationException( pht("Two workflows with name '%s!", $name)); } $this->workflows[$name] = $workflow; } $argv = $this->argv; if (empty($argv)) { // TODO: this is kind of hacky / magical. if (isset($this->workflows['help'])) { $argv = array('help'); } else { throw new PhutilArgumentUsageException(pht('No workflow selected.')); } } $flow = array_shift($argv); if (empty($this->workflows[$flow])) { $corrected = PhutilArgumentSpellingCorrector::newCommandCorrector() ->correctSpelling($flow, array_keys($this->workflows)); if (count($corrected) == 1) { $corrected = head($corrected); $this->logMessage( tsprintf( "%s\n", pht( '(Assuming "%s" is the British spelling of "%s".)', $flow, $corrected))); $flow = $corrected; } else { $this->raiseUnknownWorkflow($flow, $corrected); } } $workflow = $this->workflows[$flow]; if ($this->showHelp) { // Make "cmd flow --help" behave like "cmd help flow", not "cmd help". $help_flow = idx($this->workflows, 'help'); if ($help_flow) { if ($help_flow !== $workflow) { $workflow = $help_flow; $argv = array($flow); // Prevent parse() from dumping us back out to standard help. $this->showHelp = false; } } else { $this->printHelpAndExit(); } } $this->argv = array_values($argv); if ($workflow->shouldParsePartial()) { $this->parsePartial($workflow->getArguments()); } else { $this->parse($workflow->getArguments()); } if ($workflow->isExecutable()) { $workflow->setArgv($this); $err = $workflow->execute($this); exit($err); } else { return $workflow; } } /** * Parse "standard" arguments and apply their effects: * * --trace Enable service call tracing. * --no-ansi Disable ANSI color/style sequences. * --xprofile Write out an XHProf profile. * --help Show help. * * @return this * * @phutil-external-symbol function xhprof_enable */ public function parseStandardArguments() { try { $this->parsePartial( array( array( 'name' => 'trace', 'help' => pht('Trace command execution and show service calls.'), 'standard' => true, ), array( 'name' => 'no-ansi', 'help' => pht( 'Disable ANSI terminal codes, printing plain text with '. 'no color or style.'), 'conflicts' => array( 'ansi' => null, ), 'standard' => true, ), array( 'name' => 'ansi', 'help' => pht( "Use formatting even in environments which probably ". "don't support it."), 'standard' => true, ), array( 'name' => 'xprofile', 'param' => 'profile', 'help' => pht( 'Profile script execution and write results to a file.'), 'standard' => true, ), array( 'name' => 'help', 'short' => 'h', 'help' => pht('Show this help.'), 'standard' => true, ), array( 'name' => 'show-standard-options', 'help' => pht( 'Show every option, including standard options like this one.'), 'standard' => true, ), array( 'name' => 'recon', 'help' => pht('Start in remote console mode.'), 'standard' => true, ), )); } catch (PhutilArgumentUsageException $ex) { $this->printUsageException($ex); exit(self::PARSE_ERROR_CODE); } if ($this->getArg('trace')) { PhutilServiceProfiler::installEchoListener(); self::$traceModeEnabled = true; } if ($this->getArg('no-ansi')) { PhutilConsoleFormatter::disableANSI(true); } if ($this->getArg('ansi')) { PhutilConsoleFormatter::disableANSI(false); } if ($this->getArg('help')) { $this->showHelp = true; } $xprofile = $this->getArg('xprofile'); if ($xprofile) { if (!function_exists('xhprof_enable')) { throw new Exception( pht("To use '%s', you must install XHProf.", '--xprofile')); } xhprof_enable(0); register_shutdown_function(array($this, 'shutdownProfiler')); } $recon = $this->getArg('recon'); if ($recon) { $remote_console = PhutilConsole::newRemoteConsole(); $remote_console->beginRedirectOut(); PhutilConsole::setConsole($remote_console); } else if ($this->getArg('trace')) { $server = new PhutilConsoleServer(); $server->setEnableLog(true); $console = PhutilConsole::newConsoleForServer($server); PhutilConsole::setConsole($console); } return $this; } /* -( Reading Arguments )-------------------------------------------------- */ public function getArg($name) { if (empty($this->specs[$name])) { throw new PhutilArgumentSpecificationException( pht("No specification exists for argument '%s'!", $name)); } if (idx($this->results, $name) !== null) { return $this->results[$name]; } return $this->specs[$name]->getDefault(); } public function getUnconsumedArgumentVector() { return $this->argv; } /* -( Command Help )------------------------------------------------------- */ public function setSynopsis($synopsis) { $this->synopsis = $synopsis; return $this; } public function setTagline($tagline) { $this->tagline = $tagline; return $this; } public function printHelpAndExit() { echo $this->renderHelp(); exit(self::PARSE_ERROR_CODE); } public function renderHelp() { $out = array(); $more = array(); if ($this->bin) { $out[] = $this->format('**%s**', pht('NAME')); $name = $this->indent(6, '**%s**', basename($this->bin)); if ($this->tagline) { $name .= $this->format(' - '.$this->tagline); } $out[] = $name; $out[] = null; } if ($this->synopsis) { $out[] = $this->format('**%s**', pht('SYNOPSIS')); $out[] = $this->indent(6, $this->synopsis); $out[] = null; } if ($this->workflows) { $has_help = false; $out[] = $this->format('**%s**', pht('WORKFLOWS')); $out[] = null; $flows = $this->workflows; ksort($flows); foreach ($flows as $workflow) { if ($workflow->getName() == 'help') { $has_help = true; } $out[] = $this->renderWorkflowHelp( $workflow->getName(), $show_details = false); } if ($has_help) { $more[] = pht( 'Use **%s** __command__ for a detailed command reference.', 'help'); } } $specs = $this->renderArgumentSpecs($this->specs); if ($specs) { $out[] = $this->format('**%s**', pht('OPTION REFERENCE')); $out[] = null; $out[] = $specs; } // If we have standard options but no --show-standard-options, print out // a quick hint about it. if (!empty($this->specs['show-standard-options']) && !$this->getArg('show-standard-options')) { $more[] = pht( 'Use __%s__ to show additional options.', '--show-standard-options'); } $out[] = null; if ($more) { foreach ($more as $hint) { $out[] = $this->indent(0, $hint); } $out[] = null; } return implode("\n", $out); } public function renderWorkflowHelp( $workflow_name, $show_details = false) { $out = array(); $indent = ($show_details ? 0 : 6); $workflow = idx($this->workflows, strtolower($workflow_name)); if (!$workflow) { $out[] = $this->indent( $indent, pht('There is no **%s** workflow.', $workflow_name)); } else { $out[] = $this->indent($indent, $workflow->getExamples()); $out[] = $this->indent($indent, $workflow->getSynopsis()); if ($show_details) { $full_help = $workflow->getHelp(); if ($full_help) { $out[] = null; $out[] = $this->indent($indent, $full_help); } $specs = $this->renderArgumentSpecs($workflow->getArguments()); if ($specs) { $out[] = null; $out[] = $specs; } } } $out[] = null; return implode("\n", $out); } public function printUsageException(PhutilArgumentUsageException $ex) { $message = tsprintf( "**%s** %B\n", pht('Usage Exception:'), $ex->getMessage()); $this->logMessage($message); } private function logMessage($message) { fwrite(STDERR, $message); } /* -( Internals )---------------------------------------------------------- */ private function filterWildcardArgv(array $argv) { foreach ($argv as $key => $value) { if ($value == '--') { unset($argv[$key]); break; } else if ( is_string($value) && !strncmp($value, '-', 1) && strlen($value) > 1) { throw new PhutilArgumentUsageException( pht( "Argument '%s' is unrecognized. Use '%s' to indicate ". "the end of flags.", $value, '--')); } } return array_values($argv); } private function mergeSpecs(array $specs) { $short_map = mpull($this->specs, null, 'getShortAlias'); unset($short_map[null]); $wildcard = null; foreach ($this->specs as $spec) { if ($spec->getWildcard()) { $wildcard = $spec; break; } } foreach ($specs as $spec) { $spec->validate(); $name = $spec->getName(); if (isset($this->specs[$name])) { throw new PhutilArgumentSpecificationException( pht("Two argument specifications have the same name ('%s').", $name)); } $short = $spec->getShortAlias(); if ($short) { if (isset($short_map[$short])) { throw new PhutilArgumentSpecificationException( pht( "Two argument specifications have the same short alias ('%s').", $short)); } $short_map[$short] = $spec; } if ($spec->getWildcard()) { if ($wildcard) { throw new PhutilArgumentSpecificationException( pht( 'Two argument specifications are marked as wildcard arguments. '. 'You can have a maximum of one wildcard argument.')); } else { $wildcard = $spec; } } $this->specs[$name] = $spec; } foreach ($this->specs as $name => $spec) { foreach ($spec->getConflicts() as $conflict => $reason) { if (empty($this->specs[$conflict])) { throw new PhutilArgumentSpecificationException( pht( "Argument '%s' conflicts with unspecified argument '%s'.", $name, $conflict)); } if ($conflict == $name) { throw new PhutilArgumentSpecificationException( pht("Argument '%s' conflicts with itself!", $name)); } } } } private function renderArgumentSpecs(array $specs) { foreach ($specs as $key => $spec) { if ($spec->getWildcard()) { unset($specs[$key]); } } $out = array(); $no_standard_options = !empty($this->specs['show-standard-options']) && !$this->getArg('show-standard-options'); $specs = msort($specs, 'getName'); foreach ($specs as $spec) { if ($spec->getStandard() && $no_standard_options) { // If this is a standard argument and the user didn't pass // --show-standard-options, skip it. continue; } $name = $this->indent(6, '__--%s__', $spec->getName()); $short = null; if ($spec->getShortAlias()) { $short = $this->format(', __-%s__', $spec->getShortAlias()); } if ($spec->getParamName()) { $param = $this->format(' __%s__', $spec->getParamName()); $name .= $param; if ($short) { $short .= $param; } } $out[] = $name.$short; $out[] = $this->indent(10, $spec->getHelp()); $out[] = null; } return implode("\n", $out); } private function format($str /* , ... */) { $args = func_get_args(); return call_user_func_array( 'phutil_console_format', $args); } private function indent($level, $str /* , ... */) { $args = func_get_args(); $args = array_slice($args, 1); $text = call_user_func_array(array($this, 'format'), $args); return phutil_console_wrap($text, $level); } /** * @phutil-external-symbol function xhprof_disable */ public function shutdownProfiler() { $data = xhprof_disable(); $data = json_encode($data); Filesystem::writeFile($this->getArg('xprofile'), $data); } public static function isTraceModeEnabled() { return self::$traceModeEnabled; } private function raiseUnknownWorkflow($flow, array $maybe) { if ($maybe) { sort($maybe); $maybe_list = id(new PhutilConsoleList()) ->setWrap(false) ->setBullet(null) ->addItems($maybe) ->drawConsoleString(); $message = tsprintf( "%B\n%B", pht( 'Invalid command "%s". Did you mean:', $flow), $maybe_list); } else { $names = mpull($this->workflows, 'getName'); sort($names); $message = tsprintf( '%B', pht( 'Invalid command "%s". Valid commands are: %s.', $flow, implode(', ', $names))); } if (isset($this->workflows['help'])) { $binary = basename($this->bin); $message = tsprintf( "%B\n%s", $message, pht( 'For details on available commands, run `%s`.', "{$binary} help")); } throw new PhutilArgumentUsageException($message); } } diff --git a/src/phage/action/PhageAgentAction.php b/src/phage/action/PhageAgentAction.php index fab8329..1e996a4 100644 --- a/src/phage/action/PhageAgentAction.php +++ b/src/phage/action/PhageAgentAction.php @@ -1,183 +1,272 @@ 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); - foreach ($this->getActions() as $command) { - $key = 'command/'.$this->commandKey++; + $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); - $this->commands[$key] = array( + // 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, - ); - - $channel->write( - array( - 'type' => 'EXEC', - 'key' => $key, - 'command' => $command->getCommand()->getUnmaskedString(), - )); + 'command' => $command->getCommand()->getUnmaskedString(), + )); + + if ($throttle) { + $this->waitUntil = ($now + $throttle); } - $this->future = $future; - $this->channel = $channel; - $this->isActive = true; + 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); + if ($exit_code != 0) { $exit_code = $this->formatOutput( pht( 'Command ("%s") exited nonzero ("%s")!', $command->getCommand(), $exit_code), - $key.'/exit'); + $command->getLabel()); - fprintf(STDOUT, $exit_code); + fprintf(STDOUT, '%s', $exit_code); } - unset($this->commands[$key]); + unset($this->active[$key]); - if (!$this->commands) { + 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, $text); + 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 7eac6ec..fa477c2 100644 --- a/src/phage/action/PhageExecuteAction.php +++ b/src/phage/action/PhageExecuteAction.php @@ -1,31 +1,41 @@ 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 setExitCode($exit_code) { + $this->exitCode = $exit_code; + return $this; + } + + public function getExitCode() { + return $this->exitCode; + } + } diff --git a/src/xsprintf/csprintf.php b/src/xsprintf/csprintf.php index 448ac0a..ead744a 100644 --- a/src/xsprintf/csprintf.php +++ b/src/xsprintf/csprintf.php @@ -1,138 +1,140 @@ $pos + 1) ? $pattern[$pos + 1] : null; $is_unmasked = !empty($userdata['unmasked']); if (empty($userdata['mode'])) { $mode = PhutilCommandString::MODE_DEFAULT; } else { $mode = $userdata['mode']; } if ($value instanceof PhutilCommandString) { if ($is_unmasked) { $value = $value->getUnmaskedString(); } else { $value = $value->getMaskedString(); } } switch ($type) { case 'L': // Remove the L. $pattern = substr_replace($pattern, '', $pos, 1); $length = strlen($pattern); $type = 's'; // Check that the value is a non-empty array. if (!is_array($value)) { throw new InvalidArgumentException( pht('Expected an array for %%L%s conversion.', $next)); } switch ($next) { case 's': $values = array(); foreach ($value as $val) { $values[] = csprintf('%s', $val); } $value = implode(' ', $values); break; case 'R': $values = array(); foreach ($value as $val) { $values[] = csprintf('%R', $val); } $value = implode(' ', $values); break; default: throw new XsprintfUnknownConversionException("%L{$next}"); } break; case 'R': if (!preg_match('(^[a-zA-Z0-9:/@._+-]+$)', $value)) { $value = PhutilCommandString::escapeArgument($value, $mode); } $type = 's'; break; case 's': $value = PhutilCommandString::escapeArgument($value, $mode); $type = 's'; break; case 'P': if (!($value instanceof PhutilOpaqueEnvelope)) { throw new InvalidArgumentException( pht('Expected %s for %%P conversion.', 'PhutilOpaqueEnvelope')); } if ($is_unmasked) { $value = $value->openEnvelope(); } else { $value = '********'; } $value = PhutilCommandString::escapeArgument($value, $mode); $type = 's'; break; case 'C': $type = 's'; break; } $pattern[$pos] = $type; }