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 會以 *未加引號* 的字串形式(即完全按照提供給 proc_open() 的形式)傳遞給 cmd.exe(實際上是 %ComSpec%),並帶有 /c 旗標。這可能會導致 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
  • 表示真實文件描述符的串流資源(例如,已打開的文件、socket、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):新進程擁有新的控制台,而不是繼承其父進程的控制台

返回值

返回表示進程的資源,當您使用完畢後,應使用 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 Windows 系統上 proc_open() 的特殊行為

雖然您可能預期以下程式會在檔案 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(),因為它更容易使用。

另請參閱

新增註記

使用者貢獻的註記 35 則註記

14
Bobby Dylan
1 年前
我不確定 PHP 何時新增了「blocking_pipes (僅限 Windows)」選項,但此函式的使用者應該充分瞭解,在 Windows 上的 PHP 中沒有非阻塞管道這種東西,而且「blocking_pipes」選項的運作方式並不像您預期的那樣。傳遞「blocking_pipes」=> false 並不代表非阻塞管道。

PHP 在 Windows 上使用匿名管道來啟動程序。Windows 的 CreatePipe() 函數並不直接支援重疊式 I/O(也就是非同步),而這通常是非同步/非阻塞式 I/O 在 Windows 上的運作方式。SetNamedPipeHandleState() 有一個名為 PIPE_NOWAIT 的選項,但微軟長期以來一直不建議使用該選項。PHP 的原始碼中任何地方都沒有使用 PIPE_NOWAIT。PHP FastCGI 啟動程式碼是 PHP 原始碼中唯一使用重疊式 I/O 的地方(也是唯一使用 PIPE_WAIT 呼叫 SetNamedPipeHandleState() 的地方)。此外,Windows 上的 stream_set_blocking() 僅針對通訊端(socket)實作,而不是檔案控制代碼或管道。也就是說,在 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 通訊端和標準 HANDLE 之間路由資料,並使用臨時密鑰來防止建立 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 開啟,且指令崩潰或不存在,您將永遠被阻塞。
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);

// 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);
?>
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, // 標準輸入
$fh, // 標準輸出
$fh, // 標準錯誤輸出
];
$proc = proc_open($cmd, $descriptors, $pipes);
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, "傳送訊息給 inpipe 讀取器\n");
fclose($in);

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

pcntl_waitpid($pid, $status);

if(
pcntl_wifexited($status)) {
echo
"可靠的結束代碼: " . pcntl_wexitstatus($status) . PHP_EOL;
}

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

//子行程
else {
//父行程
if($pid = pcntl_fork()) {
pcntl_exec('/bin/sh', array('-c', "printf '傳送訊息給 outpipe 讀取器' > $outpipe 2>&1 && exit 12"));
}

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

輸出

來自 in pipe: 傳送訊息給 inpipe 讀取器
來自 out pipe: 傳送訊息給 outpipe 讀取器
可靠的結束代碼: 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(
'程序無法啟動 [' . $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」。因此,如果您在 chroot 環境中工作,您需要確保 /bin/sh 存在,目前來說。
5
bilge at boontex dot com
12 年前
`$cmd` 實際上可以透過以換行符號分隔多個指令來執行多個指令。然而,由於這個機制,即使使用 `\\\n` 語法,也不可能將一個很長的指令拆分到多行。
6
michael dot gross at NOSPAM dot flexlogic dot at
11 年前
請注意,如果您打算產生多個行程,您必須將所有結果儲存在不同的變數中(例如,使用陣列)。例如,如果您多次呼叫 `$proc = proc_open.....`,則腳本在第二次呼叫後將會阻塞,直到子行程結束(隱式呼叫 `proc_close`)。
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
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.dev.org.tw/manual/en/function.exec.php
https://php.dev.org.tw/manual/en/function.system.php

警告

如果您打算允許將來自使用者輸入的資料傳遞給此函式,那麼您應該使用 escapeshellarg() 或 escapeshellcmd() 來確保使用者無法欺騙系統執行任意命令。
5
John Wehin
16 年前
標準輸入 標準輸出 範例
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'), // 標準輸入
1 => array('pipe', 'w'), // 標準輸出
2 => array('pipe', 'a') // 標準錯誤輸出
);
?>
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
// make gz file
$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");

// open process
$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");

// set both pipes non-blocking
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)
{
// if yo remove this paused retries, process may fail
usleep(200000);
$try--;
if(!
$try)
throw new
Exception("fwrite error");
}
$str=substr($str, $len);
}
echo
strlen($text)."\n";
}
fclose($fd);
fclose($pipes[0]);

// reading the rest of output stream
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"), // 標準輸入是一個子程序將從中讀取的管道
1 => array("pipe", "w"), // 標準輸出是一個子程序將寫入的管道
2 => array("pipe", "w") // 標準錯誤是一個標準輸出將寫入的管道
);

$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 => 連接到子程序標準輸入的可寫入控制代碼
// 1 => 連接到子程序標準輸出的可讀取控制代碼
// 任何錯誤輸出都將發送到 $pipes[2]

// 關閉 $pipes[0] 因為我們不需要它
fclose( $pipes[0] );

// 標準錯誤不應阻塞,因為這會阻塞 tail 程序
stream_set_blocking($pipes[2], 0);
$count=0;
$stream = $pipes[1];

