使用Process RedirectStandardError和RedirectStandardOutput时的竞争条件

本文关键字:竞争 条件 RedirectStandardOutput Process RedirectStandardError 使用 | 更新日期: 2023-09-27 18:14:49

在订阅System.Diagnostics.Process的输出和错误流时,我发现自己处于竞争状态。下面是我所做的一个最小的例子:

    private string execute(string command, string arguments, int mstimeout)
    {
        string report = string.Empty;
        StringBuilder output = new StringBuilder();
        StringBuilder error = new StringBuilder();
        Process p = new Process();
        DataReceivedEventHandler ErrorDataReceived = (o, e) => { error.Append(e.Data); };
        DataReceivedEventHandler OutputDataReceived = (o, e) => { output.Append(e.Data); };
        try
        {
            p.StartInfo.FileName = command;
            p.StartInfo.Arguments = arguments;
            p.EnableRaisingEvents = true;
            p.StartInfo.CreateNoWindow = true;
            p.StartInfo.UseShellExecute = false;
            p.StartInfo.RedirectStandardError = true;
            p.StartInfo.RedirectStandardOutput = true;
            p.OutputDataReceived += OutputDataReceived;
            p.ErrorDataReceived += ErrorDataReceived;
            p.Start();
            p.BeginErrorReadLine();
            p.BeginOutputReadLine();
            p.WaitForExit(mstimeout);
            report = output.ToString() + "'n" + error.ToString();
        }
        finally
        {
            p.OutputDataReceived -= OutputDataReceived;
            p.ErrorDataReceived -= ErrorDataReceived;
        }
        return report;
    }

当调试缓慢的行为是我所希望的。当不停止运行时,报告将以空结束。

我假设存在一个竞态条件,在处理所有输出之前,底层流对象被处理。

我能做些什么来等待所有的输出被处理吗?

使用Process RedirectStandardError和RedirectStandardOutput时的竞争条件

我不认为你能做什么。我认为微软完全错过了启动过程的机会,你想要得到他们的输出(输出和错误)。总会有问题。至少,它是你所拥有的竞争条件。我在微软报告了一个bug: https://connect.microsoft.com/VisualStudio/feedback/details/3119134/race-condition-in-process-asynchronous-output-stream-read

作为参考,这是我现在使用的代码(它包含相同的竞争条件问题,任何在异步模式下运行的实现都会有)。

