如何在父进程中读取子(外部)进程的标准输出和标准错误输出结果
? ? ? ? 最近接手一個小項目,要求使用谷歌的aapt.exe獲取apk軟件包中的信息。依稀記得去年年中時,有個同事也問過我如何獲取被調用進程的輸出結果,當時還研究了一番,只是沒有做整理。今天花點時間,將該方法整理成文。(轉載請指明出于breaksoftware的csdn博客)
? ? ? ? 在信息化非常發達的今天,可能已經過了江湖“武俠”草莽的時代。僅憑一己之力想完成驚人的創舉,可謂難上加難。于是社會分工越來越明確:你擅長寫驅動,你就去封裝個驅動出來;他擅長寫界面,就讓他寫套界面出來。如果你非常好心,可以將自己的研究成果開源,那么可能會有千萬人受益。如果你想保持神秘感,但是還是希望別人可以分享你的成果,你可能會將模塊封裝出來供別人使用。比如你提供了一個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;
粗看該結構體,我們可以知道:我們可以通過它控制子窗口出現的位置和大小還有顯示方式。但是細看下它最后三個參數:StdInput、StdOutput和StdError。這三個參數似乎就點中了標題中的兩個關鍵字“標準輸出”、“標準錯誤輸出”。是的!我們正是靠這幾個參數來解決我們所遇到的問題。那么如何使用這些參數呢?我們選用的還是老方法——管道。
BOOL ExecDosCmd(const 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;
}
? ? ? ? 這樣,我們就可以拿到子進程輸出結果并加以分析。我這兒簡單處理了下,就輸出來。也算善始善終吧。? ? ? ?附上工程。
總結
以上是生活随笔為你收集整理的如何在父进程中读取子(外部)进程的标准输出和标准错误输出结果的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: WMI技术介绍和应用——查询系统服务
- 下一篇: WMI技术介绍和应用——查询本地用户和组