如何使用 CreateProcess() 和 CreatePipe() 从 cmd.exe 读取输出

新手上路,请多包涵

如何使用 CreateProcess() 和 CreatePipe() 从 cmd.exe 读取输出

我一直在尝试创建一个执行 cmd.exe 的子进程,命令行指定 /K dir 。目的是使用管道将命令的输出读回父进程。

我已经 CreateProcess() 工作,但是涉及管道的步骤给我带来了麻烦。使用管道,新的控制台窗口没有显示(就像以前一样),并且父进程卡在对 ReadFile() 的调用中。

有谁知道我做错了什么?

 #include <Windows.h>
#include <stdio.h>
#include <tchar.h>

#define BUFFSZ 4096

HANDLE g_hChildStd_IN_Rd = NULL;
HANDLE g_hChildStd_IN_Wr = NULL;
HANDLE g_hChildStd_OUT_Rd = NULL;
HANDLE g_hChildStd_OUT_Wr = NULL;

int wmain(int argc, wchar_t* argv[])
{
    int result;
    wchar_t aCmd[BUFFSZ] = TEXT("/K dir"); // CMD /?
    STARTUPINFO si;
    PROCESS_INFORMATION pi;
    SECURITY_ATTRIBUTES sa;

    printf("Starting...\n");

    ZeroMemory(&si, sizeof(STARTUPINFO));
    ZeroMemory(&pi, sizeof(PROCESS_INFORMATION));
    ZeroMemory(&sa, sizeof(SECURITY_ATTRIBUTES));

    // Create one-way pipe for child process STDOUT
    if (!CreatePipe(&g_hChildStd_OUT_Rd, &g_hChildStd_OUT_Wr, &sa, 0)) {
        printf("CreatePipe() error: %ld\n", GetLastError());
    }

    // Ensure read handle to pipe for STDOUT is not inherited
    if (!SetHandleInformation(g_hChildStd_OUT_Rd, HANDLE_FLAG_INHERIT, 0)) {
        printf("SetHandleInformation() error: %ld\n", GetLastError());
    }

    // Create one-way pipe for child process STDIN
    if (!CreatePipe(&g_hChildStd_IN_Rd, &g_hChildStd_IN_Wr, &sa, 0)) {
        printf("CreatePipe() error: %ld\n", GetLastError());
    }

    // Ensure write handle to pipe for STDIN is not inherited
    if (!SetHandleInformation(g_hChildStd_IN_Rd, HANDLE_FLAG_INHERIT, 0)) {
        printf("SetHandleInformation() error: %ld\n", GetLastError());
    }

    si.cb = sizeof(STARTUPINFO);
    si.hStdError = g_hChildStd_OUT_Wr;
    si.hStdOutput = g_hChildStd_OUT_Wr;
    si.hStdInput = g_hChildStd_IN_Rd;
    si.dwFlags |= STARTF_USESTDHANDLES;

    sa.nLength = sizeof(SECURITY_ATTRIBUTES);
    sa.lpSecurityDescriptor = NULL;
    // Pipe handles are inherited
    sa.bInheritHandle = true;

    // Creates a child process
    result = CreateProcess(
        TEXT("C:\\Windows\\System32\\cmd.exe"),     // Module
        aCmd,                                       // Command-line
        NULL,                                       // Process security attributes
        NULL,                                       // Primary thread security attributes
        true,                                       // Handles are inherited
        CREATE_NEW_CONSOLE,                         // Creation flags
        NULL,                                       // Environment (use parent)
        NULL,                                       // Current directory (use parent)
        &si,                                        // STARTUPINFO pointer
        &pi                                         // PROCESS_INFORMATION pointer
        );

    if (result) {
        printf("Child process has been created...\n");
    }
    else {
        printf("Child process could not be created\n");
    }

    bool bStatus;
    CHAR aBuf[BUFFSZ + 1];
    DWORD dwRead;
    DWORD dwWrite;
    // GetStdHandle(STD_OUTPUT_HANDLE)

    while (true) {
        bStatus = ReadFile(g_hChildStd_OUT_Rd, aBuf, sizeof(aBuf), &dwRead, NULL);
        if (!bStatus || dwRead == 0) {
            break;
        }
        aBuf[dwRead] = '\0';
        printf("%s\n", aBuf);
    }

        // Wait until child process exits
        WaitForSingleObject(pi.hProcess, INFINITE);

        // Close process and thread handles
        CloseHandle(pi.hProcess);
        CloseHandle(pi.hThread);

        printf("Stopping...\n");

        return 0;
    }

