2012年12月7日金曜日

外部プロセス起動

Javaで外部コマンド(プロセス)を実行する方法について。

実行できるのは実行ファイル(Windowsでいうと拡張子がexeやbat等のファイル、UNIXでいうと実行権限があるファイルsh)であり、コマンドプロンプトシェルが直接解釈するコマンド(WindowsでいうとDOSの内部コマンドdirやecho)は直接は実行できない。

コマンド(外部プロセス)を実行すると、そのプロセスは非同期で動き続ける。


Runtime

JDK1.4までは、外部プロセスの起動にRuntimeクラスを使う。
JDK1.5以降でも使えるが、JDK1.5以降ではRuntimeの内部でProcessBuilderを使っているので、素直にProcessBuilderを使う方がよい。

「java -version」を実行する例

	Runtime r = Runtime.getRuntime();  	Process p = r.exec("java -version");  	//Processの使い方は後述

コマンドの文字列を引数付き(スペース区切り)で渡すと、そのコマンドが実行される。

ただ、Windowsではスペース入りのファイル名やディレクトリ名が使えたりするので、その場合は変な所で区切られることになってしまう。
そういう場合は、配列形式で渡す。

	Runtime r = Runtime.getRuntime();  //	Process p = r.exec(new String[]{ "java", "-version" });  	Process p = r.exec(new String[] {  		"C:\\Program Files\\Java\\j2re1.4.2_13\\bin\\java",  		"-version" });  	//Processの使い方は後述

echo zzz」を実行する例

Windowsのechoコマンドプロンプトが解釈するコマンドなので、直接実行することは出来ない。(実行しようとすると例外が発生する)

java.io.IOException: CreateProcess: echo zzz error=2  	at java.lang.Win32Process.create(Native Method)

こういうものは、コマンドプロンプトを起動してやれば、その上で実行できる。(WindowsXPの場合、cmd.exeがコマンドプロンプト本体)
cmdに/cオプションを付けて実行すると コマンドを実行した後に終了してくれるので、それを利用する。

	Runtime r = Runtime.getRuntime();  	Process p = r.exec("cmd /c echo zzz");  //	Process p = r.exec(new String[]{ "cmd", "/c", "echo", "zzz" });  	//Processの使い方は後述


ちなみにDOSでは、スラッシュ区切りのオプションはスペースを入れずに指定することが出来る

C:\> cmd/c echo zzz  zzz

しかしRuntime#exec()では、あくまでスペースで区切る必要がある。


環境変数の指定

Runtime#exec()の第2引数には、環境変数を指定することが出来る。
省略時はnullを指定したのと同じ状態になっており、その場合は実行中のJavaVMと同じ環境変数が指定された扱いになる。

自分で環境変数を指定するには、以下のようにする。

	String[] env = new String[2];  	env[0] = "TEST=サンプル";  	env[1] = "PATH=" + System.getProperty("java.library.path");    	Runtime r = Runtime.getRuntime();  	Process p = r.exec("cmd /c echo %TEST%", env);  	//Processの使い方は後述

exec()の第2引数に渡すのは、使用する環境変数全て。
「cmd /c」の場合は、環境変数PATHも必要(のような感触)なので、それも自分で入れてやる必要がある。

デフォルトの環境変数はSystem#getenv("PATH")といったメソッドで取得したい感じがするが、JDK1.4ではgetenv()は非推奨メソッドであり、しかも内部が実装されていないので使えない。
(環境変数PATHの場合はSystem.getProperty("java.library.path")で取得できるからまだいいけど…)

今回の例では、 コマンド(exec()の第1引数)の中で「%TEST%」という形式で環境変数を展開しているが、これは「cmd」だから出来ること。(cmd.exeが解釈してくれる)
「java %TEST%」とやっても展開されないので注意。(「cmd /c java %TEST%」なら可)


カレントディレクトリーの指定

Runtime#exec()の第3引数には、実行中のカレントディレクトリーを指定することが出来る。
省略時はnullを指定したのと同じ状態になっており、その場合はJavaVMの作業ディレクトリーが指定される(ものと思われる)。
すなわち、System.getProperty("user.dir")で取得できるディレクトリーと同じ。

