173 lines
4.7 KiB
PHP
173 lines
4.7 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace Nih\CommandBuilder;
|
||
|
||
/**
|
||
* Representation of a running or exited child process.
|
||
*/
|
||
final class Child
|
||
{
|
||
/**
|
||
* The child's process handle.
|
||
*
|
||
* @var resource
|
||
*/
|
||
private readonly mixed $process;
|
||
|
||
/**
|
||
* The child's process identifier.
|
||
*/
|
||
private readonly int $id;
|
||
|
||
/**
|
||
* The child's exit status.
|
||
*/
|
||
private ?ExitStatus $status = null;
|
||
|
||
/**
|
||
* The handle for writing to the child’s standard input (stdin), if it has
|
||
* been captured.
|
||
*/
|
||
public readonly ?ChildStdin $stdin;
|
||
|
||
/**
|
||
* The handle for reading from the child’s standard output (stdout), if it
|
||
* has been captured.
|
||
*/
|
||
public readonly ?ChildStdout $stdout;
|
||
|
||
/**
|
||
* The handle for reading from the child’s standard error (stderr), if it
|
||
* has been captured.
|
||
*/
|
||
public readonly ?ChildStderr $stderr;
|
||
|
||
/**
|
||
* @param resource $process The child's process handle.
|
||
* @param array<int, resource> $pipes File pointers.
|
||
*/
|
||
public function __construct(mixed $process, array $pipes)
|
||
{
|
||
$this->process = $process;
|
||
|
||
$status = proc_get_status($this->process);
|
||
$this->id = $status['pid'];
|
||
|
||
$this->stdin = array_key_exists(0, $pipes)
|
||
? new ChildStdin($pipes[0])
|
||
: null;
|
||
|
||
$this->stdout = array_key_exists(1, $pipes)
|
||
? new ChildStdout($pipes[1])
|
||
: null;
|
||
|
||
$this->stderr = array_key_exists(2, $pipes)
|
||
? new ChildStderr($pipes[2])
|
||
: null;
|
||
}
|
||
|
||
/**
|
||
* Returns the OS-assigned process identifier associated with this child.
|
||
*/
|
||
public function id(): int
|
||
{
|
||
return $this->id;
|
||
}
|
||
|
||
/**
|
||
* Waits for the child to exit completely, returning the status that it
|
||
* exited with. This function will continue to have the same return value
|
||
* after it has been called at least once.
|
||
*
|
||
* The stdin handle to the child process, if any, will be closed before
|
||
* waiting. This helps avoid deadlock: it ensures that the child does not
|
||
* block waiting for input from the parent, while the parent waits for the
|
||
* child to exit.
|
||
*
|
||
* @throws ChildException If the resource was already closed
|
||
*/
|
||
public function wait(): ExitStatus
|
||
{
|
||
if ($this->status) {
|
||
return $this->status;
|
||
}
|
||
|
||
if (!is_resource($this->process)) {
|
||
throw new ChildException('Resource was already closed');
|
||
}
|
||
|
||
// Avoid possible deadlock before waiting.
|
||
$this->stdin?->close();
|
||
|
||
$status = proc_get_status($this->process);
|
||
while ($status['running']) {
|
||
// Suboptimal, but it is what it is...
|
||
usleep(50);
|
||
$status = proc_get_status($this->process);
|
||
};
|
||
|
||
proc_close($this->process);
|
||
|
||
return $this->status = new ExitStatus(
|
||
$status['exitcode'],
|
||
$status['signaled'] ? $status['termsig'] : null,
|
||
$status['stopped'] ? $status['stopsig'] : null,
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Simultaneously waits for the child to exit and collect all remaining
|
||
* output on the stdout/stderr handles, returning an {@see Output} instance.
|
||
*
|
||
* The stdin handle to the child process, if any, will be closed before
|
||
* waiting. This helps avoid deadlock: it ensures that the child does not
|
||
* block waiting for input from the parent, while the parent waits for the
|
||
* child to exit.
|
||
*
|
||
* By default, stdin, stdout and stderr are inherited from the parent. In
|
||
* order to capture the output it is necessary to create new pipes between
|
||
* parent and child. Use the `stdout` and `stderr` functions of {@see
|
||
* Command}, respectively.
|
||
*
|
||
* @throws ChildException If the resource was already closed
|
||
*/
|
||
public function waitWithOutput(): Output
|
||
{
|
||
if (!is_resource($this->process)) {
|
||
throw new ChildException('Resource was already closed');
|
||
}
|
||
|
||
// Avoid possible deadlock before waiting.
|
||
$this->stdin?->close();
|
||
|
||
$stdout = $this->stdout?->getContents();
|
||
$stderr = $this->stderr?->getContents();
|
||
$status = $this->wait();
|
||
|
||
return new Output($stdout, $stderr, $status);
|
||
}
|
||
|
||
/**
|
||
* Forces the child process to exit.
|
||
*
|
||
* This is equivalent to sending a SIGKILL.
|
||
*/
|
||
public function kill(): bool
|
||
{
|
||
if (!is_resource($this->process)) {
|
||
return true;
|
||
}
|
||
|
||
return proc_terminate($this->process, 9);
|
||
}
|
||
|
||
public function __destruct()
|
||
{
|
||
if (is_resource($this->process)) {
|
||
proc_close($this->process);
|
||
}
|
||
}
|
||
}
|