Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
912 views
in Technique[技术] by (71.8m points)

c++ - How to read output from cmd.exe using CreateProcess() and CreatePipe()

How to read output from cmd.exe using CreateProcess() and CreatePipe()

I have been trying to create a child process executing cmd.exe with a command-line designating /K dir. The purpose is to read the output from the command back into the parent process using pipes.

I've already got CreateProcess() working, however the step involving pipes are causing me trouble. Using pipes, the new console window is not displaying (like it was before), and the parent process is stuck in the call to ReadFile().

Does anyone have an idea of what I'm doing wrong?

#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...
");

    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
", 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
", 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
", 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
", 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...
");
    }
    else {
        printf("Child process could not be created
");
    }

    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] = '';
        printf("%s
", aBuf);
    }

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

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

        printf("Stopping...
");

        return 0;
    }
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

The subtle way out of your problem is to make sure you close the ends of the pipe you don't need.

Your parent process has four handles:

  • two of them are your ends of the pipe
    • g_hChildStd_IN_Wr
    • g_hChildStd_OUT_Rd
  • two of them are the child's end of the pipe
    • 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║
║                  ║                ║                  ║
╚══════════════════╝                ╚══════════════════╝

Your parent process only needs one end of each pipe:

  • writable end of the child input pipe: g_hChildStd_IN_Wr
  • readable end of the child output pipe: g_hChildStd_OUT_Rd

Once you've launched your child process: be sure to close those ends of the pipe you no longer need:

  • CloseHandle(g_hChildStd_IN_Rd)
  • CloseHandle(g_hChildStd_OUT_Wr)

Leaving:

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

Or more fully:

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;

Waiting on the Child Process (aka the deadlock waiting to happen)

The common problem with most solutions is that people try to wait on a process handle.

  • they create event objects
  • they try to MsgWait for events to be signaled
  • they try to MsgWait for child processes to end

That's wrong. That's all wrong.

There are many problems with these ideas; the main one being:

  • if you try to wait for the child the terminate
  • the child will never be able to terminate

If the child is trying to send you output through the pipe, and you're INFINITE waiting, you're not emptying your end of the pipe. Eventually the pipe the child is writing to becomes full. When the child tries to write to a pipe that is full, its WriteFile call waits for the pipe to have some room.

As a result the child process will never terminate; you've deadlocked everything.

The Right Approach - let the client do it's thing

The correct solution comes by simply reading from the pipe.

  • Once the child process terminates,
  • it will CloseHandle on its end of the pipes.
  • The next time you try to read from the pipe
  • you'll be told the pipe has been closed (ERROR_BROKEN_PIPE).
  • That's how you know the process is done and you have no more stuff to read.

?

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);

All without a dangerous MsgWaitForSingleObject - which is error-prone, difficult to use correctly, and causes the very bug you want to avoid.

Complete Example

We all know what we are using this for: run a child process, and capture it's console output.

Here is some sample Delphi code:

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;

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...