react / child-process
ReactPHP 驱动的执行子进程的库。
Requires
- php: >=5.3.0
- evenement/evenement: ^3.0 || ^2.0 || ^1.0
- react/event-loop: ^1.2
- react/stream: ^1.2
Requires (Dev)
- phpunit/phpunit: ^9.3 || ^5.7 || ^4.8.35
- react/socket: ^1.8
- sebastian/environment: ^5.0 || ^3.0 || ^2.0 || ^1.0
README
ReactPHP 驱动的执行子进程的库。
开发版本: 此分支包含即将发布的 0.7 版本的代码。要查看当前稳定版 0.6.x 的代码,请查看
0.6.x
分支。即将发布的 0.7 版本将是此包的发展方向。然而,我们仍将积极支持 0.6.x 版本,以支持尚未升级到最新版本的用户。有关更多详细信息,请参阅安装说明。
此库将程序执行与EventLoop集成。启动的子进程可以发出信号,并在终止时发出 exit
事件。此外,进程 I/O 流(即 STDIN、STDOUT、STDERR)被公开为流。
目录
快速入门示例
$process = new React\ChildProcess\Process('echo foo'); $process->start(); $process->stdout->on('data', function ($chunk) { echo $chunk; }); $process->on('exit', function($exitCode, $termSignal) { echo 'Process exited with code ' . $exitCode . PHP_EOL; });
请参阅示例。
进程
流属性
一旦启动进程,其 I/O 流将作为 React\Stream\ReadableStreamInterface
和 React\Stream\WritableStreamInterface
的实例构建。在调用 start()
之前,这些属性尚未设置。一旦进程终止,流将关闭但不会取消设置。
遵循常见的 Unix 习惯,此库将默认以以下方式启动每个子进程,其中三个管道与标准 I/O 流匹配。您可以使用命名引用用于常见用例,或者将这些作为包含所有三个管道的数组访问。
$stdin
或$pipes[0]
是一个WritableStreamInterface
$stdout
或$pipes[1]
是一个ReadableStreamInterface
$stderr
或$pipes[2]
是一个ReadableStreamInterface
请注意,此默认配置可以通过明确传递自定义管道来覆盖,在这种情况下,它们可能未设置或分配不同的值。特别是请注意,Windows 支持有限,因为它不支持非阻塞 STDIO 管道。该 $pipes
数组将始终包含对已配置的所有管道的引用,而标准 I/O 引用将始终设置为引用与上述约定匹配的管道。有关更多详细信息,请参阅自定义管道。
因为这些都实现了底层的 ReadableStreamInterface
或 WritableStreamInterface
,您可以使用它们的任何事件和方法,就像通常一样。
$process = new Process($command); $process->start(); $process->stdout->on('data', function ($chunk) { echo $chunk; }); $process->stdout->on('end', function () { echo 'ended'; }); $process->stdout->on('error', function (Exception $e) { echo 'error: ' . $e->getMessage(); }); $process->stdout->on('close', function () { echo 'closed'; }); $process->stdin->write($data); $process->stdin->end($data = null); // …
有关更多详细信息,请参阅ReadableStreamInterface
和 WritableStreamInterface
。
命令
Process
类允许您传递任何类型的命令行字符串。
$process = new Process('echo test'); $process->start();
命令行字符串通常由空格分隔的主可执行文件bin和任意数量的参数列表组成。特别需要注意的是,在传递用户输入时,应对任何参数进行转义或引号处理。同样,尤其是在Windows上,含有空格和其他特殊字符的路径名相当常见。如果您想以这种方式运行二进制文件,您必须确保使用 escapeshellarg()
将其作为单个参数进行引号处理,如下所示:
$bin = 'C:\\Program files (x86)\\PHP\\php.exe'; $file = 'C:\\Users\\me\\Desktop\\Application\\main.php'; $process = new Process(escapeshellarg($bin) . ' ' . escapeshellarg($file)); $process->start();
默认情况下,PHP将使用Unix上的sh
命令包装给定的命令行字符串来启动进程,因此第一个示例实际上在Unix下会执行sh -c echo test
。在Windows上,它不会通过在shell中包装来启动进程。
这是一个非常有用的特性,因为它不仅允许您传递单个命令,实际上还允许您传递任何类型的shell命令行,并通过命令链(使用&&
、||
、;
等)启动多个子命令,并允许您重定向STDIO流(使用2>&1
等)。这可以用来传递完整的命令行,并从包装的shell命令接收结果STDIO流,如下所示:
$process = new Process('echo run && demo || echo failed'); $process->start();
请注意,Windows支持有限,因为它根本不支持STDIO流,并且默认情况下进程不会在包装的shell中运行。如果您想运行shell内置函数,如
echo hello
或sleep 10
,可能需要在命令行前明确指定shell,如cmd /c echo hello
。
换句话说,底层shell负责管理此命令行,启动单个子命令,并按适当的方式连接它们的STDIO流。这意味着Process
类只会从包装的shell接收结果的STDIO流,因此将包含完整输入/输出,没有方法可以区分单个子命令的输入/输出。
如果您想区分单个子命令的输出,您可能需要实现一些高级协议逻辑,例如在每个子命令之间打印一个显式的边界,如下所示:
$process = new Process('cat first && echo --- && cat second'); $process->start();
作为替代方案,可以考虑一次启动一个进程,并监听其exit
事件来有条件地启动链中的下一个进程。这将为您提供一个配置后续进程I/O流的机会。
$first = new Process('cat first'); $first->start(); $first->on('exit', function () { $second = new Process('cat second'); $second->start(); });
请注意,PHP在Unix上为所有命令行使用shell包装器。虽然这看起来对于更复杂的命令行似乎是合理的,但实际上这也适用于运行最简单的单个命令。
$process = new Process('yes'); $process->start();
实际上,这将在Unix上产生类似于以下命令的命令层次结构:
5480 … \_ php example.php
5481 … \_ sh -c yes
5482 … \_ yes
这意味着尝试获取底层进程PID或发送信号实际上会针对包装的shell,这在许多情况下可能不是期望的结果。
如果您不想显示此包装shell进程,可以在Unix平台上简单地在命令字符串前加上exec
,这将导致包装shell进程被我们的进程替换。
$process = new Process('exec yes'); $process->start();
这将显示类似于以下结果的命令层次结构:
5480 … \_ php example.php
5481 … \_ yes
这意味着尝试获取底层进程PID和发送信号现在将针对实际命令,如预期的那样。
请注意,在这种情况下,命令行不会在包装的shell中运行。这意味着当使用exec
时,无法传递包含命令链或重定向STDIO流的命令行。
一般来说,大多数命令在包装shell的情况下都能正常运行。如果您传递完整的命令行(或不确定),您很可能会保留包装shell。如果您在Unix上运行,并且只想传递单个命令,您可能需要考虑在命令字符串前加上exec
以避免包装shell。
终止
当进程不再运行时,将会触发exit
事件。事件监听器将接收到退出代码和终止信号作为两个参数。
$process = new Process('sleep 10'); $process->start(); $process->on('exit', function ($code, $term) { if ($term === null) { echo 'exit with code ' . $code . PHP_EOL; } else { echo 'terminated with signal ' . $term . PHP_EOL; } });
请注意,如果进程已终止,但无法确定退出代码,则$code
为null
。同样,除非进程因收到未捕获的信号而终止,否则$term
为null
。这并不是本项目的一个限制,而是POSIX系统上实际暴露退出代码和信号的方式。更多详细信息,请参阅这里。
值得注意的是,进程终止取决于所有文件描述符事先已关闭。这意味着所有进程管道将在exit
事件之前发出一个close
事件,并且exit
事件之后不再有更多的data
事件到达。因此,如果这些管道中的任何一个处于暂停状态(pause()
方法或由于内部的pipe()
调用),这种检测可能不会触发。
可以使用terminate(?int $signal = null): bool
方法向进程发送信号(默认为SIGTERM)。根据您向进程发送的信号以及它是否注册了信号处理程序,这可以用来仅向进程发送信号,甚至强制终止它。
$process->terminate(SIGUSR1);
如果您想强制终止进程,请记住上面的内容。如果您的进程创建了子进程或隐式使用了上面提到的包装shell,其文件描述符可能被继承到子进程中,并且终止主进程并不一定会终止整个进程树。强烈建议在终止进程时明确地close()
所有进程管道。
$process = new Process('sleep 10'); $process->start(); Loop::addTimer(2.0, function () use ($process) { foreach ($process->pipes as $pipe) { $pipe->close(); } $process->terminate(); });
对于许多简单的程序,通过在命令行前加上exec
来避免包装shell及其继承的进程管道,也可以避免上述看似复杂的步骤,如上面所述。
$process = new Process('exec sleep 10'); $process->start(); Loop::addTimer(2.0, function () use ($process) { $process->terminate(); });
许多命令行程序也会在STDIN
上等待数据,并在管道关闭时干净地终止。例如,以下可以用来“软关闭”一个cat
进程:
$process = new Process('cat'); $process->start(); Loop::addTimer(2.0, function () use ($process) { $process->stdin->end(); });
虽然进程管道和终止对于初学者来说可能看起来很复杂,但上述属性实际上允许对进程终止进行一些细粒度的控制,例如首先尝试软关闭,然后在超时后应用强制关闭。
自定义管道
遵循常见的Unix约定,此库将默认以与标准I/O流匹配的三个管道启动每个子进程。对于更高级的使用情况,可能需要传入自定义管道,例如显式传递额外的文件描述符(FDs)或覆盖默认进程管道。
请注意,传递自定义管道被认为是高级用法,需要更深入地了解Unix文件描述符以及它们如何在子进程中继承和如何在多进程应用程序中共享。
如果您不想使用默认的标准I/O管道,可以像这样显式传递一个包含文件描述符规范的数组给构造函数:
$fds = array( // standard I/O pipes for stdin/stdout/stderr 0 => array('pipe', 'r'), 1 => array('pipe', 'w'), 2 => array('pipe', 'w'), // example FDs for files or open resources 4 => array('file', '/dev/null', 'r'), 6 => fopen('log.txt','a'), 8 => STDERR, // example FDs for sockets 10 => fsockopen('localhost', 8080), 12 => stream_socket_server('tcp://0.0.0.0:4711') ); $process = new Process($cmd, null, null, $fds); $process->start();
除非您的使用情况有特殊要求,否则强烈建议(至少)传入上面给出的标准I/O管道。文件描述符规范接受与底层proc_open()
函数相同的格式。
一旦开始进程,$pipes
数组将始终包含对已配置所有管道的引用,而标准输入/输出引用将始终设置为引用符合Unix常规的管道。此库支持任何数量的管道和附加文件描述符,但许多作为子进程运行的应用程序通常会期望父进程正确分配这些文件描述符。
Windows 兼容性
由于平台限制,此库在Windows上提供对创建子进程的有限支持。特别是,PHP不允许在不阻塞的情况下访问Windows上的标准I/O管道。因此,此项目将不允许使用默认进程管道构建子进程,并将默认在Windows上抛出LogicException
。
// throws LogicException on Windows $process = new Process('ping example.com'); $process->start();
如果您想在Windows上运行子进程,以下有几种替代方案和解决方案,每种都有自己的优缺点。
-
从PHP 8开始,您可以使用如下方式使用
socket
对描述符来启动子进程,代替正常的标准I/O管道。$process = new Process( 'ping example.com', null, null, [ ['socket'], ['socket'], ['socket'] ] ); $process->start(); $process->stdout->on('data', function ($chunk) { echo $chunk; });
这些
socket
对在任何平台上都支持非阻塞的进程I/O,包括Windows。然而,并非所有程序都接受stdio套接字。 -
此包在没有问题的条件下可以在
Windows Subsystem for Linux
(或WSL)上运行。当您控制应用程序的部署方式时,我们建议在Windows上运行此包时安装WSL。 -
如果您只关心子进程的退出代码来检查其执行是否成功,您可以使用自定义管道来省略任何标准I/O管道,如下所示。
$process = new Process('ping example.com', null, null, array()); $process->start(); $process->on('exit', function ($exitcode) { echo 'exit with ' . $exitcode . PHP_EOL; });
类似地,如果您的子进程通过套接字与远程服务器或父进程通信,这也很有用。这通常被认为是控制子进程与父进程通信方式的最佳替代方案。
-
如果您只关心子进程执行后的命令输出,您可以使用自定义管道来配置要传递给子进程的文件句柄,而不是管道,如下所示。
$process = new Process('ping example.com', null, null, array( array('file', 'nul', 'r'), $stdout = tmpfile(), array('file', 'nul', 'w') )); $process->start(); $process->on('exit', function ($exitcode) use ($stdout) { echo 'exit with ' . $exitcode . PHP_EOL; // rewind to start and then read full file (demo only, this is blocking). // reading from shared file is only safe if you have some synchronization in place // or after the child process has terminated. rewind($stdout); echo stream_get_contents($stdout); fclose($stdout); });
请注意,此示例仅使用
tmpfile()
/fopen()
进行说明。这不应该在实际的异步程序中使用,因为文件系统本质上是阻塞的,每次调用可能需要几秒钟。还可以参考文件系统组件作为替代方案。 -
如果您想以流的形式实时访问命令输出,您可以使用重定向来创建一个额外的进程,将标准I/O流转发到套接字,并使用自定义管道来省略任何实际的标准I/O管道,如下所示。
$server = new React\Socket\Server('127.0.0.1:0'); $server->on('connection', function (React\Socket\ConnectionInterface $connection) { $connection->on('data', function ($chunk) { echo $chunk; }); }); $command = 'ping example.com | foobar ' . escapeshellarg($server->getAddress()); $process = new Process($command, null, null, array()); $process->start(); $process->on('exit', function ($exitcode) use ($server) { $server->close(); echo 'exit with ' . $exitcode . PHP_EOL; });
注意这将创建另一个虚构的
foobar
辅助程序来消耗实际子进程的标准输出。实际上,这类似于在子进程中使用套接字连接的建议,但在此情况下不需要修改实际的子进程。在此示例中,虚构的
foobar
辅助程序可以通过简单地从标准输入读取所有数据并将其转发到套接字连接来实现,如下所示。$socket = stream_socket_client($argv[1]); do { fwrite($socket, $data = fread(STDIN, 8192)); } while (isset($data[0]));
相应地,此示例也可以用纯PHP运行,而不必依赖于任何外部辅助程序,如下所示。
$code = '$s=stream_socket_client($argv[1]);do{fwrite($s,$d=fread(STDIN, 8192));}while(isset($d[0]));'; $command = 'ping example.com | php -r ' . escapeshellarg($code) . ' ' . escapeshellarg($server->getAddress()); $process = new Process($command, null, null, array()); $process->start();
另请参阅 示例 #23。
请注意,这只是为了说明目的,您可能希望在实际生产环境中实现一些适当的错误检查和/或套接字验证,以避免其他进程连接到服务器套接字。在这种情况下,我们建议您查看优秀的 createprocess-windows。
此外,请注意,给 Process
的 命令 将原样传递给底层的 Windows-API (CreateProcess
),并且进程不会默认通过包装外壳启动。特别是,这意味着像 echo hello
或 sleep 10
这样的外壳内置函数可能需要前面加上一个显式的外壳命令,例如
$process = new Process('cmd /c echo hello', null, null, $pipes); $process->start();
安装
安装此库的推荐方法是 通过 Composer。 您是 Composer 新手吗?
一旦发布,此项目将遵循 SemVer。目前,这将安装最新的开发版本
composer require react/child-process:^0.7@dev
有关版本升级的详细信息,请参阅 变更日志。
此项目旨在在任何平台上运行,因此不需要任何 PHP 扩展,并支持在旧版 PHP 5.3 到当前 PHP 8+ 和 HHVM 上运行。强烈建议为此项目使用最新支持的 PHP 版本。
请注意,使用带有旧版 --enable-sigchild
选项编译的旧平台可能无法在某些情况下可靠地确定子进程的退出代码。这不应影响大多数安装,因为此配置选项不是默认使用的,大多数发行版(如 Debian 和 Ubuntu)都已知默认不使用此选项。此选项可能用于一些使用 Oracle OCI8 的安装,请查看 phpinfo()
输出以检查您的安装是否可能受到影响。
有关有限 Windows 兼容性 的说明,请参阅上面的注释。
测试
要运行测试套件,您首先需要克隆此存储库,然后通过 Composer 安装所有依赖项 通过 Composer
composer install
要运行测试套件,请转到项目根目录并运行
vendor/bin/phpunit
许可证
MIT,请参阅 许可文件。