Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 105 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,10 @@ as [Streams](https://github.com/reactphp/stream).
**Table of contents**

* [Quickstart example](#quickstart-example)
* [Processes](#processes)
* [EventEmitter Events](#eventemitter-events)
* [Methods](#methods)
* [Process](#process)
* [Stream Properties](#stream-properties)
* [Command](#command)
* [Termination](#termination)
* [Sigchild Compatibility](#sigchild-compatibility)
* [Windows Compatibility](#windows-compatibility)
* [Install](#install)
Expand Down Expand Up @@ -46,21 +45,7 @@ $loop->run();

See also the [examples](examples).

## Processes

### EventEmitter Events

* `exit`: Emitted whenever the process is no longer running. Event listeners
will receive the exit code and termination signal as two arguments.

### Methods

* `start()`: Launches the process and registers its IO streams with the event
loop. The stdin stream will be left in a paused state.
* `terminate()`: Send the process a signal (SIGTERM by default).

There are additional public methods on the Process class, which may be used to
access fields otherwise available through `proc_get_status()`.
## Process

### Stream Properties

Expand Down Expand Up @@ -110,6 +95,7 @@ The `Process` class allows you to pass any kind of command line string:

```php
$process = new Process('echo test');
$process->start($loop);
```

By default, PHP will launch processes by wrapping the given command line string
Expand All @@ -125,6 +111,7 @@ streams from the wrapping shell command like this:

```php
$process = new Process('echo run && demo || echo failed');
$process->start($loop);
```

In other words, the underlying shell is responsible for managing this command
Expand All @@ -140,6 +127,7 @@ boundary between each sub-command like this:

```php
$process = new Process('cat first && echo --- && cat second');
$process->start($loop);
```

As an alternative, considering launching one process at a time and listening on
Expand All @@ -162,6 +150,7 @@ also applies to running the most simple single command:

```php
$process = new Process('yes');
$process->start($loop);
```

This will actually spawn a command hierarchy similar to this:
Expand All @@ -182,6 +171,7 @@ process to be replaced by our process:

```php
$process = new Process('exec yes');
$process->start($loop);
```

This will show a resulting command hierarchy similar to this:
Expand All @@ -205,6 +195,103 @@ the wrapping shell.
If you want to pass an invidual command only, you MAY want to consider
prepending the command string with `exec` to avoid the wrapping shell.

### Termination

The `exit` event will be emitted whenever the process is no longer running.
Event listeners will receive the exit code and termination signal as two
arguments:

```php
$process = new Process('sleep 10');
$process->start($loop);

$process->on('exit', function ($code, $term) {
if ($term === null) {
echo 'exit with code ' . $code . PHP_EOL;
} else {
echo 'terminated with signal ' . $term . PHP_EOL;
}
});
```

Note that `$code` is `null` if the process has terminated, but the exit
code could not be determined (for example
[sigchild compatibility](#sigchild-compatibility) was disabled).
Similarly, `$term` is `null` unless the process has terminated in response to
an uncaught signal sent to it.
This is not a limitation of this project, but actual how exit codes and signals
are exposed on POSIX systems, for more details see also
[here](https://unix.stackexchange.com/questions/99112/default-exit-code-when-process-is-terminated).

It's also worth noting that process termination depends on all file descriptors
being closed beforehand.
This means that all [process pipes](#stream-properties) will emit a `close`
event before the `exit` event and that no more `data` events will arrive after
the `exit` event.
Accordingly, if either of these pipes is in a paused state (`pause()` method
or internally due to a `pipe()` call), this detection may not trigger.

The `terminate(?int $signal = null): bool` method can be used to send the
process a signal (SIGTERM by default).
Depending on which signal you send to the process and whether it has a signal
handler registered, this can be used to either merely signal a process or even
forcefully terminate it.

```php
$process->terminate(SIGUSR1);
```

Keep the above section in mind if you want to forcefully terminate a process.
If your process spawn sub-processes or implicitly uses the
[wrapping shell mentioned above](#command), its file descriptors may be
inherited to child processes and terminating the main process may not
necessarily terminate the whole process tree.
It is highly suggested that you explicitly `close()` all process pipes
accordingly when terminating a process:

```php
$process = new Process('sleep 10');
$process->start($loop);

$loop->addTimer(2.0, function () use ($process) {
$process->stdin->close();
$process->stout->close();
$process->stderr->close();
$process->terminate(SIGKILL);
});
```

For many simple programs these seamingly complicated steps can also be avoided
by prefixing the command line with `exec` to avoid the wrapping shell and its
inherited process pipes as [mentioned above](#command).

```php
$process = new Process('exec sleep 10');
$process->start($loop);

$loop->addTimer(2.0, function () use ($process) {
$process->terminate();
});
```

Many command line programs also wait for data on `STDIN` and terminate cleanly
when this pipe is closed.
For example, the following can be used to "soft-close" a `cat` process:

```php
$process = new Process('cat');
$process->start($loop);

$loop->addTimer(2.0, function () use ($process) {
$process->stdin->end();
});
```

While process pipes and termination may seem confusing to newcomers, the above
properties actually allow some fine grained control over process termination,
such as first trying a soft-close and then applying a force-close after a
timeout.

### Sigchild Compatibility

When PHP has been compiled with the `--enabled-sigchild` option, a child
Expand Down
27 changes: 27 additions & 0 deletions examples/04-terminate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

use React\EventLoop\Factory;
use React\ChildProcess\Process;

require __DIR__ . '/../vendor/autoload.php';

$loop = Factory::create();

// start a process that takes 10s to terminate
$process = new Process('sleep 10');
$process->start($loop);

// report when process exits
$process->on('exit', function ($exit, $term) {
var_dump($exit, $term);
});

// forcefully terminate process after 2s
$loop->addTimer(2.0, function () use ($process) {
$process->stdin->close();
$process->stdout->close();
$process->stderr->close();
$process->terminate();
});

$loop->run();
4 changes: 4 additions & 0 deletions src/Process.php
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,10 @@ public function close()
*/
public function terminate($signal = null)
{
if ($this->process === null) {
return false;
}

if ($signal !== null) {
return proc_terminate($this->process, $signal);
}
Expand Down
29 changes: 29 additions & 0 deletions tests/AbstractProcessTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,35 @@ public function testStartAlreadyRunningProcess()
$process->start($this->createLoop());
}

public function testTerminateProcesWithoutStartingReturnsFalse()
{
$process = new Process('sleep 1');

$this->assertFalse($process->terminate());
}

public function testTerminateWillExit()
{
$loop = $this->createloop();

$process = new Process('sleep 10');
$process->start($loop);

$called = false;
$process->on('exit', function () use (&$called) {
$called = true;
});

$process->stdin->close();
$process->stdout->close();
$process->stderr->close();
$process->terminate();

$loop->run();

$this->assertTrue($called);
}

public function testTerminateWithDefaultTermSignalUsingEventLoop()
{
if (defined('PHP_WINDOWS_VERSION_BUILD')) {
Expand Down