while ( (
$buf = fgets($stream,4096)) ) {
print_r($buf);
// 讀取標準錯誤以查看是否發生任何錯誤
$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
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(;;) {
// Fill up the slots
while (($this->processesRunning<$this->processes) and ($i<count($this->scripts))) {
echo
"<span style='color: orange;'>Adding script: ".$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++;
}

// Check if done
if (($this->processesRunning==0) and ($i>=count($this->scripts))) {
break;
}
// sleep, this duration depends on your script execution time, the longer execution time, the longer sleep time
sleep($this->sleep_time);

// check what is done
foreach ($this->running as $key => $val) {
if (!
$val->isRunning() or $val->isOverExecuted()) {
if (!
$val->isRunning()) echo "<span style='color: green;'>Done: ".$val->script."</span><br />";
else echo
"<span style='color: red;'>Killed: ".$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 = ''; // path to you gpg keyrings
$tmp_file = '/tmp/gpg_tmp.asc' ; // tmp file to write to
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 ?? instead of a file
);
$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)) {
// read from the pipe
$text .= $s;
}
fclose($pipes[1]);
// optional:
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.dev.org.tw/manual/en/ref.stream.php
1
cbn at grenet dot org
14 年前
以即時方式顯示輸出(stdout/stderr),並在純 PHP 中取得實際的退出代碼(無 shell 解決方案!)。它在我的機器上運作良好(大多是 Debian)。

#!/usr/bin/php
<?php
/*
* Execute and display the output in real time (stdout + stderr).
*
* Please note this snippet is prepended with an appropriate shebang for the
* CLI. You can re-use only the function.
*
* Usage example:
* chmod u+x proc_open.php
* ./proc_open.php "ping -c 5 google.fr"; echo RetVal=$?
*/
define(BUF_SIZ, 1024); # max buffer size
define(FD_WRITE, 0); # stdin
define(FD_READ, 1); # stdout
define(FD_ERR, 2); # stderr

/*
* Wrapper for proc_*() functions.
* The first parameter $cmd is the command line to execute.
* Return the exit code of the process.
*/
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);

/* Get the expected *exit* code to return the value */
$pstatus = proc_get_status($ptr);
if (!
strlen($pstatus["exitcode"]) || $pstatus["running"]) {
/* we can trust the retval of 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; /* we "deduce" an 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 程序啟動時並不會在 PHP 本身生效。因此,如果您嘗試僅使用執行檔名稱來啟動修改過 PATH 中的執行檔,PHP 和作業系統將找不到它,因此程序將無法啟動。

解決方法是讓 PHP 知道修改過的 PATH,方法是使用新的路徑字串呼叫 putenv("PATH=" . $newpath),這樣呼叫 proc_open() 時就能正確找到執行檔並成功執行它。
2
snowleopard at amused dot NOSPAMPLEASE dot com dot au
16 年前
由於我的主機供應商拒絕使用 GPG-ME,我設法製作了一組函式來與 GPG 協作。
以下包含使用較高描述符推送密碼片語來解密的範例。
歡迎提供意見和電子郵件。 :)

<?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 {
// Set up the descriptors
$Descriptors = array(
0 => array("pipe", "r"),
1 => array("pipe", "w"),
2 => array("pipe", "w"),
3 => array("pipe", "r") // This is the pipe we can feed the password into
);

// Build the command line and start the process
$CommandLine = $GPGPath . ' --homedir ' . $HomeDir . ' --quiet --batch --local-user "' . $Identity . '" --passphrase-fd 3 --decrypt -';
$ProcessHandle = proc_open( $CommandLine, $Descriptors, $Pipes);

if(
is_resource($ProcessHandle)) {
// Push passphrase to custom pipe
fwrite($Pipes[3], $PassPhrase);
fclose($Pipes[3]);

// Push input into StdIn
fwrite($Pipes[0], $InputData);
fclose($Pipes[0]);

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

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

// Close the process
$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
19 年前
由於我無法透過 Apache 存取 PAM、suexec 已開啟,也無法存取 /etc/shadow,我想出了這種根據系統使用者詳細資訊驗證使用者身分的方法。它確實很複雜且醜陋,但它有效。

<?
function authenticate($user,$password) {
$descriptorspec = array(
0 => array("pipe", "r"), // 標準輸入是一個子程序將從中讀取的管道
1 => array("pipe", "w"), // 標準輸出是一個子程序將寫入的管道
2 => array("file","/dev/null", "w") // 標準錯誤輸出是一個要寫入的檔案
);

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

if (is_resource($process)) {
// $pipes 現在看起來像這樣
// 0 => 連接到子程序標準輸入的可寫入控制代碼
// 1 => 連接到子程序標準輸出的可讀取控制代碼
// 任何錯誤輸出都將附加到 /tmp/error-output.txt

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

// 重要的是,在呼叫 proc_close 之前必須關閉所有管道,以避免死結
// proc_close($process);
$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 方法重新開啟它們以進行高階 IO(例如 fgets)。然後,子程序可以像讀取或寫入標準輸入或標準輸出一樣讀取或寫入它們。

然而,子程序必須經過特殊編碼才能使用這些控制代碼,如果終端使用者不夠聰明而不知道如何使用它們(例如 "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') ){ // 否則如果 shell_exec 函式存在
return shell_exec($cmd); // 返回 shell_exec 執行結果
}else if( function_exists('system') ){ // 否則如果 system 函式存在
$return_var = 0;
return system($cmd, $return_var); // 返回 system 執行結果
}else if( function_exists('passthru') ){ // 否則如果 passthru 函式存在
$return_var = 0;
ob_start(); // 開啟輸出緩衝
passthru($cmd, $return_var); // 使用 passthru 執行指令
$output = ob_get_contents(); // 取得輸出緩衝內容
ob_end_clean(); // 清除並關閉輸出緩衝
return $output; // 返回輸出結果
}else if( function_exists('proc_open') ){ // 否則如果 proc_open 函式存在
$proc=proc_open($cmd, // 使用 proc_open 執行指令
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