PHP Conference Japan 2024

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 上,除非在 options 中将 bypass_shell 设置为 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

错误/异常

从 PHP 8.3.0 开始,如果 command 是一个数组且至少不包含一个非空元素,则会抛出 ValueError

变更日志

版本 描述
8.3.0 如果 command 是一个数组且至少不包含一个非空元素,则会抛出 ValueError
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(),因为它更容易使用。

参见

  • popen() - 打开进程文件指针
  • exec() - 执行外部程序
  • system() - 执行外部程序并显示输出
  • passthru() - 执行外部程序并显示原始输出
  • stream_select() - 在给定的流数组上运行等效于 select() 系统调用的操作,超时时间由秒和微秒指定
  • 反引号运算符 反引号运算符

添加备注

用户贡献的备注 35 条备注

14
Bobby Dylan
1 年前
我不确定“blocking_pipes(仅限 Windows)”选项是什么时候添加到 PHP 中的,但是使用此函数的用户应该充分意识到,PHP 在 Windows 上根本不存在非阻塞管道,并且“blocking_pipes”选项的功能与您预期的大相径庭。“blocking_pipes”=> false 并不意味着非阻塞管道。

PHP 在 Windows 上使用匿名管道来启动进程。Windows CreatePipe() 函数并不直接支持重叠 I/O(又名异步),这通常是 Windows 上异步/非阻塞 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> 不是有效的流资源 in 命令行代码 on line 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 打开,并且命令崩溃或不存在,你将永远被阻塞。
15
simeonl at dbc dot co dot nz
15年前
请注意,当你调用外部脚本并从 STDOUT 和 STDERR 检索大量数据时,你可能需要以非阻塞模式交替从两者检索数据(如果没有检索到数据则适当暂停),以便你的 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);
?>
12
aaronw at catalyst dot net dot nz
9年前
如果你有一个 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);
?>
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);
10
[email protected]
13年前
如果你和我一样,厌倦了proc_open处理流和退出代码的bug方式;这个例子展示了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
[email protected]
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@dreesen[*NO*SPAM*].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 存在。
5
[email protected]
12年前
实际上,可以通过换行符分隔每个命令来使$cmd包含多个命令。但是,由于这个原因,即使使用“\\\n”语法,也不可能将一个很长的命令拆分成多行。
6
[email protected]
11年前
请注意,如果你计划生成多个进程,你必须将所有结果保存在不同的变量中(例如,在一个数组中)。例如,如果你多次调用 $proc = proc_open……,则脚本将在第二次调用后阻塞,直到子进程退出(proc_close被隐式调用)。
3
[email protected]
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
[email protected]
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"); // 发送 start
echo ("\n\n开始 ....".fgets($pipes[1],4096)); // 获取答案
fwrite($pipes[0], "get\n"); // 发送 get
echo ("获取:".fgets($pipes[1],4096)); // 获取答案
fwrite($pipes[0], "stop\n"); // 发送 stop
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 "Started\n";
return
false;
break;
case
"stop\n": echo "Stopped\n";
$keys=0;
return
false;
break;
case
"pause\n": echo "Paused\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);
8
Luceo
14 年前
在某些情况下,当 STDERR 充满时,在 Windows 下,STDOUT 上的 stream_get_contents() 似乎会无限期阻塞。

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

<?php
$descriptorspec
= array(
0 => array('pipe', 'r'), // stdin
1 => array('pipe', 'w'), // stdout
2 => array('pipe', 'a') // stderr
);
?>
2
andrew dot budd at adsciengineering dot com
18年前
由于某种原因,pty 选项实际上在源代码中通过 #if 0 && 条件被禁用了。我不确定为什么它被禁用。我移除了 0 && 并重新编译,之后 pty 选项完美地工作。只是一个说明。
3
exel at example dot com
11年前
管道通信可能会让人崩溃。我想分享一些东西来避免这种结果。
为了正确控制通过打开的子进程的“输入”和“输出”管道的通信,请记住将两者都设置为非阻塞模式,尤其要注意 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 错误");

// 设置两个管道为非阻塞
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 错误");
}
$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
[email protected]
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
[email protected]
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
[email protected]
9年前
请注意,在 Windows 中使用“bypass_shell”允许您传递长度约为 ~32767 个字符的命令。如果不使用它,您的限制大约只有 ~8191 个字符。

参见 https://support.microsoft.com/en-us/kb/830473.
2
[email protected]
9年前
对于那些发现使用 $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 年前
我为我的应用程序创建了一个智能对象进程管理器。它可以控制同时运行进程的最大数量。

进程管理器类
<?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();
}
}
}
}
}
?>

进程类
<?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
FF.ST上的MagicalTux
20年前
请注意,如果您需要与用户*和*打开的应用程序进行“交互”,可以使用`stream_select`查看管道另一端是否有等待的内容。

流函数可以用于以下类型的管道:
- 来自`popen`、`proc_open`的管道
- 来自`fopen('php://stdin')`(或stdout)的管道
- 套接字(Unix或TCP/UDP)
- 可能还有许多其他类型,但这里列出的是最重要的。

更多关于流的信息(您会发现许多有用的函数):
https://php.net/manual/en/ref.stream.php
1
grenet.org上的cbn
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(
"错误:文件描述符不足或内存不足。\n");
elseif (
$ret == 127)
die(
"找不到命令(由 sh 返回)。\n");
else
exit(
$ret);
}
?>
2
Gil Potts
1 年前
这并非真正的错误,而更像是一个意想不到的问题。如果您为 $env 传递一个数组,并包含一个修改后的 PATH,那么在启动进程时,该路径本身不会在 PHP 中生效。因此,如果您试图仅使用可执行文件名来启动修改后的 PATH 中的可执行文件,PHP 和操作系统将找不到它,因此将无法启动该进程。

解决方法是让 PHP 了解修改后的 PATH,方法是使用新的路径字符串调用 putenv("PATH=" . $newpath),以便 proc_open() 调用能够正确找到可执行文件并成功运行它。
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 . "不可执行",
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]);

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

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

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

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

} else {
trigger_error("无法创建资源", E_USER_ERROR);
die();
}
}

if (
strlen($StdOut) >= 1) {
if (
$ReturnCode <= 0) {
$ReturnValue = $StdOut;
} else {
$ReturnValue = "返回代码: " . $ReturnCode . "\n标准错误输出:\n" . $StdErr . "\n\n标准输出如下:\n\n";
}
} else {
if (
$ReturnCode <= 0) {
$ReturnValue = $StdErr;
} else {
$ReturnValue = "返回代码: " . $ReturnCode . "\n标准错误输出:\n" . $StdErr;
}
}
return
$ReturnValue;
}
?>
2
mendoza at pvv dot ntnu dot no
19年前
由于我无法通过 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
19年前
关于 Windows 兼容性的上述说明并不完全正确。

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

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

但是,子进程必须经过特殊编码才能使用这些句柄,如果最终用户不够聪明无法使用它们(例如,“openssl < commands.txt 3< cacert.der”)并且程序不够智能无法检查,则可能会导致错误或挂起。
0
mamedul.github.io
1 年前
使用 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";
}

}
To Top