Skip to content
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
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
24b0e0d
1st implementation
yuya-takeyama Aug 18, 2012
dbbb4f0
Remove duplicated closing
yuya-takeyama Aug 18, 2012
e5ec416
Add color for example
yuya-takeyama Aug 18, 2012
1643f78
Add some methods
yuya-takeyama Aug 18, 2012
161baaa
Refactor example
yuya-takeyama Aug 18, 2012
e3528ca
Fix typo
yuya-takeyama Aug 18, 2012
30cc4fc
Fix to use isset()
yuya-takeyama Aug 18, 2012
adc2a1c
Remove garbage
yuya-takeyama Aug 18, 2012
29dd20e
Remove unused argument
yuya-takeyama Aug 18, 2012
450e093
Replace $_ENV with NULL by default
yuya-takeyama Aug 18, 2012
235533c
Replace getcwd() with NULL by default
yuya-takeyama Aug 18, 2012
d428737
Add Stream class for STDOUT/STDERR
yuya-takeyama Aug 18, 2012
a88189e
Set access level
yuya-takeyama Aug 18, 2012
200af15
Fix method declaration
yuya-takeyama Aug 18, 2012
1e1b95a
Fix coding style
yuya-takeyama Aug 18, 2012
c17ae42
Fix coding style
yuya-takeyama Aug 18, 2012
c84cf66
Fix coding style
yuya-takeyama Aug 18, 2012
6155fcc
Fix method declaration
yuya-takeyama Aug 18, 2012
42502ef
Add test for React\ChildProcess\Factory
yuya-takeyama Aug 18, 2012
a1ad4ee
Add more tests for React\ChildProcess\Factory->spawn()
yuya-takeyama Aug 18, 2012
26fdf41
Add tests for command generation
yuya-takeyama Aug 18, 2012
8ef907b
Disable $_ENV test on Travis CI
yuya-takeyama Aug 18, 2012
a3024bb
Rename
yuya-takeyama Aug 20, 2012
a077a02
Add test for React\ChildProcess\Process
yuya-takeyama Aug 20, 2012
df0bc00
Add a test
yuya-takeyama Aug 20, 2012
57ccee7
Add a test
yuya-takeyama Aug 20, 2012
5849aa9
Fix
yuya-takeyama Aug 20, 2012
a28f5f5
Refactoring
yuya-takeyama Aug 20, 2012
b0e04d6
Add tests
yuya-takeyama Aug 20, 2012
13da337
Add React\ChildProcess\Process->terminate() method
yuya-takeyama Aug 25, 2012
0ede84c
Fix to wait a little for process termination
yuya-takeyama Aug 25, 2012
6297f1e
Fix for readability
yuya-takeyama Aug 25, 2012
03ed74c
Fix waiting time
yuya-takeyama Aug 25, 2012
5d3082a
Add state represents whether the process is closed
yuya-takeyama Aug 25, 2012
adcd3fa
Fix command to test
yuya-takeyama Aug 25, 2012
70115f6
Add tests
yuya-takeyama Aug 25, 2012
93a77f1
Add Process->getExitCode() and Process->getSignalCode() (WIP)
yuya-takeyama Aug 25, 2012
23cafb5
Divide handleExit() into exit() and handleExit()
yuya-takeyama Aug 25, 2012
1b0a31a
Shortened running time
yuya-takeyama Aug 25, 2012
5eabf62
Rename
yuya-takeyama Aug 25, 2012
8ce7fc7
Fix to set exited flag in Process->handleExit() method
yuya-takeyama Aug 25, 2012
106c5ee
Add tests for Process->getExitCode() and Process->getStatusCode()
yuya-takeyama Aug 25, 2012
c6820ed
Replace a test
yuya-takeyama Aug 25, 2012
2b6755a
Addd tests for when process is terminated
yuya-takeyama Aug 25, 2012
e970e59
Remove duplicated test
yuya-takeyama Aug 25, 2012
bb39ef0
Rename
yuya-takeyama Aug 25, 2012
864cb0d
Remove duplicated test
yuya-takeyama Aug 25, 2012
c213270
Remove duplicated test
yuya-takeyama Aug 25, 2012
dbd9b3f
Refactoring
yuya-takeyama Aug 25, 2012
cc64eb4
Fix exit code and signal code when terminated
yuya-takeyama Aug 25, 2012
4c0995a
Simplify
yuya-takeyama Sep 15, 2012
59a0bc7
Fix argument order
yuya-takeyama Sep 15, 2012
49f765d
Close streams gracefully
yuya-takeyama Sep 15, 2012
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
37 changes: 37 additions & 0 deletions examples/child-process.php
@@ -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();
53 changes: 53 additions & 0 deletions src/React/ChildProcess/Factory.php
@@ -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;
}
}
167 changes: 167 additions & 0 deletions src/React/ChildProcess/Process.php
@@ -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'];
Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

}

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;
}
}
19 changes: 19 additions & 0 deletions src/React/ChildProcess/Stream.php
@@ -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));
}
}
}
102 changes: 102 additions & 0 deletions tests/React/Tests/ChildProcess/FactoryTest.php
@@ -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();
}
}