重定向命令行程序的I/O

一、 原理

本文描述如何重定向命令行子进程的标准输入流、标准输出流、标准错误流。本文介绍的方式只对使用了标准输入、标准输出、标准错误流的命令行程序有用。

创建子进程时,我们一般使用CreateProcess,而CreateProcess提供的LPSTARTUPINFO参数允许我们改变进程的标准输入输出句柄。

CreateProcess函数定义如下:

BOOL WINAPI CreateProcess(
  LPCTSTR lpApplicationName,
  LPTSTR lpCommandLine,
  LPSECURITY_ATTRIBUTES lpProcessAttributes,
  LPSECURITY_ATTRIBUTES lpThreadAttributes,
  BOOL bInheritHandles,
  DWORD dwCreationFlags,
  LPVOID lpEnvironment,
  LPCTSTR lpCurrentDirectory,
  LPSTARTUPINFO lpStartupInfo,
  LPPROCESS_INFORMATION lpProcessInformation
);

参数LPSTARTUPINFO定义如下:

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;

我们需要将dwFlags设置为STARTF_USESTDHANDLES来表示hStdInput,hStdOutput,hStdError这3个成员是有效的,并且这3个值就将分别用来作为子命令行进程(必须是命令行进程)的标准输入流句柄、标准输出流句柄、标准错误流句柄。

hStdInput,hStdOutput,hStdError都是HANDLE类型,可以是任何类型的HANDLE,如文件、管道等,只需要满足2个条件:
1. 能够支持通过ReadFile, WriteFile函数进行读写。
2. 是可以被继承的(inheritable),句柄默认是不能被继承的,可以通过设置SECURITY_ATTRIBUTES安全属性来设置为可以被继承。

SECURITY_ATTRIBUTES sa;

sa.nLength = sizeof(SECURITY_ATTRIBUTES);
sa.lpSecurityDescriptor = NULL;
sa.bInheritHandle = TRUE;

我们也可以将hStdInput,hStdOutput,hStdError中的某些进行重新定向,某些维持原样。针对想维持原样的,通过调用GetStdHandle函数获取到指定类型的标准句柄设置上去即可,如:

hStdInput = GetStdHandle(STD_INPUT_HANDLE);

二、实现代码

函数ExcuteConcosle用于启动一个命令行程序,并且通过std_input传入用户输入的内容到标准输入流,std_output返回程序标准输出流内容,std_error返回程序标准错误流内容。

#include 
#include 

