如何在父进程中读取子(外部)进程的标准输出和标准错误输出结果

时间:2022-06-17
本文章向大家介绍如何在父进程中读取子(外部)进程的标准输出和标准错误输出结果,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

        最近接手一个小项目,要求使用谷歌的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;
}

        这样,我们就可以拿到子进程输出结果并加以分析。我这儿简单处理了下,就输出来。也算善始善终吧。 附上工程。