在Windows環境下的所謂shell程序就是dos命令行程序,比如VC的CL.exe命令行編譯器,JDK的javac編譯器,啟動java程序用的java.exe都是標准的shell程序。截獲一個shell程序的輸出是很有用的,比如說您可以自己編寫一個IDE(集成開發環境),當用戶發出編譯指令時候,你可以在後台啟動shell調用編譯器並截獲它們的輸出,對這些輸出信息進行分析後在更為友好的用戶界面上顯示出來。為了方便起見,我們用VB作為本文的演示語言。
通常,系統啟動Shell程序時缺省給定了3個I/O信道,標准輸入(stdin),標准輸出stdout,標准錯誤輸出stderr。之所以這麼區分是因為在早期的計算機系統如PDP-11的一些限制。那時沒有GUI,將輸出分為stdout,stderr可以避免程序的調試信息和正常輸出的信息混雜在一起。
通常,shell程序把它們的輸出寫入標准輸出管道(stdout)、把出錯信息寫入標准錯誤管道(stderr)。缺省情況下,系統將管道的輸出直接送到屏幕,這樣一來我們就能看到應用程序運行結果了。
為了捕獲一個標准控制台應用程序的輸出,我們必須把standOutput和standError管道輸出重定向到我們自定義的管道。
下面的代碼可以啟動一個shell程序,並將其輸出截獲。
'執行並返回一個命令行程序(shell程序)的標准輸出和標准錯誤輸出'通常命令行程序的所有輸出都直接送到屏幕上PrivateFunctionExecuteApp(sCmdlineAsString)AsStringDimprocAsPROCESS_INFORMATION,retAsLongDimstartAsSTARTUPINFODimsaAsSECURITY_ATTRIBUTESDimhReadPipeAsLong'負責讀取的管道DimhWritePipeAsLong'負責Shell程序的標准輸出和標准錯誤輸出的管道DimsOutputAsString'放返回的數據DimlngBytesReadAsLong,sBufferAsString*256sa.nLength=Len(sa)sa.bInheritHandle=Trueret=CreatePipe(hReadPipe,hWritePipe,sa,0)Ifret=0ThenMsgBox"CreatePipefailed.Error:"&Err.LastDllErrorExitFunctionEndIfstart.cb=Len(start)start.dwFlags=STARTF_USESTDHANDLESOrSTARTF_USESHOWWINDOW'把標准輸出和標准錯誤輸出重定向到同一個管道中去。start.hStdOutput=hWritePipestart.hStdError=hWritePipestart.wShowWindow=SW_HIDE'隱含shell程序窗口'啟動shell程序,sCmdLine指明執行的路徑ret=CreateProcessA(0&,sCmdline,sa,sa,True,NORMAL_PRIORITY_CLASS,_0&,0&,start,proc)Ifret=0ThenMsgBox"無法建立新進程,錯誤碼:"&Err.LastDllErrorExitFunctionEndIf'本例中不必向shell程序送信息,因此可以先關閉hWritePipeCloseHandlehWritePipe'循環讀取shell程序的輸出,每次讀取256個字節。Doret=ReadFile(hReadPipe,sBuffer,256,lngBytesRead,0&)sOutput=sOutput&Left$(sBuffer,lngBytesRead)LoopWhileret<>0'如果ret=0代表沒有更多的信息需要讀取了'釋放相關資源CloseHandleproc.hProcessCloseHandleproc.hThreadCloseHandlehReadPipeExecuteApp=sOutput'輸出結果EndFunction
我對這個程序進行一些解釋。
ret=CreatePipe(hReadPipe,hWritePipe,sa,0)
大家可以看到,首先我們建立一個匿名管道。該匿名管道稍候將用來取得與被截獲的應用程序的聯系。其中hReadPipe用來獲取shell程序的輸出,而hWritePipe可以用來向應用程序發送信息。如同現實世界中的水管一樣,水從管道的一端流進從另一端流出。您把水想象為信息,水管就是匿名管道,這樣一來就很好理解這段程序了。
然後就是設置shell應用程序的初始屬性。Dwflags可以指示系統在創建新進程時新進程使用了自定義的wShowWindow,hStdInput,hStdOutput和hStdError。(windows顯示屬性,標准輸入,標准輸出,標准錯誤輸出。)
再把shell應用程序的標准輸出和標准錯誤輸出都定向到我們預先建好的管道中。
代碼如下:
start.dwFlags=STARTF_USESTDHANDLESOrSTARTF_USESHOWWINDOW
start.hStdOutput=hWritePipe
start.hStdError=hWritePipe
好,現在可以調用建立新進程的函數了:
ret=CreateProcessA(0&,sCmdline,sa,sa,True,NORMAL_PRIORITY_CLASS,0&,0&,start,proc)
然後,循環讀管道裡的數據直到無數據可讀為止。
Do
ret=ReadFile(hReadPipe,sBuffer,256,lngBytesRead,0&)'每次讀256字節
sOutput=sOutput&Left$(sBuffer,lngBytesRead)'送入一個字符串中
LoopWhileret<>0'若ret=0表明沒有數據等待讀取。
然後,釋放不用的資源。
用法很簡單:比如:
MsgBoxExecuteApp("c:windowscommandmem.exe)
是很方便吧?
不過,這些程序是在NT下的,如果要在95下實現還需要一點點改動。因為如果該函數調用一個純win32的程序,沒問題。可是95是16,win32混合的系統,當你試圖調用一個16位的DOS應用程序那麼,那麼這個辦法會導致相關進程掛起。因為這涉及到WindowsNT和Windows95對shell的不同實現。
在win95中,16位shell程序關閉時並不保證重定向的管道也關閉,這樣,當你的程序試圖讀取一個已經關閉的shell程序的重定向管道時,你的程序就掛了。
那麼,有解決辦法嗎?回答是肯定的。
解決辦法就是用一個win32的應用程序作為您的應用程序和shell程序的中間人。中間人程序繼承並重定向了主程序的輸入輸出,然後中間人程序啟動指定的shell程序。該shell程序也就繼承並重定向了主程序的輸入輸出。中間人程序一直等到shell程序結束才結束。
當shell程序結束時,中間人程序也結束,同時因為中間人程序是一個win32程序,那麼它就會關閉相應的重定向了管道。這樣,你的程序可以發現管道已經關閉,便可以跳出循環。你的程序就不會掛起了。
下面是相關的中間人程序C代碼的實現:
#include#includevoidmain(intargc,char*argv[]){BOOLbRet=FALSE;STARTUPINFOsi={0};PROCESS_INFORMATIONpi={0};//Makechildprocessusethisapp'sstandardfiles.si.cb=sizeof(si);si.dwFlags=STARTF_USESTDHANDLES;si.hStdInput=GetStdHandle(STD_INPUT_HANDLE);si.hStdOutput=GetStdHandle(STD_OUTPUT_HANDLE);si.hStdError=GetStdHandle(STD_ERROR_HANDLE);bRet=CreateProcess(NULL,argv[1],NULL,NULL,TRUE,0,NULL,NULL,&si,&pi);if(bRet){WaitForSingleObject(pi.hProcess,INFINITE);CloseHandle(pi.hProcess);CloseHandle(pi.hThread);}}
把該程序編譯為conspawn.exe並放在系統可以調用到的路徑目錄中。
然後把文章開頭提到的代碼中的CreateProcessA語句改為:
ret=CreateProcessA(0&,"conspawn"""&sCmdline&"""",sa,sa,True,
NORMAL_PRIORITY_CLASS,0&,0&,start,proc)
好,這樣一來,我們這個函數可以同時很好的支持WindowsNT和Windows95/98了。->