外部プロセスの標準出力を非同期で取得する

C#でprocessクラスを使って外部プロセスを起動したときに、プロセスが出力する標準出力と標準エラー出力を非同期で取得する方法のまとめです。

コードは以下の通りです。

using (var process = new Process())
{
    process.StartInfo = new ProcessStartInfo()
    {
        FileName = @"d:\sample.exe"

        UseShellExecute = false,
        CreateNoWindow = false,

        RedirectStandardOutput = true, // ログ出力に必要な設定(1)
        RedirectStandardError = true,

        Arguments = "/mode debug"
    };
    
    process.OutputDataReceived += this.OnStdOut; // ログの出力先の指定(2)
    process.ErrorDataReceived += this.OnStdError;

    process.Start();

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

    process.WaitForExit();

    process.CancelOutputRead(); // 使い終わったら止める
    process.CancelErrorRead();
    
    Console.WriteLine("ExitCode = " + process.ExitCode;
}
// 標準出力のほう
public void OnStdOut(object sender, DataReceivedEventArgs e)
{
    Console.WriteLine("[StdOut] " + e.Data);
}

// エラー出力のほう
public void OnStdError(object sender, DataReceivedEventArgs e)
{
    Console.WriteLine("[StdError] " + e.Data);
}

コードを書く時の注意点ですが、

  1. UseShellExecuteは必ずfalseを指定する。*1
  2. RedirectStandardOutput、RedirectStandardOutputへtrueを設定し、プロセスの出力を拾うように指定する。
  3. OutputDataReceivedへ出力するためのメソッドやラムダ式を指定する。

また、非同期故にDisposeされた瞬間の前後にOutputDataReceivedが呼ばれる時があるので利用を終了するときに設定値はCancelXXXXを呼び出して解除したほうが無難です。解除しない場合、使い終わった後でもプロセスから出力があればイベントが拾ってしまうケースがあるからです。*2あと、当方環境ではあまりに高速にプロセスが起動 → 終了するとログが表示されないケースがあることを確認しています。こちらは、プロセスのストリームが全部出きったかどうか確認する術が無いためCancelOutputReadの呼び出しを少し待つなど工夫が必要かもしれません。

*1:指定しないとRedirectStandardOutput = trueを指定すると例外が発生してしまいます。

*2:ラムダ式でクラス内の変数を参照していて呼び出しクラス自体の使用が終了してしますと、しばらくしてNullReferenceExceptionが出てしまうなどはありがちです。