diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -78,10 +78,15 @@ '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', @@ -669,10 +674,15 @@ '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', diff --git a/src/phage/action/PhageAction.php b/src/phage/action/PhageAction.php new file mode 100644 --- /dev/null +++ b/src/phage/action/PhageAction.php @@ -0,0 +1,49 @@ +requireContainerAction(); + + return $this->actions; + } + + final public function addAction(PhageAction $action) { + $this->requireContainerAction(); + + $this->willAddAction($action); + + $this->actions[] = $action; + } + + protected function getAllWaitingChannels() { + if (!$this->isContainerAction()) { + throw new PhutilMethodNotImplementedException(); + } + + $channels = array(); + foreach ($this->getActions() as $action) { + foreach ($action->getAllWaitingChannels() as $channel) { + $channels[] = $channel; + } + } + + return $channels; + } + + private function requireContainerAction() { + if (!$this->isContainerAction()) { + throw new Exception(pht('This is not a container action.')); + } + } + +} diff --git a/src/phage/action/PhageAgentAction.php b/src/phage/action/PhageAgentAction.php new file mode 100644 --- /dev/null +++ b/src/phage/action/PhageAgentAction.php @@ -0,0 +1,183 @@ +isActive; + } + + 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->commands[$key] = array( + 'key' => $key, + 'command' => $command, + ); + + $channel->write( + array( + 'type' => 'EXEC', + 'key' => $key, + 'command' => $command->getCommand()->getUnmaskedString(), + )); + } + + $this->future = $future; + $this->channel = $channel; + $this->isActive = true; + } + + public function updateAgent() { + if (!$this->isActiveAgent()) { + return; + } + + $channel = $this->channel; + + while (true) { + $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']; + + if ($exit_code != 0) { + $exit_code = $this->formatOutput( + pht( + 'Command ("%s") exited nonzero ("%s")!', + $command->getCommand(), + $exit_code), + $key.'/exit'); + + fprintf(STDOUT, $exit_code); + } + + unset($this->commands[$key]); + + if (!$this->commands) { + $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!')); + } + } + } + } + + 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); + } + + 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 new file mode 100644 --- /dev/null +++ b/src/phage/action/PhageExecuteAction.php @@ -0,0 +1,31 @@ +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; + } + +} diff --git a/src/phage/action/PhageLocalAction.php b/src/phage/action/PhageLocalAction.php new file mode 100644 --- /dev/null +++ b/src/phage/action/PhageLocalAction.php @@ -0,0 +1,10 @@ +getAgents(); + foreach ($agents as $agent) { + $agent->startAgent(); + } + + while (true) { + $channels = $this->getAllWaitingChannels(); + PhutilChannel::waitForAny($channels); + + $agents = $this->getActiveAgents(); + if (!$agents) { + break; + } + + foreach ($agents as $agent) { + $agent->updateAgent(); + } + } + } + + protected function getAgents() { + return $this->getActions(); + } + + protected function getActiveAgents() { + $agents = $this->getAgents(); + + foreach ($agents as $key => $agent) { + if (!$agent->isActiveAgent()) { + unset($agents[$key]); + } + } + + return $agents; + } + + +}