进程管理
进程管理
简介
Laravel 基于 Symfony Process 组件提供了一套简洁而富有表现力的 API,让你能够方便地从 Laravel 应用中调用外部进程。Laravel 的进程管理功能专注于最常见的用例,并提供出色的开发者体验。
调用进程
要调用一个进程,你可以使用 Process 门面提供的 run 和 start 方法。run 方法会调用进程并等待其执行完成,而 start 方法用于异步执行进程。本文将介绍这两种方式。首先,让我们看看如何调用一个基本的同步进程并检查其结果:
use Illuminate\Support\Facades\Process;
$result = Process::run('ls -la');
return $result->output();
当然,run 方法返回的 Illuminate\Contracts\Process\ProcessResult 实例提供了多种有用的方法来检查进程结果:
$result = Process::run('ls -la');
$result->command();
$result->successful();
$result->failed();
$result->output();
$result->errorOutput();
$result->exitCode();
抛出异常
拿到进程结果后,如果你希望在退出码大于零(表示执行失败)时抛出 Illuminate\Process\Exceptions\ProcessFailedException 异常,可以使用 throw 和 throwIf 方法。如果进程没有失败,则会返回 ProcessResult 实例:
$result = Process::run('ls -la')->throw();
$result = Process::run('ls -la')->throwIf($condition);
进程选项
当然,在调用进程之前,你可能需要自定义进程的行为。Laravel 允许你调整各种进程选项,如工作目录、超时和环境变量。
工作目录路径
你可以使用 path 方法指定进程的工作目录。如果不调用此方法,进程将继承当前执行的 PHP 脚本的工作目录:
$result = Process::path(__DIR__)->run('ls -la');
输入
你可以使用 input 方法通过进程的「标准输入」提供输入内容:
$result = Process::input('Hello World')->run('cat');
超时
默认情况下,进程执行超过 60 秒后会抛出 Illuminate\Process\Exceptions\ProcessTimedOutException 异常。不过,你可以通过 timeout 方法自定义超时时间:
$result = Process::timeout(120)->run('bash import.sh');
timeout 和 idleTimeout 方法也接受 CarbonInterval 实例:
use function Illuminate\Support\minutes;
$result = Process::timeout(minutes(2))->run('bash import.sh');
如果你希望完全禁用进程的超时限制,可以调用 forever 方法:
$result = Process::forever()->run('bash import.sh');
idleTimeout 方法可用于指定进程在未产生任何输出时的最长运行秒数:
$result = Process::timeout(60)->idleTimeout(30)->run('bash import.sh');
环境变量
你可以通过 env 方法为进程提供环境变量。调用的进程也会继承系统中定义的所有环境变量:
$result = Process::forever()
->env(['IMPORT_PATH' => __DIR__])
->run('bash import.sh');
如果你希望从调用的进程中移除某个继承的环境变量,可以将该环境变量的值设置为 false:
$result = Process::forever()
->env(['LOAD_PATH' => false])
->run('bash import.sh');
TTY 模式
tty 方法可用于为进程启用 TTY 模式。TTY 模式会将进程的输入输出连接到你程序的输入输出,从而允许进程打开 Vim 或 Nano 这样的编辑器:
Process::forever()->tty()->run('vim');
[!WARNING]
Windows 不支持 TTY 模式。
进程输出
如前所述,你可以通过进程结果的 output(标准输出)和 errorOutput(标准错误输出)方法获取进程输出:
use Illuminate\Support\Facades\Process;
$result = Process::run('ls -la');
echo $result->output();
echo $result->errorOutput();
此外,你还可以将闭包作为第二个参数传递给 run 方法,以实时收集输出。闭包会接收两个参数:输出类型(stdout 或 stderr)和输出字符串本身:
$result = Process::run('ls -la', function (string $type, string $output) {
echo $output;
});
Laravel 还提供了 seeInOutput 和 seeInErrorOutput 方法,可以方便地判断进程输出中是否包含指定字符串:
if (Process::run('ls -la')->seeInOutput('laravel')) {
// ...
}
禁用进程输出
如果进程会产生大量你不关心的输出,可以完全禁用输出获取以节省内存。为此,在构建进程时调用 quietly 方法:
use Illuminate\Support\Facades\Process;
$result = Process::quietly()->run('bash import.sh');
管道
有时你可能希望将一个进程的输出作为另一个进程的输入,这通常称为通过「管道」传递进程输出。Process 门面提供的 pipe 方法可以轻松实现这一点。pipe 方法会同步执行管道中的各个进程,并返回管道中最后一个进程的结果:
use Illuminate\Process\Pipe;
use Illuminate\Support\Facades\Process;
$result = Process::pipe(function (Pipe $pipe) {
$pipe->command('cat example.txt');
$pipe->command('grep -i "laravel"');
});
if ($result->successful()) {
// ...
}
如果不需要自定义管道中各个进程的配置,你可以直接向 pipe 方法传递命令字符串数组:
$result = Process::pipe([
'cat example.txt',
'grep -i "laravel"',
]);
将闭包作为第二个参数传递给 pipe 方法,可以实时收集进程输出。闭包会接收两个参数:输出类型(stdout 或 stderr)和输出字符串本身:
$result = Process::pipe(function (Pipe $pipe) {
$pipe->command('cat example.txt');
$pipe->command('grep -i "laravel"');
}, function (string $type, string $output) {
echo $output;
});
Laravel 还允许你通过 as 方法为管道中的每个进程分配字符串键。这个键也会传递给 pipe 方法的输出闭包,从而让你确定输出属于哪个进程:
$result = Process::pipe(function (Pipe $pipe) {
$pipe->as('first')->command('cat example.txt');
$pipe->as('second')->command('grep -i "laravel"');
}, function (string $type, string $output, string $key) {
// ...
});
异步进程
run 方法会同步调用进程,而 start 方法可用于异步调用进程。这样,应用就能在进程后台运行时继续执行其他任务。调用进程后,你可以使用 running 方法判断进程是否仍在运行:
$process = Process::timeout(120)->start('bash import.sh');
while ($process->running()) {
// ...
}
$result = $process->wait();
如上例所示,你可以调用 wait 方法等待进程执行完成,并获取 ProcessResult 实例:
$process = Process::timeout(120)->start('bash import.sh');
// ...
$result = $process->wait();
进程 ID 与信号
id 方法可用于获取操作系统为运行中的进程分配的进程 ID:
$process = Process::start('bash import.sh');
return $process->id();
你可以使用 signal 方法向运行中的进程发送「信号」。预定义的信号常量列表可在 PHP 文档中找到:
$process->signal(SIGUSR2);
异步进程输出
当异步进程运行时,你可以使用 output 和 errorOutput 方法获取进程当前的全部输出;如果只想获取自上次读取以来产生的新输出,可以使用 latestOutput 和 latestErrorOutput 方法:
$process = Process::timeout(120)->start('bash import.sh');
while ($process->running()) {
echo $process->latestOutput();
echo $process->latestErrorOutput();
sleep(1);
}
与 run 方法一样,你也可以将闭包作为第二个参数传递给 start 方法,以实时收集异步进程的输出。闭包会接收两个参数:输出类型(stdout 或 stderr)和输出字符串本身:
$process = Process::start('bash import.sh', function (string $type, string $output) {
echo $output;
});
$result = $process->wait();
你可以使用 waitUntil 方法根据进程输出停止等待,而不必一直等到进程执行完成。当传递给 waitUntil 方法的闭包返回 true 时,Laravel 就会停止等待进程完成:
$process = Process::start('bash import.sh');
$process->waitUntil(function (string $type, string $output) {
return $output === 'Ready...';
});
异步进程超时
在异步进程运行期间,你可以使用 ensureNotTimedOut 方法检查进程是否已超时。如果进程超时,该方法会抛出超时异常:
$process = Process::timeout(120)->start('bash import.sh');
while ($process->running()) {
$process->ensureNotTimedOut();
// ...
sleep(1);
}
并发进程
Laravel 还可以轻松管理一组并发的异步进程,让你同时执行多个任务。要开始使用,请调用 pool 方法,该方法接受一个闭包,并将 Illuminate\Process\Pool 实例传入闭包中。
在闭包中,你可以定义属于该进程池的进程。通过 start 方法启动进程池后,你可以使用 running 方法获取正在运行的进程集合:
use Illuminate\Process\Pool;
use Illuminate\Support\Facades\Process;
$pool = Process::pool(function (Pool $pool) {
$pool->path(__DIR__)->command('bash import-1.sh');
$pool->path(__DIR__)->command('bash import-2.sh');
$pool->path(__DIR__)->command('bash import-3.sh');
})->start(function (string $type, string $output, int $key) {
// ...
});
while ($pool->running()->isNotEmpty()) {
// ...
}
$results = $pool->wait();
如你所见,你可以通过 wait 方法等待进程池中的所有进程执行完成并获取它们的结果。wait 方法返回一个支持数组访问的对象,让你能够通过键访问进程池中每个进程的 ProcessResult 实例:
$results = $pool->wait();
echo $results[0]->output();
为了更方便地使用,你也可以通过 concurrently 方法启动一个异步进程池,并立即等待其结果。结合 PHP 的数组解构功能,这种写法会更加简洁直观:
[$first, $second, $third] = Process::concurrently(function (Pool $pool) {
$pool->path(__DIR__)->command('ls -la');
$pool->path(app_path())->command('ls -la');
$pool->path(storage_path())->command('ls -la');
});
echo $first->output();
为进程池中的进程命名
通过数字键访问进程池的结果不够直观,因此 Laravel 允许你通过 as 方法为池中的每个进程分配字符串键。这个键也会传递给 start 方法的闭包,从而让你确定输出属于哪个进程:
$pool = Process::pool(function (Pool $pool) {
$pool->as('first')->command('bash import-1.sh');
$pool->as('second')->command('bash import-2.sh');
$pool->as('third')->command('bash import-3.sh');
})->start(function (string $type, string $output, string $key) {
// ...
});
$results = $pool->wait();
return $results['first']->output();
进程池中的进程 ID 与信号
进程池的 running 方法会返回池中所有已调用进程的集合,因此你可以轻松获取这些底层进程的 ID:
$processIds = $pool->running()->map->id();
此外,你可以直接在进程池上调用 signal 方法,向池中的每个进程发送信号:
$pool->signal(SIGUSR2);
测试
许多 Laravel 服务都提供了便捷的功能,让你能够轻松、清晰地编写测试,进程服务也不例外。Process 门面的 fake 方法允许你指示 Laravel 在调用进程时返回桩结果或模拟结果。
模拟进程
为了了解 Laravel 模拟进程的能力,假设有一个调用进程的路由:
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Route;
Route::get('/import', function () {
Process::run('bash import.sh');
return 'Import complete!';
});
测试该路由时,我们可以在 Process 门面上调用 fake 方法,不传递任何参数,以指示 Laravel 为每个调用的进程返回一个模拟的成功结果。此外,我们还可以断言某个进程确实被「运行」过:
<?php
use Illuminate\Contracts\Process\ProcessResult;
use Illuminate\Process\PendingProcess;
use Illuminate\Support\Facades\Process;
test('process is invoked', function () {
Process::fake();
$response = $this->get('/import');
// 简单的进程断言……
Process::assertRan('bash import.sh');
// 或者,检查进程配置……
Process::assertRan(function (PendingProcess $process, ProcessResult $result) {
return $process->command === 'bash import.sh' &&
$process->timeout === 60;
});
});
<?php
namespace Tests\Feature;
use Illuminate\Contracts\Process\ProcessResult;
use Illuminate\Process\PendingProcess;
use Illuminate\Support\Facades\Process;
use Tests\TestCase;
class ExampleTest extends TestCase
{
public function test_process_is_invoked(): void
{
Process::fake();
$response = $this->get('/import');
// 简单的进程断言……
Process::assertRan('bash import.sh');
// 或者,检查进程配置……
Process::assertRan(function (PendingProcess $process, ProcessResult $result) {
return $process->command === 'bash import.sh' &&
$process->timeout === 60;
});
}
}
如前所述,在 Process 门面上调用 fake 方法,会指示 Laravel 始终返回一个无输出的成功进程结果。不过,你可以通过 Process 门面的 result 方法,轻松为模拟进程指定输出和退出码:
Process::fake([
'*' => Process::result(
output: 'Test output',
errorOutput: 'Test error output',
exitCode: 1,
),
]);
模拟特定进程
如前面的示例所示,Process 门面允许你通过向 fake 方法传递数组,为不同的进程指定不同的模拟结果。
数组的键应表示你希望模拟的命令模式,对应的值则是这些命令的模拟结果。* 字符可以用作通配符。任何未被模拟的进程命令都会实际执行。你可以使用 Process 门面的 result 方法为这些命令构建桩结果或模拟结果:
Process::fake([
'cat *' => Process::result(
output: 'Test "cat" output',
),
'ls *' => Process::result(
output: 'Test "ls" output',
),
]);
如果不需要自定义模拟进程的退出码或错误输出,直接用字符串指定模拟进程的结果会更方便:
Process::fake([
'cat *' => 'Test "cat" output',
'ls *' => 'Test "ls" output',
]);
模拟进程序列
如果被测试的代码多次调用使用相同命令的进程,你可能希望为每次调用分配不同的模拟进程结果。可以通过 Process 门面的 sequence 方法实现这一点:
Process::fake([
'ls *' => Process::sequence()
->push(Process::result('First invocation'))
->push(Process::result('Second invocation')),
]);
模拟异步进程生命周期
到目前为止,我们主要讨论了如何模拟通过 run 方法同步调用的进程。如果被测试的代码需要与通过 start 方法调用的异步进程交互,你可能需要更细致地描述模拟进程。
例如,假设有以下与异步进程交互的路由:
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Route;
Route::get('/import', function () {
$process = Process::start('bash import.sh');
while ($process->running()) {
Log::info($process->latestOutput());
Log::info($process->latestErrorOutput());
}
return 'Done';
});
为了正确模拟这个进程,我们需要描述 running 方法应返回 true 的次数。此外,我们可能希望指定多行按顺序返回的输出。可以使用 Process 门面的 describe 方法实现这一点:
Process::fake([
'bash import.sh' => Process::describe()
->output('First line of standard output')
->errorOutput('First line of error output')
->output('Second line of standard output')
->exitCode(0)
->iterations(3),
]);
让我们仔细看看上面的示例。使用 output 和 errorOutput 方法,可以指定多行按顺序返回的输出。exitCode 方法可用于指定模拟进程的最终退出码。最后,iterations 方法可用于指定 running 方法应返回 true 的次数。
可用断言
如前文所述,Laravel 为功能测试提供了多种进程断言。下面将逐一介绍这些断言。
assertRan
断言某个进程已被调用:
use Illuminate\Support\Facades\Process;
Process::assertRan('ls -la');
如果调用进程时使用了参数数组,你可以将相同的数组传递给断言:
Process::assertRan(['php', 'artisan', 'migrate']);
assertRanTimes 和 assertDidntRun 方法也接受数组形式的命令。
assertRan 方法也接受一个闭包,闭包会接收进程实例和进程结果实例,让你能够检查进程的配置选项。如果闭包返回 true,断言就会「通过」:
Process::assertRan(fn ($process, $result) =>
$process->command === 'ls -la' &&
$process->path === __DIR__ &&
$process->timeout === 60
);
传递给 assertRan 闭包的 $process 是 Illuminate\Process\PendingProcess 实例,而 $result 是 Illuminate\Contracts\Process\ProcessResult 实例。
assertDidntRun
断言某个进程未被调用:
use Illuminate\Support\Facades\Process;
Process::assertDidntRun('ls -la');
与 assertRan 方法类似,assertDidntRun 方法也接受一个闭包,闭包会接收进程实例和进程结果实例,让你能够检查进程的配置选项。如果闭包返回 true,断言就会「失败」:
Process::assertDidntRun(fn (PendingProcess $process, ProcessResult $result) =>
$process->command === 'ls -la'
);
assertRanTimes
断言某个进程被调用了指定次数:
use Illuminate\Support\Facades\Process;
Process::assertRanTimes('ls -la', times: 3);
assertRanTimes 方法也接受一个闭包,闭包会接收 PendingProcess 和 ProcessResult 实例,让你能够检查进程的配置选项。如果闭包返回 true,且进程被调用了指定次数,断言就会「通过」:
Process::assertRanTimes(function (PendingProcess $process, ProcessResult $result) {
return $process->command === 'ls -la';
}, times: 3);
assertRanInOrder
断言进程按指定顺序被调用:
Process::assertRanInOrder([
'git fetch',
'composer install',
]);
与其他进程断言一样,assertRanInOrder 方法接受命令字符串、命令参数数组或闭包。
防止未模拟的进程运行
如果你希望确保在单个测试或整个测试套件中,所有调用的进程都已被模拟,可以调用 preventStrayProcesses 方法。调用此方法后,任何没有对应模拟结果的进程都会抛出异常,而不会启动实际进程:
use Illuminate\Support\Facades\Process;
Process::preventStrayProcesses();
Process::fake([
'ls *' => 'Test output...',
]);
// 返回模拟响应……
Process::run('ls -la');
// 抛出异常……
Process::run('bash import.sh');
本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
Laravel 13 中文文档
关于 LearnKu
推荐文章: