ProcessStartInfo hanging on "WaitForExit"? Why?
Asked Answered
A

23

232

I have the following code:

info = new System.Diagnostics.ProcessStartInfo("TheProgram.exe", String.Join(" ", args));
info.CreateNoWindow = true;
info.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
info.RedirectStandardOutput = true;
info.UseShellExecute = false;
System.Diagnostics.Process p = System.Diagnostics.Process.Start(info);
p.WaitForExit();
Console.WriteLine(p.StandardOutput.ReadToEnd()); //need the StandardOutput contents

I know that the output from the process I am starting is around 7MB long. Running it in the Windows console works fine. Unfortunately programmatically this hangs indefinitely at WaitForExit. Note also this code does NOT hang for smaller outputs (like 3KB).

Is it possible that the internal StandardOutput in ProcessStartInfo can't buffer 7MB? If so, what should I do instead? If not, what am I doing wrong?

Aurangzeb answered 26/9, 2008 at 13:46 Comment(6)
any final solution with full source code about it ?Glasshouse
I run into same issue and this how I was able to solve it #2285788Hagar
Yes, final solution: swap the last two lines. It's in the manual.Renferd
from msdn: The code example avoids a deadlock condition by calling p.StandardOutput.ReadToEnd before p.WaitForExit. A deadlock condition can result if the parent process calls p.WaitForExit before p.StandardOutput.ReadToEnd and the child process writes enough text to fill the redirected stream. The parent process would wait indefinitely for the child process to exit. The child process would wait indefinitely for the parent to read from the full StandardOutput stream.Brochu
it's a bit annoying how complex it is to do this properly. Was pleased to work around it with simpler command line redirects > outputfile :)Prostitution
Take a look at github.com/Tyrrrz/CliWrapSlavery
C
475

The problem is that if you redirect StandardOutput and/or StandardError the internal buffer can become full. Whatever order you use, there can be a problem:

  • If you wait for the process to exit before reading StandardOutput the process can block trying to write to it, so the process never ends.
  • If you read from StandardOutput using ReadToEnd then your process can block if the process never closes StandardOutput (for example if it never terminates, or if it is blocked writing to StandardError).

The solution is to use asynchronous reads to ensure that the buffer doesn't get full. To avoid any deadlocks and collect up all output from both StandardOutput and StandardError you can do this:

EDIT: See answers below for how avoid an ObjectDisposedException if the timeout occurs.

