あおいるかの倉庫

admin : new / comments / design / analysis

[.NET] コンソールアプリケーションの標準入出力をリダイレクトする

任意のコンソールアプリケーションを、標準入出力を自分のプログラムと接続して実行します。IDE の窓にコンパイラの出力が出てくるように、CUI の入出力を GUI に組み込むことができます。

標準出力の読み取りに関しては、process.StandardOutput と process.StandardError から読み出す同期式の方法と、イベントハンドラを設定する非同期式の方法があり、どちらかの方法をとる必要があります(混在はできません)。

同期式だと、単純な実装をするとデッドロックに陥る恐れがあります。

  • ReadLine、ReadToEndを使った場合
    (親は子が全部出力しきるまで終われない ⇔ バッファが一杯なり、子は親がある程度読み出さないと書き込めない)
  • 子が入力待ち
    (親は子が全部出力しきるまで終われない ⇔ 子は親から入力があるまで身動きできない)

というわけで、

非同期式での実装を試みる

OutputDataReceived、ErrorDataReceived にイベントハンドラを設定し、プロセスを起動したあと、BeginOutputReadLine や BeginErrorReadLine を呼びます。出力はイベントハンドラで受け取ります。

process = new Process();
process.StartInfo.FileName = "c:\hoge.exe";
process.StartInfo.UseShellExecute = false; // シェルは使えない
process.StartInfo.CreateNoWindow = true; // コマンドプロンプトを出さない
process.StartInfo.RedirectStandardOutput = true; // 標準出力をリダイレクトする
process.StartInfo.RedirectStandardInput = true; // 標準入力を   〃
process.StartInfo.RedirectStandardError = true; // 標準エラー出力を   〃
process.OutputDataReceived += new DataReceivedEventHandler(process_DataReceived);
process.ErrorDataReceived += new DataReceivedEventHandler(process_DataReceived);

process.Start();

// 非同期での読み取りの開始
process.BeginOutputReadLine();
process.BeginErrorReadLine();
    :
    :
void process_DataReceived(object sender, DataReceivedEventArgs e) {
  Console.WriteLine(e.Data);
}

↑この方法を使うと簡単に非同期で出力データを取れますが、問題もあります。この方法では、1行の出力のたびにイベントが発生しますが、アプリケーションによっては必ずしも欲しいタイミングでイベントが発生しないということです。

例えば子が時間のかかる処理をやっていて「処理中...(○○%)」みたいな出力をする場合、結局その行が終わる(改行文字が出力される)のは処理が 100% 完了してからで、それまでは親側のイベントは全く発生しないことになります。また、行の途中で入力待ちになってしまうかもしれません。

もちろん、コンパイラの出力のように一方的にズラズラ出てくるだけならこの方法でも問題ないのですが。

完全に期待したタイミングで出力を得るには、1字ずつの読み出しが可能な同期式の方法をマルチスレッドで実行するのがよさそうです。

マルチスレッドで読み出す

process = new Process();
process.StartInfo.FileName = "c:\hoge.exe";
process.StartInfo.UseShellExecute = false; // シェルは使えない
process.StartInfo.CreateNoWindow = true; // コマンドプロンプトを出さない
process.StartInfo.RedirectStandardOutput = true; // 標準出力をリダイレクトする
process.StartInfo.RedirectStandardInput = true; // 標準入力を   〃
process.StartInfo.RedirectStandardError = true; // 標準エラー出力を   〃

process.Start();

// 出力の監視を開始
stdoutWatcher = new Thread(new ParameterizedThreadStart(standardOutputWatchingThread));
stdoutWatcher.Start(process.StandardOutput);
stderrWatcher = new Thread(new ParameterizedThreadStart(standardOutputWatchingThread));
stderrWatcher.Start(process.StandardError);
    :
    :
// 出力監視スレッド
private void standardOutputWatchingThread(object parameter) {
  StreamReader reader = (StreamReader)parameter;
  while (!reader.EndOfStream) {
    Console.Write(reader.Read());
  }
  reader.Close();
}

↑これなら期待したタイミングで出力されるはずです。

Console.Write(reader.Read());

↑は

char[] buffer = new char[256];
int length = reader.Read(buffer, 0, buffer.Length);
if (length > 0) { ... }

↑とかすればある程度カタマリで読み出せますが、これだとポーリングになるので CPU 時間を無駄遣いします。length == 0 のときは Thread.Sleep(100); とかすればいいかも。

標準入力は?

process.StandardInput に Write とか WriteLine するだけ。

ちなみに

改行コードは \r\n です。

また、アプリケーションによっては出力にエスケープコードを使う場合もあるので注意が必要です(\b や \r を駆使して一度出力した内容を書き換えるとか)。

てか、そんなところまで考慮しちゃうと自前のコンソールが作れちゃいますね。

Comments

Comment Form

name :       pass :
body :
※ URL は ttp:// で始めてください(スパム対策)      secret     

おすすめタグ


すべてのタグ


最近の記事

RSSRSS

アーカイブ


全ての一覧

検索

プロフィール

プロフィール画像

syego

Gmail twitter ニコニコ動画 pixiv

My Profile by iddy

ついったー

[favorited] [more]

PR

FC2Ad

FC2ブログ

スポンサーニュース
  1. 無料アクセス解析