Skip to content

Commit

Permalink
feat(shell): refactor Shell\execute to use async streams
Browse files Browse the repository at this point in the history
Signed-off-by: azjezz <azjezz@protonmail.com>
  • Loading branch information
azjezz committed Nov 3, 2021
1 parent f2e38d5 commit 0fab519
Show file tree
Hide file tree
Showing 5 changed files with 52 additions and 13 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -16,3 +16,4 @@
* refactored `Psl\IO` handles API.
* introduced a new `Psl\File` component.
* refactor `Psl\Filesystem\write_file`, `Psl\Filesystem\append_file`, and `Psl\Filesystem\read_file` to use `Psl\File` component.
* refactor `Psl\Shell\execute` to use `Psl\IO\Stream` component.
2 changes: 1 addition & 1 deletion docs/component/shell.md
Expand Up @@ -14,6 +14,6 @@

- [escape_argument](./../../src/Psl/Shell/escape_argument.php#L17)
- [escape_command](./../../src/Psl/Shell/escape_command.php#L14)
- [execute](./../../src/Psl/Shell/execute.php#L37)
- [execute](./../../src/Psl/Shell/execute.php#L40)


1 change: 1 addition & 0 deletions src/Psl/Internal/Loader.php
Expand Up @@ -582,6 +582,7 @@ final class Loader
'Psl\Shell\Exception\FailedExecutionException',
'Psl\Shell\Exception\RuntimeException',
'Psl\Shell\Exception\PossibleAttackException',
'Psl\Shell\Exception\TimeoutException',
'Psl\Math\Exception\ArithmeticException',
'Psl\Math\Exception\DivisionByZeroException',
'Psl\Filesystem\Exception\RuntimeException',
Expand Down
9 changes: 9 additions & 0 deletions src/Psl/Shell/Exception/TimeoutException.php
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Psl\Shell\Exception;

final class TimeoutException extends RuntimeException
{
}
52 changes: 40 additions & 12 deletions src/Psl/Shell/execute.php
Expand Up @@ -4,17 +4,18 @@

namespace Psl\Shell;

use Psl\Async;
use Psl\Dict;
use Psl\Env;
use Psl\IO;
use Psl\IO\Stream;
use Psl\Str;
use Psl\Vec;

use function fclose;
use function is_dir;
use function is_resource;
use function proc_close;
use function proc_open;
use function stream_get_contents;

/**
* Execute an external program.
Expand All @@ -33,16 +34,29 @@
* @throws Exception\FailedExecutionException In case the command resulted in an exit code other than 0.
* @throws Exception\PossibleAttackException In case the command being run is suspicious ( e.g: contains NULL byte ).
* @throws Exception\RuntimeException In case $working_directory doesn't exist, or unable to create a new process.
* @throws Exception\TimeoutException If $timeout_ms is reached before being able to read the process stream.
* @throws IO\Exception\BlockingException If unable to set the process stream to non-blocking mode.
*/
function execute(
string $command,
array $arguments = [],
string $command,
array $arguments = [],
?string $working_directory = null,
array $environment = [],
bool $escape_arguments = true
array $environment = [],
bool $escape_arguments = true,
?int $timeout_ms = null
): string {
if ($escape_arguments) {
$arguments = Vec\map($arguments, static fn(string $argument): string => escape_argument($argument));
$arguments = Vec\map(
$arguments,
/**
* @param string $argument
*
* @return string
*
* @pure
*/
static fn(string $argument): string => escape_argument($argument)
);
}

$commandline = Str\join([$command, ...$arguments], ' ');
Expand Down Expand Up @@ -71,13 +85,27 @@ function execute(
}
// @codeCoverageIgnoreEnd

$stdout_content = stream_get_contents($pipes[1]);
$stderr_content = stream_get_contents($pipes[2]);
$stdout = new Stream\StreamCloseReadHandle($pipes[1]);
$stderr = new Stream\StreamCloseReadHandle($pipes[2]);

fclose($pipes[1]);
fclose($pipes[2]);
try {
[$stdout_content, $stderr_content] = Async\concurrently([
static fn(): string => $stdout->readAll(timeout_ms: $timeout_ms),
static fn(): string => $stderr->readAll(timeout_ms: $timeout_ms),
])->await();
} catch (IO\Exception\TimeoutException $previous) {
// @ignoreCodeCoverageStart
throw new Exception\TimeoutException('reached timeout while the process output is still not readable.', 0, $previous);
// @ignoreCodeCoverageEnd
} finally {
/** @psalm-suppress MissingThrowsDocblock */
$stdout->close();
/** @psalm-suppress MissingThrowsDocblock */
$stderr->close();

$code = proc_close($process);
}

$code = proc_close($process);
if ($code !== 0) {
throw new Exception\FailedExecutionException($commandline, $stdout_content, $stderr_content, $code);
}
Expand Down

0 comments on commit 0fab519

Please sign in to comment.