first commit

This commit is contained in:
Sampanna Rimal
2024-08-27 17:48:06 +05:45
commit 53c0140f58
10839 changed files with 1125847 additions and 0 deletions

View File

@ -0,0 +1,42 @@
<?php
namespace Illuminate\Process\Exceptions;
use Illuminate\Contracts\Process\ProcessResult;
use RuntimeException;
class ProcessFailedException extends RuntimeException
{
/**
* The process result instance.
*
* @var \Illuminate\Contracts\Process\ProcessResult
*/
public $result;
/**
* Create a new exception instance.
*
* @param \Illuminate\Contracts\Process\ProcessResult $result
* @return void
*/
public function __construct(ProcessResult $result)
{
$this->result = $result;
$error = sprintf('The command "%s" failed.'."\n\nExit Code: %s",
$result->command(),
$result->exitCode(),
);
if (! empty($result->output())) {
$error .= sprintf("\n\nOutput:\n================\n%s", $result->output());
}
if (! empty($result->errorOutput())) {
$error .= sprintf("\n\nError Output:\n================\n%s", $result->errorOutput());
}
parent::__construct($error, $result->exitCode() ?? 1);
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace Illuminate\Process\Exceptions;
use Illuminate\Contracts\Process\ProcessResult;
use Symfony\Component\Process\Exception\ProcessTimedOutException as SymfonyTimeoutException;
use Symfony\Component\Process\Exception\RuntimeException;
class ProcessTimedOutException extends RuntimeException
{
/**
* The process result instance.
*
* @var \Illuminate\Contracts\Process\ProcessResult
*/
public $result;
/**
* Create a new exception instance.
*
* @param \Symfony\Component\Process\Exception\ProcessTimedOutException $original
* @param \Illuminate\Contracts\Process\ProcessResult $result
* @return void
*/
public function __construct(SymfonyTimeoutException $original, ProcessResult $result)
{
$this->result = $result;
parent::__construct($original->getMessage(), $original->getCode(), $original);
}
}

View File

@ -0,0 +1,328 @@
<?php
namespace Illuminate\Process;
use Closure;
use Illuminate\Contracts\Process\ProcessResult as ProcessResultContract;
use Illuminate\Support\Traits\Macroable;
use PHPUnit\Framework\Assert as PHPUnit;
class Factory
{
use Macroable {
__call as macroCall;
}
/**
* Indicates if the process factory has faked process handlers.
*
* @var bool
*/
protected $recording = false;
/**
* All of the recorded processes.
*
* @var array
*/
protected $recorded = [];
/**
* The registered fake handler callbacks.
*
* @var array
*/
protected $fakeHandlers = [];
/**
* Indicates that an exception should be thrown if any process is not faked.
*
* @var bool
*/
protected $preventStrayProcesses = false;
/**
* Create a new fake process response for testing purposes.
*
* @param array|string $output
* @param array|string $errorOutput
* @param int $exitCode
* @return \Illuminate\Process\FakeProcessResult
*/
public function result(array|string $output = '', array|string $errorOutput = '', int $exitCode = 0)
{
return new FakeProcessResult(
output: $output,
errorOutput: $errorOutput,
exitCode: $exitCode,
);
}
/**
* Begin describing a fake process lifecycle.
*
* @return \Illuminate\Process\FakeProcessDescription
*/
public function describe()
{
return new FakeProcessDescription;
}
/**
* Begin describing a fake process sequence.
*
* @param array $processes
* @return \Illuminate\Process\FakeProcessSequence
*/
public function sequence(array $processes = [])
{
return new FakeProcessSequence($processes);
}
/**
* Indicate that the process factory should fake processes.
*
* @param \Closure|array|null $callback
* @return $this
*/
public function fake(Closure|array $callback = null)
{
$this->recording = true;
if (is_null($callback)) {
$this->fakeHandlers = ['*' => fn () => new FakeProcessResult];
return $this;
}
if ($callback instanceof Closure) {
$this->fakeHandlers = ['*' => $callback];
return $this;
}
foreach ($callback as $command => $handler) {
$this->fakeHandlers[is_numeric($command) ? '*' : $command] = $handler instanceof Closure
? $handler
: fn () => $handler;
}
return $this;
}
/**
* Determine if the process factory has fake process handlers and is recording processes.
*
* @return bool
*/
public function isRecording()
{
return $this->recording;
}
/**
* Record the given process if processes should be recorded.
*
* @param \Illuminate\Process\PendingProcess $process
* @param \Illuminate\Contracts\Process\ProcessResult $result
* @return $this
*/
public function recordIfRecording(PendingProcess $process, ProcessResultContract $result)
{
if ($this->isRecording()) {
$this->record($process, $result);
}
return $this;
}
/**
* Record the given process.
*
* @param \Illuminate\Process\PendingProcess $process
* @param \Illuminate\Contracts\Process\ProcessResult $result
* @return $this
*/
public function record(PendingProcess $process, ProcessResultContract $result)
{
$this->recorded[] = [$process, $result];
return $this;
}
/**
* Indicate that an exception should be thrown if any process is not faked.
*
* @param bool $prevent
* @return $this
*/
public function preventStrayProcesses(bool $prevent = true)
{
$this->preventStrayProcesses = $prevent;
return $this;
}
/**
* Determine if stray processes are being prevented.
*
* @return bool
*/
public function preventingStrayProcesses()
{
return $this->preventStrayProcesses;
}
/**
* Assert that a process was recorded matching a given truth test.
*
* @param \Closure|string $callback
* @return $this
*/
public function assertRan(Closure|string $callback)
{
$callback = is_string($callback) ? fn ($process) => $process->command === $callback : $callback;
PHPUnit::assertTrue(
collect($this->recorded)->filter(function ($pair) use ($callback) {
return $callback($pair[0], $pair[1]);
})->count() > 0,
'An expected process was not invoked.'
);
return $this;
}
/**
* Assert that a process was recorded a given number of times matching a given truth test.
*
* @param \Closure|string $callback
* @param int $times
* @return $this
*/
public function assertRanTimes(Closure|string $callback, int $times = 1)
{
$callback = is_string($callback) ? fn ($process) => $process->command === $callback : $callback;
$count = collect($this->recorded)->filter(function ($pair) use ($callback) {
return $callback($pair[0], $pair[1]);
})->count();
PHPUnit::assertSame(
$times, $count,
"An expected process ran {$count} times instead of {$times} times."
);
return $this;
}
/**
* Assert that a process was not recorded matching a given truth test.
*
* @param \Closure|string $callback
* @return $this
*/
public function assertNotRan(Closure|string $callback)
{
$callback = is_string($callback) ? fn ($process) => $process->command === $callback : $callback;
PHPUnit::assertTrue(
collect($this->recorded)->filter(function ($pair) use ($callback) {
return $callback($pair[0], $pair[1]);
})->count() === 0,
'An unexpected process was invoked.'
);
return $this;
}
/**
* Assert that a process was not recorded matching a given truth test.
*
* @param \Closure|string $callback
* @return $this
*/
public function assertDidntRun(Closure|string $callback)
{
return $this->assertNotRan($callback);
}
/**
* Assert that no processes were recorded.
*
* @return $this
*/
public function assertNothingRan()
{
PHPUnit::assertEmpty(
$this->recorded,
'An unexpected process was invoked.'
);
return $this;
}
/**
* Start defining a pool of processes.
*
* @param callable $callback
* @return \Illuminate\Process\Pool
*/
public function pool(callable $callback)
{
return new Pool($this, $callback);
}
/**
* Start defining a series of piped processes.
*
* @param callable|array $callback
* @return \Illuminate\Contracts\Process\ProcessResult
*/
public function pipe(callable|array $callback, ?callable $output = null)
{
return is_array($callback)
? (new Pipe($this, fn ($pipe) => collect($callback)->each(
fn ($command) => $pipe->command($command)
)))->run(output: $output)
: (new Pipe($this, $callback))->run(output: $output);
}
/**
* Run a pool of processes and wait for them to finish executing.
*
* @param callable $callback
* @param callable|null $output
* @return \Illuminate\Process\ProcessPoolResults
*/
public function concurrently(callable $callback, ?callable $output = null)
{
return (new Pool($this, $callback))->start($output)->wait();
}
/**
* Create a new pending process associated with this factory.
*
* @return \Illuminate\Process\PendingProcess
*/
public function newPendingProcess()
{
return (new PendingProcess($this))->withFakeHandlers($this->fakeHandlers);
}
/**
* Dynamically proxy methods to a new pending process instance.
*
* @param string $method
* @param array $parameters
* @return mixed
*/
public function __call($method, $parameters)
{
if (static::hasMacro($method)) {
return $this->macroCall($method, $parameters);
}
return $this->newPendingProcess()->{$method}(...$parameters);
}
}

View File

@ -0,0 +1,302 @@
<?php
namespace Illuminate\Process;
use Illuminate\Contracts\Process\InvokedProcess as InvokedProcessContract;
class FakeInvokedProcess implements InvokedProcessContract
{
/**
* The command being faked.
*
* @var string
*/
protected $command;
/**
* The underlying process description.
*
* @var \Illuminate\Process\FakeProcessDescription
*/
protected $process;
/**
* The signals that have been received.
*
* @var array
*/
protected $receivedSignals = [];
/**
* The number of times the process should indicate that it is "running".
*
* @var int|null
*/
protected $remainingRunIterations;
/**
* The general output handler callback.
*
* @var callable|null
*/
protected $outputHandler;
/**
* The current output's index.
*
* @var int
*/
protected $nextOutputIndex = 0;
/**
* The current error output's index.
*
* @var int
*/
protected $nextErrorOutputIndex = 0;
/**
* Create a new invoked process instance.
*
* @param string $command
* @param \Illuminate\Process\FakeProcessDescription $process
* @return void
*/
public function __construct(string $command, FakeProcessDescription $process)
{
$this->command = $command;
$this->process = $process;
}
/**
* Get the process ID if the process is still running.
*
* @return int|null
*/
public function id()
{
$this->invokeOutputHandlerWithNextLineOfOutput();
return $this->process->processId;
}
/**
* Send a signal to the process.
*
* @param int $signal
* @return $this
*/
public function signal(int $signal)
{
$this->invokeOutputHandlerWithNextLineOfOutput();
$this->receivedSignals[] = $signal;
return $this;
}
/**
* Determine if the process has received the given signal.
*
* @param int $signal
* @return bool
*/
public function hasReceivedSignal(int $signal)
{
return in_array($signal, $this->receivedSignals);
}
/**
* Determine if the process is still running.
*
* @return bool
*/
public function running()
{
$this->invokeOutputHandlerWithNextLineOfOutput();
$this->remainingRunIterations = is_null($this->remainingRunIterations)
? $this->process->runIterations
: $this->remainingRunIterations;
if ($this->remainingRunIterations === 0) {
while ($this->invokeOutputHandlerWithNextLineOfOutput()) {
}
return false;
}
$this->remainingRunIterations = $this->remainingRunIterations - 1;
return true;
}
/**
* Invoke the asynchronous output handler with the next single line of output if necessary.
*
* @return array|false
*/
protected function invokeOutputHandlerWithNextLineOfOutput()
{
if (! $this->outputHandler) {
return false;
}
[$outputCount, $outputStartingPoint] = [
count($this->process->output),
min($this->nextOutputIndex, $this->nextErrorOutputIndex),
];
for ($i = $outputStartingPoint; $i < $outputCount; $i++) {
$currentOutput = $this->process->output[$i];
if ($currentOutput['type'] === 'out' && $i >= $this->nextOutputIndex) {
call_user_func($this->outputHandler, 'out', $currentOutput['buffer']);
$this->nextOutputIndex = $i + 1;
return $currentOutput;
} elseif ($currentOutput['type'] === 'err' && $i >= $this->nextErrorOutputIndex) {
call_user_func($this->outputHandler, 'err', $currentOutput['buffer']);
$this->nextErrorOutputIndex = $i + 1;
return $currentOutput;
}
}
return false;
}
/**
* Get the standard output for the process.
*
* @return string
*/
public function output()
{
$this->latestOutput();
$output = [];
for ($i = 0; $i < $this->nextOutputIndex; $i++) {
if ($this->process->output[$i]['type'] === 'out') {
$output[] = $this->process->output[$i]['buffer'];
}
}
return rtrim(implode('', $output), "\n")."\n";
}
/**
* Get the error output for the process.
*
* @return string
*/
public function errorOutput()
{
$this->latestErrorOutput();
$output = [];
for ($i = 0; $i < $this->nextErrorOutputIndex; $i++) {
if ($this->process->output[$i]['type'] === 'err') {
$output[] = $this->process->output[$i]['buffer'];
}
}
return rtrim(implode('', $output), "\n")."\n";
}
/**
* Get the latest standard output for the process.
*
* @return string
*/
public function latestOutput()
{
$outputCount = count($this->process->output);
for ($i = $this->nextOutputIndex; $i < $outputCount; $i++) {
if ($this->process->output[$i]['type'] === 'out') {
$output = $this->process->output[$i]['buffer'];
$this->nextOutputIndex = $i + 1;
break;
}
$this->nextOutputIndex = $i + 1;
}
return isset($output) ? $output : '';
}
/**
* Get the latest error output for the process.
*
* @return string
*/
public function latestErrorOutput()
{
$outputCount = count($this->process->output);
for ($i = $this->nextErrorOutputIndex; $i < $outputCount; $i++) {
if ($this->process->output[$i]['type'] === 'err') {
$output = $this->process->output[$i]['buffer'];
$this->nextErrorOutputIndex = $i + 1;
break;
}
$this->nextErrorOutputIndex = $i + 1;
}
return isset($output) ? $output : '';
}
/**
* Wait for the process to finish.
*
* @param callable|null $output
* @return \Illuminate\Contracts\Process\ProcessResult
*/
public function wait(callable $output = null)
{
$this->outputHandler = $output ?: $this->outputHandler;
if (! $this->outputHandler) {
$this->remainingRunIterations = 0;
return $this->predictProcessResult();
}
while ($this->invokeOutputHandlerWithNextLineOfOutput()) {
//
}
$this->remainingRunIterations = 0;
return $this->process->toProcessResult($this->command);
}
/**
* Get the ultimate process result that will be returned by this "process".
*
* @return \Illuminate\Contracts\Process\ProcessResult
*/
public function predictProcessResult()
{
return $this->process->toProcessResult($this->command);
}
/**
* Set the general output handler for the fake invoked process.
*
* @param callable|null $output
* @return $this
*/
public function withOutputHandler(?callable $outputHandler)
{
$this->outputHandler = $outputHandler;
return $this;
}
}

View File

@ -0,0 +1,225 @@
<?php
namespace Illuminate\Process;
use Symfony\Component\Process\Process;
class FakeProcessDescription
{
/**
* The process' ID.
*
* @var int|null
*/
public $processId = 1000;
/**
* All of the process' output in the order it was described.
*
* @var array
*/
public $output = [];
/**
* The process' exit code.
*
* @var int
*/
public $exitCode = 0;
/**
* The number of times the process should indicate that it is "running".
*
* @var int
*/
public $runIterations = 0;
/**
* Specify the process ID that should be assigned to the process.
*
* @param int $processId
* @return $this
*/
public function id(int $processId)
{
$this->processId = $processId;
return $this;
}
/**
* Describe a line of standard output.
*
* @param array|string $output
* @return $this
*/
public function output(array|string $output)
{
if (is_array($output)) {
collect($output)->each(fn ($line) => $this->output($line));
return $this;
}
$this->output[] = ['type' => 'out', 'buffer' => rtrim($output, "\n")."\n"];
return $this;
}
/**
* Describe a line of error output.
*
* @param array|string $output
* @return $this
*/
public function errorOutput(array|string $output)
{
if (is_array($output)) {
collect($output)->each(fn ($line) => $this->errorOutput($line));
return $this;
}
$this->output[] = ['type' => 'err', 'buffer' => rtrim($output, "\n")."\n"];
return $this;
}
/**
* Replace the entire output buffer with the given string.
*
* @param string $output
* @return $this
*/
public function replaceOutput(string $output)
{
$this->output = collect($this->output)->reject(function ($output) {
return $output['type'] === 'out';
})->values()->all();
if (strlen($output) > 0) {
$this->output[] = [
'type' => 'out',
'buffer' => rtrim($output, "\n")."\n",
];
}
return $this;
}
/**
* Replace the entire error output buffer with the given string.
*
* @param string $output
* @return $this
*/
public function replaceErrorOutput(string $output)
{
$this->output = collect($this->output)->reject(function ($output) {
return $output['type'] === 'err';
})->values()->all();
if (strlen($output) > 0) {
$this->output[] = [
'type' => 'err',
'buffer' => rtrim($output, "\n")."\n",
];
}
return $this;
}
/**
* Specify the process exit code.
*
* @param int $exitCode
* @return $this
*/
public function exitCode(int $exitCode)
{
$this->exitCode = $exitCode;
return $this;
}
/**
* Specify how many times the "isRunning" method should return "true".
*
* @param int $iterations
* @return $this
*/
public function iterations(int $iterations)
{
return $this->runsFor(iterations: $iterations);
}
/**
* Specify how many times the "isRunning" method should return "true".
*
* @param int $iterations
* @return $this
*/
public function runsFor(int $iterations)
{
$this->runIterations = $iterations;
return $this;
}
/**
* Turn the fake process description into an actual process.
*
* @param string $command
* @return \Symfony\Component\Process\Process
*/
public function toSymfonyProcess(string $command)
{
return Process::fromShellCommandline($command);
}
/**
* Convert the process description into a process result.
*
* @param string $command
* @return \Illuminate\Contracts\Process\ProcessResult
*/
public function toProcessResult(string $command)
{
return new FakeProcessResult(
command: $command,
exitCode: $this->exitCode,
output: $this->resolveOutput(),
errorOutput: $this->resolveErrorOutput(),
);
}
/**
* Resolve the standard output as a string.
*
* @return string
*/
protected function resolveOutput()
{
$output = collect($this->output)
->filter(fn ($output) => $output['type'] === 'out');
return $output->isNotEmpty()
? rtrim($output->map->buffer->implode(''), "\n")."\n"
: '';
}
/**
* Resolve the error output as a string.
*
* @return string
*/
protected function resolveErrorOutput()
{
$output = collect($this->output)
->filter(fn ($output) => $output['type'] === 'err');
return $output->isNotEmpty()
? rtrim($output->map->buffer->implode(''), "\n")."\n"
: '';
}
}

View File

@ -0,0 +1,210 @@
<?php
namespace Illuminate\Process;
use Illuminate\Contracts\Process\ProcessResult as ProcessResultContract;
use Illuminate\Process\Exceptions\ProcessFailedException;
class FakeProcessResult implements ProcessResultContract
{
/**
* The command string.
*
* @var string
*/
protected $command;
/**
* The process exit code.
*
* @var int
*/
protected $exitCode;
/**
* The process output.
*
* @var string
*/
protected $output = '';
/**
* The process error output.
*
* @var string
*/
protected $errorOutput = '';
/**
* Create a new process result instance.
*
* @param string $command
* @param int $exitCode
* @param array|string $output
* @param array|string $errorOutput
* @return void
*/
public function __construct(string $command = '', int $exitCode = 0, array|string $output = '', array|string $errorOutput = '')
{
$this->command = $command;
$this->exitCode = $exitCode;
$this->output = $this->normalizeOutput($output);
$this->errorOutput = $this->normalizeOutput($errorOutput);
}
/**
* Normalize the given output into a string with newlines.
*
* @param array|string $output
* @return string
*/
protected function normalizeOutput(array|string $output)
{
if (empty($output)) {
return '';
} elseif (is_string($output)) {
return rtrim($output, "\n")."\n";
} elseif (is_array($output)) {
return rtrim(
collect($output)
->map(fn ($line) => rtrim($line, "\n")."\n")
->implode(''),
"\n"
);
}
}
/**
* Get the original command executed by the process.
*
* @return string
*/
public function command()
{
return $this->command;
}
/**
* Create a new fake process result with the given command.
*
* @param string $command
* @return self
*/
public function withCommand(string $command)
{
return new FakeProcessResult($command, $this->exitCode, $this->output, $this->errorOutput);
}
/**
* Determine if the process was successful.
*
* @return bool
*/
public function successful()
{
return $this->exitCode === 0;
}
/**
* Determine if the process failed.
*
* @return bool
*/
public function failed()
{
return ! $this->successful();
}
/**
* Get the exit code of the process.
*
* @return int
*/
public function exitCode()
{
return $this->exitCode;
}
/**
* Get the standard output of the process.
*
* @return string
*/
public function output()
{
return $this->output;
}
/**
* Determine if the output contains the given string.
*
* @param string $output
* @return bool
*/
public function seeInOutput(string $output)
{
return str_contains($this->output(), $output);
}
/**
* Get the error output of the process.
*
* @return string
*/
public function errorOutput()
{
return $this->errorOutput;
}
/**
* Determine if the error output contains the given string.
*
* @param string $output
* @return bool
*/
public function seeInErrorOutput(string $output)
{
return str_contains($this->errorOutput(), $output);
}
/**
* Throw an exception if the process failed.
*
* @param callable|null $callback
* @return $this
*
* @throws \Illuminate\Process\Exceptions\ProcessFailedException
*/
public function throw(callable $callback = null)
{
if ($this->successful()) {
return $this;
}
$exception = new ProcessFailedException($this);
if ($callback) {
$callback($this, $exception);
}
throw $exception;
}
/**
* Throw an exception if the process failed and the given condition is true.
*
* @param bool $condition
* @param callable|null $callback
* @return $this
*
* @throws \Throwable
*/
public function throwIf(bool $condition, callable $callback = null)
{
if ($condition) {
return $this->throw($callback);
}
return $this;
}
}

View File

@ -0,0 +1,121 @@
<?php
namespace Illuminate\Process;
use Illuminate\Contracts\Process\ProcessResult as ProcessResultContract;
use OutOfBoundsException;
class FakeProcessSequence
{
/**
* The fake process results and descriptions.
*
* @var array
*/
protected $processes = [];
/**
* Indicates that invoking this sequence when it is empty should throw an exception.
*
* @var bool
*/
protected $failWhenEmpty = true;
/**
* The response that should be returned when the sequence is empty.
*
* @var \Illuminate\Contracts\Process\ProcessResult|\Illuminate\Process\FakeProcessDescription
*/
protected $emptyProcess;
/**
* Create a new fake process sequence instance.
*
* @param array $processes
* @return void
*/
public function __construct(array $processes = [])
{
$this->processes = $processes;
}
/**
* Push a new process result or description onto the sequence.
*
* @param \Illuminate\Contracts\Process\ProcessResult|\Illuminate\Process\FakeProcessDescription|array|string $process
* @return $this
*/
public function push(ProcessResultContract|FakeProcessDescription|array|string $process)
{
$this->processes[] = $this->toProcessResult($process);
return $this;
}
/**
* Make the sequence return a default result when it is empty.
*
* @param \Illuminate\Contracts\Process\ProcessResult|\Illuminate\Process\FakeProcessDescription|array|string $process
* @return $this
*/
public function whenEmpty(ProcessResultContract|FakeProcessDescription|array|string $process)
{
$this->failWhenEmpty = false;
$this->emptyProcess = $this->toProcessResult($process);
return $this;
}
/**
* Convert the given result into an actual process result or description.
*
* @param \Illuminate\Contracts\Process\ProcessResult|\Illuminate\Process\FakeProcessDescription|array|string $process
* @return \Illuminate\Contracts\Process\ProcessResult|\Illuminate\Process\FakeProcessDescription
*/
protected function toProcessResult(ProcessResultContract|FakeProcessDescription|array|string $process)
{
return is_array($process) || is_string($process)
? new FakeProcessResult(output: $process)
: $process;
}
/**
* Make the sequence return a default result when it is empty.
*
* @return $this
*/
public function dontFailWhenEmpty()
{
return $this->whenEmpty(new FakeProcessResult);
}
/**
* Indicate that this sequence has depleted all of its process results.
*
* @return bool
*/
public function isEmpty()
{
return count($this->processes) === 0;
}
/**
* Get the next process in the sequence.
*
* @return \Illuminate\Contracts\Process\ProcessResult|\Illuminate\Process\FakeProcessDescription
*
* @throws \OutOfBoundsException
*/
public function __invoke()
{
if ($this->failWhenEmpty && count($this->processes) === 0) {
throw new OutOfBoundsException('A process was invoked, but the process result sequence is empty.');
}
if (! $this->failWhenEmpty && count($this->processes) === 0) {
return value($this->emptyProcess ?? new FakeProcessResult);
}
return array_shift($this->processes);
}
}

View File

@ -0,0 +1,121 @@
<?php
namespace Illuminate\Process;
use Illuminate\Contracts\Process\InvokedProcess as InvokedProcessContract;
use Illuminate\Process\Exceptions\ProcessTimedOutException;
use Symfony\Component\Process\Exception\ProcessTimedOutException as SymfonyTimeoutException;
use Symfony\Component\Process\Process;
class InvokedProcess implements InvokedProcessContract
{
/**
* The underlying process instance.
*
* @var \Symfony\Component\Process\Process
*/
protected $process;
/**
* Create a new invoked process instance.
*
* @param \Symfony\Component\Process\Process $process
* @return void
*/
public function __construct(Process $process)
{
$this->process = $process;
}
/**
* Get the process ID if the process is still running.
*
* @return int|null
*/
public function id()
{
return $this->process->getPid();
}
/**
* Send a signal to the process.
*
* @param int $signal
* @return $this
*/
public function signal(int $signal)
{
$this->process->signal($signal);
return $this;
}
/**
* Determine if the process is still running.
*
* @return bool
*/
public function running()
{
return $this->process->isRunning();
}
/**
* Get the standard output for the process.
*
* @return string
*/
public function output()
{
return $this->process->getOutput();
}
/**
* Get the error output for the process.
*
* @return string
*/
public function errorOutput()
{
return $this->process->getErrorOutput();
}
/**
* Get the latest standard output for the process.
*
* @return string
*/
public function latestOutput()
{
return $this->process->getIncrementalOutput();
}
/**
* Get the latest error output for the process.
*
* @return string
*/
public function latestErrorOutput()
{
return $this->process->getIncrementalErrorOutput();
}
/**
* Wait for the process to finish.
*
* @param callable|null $output
* @return \Illuminate\Process\ProcessResult
*
* @throws \Illuminate\Process\Exceptions\ProcessTimedOutException
*/
public function wait(callable $output = null)
{
try {
$this->process->wait($output);
return new ProcessResult($this->process);
} catch (SymfonyTimeoutException $e) {
throw new ProcessTimedOutException($e, new ProcessResult($this->process));
}
}
}

View File

@ -0,0 +1,67 @@
<?php
namespace Illuminate\Process;
use Countable;
class InvokedProcessPool implements Countable
{
/**
* The array of invoked processes.
*
* @var array
*/
protected $invokedProcesses;
/**
* Create a new invoked process pool.
*
* @param array $invokedProcesses
* @return void
*/
public function __construct(array $invokedProcesses)
{
$this->invokedProcesses = $invokedProcesses;
}
/**
* Send a signal to each running process in the pool, returning the processes that were signalled.
*
* @param int $signal
* @return \Illuminate\Support\Collection
*/
public function signal(int $signal)
{
return $this->running()->each->signal($signal);
}
/**
* Get the processes in the pool that are still currently running.
*
* @return \Illuminate\Support\Collection
*/
public function running()
{
return collect($this->invokedProcesses)->filter->running()->values();
}
/**
* Wait for the processes to finish.
*
* @return \Illuminate\Process\ProcessPoolResults
*/
public function wait()
{
return new ProcessPoolResults(collect($this->invokedProcesses)->map->wait()->all());
}
/**
* Get the total number of processes.
*
* @return int
*/
public function count(): int
{
return count($this->invokedProcesses);
}
}

View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) Taylor Otwell
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,424 @@
<?php
namespace Illuminate\Process;
use Closure;
use Illuminate\Process\Exceptions\ProcessTimedOutException;
use Illuminate\Support\Str;
use Illuminate\Support\Traits\Conditionable;
use LogicException;
use RuntimeException;
use Symfony\Component\Process\Exception\ProcessTimedOutException as SymfonyTimeoutException;
use Symfony\Component\Process\Process;
class PendingProcess
{
use Conditionable;
/**
* The process factory instance.
*
* @var \Illuminate\Process\Factory
*/
protected $factory;
/**
* The command to invoke the process.
*
* @var array<array-key, string>|string|null
*/
public $command;
/**
* The working directory of the process.
*
* @var string|null
*/
public $path;
/**
* The maximum number of seconds the process may run.
*
* @var int|null
*/
public $timeout = 60;
/**
* The maximum number of seconds the process may go without returning output.
*
* @var int
*/
public $idleTimeout;
/**
* The additional environment variables for the process.
*
* @var array
*/
public $environment = [];
/**
* The standard input data that should be piped into the command.
*
* @var string|int|float|bool|resource|\Traversable|null
*/
public $input;
/**
* Indicates whether output should be disabled for the process.
*
* @var bool
*/
public $quietly = false;
/**
* Indicates if TTY mode should be enabled.
*
* @var bool
*/
public $tty = false;
/**
* The options that will be passed to "proc_open".
*
* @var array
*/
public $options = [];
/**
* The registered fake handler callbacks.
*
* @var array
*/
protected $fakeHandlers = [];
/**
* Create a new pending process instance.
*
* @param \Illuminate\Process\Factory $factory
* @return void
*/
public function __construct(Factory $factory)
{
$this->factory = $factory;
}
/**
* Specify the command that will invoke the process.
*
* @param array<array-key, string>|string $command
* @return $this
*/
public function command(array|string $command)
{
$this->command = $command;
return $this;
}
/**
* Specify the working directory of the process.
*
* @param string $path
* @return $this
*/
public function path(string $path)
{
$this->path = $path;
return $this;
}
/**
* Specify the maximum number of seconds the process may run.
*
* @param int $timeout
* @return $this
*/
public function timeout(int $timeout)
{
$this->timeout = $timeout;
return $this;
}
/**
* Specify the maximum number of seconds a process may go without returning output.
*
* @param int $timeout
* @return $this
*/
public function idleTimeout(int $timeout)
{
$this->idleTimeout = $timeout;
return $this;
}
/**
* Indicate that the process may run forever without timing out.
*
* @return $this
*/
public function forever()
{
$this->timeout = null;
return $this;
}
/**
* Set the additional environment variables for the process.
*
* @param array $environment
* @return $this
*/
public function env(array $environment)
{
$this->environment = $environment;
return $this;
}
/**
* Set the standard input that should be provided when invoking the process.
*
* @param \Traversable|resource|string|int|float|bool|null $input
* @return $this
*/
public function input($input)
{
$this->input = $input;
return $this;
}
/**
* Disable output for the process.
*
* @return $this
*/
public function quietly()
{
$this->quietly = true;
return $this;
}
/**
* Enable TTY mode for the process.
*
* @param bool $tty
* @return $this
*/
public function tty(bool $tty = true)
{
$this->tty = $tty;
return $this;
}
/**
* Set the "proc_open" options that should be used when invoking the process.
*
* @param array $options
* @return $this
*/
public function options(array $options)
{
$this->options = $options;
return $this;
}
/**
* Run the process.
*
* @param array<array-key, string>|string|null $command
* @param callable|null $output
* @return \Illuminate\Contracts\Process\ProcessResult
*
* @throws \Illuminate\Process\Exceptions\ProcessTimedOutException
* @throws \RuntimeException
*/
public function run(array|string $command = null, callable $output = null)
{
$this->command = $command ?: $this->command;
try {
$process = $this->toSymfonyProcess($command);
if ($fake = $this->fakeFor($command = $process->getCommandline())) {
return tap($this->resolveSynchronousFake($command, $fake), function ($result) {
$this->factory->recordIfRecording($this, $result);
});
} elseif ($this->factory->isRecording() && $this->factory->preventingStrayProcesses()) {
throw new RuntimeException('Attempted process ['.$command.'] without a matching fake.');
}
return new ProcessResult(tap($process)->run($output));
} catch (SymfonyTimeoutException $e) {
throw new ProcessTimedOutException($e, new ProcessResult($process));
}
}
/**
* Start the process in the background.
*
* @param array<array-key, string>|string|null $command
* @param callable|null $output
* @return \Illuminate\Process\InvokedProcess
*
* @throws \RuntimeException
*/
public function start(array|string $command = null, callable $output = null)
{
$this->command = $command ?: $this->command;
$process = $this->toSymfonyProcess($command);
if ($fake = $this->fakeFor($command = $process->getCommandline())) {
return tap($this->resolveAsynchronousFake($command, $output, $fake), function (FakeInvokedProcess $process) {
$this->factory->recordIfRecording($this, $process->predictProcessResult());
});
} elseif ($this->factory->isRecording() && $this->factory->preventingStrayProcesses()) {
throw new RuntimeException('Attempted process ['.$command.'] without a matching fake.');
}
return new InvokedProcess(tap($process)->start($output));
}
/**
* Get a Symfony Process instance from the current pending command.
*
* @param array<array-key, string>|string|null $command
* @return \Symfony\Component\Process\Process
*/
protected function toSymfonyProcess(array|string|null $command)
{
$command = $command ?? $this->command;
$process = is_iterable($command)
? new Process($command, null, $this->environment)
: Process::fromShellCommandline((string) $command, null, $this->environment);
$process->setWorkingDirectory((string) ($this->path ?? getcwd()));
$process->setTimeout($this->timeout);
if ($this->idleTimeout) {
$process->setIdleTimeout($this->idleTimeout);
}
if ($this->input) {
$process->setInput($this->input);
}
if ($this->quietly) {
$process->disableOutput();
}
if ($this->tty) {
$process->setTty(true);
}
if (! empty($this->options)) {
$process->setOptions($this->options);
}
return $process;
}
/**
* Specify the fake process result handlers for the pending process.
*
* @param array $fakeHandlers
* @return $this
*/
public function withFakeHandlers(array $fakeHandlers)
{
$this->fakeHandlers = $fakeHandlers;
return $this;
}
/**
* Get the fake handler for the given command, if applicable.
*
* @param string $command
* @return \Closure|null
*/
protected function fakeFor(string $command)
{
return collect($this->fakeHandlers)
->first(fn ($handler, $pattern) => $pattern === '*' || Str::is($pattern, $command));
}
/**
* Resolve the given fake handler for a synchronous process.
*
* @param string $command
* @param \Closure $fake
* @return mixed
*/
protected function resolveSynchronousFake(string $command, Closure $fake)
{
$result = $fake($this);
if (is_string($result) || is_array($result)) {
return (new FakeProcessResult(output: $result))->withCommand($command);
}
return match (true) {
$result instanceof ProcessResult => $result,
$result instanceof FakeProcessResult => $result->withCommand($command),
$result instanceof FakeProcessDescription => $result->toProcessResult($command),
$result instanceof FakeProcessSequence => $this->resolveSynchronousFake($command, fn () => $result()),
default => throw new LogicException('Unsupported synchronous process fake result provided.'),
};
}
/**
* Resolve the given fake handler for an asynchronous process.
*
* @param string $command
* @param callable|null $output
* @param \Closure $fake
* @return \Illuminate\Process\FakeInvokedProcess
*
* @throw \LogicException
*/
protected function resolveAsynchronousFake(string $command, ?callable $output, Closure $fake)
{
$result = $fake($this);
if (is_string($result) || is_array($result)) {
$result = new FakeProcessResult(output: $result);
}
if ($result instanceof ProcessResult) {
return (new FakeInvokedProcess(
$command,
(new FakeProcessDescription)
->replaceOutput($result->output())
->replaceErrorOutput($result->errorOutput())
->runsFor(iterations: 0)
->exitCode($result->exitCode())
))->withOutputHandler($output);
} elseif ($result instanceof FakeProcessResult) {
return (new FakeInvokedProcess(
$command,
(new FakeProcessDescription)
->replaceOutput($result->output())
->replaceErrorOutput($result->errorOutput())
->runsFor(iterations: 0)
->exitCode($result->exitCode())
))->withOutputHandler($output);
} elseif ($result instanceof FakeProcessDescription) {
return (new FakeInvokedProcess($command, $result))->withOutputHandler($output);
} elseif ($result instanceof FakeProcessSequence) {
return $this->resolveAsynchronousFake($command, $output, fn () => $result());
}
throw new LogicException('Unsupported asynchronous process fake result provided.');
}
}

View File

@ -0,0 +1,102 @@
<?php
namespace Illuminate\Process;
use InvalidArgumentException;
/**
* @mixin \Illuminate\Process\Factory
* @mixin \Illuminate\Process\PendingProcess
*/
class Pipe
{
/**
* The process factory instance.
*
* @var \Illuminate\Process\Factory
*/
protected $factory;
/**
* The callback that resolves the pending processes.
*
* @var callable
*/
protected $callback;
/**
* The array of pending processes.
*
* @var array
*/
protected $pendingProcesses = [];
/**
* Create a new series of piped processes.
*
* @param \Illuminate\Process\Factory $factory
* @param callable $callback
* @return void
*/
public function __construct(Factory $factory, callable $callback)
{
$this->factory = $factory;
$this->callback = $callback;
}
/**
* Add a process to the pipe with a key.
*
* @param string $key
* @return \Illuminate\Process\PendingProcess
*/
public function as(string $key)
{
return tap($this->factory->newPendingProcess(), function ($pendingProcess) use ($key) {
$this->pendingProcesses[$key] = $pendingProcess;
});
}
/**
* Runs the processes in the pipe.
*
* @param callable|null $output
* @return \Illuminate\Contracts\Process\ProcessResult
*/
public function run(?callable $output = null)
{
call_user_func($this->callback, $this);
return collect($this->pendingProcesses)
->reduce(function ($previousProcessResult, $pendingProcess, $key) use ($output) {
if (! $pendingProcess instanceof PendingProcess) {
throw new InvalidArgumentException('Process pipe must only contain pending processes.');
}
if ($previousProcessResult && $previousProcessResult->failed()) {
return $previousProcessResult;
}
return $pendingProcess->when(
$previousProcessResult,
fn () => $pendingProcess->input($previousProcessResult->output())
)->run(output: $output ? function ($type, $buffer) use ($key, $output) {
$output($type, $buffer, $key);
} : null);
});
}
/**
* Dynamically proxy methods calls to a new pending process.
*
* @param string $method
* @param array $parameters
* @return \Illuminate\Process\PendingProcess
*/
public function __call($method, $parameters)
{
return tap($this->factory->{$method}(...$parameters), function ($pendingProcess) {
$this->pendingProcesses[] = $pendingProcess;
});
}
}

View File

@ -0,0 +1,118 @@
<?php
namespace Illuminate\Process;
use InvalidArgumentException;
/**
* @mixin \Illuminate\Process\Factory
* @mixin \Illuminate\Process\PendingProcess
*/
class Pool
{
/**
* The process factory instance.
*
* @var \Illuminate\Process\Factory
*/
protected $factory;
/**
* The callback that resolves the pending processes.
*
* @var callable
*/
protected $callback;
/**
* The array of pending processes.
*
* @var array
*/
protected $pendingProcesses = [];
/**
* Create a new process pool.
*
* @param \Illuminate\Process\Factory $factory
* @param callable $callback
* @return void
*/
public function __construct(Factory $factory, callable $callback)
{
$this->factory = $factory;
$this->callback = $callback;
}
/**
* Add a process to the pool with a key.
*
* @param string $key
* @return \Illuminate\Process\PendingProcess
*/
public function as(string $key)
{
return tap($this->factory->newPendingProcess(), function ($pendingProcess) use ($key) {
$this->pendingProcesses[$key] = $pendingProcess;
});
}
/**
* Start all of the processes in the pool.
*
* @param callable|null $output
* @return \Illuminate\Process\InvokedProcessPool
*/
public function start(?callable $output = null)
{
call_user_func($this->callback, $this);
return new InvokedProcessPool(
collect($this->pendingProcesses)
->each(function ($pendingProcess) {
if (! $pendingProcess instanceof PendingProcess) {
throw new InvalidArgumentException('Process pool must only contain pending processes.');
}
})->mapWithKeys(function ($pendingProcess, $key) use ($output) {
return [$key => $pendingProcess->start(output: $output ? function ($type, $buffer) use ($key, $output) {
$output($type, $buffer, $key);
} : null)];
})
->all()
);
}
/**
* Start and wait for the processes to finish.
*
* @return \Illuminate\Process\ProcessPoolResults
*/
public function run()
{
return $this->wait();
}
/**
* Start and wait for the processes to finish.
*
* @return \Illuminate\Process\ProcessPoolResults
*/
public function wait()
{
return $this->start()->wait();
}
/**
* Dynamically proxy methods calls to a new pending process.
*
* @param string $method
* @param array $parameters
* @return \Illuminate\Process\PendingProcess
*/
public function __call($method, $parameters)
{
return tap($this->factory->{$method}(...$parameters), function ($pendingProcess) {
$this->pendingProcesses[] = $pendingProcess;
});
}
}

View File

@ -0,0 +1,82 @@
<?php
namespace Illuminate\Process;
use ArrayAccess;
use Illuminate\Support\Collection;
class ProcessPoolResults implements ArrayAccess
{
/**
* The results of the processes.
*
* @var array
*/
protected $results = [];
/**
* Create a new process pool result set.
*
* @param array $results
* @return void
*/
public function __construct(array $results)
{
$this->results = $results;
}
/**
* Get the results as a collection.
*
* @return \Illuminate\Support\Collection
*/
public function collect()
{
return new Collection($this->results);
}
/**
* Determine if the given array offset exists.
*
* @param int $offset
* @return bool
*/
public function offsetExists($offset): bool
{
return isset($this->results[$offset]);
}
/**
* Get the result at the given offset.
*
* @param int $offset
* @return mixed
*/
public function offsetGet($offset): mixed
{
return $this->results[$offset];
}
/**
* Set the result at the given offset.
*
* @param int $offset
* @param mixed $value
* @return void
*/
public function offsetSet($offset, $value): void
{
$this->results[$offset] = $value;
}
/**
* Unset the result at the given offset.
*
* @param int $offset
* @return void
*/
public function offsetUnset($offset): void
{
unset($this->results[$offset]);
}
}

View File

@ -0,0 +1,151 @@
<?php
namespace Illuminate\Process;
use Illuminate\Contracts\Process\ProcessResult as ProcessResultContract;
use Illuminate\Process\Exceptions\ProcessFailedException;
use Symfony\Component\Process\Process;
class ProcessResult implements ProcessResultContract
{
/**
* The underlying process instance.
*
* @var \Symfony\Component\Process\Process
*/
protected $process;
/**
* Create a new process result instance.
*
* @param \Symfony\Component\Process\Process $process
* @return void
*/
public function __construct(Process $process)
{
$this->process = $process;
}
/**
* Get the original command executed by the process.
*
* @return string
*/
public function command()
{
return $this->process->getCommandLine();
}
/**
* Determine if the process was successful.
*
* @return bool
*/
public function successful()
{
return $this->process->isSuccessful();
}
/**
* Determine if the process failed.
*
* @return bool
*/
public function failed()
{
return ! $this->successful();
}
/**
* Get the exit code of the process.
*
* @return int|null
*/
public function exitCode()
{
return $this->process->getExitCode();
}
/**
* Get the standard output of the process.
*
* @return string
*/
public function output()
{
return $this->process->getOutput();
}
/**
* Determine if the output contains the given string.
*
* @param string $output
* @return bool
*/
public function seeInOutput(string $output)
{
return str_contains($this->output(), $output);
}
/**
* Get the error output of the process.
*
* @return string
*/
public function errorOutput()
{
return $this->process->getErrorOutput();
}
/**
* Determine if the error output contains the given string.
*
* @param string $output
* @return bool
*/
public function seeInErrorOutput(string $output)
{
return str_contains($this->errorOutput(), $output);
}
/**
* Throw an exception if the process failed.
*
* @param callable|null $callback
* @return $this
*
* @throws \Illuminate\Process\Exceptions\ProcessFailedException
*/
public function throw(callable $callback = null)
{
if ($this->successful()) {
return $this;
}
$exception = new ProcessFailedException($this);
if ($callback) {
$callback($this, $exception);
}
throw $exception;
}
/**
* Throw an exception if the process failed and the given condition is true.
*
* @param bool $condition
* @param callable|null $callback
* @return $this
*
* @throws \Throwable
*/
public function throwIf(bool $condition, callable $callback = null)
{
if ($condition) {
return $this->throw($callback);
}
return $this;
}
}

View File

@ -0,0 +1,38 @@
{
"name": "illuminate/process",
"description": "The Illuminate Process package.",
"license": "MIT",
"homepage": "https://laravel.com",
"support": {
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"require": {
"php": "^8.1",
"illuminate/collections": "^10.0",
"illuminate/contracts": "^10.0",
"illuminate/macroable": "^10.0",
"illuminate/support": "^10.0",
"symfony/process": "^6.2"
},
"autoload": {
"psr-4": {
"Illuminate\\Process\\": ""
}
},
"extra": {
"branch-alias": {
"dev-master": "10.x-dev"
}
},
"config": {
"sort-packages": true
},
"minimum-stability": "dev"
}