Search code examples
cwinapiprocesspipecreateprocess

Weird behaviour when passing grandchild output of CreateProcess to the grandparent through the child


I am working on an application A which calls CreateProcess to run a child process B, which in turn calls createProcess to run a child process C. The output of C should be passed through by B and then read by A. When I run B myself (outside of A), it works fine and the output of C is shown on the console. But when I run A, it gets to see all of B's output except what has been passed on from C. Apparently, I'm hitting a special case for grandchildren or something like that.

To experiment, I created a simple example test.c, which takes a digit as its argument. It outputs the letter corresponding to the digit (a for 1, etc.). It then starts the same program recursively using CreateProcess, but decreases the argument. It passes all output from the child in the format <N: OUTPUT>, where N is the digit argument.

I expect to see the following results:

> .\test.exe 1
a
> .\test.exe 2
b<2: a>
> .\test.exe 3
c<3: b><3: <2: a>>

I realize that due to buffering the last output may also be something like c<3: b<2: a>> --- the important thing is that the output a is nested in a <2:> block and that the <2:> block is nested in a <3:> block: this shows that the output of the grandchild is first received by the child and then passed on to the parent.

My program behaves correctly for arguments 1 and 2, but for 3 I consistently get this output:

c<3: a><3: b<2: >>

This does not make sense to me at all. It is as if the output from the grandchild is received directly by the topmost process, because we get <3: a>. And I don't know how to interpret <3: b<2: >> either.

How do I fix this example so that the output of the grandchild is first seen by the child and then passed on to the parent?

#include <stdio.h> 
#include <windows.h> 

static counter; /* the digit argument */
static HANDLE stdout_read;

static void error (char *s)
{
    fprintf (stderr,"error: %s\n",s);
    exit (1);
}

/* Thread function to pass through output */
static DWORD WINAPI copy_out_func (LPVOID unused)
{
    CHAR buffer[512];

    for (;;) {
        DWORD n_bytes;

        if (!ReadFile (stdout_read,buffer,sizeof (buffer),&n_bytes,NULL) || n_bytes==0)
            break;
        printf ("<%d: ",counter);
        WriteFile (GetStdHandle (STD_OUTPUT_HANDLE),buffer,n_bytes,&n_bytes,NULL);
        printf (">");
    }

    return 1;
}

int main(int argc, TCHAR *argv[]) 
{
    SECURITY_ATTRIBUTES sa;
    HANDLE stdout_write,copy_out_thread;
    char cmd[64]=".\\test.exe";
    PROCESS_INFORMATION pi; 
    STARTUPINFO si;
    BOOL bSuccess = FALSE; 

    if (argc != 2)
        error ("Usage: .\\test.exe N");

    counter = argv[1][0]-'0';

    if (counter <= 0)
        return 0; /* end recursion */

    printf ("%c",'a'+counter-1);

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

    if (!CreatePipe (&stdout_read,&stdout_write,&sa,0))
        error ("CreatePipe");
    if (!SetHandleInformation (stdout_read,HANDLE_FLAG_INHERIT,0))
        error ("SetHandleInformation");
 
    ZeroMemory (&pi,sizeof (PROCESS_INFORMATION));
 
    ZeroMemory (&si,sizeof (STARTUPINFO));
    si.cb = sizeof (STARTUPINFO); 
    si.hStdOutput = stdout_write;
    si.hStdError = stdout_write;
    si.hStdInput = GetStdHandle (STD_INPUT_HANDLE);
    si.dwFlags |= STARTF_USESTDHANDLES;

    cmd[10]=' ';
    cmd[11]='0' + (counter-1);
    cmd[12]='\0';
     
    if (!CreateProcess (NULL,cmd,NULL,NULL,TRUE,0,NULL,NULL,&si,&pi))
        error ("CreateProcess");

    CloseHandle (stdout_write);
    copy_out_thread = CreateThread (NULL,0,copy_out_func,NULL,0,NULL);

    CloseHandle (pi.hThread);

    WaitForSingleObject (pi.hProcess,INFINITE);
    WaitForSingleObject (copy_out_thread,INFINITE);

    return 0;
}

