process.Start使用标准输出和错误的异步重定向,错误将出现到标准输出中

发布于 2025-01-28 05:42:25 字数 2725 浏览 3 评论 0原文

我在实际情况下有这个问题,但是例如,我创建了一个非常简单的程序。我不知道我在做什么错,或者这是一个已知问题。无论如何,我可以使用一些有关如何解决它的技巧。 代码如下:

var process = new System.Diagnostics.Process
{
    StartInfo = new System.Diagnostics.ProcessStartInfo
    {
        FileName = "cmd.exe",
        Arguments = $"/C type lorem.txt",
        CreateNoWindow = true,
        WorkingDirectory = @"C:\_temp\",
        RedirectStandardOutput = true,
        RedirectStandardError = true,
        UseShellExecute = false,
    },
};

process.OutputDataReceived += new System.Diagnostics.DataReceivedEventHandler((sender, e) =>
{
    Console.ForegroundColor = ConsoleColor.Green;
    Console.WriteLine(e.Data);
});
process.ErrorDataReceived += new System.Diagnostics.DataReceivedEventHandler((sender, e) =>
{
    Console.ForegroundColor = ConsoleColor.Red;
    Console.WriteLine(e.Data);
});
Console.WriteLine("Starting!");
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
await process.WaitForExitAsync();
Console.ForegroundColor = ConsoleColor.White;
Console.ResetColor();
Console.WriteLine("All done!");

这是Net6中的一个控制台应用程序,即使我使用的是Visual Studio,我还是在命令提示符和PowerShell中尝试过。 “ lorem.txt”文件只是文本文件中更长的lorem ipsum文本。

问题在于,我需要在处理程序中处理错误输出,以获取错误,并且只有在那里,反之亦然,以进行输出dataTareceived。但是,实际输出并不那么清楚,如该图像所示: 文本是用错误处理程序绘制的。”

有时,所有文本都是绿色的,这意味着输出DataReceived处理所有文本。但是,有时会在第一段中切换,有时就像图像中一样,第一段都是红色的。

对我来说,这显然是种族条件,但我找不到解决方案。我本来希望暂停几毫秒的开始命令以获取开始...读取线以先注册,或者在开始之前运行这些行,但是从我看来,这是不可能的。

有人知道如何解决这个问题吗?

编辑1 /编辑2:

这与我们在实际情况下尝试实现它的方式更相似:

//Edit 2
var arguments = "/C docker-compose pull --no-cache my-image-id"

var process = new System.Diagnostics.Process
{
    StartInfo = new System.Diagnostics.ProcessStartInfo
    {
        FileName = "powershell.exe",
        Arguments = arguments,
        CreateNoWindow = true,
        WorkingDirectory = workingDirectory,
        RedirectStandardOutput = true,
        RedirectStandardError = true, 
        UseShellExecute = false,

    },

};
process.OutputDataReceived += new System.Diagnostics.DataReceivedEventHandler((sender, e) =>
{
    if (e.Data is not null)
    {
        _log.LogTrace("{data}", e.Data);
    }
});

process.ErrorDataReceived += new System.Diagnostics.DataReceivedEventHandler((sender, e) =>
{
    if (e.Data is not null)
    {
        _log.LogError("{data}", e.Data);
    }
});
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine(); 
await process.WaitForExitAsync();

使用Serilog完成日志记录。 不过,问题是一样的。这些消息不是混合的,但所有消息都出现是错误的,也不是跟踪。

I have this problem in a real scenario, but as an example, I have created a very simple program. I do not know what I am doing wrong, or if this is a known problem. In any case, I could use some tips on how to get around it.
The code is as follows:

var process = new System.Diagnostics.Process
{
    StartInfo = new System.Diagnostics.ProcessStartInfo
    {
        FileName = "cmd.exe",
        Arguments = 
quot;/C type lorem.txt",
        CreateNoWindow = true,
        WorkingDirectory = @"C:\_temp\",
        RedirectStandardOutput = true,
        RedirectStandardError = true,
        UseShellExecute = false,
    },
};

