ssube
ssube

Reputation: 48307

C# wait for shell command to return while reading async

I need to run an external process from a C# app, but wait for it to return before continuing execution. At the same time, I need to get and update a text box with progress info from stdout. Since the command may run for a few minutes and print info during that time, displaying that is absolutely necessary; but multiple commands are likely to be run and must be in order, so waiting is as well.

I attempted using:

p = Process.Start();
p.BeginOutputReadLine();
p.WaitForExit();

but that froze the UI thread while waiting and prevented the output from appearing. This:

p.Start();
p.BeginOutputReadLine();

while (!p.HasExited)
{
    Application.DoEvents();
    Thread.Sleep(100);
}

works better, but is completely wrong/a bad idea and doesn't actually wait the full period.

I also briefly attempted using a BackgroundWorker to launch the shell process, but I'm not sure how to make the UI thread wait without blocking for the worker to finish.

What I want to do is provide a DialogResult ShellExecute(String cmd) similar to ShowDialog() that returns an OK/Cancel(/fail) result, if the user allowed the command to complete, clicked cancel, or the command's return code. It shouldn't return until the command has completed (or been canceled).

All the shell commands are run with:

ProcessStartInfo info = new ProcessStartInfo
{
    UseShellExecute = false,
    CreateNoWindow = true,
    RedirectStandardOutput = true,
    RedirectStandardError = true,

    FileName = "cmd.exe",
    Arguments = "/c " + command
};

to redirect the output and effectively shell execute the command.

How would I go about correctly making a function that launches an async process, but still waits until that is finished to return?

Upvotes: 2

Views: 5925

Answers (2)

ssube
ssube

Reputation: 48307

Reworked most of the code to get this to work correctly.

The ProgressForm class provides a QueueCommand method, taking the desired shell command and pre/post delegates. When shown, the progress form uses a background worker to execute each command, handle the return code and execute the next command if appropriate.

The background worker waits for each shell command to finish (Process.WaitForExit()) while asynchronously feeding the UI thread output. When finished, it calls a method enabling the successful/ok button and hiding the progress bar.

void m_Worker_DoWork(object sender, DoWorkEventArgs e)
{
    int exec = 1;
    while (CommandQueue.Count > 0)
    {
        if (e.Cancel)
        {
            e.Result = 1;
            return;
        }

        WriteLine("Running command {0}, {1} remaining.", exec++, CommandQueue.Count);
        StagedCommand command = CommandQueue.Peek();
        try
        {
            if (command.Pre != null) command.Pre();
            int result = ShellExec(command.Command);
            if (command.Post != null) command.Post();
            CommandQueue.Dequeue();
            if (result != 0)
            {
                e.Result = result;
                return;
           }
        }
        catch (Exception exc)
        {
            WriteLine("Error: {0}", exc.Message);
            e.Result = 1;
            return;
        }
    }

    WriteLine("All commands executed successfully.");
    e.Result = 0;
    return;
}

    int ShellExec(String command)
    {
        WriteLine(command);
        Style = ProgressBarStyle.Marquee;

        ProcessStartInfo info = new ProcessStartInfo
        {
            UseShellExecute = false,
            LoadUserProfile = true,
            ErrorDialog = false,
            CreateNoWindow = true,
            WindowStyle = ProcessWindowStyle.Hidden,
            RedirectStandardOutput = true,
            StandardOutputEncoding = Encoding.UTF8,
            RedirectStandardError = true,
            StandardErrorEncoding = Encoding.UTF8,

            FileName = "cmd.exe",
            Arguments = "/c " + command
        };

        Process shell = new Process();
        shell.StartInfo = info;
        shell.EnableRaisingEvents = true;
        shell.ErrorDataReceived += new DataReceivedEventHandler(ShellErrorDataReceived);
        shell.OutputDataReceived += new DataReceivedEventHandler(ShellOutputDataReceived);

        shell.Start();
        shell.BeginErrorReadLine();
        shell.BeginOutputReadLine();
        shell.WaitForExit();

        return shell.ExitCode;
    }

Upvotes: 1

Eugen Rieck
Eugen Rieck

Reputation: 65314

Create your process from a ProcessStartInfo,

Clarification: ProcessStartinfo si needs something like

si.CreateNoWindow=true;
si.RedirectStandardOutput=true;
si.RedirectStandardError=true;
si.StandardOutputEncoding=Encoding.UTF8;
si.StandardErrorEncoding=Encoding.UTF8;
si.WindowStyle=ProcessWindowStyle.Hidden;

then before

p.Start();

use

p.OutoutDataReceived+=OutputHandler;

with

private static void OutputHandler(object theProcess, DataReceivedEventArgs evtdata)
{
  //evtdata.Data has the output data
  //use it, display it, or discard it
}

Upvotes: 2

Related Questions