New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
WIP: ChildProcess #61
Closed
yuya-takeyama
wants to merge
53
commits into
reactphp:master
from
yuya-takeyama:feature/child-process
Closed
Changes from all commits
Commits
Show all changes
53 commits
Select commit
Hold shift + click to select a range
24b0e0d
1st implementation
yuya-takeyama dbbb4f0
Remove duplicated closing
yuya-takeyama e5ec416
Add color for example
yuya-takeyama 1643f78
Add some methods
yuya-takeyama 161baaa
Refactor example
yuya-takeyama e3528ca
Fix typo
yuya-takeyama 30cc4fc
Fix to use isset()
yuya-takeyama adc2a1c
Remove garbage
yuya-takeyama 29dd20e
Remove unused argument
yuya-takeyama 450e093
Replace $_ENV with NULL by default
yuya-takeyama 235533c
Replace getcwd() with NULL by default
yuya-takeyama d428737
Add Stream class for STDOUT/STDERR
yuya-takeyama a88189e
Set access level
yuya-takeyama 200af15
Fix method declaration
yuya-takeyama 1e1b95a
Fix coding style
yuya-takeyama c17ae42
Fix coding style
yuya-takeyama c84cf66
Fix coding style
yuya-takeyama 6155fcc
Fix method declaration
yuya-takeyama 42502ef
Add test for React\ChildProcess\Factory
yuya-takeyama a1ad4ee
Add more tests for React\ChildProcess\Factory->spawn()
yuya-takeyama 26fdf41
Add tests for command generation
yuya-takeyama 8ef907b
Disable $_ENV test on Travis CI
yuya-takeyama a3024bb
Rename
yuya-takeyama a077a02
Add test for React\ChildProcess\Process
yuya-takeyama df0bc00
Add a test
yuya-takeyama 57ccee7
Add a test
yuya-takeyama 5849aa9
Fix
yuya-takeyama a28f5f5
Refactoring
yuya-takeyama b0e04d6
Add tests
yuya-takeyama 13da337
Add React\ChildProcess\Process->terminate() method
yuya-takeyama 0ede84c
Fix to wait a little for process termination
yuya-takeyama 6297f1e
Fix for readability
yuya-takeyama 03ed74c
Fix waiting time
yuya-takeyama 5d3082a
Add state represents whether the process is closed
yuya-takeyama adcd3fa
Fix command to test
yuya-takeyama 70115f6
Add tests
yuya-takeyama 93a77f1
Add Process->getExitCode() and Process->getSignalCode() (WIP)
yuya-takeyama 23cafb5
Divide handleExit() into exit() and handleExit()
yuya-takeyama 1b0a31a
Shortened running time
yuya-takeyama 5eabf62
Rename
yuya-takeyama 8ce7fc7
Fix to set exited flag in Process->handleExit() method
yuya-takeyama 106c5ee
Add tests for Process->getExitCode() and Process->getStatusCode()
yuya-takeyama c6820ed
Replace a test
yuya-takeyama 2b6755a
Addd tests for when process is terminated
yuya-takeyama e970e59
Remove duplicated test
yuya-takeyama bb39ef0
Rename
yuya-takeyama 864cb0d
Remove duplicated test
yuya-takeyama c213270
Remove duplicated test
yuya-takeyama dbd9b3f
Refactoring
yuya-takeyama cc64eb4
Fix exit code and signal code when terminated
yuya-takeyama 4c0995a
Simplify
yuya-takeyama 59a0bc7
Fix argument order
yuya-takeyama 49f765d
Close streams gracefully
yuya-takeyama File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
<?php | ||
|
||
// script to spawn multiple child processes | ||
|
||
require __DIR__.'/../vendor/autoload.php'; | ||
|
||
$loop = new React\EventLoop\StreamSelectLoop(); | ||
$factory = new React\ChildProcess\Factory($loop); | ||
|
||
$commands = array( | ||
'A' => array('cmd' => 'php', 'args' => array('-r', 'foreach (range(1, 3) as $i) { echo $i, PHP_EOL; sleep(1); } fputs(STDERR, "Bye.");')), | ||
'B' => array('cmd' => 'php', 'args' => array('-r', 'foreach (range(1, 6) as $i) { echo $i, PHP_EOL; sleep(1); } fputs(STDERR, "Bye.");')), | ||
'C' => array('cmd' => 'php', 'args' => array('-r', 'foreach (range(1, 9) as $i) { echo $i, PHP_EOL; sleep(1); } fputs(STDERR, "Bye.");')), | ||
); | ||
|
||
foreach ($commands as $id => $command) { | ||
$idLabel = "[{$id}] "; | ||
$process = $factory->spawn($command['cmd'], $command['args']); | ||
echo $idLabel, '[PID] pid is ', (string) $process->getPid(), PHP_EOL; | ||
echo $idLabel, '[CMD] ', $process->getCommand(), PHP_EOL; | ||
|
||
$process->stdout->on('data', function ($data) use ($idLabel) { | ||
echo $idLabel, '[STDOUT] '; | ||
var_dump($data); | ||
}); | ||
|
||
$process->stderr->on('data', function ($data) use ($idLabel) { | ||
echo $idLabel, '[STDERR] '; | ||
var_dump($data); | ||
}); | ||
|
||
$process->on('exit', function ($status) use ($idLabel) { | ||
echo $idLabel, '[EXIT] exited with status code ', (string) $status, PHP_EOL; | ||
}); | ||
} | ||
|
||
$loop->run(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
<?php | ||
|
||
namespace React\ChildProcess; | ||
|
||
use React\EventLoop\LoopInterface; | ||
use React\ChildProcess\Process; | ||
use React\ChildProcess\Stream; | ||
|
||
class Factory | ||
{ | ||
private $loop; | ||
|
||
public function __construct(LoopInterface $loop) | ||
{ | ||
$this->loop = $loop; | ||
} | ||
|
||
public function spawn($file, array $args = array(), $cwd = null, $env = null) | ||
{ | ||
$cmd = $this->formatCommandWithArguments($file, $args); | ||
|
||
$fdSpec = array( | ||
array('pipe', 'r'), | ||
array('pipe', 'w'), | ||
array('pipe', 'w'), | ||
); | ||
|
||
$process = proc_open($cmd, $fdSpec, $pipes, $cwd, $env); | ||
|
||
$stdin = new Stream($pipes[0], $this->loop); | ||
$stdout = new Stream($pipes[1], $this->loop); | ||
$stderr = new Stream($pipes[2], $this->loop); | ||
|
||
$stdin->pause(); | ||
|
||
stream_set_blocking($pipes[0], 0); | ||
stream_set_blocking($pipes[1], 0); | ||
stream_set_blocking($pipes[2], 0); | ||
|
||
return new Process($process, $stdin, $stdout, $stderr); | ||
} | ||
|
||
private function formatCommandWithArguments($file, $args) | ||
{ | ||
$command = $file; | ||
|
||
if (count($args) > 0) { | ||
$command .= ' ' . join(' ', array_map('escapeshellarg', $args)); | ||
} | ||
|
||
return $command; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
<?php | ||
|
||
namespace React\ChildProcess; | ||
|
||
use React\Stream\WritableStreamInterface; | ||
use React\Stream\ReadableStreamInterface; | ||
use Evenement\EventEmitter; | ||
|
||
class Process extends EventEmitter | ||
{ | ||
const SIGNAL_CODE_SIGKILL = 9; | ||
const SIGNAL_CODE_SIGTERM = 15; | ||
|
||
public $stdin; | ||
|
||
public $stdout; | ||
|
||
public $stderr; | ||
|
||
private $process; | ||
|
||
private $status = null; | ||
|
||
private $exitCode = null; | ||
|
||
private $signalCode = null; | ||
|
||
private $exited = false; | ||
|
||
public function __construct($process, WritableStreamInterface $stdin, ReadableStreamInterface $stdout, ReadableStreamInterface $stderr) | ||
{ | ||
$this->process = $process; | ||
$this->stdin = $stdin; | ||
$this->stdout = $stdout; | ||
$this->stderr = $stderr; | ||
|
||
$self = $this; | ||
|
||
$this->stdout->on('end', function () use ($self) { | ||
$self->updateStatus(); | ||
$self->observeStatus(); | ||
}); | ||
|
||
$this->stderr->on('end', function () use ($self) { | ||
$self->updateStatus(); | ||
$self->observeStatus(); | ||
}); | ||
} | ||
|
||
public function updateStatus() | ||
{ | ||
if ($this->process && is_null($this->signalCode)) { | ||
$this->status = proc_get_status($this->process); | ||
|
||
if ($this->status['signaled']) { | ||
$this->signalCode = $this->status['termsig']; | ||
} | ||
} | ||
} | ||
|
||
public function observeStatus() | ||
{ | ||
if (!$this->stdout->isReadable() && !$this->stderr->isReadable()) { | ||
$this->stdin->close(); | ||
$this->stdout->close(); | ||
$this->stderr->close(); | ||
$this->exits(); | ||
} | ||
} | ||
|
||
public function exits() | ||
{ | ||
$exitCode = proc_close($this->process); | ||
$this->process = null; | ||
|
||
if ($this->signalCode) { | ||
$this->handleExit(null, $this->signalCode); | ||
} else { | ||
$this->handleExit($exitCode, null); | ||
} | ||
} | ||
|
||
public function handleExit($exitCode, $signalCode) | ||
{ | ||
if ($this->exited) { | ||
return; | ||
} | ||
|
||
$this->exited = true; | ||
|
||
$this->exitCode = $exitCode; | ||
$this->signalCode = $signalCode; | ||
|
||
$this->emit('exit', array($exitCode, $signalCode)); | ||
$this->emit('close', array($exitCode, $signalCode)); | ||
} | ||
|
||
public function getPid() | ||
{ | ||
$status = $this->getCachedStatus(); | ||
|
||
return $status['pid']; | ||
} | ||
|
||
public function getCommand() | ||
{ | ||
$status = $this->getCachedStatus(); | ||
|
||
return $status['command']; | ||
} | ||
|
||
public function isRunning() | ||
{ | ||
if ($this->exited) { | ||
return false; | ||
} else { | ||
$status = $this->getFreshStatus(); | ||
|
||
return $status['running']; | ||
} | ||
} | ||
|
||
public function isSignaled() | ||
{ | ||
$status = $this->getFreshStatus(); | ||
|
||
return $status['signaled']; | ||
} | ||
|
||
public function isStopped() | ||
{ | ||
$status = $this->getFreshStatus(); | ||
|
||
return $status['stopped']; | ||
} | ||
|
||
public function terminate($signalCode = self::SIGNAL_CODE_SIGTERM) | ||
{ | ||
proc_terminate($this->process, $signalCode); | ||
} | ||
|
||
public function getExitCode() | ||
{ | ||
return $this->exitCode; | ||
} | ||
|
||
public function getSignalCode() | ||
{ | ||
return $this->signalCode; | ||
} | ||
|
||
private function getCachedStatus() | ||
{ | ||
if (is_null($this->status)) { | ||
$this->updateStatus(); | ||
} | ||
|
||
return $this->status; | ||
} | ||
|
||
private function getFreshStatus() | ||
{ | ||
$this->updateStatus(); | ||
|
||
return $this->status; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
<?php | ||
|
||
namespace React\ChildProcess; | ||
|
||
use React\Stream\Stream as DefaultStream; | ||
|
||
class Stream extends DefaultStream | ||
{ | ||
public function handleData($stream) | ||
{ | ||
$data = fread($stream, $this->bufferSize); | ||
|
||
if ('' === $data || false === $data) { | ||
$this->end(); | ||
} else { | ||
$this->emit('data', array($data, $this)); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
<?php | ||
|
||
namespace React\Tests\ChildProcess; | ||
|
||
use React\ChildProcess\Factory; | ||
use React\EventLoop\StreamSelectLoop; | ||
|
||
class FactoryTest extends \PHPUnit_Framework_TestCase | ||
{ | ||
public function testSpawn() | ||
{ | ||
$loop = $this->createLoop(); | ||
$factory = $this->createFactory($loop); | ||
$process = $factory->spawn('php', array('-r', 'echo "cwd = ", getcwd(), ", count of env = ", count($_ENV);')); | ||
|
||
$capturedData = ''; | ||
|
||
$process->stdout->on('data', function ($data) use (&$capturedData) { | ||
$capturedData .= $data; | ||
}); | ||
|
||
$loop->run(); | ||
|
||
$cwd = getcwd(); | ||
$this->assertSame("cwd = {$cwd}, count of env = 0", $capturedData); | ||
} | ||
|
||
public function testSpawnWithPwd() | ||
{ | ||
$loop = $this->createLoop(); | ||
$factory = $this->createFactory($loop); | ||
$process = $factory->spawn('php', array('-r', 'echo "cwd = ", getcwd();'), '/'); | ||
|
||
$capturedData = ''; | ||
|
||
$process->stdout->on('data', function ($data) use (&$capturedData) { | ||
$capturedData .= $data; | ||
}); | ||
|
||
$loop->run(); | ||
|
||
$this->assertSame('cwd = /', $capturedData); | ||
} | ||
|
||
public function testSpawnWithEnv() | ||
{ | ||
if (isset($_SERVER['TRAVIS']) && 'true' === $_SERVER['TRAVIS']) { | ||
$this->markTestSkipped(); | ||
} | ||
|
||
$loop = $this->createLoop(); | ||
$factory = $this->createFactory($loop); | ||
$process = $factory->spawn('php', array('-r', 'echo "foo = ", $_ENV["foo"];'), null, array('foo' => 'FOO')); | ||
|
||
$capturedData = ''; | ||
|
||
$process->stdout->on('data', function ($data) use (&$capturedData) { | ||
$capturedData .= $data; | ||
}); | ||
|
||
$loop->run(); | ||
|
||
$this->assertSame('foo = FOO', $capturedData); | ||
} | ||
|
||
public function testCommand() | ||
{ | ||
$loop = $this->createLoop(); | ||
$factory = $this->createFactory($loop); | ||
$process = $factory->spawn('echo'); | ||
|
||
$this->assertSame('echo', $process->getCommand()); | ||
} | ||
|
||
public function testCommandWithOneArgument() | ||
{ | ||
$loop = $this->createLoop(); | ||
$factory = $this->createFactory($loop); | ||
$process = $factory->spawn('echo', array('foo')); | ||
|
||
$this->assertSame("echo 'foo'", $process->getCommand()); | ||
} | ||
|
||
public function testCommandWithManyArguments() | ||
{ | ||
$loop = $this->createLoop(); | ||
$factory = $this->createFactory($loop); | ||
$process = $factory->spawn('echo', array('foo', 'bar', 'foo bar')); | ||
|
||
$this->assertSame("echo 'foo' 'bar' 'foo bar'", $process->getCommand()); | ||
} | ||
|
||
private function createFactory($loop) | ||
{ | ||
return new Factory($loop); | ||
} | ||
|
||
private function createLoop() | ||
{ | ||
return new StreamSelectLoop(); | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you put a new line between the variable setter and
return
please (including all following functions in this class)? It's consistent in all React source.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.