2

I have a follow-up question to this post. In my version, I have the following I want to make asynchronous. Here is what I have:

    public virtual Task<bool> ExecuteAsync()
    {
        var tcs = new TaskCompletionSource<bool>();
        string exe = Spec.GetExecutablePath();
        string args = string.Format("--input1={0} --input2={1}", Input1, Input2);

        try
        {
            var process = new Process
            {
                EnableRaisingEvents = true,
                StartInfo =
                {
                    UseShellExecute = false,
                    FileName = exe,
                    Arguments = args,
                    RedirectStandardOutput = true,
                    RedirectStandardError = true,
                    WorkingDir = CaseDir
                }
            };
            process.Exited += (sender, arguments) =>
            {
                if (process.ExitCode != 0)
                {
                    string errorMessage = process.StandardError.ReadToEndAsync();
                    tcs.SetResult(false);
                    tcs.SetException(new InvalidOperationException("The process did not exit correctly. Error message: " + errorMessage));
                }
                else
                {
                    File.WriteAllText(LogFile, process.StandardOutput.ReadToEnd());
                    tcs.SetResult(true);
                }
                process.Dispose();
            };
            process.Start();
        }
        catch (Exception e)
        {
            Logger.InfoOutputWindow(e.Message);
            tcs.SetResult(false);
            return tcs.Task;
        }
        return tcs.Task;
    }
}

Here Spec, Input1, Input2, CaseDir, LogFile are all members of the class of which the ExecuteAsync is a method. Is that OK to use them like that? The parts I am struggling with are:

  1. I can't seem to use the async keyword at the method definition (public virtual async Task<bool> ExecuteAsync()) without a warning that I need an await keyword, whereas I do have one in the lambda expression for the process. Do I even need the async keyword at the method definition? I've seen supposedly asynchronous examples where they don't use it, e.g. this one. If I take it out it compiles, but can I use this asynchronously?
  2. Is my usage of the async keyword in the lambda expression and the corresponding await process.StandardError.ReadToEndAsync() OK inside the process lambda expression? In this example, they don't use async await at the corresponding line, so I wonder how they got away with it? Wouldn't leaving it out make it blocking, since I was told that the method ReadToEnd is blocking?
  3. Is my call to File.WriteAllText(LogFile, process.StandardOutput.ReadToEnd()) going to render the whole method blocking? If so, how can I avoid that, if at all possible?
  4. Does the exception handling make sense? Should I be aware of any details of the application logger method Logger.InfoOutputWindow which I've used in the catch block?
  5. Finally, why does the process.Exited event always appear before the process.Start() in all the examples I've come across? Can I put the process.Start() before the process.Exited event?

Appreciate any ideas and thanks in advance for your interest & attention.

EDIT #1:

For #3 above, I had an idea, based in part on comment from @René Vogt below, so I made a change to move the File.WriteAllText(...) call inside the else {} block of the process.Exited event. Perhaps this addresses #3.

EDIT #2:

I made the initial list of changes (code snippet is changed now), basically removed both the async keyword at function definition and the await keyword in the process.Exited event handler based on original comments from @René Vogt. Haven't yet tried his most recent changes below. When I run, I get an exception:

A plugin has triggered error: System.InvalidOperationException; An attempt was made to transition a task to a final state when it had already completed.

The application log has call stack as follows:

UNHANDLED EXCEPTION:
Exception Type:     CLR Exception (v4)
Exception Details:  No message (.net exception object not captured)
Exception Handler:  Unhandled exception filter
Exception Thread:   Unnamed thread (id 29560)
Report Number:      0
Report ID:          {d80f5824-ab11-4626-930a-7bb57ab22a87}
Native stack:
   KERNELBASE.dll+0x1A06D  RaiseException+0x3D
   clr.dll+0x155294
   clr.dll+0x15508E
   <unknown/managed> (0x000007FE99B92E24)
   <unknown/managed> (0x000000001AC86B00)
