Freigeben über


[Windows, C#] Splitting/Forking/Cloning Standard Output

For one of my little projects at work, I’ve been creating a utility which will run as a child process which is just hosted as a console application. It prints a lot of output to console, and it would be kinda useful to be able to capture all the output from the child process, either in a text file, or by reading it in the parent process, or both. I know you can capture child process output in the parent process, or redirect it to a file by passing in a file handle to the CreateProcess API. However, it seems rather a shame to also give up the console window output in the same way…

So here I started wondering – shouldn’t it be possible to keep using Console.WriteLine and have the output appear in multiple places, by using some sort of special splitting or cloning pipe? A little research turned up nothing really explicit on what to do, but a few Windows functions which look they could feature somewhere in a solution:

GetStdHandle, SetStdHandle, CreatePipe, CreateFile.

So where do we get started?

Part 1:  Interception

By reading about SetStdHandle, GetStdHandle, and CreateFile with special “CONIN$” and “CONOUT$” files we can see that there’s a few abstraction layers here.

  1. Top layer: The console class, which we can call WriteLine on.
  2. Managed stream wrapper: there are properties on the Console class: Console.In and Console.Out which represent the file streams in managed code
  3. Underlying window ‘standard input/output’ concepts: Windows tracks ‘standard input’ and ‘standard output’ file streams for a process, which seem like they are either wrapper objects, or logical indexes into a table of file handles.
  4. Raw file handle: suitable for passing to APIs that know and care about low level file handles, and don’t have concepts like standard input and standard output. APIs like ‘Read’ and ‘Write’ (bytes)

Calling SetStdHandle will let us insert our own file handle underneath the logical ‘standard input/standard output’ layer at the raw file handle layer. But we can only make each concept logically point at a single file handle, so what should we insert in order to have output in two places?

Part 2: Splitting

The current solution I have here is to create an anonymous pipe, and point standard output at it’s ‘input’ end. Alongside this, I have a dedicated thread reading in all data from the pipe, and duplicating the output by writing it to any number of other file handles.

Part 3: Output to console window even while standard output is redirected

The Windows CreateFile API supports this scenario by allowing use of some magic file names to get the actual raw file handle for the console window: “CONOUT$” being the one I am interested in. I had a couple issues getting the p/invoke declarations right. The most aggravating one to debug was caused by assuming that the values of the .net FileAccess enumeration would correspond to the FILEACCESS parameter that CreateFile takes. They don’t.

Working Code:

DllImport("kernel32.dll", SetLastError = true)]

extern static bool SetStdHandle(int nStdHandle, SafeFileHandle handle);

 

[DllImport("kernel32.dll", SetLastError = true)]

extern static bool CreatePipe(

    ref SafeFileHandle hReadPipe,

    ref SafeFileHandle hWritePipe,

    SECURITY_ATTRIBUTES securityAttributes,

    int bufferSize);

 

[DllImport("kernel32.dll", SetLastError = true)]

public static extern SafeFileHandle CreateFile(

    string lpFileName,

    uint fileAccess,

    [MarshalAs(UnmanagedType.U4)] FileShare fileShare,

    SECURITY_ATTRIBUTES securityAttributes,

    [MarshalAs(UnmanagedType.U4)] FileMode creationDisposition,

    uint dwFlagsAndAttributes,

    IntPtr hTemplateFile);

 

private static void CloneStdOutput()

{

    SafeFileHandle hReadPipe =

        new SafeFileHandle(IntPtr.Zero, false);

 

    SafeFileHandle hWritePipe =

        new SafeFileHandle(IntPtr.Zero, false);

 

    if (!CreatePipe(ref hReadPipe, ref hWritePipe, null, 0x100))

    {

  throw new Win32Exception();

    }

       

    if (!SetStdHandle(STD_OUTPUT_HANDLE, hWritePipe))

    {

        throw new Win32Exception();

    }

 

    Thread outputCloneThread = new Thread(CloneLoop);

    outputCloneThread.IsBackground = true;

    outputCloneThread.Start(hReadPipe);

}

 

private static void CloneLoop(object hReadPipe)

{

    const int FILE_ACCESS_WRITE = 0x40000000;

 

    SafeFileHandle realStdOut = CreateFile("CONOUT$", FILE_ACCESS_WRITE, FileShare.ReadWrite, null, FileMode.OpenOrCreate, 0, IntPtr.Zero);

    if (realStdOut.IsInvalid)

    {

        throw new Win32Exception();

    }

 

    var redirectedOutputStream = new FileStream((SafeFileHandle)hReadPipe, FileAccess.Read);

    var realOutputStream = new FileStream(realStdOut, FileAccess.Write);

    var clonedStream = new FileStream("C:\\temp\\clonedstream.txt", FileMode.Create);

    byte[] buffer = new byte[0x100];

    while(true)

    {

        int nRead = redirectedOutputStream.Read(buffer, 0, 0x100);

        realOutputStream.Write(buffer, 0, nRead);

        realOutputStream.Flush();

        clonedStream.Write(buffer, 0, nRead);

        clonedStream.Flush();

    };

}

Critique: I feel a little sad that we need a whole extra dedicated to pumping data around, but oh well - it’s not that bad for what I want to do. The other issue with how I have this set up right now that isn’t necessarily obvious from reading the above code, is that currently all this code is living inside of the process whose output I want to fork - and that seems a little bit wrong, since the whole point was that this process would just think that it was writing to console without knowing anything about the forking process. This is somewhat fixable by using the CreateProcess redirect standard output feature appropriately - assuming that you are happy to be outputting to the parent app’s console window... that doesn’t sound quite right either. Hmm. Well, now that I’ve done all this work, hopefully someone will bring up an obvious and better alternative I’ve overlooked!