diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..da20d18 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,7 @@ +/.gitattributes export-ignore +/.github/workflows/ export-ignore +/.gitignore export-ignore +/examples/ export-ignore +/phpunit.xml.dist export-ignore +/phpunit.xml.legacy export-ignore +/tests/ export-ignore diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..9c09fb8 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: clue +custom: https://clue.engineering/support diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b521e6d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,37 @@ +name: CI + +on: + push: + pull_request: + +jobs: + PHPUnit: + name: PHPUnit (PHP ${{ matrix.php }}) + runs-on: ubuntu-22.04 + strategy: + matrix: + php: + - 8.3 + - 8.2 + - 8.1 + - 8.0 + - 7.4 + - 7.3 + - 7.2 + - 7.1 + - 7.0 + - 5.6 + - 5.5 + - 5.4 + - 5.3 + steps: + - uses: actions/checkout@v4 + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: xdebug + - run: composer install + - run: vendor/bin/phpunit --coverage-text + if: ${{ matrix.php >= 7.3 }} + - run: vendor/bin/phpunit --coverage-text -c phpunit.xml.legacy + if: ${{ matrix.php < 7.3 }} diff --git a/.gitignore b/.gitignore index de4a392..4fbb073 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -/vendor +/vendor/ /composer.lock diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 9fbb7fa..0000000 --- a/.travis.yml +++ /dev/null @@ -1,27 +0,0 @@ -language: php - -php: -# - 5.3 # requires old distro, see below - - 5.4 - - 5.5 - - 5.6 - - 7.0 - - 7.1 - - 7.2 - - 7.3 - -# lock distro so future defaults will not break the build -dist: trusty - -matrix: - include: - - php: 5.3 - dist: precise - -sudo: false - -install: - - composer install --no-interaction - -script: - - vendor/bin/phpunit --coverage-text diff --git a/README.md b/README.md index 339a1ac..4708425 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ -# clue/reactphp-shell [![Build Status](https://travis-ci.org/clue/reactphp-shell.svg?branch=master)](https://travis-ci.org/clue/reactphp-shell) +# clue/reactphp-shell + +[![CI status](https://github.com/clue/reactphp-shell/actions/workflows/ci.yml/badge.svg)](https://github.com/clue/reactphp-shell/actions) +[![installs on Packagist](https://img.shields.io/packagist/dt/clue/shell-react?color=blue&label=installs%20on%20Packagist)](https://packagist.org/packages/clue/shell-react) Run async commands within any interactive shell command, built on top of [ReactPHP](https://reactphp.org/). @@ -10,25 +13,30 @@ Once [installed](#install), you can use the following code to run an interactive bash shell and issue some commands within: ```php -$loop = React\EventLoop\Factory::create(); -$launcher = new ProcessLauncher($loop); +createDeferredShell('bash'); $shell->execute('echo -n $USER')->then(function ($result) { var_dump('current user', $result); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); $shell->execute('env | sort | head -n10')->then(function ($env) { var_dump('env', $env); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); $shell->end(); - -$loop->run(); ``` -See also the [examples](examples): +See also the [examples](examples/): * [Run shell commands within a bash shell](examples/bash.php) * [Run PHP code within an interactive PHP shell](examples/php.php) @@ -36,34 +44,34 @@ See also the [examples](examples): ## Install -The recommended way to install this library is [through Composer](https://getcomposer.org). +The recommended way to install this library is [through Composer](https://getcomposer.org/). [New to Composer?](https://getcomposer.org/doc/00-intro.md) This will install the latest supported version: ```bash -$ composer require clue/shell-react:^0.2 +composer require clue/shell-react:^0.2 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. This project aims to run on any platform and thus does not require any PHP -extensions and supports running on legacy PHP 5.3 through current PHP 7+. -It's *highly recommended to use PHP 7+* for this project. +extensions and supports running on legacy PHP 5.3 through current PHP 8+. +It's *highly recommended to use the latest supported PHP version* for this project. ## Tests To run the test suite, you first need to clone this repo and then install all -dependencies [through Composer](https://getcomposer.org): +dependencies [through Composer](https://getcomposer.org/): ```bash -$ composer install +composer install ``` To run the test suite, go to the project root and run: ```bash -$ php vendor/bin/phpunit +vendor/bin/phpunit ``` ## License diff --git a/composer.json b/composer.json index 63c3095..d8f087e 100644 --- a/composer.json +++ b/composer.json @@ -11,16 +11,23 @@ } ], "autoload": { - "psr-4" : { "Clue\\React\\Shell\\": "src/" } + "psr-4" : { + "Clue\\React\\Shell\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Clue\\Tests\\React\\Shell\\": "tests/" + } }, "require": { "php": ">=5.3", - "react/child-process": "^0.5 || ^0.4 || ^0.3", - "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3", - "react/stream": "^1.0 || ^0.7 || ^0.6 || ^0.5 || ^0.4.6", - "react/promise": "^2.0 || ^1.0" + "react/child-process": "^0.6.3", + "react/event-loop": "^1.2", + "react/stream": "^1.2", + "react/promise": "^3.0 || ^2.0 || ^1.0" }, "require-dev": { - "phpunit/phpunit": "^7.0 || ^6.0 || ^5.0 || ^4.8.35" + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" } } diff --git a/examples/bash.php b/examples/bash.php index 5bd971f..bf7c190 100644 --- a/examples/bash.php +++ b/examples/bash.php @@ -1,23 +1,21 @@ createDeferredShell('bash 2>&1'); $shell->execute('echo -n $USER')->then(function ($result) { var_dump('current user', $result); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); $shell->execute('env | sort | head -n10')->then(function ($env) { var_dump('env', $env); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); $shell->end(); - -$loop->run(); diff --git a/examples/docker.php b/examples/docker.php index 650ba09..ac1a5c1 100644 --- a/examples/docker.php +++ b/examples/docker.php @@ -1,23 +1,21 @@ createDeferredShell('docker run -i --rm debian bash'); $shell->execute('id')->then(function ($result) { var_dump('current user', $result); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); $shell->execute('env')->then(function ($env) { var_dump('env', $env); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); $shell->end(); - -$loop->run(); diff --git a/examples/php.php b/examples/php.php index a2a70f7..1f71dc3 100644 --- a/examples/php.php +++ b/examples/php.php @@ -1,12 +1,8 @@ createDeferredShell('php -a'); $shell->setBounding("echo '{{ bounding }}';"); @@ -21,8 +17,8 @@ CODE )->then(function ($output) { echo 'Program output: ' . PHP_EOL . $output . PHP_EOL; +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); $shell->end(); - -$loop->run(); diff --git a/phpunit.xml.dist b/phpunit.xml.dist index b3f90f3..7a7aa2c 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,19 +1,23 @@ - + + convertDeprecationsToExceptions="true"> ./tests/ - - + + ./src/ - - - \ No newline at end of file + + + + + + diff --git a/phpunit.xml.legacy b/phpunit.xml.legacy new file mode 100644 index 0000000..45513ae --- /dev/null +++ b/phpunit.xml.legacy @@ -0,0 +1,21 @@ + + + + + + + ./tests/ + + + + + ./src/ + + + + + + diff --git a/src/ProcessLauncher.php b/src/ProcessLauncher.php index c32f248..27ce977 100644 --- a/src/ProcessLauncher.php +++ b/src/ProcessLauncher.php @@ -3,17 +3,28 @@ namespace Clue\React\Shell; use React\ChildProcess\Process; +use React\EventLoop\Loop; use React\EventLoop\LoopInterface; use Clue\React\Shell\DeferredShell; use React\Stream\CompositeStream; class ProcessLauncher { + /** @var LoopInterface */ private $loop; - public function __construct(LoopInterface $loop) + /** + * This class takes an optional `LoopInterface|null $loop` parameter that can be used to + * pass the event loop instance to use for this object. You can use a `null` value + * here in order to use the [default loop](https://github.com/reactphp/event-loop#loop). + * This value SHOULD NOT be given unless you're sure you want to explicitly use a + * given event loop instance. + * + * @param ?LoopInterface $loop + */ + public function __construct(LoopInterface $loop = null) { - $this->loop = $loop; + $this->loop = $loop ?: Loop::get(); } /** @@ -41,7 +52,7 @@ public function createDeferredShell($process) // forcefully terminate process when stream closes $stream->on('close', function () use ($process) { if ($process->isRunning()) { - $process->terminate(SIGKILL); + $process->terminate(defined('SIGKILL') ? SIGKILL : null); } }); diff --git a/tests/DeferredShellTest.php b/tests/DeferredShellTest.php index 8fd8ad4..5825b33 100644 --- a/tests/DeferredShellTest.php +++ b/tests/DeferredShellTest.php @@ -1,12 +1,17 @@ stream = $this->getMockBuilder('React\Stream\DuplexStreamInterface')->getMock(); } diff --git a/tests/FunctionalTest.php b/tests/FunctionalTest.php index 7d37099..1434549 100644 --- a/tests/FunctionalTest.php +++ b/tests/FunctionalTest.php @@ -1,5 +1,7 @@ loop = Factory::create(); $this->launcher = new ProcessLauncher($this->loop); diff --git a/tests/ProcessLauncherTest.php b/tests/ProcessLauncherTest.php index 89f912f..364de12 100644 --- a/tests/ProcessLauncherTest.php +++ b/tests/ProcessLauncherTest.php @@ -1,5 +1,7 @@ loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $this->processLauncher = new ProcessLauncher($this->loop); } + public function testConstructWithoutLoopAssignsLoopAutomatically() + { + $launcher = new ProcessLauncher(); + + $ref = new \ReflectionProperty($launcher, 'loop'); + $ref->setAccessible(true); + $loop = $ref->getValue($launcher); + + $this->assertInstanceOf('React\EventLoop\LoopInterface', $loop); + } + public function testProcessWillBeStarted() { $process = $this->getMockBuilder('React\ChildProcess\Process')->disableOriginalConstructor()->getMock(); @@ -36,7 +52,7 @@ public function testClosingStreamTerminatesRunningProcess() $process->stdin->expects($this->any())->method('isWritable')->willReturn(true); $process->expects($this->once())->method('isRunning')->will($this->returnValue(true)); - $process->expects($this->once())->method('terminate')->with($this->equalTo(SIGKILL)); + $process->expects($this->once())->method('terminate')->with($this->equalTo(defined('SIGKILL') ? SIGKILL : null)); $shell = $this->processLauncher->createDeferredShell($process); diff --git a/tests/bootstrap.php b/tests/TestCase.php similarity index 83% rename from tests/bootstrap.php rename to tests/TestCase.php index 3e3aaee..052104a 100644 --- a/tests/bootstrap.php +++ b/tests/TestCase.php @@ -1,13 +1,11 @@ getMockBuilder('stdClass')->setMethods(array('__invoke'))->getMock(); + if (method_exists('PHPUnit\Framework\MockObject\MockBuilder', 'addMethods')) { + // PHPUnit 9+ + return $this->getMockBuilder('stdClass')->addMethods(array('__invoke'))->getMock(); + } else { + // legacy PHPUnit 4 - PHPUnit 8 + return $this->getMockBuilder('stdClass')->setMethods(array('__invoke'))->getMock(); + } } protected function expectPromiseResolve($promise)