前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >如何在父进程中读取子(外部)进程的标准输出和标准错误输出结果

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

作者头像
方亮
发布2019-01-16 16:05:59
3.7K0
发布2019-01-16 16:05:59
举报
文章被收录于专栏:方亮方亮

      最近接手一个小项目,要求使用谷歌的aapt.exe获取apk软件包中的信息。依稀记得去年年中时,有个同事也问过我如何获取被调用进程的输出结果,当时还研究了一番,只是没有做整理。今天花点时间,将该方法整理成文。(转载请指明出于breaksoftware的csdn博客)

        在信息化非常发达的今天,可能已经过了江湖“武侠”草莽的时代。仅凭一己之力想完成惊人的创举,可谓难上加难。于是社会分工越来越明确:你擅长写驱动,你就去封装个驱动出来;他擅长写界面,就让他写套界面出来。如果你非常好心,可以将自己的研究成果开源,那么可能会有千万人受益。如果你想保持神秘感,但是还是希望别人可以分享你的成果,你可能会将模块封装出来供别人使用。比如你提供了一个DLL文件和调用方法样例。但是,实际情况并不是我们想的那么简单。比如我文前提到的问题:别人提供了一个Console控制台程序,我们将如何获取其执行的输出结果呢?这个问题,从微软以为为我们考虑过了,我们可以从一个API中可以找到一些端倪——CreateProcess

代码语言:javascript
复制
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

代码语言:javascript
复制
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。这三个参数似乎就点中了标题中的两个关键字“标准输出”、“标准错误输出”。是的!我们正是靠这几个参数来解决我们所遇到的问题。那么如何使用这些参数呢?

        我们选用的还是老方法——管道。

代码语言:javascript
复制
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,这样我们获取的两个操作管道的句柄就有可继承属性。为什么需要可继承属性,我们会在之后说明。

        创建好管道后,我们将着手准备创建进程

代码语言:javascript
复制
    // 组装命令
    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命令的说明如下:
  • 设置标准输出和标准错误输出句柄
代码语言:javascript
复制
    si.hStdError = hWrite;            // 把创建进程的标准错误输出重定向到管道输入
    si.hStdOutput = hWrite;           // 把创建进程的标准输出重定向到管道输入
  • 隐藏CMD控制台
代码语言:javascript
复制
    si.wShowWindow = SW_HIDE;
  • 设置有效属性
代码语言:javascript
复制
    si.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;

        这两个有效属性要设置。我们设置STARTF_USESHOWWINDOW的原因是:我们要控制CMD窗口不出现,所以我们修改了wShowWindow属性。我们使用STARTF_USESTDHANDLES的原因是:我们使用了标准输出和标准错误输出句柄。此处我们还要特别将一下STARTF_USESTDHANDLES属性的说明,我们看MSDN有如下描述

代码语言:javascript
复制
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的原因。         一般来说,我们要代理的程序已经输入好信息了。我们要关闭写管道

代码语言:javascript
复制
    if ( NULL != hWrite ) {
        CloseHandle(hWrite);
        hWrite = NULL;
    }

        之后便是读取管道信息。我想应该有人借用过网上相似的代码,但是却发现一个问题,就是读取出来的信息是不全的。这个问题的关键就在读取的方法上,其实没什么玄妙,只要控制好读取起始位置就行了。

代码语言:javascript
复制
    // 先分配读取的数据空间
    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);

        因为读取的信息量是不确定的,所以我段代码动态申请了一段内存,并根据实际读取出来的结果动态调整这块内存的大小。这段注释写的很清楚了,我就不再赘述。         善始善终,最后代码处理是

代码语言:javascript
复制
    if ( NULL != hRead ) {
        CloseHandle(hRead);
        hRead = NULL;
    }

    if ( bSuc ) {
        *ppBuffer = pchReadBuffer;
    }
    else {
        delete [] pchReadBuffer;
        pchReadBuffer = NULL;
    }

    return bSuc;
}

        这个函数传入了一个指向指针的指针用于外部获取结果,外部一定要释放这段空间以免造成内存泄露。

代码语言:javascript
复制
#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;
}

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

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2013年02月20日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档