【C#】Processで起動したアプリの標準出力をリダイレクトする

By | 2011年9月15日

前回はProcessクラスを使い、C#で任意のアプリケーションを起動する例を見た。
今回は、その応用例としてProcessで起動したアプリの標準出力内容を起動元へリダイレクトしてみる。

まずは、テスト用に簡単なコンソールアプリをC++で作成した。そのソースコードは以下のようになる。

Win32ConsoleApplication.cpp

#include <tchar.h>
#include <windows.h>
#include <iostream>

int _tmain(int argc, _TCHAR* argv[])
{
	int cnt = 0;
	while(TRUE)
	{
		std::cout << cnt++ << std::endl;
		Sleep(1000);
	}

	return 0;
}

1秒おきにカウントアップした数字を出力していくだけの、非常に単純なアプリである。
このソースコードをビルドし、「Win32ConsoleApplication.exe」という名前のアプリを作成しておく。
次に、プロセス起動元のアプリを見ていく。このアプリは以下のような外観を持つWindowsFormである。

ソースコードは以下のようになる。

Form1.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Threading;
using System.Windows.Forms;
using System.Diagnostics;
using System.IO;

namespace WindowsApplication1
{
    public partial class Form1 : Form
    {
        private Thread thread = null;
        private Process process = null;

        public Form1()
        {
            InitializeComponent();
        }

        /// <summary>
        /// OnClosing処理
        /// </summary>
        /// <param name="e"></param>
        protected override void OnClosing(CancelEventArgs e)
        {
            // スレッド・プロセス終了
            if (thread != null)
            {
                if (!process.HasExited)
                {
                    process.Kill();
                }

                if (thread.IsAlive)
                {
                    thread.Abort();
                }

                thread = null;
            }

            base.OnClosing(e);
        }

        /// <summary>
        /// ボタン押下時の処理
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void button1_Click(object sender, EventArgs e)
        {
            if (thread == null)
            {
                thread = new Thread(new ThreadStart(delegate()
                {
                    process = new Process();

                    ProcessStartInfo startInfo = new ProcessStartInfo("Win32ConsoleApplication");
                    startInfo.CreateNoWindow = true;
                    startInfo.RedirectStandardOutput = true;
                    startInfo.UseShellExecute = false;

                    process.StartInfo = startInfo;
                    process.Start();

                    String line = null;

                    // StandardOutputからの入力待ちがあるため、別スレッド上で動かす必要がある
                    while ((line=process.StandardOutput.ReadLine()) != null)
                    {
                        // UI操作のため、表スレッドにて実行
                        this.BeginInvoke(new Action<String>(delegate(String str)
                        {
                            if (!this.Disposing && !this.IsDisposed)
                            {
                                this.textBox1.AppendText(line);
                                this.textBox1.AppendText(Environment.NewLine);
                            }
                        }), new object[] { line });
                    }
                }));
                thread.Start();
            }
        }
    }
}

やや複雑なコードとなった。
プロセスから標準出力を得るため、63-65行目でProcessStartInfoに以下の設定を行っている。

startInfo.CreateNoWindow = true;            // ウィンドウ表示を行わないとき、trueを設定
startInfo.RedirectStandardOutput = true;  // 標準出力をリダイレクト
startInfo.UseShellExecute = false;            // OSのシェルは使用しない

CreateNoWindowプロパティは「ウィンドウを表示させ無い」ときにtrue値を設定するため、少々ややこしい。
このように設定することで、ProcessのStandardOutputプロパティからStreamReaderを得ることができる。
ただ、そのままStreamReaderのReadLine()を呼び出すと、起動したアプリ側から標準出力が行われるまで処理をブロックしてしまう。
そのため61-85行目の一連の処理は別スレッド上で行うことにした。
そして標準出力が行われたときに、80-81行目でその内容をフォームのTextBoxへ出力しているのだが、
そのままでは別スレッドからUI操作することになってしまうので、BeginInvoke()メソッドを使い、UIの更新処理はUIスレッド上で処理されるようにしている。
アプリを起動し、ボタンを押下すると、以下のようにプロセス起動したアプリの出力がリダイレクトされる。

