如何在圖形界面中實時捕獲控制臺程序的標準輸出
[王詠剛]
IDE是集成開發環境(Integrated Development Environment)的簡稱。印象里有很多出色的IDE,比如JBuilder和Kylix,比如Visual Studio。不知大家是否留意過,大多數IDE本身只提供代碼編輯、工程管理等人機交互功能,我們在IDE中編譯代碼、調試程序時,IDE需要調用命令行的編譯器、調試器完成相應的操作。例如,使用Visual Studio編譯C++程序時,我們會在IDE下方的Output窗口中看到編譯和連接的全過程,雖然我們看不到彈出的DOS窗口,但實際上是IDE先后啟動了Microsoft C++編譯器cl.exe和連接器link.exe這兩個命令行程序,而cl.exe和link.exe的輸出又實時反映到了IDE的Output窗口中。還有,我們可以在Visual Studio中配置自己需要的工具程序(比如特殊的編譯器),然后讓Visual Studio在適當的時候運行這些工具,并將工具程序的輸出實時顯示到Output窗口中。下圖是我在Visual Studio 6.0的Output窗口中運行J2SDK的javac.exe編譯java源程序并顯示程序中語法錯誤的情形:
也就是說,大多數IDE工具都可以在集成環境中調用特定的命令行程序(WIN32里更確切的說法是控制臺程序),然后實時捕獲它們的輸出(這多半是輸出到標準的stdout和stderr流里的東西),并將捕獲到的信息顯示在圖形界面的窗口中。
這顯然是一種具備潛在價值的功能。利用這一技術,我們至少可以
1. 編寫出自己的IDE,如果我們有足夠的耐心的話;
2. 在我們自己的應用程序里嵌入全文檢索功能(調用Borland C++里的grep.exe工具),或者壓縮和解壓縮功能(調用控制臺方式的壓縮解壓程序,比如arj.exe、pkzip.exe等);
3. 連接其他人編寫的,或者我們自己很久以前編寫的控制臺程序——我經常因為難以調用一個功能強大但又沒有源碼的控制臺程序而苦惱萬分。
這樣好的功能是如何實現的呢?
首先,如果我們想做的是用一個控制臺程序調用另一個控制臺程序,那就再簡單不過了。我們只消把父進程的stdout重定向到某個匿名管道的WRITE端,然后啟動子進程,這時,子進程的stdout因為繼承的關系也連在了管道的WRITE端,子進程的所有標準輸出都寫入了管道,父進程則在管道的另一端隨時“偵聽”——這一技術叫做輸入輸出的重定向。
可現在的問題是,GUI方式的Windows程序根本沒有控制臺,沒有stdin、stdout之類的東西,子進程又是別人寫好的東西無法更改,這重定向該從何談起呢?
還有另外一招:我們可以直接在調用子進程時用命令行中的管道指令“>”將子進程的標準輸出重定向到一個文件,子進程運行完畢后再去讀取文件內容。這種方法當然可行,但它的問題是,我們很難實時監控子進程的輸出,如果子進程不是隨時刷新stdout的話,那我們只能等一整塊數據實際寫入文件之后才能看到運行結果;況且,訪問磁盤文件的開銷也遠比內存中的管道操作來得大。
我這里給出的方案其實很簡單:既然控制臺程序可以調用另一個控制臺程序并完成輸入輸出的重定向,那我們完全可以編寫一個中介程序,這個中介程序調用我們需要調用的工具程序并隨時獲取該程序的輸出信息,然后直接將信息用約定的進程間通訊方式(比如匿名管道)傳回GUI程序,就象下圖中這樣:
圖中,工具程序和中介程序都是以隱藏的方式運行的。工具程序原本輸出到stdout的信息被重定向到中介程序開辟的管道中,中介程序再利用GUI程序創建的管道將信息即時傳遞到GUI程序的一個后臺線程里,后臺線程負責刷新GUI程序的用戶界面(使用后臺線程的原因是,只有這樣才可以保證信息在GUI界面中隨時輸出時不影響用戶正在進行的其他操作,就象我們在Visual Studio中執行耗時較長的編譯功能那樣)。
我寫的中介程序名字叫wSpawn,這個名字來自Visual Studio里完成類似功能的中介程序VcSpawn(你可以在Visual Studio的安裝目錄中找到它)。我的wSpawn非常簡單,它利用系統調用_popen()同時完成創建子進程和輸入輸出重定向兩件工作。GUI程序則使用一種特殊的命令行方式調用wSpawn:
wspawn –h <n> <command> [arg1] [arg2] ...
其中,-h后跟的是GUI程序提供的管道句柄,由GUI程序自動將其轉換為十進制數字,wSpawn運行時將信息寫入該句柄中,隨后的內容是GUI程序真正要執行的命令行,例如調用C++編譯器cl.exe的方式大致如下:
wspawn –h 1903 cl /Id:\myInclude Test.cpp
wspawn.cpp的程序清單如下:
#include <stdlib.h> #include <stdio.h> #include <string.h> #include <string> #include <windows.h> using namespace std; void exit_friendly(void) { puts("請不要單獨運行wSpawn."); exit(0); } int main( int argc, char *argv[] ) { HANDLE hWrite = NULL; DWORD dwWrited; int i = 0, ret = 0, len = 0; char psBuffer[256]; FILE* child_output; string command_line = ""; // 檢查命令行,如存在管道句柄,則將其轉換為HANDLE類型 if (argc < 2) exit_friendly(); if (!stricmp(argv[1], "-h")) { if (argc < 4) exit_friendly(); hWrite = (HANDLE)atoi(argv[2]); i = 3; } else i = 1; // 提取要執行的命令 for (; i < argc; i++) { command_line += argv[i]; command_line += " "; } // 使用_popen創建子進程并重定向其標準輸出到文件指針中 if( (child_output = _popen( command_line.c_str(), "rt" )) == NULL ) exit( 1 ); while( !feof( child_output ) ) { if( fgets( psBuffer, 255, child_output ) != NULL ) { if (hWrite) { // 將子進程的標準輸出寫入管道,提供給自己的父進程 // 格式是先寫數據塊長度(0表示結束),再寫數據塊內容 len = strlen(psBuffer); WriteFile(hWrite, &len, sizeof(int), &dwWrited, NULL); WriteFile(hWrite, psBuffer, len, &dwWrited, NULL); } else // 如命令行未提供管道句柄,則直接打印輸出 printf(psBuffer); } } // 寫“0”表示所有數據都已寫完 len = 0; if (hWrite) WriteFile(hWrite, &len, sizeof(int), &dwWrited, NULL); return _pclose( child_output ); }
下面,我們就利用wSpawn程序,寫一個簡單的“IDE”工具。我們選擇Visual Studio 6.0作為開發環境(本文給出的代碼也在Visual Studio.NET 7.0中做過測試)。首先,創建Visual C++工程myIDE,工程類型為MFC AppWizard(EXE)中的Dialog based類型,即創建了一個主窗口為對話框的GUI程序。工程myIDE的主對話框類是CMyIDEDlg。現在我們要在資源編輯器中為主對話框添加一個足夠大的多行編輯框(Edit Box),它的控制ID是IDC_EDIT1,必須為IDC_EDIT1設置以下屬性:
Multiline, Horizontal scroll, Auto HScroll, Vertical scroll, Auto VScroll, Want return
然后用ClassWizard為IDC_EDIT1添加一個對應的成員變量(注意變量的類型要選CEdit型而非字符串CString型)
CEdit m_edit1;
使用ClassWizard為“確定”按鈕添加消息響應方法OnOK(),編輯該方法:
void CMyIDEDlg::OnOK() { AfxBeginThread(myThread, this); InvalidateRect(NULL); UpdateWindow(); }
也就是說,我們在“確定”按鈕按下時,啟動了后臺線程myThread(),那么,myThread()到底做了些什么呢?我們先在CMyIDEDlg類的頭文件myIDEDlg.h中加上一個成員函數聲明:
protected: static UINT myThread(LPVOID pParam);
然后,在CMyIDEDlg類的實現文件myIDEDlg.cpp里添加myThread()的實現代碼:
UINT CMyIDEDlg::myThread(LPVOID pParam) { PROCESS_INFORMATION pi; STARTUPINFO siStartInfo; SECURITY_ATTRIBUTES saAttr; CString Output, tmp; char command_line[200]; DWORD dwRead; char* buf; int len; HANDLE hRead, hWrite; CMyIDEDlg* pDlg = (CMyIDEDlg*)pParam; // 創建與wSpawn.exe通訊的可繼承的匿名管道 saAttr.nLength = sizeof(SECURITY_ATTRIBUTES); saAttr.bInheritHandle = TRUE; saAttr.lpSecurityDescriptor = NULL; if (!CreatePipe(&hRead, &hWrite, &saAttr, 0)) { AfxMessageBox("創建管道失敗"); return 0; } // 準備wSpawn的命令行,在命令行給出寫管道句柄和要wSpawn執行的命令 memset(&pi, 0, sizeof(pi)); sprintf(command_line, "wspawn -h %d cl /?", (unsigned int)hWrite); // 子進程以隱藏方式運行 ZeroMemory( &siStartInfo, sizeof(STARTUPINFO) ); siStartInfo.cb = sizeof(STARTUPINFO); siStartInfo.wShowWindow = SW_HIDE; siStartInfo.dwFlags = STARTF_USESHOWWINDOW; // 創建wSpawn子進程 if (!CreateProcess( NULL, command_line, NULL, NULL, TRUE, 0, NULL, NULL, &siStartInfo, &pi)) { AfxMessageBox("調用wSpawn時失敗"); return 0; } // 讀管道,并顯示wSpawn從管道中返回的輸出信息 if(!ReadFile( hRead, &len, sizeof(int), &dwRead, NULL) || dwRead == 0) return 0; while(len) { buf = new char[len + 1]; memset(buf, 0, len + 1); if(!ReadFile( hRead, buf, len, &dwRead, NULL) || dwRead == 0) return 0; // 將返回信息中的"\n"替換為Edit Box可識別的"\r\n" tmp = buf; tmp.Replace("\n", "\r\n"); Output += tmp; // 將結果顯示在Edit Box中,并刷新對話框 pDlg->m_edit1.SetWindowText(Output); pDlg->InvalidateRect(NULL); pDlg->UpdateWindow(); delete[] buf; if(!ReadFile( hRead, &len, sizeof(int), &dwRead, NULL) || dwRead == 0) return 0; } // 等待wSpawn結束 WaitForSingleObject(pi.hProcess, 30000); // 關閉管道句柄 CloseHandle(hRead); CloseHandle(hWrite); return 0; }
很簡單,不是嗎?后臺線程創建一個匿名管道,然后以隱藏方式啟動wSpawn.exe并將管道句柄通過命令行傳給wSpawn.exe,接下來只要從管道里讀取信息就可以了。現在我們可以試著編譯運行myIDE.exe了,記住要把myIDE.exe和wSpawn.exe放在同一目錄下。還有,我在myThread()函數中寫死了傳給wSpawn.exe的待執行的命令行是“cl /?”,這模擬了一次典型的編譯過程,如果你不打算改變這一行代碼的話,那一定要注意在你的計算機上,C++編譯器cl.exe必須位于環境變量PATH指明的路徑里,否則wSpawn.exe可就找不到cl.exe了。下面是myIDE程序的運行結果:
補充一點,上面給出的wSpawn利用_popen()完成子進程創建和輸入輸出重定向,這一方法雖然簡單,但只能重定向子進程的stdout或stdin,如果還需要重定向子進程的stderr的話(Java編譯器javac就利用stderr輸出結果信息),那我們就不能這么投機取巧了。根據以上討論,你一定可以使用傳統的_pipe()、_dup()等系統調用,寫出功能更完整的新版wSpawn來,我這里就不再羅嗦了。
[王詠剛,2002年5月]
補充:相反方向的信息傳遞
上面這篇文章在網上發布后,引起了一些反響。很多網友來信詢問這樣一個問題:上文中演示的是圖形界面程序實時捕獲控制臺程序的輸出;但有不少控制臺程序是交互式運行的(如ftp客戶端程序),需要人們在控制界面輸入特定的指令才能完成相應的功能——能不能用類似的辦法,讓圖形界面程序向控制臺程序輸入特定的命令行指令呢?
如果我們想輸入到控制臺程序的指令序列是固定的,那完全可以使用更簡單的辦法:把命令序列存儲在一個文本文件中,然后使用下面這樣的重定向指令運行控制臺程序:
foo.exe < commands.txt
但如果想輸入到控制臺的指令序列是由用戶在操作圖形界面程序時決定的,或是根據控制臺程序的輸出來決定的,我們就需要使用與上面文章中類似的管道法解決問題了。這一思路基本上和上文相同,只不過信息的傳遞方向顛倒了過來:圖形界面程序在需要時將指令序列作為字符串傳遞給中介程序,中介程序將該字符串寫入控制臺程序的標準輸入。
實現這種相反功能的中介程序proxy的代碼如下:
#include <stdlib.h> #include <stdio.h> #include <string.h> #include <string> #include <windows.h> using namespace std; void exit_friendly(void) { puts("請不要單獨運行proxy."); exit(0); } int main( int argc, char *argv[] ) { HANDLE hRead = NULL; DWORD dwReaded; int i = 0, ret = 0, len = 0; const int BUFFER_LEN = 256; char psBuffer[BUFFER_LEN]; FILE* child_input; string command_line = ""; // 檢查命令行,如存在管道句柄,則將其轉換為HANDLE類型 if (argc < 2) exit_friendly(); if (!stricmp(argv[1], "-h")) { if (argc < 4) exit_friendly(); hRead = (HANDLE)atoi(argv[2]); i = 3; } else i = 1; // 提取要執行的命令 for (; i < argc; i++) { command_line += argv[i]; command_line += " "; } // 使用_popen創建子進程并重定向其標準輸入 if( (child_input = _popen( command_line.c_str(), "wt" )) == NULL ) exit( 1 ); if (hRead) { while(1) { memset(psBuffer, 0, BUFFER_LEN); if (ReadFile(hRead, psBuffer, BUFFER_LEN, &dwReaded, NULL) && dwReaded > 0) { fputs(psBuffer, child_input); fflush(child_input); psBuffer[4] = 0; if (!stricmp(psBuffer, "quit")) break; } } } return _pclose( child_input ); }
圖形界面程序中,創建管道并啟動中介程序的示例代碼如下:
HANDLE hRead, hWrite; HANDLE hProcess; PROCESS_INFORMATION pi; STARTUPINFO siStartInfo; SECURITY_ATTRIBUTES saAttr; CString Output, tmp; char command_line[200]; // 創建與proxy.exe通訊的可繼承的匿名管道 saAttr.nLength = sizeof(SECURITY_ATTRIBUTES); saAttr.bInheritHandle = TRUE; saAttr.lpSecurityDescriptor = NULL; if (!CreatePipe(&hRead, &hWrite, &saAttr, 0)) { AfxMessageBox("創建管道失敗"); EndDialog(IDCANCEL); return FALSE; } // 準備proxy.exe的命令行,在命令行給出寫管道句柄和要proxy.exe執行的命令 memset(&pi, 0, sizeof(pi)); sprintf(command_line, "proxy -h %d ftp ...", (unsigned int)hRead); ZeroMemory( &siStartInfo, sizeof(STARTUPINFO) ); //siStartInfo.cb = sizeof(STARTUPINFO); //siStartInfo.wShowWindow = SW_HIDE; //siStartInfo.dwFlags = STARTF_USESHOWWINDOW; if (!CreateProcess( NULL, command_line, NULL, NULL, TRUE, 0, NULL, NULL, &siStartInfo, &pi)) { AfxMessageBox("調用proxy.exe時失敗"); EndDialog(IDCANCEL); return FALSE; } hProcess = pi.hProcess;
圖形界面程序中,向控制臺程序發送某特定命令的示例代碼如下:
char command = "help"; DWORD dwWritten; WriteFile(hWrite, command, strlen(command), &dwWritten, NULL);
顯然,利用這兩種方向的管道,我們很容易為一個純控制臺界面的程序加上一層圖形用戶界面的漂亮外殼。
google_ad_client = "pub-2416224910262877"; google_ad_width = 728; google_ad_height = 90; google_ad_format = "728x90_as"; google_ad_channel = ""; google_color_border = "E1771E"; google_color_bg = "FFFFFF"; google_color_link = "0000FF"; google_color_text = "000000"; google_color_url = "008000";
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