process.OutputDataReceived += new System.Diagnostics.DataReceivedEventHandler((sender, e) =>
{
    Console.ForegroundColor = ConsoleColor.Green;
    Console.WriteLine(e.Data);
});
process.ErrorDataReceived += new System.Diagnostics.DataReceivedEventHandler((sender, e) =>
{
    Console.ForegroundColor = ConsoleColor.Red;
    Console.WriteLine(e.Data);
});
Console.WriteLine("Starting!");
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
await process.WaitForExitAsync();
Console.ForegroundColor = ConsoleColor.White;
Console.ResetColor();
Console.WriteLine("All done!");

This is a Console application in net6, and even though I am using visual studio, I have tried it in both Command Prompt and Powershell. The 'lorem.txt' file is just a longer Lorem Ipsum text in a text file.

The problem is that I need the error output to be handled in the handler for ErrorDataReceived, and only there, and vice versa for OutputDataReceived. The real output, though, is not so clear, as illustrated by this image:
Some of the text is drawn with error handler.

Sometimes, all the text is green, meaning the OutputDataReceived handles all the text. However, sometimes it is switched in the first paragraph, and sometimes it is as in the image, the first paragraph is all red.

For me this is clearly a race condition, but I cannot find a way around it. I would have liked to pause the Start command a few milliseconds to get the Begin...ReadLine to get registered first, or run those lines before Start, but from what I can see, that is not possible.

Does anyone have any idea on how to get around this problem?

Edit 1 / Edit 2:

This is more similar to how we tried to implement it in the real scenario:

//Edit 2
var arguments = "/C docker-compose pull --no-cache my-image-id"

var process = new System.Diagnostics.Process
{
    StartInfo = new System.Diagnostics.ProcessStartInfo
    {
        FileName = "powershell.exe",
        Arguments = arguments,
        CreateNoWindow = true,
        WorkingDirectory = workingDirectory,
        RedirectStandardOutput = true,
        RedirectStandardError = true, 
        UseShellExecute = false,

    },

};
process.OutputDataReceived += new System.Diagnostics.DataReceivedEventHandler((sender, e) =>
{
    if (e.Data is not null)
    {
        _log.LogTrace("{data}", e.Data);
    }
});

process.ErrorDataReceived += new System.Diagnostics.DataReceivedEventHandler((sender, e) =>
{
    if (e.Data is not null)
    {
        _log.LogError("{data}", e.Data);
    }
});
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine(); 
await process.WaitForExitAsync();

The logging is done with SeriLog.
The problem is the same, though. The messages are not mixed, but all messages come as error, and none as trace.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

你与清晨阳光 2025-02-04 05:42:25

总结

问题是您正在编写stdoutstderr与同一控制台不同步。这导致了几个个人问题,其中一些问题可以处理,另一些则不能。此外,您无法正确处理输出或错误流的末端。

详细信息

问题1:否将

数据锁定在stdoutstderr上都不同步并立即将其写回控制台。两个输出处理程序都可以从不同的线程调用 - 可能是 - 可能是 - 都可以调用两个输出功能的语句的顺序。

您可能会期望这样的东西(事件处理程序名称在伪语句的前面):

...
OutputDataReceived: Console.ForegroundColor = ConsoleColor.Green;
OutputDataReceived: Console.WriteLine(e.Data);
-- thread context change
ErrorDataReceived : Console.ForegroundColor = ConsoleColor.Red;
ErrorDataReceived : Console.WriteLine(e.Data);
...

但是,您最好得到这样的东西:

...
OutputDataReceived: Console.ForegroundColor = ConsoleColor.Green;
-- thread context change
ErrorDataReceived : Console.ForegroundColor = ConsoleColor.Red;
ErrorDataReceived : Console.WriteLine(e.Data);
-- thread context change
OutputDataReceived: Console.WriteLine(e.Data);
...

这样,在控制台颜色为之后,stdout写了文本通过stderr文本的中间输出切换回RED

解决问题1:锁定输出,

以确保另一个手柄不会在设置颜色和编写输出之间干扰,您必须使用同步:

var lockObject = new object();

process.OutputDataReceived += (_, e) =>
{
    lock (lockObject)
    {
        Console.ForegroundColor = ConsoleColor.Green;
        Console.WriteLine(e.Data);
    }
};

process.ErrorDataReceived += (_, e) =>
{
    lock (lockObject)
    {
        Console.ForegroundColor = ConsoleColor.Red;
        Console.WriteLine(e.Data);
    }
};

