Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Had issues on windows - fixed them + added custom descriptors support to Shell #77

Merged
merged 7 commits into from
Feb 11, 2023
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ composer.lock
clover.xml
coverage.xml
phpcs.xml
.phpunit.result.cache
2 changes: 1 addition & 1 deletion phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
processIsolation="true"
stopOnFailure="false"
bootstrap="tests/bootstrap.php"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd">
Expand Down
81 changes: 67 additions & 14 deletions src/Helper/Shell.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@ class Shell
const STATE_CLOSED = 'closed';
const STATE_TERMINATED = 'terminated';

const DEFAULT_STDIN_WIN = ['pipe', 'r'];
const DEFAULT_STDIN_NIX = ['pipe', 'r'];

const DEFAULT_STDOUT_WIN = ['pipe', 'w'];
const DEFAULT_STDOUT_NIX = ['pipe', 'w'];

const DEFAULT_STDERR_WIN = ['pipe', 'w'];
const DEFAULT_STDERR_NIX = ['pipe', 'w'];

/** @var bool Whether to wait for the process to finish or return instantly */
protected bool $async = false;

Expand Down Expand Up @@ -98,25 +107,40 @@ public function __construct(protected string $command, protected ?string $input
$this->input = $input;
}

protected function getDescriptors(): array
protected function prepareDescriptors(?array $stdin = null, ?array $stdout = null, ?array $stderr = null): array
{
$out = $this->isWindows() ? ['file', 'NUL', 'w'] : ['pipe', 'w'];

$win = $this->isWindows();
if (!$stdin) {
$stdin = $win ? self::DEFAULT_STDIN_WIN : self::DEFAULT_STDIN_NIX;
}
if (!$stdout) {
$stdout = $win ? self::DEFAULT_STDOUT_WIN : self::DEFAULT_STDOUT_NIX;
}
if (!$stderr) {
$stderr = $win ? self::DEFAULT_STDERR_WIN : self::DEFAULT_STDERR_NIX;
}
return [
self::STDIN_DESCRIPTOR_KEY => ['pipe', 'r'],
self::STDOUT_DESCRIPTOR_KEY => $out,
self::STDERR_DESCRIPTOR_KEY => $out,
self::STDIN_DESCRIPTOR_KEY => $stdin,
self::STDOUT_DESCRIPTOR_KEY => $stdout,
self::STDERR_DESCRIPTOR_KEY => $stderr,
];
}

protected function isWindows(): bool
{
return '\\' === DIRECTORY_SEPARATOR;
// If PHP_OS is defined, use it - More reliable:
if (defined('PHP_OS')) {
return 'WIN' === strtoupper(substr(PHP_OS, 0, 3)); // May be 'WINNT' or 'WIN32' or 'Windows'
}
return '\\' === DIRECTORY_SEPARATOR; // Fallback - Less reliable (Windows 7...)
}

protected function setInput(): void
{
fwrite($this->pipes[self::STDIN_DESCRIPTOR_KEY], $this->input ?? '');
//Make sure the pipe is a stream resource before writing to it to avoid a warning
if (is_resource($this->pipes[self::STDIN_DESCRIPTOR_KEY])) {
fwrite($this->pipes[self::STDIN_DESCRIPTOR_KEY], $this->input ?? '');
}
}

protected function updateProcessStatus(): void
Expand All @@ -132,9 +156,16 @@ protected function updateProcessStatus(): void

protected function closePipes(): void
{
fclose($this->pipes[self::STDIN_DESCRIPTOR_KEY]);
fclose($this->pipes[self::STDOUT_DESCRIPTOR_KEY]);
fclose($this->pipes[self::STDERR_DESCRIPTOR_KEY]);
//Make sure the pipe are a stream resource before closing them to avoid a warning
if (is_resource($this->pipes[self::STDIN_DESCRIPTOR_KEY])) {
fclose($this->pipes[self::STDIN_DESCRIPTOR_KEY]);
}
if (is_resource($this->pipes[self::STDOUT_DESCRIPTOR_KEY])) {
fclose($this->pipes[self::STDOUT_DESCRIPTOR_KEY]);
}
if (is_resource($this->pipes[self::STDERR_DESCRIPTOR_KEY])) {
fclose($this->pipes[self::STDERR_DESCRIPTOR_KEY]);
}
}