追記:
C++の起動される側のアプリのコードを以下のように書くと、標準出力にリダイレクトされなくなってしまった。

#include <tchar.h>
#include <windows.h>
//#include <iostream>
#include <stdio.h>

int _tmain(int argc, _TCHAR* argv[])
{
	int cnt = 0;
	while(TRUE)
	{
		//std::cout << cnt++ << std::endl;
		printf("%d\n", cnt++ );
		Sleep(1000);
	}

	return 0;
}

printf文だと駄目な様なのだが、何故そうなのかは分からなかった。

追記(2011/9/30):
後で気がついたのだが、この例のプログラムで標準エラーからも出力を読み取りたい場合、プロセスの起動スレッド上でまた別にスレッドを立てる必要がある。それならば、プロセスはUIスレッド上で起動させておき、自分で立てたスレッド上の処理はプロセスからの標準出力をただ読むだけにした方がすっきりする。
標準エラーから出力を読みたいときは、別個に標準エラー出力をただ読みだけの処理をするスレッドを立てれば良い。
以上を踏まえて書き直したソースを以下に示す。

Form1.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Diagnostics;
using System.IO;
using System.Threading;

namespace WindowsApplication1
{
    public partial class Form1 : Form
    {
        private Process process = null;
        private Thread thread = null;
        private bool runFlag = false;

        public Form1()
        {
            InitializeComponent();
        }

        /// <summary>
        /// OnClosing処理
        /// </summary>
        /// <param name="e"></param>
        protected override void OnClosing(CancelEventArgs e)
        {
            // スレッド・プロセス終了
            if (process != null)
            {
                runFlag = false;
                if (thread != null )
                {
                    if (thread.IsAlive)
                    {
                        thread.Join(3000);
                        if (thread.IsAlive)
                        {
                            thread.Abort();
                        }
                    }
                    thread = null;
                }

                if (!process.HasExited)
                {
                    process.Kill();
                    process.WaitForExit();
                }

                process = null;
            }

            base.OnClosing(e);
        }


        /// <summary>
        /// ボタン押下時の処理
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void button1_Click(object sender, EventArgs e)
        {
            if( process == null )
            {
                process = new Process();

                ProcessStartInfo startInfo = new ProcessStartInfo("Win32ConsoleApplication");
                startInfo.CreateNoWindow = true;
                startInfo.RedirectStandardOutput = true;
                startInfo.UseShellExecute = false;

                process.StartInfo = startInfo;
                process.Start();

                // 標準出力の読み取りスレッドを起動する
                (thread = new Thread(new ThreadStart(delegate()
                {

                    runFlag = true;
                    while (runFlag)
                    {
                        if (process != null && !process.HasExited)
                        {
                            String line = process.StandardOutput.ReadLine();

                            // UI操作のため、表スレッドにて実行
                            this.BeginInvoke(new Action<String>(delegate(String str)
                            {
                                if (!this.Disposing && !this.IsDisposed)
                                {
                                    this.textBox1.AppendText(str);
                                    this.textBox1.AppendText(Environment.NewLine);
                                }
                            }), new object[] { line });
                        }
                    }

                }))).Start();
                
            }
        }
    }
}

スレッドの処理を匿名メソッドで書いてしまったが、そもそもC#2.0からはprocess#BeginOutputReadLine()メソッドが使える。
そのためあえてこの例のようなプログラムを書く必要は全く無い。
BeginOutputReadLine()メソッドを使った例はこの記事で示している。


【C#】Processで起動したアプリの標準出力をリダイレクトする」への2件のフィードバック

  1. ピンバック: blog lesson 01 - 【C#】Processで起動したアプリの標準出力をリダイレクトする・改良版

  2. John Doe

    printf関数の場合パイプを通すとバッファリングされてしまうんですよね。

    返信

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です