始终记住,当您访问 单<>时,可能会发生这种情况。 /em>资源,在这种情况下,console,同时从多个不同的线程中。

问题2:

除了通过不锁定单一写操作混合输出的问题以外的多个来源,当然仍然存在问题, 多个写入操作 stderr 混合 - 仅在现在的同步单一写操作的中间

...
-- stdout 1
OutputDataReceived: Console.ForegroundColor = ConsoleColor.Green;
OutputDataReceived: Console.WriteLine(e.Data);
-- stderr 1
ErrorDataReceived : Console.ForegroundColor = ConsoleColor.Red;
ErrorDataReceived : Console.WriteLine(e.Data);
-- stdout 2
OutputDataReceived: Console.ForegroundColor = ConsoleColor.Green;
OutputDataReceived: Console.WriteLine(e.Data);
-- stdout 3
OutputDataReceived: Console.ForegroundColor = ConsoleColor.Green;
OutputDataReceived: Console.WriteLine(e.Data);
-- stderr 2
ErrorDataReceived : Console.ForegroundColor = ConsoleColor.Red;
ErrorDataReceived : Console.WriteLine(e.Data);
...

stdout他们中的任何一个都可以使用。因此,上面的锁定只是有助于损坏 写操作,而不是混合多个写操作。

解决问题2的解决方案

由于问题的性质替代地写入 same console来自不同的源,因此没有一个很好的解决方案这个问题。

解决方案2.1:缓冲输出

以对其进行整理,您基本上只有一个选项可以缓冲一个输出流之一(例如stderr),然后仅在另一个流之后将其写入控制台(stdout)完成。

然后,您将首先获得stdout的输出,然后在绿色中获得stderr的输出。

您在这里支付的价格是,您必须等到程序完成,然后才能看到stderr输出。

解决方案2.2:写入不同的输出,

这不是真正的解决方案,而是一种不同的方法。如果您编写stdoutstderr将输出流分开,例如,您会看到两个文件流,即输出不会混合。

问题3:将流的末端处理

为第三个问题,流的末端无法正确处理。流已经完成其输出的指标是data值设置为null。因此,如果data == null 必须完成任何事情。在您的情况下,即使没有编写数据,您仍然会设置颜色。

解决问题3:检查null

process.OutputDataReceived += (_, e) =>
{
    if (e.Data == null)
    {
        return;
    }

    lock (lockObject)
    {
        Console.ForegroundColor = ConsoleColor.Green;
        Console.WriteLine(e.Data);
    }
};

process.ErrorDataReceived += (_, e) => ...

在写作以输出文件流的情况下,他们应该在输出处理程序中关闭data == null代码>。如果您不正确处理null值,则文件未关闭,也可能不会同步到磁盘。此外,您不能在终止过程后关闭文件。请参阅问题4。

问题4:在过程终止

事件处理程序后的输出可以 - 而且很可能会 - 在之后被称为 您等待过程的结束:

...
await process.WaitForExitAsync();
-- handler may still be called here
OutputDataReceived: Console.ForegroundColor = ConsoleColor.Green;
OutputDataReceived: Console.WriteLine(e.Data);
...

这是由于以下事实:输出处理是在调用进程,而不是在中调用过程。每当调用进程写入一些输出时,呼叫进程接管并处理输出。仅在处理outputDataReceived处理程序的呼叫结束时。

由于 process终止之后仍可能会输出,因此outputDataTareceived也将在之后称为 称为过程终止。现在,如果调用程序在此之前终止,则可能会丢失一些输出。

解决问题4:等待流的结束

解决此问题,必须等待两个流的结束。从本质上讲,这归结为等待,直到两个输出处理程序收到null值,如上所述。

