├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml.dist ├── src ├── CommandLine.php ├── Consumer │ └── ConsumerListener.php ├── Event │ ├── AbstractInputLineEvent.php │ ├── AbstractProcessEvent.php │ ├── ChannelIsWaitingEvent.php │ ├── EmptiedQueueEvent.php │ ├── EventsName.php │ ├── FrozenQueueEvent.php │ ├── InputLineDequeuedEvent.php │ ├── InputLineEnqueuedEvent.php │ ├── LoopCompletedEvent.php │ ├── LoopStartedEvent.php │ ├── ProcessCompletedEvent.php │ ├── ProcessGeneratedBufferEvent.php │ └── ProcessStartedEvent.php ├── Exception │ ├── AbstractRuntimeException.php │ ├── InvalidArgumentException.php │ ├── LoopAlreadyStartedException.php │ ├── StdInMustBeAValidResourceException.php │ └── TheQueueMustNotBeFrozenToEnqueueException.php ├── Process │ ├── Channel │ │ ├── Channel.php │ │ └── Channels.php │ ├── ClosureProcess.php │ ├── ClosureProcessFactory.php │ ├── ClosureReturnValue.php │ ├── Process.php │ ├── ProcessEnvironment.php │ ├── ProcessFactory.php │ ├── ProcessFactoryInterface.php │ ├── ProcessInterface.php │ └── Processes.php ├── ProcessorCounter.php ├── Producer │ ├── ProducerInterface.php │ └── StdInProducer.php ├── Queue │ ├── EventDispatcherQueue.php │ ├── QueueInterface.php │ └── SplQueue.php ├── Spawn.php ├── SpawnLoop.php └── UI │ └── StdOutUISubscriber.php └── tests ├── Consumer └── ConsumerListenerTest.php ├── Fixture └── proc_cpuinfo ├── Process ├── Channel │ └── ChannelsTest.php ├── ClosureReturnValueTest.php ├── ProcessTest.php └── ProcessesTest.php ├── ProcessorCounterTest.php ├── Producer └── StdInProducerTest.php ├── SpawnTest.php └── spawn_tests.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | composer.lock 3 | bin 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | sudo: false 4 | 5 | cache: 6 | directories: 7 | - $HOME/.composer/cache 8 | 9 | php: 10 | - 5.4 11 | - 5.5 12 | - 5.6 13 | - 7.0 14 | - hhvm 15 | 16 | matrix: 17 | allow_failures: 18 | - php: hhvm 19 | 20 | before_install: 21 | - composer self-update 22 | 23 | install: 24 | - composer --dev require fabpot/php-cs-fixer --no-update 25 | - composer update 26 | 27 | script: 28 | - php tests/spawn_tests.php 29 | - vendor/fabpot/php-cs-fixer/php-cs-fixer fix --level=symfony --dry-run --diff src 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2004-2015 Giulio De Donato 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Concurrent handling of PHP processes (and also closures) 2 | ======================================================== 3 | 4 | [![Build Status](https://travis-ci.org/liuggio/spawn.svg)](https://travis-ci.org/liuggio/spawn) 5 | 6 | The main job of Spawn is to improve the performance handling concurrent processes. 7 | 8 | This library tries to do its best to speedup things using all the cores of the machine 9 | useful when you need to run bunch of commands 10 | eg: unit-tests/functional-tests/CS fixes/files handling 11 | 12 | ``` php 13 | $spawn = new Spawn(); 14 | $spawn 15 | ->processes(range(1,10), "printenv > '/tmp/envs_{}{p}.log';") 16 | ->onCompleted(function(Process $singleProcess){ /* print stats */}); 17 | ->start(); 18 | ``` 19 | 20 | ## Concurrent \Closure? Really? 21 | 22 | Yes! With this library you can use concurrent closures, 23 | **BUT** PHP is not `Go-lang` neither `Erlang` or any other famous language for concurrency, 24 | and in order to simulate a isolated routine the closure is serialized and executed in a new PhpProcess, 25 | be aware this is a workaround in order to speed up your concurrent Closures. 26 | 27 | With this library you can also do: 28 | 29 | 1. executes and handles **concurrent PHP closures**. 30 | 2. **spawns** a single closure as an independent process. 31 | 32 | ### Concurrent closures: Upload images to your CDN 33 | 34 | Feed an iterator and it will break the job into multiple php scripts and spread them across many processes. 35 | In order to improve performances, the number of processes is equal to the number of computer's cores. 36 | 37 | ``` php 38 | $spawn = new Spawn(); 39 | 40 | $files = new RecursiveDirectoryIterator('/path/to/images'); 41 | $files = new RecursiveIteratorIterator($files); 42 | 43 | $spawn->closures($files, function(SplFileInfo $file) { 44 | // upload this file 45 | }) 46 | ->start(); 47 | ``` 48 | 49 | Each closure is executed in isolation using the [PhpProcess](http://symfony.com/doc/current/components/process.html#executing-php-code-in-isolation) component. 50 | 51 | ### Spawn a single isolated closure 52 | 53 | ``` php 54 | $spawn = new Spawn(); 55 | $sum = 3; 56 | 57 | $processes = $spawn 58 | ->spawn(["super", 120], function($prefix, $number) use ($sum) { 59 | echo $prefix." heavy routine"; 60 | return $number+$sum; 61 | }); 62 | 63 | // do something else here 64 | echo $processes->wait(); // 123 65 | echo $processes->getOutput(); // "super heavy routine" 66 | ``` 67 | 68 | ### Advanced 69 | 70 | 1. The callable is executed in a new isolated processes also with its "use" references. 71 | 2. It's possible to add a listener for event handling. 72 | 3. It's possible to get the return value of each callable, the ErrorOutput, the Output and other information. 73 | 74 | ``` php 75 | $collaborator = new YourCollaborator(1,2,3,4); 76 | 77 | $spawn 78 | ->closures(range(1, 7), function($input) use ($collaborator) { 79 | echo "this is the echo"; 80 | $collaborator->doSomething(); 81 | $return = new \stdClass(); 82 | $return->name = "name"; 83 | 84 | return $return; 85 | }) 86 | ->onCompleted(function(ClosureProcess $process){ 87 | // do something with 88 | $returnValue = $processes->getReturnValue(); 89 | $output = $processes->getOutput(); 90 | $errorOutput = $processes->getErrorOutput(); 91 | $time = $processes->startAt(); 92 | $memory = $processes->getMemory(); 93 | $duration = $processes->getDuration(); 94 | }) 95 | ->start(); 96 | ``` 97 | 98 | ### Events: 99 | 100 | Listeners can be attached to `closures` and `processes`. 101 | 102 | ``` php 103 | ->onStarted(function(ClosureProcess|Process $process){}); 104 | ->onCompleted(function(ClosureProcess|Process $process){}); 105 | ->onSuccessful(function(ClosureProcess|Process $process){}); 106 | ->onEmptyIterator(function (){}); 107 | ->onPartialOutput(function(ClosureProcess|Process $process){}) 108 | ->onLoopCompleted(function ($exitCode, StopwatchEvent $event) 109 | ``` 110 | 111 | ### Other libs: 112 | 113 | There are not so many libraries that handle concurrent processes. 114 | The best I found is about forking processes [spork](https://github.com/kriswallsmith/spork) 115 | it features a great API and with no work-around but it needs several PHP extensions. 116 | 117 | ### License: 118 | 119 | MIT License see the [License](./LICENSE). 120 | 121 | ### More fun? 122 | 123 | - see how the [travis.yml](./.travis.yml#16) run test suite with [spawn_tests](./tests/spawn_tests.php). 124 | - have fun with [fastest](https://github.com/liuggio/fastest) 125 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "liuggio/spawn", 3 | "description": "Concurrent processing of closures and commands in PHP with ease.", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "liuggio", 8 | "email": "liuggio@gmail.com" 9 | } 10 | ], 11 | "require": { 12 | "symfony/stopwatch": "^2.2", 13 | "symfony/process": "^2.2", 14 | "symfony/event-dispatcher": "^2.7", 15 | "jeremeamia/superclosure": "^2.1" 16 | }, 17 | "require-dev": { 18 | "phpunit/phpunit": "^4" 19 | }, 20 | "config": { 21 | "bin-dir": "bin/" 22 | }, 23 | "autoload": { 24 | "psr-4": { 25 | "Liuggio\\Spawn\\": "src" 26 | } 27 | }, 28 | "autoload-dev": { 29 | "psr-4": { 30 | "Liuggio\\Spawn\\": "tests" 31 | } 32 | }, 33 | "extra": { 34 | "branch-alias": { 35 | "dev-events": "0.0-dev" 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | tests 7 | 8 | 9 | 10 | 11 | 12 | src 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/CommandLine.php: -------------------------------------------------------------------------------- 1 | commandValue = (string) $commandValue; 25 | } 26 | 27 | /** 28 | * Creates a new CommandLine given a line string. 29 | * 30 | * @param string $commandValue 31 | * 32 | * @return static 33 | */ 34 | public static function fromString($commandValue) 35 | { 36 | return new self($commandValue); 37 | } 38 | 39 | /** 40 | * @return CommandLine 41 | */ 42 | public static function createDefault() 43 | { 44 | return new self(self::DEFAULT_COMMAND_TO_EXECUTE_TPL); 45 | } 46 | 47 | /** 48 | * @return string 49 | */ 50 | public function __toString() 51 | { 52 | return $this->commandValue; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Consumer/ConsumerListener.php: -------------------------------------------------------------------------------- 1 | queue = $queue; 59 | $this->eventDispatcher = $eventDispatcher; 60 | $this->processFactory = $processFactory; 61 | $this->cwd = $cwd; 62 | $this->template = $template; 63 | } 64 | 65 | /** 66 | * @param ChannelIsWaitingEvent $event 67 | */ 68 | public function onChannelIsWaiting(ChannelIsWaitingEvent $event) 69 | { 70 | $channel = $event->getChannel(); 71 | $event->stopPropagation(); 72 | 73 | $value = null; 74 | $isEmpty = true; 75 | while ($isEmpty) { 76 | try { 77 | $value = $this->queue->dequeue(); 78 | $isEmpty = false; 79 | } catch (\RuntimeException $e) { 80 | $isEmpty = true; 81 | } 82 | if ($isEmpty && $this->queue->isFrozen()) { 83 | return; 84 | } 85 | if ($isEmpty) { 86 | usleep(200); 87 | } 88 | } 89 | ++$this->processCounter; 90 | 91 | $process = $this->processFactory->create( 92 | $channel, 93 | $value, 94 | $this->processCounter, 95 | $this->template, 96 | $this->cwd 97 | ); 98 | $process->start(); 99 | 100 | $this->eventDispatcher->dispatch(EventsName::PROCESS_STARTED, new ProcessStartedEvent($process)); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Event/AbstractInputLineEvent.php: -------------------------------------------------------------------------------- 1 | inputLine = $command; 22 | } 23 | 24 | /** 25 | * @return mixed 26 | */ 27 | public function getInputLine() 28 | { 29 | return $this->inputLine; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Event/AbstractProcessEvent.php: -------------------------------------------------------------------------------- 1 | process = $process; 21 | } 22 | 23 | /** 24 | * @return ProcessInterface 25 | */ 26 | public function getProcess() 27 | { 28 | return $this->process; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Event/ChannelIsWaitingEvent.php: -------------------------------------------------------------------------------- 1 | channel = $channel; 23 | } 24 | 25 | /** 26 | * @return Channel 27 | */ 28 | public function getChannel() 29 | { 30 | return $this->channel; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Event/EmptiedQueueEvent.php: -------------------------------------------------------------------------------- 1 | stopwatchEvent = $stopwatchEvent; 29 | $this->exitCode = (int) $exitCode; 30 | } 31 | 32 | /** 33 | * @return StopwatchEvent 34 | */ 35 | public function getStopwatchEvent() 36 | { 37 | return $this->stopwatchEvent; 38 | } 39 | 40 | /** 41 | * @return int 42 | */ 43 | public function getExitCode() 44 | { 45 | return $this->exitCode; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Event/LoopStartedEvent.php: -------------------------------------------------------------------------------- 1 | channelsNumber = (int) $channelsNumber; 22 | } 23 | 24 | /** 25 | * @return int 26 | */ 27 | public function getChannelsNumber() 28 | { 29 | return $this->channelsNumber; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Event/ProcessCompletedEvent.php: -------------------------------------------------------------------------------- 1 | process = $process; 21 | } 22 | 23 | /** 24 | * @return ProcessInterface 25 | */ 26 | public function getProcess() 27 | { 28 | return $this->process; 29 | } 30 | 31 | /** 32 | * @return string 33 | */ 34 | public function getIncrementalOutput() 35 | { 36 | return $this->process->getIncrementalOutput(); 37 | } 38 | 39 | /** 40 | * @return string 41 | */ 42 | public function getIncrementalErrorOutput() 43 | { 44 | return $this->process->getIncrementalErrorOutput(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Event/ProcessStartedEvent.php: -------------------------------------------------------------------------------- 1 | channelId = $channelId; 39 | $this->channelsNumber = $channelsNumber; 40 | $this->assignedProcessesCounter = (int) $commandsCounter; 41 | $this->process = $process; 42 | } 43 | 44 | /** 45 | * Creates a channel. 46 | * 47 | * @param int $id 48 | * @param int $channelsNumber 49 | * 50 | * @return Channel 51 | */ 52 | public static function createAWaiting($id, $channelsNumber) 53 | { 54 | return new self($id, $channelsNumber, 0, null); 55 | } 56 | 57 | /** 58 | * Assigns a channel, incrementing the command line counter. 59 | * 60 | * @param Process|ClosureProcess $process 61 | * 62 | * @return Channel 63 | */ 64 | public function assignToAProcess($process) 65 | { 66 | return new self($this->getId(), $this->channelsNumber, 1 + $this->getAssignedProcessesCounter(), $process); 67 | } 68 | 69 | /** 70 | * The Channel is not assigned, and is waiting a Process to Run. 71 | * 72 | * @return Channel 73 | */ 74 | public function setIsWaiting() 75 | { 76 | return new self($this->getId(), $this->channelsNumber, $this->getAssignedProcessesCounter(), null); 77 | } 78 | 79 | /** 80 | * True if the Channel is free to be assigned. 81 | * 82 | * @return bool 83 | */ 84 | public function isWaiting() 85 | { 86 | return null === $this->process; 87 | } 88 | 89 | /** 90 | * True if we can assign a BeforeCommandLine for this Channel, 91 | * and the before command is the first on channel. 92 | * 93 | * @return bool 94 | */ 95 | public function isPossibleToAssignABeforeCommand() 96 | { 97 | return $this->isWaiting() && $this->assignedProcessesCounter == 0; 98 | } 99 | 100 | /** 101 | * Get The Channel identifier. 102 | * 103 | * @return int 104 | */ 105 | public function getId() 106 | { 107 | return $this->channelId; 108 | } 109 | 110 | /** 111 | * @return int 112 | */ 113 | public function getChannelsNumber() 114 | { 115 | return $this->channelsNumber; 116 | } 117 | 118 | /** 119 | * @return Process 120 | */ 121 | public function getProcess() 122 | { 123 | return $this->process; 124 | } 125 | 126 | /** 127 | * @return int 128 | */ 129 | public function getAssignedProcessesCounter() 130 | { 131 | return $this->assignedProcessesCounter; 132 | } 133 | 134 | /** 135 | * @return string 136 | */ 137 | public function __toString() 138 | { 139 | return (string) $this->channelId; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/Process/Channel/Channels.php: -------------------------------------------------------------------------------- 1 | channels[$channel->getId()] = $channel; 23 | } 24 | } 25 | 26 | /** 27 | * Creates a bunch of waiting channels. 28 | * 29 | * @param int $channelsNumber 30 | * 31 | * @return Channels 32 | */ 33 | public static function createWaiting($channelsNumber) 34 | { 35 | return new self($channelsNumber); 36 | } 37 | 38 | /** 39 | * Assign a channel to a processes. 40 | * 41 | * @param Channel $channel 42 | * @param ProcessInterface $process 43 | */ 44 | public function assignAProcess(Channel $channel, ProcessInterface $process) 45 | { 46 | $this->channels[$channel->getId()] = $channel->assignToAProcess($process); 47 | } 48 | 49 | /** 50 | * Free a channel. 51 | * 52 | * @param Channel $channel 53 | */ 54 | public function setEmpty(Channel $channel) 55 | { 56 | $this->channels[$channel->getId()] = $channel->setIsWaiting(); 57 | } 58 | 59 | /** 60 | * Array of all the waiting channels. 61 | * 62 | * @return Channel[] 63 | */ 64 | public function getWaitingChannels() 65 | { 66 | return array_values(array_filter($this->channels, function (Channel $channel) { 67 | return $channel->isWaiting(); 68 | })); 69 | } 70 | 71 | /** 72 | * Array of all the assigned channels. 73 | * 74 | * @return Channel[] 75 | */ 76 | public function getAssignedChannels() 77 | { 78 | return array_values(array_filter($this->channels, function (Channel $channel) { 79 | return !$channel->isWaiting(); 80 | })); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Process/ClosureProcess.php: -------------------------------------------------------------------------------- 1 | processEnvironment = $processEnvironment; 29 | parent::__construct((string) $script, null, $this->processEnvironment->exportToEnvsArray()); 30 | if (!$timeout) { 31 | $this->setTimeout($timeout); 32 | // compatibility to SF 2.2 33 | if (method_exists($this, 'setIdleTimeout')) { 34 | $this->setIdleTimeout($timeout); 35 | } 36 | } 37 | } 38 | 39 | /** 40 | * @return mixed 41 | */ 42 | public function getInputLine() 43 | { 44 | return $this->processEnvironment->getArguments(); 45 | } 46 | 47 | /** 48 | * Waits for the process to terminate. 49 | * 50 | * The callback receives the type of output (out or err) and some bytes 51 | * from the output in real-time while writing the standard input to the process. 52 | * It allows to have feedback from the independent process during execution. 53 | * 54 | * @param callable|null $callback A valid PHP callback 55 | * 56 | * @throws RuntimeException When process timed out 57 | * @throws RuntimeException When process stopped after receiving signal 58 | * @throws LogicException When process is not yet started 59 | * 60 | * @return int The returnValue of the Closure 61 | */ 62 | public function wait($callback = null) 63 | { 64 | parent::wait($callback); 65 | 66 | return $this->getReturnValue(); 67 | } 68 | 69 | /** 70 | * @return CommandLine 71 | */ 72 | public function getCommandLine() 73 | { 74 | return new CommandLine(parent::getCommandLine()); 75 | } 76 | 77 | /** 78 | * The current Id of the processes. 79 | * 80 | * @return int 81 | */ 82 | public function getIncrementalNumber() 83 | { 84 | return $this->processEnvironment->getIncrementalNumber(); 85 | } 86 | 87 | /** 88 | * The channel where the process is executed. 89 | * 90 | * @return Channel 91 | */ 92 | public function getChannel() 93 | { 94 | return $this->processEnvironment->getChannel(); 95 | } 96 | 97 | /** 98 | * The output of the callable. 99 | * 100 | * @return string 101 | */ 102 | public function getOutput() 103 | { 104 | $value = ClosureReturnValue::unserialize(parent::getOutput()); 105 | 106 | return $value->getOutput(); 107 | } 108 | 109 | /** 110 | * The return value of the callable. 111 | * 112 | * @return mixed 113 | */ 114 | public function getReturnValue() 115 | { 116 | $value = ClosureReturnValue::unserialize(parent::getOutput()); 117 | 118 | return $value->getReturnValue(); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/Process/ClosureProcessFactory.php: -------------------------------------------------------------------------------- 1 | autoload = $autoload; 46 | $this->fnSerializer = $fnSerializer ?: new Serializer(); 47 | $this->templateEngine = $templateEngine ?: $this->createDefaultTemplateEngine($callable); 48 | $this->timeout = $timeout; 49 | } 50 | 51 | /** 52 | * @param Channel $channel 53 | * @param mixed $inputLine 54 | * @param int $processCounter 55 | * @param string|null $template 56 | * @param string|null $cwd 57 | * 58 | * @return ClosureProcess 59 | */ 60 | public function create(Channel $channel, $inputLine, $processCounter, $template = null, $cwd = null) 61 | { 62 | $environment = new ProcessEnvironment($channel, $inputLine, $processCounter); 63 | $engine = $this->templateEngine; 64 | $commandLine = $engine($environment, (string) $template); 65 | if (is_string($commandLine)) { 66 | $commandLine = CommandLine::fromString($commandLine); 67 | } 68 | 69 | return $this->createProcess($commandLine, $environment, $cwd); 70 | } 71 | 72 | /** 73 | * @param CommandLine $commandLine 74 | * @param ProcessEnvironment $environment 75 | * @param string|null $cwd 76 | * 77 | * @return ClosureProcess 78 | */ 79 | private function createProcess(CommandLine $commandLine, ProcessEnvironment $environment, $cwd = null) 80 | { 81 | return new ClosureProcess($commandLine, $environment, $this->timeout, $cwd); 82 | } 83 | 84 | /** 85 | * @param \Closure $closure 86 | * 87 | * @return string 88 | */ 89 | private function createDefaultTemplateEngine(\Closure $closure) 90 | { 91 | return $this->createDefaultFromSerializedClosure($this->fnSerializer->serialize($closure)); 92 | } 93 | 94 | /** 95 | * @param mixed $serializedClosure 96 | * 97 | * @return string 98 | */ 99 | private function createDefaultFromSerializedClosure($serializedClosure) 100 | { 101 | $serializedClosure = base64_encode($serializedClosure); 102 | $autoload = $this->autoload; 103 | 104 | return function (ProcessEnvironment $envs, $template) use ($serializedClosure, $autoload) { 105 | 106 | $arguments = $envs->getArguments(); 107 | $function = 'call_user_func'; 108 | if (is_array($arguments)) { 109 | $function = 'call_user_func_array'; 110 | } 111 | 112 | $input = base64_encode(serialize($envs->getArguments())); 113 | 114 | return sprintf('unserialize(base64_decode("%s")); 119 | $args = unserialize(base64_decode("%s")); 120 | echo $c->stop(%s($fn, $args)); 121 | ', $autoload, $serializedClosure, $input, $function); 122 | }; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Process/ClosureReturnValue.php: -------------------------------------------------------------------------------- 1 | output = (string) $output; 55 | $this->returnValue = $returnValue; 56 | $this->stopWatch = new Stopwatch(); 57 | $this->randomName = uniqid('stop_w'); 58 | $this->duration = (int) $duration; 59 | $this->startAt = (int) $startAt; 60 | $this->memory = (int) $memory; 61 | } 62 | 63 | /** 64 | * @return ClosureReturnValue 65 | */ 66 | public static function start() 67 | { 68 | $obj = new self(); 69 | ob_start(); 70 | $obj->startStopWatch(); 71 | 72 | return $obj; 73 | } 74 | 75 | /** 76 | * @param string|null $returnValue 77 | * 78 | * @return string 79 | */ 80 | public function stop($returnValue = null) 81 | { 82 | $this->output = ob_get_clean(); 83 | $this->returnValue = $returnValue; 84 | $event = $this->stopWatch->stop($this->randomName); 85 | $this->duration = $event->getDuration(); 86 | $this->memory = $event->getMemory(); 87 | $this->startAt = $event->getOrigin(); 88 | 89 | return $this->serialize(); 90 | } 91 | 92 | /** 93 | * @return string 94 | */ 95 | private function serialize() 96 | { 97 | return base64_encode(serialize([$this->output, $this->returnValue, $this->startAt, $this->duration, $this->memory])); 98 | } 99 | 100 | /** 101 | * @param string $serialized 102 | * 103 | * @return ClosureReturnValue 104 | */ 105 | public static function unserialize($serialized) 106 | { 107 | $value = unserialize(base64_decode($serialized)); 108 | 109 | if (!is_array($value) || count(array_intersect_key(array_fill(0, 5, null), $value)) < 5) { 110 | throw new InvalidArgumentException(); 111 | } 112 | 113 | return new self($value[0], $value[1], $value[2], $value[3], $value[4]); 114 | } 115 | 116 | /** 117 | * @return string 118 | */ 119 | public function getOutput() 120 | { 121 | return $this->output; 122 | } 123 | 124 | /** 125 | * @return mixed 126 | */ 127 | public function getReturnValue() 128 | { 129 | return $this->returnValue; 130 | } 131 | 132 | /** 133 | * Start the logging event. 134 | */ 135 | public function startStopWatch() 136 | { 137 | $this->stopWatch->start($this->randomName); 138 | } 139 | 140 | /** 141 | * @return int 142 | */ 143 | public function getDuration() 144 | { 145 | return $this->duration; 146 | } 147 | 148 | /** 149 | * @return int 150 | */ 151 | public function getStartAt() 152 | { 153 | return $this->startAt; 154 | } 155 | 156 | /** 157 | * @return int 158 | */ 159 | public function getMemory() 160 | { 161 | return $this->memory; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/Process/Process.php: -------------------------------------------------------------------------------- 1 | processEnvironment = $processEnvironment; 28 | 29 | parent::__construct((string) $commandLine, $cwd, $this->processEnvironment->exportToEnvsArray()); 30 | if ($timeout) { 31 | $this->setTimeout($timeout); 32 | // compatibility to SF 2.2 33 | if (method_exists($this, 'setIdleTimeout')) { 34 | $this->setIdleTimeout($timeout); 35 | } 36 | } 37 | } 38 | 39 | /** 40 | * @return mixed 41 | */ 42 | public function getInputLine() 43 | { 44 | return new $this->processEnvironment->getInputLine(); 45 | } 46 | 47 | /** 48 | * @return CommandLine 49 | */ 50 | public function getCommandLine() 51 | { 52 | return new CommandLine(parent::getCommandLine()); 53 | } 54 | 55 | /** 56 | * The current Id of the processes. 57 | * 58 | * @return int 59 | */ 60 | public function getIncrementalNumber() 61 | { 62 | return $this->processEnvironment->getIncrementalNumber(); 63 | } 64 | 65 | /** 66 | * The channel where the process is executed. 67 | * 68 | * @return Channel 69 | */ 70 | public function getChannel() 71 | { 72 | return $this->processEnvironment->getChannel(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Process/ProcessEnvironment.php: -------------------------------------------------------------------------------- 1 | channel = $channel; 39 | $this->arguments = $arguments; 40 | $this->incrementNumber = (int) $incrementNumber; 41 | } 42 | 43 | /** 44 | * @return array 45 | */ 46 | public function exportToEnvsArray() 47 | { 48 | return [ 49 | self::ENV_TEST_CHANNEL.'='.$this->channel->getId(), 50 | self::ENV_TEST_CHANNEL_READABLE.'='.$this->getReadableChannel(), 51 | self::ENV_TEST_CHANNELS_NUMBER.'='.$this->getChannelsNumber(), 52 | self::ENV_TEST_ARGUMENT.'='.$this->getArguments(), 53 | self::ENV_TEST_INCREMENTAL_NUMBER.'='.$this->getIncrementalNumber(), 54 | self::ENV_TEST_IS_FIRST_ON_CHANNEL.'='.(int) $this->isTheFirstCommandOnChannel(), 55 | ]; 56 | } 57 | 58 | /** 59 | * @return Channel 60 | */ 61 | public function getChannel() 62 | { 63 | return $this->channel; 64 | } 65 | 66 | /** 67 | * @return Channel 68 | */ 69 | public function getChannelId() 70 | { 71 | return $this->channel->getId(); 72 | } 73 | 74 | /** 75 | * @return mixed 76 | */ 77 | public function getArguments() 78 | { 79 | return $this->arguments; 80 | } 81 | 82 | /** 83 | * @return string 84 | */ 85 | public function getReadableChannel() 86 | { 87 | return 'test_'.(int) $this->channel->getId(); 88 | } 89 | 90 | /** 91 | * @return int 92 | */ 93 | public function getChannelsNumber() 94 | { 95 | return $this->channel->getChannelsNumber(); 96 | } 97 | 98 | /** 99 | * @return int 100 | */ 101 | public function getIncrementalNumber() 102 | { 103 | return $this->incrementNumber; 104 | } 105 | 106 | /** 107 | * @return bool 108 | */ 109 | public function isTheFirstCommandOnChannel() 110 | { 111 | return $this->channel->getAssignedProcessesCounter() == 1; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Process/ProcessFactory.php: -------------------------------------------------------------------------------- 1 | templateEngine = $templateEngine ?: $this->createDefaultTemplateEngine(); 27 | $this->timeout = $timeout; 28 | } 29 | 30 | /** 31 | * @param Channel $channel 32 | * @param mixed $inputLine 33 | * @param int|null $processCounter 34 | * @param string|null $template 35 | * @param string|null $cwd 36 | * 37 | * @return Process 38 | */ 39 | public function create(Channel $channel, $inputLine, $processCounter, $template = null, $cwd = null) 40 | { 41 | $environment = new ProcessEnvironment($channel, $inputLine, $processCounter); 42 | $engine = $this->templateEngine; 43 | $commandLine = $engine($environment, (string) $template); 44 | if (is_string($commandLine)) { 45 | $commandLine = CommandLine::fromString($commandLine); 46 | } 47 | 48 | return $this->createProcess($commandLine, $environment, $cwd); 49 | } 50 | 51 | /** 52 | * @return callable 53 | */ 54 | protected function createDefaultTemplateEngine() 55 | { 56 | return function (ProcessEnvironment $processEnvironment, $template) { 57 | $commandToExecute = str_replace('{}', (string) $processEnvironment->getArguments(), (string) $template); 58 | $commandToExecute = str_replace('{p}', $processEnvironment->getChannelId(), $commandToExecute); 59 | $commandToExecute = str_replace('{inc}', $processEnvironment->getIncrementalNumber(), $commandToExecute); 60 | 61 | return $commandToExecute; 62 | }; 63 | } 64 | 65 | /** 66 | * @param CommandLine $commandLine 67 | * @param ProcessEnvironment $environment 68 | * @param string $cwd 69 | * 70 | * @return Process 71 | */ 72 | protected function createProcess(CommandLine $commandLine, ProcessEnvironment $environment, $cwd = null) 73 | { 74 | return new Process($commandLine, $environment, $this->timeout, $cwd); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Process/ProcessFactoryInterface.php: -------------------------------------------------------------------------------- 1 | eventDispatcher = $eventDispatcher; 75 | $this->pollingTime = $pollingTime ?: 200; 76 | $this->parallelChannels = $this->calculateChannels($forceToUseNChannels); 77 | $this->exitCodeStrategy = $this->createExitStrategyCallable($exitCodeStrategy); 78 | $this->channels = Channels::createWaiting($this->parallelChannels); 79 | } 80 | 81 | /** 82 | * @return array 83 | */ 84 | public static function getSubscribedEvents() 85 | { 86 | return [ 87 | EventsName::QUEUE_IS_FROZEN => ['onFrozenQueue', 100], 88 | EventsName::QUEUE_IS_EMPTY => ['onQueueEmptied', 100], 89 | EventsName::PROCESS_STARTED => ['onProcessStarted', 100], 90 | EventsName::PROCESS_COMPLETED => ['onProcessCompleted', 100], 91 | ]; 92 | } 93 | 94 | /** 95 | * @param FrozenQueueEvent $event 96 | */ 97 | public function onFrozenQueue(FrozenQueueEvent $event) 98 | { 99 | $this->queueIsFrozen = true; 100 | } 101 | 102 | /** 103 | * @param EmptiedQueueEvent $event 104 | */ 105 | public function onQueueEmptied(EmptiedQueueEvent $event) 106 | { 107 | $this->queueIsEmpty = true; 108 | } 109 | 110 | /** 111 | * @param ProcessStartedEvent $event 112 | */ 113 | public function onProcessStarted(ProcessStartedEvent $event) 114 | { 115 | $channel = $event->getProcess()->getChannel(); 116 | $this->channels->assignAProcess($channel, $event->getProcess()); 117 | } 118 | 119 | /** 120 | * @param ProcessCompletedEvent $event 121 | */ 122 | public function onProcessCompleted(ProcessCompletedEvent $event) 123 | { 124 | $channel = $event->getProcess()->getChannel(); 125 | $exitCode = $event->getProcess()->getExitCode(); 126 | $exitCodeStrategy = $this->exitCodeStrategy; 127 | $this->exitCode = $exitCodeStrategy($this->exitCode, $exitCode); 128 | 129 | $this->channels->setEmpty($channel); 130 | $this->eventDispatcher->dispatch(EventsName::CHANNEL_IS_WAITING, new ChannelIsWaitingEvent($channel)); 131 | } 132 | 133 | /** 134 | * @return int 135 | */ 136 | public function loop() 137 | { 138 | $stopWatch = new Stopwatch(); 139 | $stopWatch->start('loop'); 140 | $this->eventDispatcher->dispatch(EventsName::LOOP_STARTED, new LoopStartedEvent($this->parallelChannels)); 141 | $this->notifyWaitingChannel($this->channels->getWaitingChannels()); 142 | while (!($this->queueIsFrozen && $this->queueIsEmpty && count($this->channels->getAssignedChannels()) < 1)) { 143 | $this->checkTerminatedProcessOnChannels($this->channels->getAssignedChannels()); 144 | usleep($this->pollingTime); 145 | } 146 | $stopWatchEvent = $stopWatch->stop('loop'); 147 | $this->eventDispatcher->dispatch(EventsName::LOOP_COMPLETED, new LoopCompletedEvent($stopWatchEvent, $this->exitCode)); 148 | 149 | return $this->exitCode; 150 | } 151 | 152 | /** 153 | * @param Channel[] $waitingChannels 154 | */ 155 | private function notifyWaitingChannel($waitingChannels) 156 | { 157 | foreach ($waitingChannels as $channel) { 158 | $this->eventDispatcher->dispatch( 159 | EventsName::CHANNEL_IS_WAITING, 160 | new ChannelIsWaitingEvent($channel) 161 | ); 162 | } 163 | } 164 | 165 | /** 166 | * @param Channel[] $assignedChannels 167 | */ 168 | private function checkTerminatedProcessOnChannels($assignedChannels) 169 | { 170 | foreach ($assignedChannels as $channel) { 171 | /** @var Process|ClosureProcess $process */ 172 | $process = $channel->getProcess(); 173 | 174 | $this->eventDispatcher->dispatch( 175 | EventsName::PROCESS_GENERATED_BUFFER, 176 | new ProcessGeneratedBufferEvent($process) 177 | ); 178 | if (!$process->isTerminated()) { 179 | continue; 180 | } 181 | 182 | $this->eventDispatcher->dispatch(EventsName::PROCESS_COMPLETED, new ProcessCompletedEvent($process)); 183 | if ($process->isSuccessful()) { 184 | $this->eventDispatcher->dispatch(EventsName::PROCESS_COMPLETED_SUCCESSFUL, new ProcessCompletedEvent($process)); 185 | } 186 | } 187 | } 188 | 189 | /** 190 | * @param int $forceToUseNChannels 191 | * 192 | * @return int 193 | */ 194 | private function calculateChannels($forceToUseNChannels = 0) 195 | { 196 | if ((int) $forceToUseNChannels > 0) { 197 | return $forceToUseNChannels; 198 | } 199 | $processorCounter = new ProcessorCounter(); 200 | 201 | return $processorCounter->execute(); 202 | } 203 | 204 | /** 205 | * @param callable|null $exitStrategyCallable 206 | * 207 | * @return callable|int 208 | */ 209 | private function createExitStrategyCallable(callable $exitStrategyCallable = null) 210 | { 211 | if (null != $exitStrategyCallable) { 212 | return $exitStrategyCallable; 213 | } 214 | 215 | return function ($current, $exitCode) { 216 | 217 | return ($current == 0 && $exitCode == 0) ? 0 : $exitCode; 218 | }; 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /src/ProcessorCounter.php: -------------------------------------------------------------------------------- 1 | procCPUInfo = $procCPUInfo ?: self::PROC_CPUINFO; 35 | $this->os = $os ?: PHP_OS; 36 | } 37 | 38 | /** 39 | * @return int 40 | */ 41 | public function execute() 42 | { 43 | if (null !== self::$count) { 44 | return self::$count; 45 | } 46 | self::$count = $this->readFromProcCPUInfo(); 47 | 48 | return self::$count; 49 | } 50 | 51 | /** 52 | * @return int 53 | */ 54 | private function readFromProcCPUInfo() 55 | { 56 | if ($this->os === 'Darwin') { 57 | if ($processors = system('/usr/sbin/sysctl -n hw.physicalcpu')) { 58 | return $processors; 59 | } 60 | } elseif ($this->os === 'Linux') { 61 | if (is_file($this->procCPUInfo) && is_readable($this->procCPUInfo)) { 62 | try { 63 | $contents = trim(file_get_contents($this->procCPUInfo)); 64 | 65 | return substr_count($contents, 'processor'); 66 | } catch (\Exception $e) { 67 | } 68 | } 69 | } 70 | 71 | return self::PROC_DEFAULT_NUMBER; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Producer/ProducerInterface.php: -------------------------------------------------------------------------------- 1 | stdIn = (string) $stdIn; 26 | } 27 | 28 | /** 29 | * @param QueueInterface $queue 30 | */ 31 | public function produce(QueueInterface $queue) 32 | { 33 | $this->resource = @fopen($this->stdIn, 'r'); 34 | $this->assertResourceIsValid(); 35 | 36 | while (false !== ($line = fgets($this->resource))) { 37 | $this->addLineIfNotEmpty($queue, $line); 38 | } 39 | $queue->freeze(); 40 | } 41 | 42 | public function __destruct() 43 | { 44 | if (null !== $this->resource) { 45 | @fclose($this->resource); 46 | } 47 | } 48 | 49 | /** 50 | * @param QueueInterface $queue 51 | * @param string $line 52 | */ 53 | private function addLineIfNotEmpty(QueueInterface $queue, $line) 54 | { 55 | if ($line = trim($line)) { 56 | $queue->enqueue($line); 57 | } 58 | } 59 | 60 | /** 61 | * @throws StdInMustBeAValidResourceException 62 | */ 63 | private function assertResourceIsValid() 64 | { 65 | if (!$this->resource) { 66 | throw new StdInMustBeAValidResourceException($this->stdIn); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Queue/EventDispatcherQueue.php: -------------------------------------------------------------------------------- 1 | eventDispatcher = $eventDispatcher ?: new EventDispatcher(); 29 | parent::__construct($array); 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | public function enqueue($value) 36 | { 37 | parent::enqueue($value); 38 | $this->eventDispatcher->dispatch(EventsName::INPUT_LINE_ENQUEUED, new InputLineEnqueuedEvent($value)); 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | public function dequeue() 45 | { 46 | try { 47 | $commandLine = parent::dequeue(); 48 | } catch (\RuntimeException $e) { 49 | $this->eventDispatcher->dispatch(EventsName::QUEUE_IS_EMPTY, new EmptiedQueueEvent()); 50 | throw $e; 51 | } 52 | $this->eventDispatcher->dispatch(EventsName::INPUT_LINE_DEQUEUED, new InputLineDequeuedEvent($commandLine)); 53 | 54 | return $commandLine; 55 | } 56 | 57 | /** 58 | * {@inheritdoc} 59 | */ 60 | public function randomize() 61 | { 62 | $newQueue = parent::randomize(); 63 | 64 | return new self($this->eventDispatcher, $newQueue); 65 | } 66 | 67 | /** 68 | * {@inheritdoc} 69 | */ 70 | public function freeze() 71 | { 72 | if (parent::isFrozen()) { 73 | return; 74 | } 75 | 76 | parent::freeze(); 77 | $this->eventDispatcher->dispatch(EventsName::QUEUE_IS_FROZEN, new FrozenQueueEvent()); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Queue/QueueInterface.php: -------------------------------------------------------------------------------- 1 | enqueue($item); 23 | } 24 | } 25 | } 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | public function enqueue($value) 31 | { 32 | $this->assertIsNotFrozen(); 33 | parent::enqueue($value); 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | public function dequeue() 40 | { 41 | return parent::dequeue(); 42 | 43 | return; 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | public function randomize() 50 | { 51 | $randomizedArray = []; 52 | for ($this->rewind(); $this->valid(); $this->next()) { 53 | $randomizedArray[] = $this->current(); 54 | } 55 | 56 | shuffle($randomizedArray); 57 | 58 | $newQueue = new self(); 59 | foreach ($randomizedArray as $item) { 60 | $newQueue->enqueue($item); 61 | } 62 | 63 | return $newQueue; 64 | } 65 | 66 | /** 67 | * {@inheritdoc} 68 | */ 69 | public function isFrozen() 70 | { 71 | return $this->isFrozen; 72 | } 73 | 74 | /** 75 | * {@inheritdoc} 76 | */ 77 | public function freeze() 78 | { 79 | $this->isFrozen = true; 80 | } 81 | 82 | /** 83 | * @throws TheQueueMustNotBeFrozenToEnqueueException 84 | */ 85 | private function assertIsNotFrozen() 86 | { 87 | if ($this->isFrozen()) { 88 | throw new TheQueueMustNotBeFrozenToEnqueueException(); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Spawn.php: -------------------------------------------------------------------------------- 1 | eventDispatcher = $eventDispatcher ?: new EventDispatcher(); 31 | $this->autoloadFile = $autoloadFile ?: $this->findAutoloadFilename(); 32 | } 33 | 34 | /** 35 | * Creates a concurrent loop for Callable processes. 36 | * 37 | * @param \Iterator|array|QueueInterface $data 38 | * @param \Closure $closure 39 | * @param int|float|null $pollingTime 40 | * @param int|null $forceToNChannels 41 | * 42 | * @return SpawnLoop 43 | * 44 | * @api 45 | */ 46 | public function closures($data, \Closure $closure, $pollingTime = null, $forceToNChannels = null, $timeout = null) 47 | { 48 | $data = $this->createAQueueFromData($data); 49 | 50 | $factory = new ClosureProcessFactory($closure, $this->autoloadFile, $timeout); 51 | $consumer = new ConsumerListener($data, $this->eventDispatcher, $factory); 52 | 53 | $processes = $this->initProcesses($pollingTime, $forceToNChannels); 54 | $this->addConsumerToListener($consumer); 55 | $data->freeze(); 56 | 57 | return new SpawnLoop($processes, $this->eventDispatcher); 58 | } 59 | 60 | /** 61 | * Creates the SpawnLoop for isolated Processes. 62 | * 63 | * @param \Iterator|array|QueueInterface $data 64 | * @param string $template 65 | * @param int|null $pollingTime 66 | * @param int|null $forceToNChannels 67 | * @param string|null $cwd 68 | * 69 | * @return SpawnLoop 70 | * 71 | * @api 72 | */ 73 | public function processes($data, $template, $pollingTime = null, $forceToNChannels = null, $timeout = null, $cwd = null) 74 | { 75 | $data = $this->createAQueueFromData($data); 76 | $processFactory = new ProcessFactory(null, $timeout); 77 | $consumer = new ConsumerListener($data, $this->eventDispatcher, $processFactory, $template, $cwd); 78 | $processes = $this->initProcesses($pollingTime, $forceToNChannels); 79 | $this->addConsumerToListener($consumer); 80 | $data->freeze(); 81 | 82 | return new SpawnLoop($processes, $this->eventDispatcher); 83 | } 84 | 85 | /** 86 | * Spawns a callable into an isolated processes. 87 | * 88 | * @param array|mixed $args 89 | * @param \Closure $closure 90 | * @param int|float|null $timeout 91 | * @param string|null $cwd 92 | * 93 | * @return ClosureProcess 94 | * 95 | * @api 96 | */ 97 | public function spawn($args, \Closure $closure, $timeout = null, $cwd = null) 98 | { 99 | $factory = new ClosureProcessFactory($closure, $this->autoloadFile, $timeout); 100 | 101 | $process = $factory->create(Channel::createAWaiting(0, 0), $args, 1, null, $cwd); 102 | $process->start(); 103 | 104 | return $process; 105 | } 106 | 107 | /** 108 | * @param int|null $pollingTime 109 | * @param int|null $forceToNChannels 110 | * 111 | * @return Processes 112 | */ 113 | private function initProcesses($pollingTime, $forceToNChannels) 114 | { 115 | $processes = new Processes($this->eventDispatcher, $pollingTime, $forceToNChannels); 116 | $this->eventDispatcher->addSubscriber($processes); 117 | 118 | return $processes; 119 | } 120 | 121 | /** 122 | * @param mixed $consumer 123 | */ 124 | private function addConsumerToListener($consumer) 125 | { 126 | $this->eventDispatcher->addListener( 127 | EventsName::CHANNEL_IS_WAITING, 128 | [$consumer, 'onChannelIsWaiting'] 129 | ); 130 | } 131 | 132 | /** 133 | * @return void|string 134 | */ 135 | private function findAutoloadFilename() 136 | { 137 | foreach ([__DIR__.'/../../autoload.php', __DIR__.'/../vendor/autoload.php', __DIR__.'/vendor/autoload.php'] as $file) { 138 | if (file_exists($file)) { 139 | return $file; 140 | } 141 | } 142 | } 143 | 144 | private function createAQueueFromData($data) 145 | { 146 | if (!($data instanceof EventDispatcherQueue)) { 147 | $data = new EventDispatcherQueue($this->eventDispatcher, $data); 148 | } 149 | 150 | return $data; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/SpawnLoop.php: -------------------------------------------------------------------------------- 1 | processes = $processes; 39 | $this->eventDispatcher = $eventDispatcher ?: new EventDispatcher(); 40 | } 41 | 42 | /** 43 | * Start the Loop and wait. 44 | * 45 | * @param callable|null $callable 46 | * 47 | * @return self 48 | * 49 | * @throws LoopAlreadyStartedException 50 | * 51 | * @api 52 | */ 53 | public function start(callable $callable = null) 54 | { 55 | $this->assertLoopNotStarted(); 56 | if (null !== $callable) { 57 | $this->onCompleted($callable); 58 | } 59 | $this->loopRunning = true; 60 | $exitCode = $this->processes->loop(); 61 | $this->loopRunning = false; 62 | 63 | return $exitCode; 64 | } 65 | 66 | /** 67 | * Add the callable as listener. 68 | * 69 | * @param callable $callable 70 | * 71 | * @return self 72 | * 73 | * @api 74 | */ 75 | public function onCompleted(callable $callable) 76 | { 77 | $this->addListener($callable, EventsName::PROCESS_COMPLETED); 78 | 79 | return $this; 80 | } 81 | 82 | /** 83 | * Add the callable as listener. 84 | * 85 | * @param callable $callable 86 | * 87 | * @return self 88 | * 89 | * @api 90 | */ 91 | public function onSuccessful(callable $callable) 92 | { 93 | $this->addListener($callable, EventsName::PROCESS_COMPLETED_SUCCESSFUL); 94 | 95 | return $this; 96 | } 97 | 98 | /** 99 | * Add the callable as listener. 100 | * 101 | * @param callable $callable 102 | * 103 | * @return self 104 | * 105 | * @api 106 | */ 107 | public function onStarted(callable $callable) 108 | { 109 | $this->addListener($callable, EventsName::PROCESS_STARTED); 110 | 111 | return $this; 112 | } 113 | 114 | /** 115 | * Add the callable as listener. 116 | * 117 | * @param callable $callable 118 | * 119 | * @return self 120 | * 121 | * @api 122 | */ 123 | public function onEmptyIterator(callable $callable) 124 | { 125 | $this->assertLoopNotStarted(); 126 | 127 | $this->eventDispatcher->addListener( 128 | EventsName::QUEUE_IS_EMPTY, 129 | function (EmptiedQueueEvent $event) use ($callable) { 130 | $callable(); 131 | } 132 | ); 133 | 134 | return $this; 135 | } 136 | 137 | /** 138 | * Add the callable as listener. 139 | * 140 | * @param callable $callable 141 | * 142 | * @return self 143 | * 144 | * @api 145 | */ 146 | public function onLoopCompleted(callable $callable) 147 | { 148 | $this->assertLoopNotStarted(); 149 | $this->eventDispatcher->addListener( 150 | EventsName::LOOP_COMPLETED, 151 | function (LoopCompletedEvent $event) use ($callable) { 152 | $callable($event->getExitCode(), $event->getStopwatchEvent()); 153 | } 154 | ); 155 | 156 | return $this; 157 | } 158 | 159 | /** 160 | * Add the callable as listener. 161 | * 162 | * @param callable $callable 163 | * 164 | * @return self 165 | * 166 | * @api 167 | */ 168 | public function onPartialOutput(callable $callable) 169 | { 170 | $this->assertLoopNotStarted(); 171 | $this->eventDispatcher->addListener( 172 | EventsName::PROCESS_GENERATED_BUFFER, 173 | function (ProcessGeneratedBufferEvent $event) use ($callable) { 174 | $callable($event->getProcess()); 175 | } 176 | ); 177 | 178 | return $this; 179 | } 180 | 181 | /** 182 | * @throws LoopAlreadyStartedException 183 | */ 184 | private function assertLoopNotStarted() 185 | { 186 | if ($this->loopRunning) { 187 | throw new LoopAlreadyStartedException(); 188 | } 189 | } 190 | 191 | /** 192 | * @param callable $callable 193 | * @param string $eventName 194 | */ 195 | private function addListener(callable $callable, $eventName) 196 | { 197 | $this->assertLoopNotStarted(); 198 | $this->eventDispatcher->addListener( 199 | $eventName, 200 | function (ProcessCompletedEvent $event) use ($callable) { 201 | $callable($event->getProcess()); 202 | } 203 | ); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/UI/StdOutUISubscriber.php: -------------------------------------------------------------------------------- 1 | ['onInputLineEnqueued', 100], 19 | // EventsName::INPUT_LINE_DEQUEUED => ['onInputLineDequeued', 100], 20 | EventsName::QUEUE_IS_FROZEN => ['onFrozenQueue', 100], 21 | // EventsName::QUEUE_IS_EMPTY => ['onQueueEmptied', 100], 22 | EventsName::PROCESS_STARTED => ['onProcessStarted', 100], 23 | EventsName::PROCESS_COMPLETED => ['onProcessCompleted', 100], 24 | // EventsName::PROCESS_GENERATED_BUFFER => ['onGeneratedBuffer', 100], 25 | ]; 26 | } 27 | 28 | /** 29 | * @param string $name 30 | * @param array $arguments 31 | * 32 | * @return string 33 | */ 34 | public function __call($name, array $arguments = []) 35 | { 36 | echo ' - called:'.$name.PHP_EOL; 37 | } 38 | 39 | /** 40 | * @param ProcessCompletedEvent $event 41 | * 42 | * @return string 43 | */ 44 | public function onProcessCompleted(ProcessCompletedEvent $event) 45 | { 46 | echo sprintf( 47 | "%s] onProcessCompleted: [%s] on [%s] with %s\n", 48 | $event->getProcess()->isSuccessful() ? '✅' : '✗', 49 | $event->getProcess()->getIncrementalNumber(), 50 | $event->getProcess()->getChannel(), 51 | $event->getProcess()->getCommandLine() 52 | ); 53 | } 54 | 55 | /** 56 | * @param ProcessGeneratedBufferEvent $event 57 | * 58 | * @return string 59 | */ 60 | public function onGeneratedBuffer(ProcessGeneratedBufferEvent $event) 61 | { 62 | $err = trim($event->getProcess()->getIncrementalErrorOutput()); 63 | $out = trim($event->getProcess()->getIncrementalOutput()); 64 | if (empty($err) && empty($out)) { 65 | return; 66 | } 67 | 68 | echo sprintf( 69 | " - buffer: [%s] on [%s] with %s \n out %s|err %s\n", 70 | $event->getProcess()->getIncrementalNumber(), 71 | $event->getProcess()->getChannel(), 72 | $event->getProcess()->getCommandLine(), 73 | $out, 74 | $err 75 | ); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /tests/Consumer/ConsumerListenerTest.php: -------------------------------------------------------------------------------- 1 | getMockBuilder('\Liuggio\Spawn\Process\Process') 17 | ->disableOriginalConstructor() 18 | ->getMock(); 19 | 20 | $channel = Channel::createAWaiting(3, 5); 21 | 22 | $queue = $this->getMock('\Liuggio\Spawn\Queue\QueueInterface'); 23 | $queue->expects($this->once()) 24 | ->method('dequeue') 25 | ->willReturn(10); 26 | 27 | $processFactory = $this->getMock('\Liuggio\Spawn\Process\ProcessFactory'); 28 | $processFactory->expects($this->once()) 29 | ->method('create') 30 | ->with($this->equalTo($channel)) 31 | ->willReturn($process); 32 | 33 | $ed = $this->getMock('\Symfony\Component\EventDispatcher\EventDispatcherInterface'); 34 | 35 | $consumer = new ConsumerListener($queue, $ed, $processFactory, CommandLine::fromString("echo 'a'")); 36 | $consumer->onChannelIsWaiting(new ChannelIsWaitingEvent($channel)); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/Fixture/proc_cpuinfo: -------------------------------------------------------------------------------- 1 | processor : 0 2 | vendor_id : GenuineIntel 3 | cpu family : 6 4 | model : 69 5 | model name : Intel(R) Core(TM) i7-4500U CPU @ 1.80GHz 6 | stepping : 1 7 | microcode : 0x16 8 | cpu MHz : 768.000 9 | cache size : 4096 KB 10 | physical id : 0 11 | siblings : 4 12 | core id : 0 13 | cpu cores : 2 14 | apicid : 0 15 | initial apicid : 0 16 | fpu : yes 17 | fpu_exception : yes 18 | cpuid level : 13 19 | wp : yes 20 | flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc aperfmperf eagerfpu pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 fma cx16 xtpr pdcm pcid sse4_1 sse4_2 movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm ida arat epb xsaveopt pln pts dtherm tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid 21 | bogomips : 4788.56 22 | clflush size : 64 23 | cache_alignment : 64 24 | address sizes : 39 bits physical, 48 bits virtual 25 | power management: 26 | 27 | processor : 1 28 | vendor_id : GenuineIntel 29 | cpu family : 6 30 | model : 69 31 | model name : Intel(R) Core(TM) i7-4500U CPU @ 1.80GHz 32 | stepping : 1 33 | microcode : 0x16 34 | cpu MHz : 768.000 35 | cache size : 4096 KB 36 | physical id : 0 37 | siblings : 4 38 | core id : 0 39 | cpu cores : 2 40 | apicid : 1 41 | initial apicid : 1 42 | fpu : yes 43 | fpu_exception : yes 44 | cpuid level : 13 45 | wp : yes 46 | flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc aperfmperf eagerfpu pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 fma cx16 xtpr pdcm pcid sse4_1 sse4_2 movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm ida arat epb xsaveopt pln pts dtherm tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid 47 | bogomips : 4788.56 48 | clflush size : 64 49 | cache_alignment : 64 50 | address sizes : 39 bits physical, 48 bits virtual 51 | power management: 52 | 53 | processor : 2 54 | vendor_id : GenuineIntel 55 | cpu family : 6 56 | model : 69 57 | model name : Intel(R) Core(TM) i7-4500U CPU @ 1.80GHz 58 | stepping : 1 59 | microcode : 0x16 60 | cpu MHz : 1200.000 61 | cache size : 4096 KB 62 | physical id : 0 63 | siblings : 4 64 | core id : 1 65 | cpu cores : 2 66 | apicid : 2 67 | initial apicid : 2 68 | fpu : yes 69 | fpu_exception : yes 70 | cpuid level : 13 71 | wp : yes 72 | flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc aperfmperf eagerfpu pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 fma cx16 xtpr pdcm pcid sse4_1 sse4_2 movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm ida arat epb xsaveopt pln pts dtherm tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid 73 | bogomips : 4788.56 74 | clflush size : 64 75 | cache_alignment : 64 76 | address sizes : 39 bits physical, 48 bits virtual 77 | power management: 78 | 79 | processor : 3 80 | vendor_id : GenuineIntel 81 | cpu family : 6 82 | model : 69 83 | model name : Intel(R) Core(TM) i7-4500U CPU @ 1.80GHz 84 | stepping : 1 85 | microcode : 0x16 86 | cpu MHz : 768.000 87 | cache size : 4096 KB 88 | physical id : 0 89 | siblings : 4 90 | core id : 1 91 | cpu cores : 2 92 | apicid : 3 93 | initial apicid : 3 94 | fpu : yes 95 | fpu_exception : yes 96 | cpuid level : 13 97 | wp : yes 98 | flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc aperfmperf eagerfpu pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 fma cx16 xtpr pdcm pcid sse4_1 sse4_2 movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm ida arat epb xsaveopt pln pts dtherm tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid 99 | bogomips : 4788.56 100 | clflush size : 64 101 | cache_alignment : 64 102 | address sizes : 39 bits physical, 48 bits virtual 103 | power management: 104 | -------------------------------------------------------------------------------- /tests/Process/Channel/ChannelsTest.php: -------------------------------------------------------------------------------- 1 | assertCount(5, $channels->getWaitingChannels()); 14 | $this->assertCount(0, $channels->getAssignedChannels()); 15 | } 16 | 17 | /** 18 | * @test 19 | */ 20 | public function shouldGetAllThe4EmptyChannels() 21 | { 22 | $channels = Channels::createWaiting(5); 23 | $process = $this->getMockBuilder('\Liuggio\Spawn\Process\Process') 24 | ->disableOriginalConstructor() 25 | ->getMock(); 26 | 27 | $channels->assignAProcess(Channel::createAWaiting(1, 1), $process); 28 | 29 | $this->assertCount(4, $channels->getWaitingChannels()); 30 | $this->assertCount(1, $channels->getAssignedChannels()); 31 | } 32 | 33 | /** 34 | * @test 35 | */ 36 | public function shouldGetAllTheAssignedChannels() 37 | { 38 | $channels = Channels::createWaiting(5); 39 | 40 | foreach ($channels->getWaitingChannels() as $channel) { 41 | $process = $this->getMockBuilder('\Liuggio\Spawn\Process\Process') 42 | ->disableOriginalConstructor() 43 | ->getMock(); 44 | $channels->assignAProcess($channel, $process); 45 | } 46 | 47 | $this->assertCount(5, $channels->getAssignedChannels()); 48 | $this->assertCount(0, $channels->getWaitingChannels()); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/Process/ClosureReturnValueTest.php: -------------------------------------------------------------------------------- 1 | getMockBuilder('\Liuggio\Spawn\Process\Process') 19 | ->disableOriginalConstructor() 20 | ->getMock(); 21 | $channel = $channel->assignToAProcess($process); 22 | $envs = new ProcessEnvironment($channel, 'fileA', 11); 23 | 24 | $process = new Process( 25 | new CommandLine($assertionCommandLine), 26 | $envs 27 | ); 28 | 29 | $this->assertInstanceOf('\Liuggio\Spawn\Process\Process', $process); 30 | $this->assertEquals('bin/phpunit fileA', $process->getCommandLine()); 31 | $this->assertEquals(array( 32 | 0 => 'ENV_TEST_CHANNEL=2', 33 | 1 => 'ENV_TEST_CHANNEL_READABLE=test_2', 34 | 2 => 'ENV_TEST_CHANNELS_NUMBER=10', 35 | 3 => 'ENV_TEST_ARGUMENT=fileA', 36 | 4 => 'ENV_TEST_INC_NUMBER=11', 37 | 5 => 'ENV_TEST_IS_FIRST_ON_CHANNEL=1', ), 38 | $process->getenv()); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/Process/ProcessesTest.php: -------------------------------------------------------------------------------- 1 | getMock('\Symfony\Component\EventDispatcher\EventDispatcherInterface'); 13 | 14 | $ed->expects($this->at(0)) 15 | ->method('dispatch') 16 | ->with($this->equalTo(EventsName::LOOP_STARTED)); 17 | 18 | $ed->expects($this->at(1)) 19 | ->method('dispatch') 20 | ->with($this->equalTo(EventsName::CHANNEL_IS_WAITING)); 21 | 22 | $ed->expects($this->at(2)) 23 | ->method('dispatch') 24 | ->with($this->equalTo(EventsName::CHANNEL_IS_WAITING)); 25 | 26 | $ed->expects($this->at(3)) 27 | ->method('dispatch') 28 | ->with($this->equalTo(EventsName::LOOP_COMPLETED)); 29 | 30 | $processes = new Processes($ed, null, 2); 31 | 32 | $ev = $this->getMock('\Liuggio\Spawn\Event\FrozenQueueEvent'); 33 | $processes->onFrozenQueue($ev); 34 | $ev2 = $this->getMock('\Liuggio\Spawn\Event\EmptiedQueueEvent'); 35 | $processes->onQueueEmptied($ev2); 36 | 37 | $processes->loop(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/ProcessorCounterTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(4, $processorCount->execute()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/Producer/StdInProducerTest.php: -------------------------------------------------------------------------------- 1 | getMock('\Liuggio\Spawn\Queue\QueueInterface'); 14 | $sut = new StdInProducer('xyz'); 15 | $sut->produce($queue); 16 | } 17 | 18 | /** 19 | * @test 20 | */ 21 | public function shouldFillTheQueueWith3Lines() 22 | { 23 | $buffer = <<getMock('\Liuggio\Spawn\Queue\QueueInterface'); 32 | $queue 33 | ->expects($this->exactly(3)) 34 | ->method('enqueue'); 35 | $sut = new StdInProducer($createTmpFileName); 36 | $sut->produce($queue); 37 | } 38 | 39 | /** 40 | * @test 41 | * 42 | * This test intents to detect problems when reading from stdin, simulating a delayed input into concurrent. 43 | * 44 | * For example stream_set_blocking(stdin, false) was causing problems because it returned too fast 45 | * and stdin was read as an empty string from certain programs (i.e. behat --list-scenarios) 46 | * See issue https://github.com/liuggio/fastest/issues/10. 47 | */ 48 | public function shouldReadTestQueueFromDelayedStdIn() 49 | { 50 | $bootstrapFile = realpath(__DIR__.'/../../vendor/autoload.php'); 51 | $code = ' 52 | require "'.$bootstrapFile.'"; 53 | $queue = new \Liuggio\Spawn\Queue\SplQueue(); 54 | $producer = new \Liuggio\Spawn\Producer\StdInProducer(); 55 | $producer->produce($queue); 56 | try { 57 | while($value = $queue->dequeue()) { 58 | echo $value . PHP_EOL; 59 | } 60 | }catch(\RuntimeException $e) { 61 | // queue empty :) 62 | } 63 | '; 64 | $code = escapeshellarg($code); 65 | $command = 'php -r'.$code; 66 | $stdInLines = array( 67 | 'Line 1'.PHP_EOL, 68 | 'Line 2'.PHP_EOL, 69 | ); 70 | $expectedStdOut = implode('', $stdInLines); 71 | $stdOut = $this->executeCommandWithDelayedStdin($command, $stdInLines); 72 | $this->assertEquals($expectedStdOut, $stdOut); 73 | } 74 | 75 | private function executeCommandWithDelayedStdin($command, $stdInLines, $delayMicroseconds = 1000000) 76 | { 77 | $descriptors = array( 78 | 0 => array('pipe', 'r'), // stdin is a pipe that the child will read from 79 | 1 => array('pipe', 'w'), // stdout is a pipe that the child will write to 80 | 2 => array('pipe', 'w'), // stderr is a pipe that the child will write to 81 | ); 82 | $pipes = array(); 83 | $process = proc_open($command, $descriptors, $pipes); 84 | if (!is_resource($process)) { 85 | throw new \RuntimeException("Failed to run command '$command'"); 86 | } 87 | // $pipes now looks like this: 88 | // 0 => writable handle connected to child stdin 89 | // 1 => readable handle connected to child stdout 90 | // 2 => readable handle connected to child stderr 91 | foreach ($stdInLines as $stdInLine) { 92 | usleep($delayMicroseconds); 93 | fwrite($pipes[0], $stdInLine); 94 | } 95 | fclose($pipes[0]); 96 | $stdOut = stream_get_contents($pipes[1]); 97 | fclose($pipes[1]); 98 | $stdErr = stream_get_contents($pipes[2]); 99 | fclose($pipes[2]); 100 | if ($stdErr) { 101 | throw new \RuntimeException("Error executing $command: $stdErr"); 102 | } 103 | // It is important that to close any pipes before calling 104 | // proc_close in order to avoid a deadlock 105 | proc_close($process); 106 | 107 | return $stdOut; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /tests/SpawnTest.php: -------------------------------------------------------------------------------- 1 | closures(range(1, 7), function ($input) { 18 | echo 'this is the echo'; 19 | $return = new \stdClass(); 20 | $return->name = 'name'; 21 | 22 | return $return; 23 | }) 24 | ->onCompleted(function (ClosureProcess $process) { 25 | $this->assertEquals('', $process->getErrorOutput()); 26 | $this->assertEquals('name', $process->getReturnValue()->name); 27 | $this->assertEquals('this is the echo', $process->getOutput()); 28 | }) 29 | ->start(); 30 | } 31 | 32 | /** 33 | * @test 34 | */ 35 | public function shouldExecuteAConcurrentProcesses() 36 | { 37 | $spawn = new Spawn(); 38 | $spawn 39 | ->processes([1], $template = "echo -n '{}';") 40 | ->onCompleted(function (Process $process) { 41 | $this->assertEquals(null, $process->getErrorOutput()); 42 | $this->assertEquals('1', $process->getOutput()); 43 | }) 44 | ->start(); 45 | } 46 | 47 | /** 48 | * @test 49 | */ 50 | public function shouldExecuteAndGetPartialBuffer() 51 | { 52 | $spawn = new Spawn(); 53 | $spawn 54 | ->processes([1], $template = "echo -n '{}';sleep 2;echo -n done") 55 | ->onPartialOutput(function (Process $process) { 56 | $this->assertEquals(null, $process->getErrorOutput()); 57 | $output = $process->getIncrementalOutput(); 58 | if (!empty($output)) { 59 | $this->assertTrue((strpos('1done', $output) !== false), $output); 60 | } 61 | 62 | }) 63 | ->onCompleted(function (Process $process) { 64 | $this->assertEquals(null, $process->getErrorOutput()); 65 | $output = $process->getOutput(); 66 | $this->assertTrue((strpos('1done', $output) !== false), $output); 67 | }) 68 | ->start(); 69 | } 70 | 71 | /** 72 | * @test 73 | */ 74 | public function shouldSpawnACallable() 75 | { 76 | $sumAndPrint = function ($sum) { 77 | foreach (range(1, $sum) as $i) { 78 | echo "$i"; 79 | $sum += $i; 80 | } 81 | 82 | return $sum; 83 | }; 84 | 85 | $spawn = new Spawn(); 86 | $process = $spawn->spawn(10, $sumAndPrint); 87 | $process->wait(); 88 | $this->assertEquals(null, $process->getErrorOutput()); 89 | $this->assertEquals(65, $process->getReturnValue()); 90 | $this->assertEquals('12345678910', $process->getOutput()); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /tests/spawn_tests.php: -------------------------------------------------------------------------------- 1 | getFilesAsArray([__DIR__], ['Test.php']); 11 | 12 | $exit = $testRunner 13 | ->processes($files, __DIR__.'/../bin/phpunit {}') 14 | ->onCompleted(function (\Liuggio\Spawn\Process\Process $process) { 15 | 16 | echo $process->getCommandLine(); 17 | if ($process->getExitCode() == 0) { 18 | echo ' yes'.PHP_EOL; 19 | 20 | return; 21 | } 22 | echo ' ops'.PHP_EOL; 23 | echo $process->getErrorOutput().PHP_EOL; 24 | echo $process->getOutput().PHP_EOL; 25 | echo '====='.PHP_EOL; 26 | }) 27 | ->onLoopCompleted(function ($exitCode, \Symfony\Component\Stopwatch\StopwatchEvent $event) { 28 | 29 | echo PHP_EOL.PHP_EOL.(($exitCode == 0) ? 'successful' : 'failed').PHP_EOL; 30 | echo 'memory used: '.$event->getMemory().PHP_EOL; 31 | echo 'Duration: '.$event->getDuration().PHP_EOL; 32 | }) 33 | ->start(); 34 | 35 | exit($exit); 36 | // if you want more fun please have a look to the fastest project // https://github.com/liuggio/fastest 37 | 38 | --------------------------------------------------------------------------------