fix!: handle errors properly
This commit is contained in:
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Nih\CommandBuilder;
|
namespace Nih\CommandBuilder;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Representation of a running or exited child process.
|
* Representation of a running or exited child process.
|
||||||
*/
|
*/
|
||||||
@@ -26,6 +28,11 @@ final class Child
|
|||||||
*/
|
*/
|
||||||
private ?ExitStatus $status = null;
|
private ?ExitStatus $status = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The child's output.
|
||||||
|
*/
|
||||||
|
private ?Output $output = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The handle for writing to the child’s standard input (stdin), if it has
|
* The handle for writing to the child’s standard input (stdin), if it has
|
||||||
* been captured.
|
* been captured.
|
||||||
@@ -45,13 +52,17 @@ final class Child
|
|||||||
public readonly ?ChildStderr $stderr;
|
public readonly ?ChildStderr $stderr;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param resource $process The child's process handle.
|
* @param resource $process The child's process resource handle.
|
||||||
* @param array<int, resource> $pipes File pointers.
|
* @param array<int, resource> $pipes
|
||||||
|
* The indexed array of file pointers that set by {@see \proc_open()}.
|
||||||
|
*
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
* When the given process is not a process resource.
|
||||||
*/
|
*/
|
||||||
public function __construct(mixed $process, array $pipes)
|
public function __construct(mixed $process, array $pipes)
|
||||||
{
|
{
|
||||||
if (get_resource_type($process) !== 'process') {
|
if (get_resource_type($process) !== 'process') {
|
||||||
throw new ChildException('invalid resource type: not a process');
|
throw new InvalidArgumentException('resource is not a process');
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->process = $process;
|
$this->process = $process;
|
||||||
@@ -89,8 +100,6 @@ final class Child
|
|||||||
* waiting. This helps avoid deadlock: it ensures that the child does not
|
* 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
|
* block waiting for input from the parent, while the parent waits for the
|
||||||
* child to exit.
|
* child to exit.
|
||||||
*
|
|
||||||
* @throws ChildException If the resource was already closed
|
|
||||||
*/
|
*/
|
||||||
public function wait(): ExitStatus
|
public function wait(): ExitStatus
|
||||||
{
|
{
|
||||||
@@ -111,7 +120,7 @@ final class Child
|
|||||||
proc_close($this->process);
|
proc_close($this->process);
|
||||||
|
|
||||||
return $this->status = new ExitStatus(
|
return $this->status = new ExitStatus(
|
||||||
$status['exitcode'],
|
$status['exitcode'] < 0 ? null : $status['exitcode'],
|
||||||
$status['signaled'] ? $status['termsig'] : null,
|
$status['signaled'] ? $status['termsig'] : null,
|
||||||
$status['stopped'] ? $status['stopsig'] : null,
|
$status['stopped'] ? $status['stopsig'] : null,
|
||||||
);
|
);
|
||||||
@@ -120,6 +129,8 @@ final class Child
|
|||||||
/**
|
/**
|
||||||
* Simultaneously waits for the child to exit and collect all remaining
|
* Simultaneously waits for the child to exit and collect all remaining
|
||||||
* output on the stdout/stderr handles, returning an {@see Output} instance.
|
* output on the stdout/stderr handles, returning an {@see Output} instance.
|
||||||
|
* 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
|
* 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
|
* waiting. This helps avoid deadlock: it ensures that the child does not
|
||||||
@@ -131,12 +142,13 @@ final class Child
|
|||||||
* parent and child. Use the `stdout` and `stderr` functions of {@see
|
* parent and child. Use the `stdout` and `stderr` functions of {@see
|
||||||
* Command}, respectively.
|
* Command}, respectively.
|
||||||
*
|
*
|
||||||
* @throws ChildException If the resource was already closed
|
* @throws ChildException
|
||||||
|
* @throws StreamException
|
||||||
*/
|
*/
|
||||||
public function waitWithOutput(): Output
|
public function waitWithOutput(): Output
|
||||||
{
|
{
|
||||||
if (!is_resource($this->process)) {
|
if ($this->output) {
|
||||||
throw new ChildException('Resource was already closed');
|
return $this->output;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Avoid possible deadlock before waiting.
|
// Avoid possible deadlock before waiting.
|
||||||
@@ -146,21 +158,29 @@ final class Child
|
|||||||
$stderr = $this->stderr?->getContents();
|
$stderr = $this->stderr?->getContents();
|
||||||
$status = $this->wait();
|
$status = $this->wait();
|
||||||
|
|
||||||
return new Output($stdout, $stderr, $status);
|
return $this->output = new Output($stdout, $stderr, $status);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Forces the child process to exit.
|
* Forces the child process to exit.
|
||||||
*
|
*
|
||||||
* This is equivalent to sending a SIGKILL.
|
* This is equivalent to sending a SIGKILL.
|
||||||
|
*
|
||||||
|
* @throws ChildException
|
||||||
*/
|
*/
|
||||||
public function kill(): bool
|
public function kill(): void
|
||||||
{
|
{
|
||||||
if (!is_resource($this->process)) {
|
if ($this->status) {
|
||||||
return true;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return proc_terminate($this->process, 9);
|
try {
|
||||||
|
set_error_handler(ChildException::handleError(...));
|
||||||
|
$success = proc_terminate($this->process, 9);
|
||||||
|
assert($success);
|
||||||
|
} finally {
|
||||||
|
restore_error_handler();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function __destruct()
|
public function __destruct()
|
||||||
|
|||||||
@@ -4,5 +4,23 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Nih\CommandBuilder;
|
namespace Nih\CommandBuilder;
|
||||||
|
|
||||||
class ChildException extends CommandException
|
/**
|
||||||
{}
|
* A PHP error occured during an operation on a process resource.
|
||||||
|
*/
|
||||||
|
final class ChildException extends CommandException
|
||||||
|
{
|
||||||
|
public static function handleError(
|
||||||
|
int $severity,
|
||||||
|
string $message,
|
||||||
|
string $filename,
|
||||||
|
int $line,
|
||||||
|
): never {
|
||||||
|
throw new self(
|
||||||
|
message: $message,
|
||||||
|
code: 0,
|
||||||
|
severity: $severity,
|
||||||
|
filename: $filename,
|
||||||
|
line: $line,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -278,6 +278,8 @@ final class Command implements Stringable
|
|||||||
* Executes the command as a child process, returning a handle to it.
|
* Executes the command as a child process, returning a handle to it.
|
||||||
*
|
*
|
||||||
* By default, stdin, stdout and stderr are inherited from the parent.
|
* By default, stdin, stdout and stderr are inherited from the parent.
|
||||||
|
*
|
||||||
|
* @throws ChildException
|
||||||
*/
|
*/
|
||||||
public function spawn(): Child
|
public function spawn(): Child
|
||||||
{
|
{
|
||||||
@@ -299,6 +301,8 @@ final class Command implements Stringable
|
|||||||
* collecting its status.
|
* collecting its status.
|
||||||
*
|
*
|
||||||
* By default, stdin, stdout and stderr are inherited from the parent.
|
* By default, stdin, stdout and stderr are inherited from the parent.
|
||||||
|
*
|
||||||
|
* @throws ChildException
|
||||||
*/
|
*/
|
||||||
public function status(): ExitStatus
|
public function status(): ExitStatus
|
||||||
{
|
{
|
||||||
@@ -311,6 +315,8 @@ final class Command implements Stringable
|
|||||||
*
|
*
|
||||||
* By default, stdout and stderr are captured (and used to provide the
|
* By default, stdout and stderr are captured (and used to provide the
|
||||||
* resulting output). Stdin is not inherited from the parent.
|
* resulting output). Stdin is not inherited from the parent.
|
||||||
|
*
|
||||||
|
* @throws ChildException
|
||||||
*/
|
*/
|
||||||
public function output(): Output
|
public function output(): Output
|
||||||
{
|
{
|
||||||
@@ -396,13 +402,6 @@ final class Command implements Stringable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!is_executable($program)) {
|
|
||||||
throw new CommandException(sprintf(
|
|
||||||
'Program "%s" is not executable',
|
|
||||||
$program,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle environment inheritance.
|
// Handle environment inheritance.
|
||||||
$environment = $this->environment;
|
$environment = $this->environment;
|
||||||
if (is_array($environment) && $this->environmentInherit) {
|
if (is_array($environment) && $this->environmentInherit) {
|
||||||
@@ -414,7 +413,7 @@ final class Command implements Stringable
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
set_error_handler(CommandException::handleError(...));
|
set_error_handler(ChildException::handleError(...));
|
||||||
$proc = proc_open(
|
$proc = proc_open(
|
||||||
[$program, ...$this->args],
|
[$program, ...$this->args],
|
||||||
$descriptorSpec,
|
$descriptorSpec,
|
||||||
|
|||||||
@@ -4,16 +4,9 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Nih\CommandBuilder;
|
namespace Nih\CommandBuilder;
|
||||||
|
|
||||||
use RuntimeException;
|
use ErrorException;
|
||||||
|
|
||||||
class CommandException extends RuntimeException
|
/**
|
||||||
{
|
*
|
||||||
public static function handleError(int $errno, string $errstr, string $file, int $line): never
|
*/
|
||||||
{
|
abstract class CommandException extends ErrorException {}
|
||||||
$exception = new self($errstr);
|
|
||||||
$exception->file = $file;
|
|
||||||
$exception->line = $line;
|
|
||||||
|
|
||||||
throw $exception;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ final readonly class ExitStatus
|
|||||||
*/
|
*/
|
||||||
public function success(): bool
|
public function success(): bool
|
||||||
{
|
{
|
||||||
return $this->code === 0;
|
return $this->code === 0
|
||||||
|
&& $this->signal === null
|
||||||
|
&& $this->stoppedSignal === null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -5,6 +5,38 @@ declare(strict_types=1);
|
|||||||
namespace Nih\CommandBuilder;
|
namespace Nih\CommandBuilder;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An error prevented reading from or writing to a stream resource.
|
* A PHP error occured during an operation on a stream resource.
|
||||||
*/
|
*/
|
||||||
class StreamException extends CommandException {}
|
final class StreamException extends CommandException
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* A note on handling IO errors by setting an error handler before each
|
||||||
|
* operation: Yes, this is slower, but not by much (1.5x).
|
||||||
|
*
|
||||||
|
* However, it is even slower if wrapped with another function, so it is better
|
||||||
|
* to do this inline every time before the function (e.g. fopen) is called.
|
||||||
|
*
|
||||||
|
* ```php
|
||||||
|
* try {
|
||||||
|
* set_error_handler(CommandException::handleError(...));
|
||||||
|
* $handle = fopen('foo', 'r');
|
||||||
|
* } finally {
|
||||||
|
* restore_error_handler();
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
public static function handleError(
|
||||||
|
int $severity,
|
||||||
|
string $message,
|
||||||
|
string $filename,
|
||||||
|
int $line,
|
||||||
|
): never {
|
||||||
|
throw new self(
|
||||||
|
message: $message,
|
||||||
|
code: 0,
|
||||||
|
severity: $severity,
|
||||||
|
filename: $filename,
|
||||||
|
line: $line,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user