using System;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading;
namespace HQ.Util.General
{
    public class ProcessExecutionWithOutputCapture
    {
        // ************************************************************************
        public class ProcessWithOutputCaptureResult
        {
            public string Error { get; internal set; }
            public string Output { get; internal set; }
            public string ExecutionError
            {
                get
                {
                    if (String.IsNullOrEmpty(Error))
                    {
                        return Error;
                    }
                    return Exception?.ToString();
                }
            }
            public bool HasTimeout { get; internal set; }
            /// <summary>
            /// Can be cancel through the eventCancel which will cancel the wait (and if set, will kill the process)
            /// </summary>
            public bool HasBeenCanceled { get; internal set; }
            public int ExitCode { get; internal set; }
            public Exception Exception { get; internal set; }
            public bool HasSucceded => !HasTimeout && Exception == null;
        }
        // ************************************************************************
        private StringBuilder _sbOutput = new StringBuilder();
        private StringBuilder _sbError = new StringBuilder();
        private AutoResetEvent _outputWaitHandle = null;
        private AutoResetEvent _errorWaitHandle = null;
        // Could be usefull when user want to exit to not wait for process to end and kill it (if wanted)
        public EventWaitHandle AdditionalConditionToStopWaitingProcess { get; set; }
        public bool IsAdditionalConditionToStopWaitingProcessShouldAlsoKill { get; set; }
        public ProcessWindowStyle ProcessWindowStyle { get; set; } = ProcessWindowStyle.Hidden;
        public bool CreateWindow { get; set; } = false;
        public static ProcessWithOutputCaptureResult ExecuteWith(string executablePath, string arguments, int timeout = Timeout.Infinite, ProcessWindowStyle processWindowStyle = ProcessWindowStyle.Hidden, bool createWindow = false)
        {
            var p = new ProcessExecutionWithOutputCapture();
            return p.Execute(executablePath, arguments, timeout);
        }
        // ************************************************************************
        /// <summary>
        /// Only support existing exectuable (no association or dos command which have no executable like 'dir').
        /// But accept full path, partial path or no path where it will use regular system/user path.
        /// </summary>
        /// <param name="executablePath"></param>
        /// <param name="arguments"></param>
        /// <param name="timeout"></param>
        /// <returns></returns>
        private ProcessWithOutputCaptureResult Execute(string executablePath, string arguments = null, int timeout = Timeout.Infinite)
        {
            ProcessWithOutputCaptureResult processWithOutputCaptureResult = null;
            using (Process process = new Process())
            {
                process.StartInfo.FileName = executablePath;
                process.StartInfo.Arguments = arguments;
                process.StartInfo.UseShellExecute = false; // required to redirect output to appropriate (output or error) process stream
                process.StartInfo.WindowStyle = ProcessWindowStyle;
                process.StartInfo.CreateNoWindow = CreateWindow;
                process.StartInfo.RedirectStandardOutput = true;
                process.StartInfo.RedirectStandardError = true;
                _outputWaitHandle = new AutoResetEvent(false);
                _errorWaitHandle = new AutoResetEvent(false);
                bool asyncReadStarted = false;
                try
                {
                    process.OutputDataReceived += ProcessOnOutputDataReceived;
                    process.ErrorDataReceived += ProcessOnErrorDataReceived;
                    process.Start();
                    // Here there is a race condition. See: https://connect.microsoft.com/VisualStudio/feedback/details/3119134/race-condition-in-process-asynchronous-output-stream-read
                    process.BeginOutputReadLine();
                    process.BeginErrorReadLine();
                    asyncReadStarted = true;
                    // See: ProcessStartInfo.RedirectStandardOutput Property:
                    //      https://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k(System.Diagnostics.ProcessStartInfo.RedirectStandardOutput);k(TargetFrameworkMoniker-.NETFramework,Version%3Dv4.5.2);k(DevLang-csharp)&rd=true
                    // All 4 next lines should only be called when not using asynchronous read (process.BeginOutputReadLine() and process.BeginErrorReadLine())
                    //_sbOutput.AppendLine(process.StandardOutput.ReadToEnd());
                    //_sbError.AppendLine(process.StandardError.ReadToEnd());
                    //_sbOutput.AppendLine(process.StandardOutput.ReadToEnd());
                    //_sbError.AppendLine(process.StandardError.ReadToEnd());
                    var waitHandles = new WaitHandle[1 + (AdditionalConditionToStopWaitingProcess == null ? 0 : 1)];
                    waitHandles[0] = new ProcessWaitHandle(process);
                    if (AdditionalConditionToStopWaitingProcess != null)
                    {
                        waitHandles[1] = AdditionalConditionToStopWaitingProcess;
                    }
                    bool hasSucceded = false;
                    int waitResult = WaitHandle.WaitAny(waitHandles, timeout);
                    if (waitResult == 1) // The wait has been interrrupted by an external event
                    {
                        if (IsAdditionalConditionToStopWaitingProcessShouldAlsoKill)
                        {
                            process.Kill();
                        }
                    }
                    else if (waitResult == 0) // Process has completed normally, no timeout or external event
                    {
                        // Ensure internal process code has completed like ensure to wait until stdout et stderr had been fully completed
                        hasSucceded = process.WaitForExit(timeout);
                        if (_outputWaitHandle.WaitOne(timeout) && _errorWaitHandle.WaitOne(timeout))
                        {
                            processWithOutputCaptureResult = new ProcessWithOutputCaptureResult();
                            processWithOutputCaptureResult.ExitCode = process.ExitCode;
                            processWithOutputCaptureResult.Output = _sbOutput.ToString();
                            processWithOutputCaptureResult.Error = _sbError.ToString();
                        }
                    }
                    else // Process timeout
                    {
                        processWithOutputCaptureResult = new ProcessWithOutputCaptureResult();
                        processWithOutputCaptureResult.HasTimeout = true;
                    }
                }
                catch (Exception ex)
                {
                    if (ex.HResult == -2147467259)
                    {
                        processWithOutputCaptureResult = new ProcessWithOutputCaptureResult();
                        processWithOutputCaptureResult.Exception = new FileNotFoundException("File not found: " + executablePath, ex);
                    }
                    else
                    {
                        processWithOutputCaptureResult = new ProcessWithOutputCaptureResult();
                        processWithOutputCaptureResult.Exception = ex;
                    }
                }
                finally
                {
                    if (asyncReadStarted)
                    {
                        process.CancelOutputRead();
                        process.CancelErrorRead();
                    }
                    process.OutputDataReceived -= ProcessOnOutputDataReceived;
                    process.ErrorDataReceived -= ProcessOnOutputDataReceived;
                    _outputWaitHandle.Close();
                    _outputWaitHandle.Dispose();
                    _errorWaitHandle.Close();
                    _errorWaitHandle.Dispose();
                }
            }
            return processWithOutputCaptureResult;
        }
        // ************************************************************************
        private void ProcessOnOutputDataReceived(object sender, DataReceivedEventArgs e)
        {
            if (e.Data == null)
            {
                _outputWaitHandle.Set();
            }
            else
            {
                _sbOutput.AppendLine(e.Data);
            }
        }
        // ************************************************************************
        private void ProcessOnErrorDataReceived(object sender, DataReceivedEventArgs e)
        {
            if (e.Data == null)
            {
                _errorWaitHandle.Set();
            }
            else
            {
                _sbError.AppendLine(e.Data);
            }
        }
        // ************************************************************************
    }
}

