怎么在父进程中读取子(外部)进程的标准输出和标准异常输出结果
最近接手一个小项目,要求使用谷歌的aapt.exe获取apk软件包中的信息。依稀记得去年年中时,有个同事也问过我如何获取被调用进程的输出结果,当时还研究了一番,只是没有做整理。今天花点时间,将该方法整理成文。(转载请指明出于breaksoftware的****博客)
在信息化非常发达的今天,可能已经过了江湖“武侠”草莽的时代。仅凭一己之力想完成惊人的创举,可谓难上加难。于是社会分工越来越明确:你擅长写驱动,你就去封装个驱动出来;他擅长写界面,就让他写套界面出来。如果你非常好心,可以将自己的研究成果开源,那么可能会有千万人受益。如果你想保持神秘感,但是还是希望别人可以分享你的成果,你可能会将模块封装出来供别人使用。比如你提供了一个DLL文件和调用方法样例。但是,实际情况并不是我们想的那么简单。比如我文前提到的问题:别人提供了一个Console控制台程序,我们将如何获取其执行的输出结果呢?这个问题,从微软以为为我们考虑过了,我们可以从一个API中可以找到一些端倪——CreateProcess。
BOOL WINAPI CreateProcess( _In_opt_ LPCTSTR lpApplicationName, _Inout_opt_ LPTSTR lpCommandLine, _In_opt_ LPSECURITY_ATTRIBUTES lpProcessAttributes, _In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes, _In_ BOOL bInheritHandles, _In_ DWORD dwCreationFlags, _In_opt_ LPVOID lpEnvironment, _In_opt_ LPCTSTR lpCurrentDirectory, _In_ LPSTARTUPINFO lpStartupInfo, _Out_ LPPROCESS_INFORMATION lpProcessInformation );
做Windows开发的同学对CreateProcess这个API应该非常眼熟,也应该经常调用过。但是仔细研究过这个API每个参数的同学应该不会太多吧。这个API的参数非常多,我想我们工程中对CreateProcess的调用可能就关注于程序路径(lpApplicationName),或者命令行(lpCommandLine)。而其他参数我们可能就保守的选择了NULL。(遥想2年前,我就是在这个API上栽了一个大大的跟头。)
本文,我们将关注一个可能很少使用的参数lpStartupInfo。它是我们启动子进程时,控制子进程启动方式的参数。其结构体是STARTUPINFO
typedef struct _STARTUPINFO { DWORD cb; LPTSTR lpReserved; LPTSTR lpDesktop; LPTSTR lpTitle; DWORD dwX; DWORD dwY; DWORD dwXSize; DWORD dwYSize; DWORD dwXCountChars; DWORD dwYCountChars; DWORD dwFillAttribute; DWORD dwFlags; WORD wShowWindow; WORD cbReserved2; LPBYTE lpReserved2; HANDLE hStdInput; HANDLE hStdOutput; HANDLE hStdError; } STARTUPINFO, *LPSTARTUPINFO;
BOOL ExecDosCmd(CString cstrCmd, char** ppBuffer) { HANDLE hRead = NULL; HANDLE hWrite = NULL; SECURITY_ATTRIBUTES sa; sa.nLength = sizeof(SECURITY_ATTRIBUTES); sa.lpSecurityDescriptor = NULL; // 新创建的进程继承管道读写句柄 sa.bInheritHandle = TRUE; if ( FALSE == CreatePipe( &hRead, &hWrite, &sa, 0 ) ) { return FALSE; } if ( NULL == hRead || NULL == hWrite ) { return FALSE; }这儿我们创建一个管道,该管道提供两个句柄:hRead和hWrite。我们之后将hWrite交给我们创建的子进程,让它去将信息写入管道。而我们父进程,则使用hRead去读取子进程写入管道的内容。此处要注意的就是将SECURITY_ATTRIBUTES对象的bInheritHandle设置为TRUE,这样我们获取的两个操作管道的句柄就有可继承属性。为什么需要可继承属性,我们会在之后说明。
// 组装命令 CString cstrNewDosCmd = L"Cmd.exe /C "; cstrNewDosCmd += cstrCmd; // 设置启动程序属性,将 STARTUPINFO si; si.cb = sizeof(STARTUPINFO); GetStartupInfo(&si); si.hStdError = hWrite; // 把创建进程的标准错误输出重定向到管道输入 si.hStdOutput = hWrite; // 把创建进程的标准输出重定向到管道输入 si.wShowWindow = SW_HIDE; // STARTF_USESHOWWINDOW:The wShowWindow member contains additional information. // STARTF_USESTDHANDLES:The hStdInput, hStdOutput, and hStdError members contain additional information. si.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES; PROCESS_INFORMATION pi; // 启动进程 BOOL bSuc = CreateProcess(NULL, cstrNewDosCmd.GetBuffer(), NULL, NULL, TRUE, NULL, NULL, NULL, &si, &pi); cstrNewDosCmd.ReleaseBuffer();此处我们要注意几个点:
- “Cmd..exe /C” 我们使用CMD运行我们代理的程序。注意,我们启动的是CMD,而不是我们传入的文件路径。关于CMD命令的说明如下:
- 设置标准输出和标准错误输出句柄
si.hStdError = hWrite; // 把创建进程的标准错误输出重定向到管道输入 si.hStdOutput = hWrite; // 把创建进程的标准输出重定向到管道输入
- 隐藏CMD控制台
si.wShowWindow = SW_HIDE;
- 设置有效属性
si.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;这两个有效属性要设置。我们设置STARTF_USESHOWWINDOW的原因是:我们要控制CMD窗口不出现,所以我们修改了wShowWindow属性。我们使用STARTF_USESTDHANDLES的原因是:我们使用了标准输出和标准错误输出句柄。此处我们还要特别将一下STARTF_USESTDHANDLES属性的说明,我们看MSDN有如下描述
If this flag is specified when calling one of the process creation functions, the handles must be inheritable and the function's bInheritHandles parameter must be set to TRUE.也就是说,我们设置的这些句柄要有可继承性。这就解释了我们之前为什么在创建管道时要将句柄可继承性设置为TRUE的原因。
一般来说,我们要代理的程序已经输入好信息了。我们要关闭写管道
if ( NULL != hWrite ) { CloseHandle(hWrite); hWrite = NULL; }之后便是读取管道信息。我想应该有人借用过网上相似的代码,但是却发现一个问题,就是读取出来的信息是不全的。这个问题的关键就在读取的方法上,其实没什么玄妙,只要控制好读取起始位置就行了。
// 先分配读取的数据空间 DWORD dwTotalSize = NEWBUFFERSIZE; // 总空间 char* pchReadBuffer = new char[dwTotalSize]; memset(pchReadBuffer, 0, NEWBUFFERSIZE); DWORD dwFreeSize = dwTotalSize; // 闲置空间 do { if ( FALSE == bSuc ) { break; } // 重置成功标志,之后要视读取是否成功来决定 bSuc = FALSE; char chTmpReadBuffer[NEWBUFFERSIZE] = {0}; DWORD dwbytesRead = 0; // 用于控制读取偏移 OVERLAPPED Overlapped; memset(&Overlapped, 0, sizeof(OVERLAPPED) ); while (true) { // 清空缓存 memset(chTmpReadBuffer, 0, NEWBUFFERSIZE); // 读取管道 BOOL bRead = ReadFile( hRead, chTmpReadBuffer, NEWBUFFERSIZE, &dwbytesRead, &Overlapped ); DWORD dwLastError = GetLastError(); if ( bRead ) { if ( dwFreeSize >= dwbytesRead ) { // 空闲空间足够的情况下,将读取的信息拷贝到剩下的空间中 memcpy_s( pchReadBuffer + Overlapped.Offset, dwFreeSize, chTmpReadBuffer, dwbytesRead ); // 重新计算新空间的空闲空间 dwFreeSize -= dwbytesRead; } else { // 计算要申请的空间大小 DWORD dwAddSize = ( 1 + dwbytesRead / NEWBUFFERSIZE ) * NEWBUFFERSIZE; // 计算新空间大小 DWORD dwNewTotalSize = dwTotalSize + dwAddSize; // 计算新空间的空闲大小 dwFreeSize += dwAddSize; // 新分配合适大小的空间 char* pTempBuffer = new char[dwNewTotalSize]; // 清空新分配的空间 memset( pTempBuffer, 0, dwNewTotalSize ); // 将原空间数据拷贝过来 memcpy_s( pTempBuffer, dwNewTotalSize, pchReadBuffer, dwTotalSize ); // 保存新的空间大小 dwTotalSize = dwNewTotalSize; // 将读取的信息保存到新的空间中 memcpy_s( pTempBuffer + Overlapped.Offset, dwFreeSize, chTmpReadBuffer, dwbytesRead ); // 重新计算新空间的空闲空间 dwFreeSize -= dwbytesRead; // 将原空间释放掉 delete [] pchReadBuffer; // 将原空间指针指向新空间地址 pchReadBuffer = pTempBuffer; } // 读取成功,则继续读取,设置偏移 Overlapped.Offset += dwbytesRead; } else{ if ( ERROR_BROKEN_PIPE == dwLastError ) { bSuc = TRUE; } break; } } } while (0);因为读取的信息量是不确定的,所以我段代码动态申请了一段内存,并根据实际读取出来的结果动态调整这块内存的大小。这段注释写的很清楚了,我就不再赘述。
善始善终,最后代码处理是
if ( NULL != hRead ) { CloseHandle(hRead); hRead = NULL; } if ( bSuc ) { *ppBuffer = pchReadBuffer; } else { delete [] pchReadBuffer; pchReadBuffer = NULL; } return bSuc; }这个函数传入了一个指向指针的指针用于外部获取结果,外部一定要释放这段空间以免造成内存泄露。
#define NEWBUFFERSIZE 0x100 #define EXECDOSCMD L"aapt.exe" int _tmain(int argc, _TCHAR* argv[]) { char* pBuffer = NULL; WCHAR wchFilePath[MAX_PATH] = {0}; DWORD dwSize = MAX_PATH - 1; if ( FALSE == GetModuleFileName(NULL, wchFilePath, dwSize) ) { return -1; } CString cstrFilePath = wchFilePath; int nIndex = cstrFilePath.ReverseFind('\\'); if ( nIndex == -1 ) { return -1; } cstrFilePath = cstrFilePath.Left(nIndex + 1); cstrFilePath += EXECDOSCMD; cstrFilePath += L"\""; cstrFilePath = L"\"" + cstrFilePath; if ( ExecDosCmd( cstrFilePath, &pBuffer ) && NULL != pBuffer ) { CString cstrBuffer = CA2W(pBuffer, CP_UTF8); delete [] pBuffer; wprintf(L"%s", cstrBuffer); } return 0; }这样,我们就可以拿到子进程输出结果并加以分析。我这儿简单处理了下,就输出来。也算善始善终吧。
附上工程。