重定向子進程控制台程序的輸入輸出
重定向所做的工作都在父進程,但需要子進程遵守下面的規則:
子進程程序在輸出代碼后,等待輸入之前需要調用fflush(stdout)函數,這樣把輸出的內容放入緩沖區,父進程才能及時的讀到輸出數據。
不遵守以上規則就沒辦法實現有效的交互了,cmd.exe是遵守這個規則的典范,大部分控制台程序都不遵守這個規則。今天我試圖給Google的V8 Javascript 的Shell搞一個GUI,方便我輸入Javascript程序,就遇到了v8_shell不遵守這個的問題。好在有源代碼,研究了一下,加上兩個fflush(stdout)就行了。
還有一個需要注意的就是,在向子進程的stdin寫內容時,一定要在最后加上"\r\n",否則直到父進程終止時子進程才能收到這些輸入,這時已經太晚了。這很奇怪,但確實如此。加上fflush(stdin)應該會更加保險。
MSDN的CreatePipe函數的Examples的鏈接內容對於這個主題也有講述,但不夠清晰扼要。
CodeProject也有個例子,能夠正確運行,但建議你用cmd.exe做子進程進行試驗。
管道是一種有兩個端點的通信通道。你使用管道在兩個進程間或同一進程內交換數據。它有點像手提對講機,雙方一人一個就可以通話了。
存在兩種管道:匿名管道和命名管道。匿名管道是匿名的,使用時你無需知道它的名字。命名管道相反,使用時你必須知道它的名字。
另一種分類:單向管道和雙向管道。單向管道數據流式單向的,像發傳真;雙向管道數據流是雙向的,像打電話。
匿名管道總是單向的,命名管道可以是單向或雙向的。命名管道可以用在網絡環境,服務器通過它連接到幾個客戶。在本教材,我們詳細研究匿名管道。匿名管道用於父子進程間的通信,或子進程間的通信。當你處理控制台程序時,匿名管道真的很有用。控制台程序類似於DOS窗口,但是它是完全的32位Windows程序,可以使用任何GUI函數,只不過它比通常的GUI程序多了一個控制台而已。控制台程序有三個句柄,標准輸入、標准輸出、錯誤輸出。
你可以調用GetStdHandle獲得控制台程序的三個句柄。GUI程序沒有控制台,如果你調用GetStdHandle,會返回錯誤。如果你想在GUI程序中使用控制台,那么可以使用AllocConsole,用完后請調用FreeConsole。
匿名管道常用於重定向控制台子進程的輸入輸入,父進程可以是控制台或GUI程序。我們可以替換控制台程序的輸入和輸出,而那個控制台程序渾然不覺。引用面向對象編程的術語,這是一種多態。這種方法很強大,我們無需對子進程做任何改變。
另外你要知道的是控制台程序從哪里獲得那些標准句柄。當控制台進程被創建時,父進程有兩個選擇:為子進程創建一個新的控制台,或者讓子進程繼承它的控制台。
介紹一下CreatePipe函數,該函數用於創建匿名管道。
BOOL WINAPI CreatePipe(
__out PHANDLE hReadPipe,
__out PHANDLE hWritePipe,
__in_opt LPSECURITY_ATTRIBUTES lpPipeAttributes,
__in DWORD nSize
);
hReadPipe 接受管道讀句柄的變量指針
hWritePipe 接受管道寫句柄的變量指針
lpPipeAttributes 安全屬性,決定子進程是否可以繼承返回的讀寫句柄
nSize 管道保留使用的緩沖大小,這僅僅是一個建議大小,你可以使用NULL表示使用缺省大小。
如果調用成功,返回值非0,否則,返回值為0。
如果調用成功,你會得到兩個句柄,一頭用於讀,一頭用於寫。下面我列出重定向子進程標准輸出到父進程的步驟,我們使用GUI做為父進程,因為這更有意義。
1 使用CreatePipe創建一個匿名管道。不要忘記設置SECURITY_ATTRIBUTES的bInheritable成員為TRUE,這樣子進程就可以繼承這些句柄。
2 調用CreateProcess創建子進程。我們需要准備CreateProcess函數的STARTUPINFO參數
cb : STARTUPINFO結構體的大小。
dwFlags : flag枚舉。為了達到目的,我們應該使用STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES
hStdOutput and hStdError : 你希望子進程使用的標准輸出和錯誤輸出的句柄。為了達到目的,我們把這個管道的寫端用於子進程的標准輸出和錯誤輸出。這樣當子進程輸出錯誤和其他信息時,實際是在向管道寫入。
wShowWindow 決定窗口的顯示狀態。為了達到目的,我們使用SW_HIDE來隱藏控制台窗口。
子進程創建后,是處於休眠狀態的。
3 使用CloseHandle關閉管道的寫句柄。這是必須的。父進程不使用這個句柄,如果存在多個寫句柄,那么管道不會工作。但是,不要在調用CreateProcess前關閉管道的寫句柄,否則管道就作廢了。你應該在CreateProcess調用返回后,從讀管道端讀取子進程的數據之前關閉管道。
4 使用ReadFile從讀管道端讀取數據。在你調用ReadFile后,子進程會開始運行。你可以把讀數據想象為用吸管喝飲料。你一直使用ReadFile讀數據,直到它返回0。對於讀來的數據,你想怎樣就怎樣,比如放進一個Edit控件。
5 最后,關閉讀管道句柄。
http://www.adintr.com/article/278
Windows管道(Pipe)重定向stdout,stderr,stdin
March 25, 2012, 1:50 p.m.
stdin是標准輸入,stdout是標准輸出,stderr是標准錯誤輸出。
大多數的命令行程序從stdin輸入,輸出到stdout或stderr,有時我們需要重定向stdout,stderr,stdin。比如:將輸出寫入文件,又或者我們要將命令行程序輸出結果顯示到Windows對話框中。
在Windows編程中,重定向需要用到管道(Pipe)的概念。管道是一種用於在進程間共享數據的機制。一個管道類似於一個管子的兩端,一端是寫入的,一端是讀出的。由一個進程從寫入端寫入、另一個進程從讀出端讀出,從而實現通信,就向一個“管道”一樣。
重定向的原理是:
首先聲明兩個概念:主程序(重定向的操縱者)、子進程(被重定向的子進程)
如果要重定位stdout的話,先生成一個管道, 管道的寫入端交給子進程去寫,主程序從管道的讀出端讀數據,然后可以把數據寫成文件、顯示等等。重定向stderr和stdout是相同的。
同理,要重定向stdin的話,生成一個管道, 管道的寫入端由主程序寫,子進程從管道的讀出端讀數據。
其中需要用到幾個Windows API : CreatePipe, DuplicateHandle, CreateProcess, ReadFile, WriteFile 等,函數詳解可參見MSDN.
一、編程實現原理 ( C語言)
- #include <windows.h>
- //定義句柄: 構成stdin管道的兩端句柄
- HANDLE hStdInRead; //子進程用的stdin的讀入端
- HANDLE hStdInWrite; //主程序用的stdin的讀入端
- //定義句柄: 構成stdout管道的兩端句柄
- HANDLE hStdOutRead; ///主程序用的stdout的讀入端
- HANDLE hStdOutWrite; ///子進程用的stdout的寫入端
- //定義句柄: 構成stderr管道的句柄,由於stderr一般等於stdout,我們沒有定義hStdErrRead,直接用hStdOutRead即可
- HANDLE hStdErrWrite; ///子進程用的stderr的寫入端
- //定義一個用於產生子進程的STARTUPINFO結構體 (定義見CreateProcess,函數說明)
- STARTUPINFO siStartInfo;
- //定義一個用於產生子進程的PROCESS_INFORMATION結構體 (定義見CreateProcess,函數說明)
- PROCESS_INFORMATION piProcInfo;
- //產生一個用於stdin的管道,得到兩個HANDLE: hStdInRead用於子進程讀出數據,hStdInWrite用於主程序寫入數據
- //其中saAttr是一個STARTUPINFO結構體,定義見CreatePipe函數說明
- if (!CreatePipe(&hStdInRead, &hStdInWrite,&saAttr, 0))
- return;
- //產生一個用於stdout的管道,得到兩個HANDLE: hStdInRead用於主程序讀出數據,hStdInWrite用於子程序寫入數據
- if (!CreatePipe(&hStdOutRead, &hStdOutWrite,&saAttr, 0))
- return;
- //由於stderr一般就是stdout, 直接復制句柄hStdOutWrite,得到 hStdErrWrite
- if (!DuplicateHandle(GetCurrentProcess(), hStdOutWrite, GetCurrentProcess(), &hStdErrWrite, 0, TRUE, DUPLICATE_SAME_ACCESS))
- return;
- //對STARTUPINFO結構體賦值,對stdin,stdout,stderr的Handle設置為剛才得到的管道HANDLE
- ZeroMemory( &siStartInfo, sizeof(STARTUPINFO) );
- siStartInfo.cb = sizeof(STARTUPINFO);
- siStartInfo.dwFlags |= STARTF_USESHOWWINDOW;
- siStartInfo.dwFlags |= STARTF_USESTDHANDLES;
- siStartInfo.hStdOutput = hStdOutWrite; //意思是:子進程的stdout輸出到hStdOutWrite
- siStartInfo.hStdError = hStdErrWrite; //意思是:子進程的stderr輸出到hStdErrWrite
- siStartInfo.hStdInput = hStdInRead;
- // 產生子進程,具體參數說明見CreateProcess函數
- bSuccess = CreateProcess(NULL,
- CommandLine, // 子進程的命令行
- NULL, // process security attributes
- NULL, // primary thread security attributes
- TRUE, // handles are inherited
- 0, // creation flags
- NULL, // use parent's environment
- NULL, // use parent's current directory
- &siStartInfo, // STARTUPINFO pointer
- &piProcInfo); // receives PROCESS_INFORMATION
- //如果失敗,退出
- if (!bSuccess ) return;
- //然后,就可以讀寫管道了
- //寫入stdin,具體代碼在一個WriteToPipe函數中
- WriteToPipe();
- //不斷子檢測進程有否結束
- while (GetExitCodeProcess(piProcInfo.hProcess,&process_exit_code))
- {
- //讀stdout,stderr
- ReadFromPipe();
- //如果子進程結束,退出循環
- if (process_exit_code!=STILL_ACTIVE) break;
- }
具體看一下WriteToPipe(), ReadFromPipe函數:
- //寫入stdin
- BOOL WriteToPipe()
- {
- DWORD dwWritten;
- BOOL bSuccess = FALSE;
- //用WriteFile,從hStdInWrite寫入數據,數據在in_buffer中,長度為dwSize
- bSuccess = WriteFile( hStdInWrite, in_buffer, dwSize, &dwWritten, NULL);
- return bSuccess;
- }
- // 讀出stdout
- BOOL ReadFromPipe()
- {
- char out_buffer[4096];
- DWORD dwRead;
- BOOL bSuccess = FALSE;
- //用WriteFile,從hStdOutRead讀出子進程stdout輸出的數據,數據結果在out_buffer中,長度為dwRead
- bSuccess = ReadFile( hStdOutRead, out_buffer, BUFSIZE, &dwRead, NULL);
- if ((bSuccess) && (dwRead!=0)) //如果成功了,且長度>0
- {
- // 此處加入你自己的代碼
- // 比如:將數據寫入文件或顯示到窗口中
- }
- return bSuccess;
- }
OK,到此原理寫完了。為簡化說明原理,上述代碼省略了出錯處理、結束處理(如:CloseHandle等),具體可以參見我的源碼。
二、封裝、實用的代碼
上述過程有些麻煩,實際使用中,我封裝成幾個函數:
首先定義三個回調函數 (就是函數指針類型)
//當stdin輸入時,調用此函數。將要寫的數據寫入buf中,*p_size寫為數據長度即可。
typedef void FuncIn(char *buf,int *p_size);
//當stdout,stderr輸出時,調用此函數。可讀取的數據在buf中,數據長度為size。
typedef void FuncOut(char *buf,int size);
//當子進程持續過程中,周期性調用此函數,設置p_abort可中斷子進程。
typedef void FuncProcessing(int *p_abort);
然后定義了四個函數
//執行一個命令行,重定向stdin, stdout,stderr。
//OnStdOut是回調函數指針,當有輸出時,OnStdOut被調用。
//OnStdIn是回調函數指針,當輸入時,OnStdIn被調用。
int ExecCommandEx(char *szCommandLine,char *CurrentDirectory,char *Environment,unsigned short ShowWindow,
FuncOut *OnStdOut,FuncProcessing *OnProcessing,FuncIn *OnStdIn);
//執行一個命令行,重定向stdout,stderr。
//OnLineOut是回調函數指針,當有一行輸出時,OnLineOut被調用。
int ExecCommandOutput(char *szCommandLine,char *Environment,unsigned short ShowWindow,
FuncOut *OnLineOut,FuncProcessing *OnProcessing);
//執行一個命令行,等待子進程結束,返回子進程的程序退出代碼。不處理重定向。
int ExecCommandWait(char *szCommandLine,unsigned short ShowWindow,FuncProcessing *OnProcessing);
//執行一個命令行,不等待子進程結束,即返回。不處理重定向。功能相當於 Windows API WinExec.
int ExecCommandNoWait(char *szCommandLine,unsigned short ShowWindow);
還定義了一個存儲數據的EXEC_INFO結構體及操作它的函數。
全部代碼為C語言,在JExecution.c, JExecution.h兩個文件中。只用到了Windows API,沒有用MFC及其他庫。
三、使用方法
有了JExecution.c,使用就很方便了。比如:
- #include <stdio.h>
- #include <windows.h>
- #include "JExecution.h"
- //定義一個處理輸出的回調函數
- void OnLineOut(char *buf,int size)
- {
- printf("%s\n", buf);
- }
- int main(int argc, char* argv[])
- {
- int n;
- char *command;
- command = "cmd.exe /r dir/w "; //這個命令的意思就是調用DOS,執行dir命令,顯示當前目前下的文件
- n=ExecCommandOutput(command,NULL,SW_HIDE,OnLineOut,NULL);
- printf("<Return:>%d",n);
- }