PHPのお勉強!

PHP TOP

proc_open

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

proc_open コマンドを実行し、入出力用にファイルポインタを開く

説明

proc_open(
    array|string $command,
    array $descriptor_spec,
    array &$pipes,
    ?string $cwd = null,
    ?array $env_vars = null,
    ?array $options = null
): resource|false

proc_open()popen() と よく似ていますが、プログラムの実行をさらに細かく制御できる点で違います。

パラメータ

command

実行するコマンドラインを string として渡します。 特殊な文字は適切にエスケープされ、適切にクォートされます。

注意: Windows では、 optionsbypass_shelltrue に設定しないと、 cmd.exe (実際は%ComSpec%) に command の値を クォートしないまま (つまり、proc_open() に渡されたそのままの値を) /c と一緒に渡してしまいます。 この振る舞いによって、cmd.execommand からクォートを削除してしまうため、 (詳細は cmd.exe のドキュメントを参照ください) 予期しない、潜在的に危険とさえ言える結果になります。なぜなら、 cmd.exe のエラーメッセージには、 渡された command (の一部) が含まれる可能性があるからです(下の例を見てください)。

PHP 7.4.0 以降、command にはコマンドの引数も含めた array を渡せるようになりました。 この場合、プロセスは直接(シェルを介さずに)オープンされ、PHP が必要な引数のエスケープを全て行います。

注意:

Windows では、array で渡されるコマンドライン引数のエスケープは、 コマンドライン引数の解釈が VCランタイムによって行われるものと互換性があることを前提にして行われます。

descriptor_spec

数値添字の配列で、ディスクリプタ番号をキーとし、PHP がその ディスクリプタをどのように子プロセスに渡すかを表すのが 対応する値となります。 0 が標準入力 (stdin)、1 が標準出力 (stdout) で、 2 が標準エラー出力 (stderr) となります。

各要素は、次のようになります。

  • プロセスに渡すパイプをあらわす配列。 最初の要素はディスクリプタの型で、2 番目の要素がその型に対応するオプションとなります。 使用できる型は pipe (2 番目の要素は、 プロセスにパイプの読み込み側を渡すのなら r、 書き込み側を渡すのなら w) および file (2 番目の要素はファイル名) です。 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 シェルをバイパスします。
  • blocking_pipes (windows のみ): true に設定すると、パイプを強制的に切断します。
  • create_process_group (Windows のみ): true に設定すると、 子プロセスが CTRL イベントを ハンドルすることを許可します。
  • create_new_console (windows のみ): 新しいプロセスは親の console を継承せず、新しい console を持ちます。

戻り値

プロセスを表すリソースを返します。このリソースは、使用し終えた際に proc_close() を使用して開放する必要があります。 失敗した場合は false を返します。

変更履歴

バージョン 説明
7.4.4 options パラメータに オプション create_new_console が追加されました。
7.4.0 proc_open() 関数は、 commandarray を渡せるようになりました。
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") // はファイルで、そこに書き込みます。
);

$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
"command returned $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() システムコールと同等の操作を、 ストリームの配列に対して seconds と microseconds で指定されたタイムアウト時間をもって行う
  • バッククォート演算子

add a note

User Contributed Notes 35 notes

up
14
Bobby Dylan
1 year ago
I'm not sure when the "blocking_pipes (windows only)" option was added to PHP, but users of this function should be fully aware that there is no such thing as a non-blocking pipe in PHP on Windows and that the "blocking_pipes" option does NOT function like you might expect. Passing "blocking_pipes" => false does NOT mean non-blocking pipes.

PHP uses anonymous pipes to start processes on Windows. The Windows CreatePipe() function does not directly support overlapped I/O (aka asynchronous), which is typically how async/non-blocking I/O happens on Windows. SetNamedPipeHandleState() has an option called PIPE_NOWAIT but Microsoft has long discouraged the use of that option. PHP does not use PIPE_NOWAIT anywhere in the source code tree. PHP FastCGI startup code is the only place within the PHP source code that uses overlapped I/O (and also the only place that calls SetNamedPipeHandleState() with PIPE_WAIT). Further, stream_set_blocking() on Windows is only implemented for sockets - not file handles or pipes. That is, calling stream_set_blocking() on pipe handles returned by proc_open() will actually do nothing on Windows. We can derive from these facts that PHP does not have a non-blocking implementation for pipes on Windows and will therefore block/deadlock when using proc_open().

PHP's pipe read implementation on Windows uses PeekNamedPipe() by polling on the pipe until there is some data available to read OR until ~32 seconds (3200000 * 10 microseconds of sleep) have passed before giving up, whichever comes first. The "blocking_pipes" option, when set to true, changes that behavior to wait indefinitely (i.e. always block) until there is data on the pipe. It's better to view the "blocking_pipes" option as a "possibly 32 second busy wait" timeout (false - the default value) vs. no timeout (true). In either case, the boolean value for this option effectively blocks...it just happens to block a lot longer when set to true.

