├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json ├── composer.lock ├── examples ├── blocking-download.php └── non-blocking-download.php ├── phpunit.xml.dist ├── src ├── FutureProcess.php ├── FutureResult.php ├── FutureValue.php ├── Pipes.php ├── Shell.php └── TimeoutException.php └── tests ├── IntegrationTest.php └── bootstrap.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /nbproject/ 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.3 5 | - 5.4 6 | - 5.5 7 | - 5.6 8 | - 7.0 9 | - hhvm 10 | - hhvm-nightly 11 | 12 | matrix: 13 | fast_finish: true 14 | allow_failures: 15 | - php: hhvm-nightly 16 | 17 | sudo: false 18 | 19 | before_install: 20 | - if [[ -n $GITHUB_TOKEN ]]; then composer config -g github-oauth.github.com $GITHUB_TOKEN; fi 21 | - composer self-update 22 | 23 | install: composer install --no-interaction 24 | 25 | script: php vendor/bin/phpunit --coverage-clover build/logs/clover.xml 26 | 27 | cache: 28 | directories: vendor 29 | 30 | after_success: bash <(curl -s https://codecov.io/bash) 31 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | This project adheres to [Semantic Versioning](http://semver.org/). 4 | 5 | ## [Unreleased] 6 | ### Added 7 | - Add optional `$length` param to `readFromPipe()` methods to allow reading pipe without fully draining buffer. 8 | 9 | ### Changed 10 | - Rename `readFromBuffer()` and `writeToBuffer()` methods to `readFromPipe()` and `writeToPipe()` respectively. 11 | 12 | ### Removed 13 | - Remove unused `$onProgress` param from `then()` methods. 14 | 15 | ## [0.2.0] - 2015-04-06 16 | ### Added 17 | - Automatic buffering of child process i/o to prevent child processes becoming blocked by filled output buffers. 18 | - Process timeout functionality. 19 | - Support for PHP 7.0 and HHVM. 20 | 21 | ### Changed 22 | - Replace `FutureProcess::kill()` and `detach()` with `abort()`. 23 | - Change `Shell::startProcess()` params to `string $commandLine, array $options = []`. 24 | - Move i/o functionality into new `Pipes` class to reduce complexity of `FutureProcess` class. 25 | 26 | ## 0.1.0 - 2015-03-01 27 | ### Added 28 | - `Shell` class for parallel execution of command lines with automatic queueing of commands. 29 | - `FutureProcess` and `FutureResult` classes for mixed asynchronous and synchronous interfaces to child processes. 30 | - Support for PHP 5.3, 5.4, 5.5 and 5.6. 31 | 32 | [unreleased]: https://github.com/joshdifabio/future-process/compare/v0.2.0...HEAD 33 | [0.2.0]: https://github.com/joshdifabio/future-process/compare/v0.1.0...v0.2.0 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Joshua Di Fabio 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Future Process 2 | 3 | [![Build Status](https://img.shields.io/travis/joshdifabio/future-process.svg?style=flat-square)](https://travis-ci.org/joshdifabio/future-process) 4 | [![Codecov](https://img.shields.io/codecov/c/github/joshdifabio/future-process.svg?style=flat-square)](https://codecov.io/github/joshdifabio/future-process) 5 | [![Code Quality](https://img.shields.io/scrutinizer/g/joshdifabio/future-process.svg?style=flat-square)](https://scrutinizer-ci.com/g/joshdifabio/future-process/) 6 | 7 | ## Introduction 8 | 9 | Future Process is object-oriented `proc_open` with an asynchronous API and automatic queueing of commands. 10 | 11 | ## Usage 12 | 13 | ```php 14 | // we use Shell to start new processes 15 | $shell = new \FutureProcess\Shell; 16 | 17 | // run a maximum of 5 concurrent processes - additional ones will be queued 18 | $shell->setProcessLimit(5); 19 | 20 | // let's download this package's license file from GitHub using wget 21 | $url = 'https://raw.githubusercontent.com/joshdifabio/future-process/master/LICENSE'; 22 | $process = $shell->startProcess("wget -O - $url"); 23 | ``` 24 | 25 | ### Non-blocking 26 | 27 | We can consume the process output using [promises](https://github.com/reactphp/promise). 28 | 29 | ```php 30 | // this will not block, even if the process is queued 31 | $process->then(function ($process) { 32 | echo "Downloading file...\n"; 33 | }); 34 | 35 | // this will not block, even if the process is queued 36 | $process->getResult()->then(function ($result) { 37 | echo "File contents:\n{$result->readFromPipe(1)}\n"; 38 | }); 39 | 40 | // this will block until all processes have exited 41 | $shell->wait(); 42 | ``` 43 | 44 | ### Blocking 45 | 46 | We can also consume the process output synchronously. 47 | 48 | ```php 49 | // this will block until the process starts 50 | $process->wait(); 51 | echo "Downloading file...\n"; 52 | 53 | // this will block until the process exits 54 | echo "File contents:\n{$process->getResult()->readFromPipe(1)}\n"; 55 | ``` 56 | 57 | ## Installation 58 | 59 | Install Future Process using [composer](https://getcomposer.org/). 60 | 61 | ``` 62 | composer require joshdifabio/future-process 63 | ``` 64 | 65 | ## License 66 | 67 | Future Process is released under the [MIT](https://github.com/joshdifabio/future-process/blob/master/LICENSE) license. 68 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "joshdifabio/future-process", 3 | "description": "Process execution for PHP using futures and promises", 4 | "keywords": ["process", "child-process", "promise", "react"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Josh Di Fabio", 9 | "email": "joshdifabio@gmail.com" 10 | } 11 | ], 12 | "require": { 13 | "php": ">=5.3.0", 14 | "react/promise": "~1.0|~2.0" 15 | }, 16 | "require-dev": { 17 | "phpunit/phpunit": "~3.5|~4.0", 18 | "symfony/process": "~2.2" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "FutureProcess\\": "src" 23 | } 24 | }, 25 | "autoload-dev": { 26 | "psr-4": { 27 | "FutureProcess\\": "tests" 28 | } 29 | }, 30 | "extra": { 31 | "branch-alias": { 32 | "dev-master": "0.3-dev" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", 5 | "This file is @generated automatically" 6 | ], 7 | "hash": "bea2eef8393124b463186d48bd118741", 8 | "packages": [ 9 | { 10 | "name": "react/promise", 11 | "version": "v1.0.4", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/reactphp/promise.git", 15 | "reference": "d6de8cae1dbb4878d909c41cb89aff764504472c" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/reactphp/promise/zipball/d6de8cae1dbb4878d909c41cb89aff764504472c", 20 | "reference": "d6de8cae1dbb4878d909c41cb89aff764504472c", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "php": ">=5.3.3" 25 | }, 26 | "type": "library", 27 | "extra": { 28 | "branch-alias": { 29 | "dev-master": "1.0-dev" 30 | } 31 | }, 32 | "autoload": { 33 | "psr-0": { 34 | "React\\Promise": "src/" 35 | } 36 | }, 37 | "notification-url": "https://packagist.org/downloads/", 38 | "license": [ 39 | "MIT" 40 | ], 41 | "authors": [ 42 | { 43 | "name": "Jan Sorgalla", 44 | "email": "jsorgalla@googlemail.com", 45 | "homepage": "http://sorgalla.com", 46 | "role": "maintainer" 47 | } 48 | ], 49 | "description": "A lightweight implementation of CommonJS Promises/A for PHP", 50 | "time": "2013-04-03 14:05:55" 51 | } 52 | ], 53 | "packages-dev": [ 54 | { 55 | "name": "doctrine/instantiator", 56 | "version": "1.0.4", 57 | "source": { 58 | "type": "git", 59 | "url": "https://github.com/doctrine/instantiator.git", 60 | "reference": "f976e5de371104877ebc89bd8fecb0019ed9c119" 61 | }, 62 | "dist": { 63 | "type": "zip", 64 | "url": "https://api.github.com/repos/doctrine/instantiator/zipball/f976e5de371104877ebc89bd8fecb0019ed9c119", 65 | "reference": "f976e5de371104877ebc89bd8fecb0019ed9c119", 66 | "shasum": "" 67 | }, 68 | "require": { 69 | "php": ">=5.3,<8.0-DEV" 70 | }, 71 | "require-dev": { 72 | "athletic/athletic": "~0.1.8", 73 | "ext-pdo": "*", 74 | "ext-phar": "*", 75 | "phpunit/phpunit": "~4.0", 76 | "squizlabs/php_codesniffer": "2.0.*@ALPHA" 77 | }, 78 | "type": "library", 79 | "extra": { 80 | "branch-alias": { 81 | "dev-master": "1.0.x-dev" 82 | } 83 | }, 84 | "autoload": { 85 | "psr-0": { 86 | "Doctrine\\Instantiator\\": "src" 87 | } 88 | }, 89 | "notification-url": "https://packagist.org/downloads/", 90 | "license": [ 91 | "MIT" 92 | ], 93 | "authors": [ 94 | { 95 | "name": "Marco Pivetta", 96 | "email": "ocramius@gmail.com", 97 | "homepage": "http://ocramius.github.com/" 98 | } 99 | ], 100 | "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", 101 | "homepage": "https://github.com/doctrine/instantiator", 102 | "keywords": [ 103 | "constructor", 104 | "instantiate" 105 | ], 106 | "time": "2014-10-13 12:58:55" 107 | }, 108 | { 109 | "name": "phpdocumentor/reflection-docblock", 110 | "version": "2.0.4", 111 | "source": { 112 | "type": "git", 113 | "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", 114 | "reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8" 115 | }, 116 | "dist": { 117 | "type": "zip", 118 | "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/d68dbdc53dc358a816f00b300704702b2eaff7b8", 119 | "reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8", 120 | "shasum": "" 121 | }, 122 | "require": { 123 | "php": ">=5.3.3" 124 | }, 125 | "require-dev": { 126 | "phpunit/phpunit": "~4.0" 127 | }, 128 | "suggest": { 129 | "dflydev/markdown": "~1.0", 130 | "erusev/parsedown": "~1.0" 131 | }, 132 | "type": "library", 133 | "extra": { 134 | "branch-alias": { 135 | "dev-master": "2.0.x-dev" 136 | } 137 | }, 138 | "autoload": { 139 | "psr-0": { 140 | "phpDocumentor": [ 141 | "src/" 142 | ] 143 | } 144 | }, 145 | "notification-url": "https://packagist.org/downloads/", 146 | "license": [ 147 | "MIT" 148 | ], 149 | "authors": [ 150 | { 151 | "name": "Mike van Riel", 152 | "email": "mike.vanriel@naenius.com" 153 | } 154 | ], 155 | "time": "2015-02-03 12:10:50" 156 | }, 157 | { 158 | "name": "phpspec/prophecy", 159 | "version": "v1.4.1", 160 | "source": { 161 | "type": "git", 162 | "url": "https://github.com/phpspec/prophecy.git", 163 | "reference": "3132b1f44c7bf2ec4c7eb2d3cb78fdeca760d373" 164 | }, 165 | "dist": { 166 | "type": "zip", 167 | "url": "https://api.github.com/repos/phpspec/prophecy/zipball/3132b1f44c7bf2ec4c7eb2d3cb78fdeca760d373", 168 | "reference": "3132b1f44c7bf2ec4c7eb2d3cb78fdeca760d373", 169 | "shasum": "" 170 | }, 171 | "require": { 172 | "doctrine/instantiator": "^1.0.2", 173 | "phpdocumentor/reflection-docblock": "~2.0", 174 | "sebastian/comparator": "~1.1" 175 | }, 176 | "require-dev": { 177 | "phpspec/phpspec": "~2.0" 178 | }, 179 | "type": "library", 180 | "extra": { 181 | "branch-alias": { 182 | "dev-master": "1.4.x-dev" 183 | } 184 | }, 185 | "autoload": { 186 | "psr-0": { 187 | "Prophecy\\": "src/" 188 | } 189 | }, 190 | "notification-url": "https://packagist.org/downloads/", 191 | "license": [ 192 | "MIT" 193 | ], 194 | "authors": [ 195 | { 196 | "name": "Konstantin Kudryashov", 197 | "email": "ever.zet@gmail.com", 198 | "homepage": "http://everzet.com" 199 | }, 200 | { 201 | "name": "Marcello Duarte", 202 | "email": "marcello.duarte@gmail.com" 203 | } 204 | ], 205 | "description": "Highly opinionated mocking framework for PHP 5.3+", 206 | "homepage": "https://github.com/phpspec/prophecy", 207 | "keywords": [ 208 | "Double", 209 | "Dummy", 210 | "fake", 211 | "mock", 212 | "spy", 213 | "stub" 214 | ], 215 | "time": "2015-04-27 22:15:08" 216 | }, 217 | { 218 | "name": "phpunit/php-code-coverage", 219 | "version": "2.0.16", 220 | "source": { 221 | "type": "git", 222 | "url": "https://github.com/sebastianbergmann/php-code-coverage.git", 223 | "reference": "934fd03eb6840508231a7f73eb8940cf32c3b66c" 224 | }, 225 | "dist": { 226 | "type": "zip", 227 | "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/934fd03eb6840508231a7f73eb8940cf32c3b66c", 228 | "reference": "934fd03eb6840508231a7f73eb8940cf32c3b66c", 229 | "shasum": "" 230 | }, 231 | "require": { 232 | "php": ">=5.3.3", 233 | "phpunit/php-file-iterator": "~1.3", 234 | "phpunit/php-text-template": "~1.2", 235 | "phpunit/php-token-stream": "~1.3", 236 | "sebastian/environment": "~1.0", 237 | "sebastian/version": "~1.0" 238 | }, 239 | "require-dev": { 240 | "ext-xdebug": ">=2.1.4", 241 | "phpunit/phpunit": "~4" 242 | }, 243 | "suggest": { 244 | "ext-dom": "*", 245 | "ext-xdebug": ">=2.2.1", 246 | "ext-xmlwriter": "*" 247 | }, 248 | "type": "library", 249 | "extra": { 250 | "branch-alias": { 251 | "dev-master": "2.0.x-dev" 252 | } 253 | }, 254 | "autoload": { 255 | "classmap": [ 256 | "src/" 257 | ] 258 | }, 259 | "notification-url": "https://packagist.org/downloads/", 260 | "license": [ 261 | "BSD-3-Clause" 262 | ], 263 | "authors": [ 264 | { 265 | "name": "Sebastian Bergmann", 266 | "email": "sb@sebastian-bergmann.de", 267 | "role": "lead" 268 | } 269 | ], 270 | "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", 271 | "homepage": "https://github.com/sebastianbergmann/php-code-coverage", 272 | "keywords": [ 273 | "coverage", 274 | "testing", 275 | "xunit" 276 | ], 277 | "time": "2015-04-11 04:35:00" 278 | }, 279 | { 280 | "name": "phpunit/php-file-iterator", 281 | "version": "1.4.0", 282 | "source": { 283 | "type": "git", 284 | "url": "https://github.com/sebastianbergmann/php-file-iterator.git", 285 | "reference": "a923bb15680d0089e2316f7a4af8f437046e96bb" 286 | }, 287 | "dist": { 288 | "type": "zip", 289 | "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a923bb15680d0089e2316f7a4af8f437046e96bb", 290 | "reference": "a923bb15680d0089e2316f7a4af8f437046e96bb", 291 | "shasum": "" 292 | }, 293 | "require": { 294 | "php": ">=5.3.3" 295 | }, 296 | "type": "library", 297 | "extra": { 298 | "branch-alias": { 299 | "dev-master": "1.4.x-dev" 300 | } 301 | }, 302 | "autoload": { 303 | "classmap": [ 304 | "src/" 305 | ] 306 | }, 307 | "notification-url": "https://packagist.org/downloads/", 308 | "license": [ 309 | "BSD-3-Clause" 310 | ], 311 | "authors": [ 312 | { 313 | "name": "Sebastian Bergmann", 314 | "email": "sb@sebastian-bergmann.de", 315 | "role": "lead" 316 | } 317 | ], 318 | "description": "FilterIterator implementation that filters files based on a list of suffixes.", 319 | "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", 320 | "keywords": [ 321 | "filesystem", 322 | "iterator" 323 | ], 324 | "time": "2015-04-02 05:19:05" 325 | }, 326 | { 327 | "name": "phpunit/php-text-template", 328 | "version": "1.2.0", 329 | "source": { 330 | "type": "git", 331 | "url": "https://github.com/sebastianbergmann/php-text-template.git", 332 | "reference": "206dfefc0ffe9cebf65c413e3d0e809c82fbf00a" 333 | }, 334 | "dist": { 335 | "type": "zip", 336 | "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/206dfefc0ffe9cebf65c413e3d0e809c82fbf00a", 337 | "reference": "206dfefc0ffe9cebf65c413e3d0e809c82fbf00a", 338 | "shasum": "" 339 | }, 340 | "require": { 341 | "php": ">=5.3.3" 342 | }, 343 | "type": "library", 344 | "autoload": { 345 | "classmap": [ 346 | "Text/" 347 | ] 348 | }, 349 | "notification-url": "https://packagist.org/downloads/", 350 | "include-path": [ 351 | "" 352 | ], 353 | "license": [ 354 | "BSD-3-Clause" 355 | ], 356 | "authors": [ 357 | { 358 | "name": "Sebastian Bergmann", 359 | "email": "sb@sebastian-bergmann.de", 360 | "role": "lead" 361 | } 362 | ], 363 | "description": "Simple template engine.", 364 | "homepage": "https://github.com/sebastianbergmann/php-text-template/", 365 | "keywords": [ 366 | "template" 367 | ], 368 | "time": "2014-01-30 17:20:04" 369 | }, 370 | { 371 | "name": "phpunit/php-timer", 372 | "version": "1.0.5", 373 | "source": { 374 | "type": "git", 375 | "url": "https://github.com/sebastianbergmann/php-timer.git", 376 | "reference": "19689d4354b295ee3d8c54b4f42c3efb69cbc17c" 377 | }, 378 | "dist": { 379 | "type": "zip", 380 | "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/19689d4354b295ee3d8c54b4f42c3efb69cbc17c", 381 | "reference": "19689d4354b295ee3d8c54b4f42c3efb69cbc17c", 382 | "shasum": "" 383 | }, 384 | "require": { 385 | "php": ">=5.3.3" 386 | }, 387 | "type": "library", 388 | "autoload": { 389 | "classmap": [ 390 | "PHP/" 391 | ] 392 | }, 393 | "notification-url": "https://packagist.org/downloads/", 394 | "include-path": [ 395 | "" 396 | ], 397 | "license": [ 398 | "BSD-3-Clause" 399 | ], 400 | "authors": [ 401 | { 402 | "name": "Sebastian Bergmann", 403 | "email": "sb@sebastian-bergmann.de", 404 | "role": "lead" 405 | } 406 | ], 407 | "description": "Utility class for timing", 408 | "homepage": "https://github.com/sebastianbergmann/php-timer/", 409 | "keywords": [ 410 | "timer" 411 | ], 412 | "time": "2013-08-02 07:42:54" 413 | }, 414 | { 415 | "name": "phpunit/php-token-stream", 416 | "version": "1.4.1", 417 | "source": { 418 | "type": "git", 419 | "url": "https://github.com/sebastianbergmann/php-token-stream.git", 420 | "reference": "eab81d02569310739373308137284e0158424330" 421 | }, 422 | "dist": { 423 | "type": "zip", 424 | "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/eab81d02569310739373308137284e0158424330", 425 | "reference": "eab81d02569310739373308137284e0158424330", 426 | "shasum": "" 427 | }, 428 | "require": { 429 | "ext-tokenizer": "*", 430 | "php": ">=5.3.3" 431 | }, 432 | "require-dev": { 433 | "phpunit/phpunit": "~4.2" 434 | }, 435 | "type": "library", 436 | "extra": { 437 | "branch-alias": { 438 | "dev-master": "1.4-dev" 439 | } 440 | }, 441 | "autoload": { 442 | "classmap": [ 443 | "src/" 444 | ] 445 | }, 446 | "notification-url": "https://packagist.org/downloads/", 447 | "license": [ 448 | "BSD-3-Clause" 449 | ], 450 | "authors": [ 451 | { 452 | "name": "Sebastian Bergmann", 453 | "email": "sebastian@phpunit.de" 454 | } 455 | ], 456 | "description": "Wrapper around PHP's tokenizer extension.", 457 | "homepage": "https://github.com/sebastianbergmann/php-token-stream/", 458 | "keywords": [ 459 | "tokenizer" 460 | ], 461 | "time": "2015-04-08 04:46:07" 462 | }, 463 | { 464 | "name": "phpunit/phpunit", 465 | "version": "4.6.6", 466 | "source": { 467 | "type": "git", 468 | "url": "https://github.com/sebastianbergmann/phpunit.git", 469 | "reference": "3afe303d873a4d64c62ef84de491b97b006fbdac" 470 | }, 471 | "dist": { 472 | "type": "zip", 473 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3afe303d873a4d64c62ef84de491b97b006fbdac", 474 | "reference": "3afe303d873a4d64c62ef84de491b97b006fbdac", 475 | "shasum": "" 476 | }, 477 | "require": { 478 | "ext-dom": "*", 479 | "ext-json": "*", 480 | "ext-pcre": "*", 481 | "ext-reflection": "*", 482 | "ext-spl": "*", 483 | "php": ">=5.3.3", 484 | "phpspec/prophecy": "~1.3,>=1.3.1", 485 | "phpunit/php-code-coverage": "~2.0,>=2.0.11", 486 | "phpunit/php-file-iterator": "~1.4", 487 | "phpunit/php-text-template": "~1.2", 488 | "phpunit/php-timer": "~1.0", 489 | "phpunit/phpunit-mock-objects": "~2.3", 490 | "sebastian/comparator": "~1.1", 491 | "sebastian/diff": "~1.2", 492 | "sebastian/environment": "~1.2", 493 | "sebastian/exporter": "~1.2", 494 | "sebastian/global-state": "~1.0", 495 | "sebastian/version": "~1.0", 496 | "symfony/yaml": "~2.1|~3.0" 497 | }, 498 | "suggest": { 499 | "phpunit/php-invoker": "~1.1" 500 | }, 501 | "bin": [ 502 | "phpunit" 503 | ], 504 | "type": "library", 505 | "extra": { 506 | "branch-alias": { 507 | "dev-master": "4.6.x-dev" 508 | } 509 | }, 510 | "autoload": { 511 | "classmap": [ 512 | "src/" 513 | ] 514 | }, 515 | "notification-url": "https://packagist.org/downloads/", 516 | "license": [ 517 | "BSD-3-Clause" 518 | ], 519 | "authors": [ 520 | { 521 | "name": "Sebastian Bergmann", 522 | "email": "sebastian@phpunit.de", 523 | "role": "lead" 524 | } 525 | ], 526 | "description": "The PHP Unit Testing framework.", 527 | "homepage": "https://phpunit.de/", 528 | "keywords": [ 529 | "phpunit", 530 | "testing", 531 | "xunit" 532 | ], 533 | "time": "2015-04-29 15:18:52" 534 | }, 535 | { 536 | "name": "phpunit/phpunit-mock-objects", 537 | "version": "2.3.1", 538 | "source": { 539 | "type": "git", 540 | "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", 541 | "reference": "74ffb87f527f24616f72460e54b595f508dccb5c" 542 | }, 543 | "dist": { 544 | "type": "zip", 545 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/74ffb87f527f24616f72460e54b595f508dccb5c", 546 | "reference": "74ffb87f527f24616f72460e54b595f508dccb5c", 547 | "shasum": "" 548 | }, 549 | "require": { 550 | "doctrine/instantiator": "~1.0,>=1.0.2", 551 | "php": ">=5.3.3", 552 | "phpunit/php-text-template": "~1.2" 553 | }, 554 | "require-dev": { 555 | "phpunit/phpunit": "~4.4" 556 | }, 557 | "suggest": { 558 | "ext-soap": "*" 559 | }, 560 | "type": "library", 561 | "extra": { 562 | "branch-alias": { 563 | "dev-master": "2.3.x-dev" 564 | } 565 | }, 566 | "autoload": { 567 | "classmap": [ 568 | "src/" 569 | ] 570 | }, 571 | "notification-url": "https://packagist.org/downloads/", 572 | "license": [ 573 | "BSD-3-Clause" 574 | ], 575 | "authors": [ 576 | { 577 | "name": "Sebastian Bergmann", 578 | "email": "sb@sebastian-bergmann.de", 579 | "role": "lead" 580 | } 581 | ], 582 | "description": "Mock Object library for PHPUnit", 583 | "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", 584 | "keywords": [ 585 | "mock", 586 | "xunit" 587 | ], 588 | "time": "2015-04-02 05:36:41" 589 | }, 590 | { 591 | "name": "sebastian/comparator", 592 | "version": "1.1.1", 593 | "source": { 594 | "type": "git", 595 | "url": "https://github.com/sebastianbergmann/comparator.git", 596 | "reference": "1dd8869519a225f7f2b9eb663e225298fade819e" 597 | }, 598 | "dist": { 599 | "type": "zip", 600 | "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/1dd8869519a225f7f2b9eb663e225298fade819e", 601 | "reference": "1dd8869519a225f7f2b9eb663e225298fade819e", 602 | "shasum": "" 603 | }, 604 | "require": { 605 | "php": ">=5.3.3", 606 | "sebastian/diff": "~1.2", 607 | "sebastian/exporter": "~1.2" 608 | }, 609 | "require-dev": { 610 | "phpunit/phpunit": "~4.4" 611 | }, 612 | "type": "library", 613 | "extra": { 614 | "branch-alias": { 615 | "dev-master": "1.1.x-dev" 616 | } 617 | }, 618 | "autoload": { 619 | "classmap": [ 620 | "src/" 621 | ] 622 | }, 623 | "notification-url": "https://packagist.org/downloads/", 624 | "license": [ 625 | "BSD-3-Clause" 626 | ], 627 | "authors": [ 628 | { 629 | "name": "Jeff Welch", 630 | "email": "whatthejeff@gmail.com" 631 | }, 632 | { 633 | "name": "Volker Dusch", 634 | "email": "github@wallbash.com" 635 | }, 636 | { 637 | "name": "Bernhard Schussek", 638 | "email": "bschussek@2bepublished.at" 639 | }, 640 | { 641 | "name": "Sebastian Bergmann", 642 | "email": "sebastian@phpunit.de" 643 | } 644 | ], 645 | "description": "Provides the functionality to compare PHP values for equality", 646 | "homepage": "http://www.github.com/sebastianbergmann/comparator", 647 | "keywords": [ 648 | "comparator", 649 | "compare", 650 | "equality" 651 | ], 652 | "time": "2015-01-29 16:28:08" 653 | }, 654 | { 655 | "name": "sebastian/diff", 656 | "version": "1.3.0", 657 | "source": { 658 | "type": "git", 659 | "url": "https://github.com/sebastianbergmann/diff.git", 660 | "reference": "863df9687835c62aa423a22412d26fa2ebde3fd3" 661 | }, 662 | "dist": { 663 | "type": "zip", 664 | "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/863df9687835c62aa423a22412d26fa2ebde3fd3", 665 | "reference": "863df9687835c62aa423a22412d26fa2ebde3fd3", 666 | "shasum": "" 667 | }, 668 | "require": { 669 | "php": ">=5.3.3" 670 | }, 671 | "require-dev": { 672 | "phpunit/phpunit": "~4.2" 673 | }, 674 | "type": "library", 675 | "extra": { 676 | "branch-alias": { 677 | "dev-master": "1.3-dev" 678 | } 679 | }, 680 | "autoload": { 681 | "classmap": [ 682 | "src/" 683 | ] 684 | }, 685 | "notification-url": "https://packagist.org/downloads/", 686 | "license": [ 687 | "BSD-3-Clause" 688 | ], 689 | "authors": [ 690 | { 691 | "name": "Kore Nordmann", 692 | "email": "mail@kore-nordmann.de" 693 | }, 694 | { 695 | "name": "Sebastian Bergmann", 696 | "email": "sebastian@phpunit.de" 697 | } 698 | ], 699 | "description": "Diff implementation", 700 | "homepage": "http://www.github.com/sebastianbergmann/diff", 701 | "keywords": [ 702 | "diff" 703 | ], 704 | "time": "2015-02-22 15:13:53" 705 | }, 706 | { 707 | "name": "sebastian/environment", 708 | "version": "1.2.2", 709 | "source": { 710 | "type": "git", 711 | "url": "https://github.com/sebastianbergmann/environment.git", 712 | "reference": "5a8c7d31914337b69923db26c4221b81ff5a196e" 713 | }, 714 | "dist": { 715 | "type": "zip", 716 | "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/5a8c7d31914337b69923db26c4221b81ff5a196e", 717 | "reference": "5a8c7d31914337b69923db26c4221b81ff5a196e", 718 | "shasum": "" 719 | }, 720 | "require": { 721 | "php": ">=5.3.3" 722 | }, 723 | "require-dev": { 724 | "phpunit/phpunit": "~4.4" 725 | }, 726 | "type": "library", 727 | "extra": { 728 | "branch-alias": { 729 | "dev-master": "1.3.x-dev" 730 | } 731 | }, 732 | "autoload": { 733 | "classmap": [ 734 | "src/" 735 | ] 736 | }, 737 | "notification-url": "https://packagist.org/downloads/", 738 | "license": [ 739 | "BSD-3-Clause" 740 | ], 741 | "authors": [ 742 | { 743 | "name": "Sebastian Bergmann", 744 | "email": "sebastian@phpunit.de" 745 | } 746 | ], 747 | "description": "Provides functionality to handle HHVM/PHP environments", 748 | "homepage": "http://www.github.com/sebastianbergmann/environment", 749 | "keywords": [ 750 | "Xdebug", 751 | "environment", 752 | "hhvm" 753 | ], 754 | "time": "2015-01-01 10:01:08" 755 | }, 756 | { 757 | "name": "sebastian/exporter", 758 | "version": "1.2.0", 759 | "source": { 760 | "type": "git", 761 | "url": "https://github.com/sebastianbergmann/exporter.git", 762 | "reference": "84839970d05254c73cde183a721c7af13aede943" 763 | }, 764 | "dist": { 765 | "type": "zip", 766 | "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/84839970d05254c73cde183a721c7af13aede943", 767 | "reference": "84839970d05254c73cde183a721c7af13aede943", 768 | "shasum": "" 769 | }, 770 | "require": { 771 | "php": ">=5.3.3", 772 | "sebastian/recursion-context": "~1.0" 773 | }, 774 | "require-dev": { 775 | "phpunit/phpunit": "~4.4" 776 | }, 777 | "type": "library", 778 | "extra": { 779 | "branch-alias": { 780 | "dev-master": "1.2.x-dev" 781 | } 782 | }, 783 | "autoload": { 784 | "classmap": [ 785 | "src/" 786 | ] 787 | }, 788 | "notification-url": "https://packagist.org/downloads/", 789 | "license": [ 790 | "BSD-3-Clause" 791 | ], 792 | "authors": [ 793 | { 794 | "name": "Jeff Welch", 795 | "email": "whatthejeff@gmail.com" 796 | }, 797 | { 798 | "name": "Volker Dusch", 799 | "email": "github@wallbash.com" 800 | }, 801 | { 802 | "name": "Bernhard Schussek", 803 | "email": "bschussek@2bepublished.at" 804 | }, 805 | { 806 | "name": "Sebastian Bergmann", 807 | "email": "sebastian@phpunit.de" 808 | }, 809 | { 810 | "name": "Adam Harvey", 811 | "email": "aharvey@php.net" 812 | } 813 | ], 814 | "description": "Provides the functionality to export PHP variables for visualization", 815 | "homepage": "http://www.github.com/sebastianbergmann/exporter", 816 | "keywords": [ 817 | "export", 818 | "exporter" 819 | ], 820 | "time": "2015-01-27 07:23:06" 821 | }, 822 | { 823 | "name": "sebastian/global-state", 824 | "version": "1.0.0", 825 | "source": { 826 | "type": "git", 827 | "url": "https://github.com/sebastianbergmann/global-state.git", 828 | "reference": "c7428acdb62ece0a45e6306f1ae85e1c05b09c01" 829 | }, 830 | "dist": { 831 | "type": "zip", 832 | "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/c7428acdb62ece0a45e6306f1ae85e1c05b09c01", 833 | "reference": "c7428acdb62ece0a45e6306f1ae85e1c05b09c01", 834 | "shasum": "" 835 | }, 836 | "require": { 837 | "php": ">=5.3.3" 838 | }, 839 | "require-dev": { 840 | "phpunit/phpunit": "~4.2" 841 | }, 842 | "suggest": { 843 | "ext-uopz": "*" 844 | }, 845 | "type": "library", 846 | "extra": { 847 | "branch-alias": { 848 | "dev-master": "1.0-dev" 849 | } 850 | }, 851 | "autoload": { 852 | "classmap": [ 853 | "src/" 854 | ] 855 | }, 856 | "notification-url": "https://packagist.org/downloads/", 857 | "license": [ 858 | "BSD-3-Clause" 859 | ], 860 | "authors": [ 861 | { 862 | "name": "Sebastian Bergmann", 863 | "email": "sebastian@phpunit.de" 864 | } 865 | ], 866 | "description": "Snapshotting of global state", 867 | "homepage": "http://www.github.com/sebastianbergmann/global-state", 868 | "keywords": [ 869 | "global state" 870 | ], 871 | "time": "2014-10-06 09:23:50" 872 | }, 873 | { 874 | "name": "sebastian/recursion-context", 875 | "version": "1.0.0", 876 | "source": { 877 | "type": "git", 878 | "url": "https://github.com/sebastianbergmann/recursion-context.git", 879 | "reference": "3989662bbb30a29d20d9faa04a846af79b276252" 880 | }, 881 | "dist": { 882 | "type": "zip", 883 | "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/3989662bbb30a29d20d9faa04a846af79b276252", 884 | "reference": "3989662bbb30a29d20d9faa04a846af79b276252", 885 | "shasum": "" 886 | }, 887 | "require": { 888 | "php": ">=5.3.3" 889 | }, 890 | "require-dev": { 891 | "phpunit/phpunit": "~4.4" 892 | }, 893 | "type": "library", 894 | "extra": { 895 | "branch-alias": { 896 | "dev-master": "1.0.x-dev" 897 | } 898 | }, 899 | "autoload": { 900 | "classmap": [ 901 | "src/" 902 | ] 903 | }, 904 | "notification-url": "https://packagist.org/downloads/", 905 | "license": [ 906 | "BSD-3-Clause" 907 | ], 908 | "authors": [ 909 | { 910 | "name": "Jeff Welch", 911 | "email": "whatthejeff@gmail.com" 912 | }, 913 | { 914 | "name": "Sebastian Bergmann", 915 | "email": "sebastian@phpunit.de" 916 | }, 917 | { 918 | "name": "Adam Harvey", 919 | "email": "aharvey@php.net" 920 | } 921 | ], 922 | "description": "Provides functionality to recursively process PHP variables", 923 | "homepage": "http://www.github.com/sebastianbergmann/recursion-context", 924 | "time": "2015-01-24 09:48:32" 925 | }, 926 | { 927 | "name": "sebastian/version", 928 | "version": "1.0.5", 929 | "source": { 930 | "type": "git", 931 | "url": "https://github.com/sebastianbergmann/version.git", 932 | "reference": "ab931d46cd0d3204a91e1b9a40c4bc13032b58e4" 933 | }, 934 | "dist": { 935 | "type": "zip", 936 | "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/ab931d46cd0d3204a91e1b9a40c4bc13032b58e4", 937 | "reference": "ab931d46cd0d3204a91e1b9a40c4bc13032b58e4", 938 | "shasum": "" 939 | }, 940 | "type": "library", 941 | "autoload": { 942 | "classmap": [ 943 | "src/" 944 | ] 945 | }, 946 | "notification-url": "https://packagist.org/downloads/", 947 | "license": [ 948 | "BSD-3-Clause" 949 | ], 950 | "authors": [ 951 | { 952 | "name": "Sebastian Bergmann", 953 | "email": "sebastian@phpunit.de", 954 | "role": "lead" 955 | } 956 | ], 957 | "description": "Library that helps with managing the version number of Git-hosted PHP projects", 958 | "homepage": "https://github.com/sebastianbergmann/version", 959 | "time": "2015-02-24 06:35:25" 960 | }, 961 | { 962 | "name": "symfony/process", 963 | "version": "v2.6.7", 964 | "target-dir": "Symfony/Component/Process", 965 | "source": { 966 | "type": "git", 967 | "url": "https://github.com/symfony/Process.git", 968 | "reference": "9f3c4baaf840ed849e1b1f7bfd5ae246e8509562" 969 | }, 970 | "dist": { 971 | "type": "zip", 972 | "url": "https://api.github.com/repos/symfony/Process/zipball/9f3c4baaf840ed849e1b1f7bfd5ae246e8509562", 973 | "reference": "9f3c4baaf840ed849e1b1f7bfd5ae246e8509562", 974 | "shasum": "" 975 | }, 976 | "require": { 977 | "php": ">=5.3.3" 978 | }, 979 | "require-dev": { 980 | "symfony/phpunit-bridge": "~2.7" 981 | }, 982 | "type": "library", 983 | "extra": { 984 | "branch-alias": { 985 | "dev-master": "2.6-dev" 986 | } 987 | }, 988 | "autoload": { 989 | "psr-0": { 990 | "Symfony\\Component\\Process\\": "" 991 | } 992 | }, 993 | "notification-url": "https://packagist.org/downloads/", 994 | "license": [ 995 | "MIT" 996 | ], 997 | "authors": [ 998 | { 999 | "name": "Fabien Potencier", 1000 | "email": "fabien@symfony.com" 1001 | }, 1002 | { 1003 | "name": "Symfony Community", 1004 | "homepage": "https://symfony.com/contributors" 1005 | } 1006 | ], 1007 | "description": "Symfony Process Component", 1008 | "homepage": "https://symfony.com", 1009 | "time": "2015-05-02 15:18:45" 1010 | }, 1011 | { 1012 | "name": "symfony/yaml", 1013 | "version": "v2.6.7", 1014 | "target-dir": "Symfony/Component/Yaml", 1015 | "source": { 1016 | "type": "git", 1017 | "url": "https://github.com/symfony/Yaml.git", 1018 | "reference": "f157ab074e453ecd4c0fa775f721f6e67a99d9e2" 1019 | }, 1020 | "dist": { 1021 | "type": "zip", 1022 | "url": "https://api.github.com/repos/symfony/Yaml/zipball/f157ab074e453ecd4c0fa775f721f6e67a99d9e2", 1023 | "reference": "f157ab074e453ecd4c0fa775f721f6e67a99d9e2", 1024 | "shasum": "" 1025 | }, 1026 | "require": { 1027 | "php": ">=5.3.3" 1028 | }, 1029 | "require-dev": { 1030 | "symfony/phpunit-bridge": "~2.7" 1031 | }, 1032 | "type": "library", 1033 | "extra": { 1034 | "branch-alias": { 1035 | "dev-master": "2.6-dev" 1036 | } 1037 | }, 1038 | "autoload": { 1039 | "psr-0": { 1040 | "Symfony\\Component\\Yaml\\": "" 1041 | } 1042 | }, 1043 | "notification-url": "https://packagist.org/downloads/", 1044 | "license": [ 1045 | "MIT" 1046 | ], 1047 | "authors": [ 1048 | { 1049 | "name": "Fabien Potencier", 1050 | "email": "fabien@symfony.com" 1051 | }, 1052 | { 1053 | "name": "Symfony Community", 1054 | "homepage": "https://symfony.com/contributors" 1055 | } 1056 | ], 1057 | "description": "Symfony Yaml Component", 1058 | "homepage": "https://symfony.com", 1059 | "time": "2015-05-02 15:18:45" 1060 | } 1061 | ], 1062 | "aliases": [], 1063 | "minimum-stability": "stable", 1064 | "stability-flags": [], 1065 | "prefer-stable": false, 1066 | "prefer-lowest": false, 1067 | "platform": { 1068 | "php": ">=5.3.0" 1069 | }, 1070 | "platform-dev": [] 1071 | } 1072 | -------------------------------------------------------------------------------- /examples/blocking-download.php: -------------------------------------------------------------------------------- 1 | startProcess("wget -O - $url"); 10 | 11 | // this will block until the process starts 12 | $process->wait(); 13 | echo "Downloading file...\n"; 14 | 15 | // this will block until the process exits 16 | echo "File contents:\n{$process->getResult()->readFromPipe(1)}\n"; 17 | -------------------------------------------------------------------------------- /examples/non-blocking-download.php: -------------------------------------------------------------------------------- 1 | startProcess("wget -O - $url"); 12 | 13 | // this will not block, even if the process is queued 14 | $process->then(function (FutureProcess $process) { 15 | echo "Downloading file...\n"; 16 | }); 17 | 18 | // this will not block, even if the process is queued 19 | $process->getResult()->then(function (FutureResult $result) { 20 | echo "File contents:\n{$result->readFromPipe(1)}\n"; 21 | }); 22 | 23 | // this will block until all processes have exited 24 | $shell->wait(); 25 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | ./tests/ 10 | 11 | 12 | 13 | 14 | 15 | ./src/ 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/FutureProcess.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class FutureProcess 11 | { 12 | const STATUS_QUEUED = 0; 13 | const STATUS_RUNNING = 1; 14 | const STATUS_EXITED = 2; 15 | const STATUS_ABORTED = 3; 16 | const STATUS_ERROR = 4; 17 | 18 | private static $defaultOptions; 19 | 20 | private $promise; 21 | private $options; 22 | private $futureExitCode; 23 | private $queueSlot; 24 | private $status; 25 | private $resource; 26 | private $startTime; 27 | private $pid; 28 | private $pipes; 29 | private $result; 30 | 31 | public function __construct( 32 | $command, 33 | array $options, 34 | FutureValue $futureExitCode, 35 | FutureValue $queueSlot = null 36 | ) { 37 | $options = $this->prepareOptions($options); 38 | 39 | $this->options = $options; 40 | $this->futureExitCode = $futureExitCode; 41 | $this->pipes = new Pipes($options['io']); 42 | 43 | $startFn = $this->getStartFn(); 44 | 45 | if ($this->queueSlot = $queueSlot) { 46 | $this->status = self::STATUS_QUEUED; 47 | $that = $this; 48 | $this->promise = $queueSlot->then( 49 | function () use ($startFn, $command, $options, $that) { 50 | $startFn($command, $options); 51 | return $that; 52 | }, 53 | function (\Exception $e) use ($that) { 54 | $that->abort($e); 55 | throw $e; 56 | } 57 | ); 58 | } else { 59 | $startFn($command, $options); 60 | $this->promise = new FulfilledPromise($this); 61 | } 62 | } 63 | 64 | /** 65 | * @return int 66 | */ 67 | public function getPid() 68 | { 69 | $this->wait(); 70 | 71 | return $this->pid; 72 | } 73 | 74 | /** 75 | * @param int $descriptor 76 | * @return null|resource 77 | */ 78 | public function getPipe($descriptor) 79 | { 80 | $this->wait(); 81 | 82 | return $this->pipes->getResource($descriptor); 83 | } 84 | 85 | /** 86 | * @param int $descriptor 87 | * @param string $data 88 | */ 89 | public function writeToPipe($descriptor, $data) 90 | { 91 | $this->pipes->write($descriptor, $data); 92 | } 93 | 94 | /** 95 | * @param int $descriptor 96 | * @param int|null $length 97 | * @return string 98 | */ 99 | public function readFromPipe($descriptor, $length = null) 100 | { 101 | $this->wait(); 102 | 103 | return $this->pipes->read($descriptor, $length); 104 | } 105 | 106 | /** 107 | * @param bool $refresh OPTIONAL 108 | * @return int One of the status constants defined in this class 109 | */ 110 | public function getStatus($refresh = true) 111 | { 112 | if ($refresh && $this->status === self::STATUS_RUNNING) { 113 | $this->refreshStatus(); 114 | } 115 | 116 | return $this->status; 117 | } 118 | 119 | /** 120 | * @return FutureResult 121 | */ 122 | public function getResult() 123 | { 124 | if (is_null($this->result)) { 125 | $this->result = new FutureResult($this->pipes, $this->futureExitCode); 126 | } 127 | 128 | return $this->result; 129 | } 130 | 131 | /** 132 | * @param \Exception|null $error 133 | * @param int|null $signal If null is passed, no signal will be sent to the process 134 | */ 135 | public function abort(\Exception $error = null, $signal = 15) 136 | { 137 | if ($this->status === self::STATUS_RUNNING) { 138 | if (null !== $signal) { 139 | proc_terminate($this->resource, $signal); 140 | } 141 | } elseif ($this->status === self::STATUS_QUEUED) { 142 | if ($error) { 143 | $this->queueSlot->reject($error); 144 | } else { 145 | $this->queueSlot->resolve(); 146 | } 147 | } else { 148 | return; 149 | } 150 | 151 | $this->doExit(self::STATUS_ABORTED, $error); 152 | } 153 | 154 | /** 155 | * Wait for the process to start 156 | * 157 | * @param double $timeout OPTIONAL 158 | * @return static 159 | */ 160 | public function wait($timeout = null) 161 | { 162 | if ($this->queueSlot) { 163 | $this->queueSlot->wait($timeout); 164 | } 165 | 166 | return $this; 167 | } 168 | 169 | /** 170 | * @return PromiseInterface 171 | */ 172 | public function promise() 173 | { 174 | return $this->promise; 175 | } 176 | 177 | /** 178 | * @param callable $onFulfilled 179 | * @param callable $onError 180 | * @return PromiseInterface 181 | */ 182 | public function then($onFulfilled = null, $onError = null) 183 | { 184 | return $this->promise->then($onFulfilled, $onError); 185 | } 186 | 187 | private function refreshStatus() 188 | { 189 | if (false === $status = proc_get_status($this->resource)) { 190 | $this->doExit(self::STATUS_ERROR, new \RuntimeException('An unknown error occurred.')); 191 | } elseif (!$status['running']) { 192 | $exitCode = (-1 == $status['exitcode'] ? null : $status['exitcode']); 193 | $this->doExit(self::STATUS_EXITED, $exitCode); 194 | } elseif (!$this->hasExceededTimeLimit()) { 195 | $this->pipes->readAndWrite(); 196 | } 197 | 198 | return $status; 199 | } 200 | 201 | private function hasExceededTimeLimit() 202 | { 203 | if ($this->options['timeout'] && microtime(true) > $this->startTime + $this->options['timeout']) { 204 | $this->abort( 205 | $this->options['timeout_error'], 206 | $this->options['timeout_signal'] 207 | ); 208 | 209 | return true; 210 | } 211 | 212 | return false; 213 | } 214 | 215 | private static function prepareOptions(array $options) 216 | { 217 | return array_merge( 218 | self::getDefaultOptions(), 219 | $options 220 | ); 221 | } 222 | 223 | private static function getDefaultOptions() 224 | { 225 | if (is_null(self::$defaultOptions)) { 226 | self::$defaultOptions = array( 227 | 'io' => array( 228 | 0 => array('pipe', 'r'), 229 | 1 => array('pipe', 'w'), 230 | 2 => array('pipe', 'w'), 231 | ), 232 | 'working_dir' => null, 233 | 'environment' => null, 234 | 'timeout' => null, 235 | 'timeout_signal' => 15, 236 | 'timeout_error' => new \RuntimeException('The process exceeded its time limit and was aborted.'), 237 | ); 238 | } 239 | 240 | return self::$defaultOptions; 241 | } 242 | 243 | private function getStartFn() 244 | { 245 | $procResource = &$this->resource; 246 | $pipes = $this->pipes; 247 | $status = &$this->status; 248 | $startTime = &$this->startTime; 249 | $pid = &$this->pid; 250 | 251 | return function ($command, array $options) use (&$procResource, $pipes, &$status, &$startTime, &$pid) { 252 | $procResource = proc_open( 253 | "exec $command", 254 | $options['io'], 255 | $pipeResources, 256 | $options['working_dir'], 257 | $options['environment'] 258 | ); 259 | $pipes->setResources($pipeResources); 260 | $startTime = microtime(true); 261 | $status = FutureProcess::STATUS_RUNNING; 262 | if (false === $procStatus = proc_get_status($procResource)) { 263 | throw new \RuntimeException('Failed to start process.'); 264 | } 265 | $pid = $procStatus['pid']; 266 | }; 267 | } 268 | 269 | private function doExit($status, $exitCodeOrException) 270 | { 271 | $this->status = $status; 272 | 273 | $this->pipes->close(); 274 | 275 | if ($exitCodeOrException instanceof \Exception) { 276 | $this->futureExitCode->reject($exitCodeOrException); 277 | } else { 278 | $this->futureExitCode->resolve($exitCodeOrException); 279 | } 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /src/FutureResult.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | class FutureResult 10 | { 11 | private $pipes; 12 | private $futureExitCode; 13 | private $promise; 14 | 15 | public function __construct(Pipes $pipes, FutureValue $futureExitCode) 16 | { 17 | $this->pipes = $pipes; 18 | $this->futureExitCode = $futureExitCode; 19 | } 20 | 21 | /** 22 | * @param int $descriptor 23 | * @param int|null $length 24 | * @return string 25 | */ 26 | public function readFromPipe($descriptor, $length = null) 27 | { 28 | $this->wait(); 29 | 30 | return $this->pipes->read($descriptor, $length); 31 | } 32 | 33 | /** 34 | * @return null|int 35 | */ 36 | public function getExitCode() 37 | { 38 | return $this->futureExitCode->wait(); 39 | } 40 | 41 | /** 42 | * Wait for the process to end 43 | * 44 | * @param double $timeout OPTIONAL 45 | * @return static 46 | */ 47 | public function wait($timeout = null) 48 | { 49 | $this->futureExitCode->wait($timeout); 50 | 51 | return $this; 52 | } 53 | 54 | /** 55 | * @return PromiseInterface 56 | */ 57 | public function promise() 58 | { 59 | if (!$this->promise) { 60 | $that = $this; 61 | $this->promise = $this->futureExitCode->then(function () use ($that) { 62 | return $that; 63 | }); 64 | } 65 | 66 | return $this->promise; 67 | } 68 | 69 | /** 70 | * @param callable $onFulfilled 71 | * @param callable $onError 72 | * @return PromiseInterface 73 | */ 74 | public function then($onFulfilled = null, $onError = null) 75 | { 76 | return $this->promise()->then($onFulfilled, $onError); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/FutureValue.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class FutureValue 11 | { 12 | private $waitFn; 13 | private $deferred; 14 | private $isRealised = false; 15 | private $value; 16 | private $error; 17 | 18 | public function __construct($waitFn) 19 | { 20 | $this->waitFn = $waitFn; 21 | $this->deferred = new Deferred; 22 | } 23 | 24 | /** 25 | * @return bool 26 | */ 27 | public function isRealised() 28 | { 29 | return $this->isRealised; 30 | } 31 | 32 | /** 33 | * @param mixed $value OPTIONAL 34 | */ 35 | public function resolve($value = null) 36 | { 37 | if (!$this->isRealised) { 38 | $this->value = $value; 39 | $this->isRealised = true; 40 | $this->deferred->resolve($value); 41 | } 42 | } 43 | 44 | /** 45 | * @param \Exception $e 46 | */ 47 | public function reject(\Exception $e) 48 | { 49 | if (!$this->isRealised) { 50 | $this->error = $e; 51 | $this->isRealised = true; 52 | $this->deferred->reject($e); 53 | } 54 | } 55 | 56 | /** 57 | * @param double $timeout 58 | * @return mixed 59 | */ 60 | public function wait($timeout = null) 61 | { 62 | if (!$this->isRealised) { 63 | call_user_func($this->waitFn, $timeout, $this); 64 | } 65 | 66 | if ($this->error) { 67 | throw $this->error; 68 | } 69 | 70 | return $this->value; 71 | } 72 | 73 | /** 74 | * @param callable $onFulfilled 75 | * @param callable $onError 76 | * @return PromiseInterface 77 | */ 78 | public function then($onFulfilled = null, $onError = null) 79 | { 80 | return $this->deferred->promise()->then($onFulfilled, $onError); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Pipes.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | class Pipes 8 | { 9 | private static $modesByType = array( 10 | 'read' => array('r+', 'w', 'w+', 'a', 'a+', 'x', 'x+', 'c', 'c+'), 11 | 'write' => array('r', 'r+', 'w+', 'a+', 'x+', 'c+'), 12 | ); 13 | 14 | private $resources = array(); 15 | private $resourcesByType = array( 16 | 'read' => array(), 17 | 'write' => array(), 18 | ); 19 | private $buffers = array( 20 | 'read' => array(), 21 | 'write' => array(), 22 | ); 23 | 24 | public function __construct(array $descriptorSpec) 25 | { 26 | $pipes = array_filter($descriptorSpec, function ($element) { 27 | return isset($element[0]) && $element[0] === 'pipe'; 28 | }); 29 | 30 | foreach (self::$modesByType as $type => $modes) { 31 | $matchedPipes = array_filter($pipes, function ($pipe) use ($modes) { 32 | return in_array($pipe[1], $modes); 33 | }); 34 | 35 | $this->buffers[$type] = array_fill_keys(array_keys($matchedPipes), ''); 36 | } 37 | } 38 | 39 | public function setResources(array $resources) 40 | { 41 | foreach (self::$modesByType as $type => $modes) { 42 | $this->resourcesByType[$type] = array_intersect_key($resources, $this->buffers[$type]); 43 | } 44 | 45 | $this->resources = $resources; 46 | } 47 | 48 | public function getResource($descriptor) 49 | { 50 | if (!isset($this->resources[$descriptor])) { 51 | throw new \RuntimeException('No pipe exists for the specified descriptor.'); 52 | } 53 | 54 | return $this->resources[$descriptor]; 55 | } 56 | 57 | public function read($descriptor, $length = null) 58 | { 59 | if (!isset($this->buffers['read'][$descriptor])) { 60 | throw new \RuntimeException('No pipe exists for the specified descriptor.'); 61 | } 62 | 63 | if ($readResources = $this->resourcesByType['read']) { 64 | $this->select($readResources, $writeResources); 65 | $this->drainProcessOutputBuffers($readResources); 66 | } 67 | 68 | if (is_int($length)) { 69 | $data = substr($this->buffers['read'][$descriptor], 0, $length); 70 | $this->buffers['read'][$descriptor] = substr($this->buffers['read'][$descriptor], $length); 71 | } else { 72 | $data = $this->buffers['read'][$descriptor]; 73 | $this->buffers['read'][$descriptor] = ''; 74 | } 75 | 76 | return $data; 77 | } 78 | 79 | public function write($descriptor, $data) 80 | { 81 | if (!isset($this->buffers['write'][$descriptor])) { 82 | throw new \RuntimeException('No pipe exists for the specified descriptor.'); 83 | } 84 | 85 | $this->buffers['write'][$descriptor] .= $data; 86 | 87 | if ($writeResources = $this->resourcesByType['write']) { 88 | $this->select($readResources, $writeResources); 89 | $this->drainWriteBuffers($writeResources); 90 | } 91 | } 92 | 93 | public function readAndWrite() 94 | { 95 | $readResources = $this->resourcesByType['read']; 96 | $writeResources = $this->resourcesByType['write']; 97 | $this->select($readResources, $writeResources); 98 | $this->drainWriteBuffers($writeResources); 99 | $this->drainProcessOutputBuffers($readResources); 100 | } 101 | 102 | public function close() 103 | { 104 | if ($readResources = $this->resourcesByType['read']) { 105 | $this->select($readResources, $writeResources); 106 | $this->drainProcessOutputBuffers($readResources); 107 | } 108 | 109 | foreach ($this->resources as $descriptor => $resource) { 110 | fclose($resource); 111 | unset($this->resources[$descriptor]); 112 | unset($this->resourcesByType['read'][$descriptor]); 113 | unset($this->resourcesByType['write'][$descriptor]); 114 | } 115 | } 116 | 117 | private function drainProcessOutputBuffers(array $resources) 118 | { 119 | foreach ($resources as $descriptor => $resource) { 120 | stream_set_blocking($resource, 0); 121 | while (strlen($data = fread($resource, 8192))) { 122 | $this->buffers['read'][$descriptor] .= $data; 123 | } 124 | } 125 | } 126 | 127 | private function drainWriteBuffers(array $resources) 128 | { 129 | foreach ($resources as $descriptor => $resource) { 130 | stream_set_blocking($resource, 0); 131 | $descriptor = array_search($resource, $this->resourcesByType); 132 | while (strlen($this->buffers['write'][$descriptor])) { 133 | $written = fwrite($resource, $this->buffers['write'][$descriptor], 2 << 18); // write 512k 134 | if ($written > 0) { 135 | $this->buffers['write'][$descriptor] = (string)substr($this->buffers['write'][$descriptor], $written); 136 | } else { 137 | break; 138 | } 139 | } 140 | } 141 | } 142 | 143 | private function select(&$readResources, &$writeResources) 144 | { 145 | if (false === stream_select($readResources, $writeResources, $except, 0)) { 146 | throw new \RuntimeException('An error occurred when polling process pipes.'); 147 | } 148 | 149 | // prior PHP 5.4 the array passed to stream_select is modified and 150 | // lose key association, we have to find back the key 151 | 152 | if (count($readResources)) { 153 | $readResources = array_intersect($this->resources, $readResources); 154 | } 155 | 156 | if (count($writeResources)) { 157 | $writeResources = array_intersect($this->resources, $writeResources); 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/Shell.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | class Shell 8 | { 9 | private $processLimit = 10; 10 | private $activeProcesses; 11 | private $queue; 12 | private $canStartProcessFn; 13 | private $handleQueueFn; 14 | private $runUntilFutureRealisedFn; 15 | 16 | public function __construct() 17 | { 18 | $this->activeProcesses = new \SplObjectStorage; 19 | $this->queue = new \SplQueue; 20 | $this->canStartProcessFn = $this->createCanStartProcessFn(); 21 | $this->handleQueueFn = $this->createHandleQueueFn(); 22 | $this->runUntilFutureRealisedFn = $this->createRunUntilFutureRealisedFn(); 23 | } 24 | 25 | /** 26 | * @return FutureProcess 27 | */ 28 | public function startProcess($command, array $options = array()) 29 | { 30 | $handleQueueFn = $this->handleQueueFn; 31 | $handleQueueFn(); 32 | 33 | $process = $this->createProcess($command, $options); 34 | 35 | $activeProcesses = $this->activeProcesses; 36 | $process->then(function () use ($activeProcesses, $process) { 37 | $activeProcesses->attach($process); 38 | }); 39 | $onComplete = function () use ($activeProcesses, $process, $handleQueueFn) { 40 | $activeProcesses->detach($process); 41 | $handleQueueFn(); 42 | }; 43 | $process->getResult()->then($onComplete, $onComplete); 44 | 45 | return $process; 46 | } 47 | 48 | /** 49 | * @param null|int $processLimit 50 | */ 51 | public function setProcessLimit($processLimit) 52 | { 53 | $this->processLimit = $processLimit; 54 | } 55 | 56 | /** 57 | * @param double $timeout OPTIONAL 58 | * @throws TimeoutException 59 | */ 60 | public function wait($timeout = null) 61 | { 62 | if ($timeout) { 63 | $absoluteTimeout = microtime(true) + $timeout; 64 | 65 | while ($this->activeProcesses->count() || $this->queue->count()) { 66 | $this->refreshAllProcesses(); 67 | 68 | if (microtime(true) >= $absoluteTimeout) { 69 | throw new TimeoutException; 70 | } 71 | 72 | usleep(1000); 73 | } 74 | } else { 75 | while ($this->activeProcesses->count() || $this->queue->count()) { 76 | $this->refreshAllProcesses(); 77 | usleep(1000); 78 | } 79 | } 80 | } 81 | 82 | public function refreshAllProcesses() 83 | { 84 | foreach ($this->activeProcesses as $process) { 85 | $process->getStatus(true); 86 | } 87 | } 88 | 89 | private function createProcess($command, array $options) 90 | { 91 | $futureExitCode = new FutureValue($this->runUntilFutureRealisedFn); 92 | 93 | $canStartProcessFn = $this->canStartProcessFn; 94 | if (!$canStartProcessFn()) { 95 | $queueSlot = new FutureValue($this->runUntilFutureRealisedFn); 96 | $process = new FutureProcess($command, $options, $futureExitCode, $queueSlot); 97 | $this->queue->enqueue($queueSlot); 98 | } else { 99 | $process = new FutureProcess($command, $options, $futureExitCode); 100 | } 101 | 102 | return $process; 103 | } 104 | 105 | private function createCanStartProcessFn() 106 | { 107 | $activeProcesses = $this->activeProcesses; 108 | $processLimit = &$this->processLimit; 109 | 110 | return function () use ($activeProcesses, &$processLimit) { 111 | return (!$processLimit || $activeProcesses->count() < $processLimit); 112 | }; 113 | } 114 | 115 | private function createHandleQueueFn() 116 | { 117 | $queue = $this->queue; 118 | $canStartProcessFn = $this->canStartProcessFn; 119 | 120 | return function () use ($queue, $canStartProcessFn) { 121 | while ($queue->count() && $canStartProcessFn()) { 122 | $queue->dequeue()->resolve(); 123 | } 124 | }; 125 | } 126 | 127 | private function createRunUntilFutureRealisedFn() 128 | { 129 | $activeProcesses = $this->activeProcesses; 130 | 131 | return function ($timeout, $futureValue) use ($activeProcesses) { 132 | $absoluteTimeout = $timeout ? microtime(true) + $timeout : null; 133 | 134 | while (!$futureValue->isRealised()) { 135 | foreach ($activeProcesses as $process) { 136 | $process->getStatus(true); 137 | if ($futureValue->isRealised()) { 138 | return; 139 | } 140 | } 141 | 142 | if ($absoluteTimeout && microtime(true) >= $absoluteTimeout) { 143 | throw new TimeoutException; 144 | } 145 | 146 | usleep(1000); 147 | } 148 | }; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/TimeoutException.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | class TimeoutException extends \RuntimeException 8 | { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /tests/IntegrationTest.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | class IntegrationTest extends \PHPUnit_Framework_TestCase 10 | { 11 | private $phpExecutablePath; 12 | 13 | public function __construct($name = null, array $data = array(), $dataName = '') 14 | { 15 | parent::__construct($name, $data, $dataName); 16 | 17 | $finder = new PhpExecutableFinder; 18 | $this->phpExecutablePath = $finder->find(); 19 | } 20 | 21 | public function testLargeIO() 22 | { 23 | $shell = new Shell; 24 | 25 | $process = $shell->startProcess(sprintf('%s -r %s', 26 | $this->phpExecutablePath, 27 | escapeshellarg( 28 | '$stdin = fopen("php://stdin", "r");' . 29 | '$data = "";' . 30 | 'while (strlen($data) < 10000000) {' . 31 | '$data .= fread($stdin, 2 << 18);' . 32 | '}' . 33 | 'echo $data;' 34 | ) 35 | )); 36 | 37 | $process->writeToPipe(0, str_repeat("X", 10000000)); 38 | $process->getResult()->wait(5); 39 | $this->assertSame(0, $process->getResult()->getExitCode()); 40 | $this->assertSame(10000000, strlen($process->readFromPipe(1))); 41 | } 42 | 43 | public function testProcessTimeLimitExceeded() 44 | { 45 | $shell = new Shell; 46 | 47 | $command = sprintf('%s -r %s', 48 | $this->phpExecutablePath, 49 | escapeshellarg( 50 | 'echo "Hello world!";' . 51 | 'sleep(1);' . 52 | 'echo "Goodbye world!";' 53 | ) 54 | ); 55 | 56 | $thrown = new Exception; 57 | 58 | $process = $shell->startProcess($command, array( 59 | 'timeout' => 0.5, 60 | 'timeout_error' => $thrown, 61 | )); 62 | 63 | $process->wait(1); 64 | 65 | try { 66 | $process->getResult()->wait(2); 67 | $this->fail('The expected exception was not thrown'); 68 | } catch (Exception $caught) { 69 | $this->assertSame($thrown, $caught); 70 | } 71 | 72 | $process->wait(1); 73 | 74 | $this->assertSame('Hello world!', $process->readFromPipe(1)); 75 | } 76 | 77 | public function testQuietlyAbortRunningProcess() 78 | { 79 | $shell = new Shell; 80 | $process = $shell->startProcess($this->phpSleepCommand(0.5)); 81 | 82 | $process->then(function ($process) { 83 | $process->abort(); 84 | }); 85 | 86 | $process->wait(0.5); // this should not error 87 | $process->getResult()->wait(1); // this should not error 88 | $process->wait(0); // this should not error 89 | 90 | $that = $this; 91 | $processPromiseResolved = false; 92 | $process->then( 93 | function () use (&$processPromiseResolved) { 94 | $processPromiseResolved = true; 95 | }, 96 | function () use ($that) { 97 | $that->fail(); 98 | } 99 | ); 100 | $this->assertTrue($processPromiseResolved); 101 | 102 | $processPromiseResolved = false; 103 | $process->then( 104 | function () use (&$processPromiseResolved) { 105 | $processPromiseResolved = true; 106 | }, 107 | function () use ($that) { 108 | $that->fail(); 109 | } 110 | ); 111 | $this->assertTrue($processPromiseResolved); 112 | } 113 | 114 | public function testQuietlyAbortQueuedProcess() 115 | { 116 | $shell = new Shell; 117 | $shell->setProcessLimit(1); 118 | $process1 = $shell->startProcess($this->phpSleepCommand(0.5)); 119 | $process2 = $shell->startProcess($this->phpSleepCommand(0.5)); 120 | 121 | $this->assertSame(FutureProcess::STATUS_RUNNING, $process1->getStatus()); 122 | $this->assertSame(FutureProcess::STATUS_QUEUED, $process2->getStatus()); 123 | 124 | $process2->abort(); 125 | 126 | $process2->wait(0); 127 | $process2->getResult()->wait(0); 128 | 129 | $that = $this; 130 | $processPromiseResolved = false; 131 | $process2->then( 132 | function () use (&$processPromiseResolved) { 133 | $processPromiseResolved = true; 134 | }, 135 | function () use ($that) { 136 | $that->fail(); 137 | } 138 | ); 139 | $this->assertTrue($processPromiseResolved); 140 | 141 | $processPromiseResolved = false; 142 | $process2->then( 143 | function () use (&$processPromiseResolved) { 144 | $processPromiseResolved = true; 145 | }, 146 | function () use ($that) { 147 | $that->fail(); 148 | } 149 | ); 150 | $this->assertTrue($processPromiseResolved); 151 | } 152 | 153 | public function testLateAbort() 154 | { 155 | $shell = new Shell; 156 | 157 | $process = $shell->startProcess(sprintf('%s -r %s', 158 | $this->phpExecutablePath, 159 | escapeshellarg('echo "Hello world!";') 160 | )); 161 | 162 | $process->getResult()->wait(2); 163 | $process->abort(new Exception); 164 | $process->wait(0); 165 | $process->getResult()->wait(0); // ensure no Exception is thrown 166 | $this->assertSame(FutureProcess::STATUS_EXITED, $process->getStatus(false)); 167 | 168 | $that = $this; 169 | $process->promise()->then(null, function () use ($that) { 170 | $that->fail(); 171 | }); 172 | $process->getResult()->promise()->then(null, function () use ($that) { 173 | $that->fail(); 174 | }); 175 | } 176 | 177 | public function testRepeatAbort() 178 | { 179 | $shell = new Shell; 180 | 181 | $process = $shell->startProcess(sprintf('%s -r %s', 182 | $this->phpExecutablePath, 183 | escapeshellarg('echo fread(fopen("php://stdin", "r"), 20);') 184 | )); 185 | 186 | $process->wait(0.1); 187 | $process->abort(new Exception(null, 1)); 188 | $process->abort(new Exception(null, 2)); 189 | 190 | try { 191 | $process->getResult()->wait(0); 192 | $this->fail('The expected exception was not thrown'); 193 | } catch (Exception $e) { 194 | $this->assertSame(1, $e->getCode()); 195 | } 196 | } 197 | 198 | public function testProcessGetPipeDescriptorValidation() 199 | { 200 | $shell = new Shell; 201 | 202 | $process = $shell->startProcess(sprintf('%s -r %s', 203 | $this->phpExecutablePath, 204 | escapeshellarg('usleep(100000); echo "Hello world!";') 205 | )); 206 | 207 | $process->writeToPipe(0, 'Hello!'); 208 | try { 209 | $process->writeToPipe(5, 'Hello!'); 210 | $this->fail('The expected exception was not thrown'); 211 | } catch (\RuntimeException $e) { 212 | 213 | } 214 | 215 | $process->readFromPipe(1); 216 | try { 217 | $process->readFromPipe(5); 218 | $this->fail('The expected exception was not thrown'); 219 | } catch (\RuntimeException $e) { 220 | 221 | } 222 | 223 | $process->getPipe(1); 224 | try { 225 | $process->getPipe(5); 226 | $this->fail('The expected exception was not thrown'); 227 | } catch (\RuntimeException $e) { 228 | 229 | } 230 | } 231 | 232 | public function testReadFromPipeAndBuffer() 233 | { 234 | $shell = new Shell; 235 | 236 | $process = $shell->startProcess(sprintf('%s -r %s', 237 | $this->phpExecutablePath, 238 | escapeshellarg('echo "Hello world!"; usleep(100000); echo "Goodbye world!";') 239 | )); 240 | 241 | $this->assertSame('Hello world!', fread($process->getPipe(1), 20)); 242 | $this->assertSame(0, $process->getResult()->getExitCode()); 243 | $this->assertSame('Goodbye world!', $process->readFromPipe(1)); 244 | $this->assertSame('', $process->getResult()->readFromPipe(1)); 245 | } 246 | 247 | public function testReadFromPipe() 248 | { 249 | $shell = new Shell; 250 | 251 | $process = $shell->startProcess(sprintf('%s -r %s', 252 | $this->phpExecutablePath, 253 | escapeshellarg('usleep(100000); echo "Hello world!";') 254 | )); 255 | 256 | $this->assertSame('Hello world!', fread($process->getPipe(1), 20)); 257 | } 258 | 259 | public function testWriteToPipe() 260 | { 261 | $shell = new Shell; 262 | $process = $shell->startProcess(sprintf('%s -r %s', 263 | $this->phpExecutablePath, 264 | escapeshellarg('echo fread(fopen("php://stdin", "r"), 20);') 265 | )); 266 | 267 | fwrite($process->getPipe(0), "Hello world!\n"); 268 | 269 | $result = $process->getResult()->wait(2); 270 | 271 | $this->assertSame(0, $result->getExitCode(), $result->readFromPipe(2)); 272 | $this->assertSame("Hello world!\n", $result->readFromPipe(1)); 273 | } 274 | 275 | public function testWriteToStdin() 276 | { 277 | $shell = new Shell; 278 | $process = $shell->startProcess(sprintf('%s -r %s', 279 | $this->phpExecutablePath, 280 | escapeshellarg('echo fread(fopen("php://stdin", "r"), 20);') 281 | )); 282 | 283 | $process->writeToPipe(0, "Hello world!\n"); 284 | 285 | $result = $process->getResult()->wait(2); 286 | 287 | $this->assertSame(0, $result->getExitCode(), $result->readFromPipe(2)); 288 | $this->assertSame("Hello world!\n", $result->readFromPipe(1)); 289 | } 290 | 291 | public function testWriteEmptyStringToStdin() 292 | { 293 | $shell = new Shell; 294 | $process = $shell->startProcess( 295 | "{$this->phpExecutablePath} -r " 296 | . escapeshellarg(implode("\n", array( 297 | '$stdin = fopen("php://stdin", "r");', 298 | 'echo fread($stdin, 20);', 299 | ))) 300 | ); 301 | $process->promise()->then(function ($process) { 302 | $process->writeToPipe(0, '0'); 303 | }); 304 | 305 | $result = $process->getResult()->wait(2); 306 | 307 | $this->assertSame(0, $result->getExitCode(), $result->readFromPipe(2)); 308 | $this->assertSame('0', $result->readFromPipe(1)); 309 | } 310 | 311 | public function testAbortRunningProcess() 312 | { 313 | $shell = new Shell; 314 | $process = $shell->startProcess($this->phpSleepCommand(0.5)); 315 | 316 | $thrown = new Exception; 317 | $process->then(function ($process) use ($thrown) { 318 | $process->abort($thrown); 319 | }); 320 | 321 | $process->wait(0.5); // this should not error 322 | 323 | try { 324 | $process->getResult()->wait(1); 325 | $this->fail('Expected Exception was not thrown'); 326 | } catch (\Exception $caught) { 327 | $this->assertSame($thrown, $caught); 328 | } 329 | 330 | $process->wait(0); // this should not error 331 | 332 | $that = $this; 333 | $processPromiseResolved = false; 334 | $process->then( 335 | function () use (&$processPromiseResolved) { 336 | $processPromiseResolved = true; 337 | }, 338 | function () use ($that) { 339 | $that->fail(); 340 | } 341 | ); 342 | $this->assertTrue($processPromiseResolved); 343 | 344 | $resultPromiseRejected = false; 345 | $process->getResult()->promise()->then( 346 | function () use ($that) { 347 | $that->fail(); 348 | }, 349 | function () use (&$resultPromiseRejected) { 350 | $resultPromiseRejected = true; 351 | } 352 | ); 353 | $this->assertTrue($resultPromiseRejected); 354 | } 355 | 356 | public function testAbortQueuedProcess() 357 | { 358 | $shell = new Shell; 359 | $shell->setProcessLimit(1); 360 | $process1 = $shell->startProcess($this->phpSleepCommand(0.5)); 361 | $process2 = $shell->startProcess($this->phpSleepCommand(0.5)); 362 | 363 | $this->assertSame(FutureProcess::STATUS_RUNNING, $process1->getStatus()); 364 | $this->assertSame(FutureProcess::STATUS_QUEUED, $process2->getStatus()); 365 | 366 | $thrown = new Exception; 367 | $process2->abort($thrown); 368 | 369 | try { 370 | $process2->wait(0); 371 | $this->fail('Expected Exception was not thrown'); 372 | } catch (Exception $caught) { 373 | $this->assertSame($thrown, $caught); 374 | } 375 | 376 | try { 377 | $process2->getResult()->wait(0); 378 | $this->fail('Expected Exception was not thrown'); 379 | } catch (\Exception $caught) { 380 | $this->assertSame($thrown, $caught); 381 | } 382 | 383 | $processPromiseError = null; 384 | $process2->then(null, function ($caught) use (&$processPromiseError) { 385 | $processPromiseError = $caught; 386 | }); 387 | $this->assertSame($thrown, $processPromiseError); 388 | 389 | $resultPromiseError = null; 390 | $process2->getResult()->then(null, function ($caught) use (&$resultPromiseError) { 391 | $resultPromiseError = $caught; 392 | }); 393 | $this->assertSame($thrown, $resultPromiseError); 394 | } 395 | 396 | public function testPHPHelloWorld() 397 | { 398 | $shell = new Shell; 399 | $command = "{$this->phpExecutablePath} -r \"echo 'Hello World';\""; 400 | $result = $shell->startProcess($command)->getResult()->wait(2); 401 | 402 | $this->assertSame(0, $result->getExitCode(), $result->readFromPipe(2)); 403 | $this->assertSame('Hello World', $result->readFromPipe(1)); 404 | } 405 | 406 | public function testPartiallyDrainReadBuffer() 407 | { 408 | $shell = new Shell; 409 | $command = "{$this->phpExecutablePath} -r \"echo 'Hello World';\""; 410 | $process = $shell->startProcess($command); 411 | $result = $process->getResult()->wait(2); 412 | 413 | $this->assertSame(0, $result->getExitCode(), $result->readFromPipe(2)); 414 | $this->assertSame('Hello', $result->readFromPipe(1, 5)); 415 | $this->assertSame(' Wo', $process->readFromPipe(1, 3)); 416 | $this->assertSame('rld', $result->readFromPipe(1, 3)); 417 | } 418 | 419 | public function testExecuteCommandWithTimeout() 420 | { 421 | $shell = new Shell; 422 | $command = $this->phpSleepCommand(0.1); 423 | 424 | $startTime = microtime(true); 425 | try { 426 | $shell->startProcess($command)->getResult()->wait(0.05); 427 | $this->fail('Expected TimeoutException was not thrown'); 428 | } catch (TimeoutException $e) { 429 | $runTime = microtime(true) - $startTime; 430 | $this->assertGreaterThanOrEqual(0.05, $runTime); 431 | } 432 | 433 | $result = $shell->startProcess($command)->getResult()->wait(0.5); 434 | $this->assertSame(0, $result->getExitCode()); 435 | } 436 | 437 | public function testQueue() 438 | { 439 | $shell = new Shell; 440 | $shell->setProcessLimit(2); 441 | 442 | $process1 = $shell->startProcess($this->phpSleepCommand(0.5)); 443 | $process2 = $shell->startProcess($this->phpSleepCommand(0.5)); 444 | $process3 = $shell->startProcess($this->phpSleepCommand(0.5)); 445 | 446 | usleep(100000); 447 | 448 | $this->assertSame(FutureProcess::STATUS_RUNNING, $process1->getStatus()); 449 | $this->assertSame(FutureProcess::STATUS_RUNNING, $process2->getStatus()); 450 | $this->assertSame(FutureProcess::STATUS_QUEUED, $process3->getStatus()); 451 | 452 | $this->assertSame(FutureProcess::STATUS_RUNNING, $process3->wait(3)->getStatus()); 453 | } 454 | 455 | public function testAwaitShell() 456 | { 457 | $shell = new Shell; 458 | $shell->setProcessLimit(2); 459 | 460 | $command = sprintf('%s -r %s', 461 | $this->phpExecutablePath, 462 | escapeshellarg( 463 | 'usleep(100000);' . 464 | 'echo "Hello world!";' 465 | ) 466 | ); 467 | 468 | $processes = array(); 469 | 470 | for ($i = 0; $i < 4; $i++) { 471 | $processes[$i] = $shell->startProcess($command); 472 | } 473 | 474 | $shell->wait(5); 475 | 476 | foreach ($processes as $process) { 477 | $this->assertSame(FutureProcess::STATUS_EXITED, $process->getStatus(false)); 478 | $this->assertSame('Hello world!', $process->readFromPipe(1)); 479 | } 480 | } 481 | 482 | public function testShellTimeout() 483 | { 484 | $shell = new Shell; 485 | $shell->setProcessLimit(1); 486 | 487 | $command1 = sprintf('%s -r %s', 488 | $this->phpExecutablePath, 489 | escapeshellarg( 490 | 'echo "Hello world!";' 491 | ) 492 | ); 493 | 494 | $command2 = sprintf('%s -r %s', 495 | $this->phpExecutablePath, 496 | escapeshellarg( 497 | 'sleep(5);' . 498 | 'echo "Hello world!";' 499 | ) 500 | ); 501 | 502 | $process1 = $shell->startProcess($command1); 503 | $process2 = $shell->startProcess($command2); 504 | 505 | try { 506 | $shell->wait(0.5); 507 | $this->fail('The expected exception was not thrown'); 508 | } catch (TimeoutException $e) { 509 | 510 | } 511 | 512 | $this->assertSame(FutureProcess::STATUS_EXITED, $process1->getStatus(false)); 513 | $this->assertSame('Hello world!', $process1->readFromPipe(1)); 514 | 515 | $this->assertSame(FutureProcess::STATUS_RUNNING, $process2->getStatus(false)); 516 | $process2->abort(); 517 | } 518 | 519 | public function testGetPid() 520 | { 521 | $shell = new Shell; 522 | 523 | $process = $shell->startProcess("{$this->phpExecutablePath} -r \"echo getmypid();\""); 524 | 525 | $reportedPid = $process->getPid(); 526 | 527 | $actualPid = (int)$process->getResult()->readFromPipe(1); 528 | 529 | $this->assertSame($actualPid, $reportedPid); 530 | } 531 | 532 | public function testLateStreamResolution() 533 | { 534 | $shell = new Shell; 535 | 536 | $result = $shell->startProcess("{$this->phpExecutablePath} -r \"echo 'hello';\"") 537 | ->getResult(); 538 | 539 | $output = null; 540 | $result->then(function ($result) use (&$output) { 541 | $output = $result->readFromPipe(1); 542 | }); 543 | 544 | $result->wait(2); 545 | 546 | $this->assertSame('hello', $output); 547 | } 548 | 549 | public function testBufferFill() 550 | { 551 | $shell = new Shell; 552 | 553 | $result = $shell->startProcess("php -r \"echo str_repeat('x', 100000);\"") 554 | ->getResult(); 555 | 556 | try { 557 | $result->wait(0.5); 558 | } catch (TimeoutException $e) { 559 | $this->fail('The child process is blocked. The output buffer is probably full.'); 560 | } 561 | 562 | $this->assertSame(100000, strlen($result->readFromPipe(1))); 563 | } 564 | 565 | public function testRepeatedReadCalls() 566 | { 567 | $shell = new Shell; 568 | $command = "{$this->phpExecutablePath} -r \"echo 'Hello World';\""; 569 | $result = $shell->startProcess($command)->getResult()->wait(2); 570 | 571 | $this->assertSame(0, $result->getExitCode(), $result->readFromPipe(2)); 572 | $this->assertSame('Hello World', $result->readFromPipe(1)); 573 | $this->assertSame('', $result->readFromPipe(1)); 574 | } 575 | 576 | private function phpSleepCommand($seconds) 577 | { 578 | $microSeconds = $seconds * 1000000; 579 | 580 | return "{$this->phpExecutablePath} -r \"usleep($microSeconds);\""; 581 | } 582 | } 583 | 584 | class Exception extends \Exception 585 | { 586 | 587 | } 588 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 |