Skip to content

Conversation

taylorotwell
Copy link
Member

@taylorotwell taylorotwell commented Dec 14, 2022

This PR continues and expands upon @nunomaduro's work on providing a convenient process layer in Laravel. Some features have been rebuilt and others have been added.

Notably, the ability to describe fake process lifecycles for async process handling / testing as well as process result sequences. In addition, the ability to interleave incoming output from a pool of processes and key them by name. Functionality for preventing stray processes during testing has also been added.

In addition, the object model has been rebuilt.

Usage

Basic API

The most basic usage of this feature looks like the following:

use Illuminate\Support\Facades\Process;

$result = Process::run('ls -la');

$result->successful();
$result->failed();
$result->exitCode();
$result->output();
$result->errorOutput();
$result->throw();
$result->throwIf(condition);

Building Processes

A variety of options can be set on the process before actually invoking it, including setting the timeout, idle timeout, TTY support, and environment variables:

$result = Process::timeout(60)->path(base_path())->env([...])->run('ls -la');

$result = Process::forever()->run('ls -la');

Process Output

A callback may be passed to the run function to gather the process output as it is received. The signature on this callback is the same as the underlying Symfony Process callbacks you may be used to:

$result = Process::run('ls -la', function ($type, $buffer) {
    echo $buffer;
});

Asynchronous Processes

The start method may be used to start an asynchronous process. Calling this method returns an implementation of InvokedProcess, allowing you to perform other tasks while the external process is running. The wait method may be used to resolve the invoked process into a process result, waiting on the process to finish executing if necessary:

$process = Process::forever()->start('ls -la', function ($type, $buffer) {
     ...
});

while ($process->running()) {
    echo $process->pid();
    echo $process->signal(...);
    echo $process->output();
    echo $process->errorOutput();
    echo $process->latestOutput();
    echo $process->latestErrorOutput();
}

$result = $process->wait();

echo $result->output();

Process Pools

Process pools allow you to manage and interact with a pool of processes at once, then access their results by key. A callback may be passed to the start method of the pool to capture output from all processes by key (the third argument given to the closure). The start method returns an instance of InvokedProcessPool, allowing you to interact with the pool via various methods. Calling the wait method on the invoked pool will wait until all processes in the pool have finished running and run an instance of ProcessPoolResults, which may be accessed like an array:

$pool = Process::pool(function (Pool $pool) {
    $pool->path(base_path())->command('ls -la');
    $pool->path(base_path())->command('ls -la');
    $pool->path(base_path())->command('ls -la');
})->start(function ($type, $buffer, $key) {
    dump($type, $buffer, $key);
});

// Get a collection of all of the running processes...
echo $pool->running()->count();

// Send a signal to every running process in the pool...
$pool->signal(SIGUSR2);

$results = $pool->wait();

echo $results[0]->output();

For convenience, process pool processes may also be assigned string keys, just like HTTP request pools:

$pool = Process::pool(function (Pool $pool) {
    $pool->as('first')->path(base_path())->command('ls -la');
    $pool->as('second')->path(base_path())->command('ls -la');
})->start(function ($type, $buffer, $key) {
    dump($type, $buffer, $key);
});

$results = $pool->wait();

echo $results['first']->output();

In addition, for convenience, a concurrently method is provided which is the equivalent to calling start and wait on the process pool to resolve the results synchronously. In this example, we are using array destructuring to assign the pool process results to individual variables:

[$first, $second, $third] = Process::concurrently(function (Pool $pool) {
    $pool->path(base_path())->command('ls -la');
    $pool->path(base_path())->command('ls -la');
    $pool->path(base_path())->command('ls -la');
});

echo $first->output();

Testing

Testing is similar to @nunomaduro's API, with the following basic API:

Process::fake([
    'ls *' => Process::result('Hello World'),
]);

$result = Process::run('ls -la');

Process::assertRan(function ($process, $result) {
    return $process->command == 'ls -la';
});

Process::assertRanTimes(function ($process, $result) {
    return $process->command == 'ls -la';
}, times: 1);

Process::assertNotRan(function ($process, $result) {
    return $process->command == 'cat foo';
});

Fake process results can be defined in a variety of ways, from calling Process::result to just passing simple strings and arrays:

Process::fake(['ls -la' => 'Hello World']);

Process::fake(['ls -la' => ['Line 1', 'Line 2']]);

Process Sequences

Like the HTTP fake's functionality, you may also define sequences of results for commands that are invoked multiple times in a request:

Process::fake([
    'ls *' => Process::sequence()
                       ->push(Process::result('first'))
                       ->push(Process::result('second')),
]);

$first = Process::run('ls -la');
$second = Process::run('ls -la');

Async Process Lifecycles

Finally, you may also describe async process lifecycles in order to test code that starts processes asynchronously and then interacts with them while running. In this example, running will return true three times as we specified by calling iterations(3) when describing our process. In addition, we define several lines of standard output as well as some error output:

Process::fake([
    '*' => Process::describe()
                   ->output('First line of output')
                   ->output('Next line of output')
                   ->errorOutput('Next line of error output')
                   ->exitCode(0)
                   ->iterations(3),
]);

$process = Process::start('test-async-process');

while ($process->running()) {
    echo $process->latestOutput();
}

echo $process->wait()->output();

Preventing Stray Processes

By default, if Process::fake has been called and Laravel isn't able to find a matching fake definition for a given process, the process will actually execute. This behavior is consistent with the HTTP facade's fake functionality. You can prevent this behavior by invoking the preventStrayProcesses method. This will cause Laravel to throw an exception when attempting to invoke a process that does not have a corresponding fake definition:

Process::preventStrayProcesses();

Process::fake(['cat *' => Process::result('Hello World')]);

$result = Process::run('ls -la');

@geowrgetudor
Copy link

Outstanding work! What do you think about Process::remote($ip, $port)->run(…)? I know this can be achieved with the code presented above, but it can be pretty useful helper function to run commands via ssh.

@taylorotwell
Copy link
Member Author

@geowrgetudor not sure I want to go down that road - would require supporting all sorts of SSH options, etc.

@taylorotwell
Copy link
Member Author

Leaving thought to myself... if I have the same process but want to turn it into a pool without repeating myself, it could be nice to do something like:

$pool = Process::timeout(60)->pool(10)->run('some-command');

$pool->start();

@driesvints driesvints changed the title Process DX Layer [9.x] Process DX Layer Jan 5, 2023
@gofish543
Copy link

What's the reason for removing the functions like beforeCallback($closure) and afterCallback($closure) present in the previous iteration of the Process module in Laravel? I'm actively using the previous branch / this branch in a upcoming project and was a fan of the before and after callback hooks.

@driesvints driesvints changed the base branch from 9.x to 10.x January 16, 2023 15:00
@driesvints driesvints changed the title [9.x] Process DX Layer [10.x] Process DX Layer Jan 16, 2023
@taylorotwell taylorotwell merged commit 099ff00 into 10.x Jan 19, 2023
@taylorotwell taylorotwell deleted the process branch January 19, 2023 19:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants