在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:\windows\command\mem.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};//Makechildprocessusethisappsstandardfiles.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了。->