如果您嘗試在 Windows 下執行命令,PHP 腳本通常會等待直到程序終止。執行長期程序會暫停 PHP 腳本,即使您不想等待程序結束。
要找到這個在 Windows 下啟動程序而不等待其終止的漂亮範例並不容易
<?php
$commandString = 'start /b c:\\programToRun.exe -attachment "c:\\temp\file1.txt"';
pclose(popen($commandString, 'r'));
?>
(PHP 4, PHP 5, PHP 7, PHP 8)
popen — 開啟程序檔案指標
command
命令
mode
模式。讀取為 'r'
,寫入為 'w'
。
在 Windows 上,popen() 預設為文字模式,也就是說任何寫入或從管道讀取的 \n
字元都會被轉換為 \r\n
。如果不需要此行為,可以將 mode
分別設定為 'rb'
和 'wb'
來強制使用二進制模式。
傳回一個檔案指標,與 fopen() 傳回的檔案指標相同,只是它是單向的(只能用於讀取或寫入),並且必須使用 pclose() 關閉。此指標可用於 fgets()、fgetss() 和 fwrite()。當模式為 'r' 時,傳回的檔案指標等於命令的 STDOUT,當模式為 'w' 時,傳回的檔案指標等於命令的 STDIN。
如果發生錯誤,則傳回 false
。
範例 1:popen() 範例
<?php
$handle = popen("/bin/ls", "r");
?>
如果找不到要執行的命令,則會傳回有效的資源。這看起來可能很奇怪,但其實很合理;它允許您存取 Shell 傳回的任何錯誤訊息。
範例 2:popen() 範例
<?php
error_reporting(E_ALL);
/* 加入重新導向,以便我們可以取得 stderr。 */
$handle = popen('/path/to/executable 2>&1', 'r');
echo "'$handle'; " . gettype($handle) . "\n";
$read = fread($handle, 2096);
echo $read;
pclose($handle);
?>
注意:
如果您正在尋找雙向支援(雙向),請使用 proc_open()。
如果您嘗試在 Windows 下執行命令,PHP 腳本通常會等待直到程序終止。執行長期程序會暫停 PHP 腳本,即使您不想等待程序結束。
要找到這個在 Windows 下啟動程序而不等待其終止的漂亮範例並不容易
<?php
$commandString = 'start /b c:\\programToRun.exe -attachment "c:\\temp\file1.txt"';
pclose(popen($commandString, 'r'));
?>
不要期望此函式在可執行檔一開始不存在時傳回 false。無論如何都會開啟一個串流,但無法從中讀取任何內容。類似於「sh: 1: asdfasdfasdf: not found」的錯誤會列印到 STDERR。
解決方案 1:查看 pclose() 的傳回值,它將是執行該命令的 Shell 的結束狀態。在 Linux 上,如果找不到可執行檔,則將為 127。否則就是可執行檔本身的結束狀態。
解決方案 2:改用 proc_open(),它也允許擷取 STDERR,然後剖析是否有錯誤。
您可能應該兩者都做。
作為 anonymous at anon dot com 所提供程式碼的附註
$cmd = "php longscript.php";
function execInBackground($cmd) {
if (substr(php_uname(), 0, 7) == "Windows"){
pclose(popen("start /B ". $cmd, "r"));
} else {
exec($cmd . " > /dev/null &");
}
}
我遇到一個問題,Windows 會在整個腳本被解譯之前太快關閉呼叫,但我不想讓我的主要腳本掛起,直到它完全載入。
作為一種解決方法,我呼叫了一個小的 .php 腳本,然後該腳本會呼叫較大的腳本。
myfile.php
<?php
$cmd = "php timewrapper.php";
function execInBackground($cmd) {
if (substr(php_uname(), 0, 7) == "Windows"){
pclose(popen("start /B ". $cmd, "r"));
} else {
exec($cmd . " > /dev/null &");
}
}
?>
timewrapper.php
<?php
$cmd = "php longscript.php";
$timer = popen("start /B ". $cmd, "r");
sleep(30);
pclose($timer);
?>
這樣我的主腳本就能繼續執行,而無需暫停,同時小腳本會在載入較大的檔案時暫停。
如果在 Windows 上,你需要啟動一個需要管理員權限的批次檔,那麼你可以建立一個該批次檔的捷徑,點擊內容,在其中一個內容頁面上勾選「以管理員身分執行」,然後雙擊該捷徑一次(以初始化「以管理員身分執行」的設定)。
使用 popen("/path/to/shortcut.lnk") 將會以管理員權限執行你的批次檔。
這對於當你想使用 CLI 的 PHP 來執行一些長時間運行的任務,而該 PHP-CLI 需要使用 sessions 時非常方便。
如果你想在 Windows 下 fork 一個處理程序,這是要使用的函式。我建立了一個名為 runcmd.bat 的批次檔,其中包含以下行:
start %1 %2 %3 %4
然後我有以下函式:
<?php
define('RUNCMDPATH', 'c:\\htdocs\\nonwebspace\\runcmd.bat');
function runCmd($cmd) {
$externalProcess=popen(RUNCMDPATH.' '.$cmd, 'r');
pclose($externalProcess);
}
?>
有了這個,執行類似這樣的操作:
<?php runCmd('php.exe printWorkOrder.php 3498'); ?>
將會在 Apache 之外啟動 php.exe,並允許呼叫 runCmd() 函式的腳本繼續執行,而無需等待命令列處理程序返回。該處理程序將在 Apache(或您正在執行的任何網頁伺服器)的相同使用者帳戶下執行,因此請確保它具有執行您需要執行的任何操作的權限。此外,請確保批次檔具有足夠的 %n 以便傳遞您可能需要傳遞的所有命令列變數。
特別感謝 devshed 論壇的 kicken 提出這個想法。
如果你想從 Linux 伺服器下載大於 2GB 的檔案,你可以使用以下方法:
<?php
function serveFile( $file , $as ){
header( 'Expires: Mon, 1 Apr 1974 05:00:00 GMT' );
header( 'Pragma: no-cache' );
header( 'Cache-Control: must-revalidate, post-check=0, pre-check=0' );
header( 'Content-Description: File Download' );
header( 'Content-Type: application/octet-stream' );
header( 'Content-Length: '.trim(`stat -c%s "$file"`) );
header( 'Content-Disposition: attachment; filename="'. $as .'"' );
header( 'Content-Transfer-Encoding: binary' );
//@readfile( $file );
flush();
$fp = popen("tail -c ".trim(`stat -c%s "$file"`)." ".$file.' 2>&1', "r");
while(!feof($fp))
{
// send the current file part to the browser
print fread($fp, 1024);
// flush the content to the browser
flush();
}
fclose($fp);
}
?>
ps 命令的輸出被截斷?
解決方案在於 ps 顯示其資訊的方式。
特別是 -w 選項,它:
「使用 132 個欄位來顯示資訊,
而不是預設的視窗大小。」
不知何故,在 PHP 中使用 fgets 會導致 74 個字元
無論初始長度參數為何。
一些程式碼:
<?php
echo '<table width="99%"><tr><td>cron</td></tr>' . "\n";
$fp=popen("/bin/ps -waux","r");
while (!feof($fp)) {
$buffer = fgets($fp, 4096);
$croninf .= '<tr><td>' . $buffer . '</td></tr>' . "\n";
}
pclose($fp);
echo $croninf;
echo '</table><br><br>' . "\n";
?>
Ciao,
Rene =<>=
有一種簡單的方法可以在背景啟動一個處理程序,同時仍然可以找出處理程序的結果。我結合了下面一些使用者的資訊和我自己的一些想法,提出了以下內容:
<?php
$bat_filename = "C:\\my_bat_file.bat";
$bat_log_filename = "C:\\my_bat_file_bat.log";
$bat_file = fopen($bat_filename, "w");
if($bat_file) {
fwrite($bat_file, "@echo off"."\n");
fwrite($bat_file, "echo Starting proces >> ".$bat_log_filename."\n");
fwrite($bat_file, "php c:\\my_php_process.php >> ".$bat_log_filename."\n");
fwrite($bat_file, "echo End proces >> ".$bat_log_filename."\n");
fwrite($bat_file, "EXIT"."\n");
fclose($bat_file);
}
//
// Start the process in the background
//
$exe = "start /b ".$bat_filename;
if( pclose(popen($exe, 'r')) ) {
return true;
}
return false;
?>
在我的情況下,.bat 和 .log 檔案的檔名並不總是相同,因此我需要一種動態建立 .bat 檔案的方法。php 命令的輸出使用 >> 命令儲存到日誌檔案中。所有列印和錯誤都儲存在那裡。稍後您可以開啟日誌檔案,查看發生了什麼。
以下程式碼適用於雙向處理;) 祝大家玩得開心
<?php
system("mkfifo pipeout");
$pipe = popen("./nwserver -module Chapter1E > pipeout","w");
$pipeout = fopen("pipeout", "r");
while ($s = fgets($pipeout,1024)) {
echo $s;
}
?>
Windows 使用者在使用 popen 和 start 來執行外部腳本,而無需讓 PHP 等待時請注意。
例如:
pclose( popen( 'start /b php someLongScript.php *> nul', 'rb' ) );
如果 start 找不到 exe,它會開啟一個快顯訊息,而 pclose 會掛起直到關閉快顯視窗。
另一個針對在 "w" 模式下使用 popen(),以使命令的 stdout 傳送到瀏覽器的變通方法
一個簡單的解決方案是使用兩個 PHP 腳本;一個是包含 popen($cmd, "w") 命令的 "real.php",另一個是 "wrapper.php",一個簡單調用 system("php real.php") 的單行腳本。
從瀏覽器調用 "wrapper.php" 允許 "real.php" 中的 popen($cmd,"w") 如預期般工作,使得 $cmd 的 stdout 傳送到瀏覽器。如果你嘗試跳過 wrapper 直接執行 "real.php",$cmd 的 stdout 會遺失到 /dev/null。
我應該說,我的主機使用修改過的 Safe Mode,所以我不知道這是否會導致 "popen" 而不是 "proc_open" 的問題。在啟用 Safe Mode 的情況下,初始命令字串之後的所有單字都會被視為單一參數。因此,`echo y | echo x` 會變成 `echo "y | echo x"`。[因此,] LinixDude010 的腳本對我來說無效。根據手冊,使用 popen 進行讀寫似乎是錯誤的。
該腳本產生了 PGP 文字,但文字有些問題,我無法解碼。
這個使用 proc_open 的替換腳本,它可以讀寫,確實可以工作
<?php
function pgp_encrypt($keyring_location, $public_key_id, $plain_text) {
$encrypted_text='';
$key_id = EscapeShellArg($public_key_id);
putenv("PGPPATH=$keyring_location");
// 加密訊息
$descriptorspec = array(
0 => array("pipe", "r"), // stdin
1 => array("pipe", "w"), // stdout
2 => array("pipe", "w") // stderr ?? 而不是一個檔案
);
$process = proc_open("pgpe -r $key_id -af", $descriptorspec, $pipes);
if (is_resource($process)) {
fwrite($pipes[0], $plain_text);
fclose($pipes[0]);
while($s= fgets($pipes[1], 1024)) {
// 從管道讀取
$encrypted_text .= $s;
}
fclose($pipes[1]);
// 可選:
while($s= fgets($pipes[2], 1024)) {
$encrypted_text.= "\n<p>錯誤:$s</p>\n";
}
fclose($pipes[2]);
}
return $encrypted_text;
}
$message = pgp_encrypt("/home/username/.pgp", "to@domain.com", "要加密的虛擬文字");
print nl2br($message);
?>
來自 popen Linux 程式設計師手冊
「command 參數是指向包含 shell 命令行的 null 終止字串的指標。此命令會使用 -c 旗標傳遞至 /bin/sh。」
由於 PHP 使用此 popen 函數,您需要確保 /bin/sh 存在。此檔案可能不存在於 chroot() 環境中。
我用 PGP 加密訊息遇到了各種問題,但最終還是成功了。訣竅是 'chmod o+r pubring.pkr',這樣 Apache 伺服器才能讀取公鑰!!!然後,這個函數就能正常運作了
<?PHP
function pgp_encrypt($keyring_location, $public_key_id, $plain_text) {
$key_id = EscapeShellArg($public_key_id);
putenv("PGPPATH=$keyring_location");
// 加密訊息
$pipe = popen("pgpe -r $key_id -af", "r");
fwrite($pipe, $plain_text);
$encrypted_text = '';
while($s = fgets($pipe, 1024)) {
// 從管道讀取
$encrypted_text .= $s;
}
pclose($pipe);
return $encrypted_text;
}
$message = pgp_encrypt("/home/username/.pgp", "to@domain.com", "要加密的虛擬文字");
print nl2br($message);
?>
如果您在 Debian "Squeeze" 上於 chroot 環境中執行,此命令將無法運作;最終 popen() 呼叫的核心程式碼存在問題。
請注意,pecl 大量使用此命令,因此如果您在此環境中執行,則需要從原始碼安裝 pecl 擴充功能。
這裡有一個不錯的小腳本,用於監控您的 HTTP 存取日誌。
<?php
$handle = popen("tail -f /etc/httpd/logs/access.log 2>&1", 'r');
while(!feof($handle)) {
$buffer = fgets($handle);
echo "$buffer<br/>\n";
ob_flush();
flush();
}
pclose($handle);
?>
----
www.eviltree.co.uk
www.solidsites.co.uk
www.mongbong.com
<?php
// 上述匯入函式可以使用
// /usr/local/bin/xls2csv (catdoc 的一部分) 和 popen
// 輕易擴充來直接讀取 Excel 檔案。
// 在我們的特定應用中,第一行是檔案標題。
function importxls($file,$head=true,$throwfirst=true,$delim=",",$len=1000) {
$return = false;
$handle = popen("/usr/local/bin/xls2csv $file", "r");
// 如果不存在則終止。
if ($throwfirst) {
$throw = fgetcsv($handle, $len, $delim);
}
if ($head) {
$header = fgetcsv($handle, $len, $delim);
}
while (($data = fgetcsv($handle, $len, $delim)) !== FALSE) {
if ($head AND isset($header)) {
foreach ($header as $key=>$heading) {
$row[$heading]=(isset($data[$key])) ? $data[$key] : '';
print "<li>". $heading ."=>" . $row[$heading]."</li>";
}
$return[]=$row;
} else {
$return[]=$data;
}
}
fclose($handle);
return $return;
}
?>
我注意到上面的一些範例似乎提倡在沒有雙向 popen (在某些作業系統上) 的情況下,透過管道 shell escape 將未加密的資料傳遞給 gpg。
我採用的方法類似於
<?php
$prefix = 'example';
$command = '/usr/local/bin/gpg --encrypt --armor --no-tty --batch --no-secmem-warning --recipient "joe.soap@example.com"';
$tmpfile = tempnam('/tmp', $prefix);
$pipe = popen("$command 2>&1 >$tmpfile", 'w');
if (!$pipe) {
unlink($tmpfile);
} else {
fwrite($pipe, $plaintxt, strlen($plaintxt));
pclose($pipe);
$fd = fopen($tmpfile, "rb");
$output = fread($fd, filesize($tmpfile));
fclose($fd);
unlink($tmpfile);
}
return $output;
?>
這表示未加密的資訊不會透過 (可能可讀的) shell 命令傳遞,只有加密的資訊會儲存在磁碟上。
請注意,您*必須*在可以使用 feof() 之前先對 handle 執行讀取操作,即使命令沒有輸出任何內容!所以...
<?php
$f=popen("sleep 2","r");
while (!feof($f)) {}
pclose($f);
print "done";
?>
將永遠不會完成。
我嘗試使用雙向管道使用 popen,但由於顯而易見的原因而無法運作,但我設法建立了一個簡單的腳本來解決這個問題。這個範例是針對 gpg 加密的。
<?php
$message = "this is the text to encrypt with gpg";
$sendto = 'Dummy Key <another@fake.email>';
system("mkfifo pipein");
system("mkfifo pipeout");
system("gpg --encrypt -a -r '$sendto' > pipeout < pipein &");
$fo = fopen("pipeout", "r");
$fi = fopen("pipein", "w");
fwrite($fi, $message, strlen($message));
fclose($fi);
while (!feof($fo)) {
$buf .= fread($fo, 1024);
}
echo $buf;
unlink("pipein");
unlink("pipeout");
?>
如果有人有更好的方法可以做到這一點,我很想看看。
在執行時間較長的子處理程序時,需要謹慎處理。假設您想執行 tail -f /var/log/messages,或者在我的情況下是燒錄 DVD。如果您有忙碌等待,Apache2 可以達到 100% CPU 並穩定地增加記憶體。在我的情況下,我大約在一個小時後當機了伺服器,而且燒錄了 90% 的 DVD。在那段時間裡,apache 消耗了一個 GB 的交換空間。
有問題的程式碼 - 請勿複製
<?php
$ThisCommand = sprintf("%s %s",COMMAND,$ThisFile);
$fp=popen($ThisCommand,"r");
while (!feof($fp)) {
set_time_limit (20);
$results = fgets($fp, 4096);
if (strlen($results) == 0) {
// 停止瀏覽器逾時
echo " ";
flush();
} else {
$tok = strtok($results, "\n");
while ($tok !== false) {
echo htmlentities(sprintf("%s\n",$tok))."<br/>";
flush();
$tok = strtok("\n");
}
}
}
pclose($fp);
?>
要從零記憶體和 100% CPU 轉換到可忽略的記憶體和可忽略的 CPU,請新增睡眠。
<?php
while (!feof($fp)) {
set_time_limit (20);
$results = fgets($fp, 256);
if (strlen($results) == 0) {
// 停止瀏覽器逾時
echo " ";
flush();
} else {
$tok = strtok($results, "\n");
while ($tok !== false) {
echo htmlentities(sprintf("%s\n",$tok))."<br/>";
flush();
$tok = strtok("\n");
}
}
// 避免忙碌等待
sleep(1);
}
?>
我認為持續敲擊空白鍵以保持瀏覽器喚醒的動作,觸發了 Apache 的一些問題。
這裡有一個在 PHP 中沒有雙向管道的解決方案。
如果你有雙向管道支援,則不必理會這個方法。
這裡的技巧是在命令行上將輸入發送到目標應用程式。特別是,我想要在不使用臨時文件或具名管道的情況下使用 openssl。這個解決方案也應該是線程/進程安全的。
這在 Linux(RedHat 7)上可以運作。
<?php
function filterThroughCmd($input, $commandLine) {
$pipe = popen("echo \"$input\"|$commandLine" , 'r');
if (!$pipe) {
print "管道失敗。";
return "";
}
$output = '';
while(!feof($pipe)) {
$output .= fread($pipe, 1024);
}
pclose($pipe);
return $output;
}
# 範例:
print filterThroughCmd("hello", "cat");
# 管道傳送到 cat 的效果是回顯你的輸入。
?>
另一個在 PHP 中沒有雙向管道的解決方案。
<?php
$Cmd =
"bc 2>&1 << END\n" .
"100+221\n" .
"1+3*3\n" .
"quit\n" .
"END\n";
$fp = popen($Cmd, 'r');
$read = fread($fp, 1024);
echo $read;
pclose($fp);
?>