├── .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 | [](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 |
--------------------------------------------------------------------------------