Background: I get a lot of traffic looking for details on running a process and collecting the process output. If you haven’t already done so, you should read “How to use System.Diagnostics.Process correctly“. It outlines the major pitfalls of using this class. Another post on “Using the ProcessRunner class” will demonstrate using the helper class I wrote: CSharpTest.Net.Processes.ProcessRunner.

So again I find myself writing about using System.Diagnostics.Process and the Process.Start method. This time it’s not to provide any more information, but rather a simplified example. This example will build upon the previous two posts “How to search the environment’s path for an exe or dll“, and “How to correctly escape command line arguments” to use those methods for solving those needs. Otherwise this example is a relatively stand-alone method for running a process and capturing it’s output.

    /// <summary>
    /// Runs the specified executable with the provided arguments and returns the process' exit code.
    /// </summary>
    /// <param name="output">Recieves the output of either std/err or std/out</param>
    /// <param name="input">Provides the line-by-line input that will be written to std/in, null for empty</param>
    /// <param name="exe">The executable to run, may be unqualified or contain environment variables</param>
    /// <param name="args">The list of unescaped arguments to provide to the executable</param>
    /// <returns>Returns process' exit code after the program exits</returns>
    /// <exception cref="System.IO.FileNotFoundException">Raised when the exe was not found</exception>
    /// <exception cref="System.ArgumentNullException">Raised when one of the arguments is null</exception>
    /// <exception cref="System.ArgumentOutOfRangeException">Raised if an argument contains '\0', '\r', or '\n'
    public static int Run(Action<string> output, TextReader input, string exe, params string[] args)
    {
        if (String.IsNullOrEmpty(exe))
            throw new FileNotFoundException();
        if (output == null)
            throw new ArgumentNullException("output");

        ProcessStartInfo psi = new ProcessStartInfo();
        psi.UseShellExecute = false;
        psi.RedirectStandardError = true;
        psi.RedirectStandardOutput = true;
        psi.RedirectStandardInput = true;
        psi.WindowStyle = ProcessWindowStyle.Hidden;
        psi.CreateNoWindow = true;
        psi.ErrorDialog = false;
        psi.WorkingDirectory = Environment.CurrentDirectory;
        psi.FileName = FindExePath(exe); //see http://csharptest.net/?p=526
        psi.Arguments = EscapeArguments(args); // see http://csharptest.net/?p=529

        using (Process process = Process.Start(psi))
        using (ManualResetEvent mreOut = new ManualResetEvent(false),
             mreErr = new ManualResetEvent(false))
        {
            process.OutputDataReceived += (o, e) => { if (e.Data == null) mreOut.Set(); else output(e.Data); };
            process.BeginOutputReadLine();
            process.ErrorDataReceived += (o, e) => { if (e.Data == null) mreErr.Set(); else output(e.Data); };
            process.BeginErrorReadLine();

            string line;
            while (input != null && null != (line = input.ReadLine()))
                process.StandardInput.WriteLine(line);

            process.StandardInput.Close();
            process.WaitForExit();

            mreOut.WaitOne();
            mreErr.WaitOne();
            return process.ExitCode;
        }
    }

Though most people won’t require writing to std::in, it is demonstrated here. If you do decide to remove this be careful that you still set psi.RedirectStandardInput to true, and call process.StandardInput.Close() before you call process.WaitForExit().

If you want to read a single character at a time (so as not to wait for a new line) that can also be done. Unfortunately it does require more work. The following sample class reads one character at a time and writes it to the console:

    //In the above example you would remove the event subscription and the call to
    //process.BeginOutputReadLine() and replace it with the following:
    new ReadOutput(process.StandardInput, mreOut);

    private class ReadOutput
    {
        private StreamReader _reader;
        private ManualResetEvent _complete;

        public ReadOutput(StreamReader reader, ManualResetEvent complete)
        {
            _reader = reader;
            _complete = complete;
            Thread t = new Thread(ReadAll);
            t.Start();
        }

        void ReadAll()
        {
            int ch;
            while(-1 != (ch = _reader.Read()))
            {
                Console.Write((char) ch);
            }
            _complete.Set();
        }
    }

Always remember you need two threads reading both std::out and std::err or you run into the possibility of hanging the process.

Comments