自分でカレントディレクトリーを指定するには、以下のようにする。

	File dir = new File("C:/temp");    	Runtime r = Runtime.getRuntime();  	Process p = r.exec("cmd /c cd", null, dir); //Windowsのcdコマンドは、カレントディレクトリーを表示する  	//Processの使い方は後述

ProcessBuilder

ProcessBuilderはJDK1.5で新設されたクラス。Runtimeも内部ではこのクラスを使用するようになった。

「java -version」を実行する例

	ProcessBuilder pb = new ProcessBuilder("java", "-version");  	Process p = pb.start();  	//Processの使い方は後述
	ProcessBuilder pb = new ProcessBuilder();  	pb.command("java", "-version");  	Process p = pb.start();  	//Processの使い方は後述

これらは可変長引数であり、いくつでも引数を指定できる。
(逆に、一つの文字列内にずらずらとコマンド・オプションを書くことは出来なくなった。が、それはスペース入りのパスに対応できないから、別に要らないだろう)

他に、以下のような書き方も出来る。

	ProcessBuilder pb = new ProcessBuilder(new String[] { "java", "-version" });
	List<String> list = new ArrayList<String>();  	list.add("java");  	list.add("-version");  	ProcessBuilder pb = new ProcessBuilder(list);

echo zzz」を実行する例

この辺りはRuntimeを使う場合と同じで、「cmd /c」を呼ぶだけ。

	ProcessBuilder pb = new ProcessBuilder("cmd", "/c", "echo", "zzz");  	Process p = pb.start();  	//Processの使い方は後述

環境変数の指定

独自の環境変数は、Runtimeより扱いやすくなった。

	ProcessBuilder pb = new ProcessBuilder("cmd", "/c", "echo", "%TEST%");    	Map<String, String> env = pb.environment();	//環境変数を取得  	env.put("TEST", "sample");    	Process p = pb.start();  	//Processの使い方は後述

ProcessBuilder#environment()により、デフォルトの環境変数全てが取得できる。
こういったメソッドで返すマップは、よくあるパターンでは変更不可能(あるいは変更しても元のオブジェクトには影響しない)なのだが、ProcessBuilderの場合は、これを書き換えると元のオブジェクトに反映される。
なのでこのマップを使って、一部の環境変数を書き換えたり新しい環境変数を追加したり、あるいは全て削除(clear())したりすることが出来る。


カレントディレクトリーの指定

実行中の作業ディレクトリー(カレントディレクトリー)を指定するには、以下のようにする。

	ProcessBuilder pb = new ProcessBuilder("cmd", "/c", "cd"); //Windowsのcdコマンドは、カレントディレクトリーを表示する    	File dir = new File("C:/temp");  	pb.directory(dir);    	Process p = pb.start();  	//Processの使い方は後述

標準エラーの、標準出力へのマージ

実行したプロセスの標準出力や標準エラーの内容を取得することができる(後述)のだが、
ProcessBuilderでは、標準エラーに出力されたものを標準出力にマージ(統合/リダイレクト)し、標準出力から読み取るだけでどちらの内容も取得できるようにすることが可能。

	ProcessBuilder pb = new ProcessBuilder("java", "-version");    	pb.redirectErrorStream(true); //デフォルトはfalse:マージしない(標準出力と標準エラーは別々)    	Process p = pb.start();  	//Processの使い方は後述

標準出力と標準エラーの内容が順不同で入り混じる可能性はあるが…
どちらかしか出力されないと分かっているような場合や、出力内容は問わないというような場合なら使い道があるかも。


Process

ProcessBuilder#start()(あるいはRuntime#exec())によって 別プロセスが起動し、非同期で実行される。
(そのstart()やexec()の戻り値である)Processを使うことによって、起動したプロセスの実行結果を取得することが出来る。

プロセスの終了待ち

起動したプロセスが終了するのを待つには、以下のようにする。

	ProcessBuilder pb = new ProcessBuilder(〜);  	Process p = pb.start();    	int ret = p.waitFor();  	System.out.println("戻り値:" + ret);
	p.waitFor();  	int ret = p.exitValue();  	System.out.println("戻り値:" + ret);