用法(作为转发执行的应用程序):

using System;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using HQ.Util.General;
using System.Reflection;
namespace ExecutionForwarder
{
    class Program
    {
        static void Main(string[] args)
        {
            Stopwatch stopwatch = Stopwatch.StartNew();
            Console.WriteLine($"App: {Assembly.GetEntryAssembly().FullName}");
            Console.WriteLine($"Executing from folder: {Environment.CurrentDirectory}");
            Console.WriteLine($"at: {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}");
            Console.WriteLine($"With args: [{string.Join(" ", args.Skip(1))}]");
            if (args.Length == 1 && args[0].ToLower().StartsWith("-delay:"))
            {
                int millisec;
                if (Int32.TryParse(args[0].Substring(args[0].IndexOf(":") + 1), out millisec))
                {
                    Console.WriteLine($"Sleeping for {millisec} milliseconds and will exit.");
                    Thread.Sleep(millisec);
                }
                else
                {
                    Console.Error.WriteLine("Error while trying to read the delay.");
                    Environment.ExitCode = -99;
                }
            }
            else
            {
                if (args.Length == 0)
                {
                    Console.Error.WriteLine($"Can't forward execution. There is no argument (executable) provided.");
                    Environment.ExitCode = -99;
                }
                else
                {
                    var result = ProcessExecutionWithOutputCapture.ExecuteWith(args[0], string.Join(" ", args.Skip(1)));
                    Console.Write(result.Output);
                    Console.Error.Write(result.Error);
                    Environment.ExitCode = result.ExitCode;
                }
            }
            Console.WriteLine($"Done in {stopwatch.ElapsedMilliseconds} millisecs");
        }
    }
}

问题是在某些情况下超时。我需要将Kill改为Process,以避免后续问题。

            if(!p.WaitForExit(mstimeout))
            {
                p.Kill();
            }

为了更好地测量,我在finally部分中添加了可能不需要的清理。

<罢工>

        finally
        {
            p.OutputDataReceived -= OutputDataReceived;
            p.ErrorDataReceived -= ErrorDataReceived;
            p.Dispose();
            p = null;
            GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, blocking: true);
        }

编辑:我删除了最后一部分,因为下面的评论似乎是正确的。

编辑:有一个更深层次的问题,因为需要输入而达到超时。我最后调用了一个不同的命令,它是静默的。


为了将来的参考-我现在决定了:

    private string execute(string command, string arguments, int mstimeout)
    {
        bool timeout = false;
        string report = string.Empty;
        StringBuilder output = new StringBuilder();
        StringBuilder error = new StringBuilder();
        Process p = new Process();
        DataReceivedEventHandler StoreError = (o, e) => { error.Append(e.Data); };
        DataReceivedEventHandler StoreOutput = (o, e) => { output.Append(e.Data); };
        try
        {
            Debug.WriteLine(command);
            Debug.WriteLine(arguments);
            p.StartInfo.FileName = command;
            p.StartInfo.Arguments = arguments;
            p.EnableRaisingEvents = true;
            p.StartInfo.CreateNoWindow = true;
            p.StartInfo.UseShellExecute = false;
            p.StartInfo.RedirectStandardError = true;
            p.StartInfo.RedirectStandardOutput = true;
            p.OutputDataReceived += StoreOutput;
            p.ErrorDataReceived += StoreError;
            p.Start();
            p.BeginErrorReadLine();
            p.BeginOutputReadLine();
            if (!p.WaitForExit(mstimeout))
            {
                p.Kill();
                timeout = true;
                Debug.WriteLine("Process killed");
            }
            else
            {
                p.WaitForExit();
            }
        }
        finally
        {
            report = output.ToString() + "'n" + error.ToString();
            Debug.WriteLine(report);
            p.Dispose();
        }
        if (timeout)
        {
            throw new TimeoutException("Timeout during call: " + command + " " + arguments);
        }
        return report;
    }