原文由 Shuzheng 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 2.3k
1 个回答

解决问题的微妙方法是确保关闭不需要的管道末端。

您的父进程有四个句柄:

  • 其中两个是 的管道末端
    • g_hChildStd_IN_Wr
    • g_hChildStd_OUT_Rd
  • 其中两个是 孩子的 管道末端
    • g_hChildStd_IN_Rd
    • g_hChildStd_OUT_Wr
 ╔══════════════════╗                ╔══════════════════╗
║  Parent Process  ║                ║  Child Process   ║
╠══════════════════╣                ╠══════════════════╣
║                  ║                ║                  ║
║ g_hChildStd_IN_Wr╟───────────────>║g_hChildStd_IN_Rd ║
║                  ║                ║                  ║
║g_hChildStd_OUT_Rd║<───────────────╢g_hChildStd_OUT_Wr║
║                  ║                ║                  ║
╚══════════════════╝                ╚══════════════════╝

您的父进程只需要每个管道的 一端

  • 子输入管道的可写端: g_hChildStd_IN_Wr
  • 子输出管道的可读端: g_hChildStd_OUT_Rd

启动子进程后:确保关闭不再需要的管道末端:

  • CloseHandle(g_hChildStd_IN_Rd)
  • CloseHandle(g_hChildStd_OUT_Wr)

离开:

 ╔══════════════════╗                ╔══════════════════╗
║  Parent Process  ║                ║  Child Process   ║
╠══════════════════╣                ╠══════════════════╣
║                  ║                ║                  ║
║ g_hChildStd_IN_Wr╟───────────────>║                  ║
║                  ║                ║                  ║
║g_hChildStd_OUT_Rd║<───────────────╢                  ║
║                  ║                ║                  ║
╚══════════════════╝                ╚══════════════════╝

或者更全面:

 STARTUP_INFO si;
PROCESS_INFO pi;
result = CreateProcess(..., ref si, ref pi);

//Bonus chatter: A common bug among a lot of programmers:
// they don't realize they are required to call CloseHandle
// on the two handles placed in PROCESS_INFO.
// That's why you should call ShellExecute - it closes them for you.
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);

/*
   We've given the console app the writable end of the pipe during CreateProcess; we don't need it anymore.
   We do keep the handle for the *readable* end of the pipe; as we still need to read from it.
   The other reason to close the writable-end handle now is so that there's only one out-standing reference to the writeable end: held by the child process.
   When the child processes closes, it will close the pipe, and
   your call to ReadFile will fail with error code:
      109 (The pipe has been ended).

   That's how we'll know the console app is done. (no need to wait on process handles with buggy infinite waits)
*/
CloseHandle(g_hChildStd_OUT_Wr);
g_hChildStd_OUT_Wr = 0;
CloseHandle(g_hChildStd_IN_Rd);
g_hChildStd_OUT_Wr = 0;

等待子进程(也就是等待发生的死锁)

大多数解决方案的常见问题是 人们试图 等待进程句柄。

  • 他们创建事件对象
  • 他们尝试向 MsgWait 发送事件信号
  • 他们尝试 MsgWait 让子进程结束

那是错误的。 那都是错的

这些想法有很多问题;主要的是:

  • 如果您尝试等待孩子终止
  • 孩子 永远 无法终止

如果孩子试图通过管道向您发送输出,而您正在 INFINITE 等待,那么您并没有清空管道的末端。最终,孩子正在写信的管道变满了。当孩子尝试写入已满的管道时,它的 WriteFile 调用 等待 块)管道有一些空间。

  • 你被阻止等待孩子
  • 孩子试图写入管道
  • 你被阻止等待孩子,所以你没有从管道中读取数据
  • 管子变满了
  • 孩子在等你
  • 父母和孩子都被阻止等待对方
  • 僵局

结果, 进程 永远不会 终止;你让一切都陷入僵局。

正确的方法 - 让客户做这件事

正确的解决方案是 简单地从管道中读取

  • 一旦子进程终止,
  • 它将 管道末端 CloseHandle
  • 下次您尝试从管道中读取
  • 您会被告知管道已关闭( ERROR_BROKEN_PIPE )。
  • 这就是您知道该过程已完成并且您没有更多要阅读的内容的方式。
 String outputText = "";

