Lines 100.00% 11 / 11
Functions and Methods 100.00% 1 / 1
Classes and Traits 100.00% 1 / 1
Name Lines Functions and Methods CRAP Classes and Traits
AmpExternalCommandRunner 100.00% 11 / 11 100.00% 1 / 1 3 100.00% 1 / 1
 runCommand 100.00% 11 / 11 100.00% 1 / 1 3
1<?php
2
3declare(strict_types=1);
4
5namespace Stewart\Runtime\Broker\Deploy;
6
7use Amp\Cancellation;
8use Amp\CancelledException;
9use Amp\CompositeCancellation;
10use Amp\Future;
11use Amp\Process\Process;
12use Amp\TimeoutCancellation;
13use Stewart\Contracts\Time\Duration;
14use Stewart\Runtime\Exception\DeployError;
15use Stewart\Runtime\Exception\DeployException;
16
17use function Amp\async;
18use function Amp\ByteStream\buffer;
19
20final readonly class AmpExternalCommandRunner implements ExternalCommandRunner
21{
22    public function runCommand(array $command, Duration $timeout, Cancellation $cancellation): ExternalCommandResult
23    {
24        $process = Process::start($command);
25        $process->getStdin()->close();
26        // Both pipes drain while the command runs, so a chatty composer install cannot fill one and stall.
27        /** @var Future<string> $output */
28        $output = async(static fn(): string => buffer($process->getStdout()));
29        /** @var Future<string> $errorOutput */
30        $errorOutput = async(static fn(): string => buffer($process->getStderr()));
31
32        try {
33            $exitCode = $process->join(new CompositeCancellation($cancellation, new TimeoutCancellation($timeout->toSeconds())));
34        } catch (CancelledException $e) {
35            $process->kill();
36
37            if ($cancellation->isRequested()) {
38                throw $e;
39            }
40
41            throw DeployException::createForReason(DeployError::CommandTimedOut, $e, command: implode(' ', $command), timeout: (string) $timeout);
42        }
43
44        return new ExternalCommandResult($exitCode, $output->await(), $errorOutput->await());
45    }
46}