protected function wait(): ?int
Expand Down Expand Up @@ -177,14 +208,25 @@ public function setOptions(

return $this;
}

public function execute(bool $async = false): self

/**
* execute
* Execute the command with optional stdin, stdout and stderr which override the defaults
* If async is set to true, the process will be executed in the background
*
* @param bool $async - default false
* @param ?array $stdin - default null (loads default descriptor)
* @param ?array $stdout - default null (loads default descriptor)
* @param ?array $stderr - default null (loads default descriptor)
* @return self
*/
public function execute(bool $async = false, ?array $stdin = null, ?array $stdout = null, ?array $stderr = null): self
{
if ($this->isRunning()) {
throw new RuntimeException('Process is already running.');
}

$this->descriptors = $this->getDescriptors();
$this->descriptors = $this->prepareDescriptors($stdin, $stdout, $stderr);
$this->processStartTime = microtime(true);

$this->process = proc_open(
Expand Down Expand Up @@ -218,6 +260,10 @@ public function execute(bool $async = false): self

private function setOutputStreamNonBlocking(): bool
{
// Make sure the pipe is a stream resource before setting it to non-blocking to avoid a warning
if (!is_resource($this->pipes[self::STDOUT_DESCRIPTOR_KEY])) {
return false;
}
return stream_set_blocking($this->pipes[self::STDOUT_DESCRIPTOR_KEY], false);
}

Expand All @@ -228,11 +274,18 @@ public function getState(): string

public function getOutput(): string
{
// Make sure the pipe is a stream resource before reading it to avoid a warning
if (!is_resource($this->pipes[self::STDOUT_DESCRIPTOR_KEY])) {
return '';
}
return stream_get_contents($this->pipes[self::STDOUT_DESCRIPTOR_KEY]);
}

public function getErrorOutput(): string
{
if (!is_resource($this->pipes[self::STDERR_DESCRIPTOR_KEY])) {
return '';
}
return stream_get_contents($this->pipes[self::STDERR_DESCRIPTOR_KEY]);
}

Expand Down
13 changes: 9 additions & 4 deletions tests/ApplicationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,19 @@ class ApplicationTest extends TestCase

public function setUp(): void
{
file_put_contents(static::$in, '');
file_put_contents(static::$ou, '');
file_put_contents(static::$in, '', LOCK_EX);
file_put_contents(static::$ou, '', LOCK_EX);
}

public function tearDown(): void
{
unlink(static::$in);
unlink(static::$ou);
// Make sure we clean up after ourselves:
if (file_exists(static::$in)) {
unlink(static::$in);
}
if (file_exists(static::$ou)) {
unlink(static::$ou);
}
}

public function test_new()
Expand Down
7 changes: 5 additions & 2 deletions tests/CliTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public function setUp(): void
{
ob_start();
StreamInterceptor::$buffer = '';
file_put_contents(static::$ou, '');
file_put_contents(static::$ou, '', LOCK_EX);
}

public function tearDown(): void
Expand All @@ -46,7 +46,10 @@ public function tearDown(): void

public static function tearDownAfterClass(): void
{
unlink(static::$ou);
// Make sure we clean up after ourselves:
if (file_exists(static::$ou)) {
unlink(static::$ou);
}
}

public function buffer()
Expand Down
7 changes: 5 additions & 2 deletions tests/Helper/OutputHelperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,15 @@ class OutputHelperTest extends TestCase

public function setUp(): void
{
file_put_contents(static::$ou, '');
file_put_contents(static::$ou, '', LOCK_EX);
}

public static function tearDownAfterClass(): void
{
unlink(static::$ou);
// Make sure we clean up after ourselves:
if (file_exists(static::$ou)) {
unlink(static::$ou);
}
}

public function test_show_arguments()
Expand Down
4 changes: 2 additions & 2 deletions tests/Helper/ShellTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public function test_get_output()

$shell->execute();

$this->assertSame("hello\n", $shell->getOutput());
$this->assertSame("hello", trim($shell->getOutput())); // trim to remove trailing newline which is OS dependent
$this->assertSame(0, $shell->getExitCode());
}

Expand All @@ -35,7 +35,7 @@ public function test_get_process_id()
$shell->execute(true);

$this->assertIsInt($pid = $shell->getProcessId());
$this->assertGreaterThan(getmypid(), $pid);
// $this->assertGreaterThan(getmypid(), $pid); // this is not always true especially on windows
}

