如何在父程序中讀取子(外部)程序的標準輸出和標準錯誤輸出結果
最近接手一個小專案,要求使用谷歌的aapt.exe獲取apk軟體包中的資訊。依稀記得去年年中時,有個同事也問過我如何獲取被呼叫程序的輸出結果,當時還研究了一番,只是沒有做整理。今天花點時間,將該方法整理成文。(轉載請指明出於breaksoftware的csdn部落格)
在資訊化非常發達的今天,可能已經過了江湖“武俠”草莽的時代。僅憑一己之力想完成驚人的創舉,可謂難上加難。於是社會分工越來越明確:你擅長寫驅動,你就去封裝個驅動出來;他擅長寫介面,就讓他寫套介面出來。如果你非常好心,可以將自己的研究成果開源,那麼可能會有千萬人受益。如果你想保持神祕感,但是還是希望別人可以分享你的成果,你可能會將模組封裝出來供別人使用。比如你提供了一個DLL檔案和呼叫方法樣例。但是,實際情況並不是我們想的那麼簡單。比如我文前提到的問題:別人提供了一個Console控制檯程式,我們將如何獲取其執行的輸出結果呢?這個問題,從微軟以為為我們考慮過了,我們可以從一個API中可以找到一些端倪——
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;
}
這樣,我們就可以拿到子程序輸出結果並加以分析。我這兒簡單處理了下,就輸出來。也算善始善終吧。附上工程。