アプリケーションの標準出力をテキストファイルに出力してみた(1)

2023年7月23日

どもです。

最近、「アプリケーションの標準出力をテキストファイルに残したい!」と思う場面に遭遇しました。この問題を解決する方法はいくつかありますが、最も有効と思われる方法は、「標準出力のリダイレクト」かと思います。しかしこの方法では、実行中に標準出力の内容を確認ができない、という問題があります。というのも、私がやりたいことが、

  • アプリケーションの標準出力の内容を、ファイルに出力したい!
  • アプリケーション実行中も、標準出力の内容を確認したい!

だからです。

これらの「やりたいこと」は、「標準出力をリダイレクトする」方法では、同時に実現ができません。そこで、この課題を解決する方法を考えてみた結果を書きます。

0. 作業環境

今回のエントリでは、以下の環境で作業を行います。

開発環境
項目 内容
OS Windows10 Pro(22H2)
CPU i7-8700
メモリ 16GB
IDE Visual Studio Commnuity 2022(64bit)
version 17.3.6

1.方法

やってみた方法ですが、実にありふれた方法です。すなわち、

  • 目的のプロセスを、別プロセスとして起動する。
  • 起動した別プロセスの標準出力の内容を、呼び出し元プロセスで受け取る。
  • 受け取った標準出力の内容を、ファイルに出力する。

という方法です。

2. 実際にやってみた

先に挙げた方法を、1つ1つ順番に実相していきます。

2.1. 目的のプロセスを、別プロセスとして起動する。

「別プロセスとして起動する」というのは、先述の通りありふれた方法です。ネットで調べると、非常に多くの情報が得られるので、詳細はそれらの方法に任せます。ココでは、基本的な実装のみ紹介します。

「別プロセスとして起動する」ための実装は、以下です。

class ProcessRunner : AOutputProcRunner
{
	protected virtual void SetupStartInfo(string procName, string procArgs)
	{
		_procStartInfo = new ProcessStartInfo()
		{
			FileName = procName,
			UseShellExecute = false,
		};

		protected virtual void SetStartInfo(Process proc)
		{
			proc.StartInfo = _procStartInfo;
		}

		protected virtual void Run()
		{
			using (var proc = new Process())
			{
				SetStartInfo(proc);
				RunProcess(proc);
			}
		}

		protected virtual void RunProcess(Process proc)
		{
			proc.Start();
			proc.WaitForExit();
		}
	}
}

この実装については、ネットで調べれば非常に多くの情報が得られます。

今回は、この実装をベースに話を進めます。

2.1. 起動した別プロセスの標準出力の内容を、呼び出し元プロセスで受け取る。

次に、標準出力の内容の受け取りです。

起動した別プロセスの内容を呼び出し元で受け取るためには、別プロセス起動時の設定に少し追加します。即ち、以下のように実装を変更します。

class ProcessRunner : AOutputProcRunner
{
	protected virtual void SetupStartInfo(string procName, string procArgs)
	{
		_procStartInfo = new ProcessStartInfo()
		{
			FileName = procName,
			UseShellExecute = false,
			RedirectStandardOutput = true	// このコードを追加
		};
	}

	protected virtual void Run()
	{
		using (var proc = new Process())
		{
			SetStartInfo(proc);
			SetupEventHandler(proc);	// このコードを追加/イベントハンドラの設定
			RunProcess(proc);
		}
	}

	//イベントハンドラの設定/この関数を追加
	protected void SetupEventHandler(Process proc)
	{
		_outputDataReceiveEventHandler = new DataReceivedEventHandler(StandardDataReceived);
		proc.OutputDataReceived += _outputDataReceiveEventHandler;
	}

	//イベントハンドラ本体/この関数を追加
	protected virtual void StandardDataReceived(object sender, DataReceivedEventArgs e)
	{
		ReceiveStandardData?.Invoke(sender, e);
	}
}

この実装についても、やはりネットで非常に多くの情報が得られます。そのため、詳細は省きます。

2.3. 受け取った標準出力の内容を、ファイルに出力する。

最後に、「受け取った標準出力の内容を、ファイルに出力する」方法です。これは、以下の手順で実施します。すなわち、

  1. 受け取った標準出力の内容を、コレクションに追加する。
  2. プロセスの実行が完了したら、ファイルに出力する。

という手順です。

なお、これらの処理は別クラスで実装してみます。実装するクラス(一部)は、以下のようになります。

class OutputDataRecorder
{
	protected List<string> _receivedDataCollection;

	public string OutputFilePath { get; set; }

	public OutputDataRecorder()
	{
		OutputFilePath = string.Empty;
		_receivedDataCollection = new List<string>();
	}

	//受け取った標準出力の内容を、コレクションに追加する。
	public void DataReceivedEventHandler(object sender, DataReceivedEventArgs e)
	{
		string receiveData = e.Data;
		_receivedDataCollection.Add(receiveData);
	}

	//プロセスの実行が完了したら、ファイルに出力する。
	public void DataReceiveFinishedEventHandler(object sender, EventArgs e)
	{
		Flush();
	}

	public void Flush()
	{
		using (var writer = new StreamWriter(OutputFilePath, false))
		{
			Flush(writer);
		}
	}

	public void Flush(StreamWriter outputStream)
	{
		foreach (var item in _receivedDataCollection)
		{
			if (null != item)
			{
				outputStream.WriteLine(item);
			}
		}
	}
}

ここでは、基本的な実装のみ記載しています。エラー処理、例外処理は省略しています。

なお、「プロセスの実行が完了したら、ファイルに出力する」ためには、当然「プロセスの実行が完了した」通知を受け取れるようにする必要があります。そのために、呼び出し元のクラスの実装にも追加が必要です。追加する内容は、以下の通りです。

class ProcessRunner : AOutputProcRunner
{
	//イベントハンドラの設定/この関数を追加
	protected void SetupEventHandler(Process proc)
	{
		_outputDataReceiveEventHandler = new DataReceivedEventHandler(StandardDataReceived);
		proc.OutputDataReceived += _outputDataReceiveEventHandler;

		_exitEventHandler = new EventHandler(DataReceiveFinished);	//このコードを追加
		proc.EnableRaisingEvents = true;	//このコードを追加/終了イベントの受信を有効にする。
		proc.Exited += _exitEventHandler;	//このコードを追加/イベントハンドラの追加
	}

	//イベントハンドラ本体/この関数を追加
	protected override void DataReceiveFinished(object sender, EventArgs e)
	{
		ReceiveFinished?.Invoke(sender, e);
	}
}

あとは、この「ReceiveFinished」に「DataReceiveFinishedEventHandler」を登録すればOK!です。

3. まとめ

今回は、プロセスの標準出力をファイルに出力する方法について書きました。

本文中では、ファイルに出力する実装のみ記載しており、このままでは標準出力の内容は表示されません。しかし、「OutputDataRecorder」クラスと同じ要領で標準出力に出力するクラスを実装、イベントハンドラに登録することで、別プロセスの標準出力の内容を、標準出力とファイルの両方に、同時に出力することができます。

ではっ!

Ex. つづけます!

このエントリ、実はやりたいことが、まだ別にあります。それを書くまで、シリーズ化(!?)してみます。

…とは言っても、たぶん全部で3回程度です。

アプリケーションの標準出力をテキストファイルに出力してみた(2)