using (Process process = new Process())
{
    process.StartInfo.FileName = filename;
    process.StartInfo.Arguments = arguments;
    process.StartInfo.UseShellExecute = false;
    process.StartInfo.RedirectStandardOutput = true;
    process.StartInfo.RedirectStandardError = true;

    StringBuilder output = new StringBuilder();
    StringBuilder error = new StringBuilder();

    using (AutoResetEvent outputWaitHandle = new AutoResetEvent(false))
    using (AutoResetEvent errorWaitHandle = new AutoResetEvent(false))
    {
        process.OutputDataReceived += (sender, e) => {
            if (e.Data == null)
            {
                outputWaitHandle.Set();
            }
            else
            {
                output.AppendLine(e.Data);
            }
        };
        process.ErrorDataReceived += (sender, e) =>
        {
            if (e.Data == null)
            {
                errorWaitHandle.Set();
            }
            else
            {
                error.AppendLine(e.Data);
            }
        };

        process.Start();

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

        if (process.WaitForExit(timeout) &&
            outputWaitHandle.WaitOne(timeout) &&
            errorWaitHandle.WaitOne(timeout))
        {
            // Process completed. Check process.ExitCode here.
        }
        else
        {
            // Timed out.
        }
    }
}
Cohesion answered 30/9, 2011 at 10:5 Comment(31)
How can I get exit code ? any sample about it ? I get error using process.ExitCode.Glasshouse
I tried this example. No matter what I do, my code hangs on process.Start(). Has anyone else experienced this?Pudendas
Had no idea redirecting the output was causing the issue but sure enough it was. Spent 4 hours pounding my head on this and fixed it in 5 minutes after reading your post. Nice work!Coact
Excellent answer. I spent several days re-running a test suite that takes 4 hours and kept hanging at the end. This was the resolution.Darter
Same here, spent a day trying to figure out why a custom msbuild task would randomly hang :)Adductor
@BobHorn: if you are running cmd.exe, make sure you call exit in your script or pass /c as an input arg, otherwise it will run forever.Coastland
@AlexPeck The issue was running this as a console app. Hans Passant identified the issue here: https://mcmap.net/q/88571/-process-start-hangs-when-running-on-a-background-threadPudendas
thanks MSDN, for not cluttering the important documentation (msdn.microsoft.com/en-us/library/…) with redundancies like explaining the "fire-with-null-args" patternHodometer
Great solution. My program was reading StandardOutput and then StandardErr, but since some error scenarios happened, it would block because I needed to read StandardErr first. This solution handles both very nicely.Erechtheus
@AlexPeck Could you elaborate more? I think i am having the same issue, although I used the code above it still blocks...Rejoin
@Anton if you don't tell cmd.exe to exit, it will run forever waiting for more input.Coastland
@AlexPeck I opened another question, to explain the problem I am having: goo.gl/JxMjdR Maybe you could have a look?Rejoin
For anyone else who is having the same problem: If finally found a solution. The problem is, that unity starts adb.exe when entering playmode, which interrupted the server from being shut down properly. Link to the bug: forum.unity3d.com/threads/… Link to my old question: #24717259Rejoin
everytime the command prompt closes, this appears: An unhandled exception of type"System.ObjectDisposed" occurred in mscorlib.dll Additional info:Safe handle has been closedHesperidin
We had a similar problem as described by @Hesperidin above. Do you think it is possible that using statements for the event handlers need to be above the using statement for the process itself?Outstretched
I don't think the wait handles are needed. As per msdn, just finish off with the non-timeout version of WaitForExit: When standard output has been redirected to asynchronous event handlers, it is possible that output processing will not have completed when this method returns. To ensure that asynchronous event handling has been completed, call the WaitForExit() overload that takes no parameter after receiving a true from this overload.Edessa
How do we know that a null value for e.Data indicates end of stream? I can't find that documented anywhere.Huggermugger
Oh dear. It looks like this pattern has issues with race conditions.Huggermugger
Yup. This method definitely misses any lines written between process.Start() and process.BeginOutputReadLine() :(Huggermugger
The similar implementation for PowerShell is in How to capture process output asynchronously in powershell?Thusly
That solve my problem. I created a console application which is calling another console application that I also created. The console that I was calling have "console.readline" statement which is the reason the process waiting infinitely. I remove the console.readline and everything is find in my case.Proprietor
the problem with this approach is that the events only fire when the line written to the streams ends with an EOL character; if it doesn't, it doesn't fire, and your wrapper cannot catch those charactersRehearing
I got An unhandled exception of type"System.ObjectDisposed" occurred in mscorlib.dll Additional info:Safe handle has been closed every time. in line:outputWaitHandle.Set(); I am wondering why?Absorb
I found my issue is due to the timeout.Absorb
@Ahmad It's an int value representing the duration, in milliseconds, after which the program should "give up" waiting for the process to finish (e.g. because it's taking way longer than expected and may have hung). You'll have to declare it and assign an appropriate value to it yourself.Flacon
Nice work ! But beware of the timeout infinte value: The WaitForExit method works on Processes that are OS components whose value for infiniteTimeout is Int32.MaxValue, contrary to .Net Framework infinite value (-1). int processTimeout = timeout == Timeout.Infinite ? Int32.MaxValue : timeout;Boycott
The explanation at MSDN is in fact confusing. It never makes the reader clear with how the buffer works and what exactly shared between the parent process and child process! learn.microsoft.com/en-us/dotnet/api/…Precincts
@Huggermugger If I have to choose between a deadlock or a race condition that could cause me to lose, maybe, one line of output, I'll take the race condition. Knowing the command-line applications in my case are mostly Microsoft's, the first few lines are of little importance, anyway.Rigmarole
This code was working fine. But for few cases it is not working. i am using proc.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;. Will this make any change?Susy
output.Add(ConvertProcess.StandardOutput.ReadLine() does not add entine output use output.Add(ConvertProcess.StandardOutput.ReadToEnd() insteadTrophic
alx9r's link is gone; archive.orgMantle
V
117

The documentation for Process.StandardOutput says to read before you wait otherwise you can deadlock, snippet copied below:

 // Start the child process.
 Process p = new Process();
 // Redirect the output stream of the child process.
 p.StartInfo.UseShellExecute = false;
 p.StartInfo.RedirectStandardOutput = true;
 p.StartInfo.FileName = "Write500Lines.exe";
 p.Start();
 // Do not wait for the child process to exit before
 // reading to the end of its redirected stream.
 // p.WaitForExit();
 // Read the output stream first and then wait.
 string output = p.StandardOutput.ReadToEnd();
 p.WaitForExit();
Valiant answered 26/9, 2008 at 13:49 Comment(4)
I'm not 100% certain if this is just a result of my environment, but I found if you have set RedirectStandardOutput = true; and don't use p.StandardOutput.ReadToEnd(); you get a deadlock/hang.Bespangle
True. I was in a similar situation. I was redirecting StandardError for no reason when converting with ffmpeg in a process, it was writting enough in the StandardError stream to create a deadlock.Intracranial
This still hangs for me even with redirecting and reading standard output.Tetrahedral
@Tetrahedral I guess this is only applicable if the buffer behind the StandardOutput is not fully filled. Here the MSDN does not do its justice. A great article that I would recommend you to read is at: dzone.com/articles/async-io-and-threadpoolPrecincts
R
32

This is a more modern awaitable, Task Parallel Library (TPL) based solution for .NET 4.5 and above.

Usage Example

try
{
    var exitCode = await StartProcess(
        "dotnet", 
        "--version", 
        @"C:\",
        10000, 
        Console.Out, 
        Console.Out);
    Console.WriteLine($"Process Exited with Exit Code {exitCode}!");
}
catch (TaskCanceledException)
{
    Console.WriteLine("Process Timed Out!");
}

Implementation

public static async Task<int> StartProcess(
    string filename,
    string arguments,
    string workingDirectory= null,
    int? timeout = null,
    TextWriter outputTextWriter = null,
    TextWriter errorTextWriter = null)
{
    using (var process = new Process()
    {
        StartInfo = new ProcessStartInfo()
        {
            CreateNoWindow = true,
            Arguments = arguments,
            FileName = filename,
            RedirectStandardOutput = outputTextWriter != null,
            RedirectStandardError = errorTextWriter != null,
            UseShellExecute = false,
            WorkingDirectory = workingDirectory
        }
    })
    {
        var cancellationTokenSource = timeout.HasValue ?
            new CancellationTokenSource(timeout.Value) :
            new CancellationTokenSource();

        process.Start();

        var tasks = new List<Task>(3) { process.WaitForExitAsync(cancellationTokenSource.Token) };
        if (outputTextWriter != null)
        {
            tasks.Add(ReadAsync(
                x =>
                {
                    process.OutputDataReceived += x;
                    process.BeginOutputReadLine();
                },
                x => process.OutputDataReceived -= x,
                outputTextWriter,
                cancellationTokenSource.Token));
        }

        if (errorTextWriter != null)
        {
            tasks.Add(ReadAsync(
                x =>
                {
                    process.ErrorDataReceived += x;
                    process.BeginErrorReadLine();
                },
                x => process.ErrorDataReceived -= x,
                errorTextWriter,
                cancellationTokenSource.Token));
        }

        await Task.WhenAll(tasks);
        return process.ExitCode;
    }
}

/// <summary>
/// Waits asynchronously for the process to exit.
/// </summary>
/// <param name="process">The process to wait for cancellation.</param>
/// <param name="cancellationToken">A cancellation token. If invoked, the task will return
/// immediately as cancelled.</param>
/// <returns>A Task representing waiting for the process to end.</returns>
public static Task WaitForExitAsync(
    this Process process,
    CancellationToken cancellationToken = default(CancellationToken))
{
    process.EnableRaisingEvents = true;

    var taskCompletionSource = new TaskCompletionSource<object>();

    EventHandler handler = null;
    handler = (sender, args) =>
    {
        process.Exited -= handler;
        taskCompletionSource.TrySetResult(null);
    };
    process.Exited += handler;

    if (cancellationToken != default(CancellationToken))
    {
        cancellationToken.Register(
            () =>
            {
                process.Exited -= handler;
                taskCompletionSource.TrySetCanceled();
            });
    }

    return taskCompletionSource.Task;
}

/// <summary>
/// Reads the data from the specified data recieved event and writes it to the
/// <paramref name="textWriter"/>.
/// </summary>
/// <param name="addHandler">Adds the event handler.</param>
/// <param name="removeHandler">Removes the event handler.</param>
/// <param name="textWriter">The text writer.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task representing the asynchronous operation.</returns>
public static Task ReadAsync(
    this Action<DataReceivedEventHandler> addHandler,
    Action<DataReceivedEventHandler> removeHandler,
    TextWriter textWriter,
    CancellationToken cancellationToken = default(CancellationToken))
{
    var taskCompletionSource = new TaskCompletionSource<object>();

    DataReceivedEventHandler handler = null;
    handler = new DataReceivedEventHandler(
        (sender, e) =>
        {
            if (e.Data == null)
            {
                removeHandler(handler);
                taskCompletionSource.TrySetResult(null);
            }
            else
            {
                textWriter.WriteLine(e.Data);
            }
        });

    addHandler(handler);

    if (cancellationToken != default(CancellationToken))
    {
        cancellationToken.Register(
            () =>
            {
                removeHandler(handler);
                taskCompletionSource.TrySetCanceled();
            });
    }

    return taskCompletionSource.Task;
}
Rubeola answered 5/10, 2016 at 10:54 Comment(8)
best and most complete answer to dateGumm
For some reaon, this was the only solution that worked for me, the application stoped hangging.Larder
It seems, you doesn't handle the condition, where the process end after it started, but before the Exited event was attached. My suggestion - starting the process after all registrations.Womanize
@StasBoyarincev Thanks, updated. I had forgotten to update the StackOverflow answer with this change.Rubeola
@MuhammadRehanSaeed Yet another thing - it seems not allowed call process.BeginOutputReadLine() or process.BeginErrorReadLine() before process.Start. In this case i get the error: StandardOut has not been redirected or the process hasn't started yet.Womanize
That must be why I did that.Rubeola
This only works for me if I don't wrap the process in a using. It doesn't fire the exit handler if I do. See this answerDuwe
Removing an event handler from within an event handler itself is pretty dirty imo. Surely there is a better solution to this? This too is the only approach that does not result in hanging during the output streams reading for me, so thanks for providing it!Greggs
H
25

Mark Byers' answer is excellent, but I would just add the following:

The OutputDataReceived and ErrorDataReceived delegates need to be removed before the outputWaitHandle and errorWaitHandle get disposed. If the process continues to output data after the timeout has been exceeded and then terminates, the outputWaitHandle and errorWaitHandle variables will be accessed after being disposed.

(FYI I had to add this caveat as an answer as I couldn't comment on his post.)

Higher answered 10/4, 2012 at 10:23 Comment(3)
Perhaps it would be better to call CancelOutputRead?Cohesion
Adding Mark's edited code to this answer would be rather awesome! I am having the exact same issue at the minute.Amalia
@Amalia Easiest way to solve this is to put the using(Process p ...) inside the using(AutoResetEvent errorWaitHandle...)Tree
K
20

The problem with unhandled ObjectDisposedException happens when the process is timed out. In such case the other parts of the condition:

if (process.WaitForExit(timeout) 
    && outputWaitHandle.WaitOne(timeout) 
    && errorWaitHandle.WaitOne(timeout))

are not executed. I resolved this problem in a following way:

using (AutoResetEvent outputWaitHandle = new AutoResetEvent(false))
using (AutoResetEvent errorWaitHandle = new AutoResetEvent(false))
{
    using (Process process = new Process())
    {
        // preparing ProcessStartInfo

        try
        {
            process.OutputDataReceived += (sender, e) =>
                {
                    if (e.Data == null)
                    {
                        outputWaitHandle.Set();
                    }
                    else
                    {
                        outputBuilder.AppendLine(e.Data);
                    }
                };
            process.ErrorDataReceived += (sender, e) =>
                {
                    if (e.Data == null)
                    {
                        errorWaitHandle.Set();
                    }
                    else
                    {
                        errorBuilder.AppendLine(e.Data);
                    }
                };

            process.Start();

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

            if (process.WaitForExit(timeout))
            {
                exitCode = process.ExitCode;
            }
            else
            {
                // timed out
            }

            output = outputBuilder.ToString();
        }
        finally
        {
            outputWaitHandle.WaitOne(timeout);
            errorWaitHandle.WaitOne(timeout);
        }
    }
}
Kalli answered 9/4, 2014 at 8:30 Comment(5)
for the sake of completeness, this is missing setting up the redirects to trueAssegai
and I've removed the timeouts in my end since the process may ask for user input (e.g. type something) so I don't want to require the user to be fastAssegai
Why have you changed output and error to outputBuilder? Can someone please provide complete answer that works?Phyllotaxis
System.ObjectDisposedException: Safe handle has been closed occurs on this version for me as wellCorrode
Microsoft says "When standard output has been redirected to asynchronous event handlers, it is possible that output processing will not have completed when this method WaitForExit(Int32) returns". Is it a good idea to call `outputBuilder.ToString(); before the waitHandles are set?Mantle
E
11

Rob answered it and saved me few more hours of trials. Read the output/error buffer before waiting:

// Read the output stream first and then wait.
string output = p.StandardOutput.ReadToEnd();
p.WaitForExit();
Empress answered 8/1, 2016 at 22:39 Comment(6)
but what if more data comes after you have called WaitForExit()?Assegai
@Assegai based on my tests, ReadToEnd or similar methods (like StandardOutput.BaseStream.CopyTo) will return after ALL data is read. nothing will come after itArsonist
you're saying that ReadToEnd() also waits for the exit?Assegai
@Assegai you're trying to make sense of an API created by microsoft?Supernatural
The problem of the corresponding MSDN page is, it didn't explain that the buffer behind the StandardOutput can become full and in that situation the child must stop writing and wait until the buffer is drained (the parent read away the data in the buffer). ReadToEnd() can only synch-ly read until the buffer is closed or the buffer is full, or child exits with buffer not full. That is my understanding.Precincts
@knocte: ReadToEnd() waits for the write end of the pipe to be closed. That will happen at process exit, it can also happen if the program intentionally closes its standard output handle (but that also guarantees that no more data is coming).Saponaceous
A
8

We have this issue as well (or a variant).

Try the following:

1) Add a timeout to p.WaitForExit(nnnn); where nnnn is in milliseconds.

2) Put the ReadToEnd call before the WaitForExit call. This is what we've seen MS recommend.

Alboin answered 26/9, 2008 at 13:57 Comment(0)
S
6

Credit to EM0 for https://mcmap.net/q/88574/-how-to-read-to-end-process-output-asynchronously-in-c

The other solutions (including EM0's) still deadlocked for my application, due to internal timeouts and the use of both StandardOutput and StandardError by the spawned application. Here is what worked for me:

Process p = new Process()
{
  StartInfo = new ProcessStartInfo()
  {
    FileName = exe,
    Arguments = args,
    UseShellExecute = false,
    RedirectStandardOutput = true,
    RedirectStandardError = true
  }
};
p.Start();

string cv_error = null;
Thread et = new Thread(() => { cv_error = p.StandardError.ReadToEnd(); });
et.Start();

string cv_out = null;
Thread ot = new Thread(() => { cv_out = p.StandardOutput.ReadToEnd(); });
ot.Start();

p.WaitForExit();
ot.Join();
et.Join();

Edit: added initialization of StartInfo to code sample

Stall answered 10/11, 2017 at 0:33 Comment(1)
This is what I use and never had problems anymore with a deadlock.Abiding
A
4

I solved it this way:

            Process proc = new Process();
            proc.StartInfo.FileName = batchFile;
            proc.StartInfo.UseShellExecute = false;
            proc.StartInfo.CreateNoWindow = true;
            proc.StartInfo.RedirectStandardError = true;
            proc.StartInfo.RedirectStandardInput = true;
            proc.StartInfo.RedirectStandardOutput = true;
            proc.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;      
            proc.Start();
            StreamWriter streamWriter = proc.StandardInput;
            StreamReader outputReader = proc.StandardOutput;
            StreamReader errorReader = proc.StandardError;
            while (!outputReader.EndOfStream)
            {
                string text = outputReader.ReadLine();                    
                streamWriter.WriteLine(text);
            }

            while (!errorReader.EndOfStream)
            {                   
                string text = errorReader.ReadLine();
                streamWriter.WriteLine(text);
            }

            streamWriter.Close();
            proc.WaitForExit();

I redirected both input, output and error and handled reading from output and error streams. This solution works for SDK 7- 8.1, both for Windows 7 and Windows 8

Aylmer answered 8/9, 2015 at 11:53 Comment(1)
Elina: thanks for your answer. There are some notes at the bottom of this MSDN doc (msdn.microsoft.com/en-us/library/…) that warn about potential deadlocks if you read to the end of both redirected stdout and stderr streams synchronously. It's hard to tell if your solution is susceptible to this issue. Also, it appears that you are sending the process' stdout/stderr output right back in as input. Why? :)Broken
F
3

I tried to make a class that would solve your problem using asynchronous stream read, by taking in account Mark Byers, Rob, stevejay answers. Doing so I realised that there is a bug related to asynchronous process output stream read.

I reported that bug at Microsoft: https://connect.microsoft.com/VisualStudio/feedback/details/3119134

Summary:

You can't do that:

process.BeginOutputReadLine(); process.Start();

You will receive System.InvalidOperationException : StandardOut has not been redirected or the process hasn't started yet.

============================================================================================================================

Then you have to start asynchronous output read after the process is started:

process.Start(); process.BeginOutputReadLine();

Doing so, make a race condition because the output stream can receive data before you set it to asynchronous:

process.Start(); 
// Here the operating system could give the cpu to another thread.  
// For example, the newly created thread (Process) and it could start writing to the output
// immediately before next line would execute. 
// That create a race condition.
process.BeginOutputReadLine();

============================================================================================================================

Then some people could say that you just have to read the stream before you set it to asynchronous. But the same problem occurs. There will be a race condition between the synchronous read and set the stream into asynchronous mode.

============================================================================================================================

There is no way to acheive safe asynchronous read of an output stream of a process in the actual way "Process" and "ProcessStartInfo" has been designed.

You are probably better using asynchronous read like suggested by other users for your case. But you should be aware that you could miss some information due to race condition.

Fulllength answered 18/1, 2017 at 15:9 Comment(1)
"the output stream can receive data before you set it to asynchronous" is fine, it is sitting in the pipe buffer and you will see it when you call BeginOutputReadLine(). Nothing is lost.Saponaceous
O
2

I think with async, it is possible to have a more elegant solution and not having deadlocks even when using both standardOutput and standardError:

using (Process process = new Process())
{
    process.StartInfo.FileName = filename;
    process.StartInfo.Arguments = arguments;
    process.StartInfo.UseShellExecute = false;
    process.StartInfo.RedirectStandardOutput = true;
    process.StartInfo.RedirectStandardError = true;

    process.Start();

    var tStandardOutput = process.StandardOutput.ReadToEndAsync();
    var tStandardError = process.StandardError.ReadToEndAsync();

    if (process.WaitForExit(timeout))
    {
        string output = await tStandardOutput;
        string errors = await tStandardError;

        // Process completed. Check process.ExitCode here.
    }
    else
    {
        // Timed out.
    }
}

It is base on Mark Byers answer. If you are not in an async method, you can use string output = tStandardOutput.result; instead of await

Omnidirectional answered 27/11, 2018 at 17:6 Comment(0)
D
2

I've read many of the answers and made my own. Not sure this one will fix in any case, but it fixes in my environment. I'm just not using WaitForExit and use WaitHandle.WaitAll on both output & error end signals. I will be glad, if someone will see possible problems with that. Or if it will help someone. For me it's better because not uses timeouts.

private static int DoProcess(string workingDir, string fileName, string arguments)
{
    int exitCode;
    using (var process = new Process
    {
        StartInfo =
        {
            WorkingDirectory = workingDir,
            WindowStyle = ProcessWindowStyle.Hidden,
            CreateNoWindow = true,
            UseShellExecute = false,
            FileName = fileName,
            Arguments = arguments,
            RedirectStandardError = true,
            RedirectStandardOutput = true
        },
        EnableRaisingEvents = true
    })
    {
        using (var outputWaitHandle = new AutoResetEvent(false))
        using (var errorWaitHandle = new AutoResetEvent(false))
        {
            process.OutputDataReceived += (sender, args) =>
            {
                // ReSharper disable once AccessToDisposedClosure
                if (args.Data != null) Debug.Log(args.Data);
                else outputWaitHandle.Set();
            };
            process.ErrorDataReceived += (sender, args) =>
            {
                // ReSharper disable once AccessToDisposedClosure
                if (args.Data != null) Debug.LogError(args.Data);
                else errorWaitHandle.Set();
            };

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

            WaitHandle.WaitAll(new WaitHandle[] { outputWaitHandle, errorWaitHandle });

            exitCode = process.ExitCode;
        }
    }
    return exitCode;
}
Dowitcher answered 15/9, 2019 at 1:11 Comment(2)
I used this and wrapped with Task.Run to handle timeout, I also return processid to kill on timeoutDukas
I used this code-pattern and extened it with a timeout. bool ProcHasExited = false; using (var outputWaitHandle = new AutoResetEvent(false)) using (var errorWaitHandle = new AutoResetEvent(false)) { //... ProcHasExited = WaitHandle.WaitAll(new WaitHandle[] {outputWaitHandle, errorWaitHandle}, Timeout); } if (ProcHasExited) { ResultCode = proc.ExitCode; //... } else { proc.Kill(); //... }Parlay
S
1

I thing that this is simple and better approach (we don't need AutoResetEvent):

public static string GGSCIShell(string Path, string Command)
{
    using (Process process = new Process())
    {
        process.StartInfo.WorkingDirectory = Path;
        process.StartInfo.FileName = Path + @"\ggsci.exe";
        process.StartInfo.CreateNoWindow = true;
        process.StartInfo.RedirectStandardOutput = true;
        process.StartInfo.RedirectStandardInput = true;
        process.StartInfo.UseShellExecute = false;

        StringBuilder output = new StringBuilder();
        process.OutputDataReceived += (sender, e) =>
        {
            if (e.Data != null)
            {
                output.AppendLine(e.Data);
            }
        };

        process.Start();
        process.StandardInput.WriteLine(Command);
        process.BeginOutputReadLine();


        int timeoutParts = 10;
        int timeoutPart = (int)TIMEOUT / timeoutParts;
        do
        {
            Thread.Sleep(500);//sometimes halv scond is enough to empty output buff (therefore "exit" will be accepted without "timeoutPart" waiting)
            process.StandardInput.WriteLine("exit");
            timeoutParts--;
        }
        while (!process.WaitForExit(timeoutPart) && timeoutParts > 0);

        if (timeoutParts <= 0)
        {
            output.AppendLine("------ GGSCIShell TIMEOUT: " + TIMEOUT + "ms ------");
        }

        string result = output.ToString();
        return result;
    }
}
Sylvie answered 14/6, 2012 at 14:29 Comment(2)
True, but shouldn't you be doing .FileName = Path + @"\ggsci.exe" + @" < obeycommand.txt" to simplify your code too? Or maybe something equivalent to "echo command | " + Path + @"\ggsci.exe" if you really don't want to use a separate obeycommand.txt file.Renferd
Your solution does not need AutoResetEvent but you poll. When you do poll instead of using event (when they are available) then you are using CPU for no reason and that indicate that you are a bad programmer. Your solution is really bad when compared with the other using AutoResetEvent. (But I did not give you -1 because you tried to help!).Fulllength
R
1

None of the answers above is doing the job.

Rob solution hangs and 'Mark Byers' solution get the disposed exception.(I tried the "solutions" of the other answers).

So I decided to suggest another solution:

public void GetProcessOutputWithTimeout(Process process, int timeoutSec, CancellationToken token, out string output, out int exitCode)
{
    string outputLocal = "";  int localExitCode = -1;
    var task = System.Threading.Tasks.Task.Factory.StartNew(() =>
    {
        outputLocal = process.StandardOutput.ReadToEnd();
        process.WaitForExit();
        localExitCode = process.ExitCode;
    }, token);

    if (task.Wait(timeoutSec, token))
    {
        output = outputLocal;
        exitCode = localExitCode;
    }
    else
    {
        exitCode = -1;
        output = "";
    }
}

using (var process = new Process())
{
    process.StartInfo = ...;
    process.Start();
    string outputUnicode; int exitCode;
    GetProcessOutputWithTimeout(process, PROCESS_TIMEOUT, out outputUnicode, out exitCode);
}

This code debugged and works perfectly.

Rhizobium answered 9/2, 2017 at 15:32 Comment(1)
Good! just note that the token parameter is not provided when calling GetProcessOutputWithTimeout method.Arsonist
C
1

Introduction

Currently accepted answer doesn't work (throws exception) and there are too many workarounds but no complete code. This is obviously wasting lots of people's time because this is a popular question.

Combining Mark Byers' answer and Karol Tyl's answer I wrote full code based on how I want to use the Process.Start method.

Usage

I have used it to create progress dialog around git commands. This is how I've used it:

    private bool Run(string fullCommand)
    {
        Error = "";
        int timeout = 5000;

        var result = ProcessNoBS.Start(
            filename: @"C:\Program Files\Git\cmd\git.exe",
            arguments: fullCommand,
            timeoutInMs: timeout,
            workingDir: @"C:\test");

        if (result.hasTimedOut)
        {
            Error = String.Format("Timeout ({0} sec)", timeout/1000);
            return false;
        }

        if (result.ExitCode != 0)
        {
            Error = (String.IsNullOrWhiteSpace(result.stderr)) 
                ? result.stdout : result.stderr;
            return false;
        }

        return true;
    }

In theory you can also combine stdout and stderr, but I haven't tested that.

Code

public struct ProcessResult
{
    public string stdout;
    public string stderr;
    public bool hasTimedOut;
    private int? exitCode;

    public ProcessResult(bool hasTimedOut = true)
    {
        this.hasTimedOut = hasTimedOut;
        stdout = null;
        stderr = null;
        exitCode = null;
    }

    public int ExitCode
    {
        get 
        {
            if (hasTimedOut)
                throw new InvalidOperationException(
                    "There was no exit code - process has timed out.");

            return (int)exitCode;
        }
        set
        {
            exitCode = value;
        }
    }
}

public class ProcessNoBS
{
    public static ProcessResult Start(string filename, string arguments,
        string workingDir = null, int timeoutInMs = 5000,
        bool combineStdoutAndStderr = false)
    {
        using (AutoResetEvent outputWaitHandle = new AutoResetEvent(false))
        using (AutoResetEvent errorWaitHandle = new AutoResetEvent(false))
        {
            using (var process = new Process())
            {
                var info = new ProcessStartInfo();

                info.CreateNoWindow = true;
                info.FileName = filename;
                info.Arguments = arguments;
                info.UseShellExecute = false;
                info.RedirectStandardOutput = true;
                info.RedirectStandardError = true;

                if (workingDir != null)
                    info.WorkingDirectory = workingDir;

                process.StartInfo = info;

                StringBuilder stdout = new StringBuilder();
                StringBuilder stderr = combineStdoutAndStderr
                    ? stdout : new StringBuilder();

                var result = new ProcessResult();

                try
                {
                    process.OutputDataReceived += (sender, e) =>
                    {
                        if (e.Data == null)
                            outputWaitHandle.Set();
                        else
                            stdout.AppendLine(e.Data);
                    };
                    process.ErrorDataReceived += (sender, e) =>
                    {
                        if (e.Data == null)
                            errorWaitHandle.Set();
                        else
                            stderr.AppendLine(e.Data);
                    };

                    process.Start();

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

                    if (process.WaitForExit(timeoutInMs))
                        result.ExitCode = process.ExitCode;
                    // else process has timed out 
                    // but that's already default ProcessResult

                    result.stdout = stdout.ToString();
                    if (combineStdoutAndStderr)
                        result.stderr = null;
                    else
                        result.stderr = stderr.ToString();

                    return result;
                }
                finally
                {
                    outputWaitHandle.WaitOne(timeoutInMs);
                    errorWaitHandle.WaitOne(timeoutInMs);
                }
            }
        }
    }
}
Confectioner answered 4/4, 2017 at 15:31 Comment(1)
Still get System.ObjectDisposedException: Safe handle has been closed on this version too.Corrode
S
1

I know that this is supper old but, after reading this whole page none of the solutions was working for me, although I didn't try Muhammad Rehan as the code was a little hard to follow, although I guess he was on the right track. When I say it didn't work that's not entirely true, sometimes it would work fine, I guess it is something to do with the length of the output before an EOF mark.

Anyway, the solution that worked for me was to use different threads to read the StandardOutput and StandardError and write the messages.

        StreamWriter sw = null;
        var queue = new ConcurrentQueue<string>();

        var flushTask = new System.Timers.Timer(50);
        flushTask.Elapsed += (s, e) =>
        {
            while (!queue.IsEmpty)
            {
                string line = null;
                if (queue.TryDequeue(out line))
                    sw.WriteLine(line);
            }
            sw.FlushAsync();
        };
        flushTask.Start();

        using (var process = new Process())
        {
            try
            {
                process.StartInfo.FileName = @"...";
                process.StartInfo.Arguments = $"...";
                process.StartInfo.UseShellExecute = false;
                process.StartInfo.RedirectStandardOutput = true;
                process.StartInfo.RedirectStandardError = true;

                process.Start();

                var outputRead = Task.Run(() =>
                {
                    while (!process.StandardOutput.EndOfStream)
                    {
                        queue.Enqueue(process.StandardOutput.ReadLine());
                    }
                });

                var errorRead = Task.Run(() =>
                {
                    while (!process.StandardError.EndOfStream)
                    {
                        queue.Enqueue(process.StandardError.ReadLine());
                    }
                });

                var timeout = new TimeSpan(hours: 0, minutes: 10, seconds: 0);

                if (Task.WaitAll(new[] { outputRead, errorRead }, timeout) &&
                    process.WaitForExit((int)timeout.TotalMilliseconds))
                {
                    if (process.ExitCode != 0)
                    {
                        throw new Exception($"Failed run... blah blah");
                    }
                }
                else
                {
                    throw new Exception($"process timed out after waiting {timeout}");
                }
            }
            catch (Exception e)
            {
                throw new Exception($"Failed to succesfully run the process.....", e);
            }
        }
    }

Hope this helps someone, who thought this could be so hard!

Stamey answered 8/6, 2017 at 7:2 Comment(1)
Exception: sw.FlushAsync(): Object is not set to an instance of an object. sw is null. How/where should sw be defined?Stealing
R
1

After reading all the posts here, i settled on the consolidated solution of Marko Avlijaš. However, it did not solve all of my issues.

In our environment we have a Windows Service which is scheduled to run hundreds of different .bat .cmd .exe,... etc. files which have accumulated over the years and were written by many different people and in different styles. We have no control over the writing of the programs & scripts, we are just responsible for scheduling, running, and reporting on success/failure.

So i tried pretty much all of the suggestions here with different levels of success. Marko's answer was almost perfect, but when run as a service, it didnt always capture stdout. I never got to the bottom of why not.

The only solution we found that works in ALL our cases is this : http://csharptest.net/319/using-the-processrunner-class/index.html

Rahal answered 1/2, 2018 at 14:19 Comment(3)
I am going to try this library. I have scoped the code, and it looks to be using delegates sensibly. It is nicely packaged in Nuget. It basically stinks of professionalism, something that I could never be accused of. If it bites, will tell.Ursola
The link to the source code is dead. Please next time copy the code to the answer.Vermeil
@VitalyZdanevich: The code has been ported to github.com/csharptest/CSharpTest.Net.Tools. One can find it under CmdTool/Processes.Dort
P
1

Workaround I ended up using to avoid all the complexity:

var outputFile = Path.GetTempFileName();
info = new System.Diagnostics.ProcessStartInfo("TheProgram.exe", String.Join(" ", args) + " > " + outputFile + " 2>&1");
info.CreateNoWindow = true;
info.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
info.UseShellExecute = false;
System.Diagnostics.Process p = System.Diagnostics.Process.Start(info);
p.WaitForExit();
Console.WriteLine(File.ReadAllText(outputFile)); //need the StandardOutput contents

So I create a temp file, redirect both the output and error to it by using > outputfile > 2>&1 and then just read the file after the process has finished.

The other solutions are fine for scenarios where you want to do other stuff with the output, but for simple stuff this avoids a lot of complexity.

Prostitution answered 7/6, 2019 at 8:32 Comment(0)
L
0

In my case I had an error so I just waited in vain for a normal ouput.

I switched the order from this:

string result = process.StandardOutput.ReadToEnd();
string error = process.StandardError.ReadToEnd();

To this:

string error = process.StandardError.ReadToEnd();

if (string.IsNullOrEmpty(error))
    string result = process.StandardOutput.ReadToEnd();
Lambency answered 23/8, 2022 at 7:52 Comment(0)
M
-1

This post maybe outdated but i found out the main cause why it usually hang is due to stack overflow for the redirectStandardoutput or if you have redirectStandarderror.

As the output data or the error data is large, it will cause a hang time as it is still processing for indefinite duration.

so to resolve this issue:

p.StartInfo.RedirectStandardoutput = False
p.StartInfo.RedirectStandarderror = False
Malay answered 26/3, 2011 at 8:28 Comment(1)
The problem is that people explicitly set those to true because they want to be able to access those streams! Else indeed we can just leave them to false.Dena
D
-1

Let us call the sample code posted here the redirector and the other program the redirected. If it were me then I would probably write a test redirected program that can be used to duplicate the problem.

So I did. For test data I used the ECMA-334 C# Language Specificationv PDF; it is about 5MB. The following is the important part of that.

StreamReader stream = null;
try { stream = new StreamReader(Path); }
catch (Exception ex)
{
    Console.Error.WriteLine("Input open error: " + ex.Message);
    return;
}
Console.SetIn(stream);
int datasize = 0;
try
{
    string record = Console.ReadLine();
    while (record != null)
    {
        datasize += record.Length + 2;
        record = Console.ReadLine();
        Console.WriteLine(record);
    }
}
catch (Exception ex)
{
    Console.Error.WriteLine($"Error: {ex.Message}");
    return;
}

The datasize value does not match the actual file size but that does not matter. It is not clear if a PDF file always uses both CR and LF at the end of lines but that does not matter for this. You can use any other large text file to test with.

Using that the sample redirector code hangs when I write the large amount of data but not when I write a small amount.

I tried very much to somehow trace the execution of that code and I could not. I commented out the lines of the redirected program that disabled creation of a console for the redirected program to try to get a separate console window but I could not.

Then I found How to start a console app in a new window, the parent’s window, or no window. So apparently we cannot (easily) have a separate console when one console program starts another console program without ShellExecute and since ShellExecute does not support redirection we must share a console, even if we specify no window for the other process.

I assume that if the redirected program fills up a buffer somewhere then it must wait for the data to be read and if at that point no data is read by the redirector then it is a deadlock.

The solution is to not use ReadToEnd and to read the data while the data is being written but it is not necessary to use asynchronous reads. The solution can be quite simple. The following works for me with the 5 MB PDF.

ProcessStartInfo info = new ProcessStartInfo(TheProgram);
info.CreateNoWindow = true;
info.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
info.RedirectStandardOutput = true;
info.UseShellExecute = false;
Process p = Process.Start(info);
string record = p.StandardOutput.ReadLine();
while (record != null)
{
    Console.WriteLine(record);
    record = p.StandardOutput.ReadLine();
}
p.WaitForExit();

Another possibility is to use a GUI program to do the redirection. The preceding code works in a WPF application except with obvious modifications.

Dulsea answered 15/1, 2019 at 0:13 Comment(0)
S
-1

This is my almost synchronous solution. You should invoke StandardOutput.ReadToEndAsync() and StandardError.ReadToEndAsync() together at the same time!

    public static void EvaluateJavaScript(string script) {
        script = script.Replace( @"""", @"\""" );
        using var process = System.Diagnostics.Process.Start( new System.Diagnostics.ProcessStartInfo() {
            FileName = "node",
            Arguments = $@"--eval ""{script}""",
            UseShellExecute = false,
            CreateNoWindow = true,
            RedirectStandardInput = true,
            RedirectStandardOutput = true,
            RedirectStandardError = true,
        } );
        {
            while (true) {
                var output = process.StandardOutput.ReadToEndAsync();
                var error = process.StandardError.ReadToEndAsync();

                if (!string.IsNullOrEmpty( output.Result )) {
                    Debug.Log( output.Result );
                }
                if (!string.IsNullOrEmpty( error.Result )) {
                    Debug.LogError( error.Result );
                }
                if (string.IsNullOrEmpty( output.Result ) && string.IsNullOrEmpty( error.Result )) {
                    break;
                }
            }

            process.StandardInput.Close();
            process.StandardOutput.Close();
            process.StandardError.Close();
        }
        process.WaitForExit( 10_000 );
    }
Secretariat answered 26/6, 2023 at 20:41 Comment(1)
Denis, can you explain why you need "while (true)"? output.Result already assumes task is finished and Result is complete output of a program.Kane
H
-3

I was having the same issue, but the reason was different. It would however happen under Windows 8, but not under Windows 7. The following line seems to have caused the problem.

pProcess.StartInfo.UseShellExecute = False

The solution was to NOT disable UseShellExecute. I now received a Shell popup window, which is unwanted, but much better than the program waiting for nothing particular to happen. So I added the following work-around for that:

pProcess.StartInfo.WindowStyle = ProcessWindowStyle.Hidden

Now the only thing bothering me is to why this is happening under Windows 8 in the first place.

Hagiarchy answered 13/1, 2015 at 10:35 Comment(1)
You need UseShellExecute to be set to false if you want to redirect the output.Nudge

© 2022 - 2024 — McMap. All rights reserved.