feat: emulate rust's process api more closely

Yoink some docs as well
This commit is contained in:
2025-08-17 17:16:56 +02:00
parent 34e1b0b791
commit 6b1b98b662
4 changed files with 335 additions and 71 deletions

View File

@@ -4,32 +4,138 @@ declare(strict_types=1);
namespace Nih\CommandBuilder;
/**
* Representation of a running or exited child process.
*/
final class Child
{
/**
* @param resource $proc The process handle.
* The child's process handle.
*
* @var resource
*/
public function __construct(
public readonly ?ChildStdin $stdin,
public readonly ?ChildStdout $stdout,
public readonly ?ChildStderr $stderr,
public $proc,
) {
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 childs standard input (stdin), if it has
* been captured.
*/
public readonly ?ChildStdin $stdin;
/**
* The handle for reading from the childs standard output (stdout), if it
* has been captured.
*/
public readonly ?ChildStdout $stdout;
/**
* The handle for reading from the childs 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
{
if (!is_resource($this->proc)) {
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');
}
$status = proc_get_status($this->proc);
return $status['pid'];
// 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->proc)) {
if (!is_resource($this->process)) {
throw new ChildException('Resource was already closed');
}
@@ -38,36 +144,29 @@ final class Child
$stdout = $this->stdout?->getContents();
$stderr = $this->stderr?->getContents();
$status = new ExitStatus(proc_close($this->proc));
$status = $this->wait();
return new Output($stdout, $stderr, $status);
}
public function wait(): ExitStatus
{
if (!is_resource($this->proc)) {
throw new ChildException('Resource was already closed');
}
// Avoid possible deadlock before waiting.
$this->stdin?->close();
return new ExitStatus(proc_close($this->proc));
}
/**
* Forces the child process to exit.
*
* This is equivalent to sending a SIGKILL.
*/
public function kill(): bool
{
if (!is_resource($this->proc)) {
throw new ChildException('Resource was already closed');
if (!is_resource($this->process)) {
return true;
}
return proc_terminate($this->proc, 10);
return proc_terminate($this->process, 9);
}
public function __destruct()
{
if (is_resource($this->proc)) {
proc_close($this->proc);
if (is_resource($this->process)) {
proc_close($this->process);
}
}
}