I thought this could be because some handles of the grandparent were incorrectly inherited by the grandchild. So I created a somewhat longer example using STARTUPINFOEX with a LPPROC_THREAD_ATTRIBUTE_LIST to indicate that only two streams should be inherited. This program has the same faulty behaviour:

#include <stdio.h> 
#include <windows.h> 

static counter;
static HANDLE stdout_read;

static void error (char *s)
{
    fprintf (stderr,"error: %s (%d)\n",s,GetLastError());
    exit (1);
}

static DWORD WINAPI copy_out_func (LPVOID unused)
{
    CHAR buffer[512];

    for (;;) {
        DWORD n_bytes;

        if (!ReadFile (stdout_read,buffer,sizeof (buffer),&n_bytes,NULL) || n_bytes==0)
            break;
        printf ("<%d: ",counter);
        WriteFile (GetStdHandle (STD_OUTPUT_HANDLE),buffer,n_bytes,&n_bytes,NULL);
        printf (">");
    }

    return 1;
}

int main(int argc, TCHAR *argv[]) 
{
    SECURITY_ATTRIBUTES sa;
    HANDLE stdout_write,copy_out_thread;
    char cmd[64]=".\\test.exe";
    PROCESS_INFORMATION pi; 
    SIZE_T size;
    LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList;
    HANDLE handlesToInherit[3];
    STARTUPINFOEX si;

    if (argc != 2)
        error ("Usage: .\\test.exe N");

    counter = argv[1][0]-'0';

    if (counter <= 0)
        return 0;

    printf ("%c",'a'+counter-1);

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

    if (!CreatePipe (&stdout_read,&stdout_write,&sa,0))
        error ("CreatePipe");
    if (!SetHandleInformation (stdout_read,HANDLE_FLAG_INHERIT,0))
        error ("SetHandleInformation");
 
    ZeroMemory (&pi,sizeof (PROCESS_INFORMATION));

    handlesToInherit[0] = GetStdHandle (STD_INPUT_HANDLE);
    handlesToInherit[1] = stdout_write;
    handlesToInherit[2] = NULL;
    InitializeProcThreadAttributeList (NULL,1,0,&size);
    lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)(HeapAlloc (GetProcessHeap(),0,size));
    if (lpAttributeList == NULL)
        error ("HeapAlloc");
    if (!InitializeProcThreadAttributeList (lpAttributeList,1,0,&size))
        error ("InitializeProcThreadAttributeList");
    if (!UpdateProcThreadAttribute (lpAttributeList,0,PROC_THREAD_ATTRIBUTE_HANDLE_LIST,handlesToInherit,2*sizeof (HANDLE),NULL,NULL))
        error ("UpdateProcThreadAttribute");
 
    ZeroMemory (&si,sizeof (STARTUPINFOEX));
    si.StartupInfo.cb = sizeof (STARTUPINFOEX);
    si.StartupInfo.hStdOutput = stdout_write;
    si.StartupInfo.hStdError = stdout_write;
    si.StartupInfo.hStdInput = GetStdHandle (STD_INPUT_HANDLE);
    si.StartupInfo.dwFlags |= STARTF_USESTDHANDLES;
    si.lpAttributeList = lpAttributeList;

    cmd[10]=' ';
    cmd[11]='0' + (counter-1);
    cmd[12]='\0';
     
    if (!CreateProcess (NULL,cmd,NULL,NULL,TRUE,EXTENDED_STARTUPINFO_PRESENT,NULL,NULL,&si.StartupInfo,&pi))
        error ("CreateProcess");

    DeleteProcThreadAttributeList (lpAttributeList);
    HeapFree (GetProcessHeap(),0,lpAttributeList);

    CloseHandle (stdout_write);
    copy_out_thread = CreateThread (NULL,0,copy_out_func,NULL,0,NULL);

    CloseHandle (pi.hThread);

    WaitForSingleObject (pi.hProcess,INFINITE);
    WaitForSingleObject (copy_out_thread,INFINITE);

    return 0;
}

Solution

  • It turns out this was related to mixing WriteFile and printf. As I understand it, text printed with printf will be buffered and passed to WriteFile once the buffer is full (or, in this case, when the program exits). Because of this, if you printf A before you WriteFile B, A may actually appear after B.

    The solution therefore is to use either printf (& friends) or WriteFile throughout, or to insert fflush before switching to the WinAPI.