戻り値は、起動したプロセス(コマンド)の戻り値(終了コード)。一般的に、正常終了なら0が返る。


標準出力・標準エラーの読み込み

起動したプロセスが標準出力や標準エラーに書き込んだ内容は、InputStreamを介して取得することが出来る。

	Process p = pb.start();    前待ち	p.waitFor();    	InputStream is = p.getInputStream();	//標準出力  	printInputStream(is);  	InputStream es = p.getErrorStream();	//標準エラー  	printInputStream(es);    後待ち	p.waitFor();
	public static void printInputStream(InputStream is) throws IOException {  		BufferedReader br = new BufferedReader(new InputStreamReader(is));  		try {  			for (;;) {  				String line = br.readLine();  				if (line == null) break;  				System.out.println(line);  			}  		} finally {  			br.close();  		}  	}

これらのInputStream自体は、プロセスの終了待ちのでも終了でも使うことが出来る。
のだが、このままでは上手く行かないケースがある


後から読み込むと駄目な理由

個人的には、プロセスが終了してから、書かれた内容をゆっくり取得したい。(起動したプロセスの標準入力へのデータ渡しとかは気にしないからw)

	Process p = pb.start();    	p.waitFor();    	printInputStream(p.getInputStream());  	printInputStream(p.getErrorStream());

しかし、別プロセスからの出力量が多いときは、このプログラムは止まってしまう。
(試してみた感じでは、出力側(起動したプロセス)が標準出力or標準エラーのどちらかに512文字(たぶんUNICODE(UTF16)なので、1024バイト)を超えて出力するとNG)

外部プロセスは標準出力(や標準エラー)にがんがん書き込みたいのだが、その受け側であるProcessのInputStreamはバッファーサイズに限りがある
したがって、データ量がそのバッファー内に収まっている間はいいのだが、ストリームから読み出してやらないとバッファーが足りなくなって、それ以上読み込めなくなる。
InputStreamが読み込めないと、書き込み側(外部プロセス)がブロッキング(一時停止)される。したがって、そのプロセスは終了できないことになる。

でも上記のプログラムでは、プロセスの終了待ち(waitFor())をしているだけで、その間にInputStreamから読み込む処理は無い。
したがって永久にバッファーは空かず、起動したプロセスも終了せず、永久待ち=プログラム停止状態になる。(いわゆるデッドロック状態)


先に読み込むと駄目な理由

では、プロセス終了待ちをする前に読み込めば大丈夫か。

	Process p = pb.start();    	printInputStream(p.getInputStream());  	printInputStream(p.getErrorStream());    	p.waitFor();

これも、標準エラーにがんがん書き込まれると、同じ状態になる。
先に標準出力が全て来るまでループしているが、その間に標準エラーに書かれてバッファーが一杯になると、外部プロセスはブロッキングされる。
すると標準出力にもそれ以上データは吐かれなくなり、待ち状態に入ってしまう。