在主要程序终止之前,应使用某些并发机制等待两个输出流的结束。这可能是ManualResetevent(小心,这是一个低级系统事件,而不是C#事件)。有关实施,请参见下面的代码。

解决方案

最终程序可能看起来像这样:

public static async Task TestOutputRedirection()
{
    var process = new System.Diagnostics.Process
    {
        StartInfo = new System.Diagnostics.ProcessStartInfo
        {
            FileName = "cmd.exe",
            Arguments = $"/C type lorem.txt",
            CreateNoWindow = true,
            WorkingDirectory = @"C:\_temp\",
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            UseShellExecute = false,
        }
    };

    var lockObject = new object();
    var stdOutTerminated = new ManualResetEvent(false);
    var stdErrTerminated = new ManualResetEvent(false);

    process.OutputDataReceived += (_, e) =>
    {
        if (e.Data != null)
        {
            lock (lockObject)
            {
                Console.ForegroundColor = ConsoleColor.Green;
                Console.WriteLine(e.Data);
            }
        }
        else
        {
            // do your own cleanup here, e.g. if writing to a file
            stdOutTerminated.Set();
        }
    };

    process.ErrorDataReceived += (_, e) =>
    {
        if (e.Data != null)
        {
            lock (lockObject)
            {
                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine(e.Data);
            }
        }
        else
        {
            // do your own cleanup here, e.g. if writing to a file
            stdErrTerminated.Set();
        }
    };

    Console.WriteLine("Starting!");
    process.Start();

    process.BeginOutputReadLine();
    process.BeginErrorReadLine();

    // wait for the process to finish
    await process.WaitForExitAsync();

    // now wait for both outputs to terminate
    WaitHandle.WaitAll(new WaitHandle[] { stdOutTerminated, stdErrTerminated });

    // now proceed with resetting the console
    Console.ForegroundColor = ConsoleColor.White;
    Console.ResetColor();
    Console.WriteLine("All done!");
}

Summary

The problem is you are writing both stdout and stderr unsynchronized to the same console. This results in several individual problems, some of them can be handled, others not. Additionally you do not handle the end of the output or error streams correctly.

Details

Problem 1: No Locking

The data on stdout and stderr both come in asynchronously and get written back to the console immediately. Both output handlers can - and probably are - called from different threads and therefore there's no control in which order the statements of both output functions are called.

You probably expect something like this (the event handler name is at front of the pseudo statements):

...
OutputDataReceived: Console.ForegroundColor = ConsoleColor.Green;
OutputDataReceived: Console.WriteLine(e.Data);
-- thread context change
ErrorDataReceived : Console.ForegroundColor = ConsoleColor.Red;
ErrorDataReceived : Console.WriteLine(e.Data);
...

But you might as well get something like this:

...
OutputDataReceived: Console.ForegroundColor = ConsoleColor.Green;
-- thread context change
ErrorDataReceived : Console.ForegroundColor = ConsoleColor.Red;
ErrorDataReceived : Console.WriteLine(e.Data);
-- thread context change
OutputDataReceived: Console.WriteLine(e.Data);
...

This way the stdout text is written, after the console color is switched back to red, by the intermediate output of the stderr text.

Solution to Problem 1: Lock the output

To ensure that the other hander does not interfere between setting the color and writing the output, you have to use synchronization:

var lockObject = new object();

process.OutputDataReceived += (_, e) =>
{
    lock (lockObject)
    {
        Console.ForegroundColor = ConsoleColor.Green;
        Console.WriteLine(e.Data);
    }
};

process.ErrorDataReceived += (_, e) =>
{
    lock (lockObject)
    {
        Console.ForegroundColor = ConsoleColor.Red;
        Console.WriteLine(e.Data);
    }
};

Always remember that things like this might happen, when you access a single resource, in this case the console, simultaneously from multiple different threads.

Problem 2: Multiple Sources

Apart from the problem that the output is mixed up by not locking a single write operation, there is of course still the problem, that multiple write operations from stdout and stderr mix up - only not in the middle of a now synchnonized single write operation, e.g.:

...
-- stdout 1
OutputDataReceived: Console.ForegroundColor = ConsoleColor.Green;
OutputDataReceived: Console.WriteLine(e.Data);
-- stderr 1
ErrorDataReceived : Console.ForegroundColor = ConsoleColor.Red;
ErrorDataReceived : Console.WriteLine(e.Data);
-- stdout 2
OutputDataReceived: Console.ForegroundColor = ConsoleColor.Green;
OutputDataReceived: Console.WriteLine(e.Data);
-- stdout 3
OutputDataReceived: Console.ForegroundColor = ConsoleColor.Green;
OutputDataReceived: Console.WriteLine(e.Data);
-- stderr 2
ErrorDataReceived : Console.ForegroundColor = ConsoleColor.Red;
ErrorDataReceived : Console.WriteLine(e.Data);
...

Both event handlers get called multiple times when there's output available for either of them. So the above locking just helps against corrupting a single write operation, not against mixing multiple write operations.

Solution to Problem 2

Due to the nature of the problem of alternatively writing into the same Console from different sources, there's not a really nice solution to this problem.

Solution 2.1: Buffer the Output

To sort this out you basically only have the option to buffer one of the output streams (e.g. stderr) and write it to the console only after the other stream (stdout) finished.

Then you would get the output of stdout first, in green and after that the output of stderr in red.

The price you pay here is that you have to wait until the program finishes, before you see the stderr output.

Solution 2.2: Write to different Outputs

This is not really a solution, but rather a different approach. If you write stdout and stderr to separate output streams, e.g. two file streams you will see, that the output won't get mixed up.

Problem 3: Handle End of Stream

As a third problem, the end of stream is not handled correctly. The indicator that the stream has finished it's output, is that the Data value is set to null. So if Data == null nothing must be done. In your case you still set the color, even when no data shall be written.

Solution to Problem 3: Check for null

process.OutputDataReceived += (_, e) =>
{
    if (e.Data == null)
    {
        return;
    }

    lock (lockObject)
    {
        Console.ForegroundColor = ConsoleColor.Green;
        Console.WriteLine(e.Data);
    }
};

process.ErrorDataReceived += (_, e) => ...

In case of writing to output filestreams they should be closed in the output handler, as soon as Data == null. If you don't handle the null value correctly, the files are not closed and possibly not synced to disk. Additionally you cannot just close the files after the process terminated. See Problem 4 for that.

Problem 4: Output after the Process terminated

The event handlers can - and most probably will - be called after you awaited the end of the process:

...
await process.WaitForExitAsync();
-- handler may still be called here
OutputDataReceived: Console.ForegroundColor = ConsoleColor.Green;
OutputDataReceived: Console.WriteLine(e.Data);
...

This is due to the fact that the output handling is done in the calling process, not in the called process. Whenever the called process writes some output, the calling process takes over and processes the output. Only at the end of that processing a call to the OutputDataReceived handler is made.

As there might still be output in the pipeline after the called process terminated, the OutputDataReceived will also be called after the called process terminated. Now, if the calling program terminates before that, some of the output may be lost.

Solution to Problem 4: Wait for the End of Stream

To solve this problem, the end of both streams has to be waited for. Essentially this boils down to waiting until both output handlers receive a null value, as stated above.

Some concurrency mechanism should be used to wait for the end of both output streams, before the main program terminates. This could possibly be a ManualResetEvent (Careful, that's a low level system event, not a C# event). See the code below for an implementation.

Solution

The final program might look like this:

public static async Task TestOutputRedirection()
{
    var process = new System.Diagnostics.Process
    {
        StartInfo = new System.Diagnostics.ProcessStartInfo
        {
            FileName = "cmd.exe",
            Arguments = 
quot;/C type lorem.txt",
            CreateNoWindow = true,
            WorkingDirectory = @"C:\_temp\",
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            UseShellExecute = false,
        }
    };

    var lockObject = new object();
    var stdOutTerminated = new ManualResetEvent(false);
    var stdErrTerminated = new ManualResetEvent(false);

    process.OutputDataReceived += (_, e) =>
    {
        if (e.Data != null)
        {
            lock (lockObject)
            {
                Console.ForegroundColor = ConsoleColor.Green;
                Console.WriteLine(e.Data);
            }
        }
        else
        {
            // do your own cleanup here, e.g. if writing to a file
            stdOutTerminated.Set();
        }
    };

    process.ErrorDataReceived += (_, e) =>
    {
        if (e.Data != null)
        {
            lock (lockObject)
            {
                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine(e.Data);
            }
        }
        else
        {
            // do your own cleanup here, e.g. if writing to a file
            stdErrTerminated.Set();
        }
    };

    Console.WriteLine("Starting!");
    process.Start();

    process.BeginOutputReadLine();
    process.BeginErrorReadLine();

    // wait for the process to finish
    await process.WaitForExitAsync();

    // now wait for both outputs to terminate
    WaitHandle.WaitAll(new WaitHandle[] { stdOutTerminated, stdErrTerminated });

    // now proceed with resetting the console
    Console.ForegroundColor = ConsoleColor.White;
    Console.ResetColor();
    Console.WriteLine("All done!");
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文