bool ExcuteConcosle(const std::string &cmd, const std::string &std_input, std::string &std_output, std::string &std_error) {
    HANDLE std_input_read = INVALID_HANDLE_VALUE;
    HANDLE std_input_write = INVALID_HANDLE_VALUE;

    HANDLE std_output_read = INVALID_HANDLE_VALUE;
    HANDLE std_output_write = INVALID_HANDLE_VALUE;

    HANDLE std_err_read = INVALID_HANDLE_VALUE;
    HANDLE std_err_write = INVALID_HANDLE_VALUE;

    bool ret = false;

    SECURITY_ATTRIBUTES sa;
    sa.nLength = sizeof(SECURITY_ATTRIBUTES);
    sa.lpSecurityDescriptor = NULL;
    sa.bInheritHandle = TRUE;

    do 
    {
        if (CreatePipe(&std_input_read, &std_input_write, &sa, 0) == 0) {
            break;
        }

        if (CreatePipe(&std_output_read, &std_output_write, &sa, 0) == 0) {
            break;
        }

        if (CreatePipe(&std_err_read, &std_err_write, &sa, 0) == 0) {
            break;
        }

        // 向标准输入管道写入用户输入内容
        //
        // TODO:目前只支持用户输入一次数据
        //
        DWORD dwWritten = 0;
        if (!WriteFile(std_input_write, std_input.c_str(), std_input.length(), &dwWritten, NULL) || dwWritten != std_input.length()) {
            break;
        }


        PROCESS_INFORMATION pi;
        STARTUPINFOA si;

        ZeroMemory(&si, sizeof(STARTUPINFO));
        si.cb = sizeof(STARTUPINFO);
        si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;
        si.wShowWindow = SW_HIDE; // 隐藏界面
        si.hStdInput = std_input_read;
        si.hStdOutput = std_output_write;
        si.hStdError = std_err_write;

        size_t cmd_len = cmd.length();
        char *cmd_buf = new char[cmd_len + 1];
        memcpy(cmd_buf, cmd.c_str(), cmd_len);
        cmd_buf[cmd_len] = 0;

        if (!CreateProcessA(NULL, cmd_buf, NULL, NULL, TRUE, NULL, NULL, NULL, &si, &pi)) {
            delete[]cmd_buf;
            break;
        }
        delete[] cmd_buf;


        // 等待子进程结束
        WaitForSingleObject(pi.hProcess, INFINITE);

        CloseHandle(pi.hThread);
        CloseHandle(pi.hProcess);


        // 开始读取管道中的内容
        // 在读取管道内容前先关闭的管道的写入句柄,这样在就可以通过ReadFile返回值结合ERROR_BROKEN_PIPE来判断读取是否结束
        //
        // TODO: 子进程可以能输出的内容较多,将管道的缓冲区填满,导致WriteFile一直阻塞,从而导致子进程无法退出,前面的WaitForSingleObject一直阻塞住
        // 解决方案:将管道内容的读取放到独立的线程中,在CreateProcess之前启动线程开始读取。
        //

        char lpBuffer[256];
        DWORD nBytesRead = 0;

        // 关闭标准输出管道的写入句柄,如果不关闭ReadFile会一直阻塞等待读取
        if (std_output_write != INVALID_HANDLE_VALUE) {
            CloseHandle(std_output_write);
            std_output_write = INVALID_HANDLE_VALUE;
        }

        while (true) {
            ZeroMemory(lpBuffer, 256);
            if (!ReadFile(std_output_read, lpBuffer, sizeof(lpBuffer) - 1,
                &nBytesRead, NULL) || !nBytesRead) {
                if (GetLastError() == ERROR_BROKEN_PIPE)
                    break; // pipe done - normal exit
            }

            std_output.append(lpBuffer, nBytesRead);
        }

        // 关闭标准错误管道的写入句柄,如果不关闭ReadFile会一直阻塞等待读取
        if (std_err_write != INVALID_HANDLE_VALUE) {
            CloseHandle(std_err_write);
            std_err_write = INVALID_HANDLE_VALUE;
        }

        while (true) {
            ZeroMemory(lpBuffer, 256);
            if (!ReadFile(std_err_read, lpBuffer, sizeof(lpBuffer) - 1,
                &nBytesRead, NULL) || !nBytesRead) {
                if (GetLastError() == ERROR_BROKEN_PIPE)
                    break; // pipe done - normal exit
            }

            std_error.append(lpBuffer, nBytesRead);
        }

        ret = true;
    } while (false);

    if (std_input_read != INVALID_HANDLE_VALUE)
        CloseHandle(std_input_read);

    if (std_input_write != INVALID_HANDLE_VALUE)
        CloseHandle(std_input_write);

    if (std_output_read != INVALID_HANDLE_VALUE)
        CloseHandle(std_output_read);

    if (std_output_write != INVALID_HANDLE_VALUE)
        CloseHandle(std_output_write);

    if (std_err_read != INVALID_HANDLE_VALUE)
        CloseHandle(std_err_read);

    if (std_err_write != INVALID_HANDLE_VALUE)
        CloseHandle(std_err_write);

    return ret;
}

用于测试的命令行程序如下(假设编译生成test.exe),该程序使用到了标准输入、输出、错误:

#include 
#include 

int main()
{
    std::string name;
    std::cin >> name;

    std::cout << "input name is " << name << std::endl;

    if (name != "jeff") {
        std::cerr << "name input error\n";
    }

    return 0;
}

在其他进程中调用ExcuteConcosle函数启动测试进程test.exe,传入输入内容,获取输出内容和错误内容:


int main()
{
    std::string std_input = "jim\n"; // 传入用户输入内容jim\n,“\n”模拟用户输入结束
    std::string std_output;
    std::string std_error;


    bool ret = ExcuteConcosle("D:\\test.exe", std_input, std_output, std_error);

    printf("标准输出:%s\n", std_output.c_str());

    printf("标准错误:%s\n", std_error.c_str());

    return 0;
}

执行结果如图:
这里写图片描述

参考:https://support.microsoft.com/zh-cn/help/190351/how-to-spawn-console-processes-with-redirected-standard-handles

你可能感兴趣的:(#,Windows核心编程)