じゃあ読み込み側で、InputStreamとErrorStreamを交互に読むようにしてみるか。

	public static void print2(InputStream is, InputStream es) throws IOException {  		BufferedReader br = new BufferedReader(new InputStreamReader(is));  		BufferedReader er = new BufferedReader(new InputStreamReader(es));  		try {  			boolean b = true, e = true;  			while (b || e) {  				String line = br.readLine();	//標準出力から読み込み  				if (line != null) {  					System.out.println(line);  					b = true;  				} else {  					b = false;  				}    				line = er.readLine();		//標準エラーから読み込み  				if (line != null) {  					System.err.println(line);  					e = true;  				} else {  					e = false;  				}  			}  		} finally {  			try {  				br.close();  			} finally {  				er.close();  			}  		}  	}
	Process p = pb.start();    	print2(p.getInputStream(), p.getErrorStream());    	p.waitFor();

しかしこれでも、ダメなケースがある。
標準入力と標準エラーが交互に出力されてくるならこれでいいが、どちらか片方が出力されないと、そこでreadLine()が入力待ちに入ってしまう。
また、改行抜きで大量のデータが来た場合もreadLine()で止まってしまう模様。

readLine()==nullという判定でなくready()を使えば、前者に対しては解決できるか?

	public static void print2_(InputStream is, InputStream es) throws IOException {  		BufferedReader br = new BufferedReader(new InputStreamReader(is));  		BufferedReader er = new BufferedReader(new InputStreamReader(es));  		try {  			boolean b = true, e = true;  			while (b || e) {  				if (br.ready()) {  					String line = br.readLine();  					if (line != null) {  						System.out.println(line);  						b = true;  					} else {  						b = false;  					}  				}  				if (er.ready()) {  					String line = er.readLine();  					if (line != null) {  						System.err.println(line);  						e = true;  					} else {  						e = false;  					}  				}  			}  		} finally {  			try {  				br.close();  			} finally {  				er.close();  			}  		}  	}

これで確かに全データは読めるようになったが、しかし、プロセスが終了したことを判断できなくて、これまた永久待ちになってしまう。
(ready()は、データが無ければ(プロセスが終了していても?)素直にfalseを返す。つまり最終的には常にfalseなのでフラグをクリアするタイミングが無く、終了できない。
「br.ready()とer.ready()双方がfalseだったら終了」というループ条件にしたら…データが両方とれなかった瞬間があれば、その時点でループが終了してしまう(苦笑))


データを全て捨て去ってもよい場合の解決策

これらの問題は標準エラーと標準出力を別々に取得しようとするから起こるのであって、どちらかしかデータが出力されないなら、そちらだけを取得することで、問題は発生しない。
ProcessBuilderなら標準エラーを標準出力にリダイレクトしてしまうのもひとつの解決方法だろう。

出力内容を捨ててとにかくプロセス終了を待つなら、以下のようなプログラムでいけそう。

	ProcessBuilder pb = new ProcessBuilder(〜);  	pb.redirectErrorStream(true);    	Process p = pb.start();    	InputStream is = p.getInputStream();  	try {  		while(is.read() >= 0); //標準出力だけ読み込めばよい  	} finally {  		is.close();  	}    //	p.waitFor();  	System.out.println("戻り値:" + p.exitValue());

この場合、waitFor()で待つ必要は無い。なぜなら、is.read()が-1を返すのはストリームがクローズされた場合のみ。すなわち、起動したプロセスが終了したことを意味するから。


読み込みのスレッド化

という訳で、標準出力・標準エラーの内容が両方とも欲しいのであれば、一番確実なのは、標準出力・標準エラーからの読み込みをスレッド化してしまう方法かもしれない。

import java.io.*;  import java.util.*;    /**   * InputStreamを読み込むスレッド   */  public class InputStreamThread extends Thread {    	private BufferedReader br;    	private List<String> list = new ArrayList<String>();    	/** コンストラクター */  	public InputStreamThread(InputStream is) {  		br = new BufferedReader(new InputStreamReader(is));  	}    	/** コンストラクター */  	public InputStreamThread(InputStream is, String charset) {  		try {  			br = new BufferedReader(new InputStreamReader(is, charset));  		} catch (UnsupportedEncodingException e) {  			throw new RuntimeException(e);  		}  	}    	@Override  	public void run() {  		try {  			for (;;) {  				String line = br.readLine();  				if (line == null) 	break;  				list.add(line);  			}  		} catch (IOException e) {  			throw new RuntimeException(e);  		} finally {  			br.close();  		}  	}    	/** 文字列取得 */  	public List<String> getStringList() {  		return list;  	}  }
	Process p = pb.start();    	//InputStreamのスレッド開始  	InputStreamThread it = new InputStreamThread(p.getInputStream());  	InputStreamThread et = new InputStreamThread(p.getErrorStream());  	it.start();  	et.start();    	//プロセスの終了待ち  	p.waitFor();    	//InputStreamのスレッド終了待ち  	it.join();  	et.join();    	System.out.println("戻り値:" + p.exitValue());    	//標準出力の内容を出力  	for (String s : it.getStringList()) {  		System.out.println(s);  	}  	//標準エラーの内容を出力  	for (String s : et.getStringList()) {  		System.err.println(s);  	}

この方法なら、標準出力用のbr.readLine()がブロッキングされても、別スレッドで標準エラー用のbr.readLine()が処理されるので問題ない。
また、並行してプロセス終了待ち(p.waitFor())も実行されているので、起動したプロセスが終了したら、InputStreamも終了する。
するとbr.readLine()はnullを返すので、スレッドも無事に終了する。
一応タイムラグはあるかもしれないので、Thread#join()を使ってスレッドの終了待ちも行う。


リダイレクションの利用

標準出力・標準エラーをスレッドで読み込む方法の欠点は、ずばり「スレッドを使用する」こと。
例えばWebLogic8.1ではアプリケーションがスレッドを生成することが許されていないので、プロセス起動時にスレッドを起こす方法は使えない。

そういう時は、標準出力・標準エラーの内容を ファイルにリダイレクトしてしまう方法も考えられる。
そうすればプロセスを起動したアプリ側のInputStreamには何も渡ってこないので、ブロックがどうこうという問題から解放される。
DOSのリダイレクションUNIXのリダイレクション

//ダメな例1  	ProcessBuilder pb = new ProcessBuilder("javac", "-version", ">", "c:/t.txt");
//ダメな例2  	ProcessBuilder pb = new ProcessBuilder("javac", "-version", "> c:/t.txt");

リダイレクトの記号のつもりで「>」を入れても、ただ単にコマンドの引数として解釈されてしまう。

リダイレクトの処理はコマンドプロンプトが行う(と思われる)ので、これもcmd.exeを介してやる必要がある。

//標準出力・標準エラー共にファイルへ出力してしまう例  	ProcessBuilder pb = new ProcessBuilder(  		"cmd", "/c",  		"javac", "-help",  		">", "c:/temp/stdout.txt", "2>", "c:/temp/stderr.txt"  	);  	Process p = pb.start();    	//標準出力・標準エラーのストリームを読み込む必要は無い    	int ret = p.waitFor();  	System.out.println("戻り値" + ret);
//全て標準出力へ統合する例 …素直にredirectErrorStream()を使うべきだと思うが^^;  	ProcessBuilder pb = new ProcessBuilder(  		"cmd", "/c",  		"javac", "-help",  		"2>&1"  	);  	Process p = pb.start();    	printInputStream(p.getInputStream()); //標準出力だけ読み込む    	int ret = p.waitFor();  	System.out.println("戻り値" + ret);
//標準エラーだけファイルへ出力する例  	ProcessBuilder pb = new ProcessBuilder("javac", "-help");    	//実行したいコマンドの前後に、コマンドプロンプト起動を設定  	List<String> clist = pb.command();  	clist.add(0, "cmd");  	clist.add(1, "/c");  	clist.add("2>>");	//標準エラーを既存ファイルへ追加出力(append)  	clist.add("c:/temp/log.txt");    	Process p = pb.start();    	printInputStream(p.getInputStream()); //標準出力だけ読み込む    	int ret = p.waitFor();  	System.out.println("戻り値" + ret);

ストリームのクローズについて

Process#getInputStream()・getErrorStream()で取得したストリームや、それをラップしたBufferedReaderは、使い終わったらクローズすべきなのは確か。[2010-12-26]
しかし明示的にクローズしなくても、そんなに害は無いと思われる。

まず、ラップしているBufferedReader・InputStreamReaderはInputStreamをバッファリングしているだけなので、使われなくなったらGCで回収されるから特に問題ない(とは言っても明示的にクローズすべきではあるが)。

Processから取得した標準出力・標準エラーのストリームは、内部でファイルディスクリプターを(たぶんOSから取得して)使っているので、クローズしなかったらリソースの枯渇問題になるだろう。
しかし個別にクローズしなくても、ProcessインスタンスがGCで回収される際にファイナライザーでクローズされる。
また、これまでの例では表立って使っていないけれど、Process#getOutputStream()で取得できるストリームも、Process内部ではオープンされている。
したがってこれもクローズすべきだが、明示的に取得してクローズしない限りは、ファイナライザーでクローズされる。
(getInputStream()だけ取得してgetErrorStream()を取得しない場合も同様)
結局はファイナライザーで全てがクローズされるので、個別にクローズしなくても、最終的には問題ないと考えられる。
(メモリーが有り余っていてGCが全く実行されないと、ファイルディスクリプターが解放されなくてリソースが枯渇する可能性は考えられるが(苦笑))
(他にファイナライザーが実行されないのは、プログラムが直接終了する場合。この場合はたぶん確保したファイルディスクリプターは全て解放されると思う。さすがに今どきのJavaVMがそこでリークするとは思えない^^;)

だから本当はProcessクラス自体にclose()があれば一番いいと思うけれど、無いので仕方ない。
ちなみにファイナライザーProcess#finalize()はprotectedなので、外部から明示的に呼び出すことは出来ない。


プロセスの強制終了

Process#waitFor()は、プロセスが終了するまで無限に待つ。

一定時間だけ待つ、waitFor(long timeout)といった(Object#wait(long)のような)メソッドがあれば便利だと思うのだが、そういうものは無い。
プロセスを強制終了させるにはProcess#destroy()を呼び出せばよいので、自分でタイマーを起動して、一定時間後にdestroy()するしかないようだ。

class class ProcessDestroyer extends TimerTask {    	private Process p;    	public ProcessDestroyer(Process p) {  		this.p = p;  	}    	@Override  	public void run() {  		p.destroy(); //プロセスを強制終了  	}  }
	ProcessBuilder pb = new ProcessBuilder("calc"); //Windowsの「電卓」  	Process p = pb.start();    	TimerTask task = new ProcessDestroyer(p);  	Timer timer = new Timer("プロセス停止タイマー");  	timer.schedule(task, TimeUnit.SECONDS.toMillis(3));	//3秒後にProcessDestroyer#run()が呼ばれる    	for (;;) {  		try {  			p.waitFor();	//プロセスの終了待ち  			break;  		} catch (InterruptedException e) {  			e.printStackTrace();    			//waitFor()はInterruptedExceptionが発生する可能性があるが、  			//今回の例では、その場合もプロセスの終了待ちを繰り返す。  			//(プロセスの強制終了とInterruptedExceptionは無関係)  		}  	}    	timer.cancel();	//タイマーのキャンセル(必須)  	System.out.println("戻り値:" + p.exitValue());

WindowsXPの場合、destroy()による強制終了は戻り値が1になるようだ。(例外が発生するわけではない)
ちなみに、開いているウィンドウ(この例で言えば「電卓」)をタスクマネージャから「プロセスの終了」で強制終了させると、同じく1が返ってきた。


exitValue()を利用したタイムアウト

タイマー(Timerクラス)を使ってタイムアウトさせる方法は理に適っていると思うけれども、やはりスレッドを使う点がちょっとネック。

泥臭いけど、exitValue()を使ってポーリングするという方法もあるようだ。
exitValue()はプロセスの戻り値を返すメソッドだが、プロセスが終了していないと例外が発生する。なので、例外の有無でもってプロセスが終了したかどうか判定できる。

	Process p = pb.start();    	long begin = System.currentTimeMillis();  //	System.out.println(new Date(begin));    	for (;;) {  		try { Thread.sleep(100); } catch (InterruptedException e) {}  		try {  			p.exitValue();  		} catch (IllegalThreadStateException e) {  			//exitValue()でプロセスが終了していないと この例外が発生する    			long now = System.currentTimeMillis();  			if (TimeUnit.MILLISECONDS.toSeconds(now - begin) < 10) { //10秒以上経過したかどうか  				continue; //forループを繰り返す  			}  //			System.out.println("タイムアウト!" + new Date(now));  			p.destroy(); //プロセスを強制終了  			break; //forループを抜ける  		}    //		System.out.println("正常終了");  		break; //forループを抜ける  	}

0 件のコメント:

コメントを投稿