mrb
mrb

Reputation: 3330

Connect Process StandardOutput to another Process StandardInput

I'm trying to create a pipeline between several Process objects.

I can run a single Process and capture its StandardOutput, but when I try to connect multiple Process objects, the second one doesn't seem to receive any data from the first one's StandardInput.

In this code, I'm using cat, which either prints the contents of its arguments to stdout, or just copies stdin to stdout.

Using one process works fine:

// Works
private async void launchButtonClicked(object sender, EventArgs e)
{
    var outputBuffer = new MemoryStream();

    var tasks = new List<Task>();

    Process process1;

    {
        process1 = new Process();

        process1.StartInfo.FileName = "cat.exe";
        process1.StartInfo.Arguments = "K:\\temp\\streams.txt";
        process1.StartInfo.UseShellExecute = false;
        process1.StartInfo.RedirectStandardOutput = true;
        process1.StartInfo.RedirectStandardInput = false;
        process1.StartInfo.CreateNoWindow = true;
        process1.EnableRaisingEvents = true;
        process1.Start();
    }

    tasks.Add(process1.StandardOutput.BaseStream.CopyToAsync(outputBuffer));

    await Task.WhenAll(tasks);

    // OK: This prints the contents of the file
    Console.WriteLine("Final output: {0}", UTF8Encoding.UTF8.GetString(outputBuffer.GetBuffer()));

}

But when I add a second process and try to copy Process1's StandardOutput to Process2's StandardInput, I don't ever get any output from Process2 and the await never completes:

// Doesn't work
private async void launchButtonClicked(object sender, EventArgs e)
{
    var outputBuffer = new MemoryStream();

    var tasks = new List<Task>();

    Process process1, process2;

    {
        process1 = new Process();

        process1.StartInfo.FileName = "cat.exe";
        process1.StartInfo.Arguments = "K:\\temp\\streams.txt";
        process1.StartInfo.UseShellExecute = false;
        process1.StartInfo.RedirectStandardOutput = true;
        process1.StartInfo.RedirectStandardInput = false;
        process1.StartInfo.CreateNoWindow = true;
        process1.Start();
    }

    {
        process2 = new Process();

        process2.StartInfo.FileName = "cat.exe";
        process2.StartInfo.Arguments = "";
        process2.StartInfo.UseShellExecute = false;
        process2.StartInfo.RedirectStandardOutput = true;
        process2.StartInfo.RedirectStandardInput = true;
        process2.StartInfo.CreateNoWindow = true;
        process2.Start();
    }

    tasks.Add(process1.StandardOutput.BaseStream.CopyToAsync(process2.StandardInput.BaseStream));
    tasks.Add(process2.StandardOutput.BaseStream.CopyToAsync(outputBuffer));

    await Task.WhenAll(tasks); // Never returns!

    Console.WriteLine("Final output: {0}", UTF8Encoding.UTF8.GetString(outputBuffer.GetBuffer()));

}

Doing something like this works fine from the command line:

C:\>cat.exe K:\temp\streams.txt | cat.exe
... contents of file ...

C:\>

I tried adding Exited event handlers to these Process objects. Process1 exits fine, but Process2 never exits.

I tried some other commands too (like sed), but Process2 still never seems to do anything.

I'm using the streams' BaseStream properties, because I will eventually be working with binary data. (https://stackoverflow.com/a/4535927/339378) This also means I can't use OutputDataReceived, which returns a string (and that would be more complicated anyway).

Thanks!

Upvotes: 2

Views: 755

Answers (1)

mrb
mrb

Reputation: 3330

Apparently, CopyToAsync doesn't close the output stream on completion. This didn't matter for my MemoryStream, but obviously I need to close a process's StandardInput or it won't ever complete.

I made this method:

private static async Task CopyThenClose(Stream from, Stream to)
{
    await from.CopyToAsync(to);
    to.Close();
}

And replaced the calls to CopyToAsync; eg.:

tasks.Add(CopyThenClose(process1.StandardOutput.BaseStream, process2.StandardInput.BaseStream));

Upvotes: 3

Related Questions