The undocumented string "socket" descriptor type can be passed to proc_open() and PHP will start a temporary TCP/IP server and generate a pre-connected TCP/IP socket pair for the pipe and pass one socket to the target process and return the other as the associated pipe. However, passing a socket handle for stdout/stderr on Windows causes the last chunk(s) of output to occasionally get lost and not be delivered to the receiving end. This is actually a known bug in Windows itself and Microsoft's response at one point was that CreateProcess() only officially supports anonymous pipes and file handles for the standard handles (i.e. not named pipes or socket handles) and that other handle types will produce "undefined behavior." For sockets, it will "sometimes work fine and sometimes truncate the output." The "socket" descriptor type also introduces a race condition that is probably a security vulnerability in proc_open() where another process can successfully connect to the server side BEFORE the original process connects to the socket to create the socket pair. This allows a rogue application to send malformed data to a process, which could trigger anything from privilege escalation to SQL injection depending on what the application does with the information on stdout/stderr.

To get true non-blocking I/O in PHP for Windows for standard process handles (i.e. stdin, stdout, stderr) without obscure bugs cropping up, the only currently working option is to use an intermediary process that uses TCP/IP blocking sockets to route data to blocking standard handles via multithreading (i.e. start three threads to route data between the TCP/IP socket and the standard HANDLE and use a temporary secret to prevent race conditions when establishing the TCP/IP socket handles). For those who lost count: That's one extra process, up to four extra threads, and up to four TCP/IP sockets just to get functionally correct non-blocking I/O for proc_open() on Windows. If you vomited a little bit at that idea/concept, well, people actually do this! Feel free to vomit some more.
up
27
php at keith tyler dot com
14 years ago
Interestingly enough, it seems you actually have to store the return value in order for your streams to exist. You can't throw it away.

In other words, this works:

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

prints:
foo

but this doesn't work:

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

outputs:
Warning: stream_get_contents(): <n> is not a valid stream resource in Command line code on line 1

The only difference is that in the second case we don't save the output of proc_open to a variable.
up
26
devel at romanr dot info
12 years ago
The call works as should. No bugs.
But. In most cases you won't able to work with pipes in blocking mode.
When your output pipe (process' input one, $pipes[0]) is blocking, there is a case, when you and the process are blocked on output.
When your input pipe (process' output one, $pipes[1]) is blocking, there is a case, when you and the process both are blocked on own input.
So you should switch pipes into NONBLOCKING mode (stream_set_blocking).
Then, there is a case, when you're not able to read anything (fread($pipes[1],...) == "") either write (fwrite($pipes[0],...) == 0). In this case, you better check the process is alive (proc_get_status) and if it still is - wait for some time (stream_select). The situation is truly asynchronous, the process may be busy working, processing your data.
Using shell effectively makes not possible to know whether the command is exists - proc_open always returns valid resource. You may even write some data into it (into shell, actually). But eventually it will terminate, so check the process status regularly.
I would advice not using mkfifo-pipes, because filesystem fifo-pipe (mkfifo) blocks open/fopen call (!!!) until somebody opens other side (unix-related behavior). In case the pipe is opened not by shell and the command is crashed or is not exists you will be blocked forever.
up
15
simeonl at dbc dot co dot nz
15 years ago
Note that when you call an external script and retrieve large amounts of data from STDOUT and STDERR, you may need to retrieve from both alternately in non-blocking mode (with appropriate pauses if no data is retrieved), so that your PHP script doesn't lock up. This can happen if you waiting on activity on one pipe while the external script is waiting for you to empty the other, e.g:

<?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);

// dual reading of STDOUT and STDERR stops one full pipe blocking the other, because the external script is waiting
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); // sleep for $ms milliseconds
if ($ms < 160)
{
$ms = $ms * 2;
}
}
}

return
proc_close($process);
?>
up
12
aaronw at catalyst dot net dot nz
9 years ago
If you have a CLI script that prompts you for a password via STDIN, and you need to run it from PHP, proc_open() can get you there. It's better than doing "echo $password | command.sh", because then your password will be visible in the process list to any user who runs "ps". Alternately you could print the password to a file and use cat: "cat passwordfile.txt | command.sh", but then you've got to manage that file in a secure manner.

If your command will always prompt you for responses in a specific order, then proc_open() is quite simple to use and you don't really have to worry about blocking & non-blocking streams. For instance, to run the "passwd" command:

<?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
);

// It wil prompt for existing password, then new password twice.
// You don't need to escapeshellarg() these, but you should whitelist
// them to guard against control characters, perhaps by using ctype_print()
fwrite($pipes[0], "$oldpassword\n$newpassword\n$newpassword\n");

// Read the responses if you want to look at them
$stdout = fread($pipes[1], 1024);
$stderr = fread($pipes[2], 1024);

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

// It returns 0 on successful password change
$success = ($exit_status === 0);
?>
up
3
vanyazin at gmail dot com
9 years ago
If you want to use proc_open() function with socket streams, you can open connection with help of fsockopen() function and then just put handlers into array of io descriptors:

<?php

$fh
= fsockopen($address, $port);
$descriptors = [
$fh, // stdin
$fh, // stdout
$fh, // stderr
];
$proc = proc_open($cmd, $descriptors, $pipes);