public function test_async_stop()
Expand Down
15 changes: 10 additions & 5 deletions tests/IO/InteractorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,19 @@ class InteractorTest extends TestCase

public function setUp(): void
{
file_put_contents(static::$in, '');
file_put_contents(static::$ou, '');
file_put_contents(static::$in, '', LOCK_EX);
file_put_contents(static::$ou, '', LOCK_EX);
}

public function tearDown(): void
{
unlink(static::$in);
unlink(static::$ou);
// Make sure we clean up after ourselves:
if (file_exists(static::$in)) {
unlink(static::$in);
}
if (file_exists(static::$ou)) {
unlink(static::$ou);
}
}

public function test_components()
Expand Down Expand Up @@ -152,7 +157,7 @@ public function test_call()

protected function newInteractor(string $in = '')
{
file_put_contents(static::$in, $in);
file_put_contents(static::$in, $in, LOCK_EX);

return new Interactor(static::$in, static::$ou);
}
Expand Down
9 changes: 6 additions & 3 deletions tests/Input/ReaderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,15 @@ class ReaderTest extends TestCase

public function setUp(): void
{
file_put_contents(static::$in, '');
file_put_contents(static::$in, '', LOCK_EX);
}

public static function tearDownAfterClass(): void
{
unlink(static::$in);
// Make sure we clean up after ourselves:
if (file_exists(static::$in)) {
unlink(static::$in);
}
}

public function test_default()
Expand All @@ -38,7 +41,7 @@ public function test_default()

public function test_callback()
{
file_put_contents(static::$in, 'the value');
file_put_contents(static::$in, 'the value', LOCK_EX);

$r = new Reader(static::$in);

Expand Down
9 changes: 5 additions & 4 deletions tests/Output/ColorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,13 @@ public function test_colors()
{
$c = new Color;

$this->assertSame("\nabc\n", $c->colors('<eol>abc</eol>'));
// We use PHP_EOL here because it is platform dependent and eol tag will be replaced by it.
$this->assertSame(PHP_EOL."abc".PHP_EOL, $c->colors('<eol>abc</eol>'));
$this->assertSame("\033[0;31mRed\033[0m", $c->colors('<red>Red</end>'));
$this->assertSame("\033[1;31mBoldRed\n\033[0m", $c->colors('<boldRed>BoldRed<eol/></end>'));
$this->assertSame("\033[0;36;42mBgGreenCyan\033[0m\n", $c->colors('<bgGreenCyan>BgGreenCyan</end><eol>'));
$this->assertSame("\033[1;31mBoldRed".PHP_EOL."\033[0m", $c->colors('<boldRed>BoldRed<eol/></end>'));
$this->assertSame("\033[0;36;42mBgGreenCyan\033[0m".PHP_EOL, $c->colors('<bgGreenCyan>BgGreenCyan</end><eol>'));
$this->assertSame(
"\033[0;31mRed\033[0m\nNormal\n\033[1;37mBOLD\033[0m",
"\033[0;31mRed\033[0m".PHP_EOL."Normal".PHP_EOL."\033[1;37mBOLD\033[0m",
$c->colors("<red>Red</end>\r\nNormal\n<bold>BOLD</end>")
);
}
Expand Down