Managed stack:
   at System.Threading.Tasks.TaskCompletionSource`1.SetException(Exception exception)
   at <namespace>.<MyClass>.<>c__DisplayClass3.<ExecuteAsync>b__2(Object sender, EventArgs arguments)
   at System.Diagnostics.Process.RaiseOnExited()
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading._ThreadPoolWaitOrTimerCallback.PerformWaitOrTimerCallback(Object state, Boolean timedOut)
Community
  • 1
  • 1
squashed.bugaboo
  • 1,338
  • 2
  • 20
  • 36
  • This question seems very broad. Five different questions in the same question? You should implement, debug, and seek help for one part at a time. Make sure you get that part working before moving on to the next. – Peter Duniho May 20 '16 at 16:20
  • 1
    I agree with René that you shouldn't call `ReadToEndAsync()`, but you also have two other problems: you never set `RedirectStandardError` or `RedirectStandardOutput`, and waiting until the process exits to read the stdout and stderr streams is a good way to deadlock the process (it's a classic mistake: the output stream buffers fill, the process blocks, your code, run only when the process _exits_, never gets to read the streams to empty the buffers) – Peter Duniho May 20 '16 at 16:20
  • Check out this answer which may help you to simplify your code: https://stackoverflow.com/a/65804377/1653521 – Hieu Jan 20 '21 at 06:51

1 Answers1

4
  1. You don't need the async on your method signature, because you don't use await. It's enough to return a Task. The caller may await that Task - or not, it has nothing to do with your method.

  2. Don't use the async keyword on that lambda and don't use the asynchronous ReadToEnd inside that lambda. It's hard to predict what happens if you return from that event handler before it's really finished. And anyway you want to finish that method. It's called when the process exited, there is no need to make this async.

  3. Here it's the same as in (2). I think it's ok to do this "synchronously" inside this event handler. It will only block this handler, but the handler is called after the process has exited, so I guess it's ok for you.

  4. Your exception handling looks OK, but I would add another try/catch block inside the Exited event handler. But that's not based on knowledge, rather on experience that everywhere something can go wrong :)


For a better way to get the standard and error output, I suggest to subscribe to the ErrorDataReceived and OutputDataReceived events and fill StringBuilders with the received data.

In your method, declare two StringBuilders:

StringBuilder outputBuilder = new StringBuilder();
StringBuilder errorBuilder = new StringBuilder();

And subscribe to the events right after you instantiated process:

process.OutputDataReceived += (sender, e) => outputBuilder.AppendLine(e.Data);
process.ErrorDataReceived += (sender, e) => errorBuilder.AppendLine(e.Data);

Then you only need to call these two methods right after you called process.Start() (it won't work before, because the stdout and stderr are not yet opened):

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

In your Exited event handler you can then call outputBuilder.ToString() (or errorBuilder.ToString() respectively) instead of ReadToEnd and everything should work fine.

Unfortunatly there is a drawback, too: if the process is very very fast, your Exited handler may theoritically be called before those Begin*ReadLine calls. Don't know how to handle that, but it's not very likely to happen.

René Vogt
  • 43,056
  • 14
  • 77
  • 99
  • Thanks @René Vogt, will test (please bear with me on that, as I need to make several other changes before I can test) and then mark answer with any modifications as needed. – squashed.bugaboo May 20 '16 at 15:45
  • @squashed.bugaboo take your time, but I'm off for weekend then and don't know if I find the time to get back to you before monday (UTC+1 here), but I'll try. – René Vogt May 20 '16 at 15:49
  • @squashed.bugaboo Added an alternative way of reading standard/error output from the process that avoids the problems of `ReadToEnd` – René Vogt May 20 '16 at 20:08
  • @@René Vogt, I started to test. Following the comment of @Peter Duniho, I set the RedirectStandardOutput and RedirectStandardError to true. Are you saying that even with those edits, I would need to make the change you mentioned most recently? When I tested, it crashed by application by throwing the "Invalid Operation Exception". The way it stands (after making the corrections already discussed initially), StandardOutput should be OK since it is called after the process completes right? So only standard error should be the problem? I don't see the problem. – squashed.bugaboo May 20 '16 at 21:36
  • Hi @René Vogt, I was able to test with the fix after initial comments, not including your most recent recommendations and it appears to work as desired, i.e., I am able to use the UI when the process is running (the exception doesn't show up anymore and turns out to be due to some other issue). So I am still at a loss about why I need to do all of the above if I am handling the output after the process completes, and the error inside with exception added). – squashed.bugaboo May 23 '16 at 22:16
  • @squashed.bugaboo you don't need my last suggestions if you got a working solution. only if the process you start outputs a very lot it may happen that it's stdout buffer gets full and blocks and the process never terminates and so blocks your own application. – René Vogt May 23 '16 at 22:21
  • In the above question if I had two processes that I wish to run back to back in serial but asynchronously, how to go about doing that? – squashed.bugaboo Jun 16 '16 at 21:30
  • @squashed.bugaboo SO is a Q&A site, not a hire-my-personal-trainer site. if you got stuck on another problem, please ask a new question. – René Vogt Jun 16 '16 at 21:42
  • Sorry about that. Posted here: http://stackoverflow.com/questions/37869821/multiple-processes-executed-asynchronously – squashed.bugaboo Jun 16 '16 at 21:56