//Read will return when the buffer is full, or if the pipe on the other end has been broken
while (ReadFile(stdOutRead, aBuf, Length(aBuf), &bytesRead, null)
   outputText = outputText + Copy(aBuf, 1, bytesRead);

//ReadFile will either tell us that the pipe has closed, or give us an error
DWORD le = GetLastError;

//And finally cleanup
CloseHandle(g_hChildStd_IN_Wr);
CloseHandle(g_hChildStd_OUT_Rd);

if (le != ERROR_BROKEN_PIPE) //"The pipe has been ended."
   RaiseLastOSError(le);

所有这些都没有危险的 MsgWaitForSingleObject - 这很容易出错,难以正确使用,并且会导致您想要避免的错误。

完整示例

我们都知道我们使用它的目的:运行一个子进程,并捕获它的控制台输出。

这是一些示例 Delphi 代码:

 function ExecuteAndCaptureOutput(CommandLine: string): string;
var
    securityAttributes: TSecurityAttributes;
    stdOutRead, stdOutWrite: THandle;
    startupInfo: TStartupInfo;
    pi: TProcessInformation;
    buffer: AnsiString;
    bytesRead: DWORD;
    bRes: Boolean;
    le: DWORD;
begin
    {
        Execute a child process, and capture it's command line output.
    }
    Result := '';

    securityAttributes.nlength := SizeOf(TSecurityAttributes);
    securityAttributes.bInheritHandle := True;
    securityAttributes.lpSecurityDescriptor := nil;

    if not CreatePipe({var}stdOutRead, {var}stdOutWrite, @securityAttributes, 0) then
        RaiseLastOSError;
    try
        // Set up members of the STARTUPINFO structure.
        startupInfo := Default(TStartupInfo);
        startupInfo.cb := SizeOf(startupInfo);

        // This structure specifies the STDIN and STDOUT handles for redirection.
        startupInfo.dwFlags := startupInfo.dwFlags or STARTF_USESTDHANDLES; //The hStdInput, hStdOutput, and hStdError handles will be valid.
            startupInfo.hStdInput := GetStdHandle(STD_INPUT_HANDLE); //don't forget to make it valid (zero is not valid)
            startupInfo.hStdOutput := stdOutWrite; //give the console app the writable end of the pipe
            startupInfo.hStdError := stdOutWrite; //give the console app the writable end of the pipe

        // We also want the console window to be hidden
        startupInfo.dwFlags := startupInfo.dwFlags or STARTF_USESHOWWINDOW; //The nShowWindow member member will be valid.
            startupInfo.wShowWindow := SW_HIDE; //default is that the console window is visible

        // Set up members of the PROCESS_INFORMATION structure.
        pi := Default(TProcessInformation);

        //WARNING: The Unicode version of CreateProcess can modify the contents of CommandLine.
        //Therefore CommandLine cannot point to read-only memory.
        //We can ensure it's not read-only with the RTL function UniqueString
        UniqueString({var}CommandLine);

        bRes := CreateProcess(nil, PChar(CommandLine), nil, nil, True, 0, nil, nil, startupInfo, {var}pi);
        if not bRes then
            RaiseLastOSError;

        //CreateProcess demands that we close these two populated handles when we're done with them. We're done with them.
        CloseHandle(pi.hProcess);
        CloseHandle(pi.hThread);

        {
            We've given the console app the writable end of the pipe during CreateProcess; we don't need it anymore.
            We do keep the handle for the *readable* end of the pipe; as we still need to read from it.
            The other reason to close the writable-end handle now is so that there's only one out-standing reference to the writeable end: held by the console app.
            When the app closes, it will close the pipe, and ReadFile will return code 109 (The pipe has been ended).
            That's how we'll know the console app is done. (no need to wait on process handles)
        }
        CloseHandle(stdOutWrite);
        stdOutWrite := 0;

        SetLength(buffer, 4096);

        //Read will return when the buffer is full, or if the pipe on the other end has been broken
        while ReadFile(stdOutRead, buffer[1], Length(buffer), {var}bytesRead, nil) do
            Result := Result + string(Copy(buffer, 1, bytesRead));

        //ReadFile will either tell us that the pipe has closed, or give us an error
        le := GetLastError;
        if le <> ERROR_BROKEN_PIPE then //"The pipe has been ended."
            RaiseLastOSError(le);
    finally
        CloseHandle(stdOutRead);
        if stdOutWrite <> 0 then
            CloseHandle(stdOutWrite);
    end;
end;

原文由 Ian Boyd 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题