S-JIS[2008-07-30/2015-04-18]変更履歴
Javaで外部コマンド(プロセス)を実行する方法について。
実行できるのは実行ファイル(Windowsでいうと拡張子がexeやbat等のファイル、UNIXでいうと実行権限があるファイルやsh)であり、コマンドプロンプトやシェルが直接解釈するコマンド(WindowsでいうとDOSの内部コマンドdirやecho)は直接は実行できない。
コマンド(外部プロセス)を実行すると、そのプロセスは非同期で動き続ける。
JDK1.4までは、外部プロセスの起動にRuntimeクラスを使う。
JDK1.5以降でも使えるが、JDK1.5以降ではRuntimeの内部でProcessBuilderを使っているので、素直にProcessBuilderを使う方がよい。
Runtime r = Runtime.getRuntime();Process process = r.exec("java -version");//Processの使い方は後述コマンドの文字列を引数付き(スペース区切り)で渡すと、そのコマンドが実行される。
ただ、Windowsではスペース入りのファイル名やディレクトリ名が使えたりするので、その場合は変な所で区切られることになってしまう。
そういう場合は、配列形式で渡す。
Runtime r = Runtime.getRuntime();//Process process = r.exec(new String[]{ "java", "-version" });Process process = r.exec(new String[] {"C:\\Program Files\\Java\\j2re1.4.2_13\\bin\\java","-version" });//Processの使い方は後述
Windowsのechoはコマンドプロンプトが解釈するコマンドなので、直接実行することは出来ない。(実行しようとすると例外が発生する)
java.io.IOException: CreateProcess:echo zzz error=2at java.lang.Win32Process.create(Native Method)こういうものは、コマンドプロンプトを起動してやれば、その上で実行できる。(WindowsXPの場合、cmd.exeがコマンドプロンプト本体)
cmdに/cオプションを付けて実行すると コマンドを実行した後に終了してくれるので、それを利用する。
Runtime r = Runtime.getRuntime();Process process = r.exec("cmd /c echo zzz");//Process process = r.exec(new String[]{ "cmd", "/c", "echo", "zzz" });//Processの使い方は後述
ちなみにDOSでは、スラッシュ区切りのオプションはスペースを入れずに指定することが出来る。
C:\> cmd/c echo zzzzzz
しかし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 process = 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")で取得できるディレクトリーと同じ。
自分でカレントディレクトリーを指定するには、以下のようにする。
Filedir = new File("C:/temp");Runtime r = Runtime.getRuntime();Process process = r.exec("cmd /ccd", null,dir); //Windowsのcdコマンドは、カレントディレクトリーを表示する//Processの使い方は後述
ProcessBuilderはJDK1.5で新設されたクラス。Runtimeも内部ではこのクラスを使用するようになった。
ProcessBuilder pb = newProcessBuilder("java", "-version");Process process = pb.start();//Processの使い方は後述ProcessBuilder pb = newProcessBuilder();pb.command("java", "-version");Process process = pb.start();//Processの使い方は後述これらは可変長引数であり、いくつでも引数を指定できる。
(逆に、一つの文字列内にずらずらとコマンド・オプションを書くことは出来なくなった。が、それはスペース入りのパスに対応できないから、別に要らないだろう)
他に、以下のような書き方も出来る。
ProcessBuilder pb = newProcessBuilder(new String[] { "java", "-version" });List<String> list = new ArrayList<String>();list.add("java");list.add("-version");ProcessBuilder pb = newProcessBuilder(list);
この辺りはRuntimeを使う場合と同じで、「cmd /c」を呼ぶだけ。
ProcessBuilder pb = newProcessBuilder("cmd", "/c", "echo", "zzz");Process process = pb.start();//Processの使い方は後述独自の環境変数は、Runtimeより扱いやすくなった。
ProcessBuilder pb = newProcessBuilder("cmd", "/c", "echo", "%TEST%");Map<String, String>env = pb.environment();//環境変数を取得env.put("TEST", "sample");Process process = pb.start();//Processの使い方は後述ProcessBuilder#environment()により、デフォルトの環境変数全てが取得できる。
こういったメソッドで返すマップは、よくあるパターンでは変更不可能(あるいは変更しても元のオブジェクトには影響しない)なのだが、ProcessBuilderの場合は、これを書き換えると元のオブジェクトに反映される。
なのでこのマップを使って、一部の環境変数を書き換えたり新しい環境変数を追加したり、あるいは全て削除(clear())したりすることが出来る。
実行中の作業ディレクトリー(カレントディレクトリー)を指定するには、以下のようにする。
ProcessBuilder pb = newProcessBuilder("cmd", "/c", "cd"); //Windowsのcdコマンドは、カレントディレクトリーを表示するFiledir = new File("C:/temp");pb.directory(dir);Process process = pb.start();//Processの使い方は後述実行したプロセスの標準出力や標準エラーの内容を取得することができる(後述)のだが、
ProcessBuilderでは、標準エラーに出力されたものを標準出力にマージ(統合/リダイレクト)し、標準出力から読み取るだけでどちらの内容も取得できるようにすることが可能。
ProcessBuilder pb = newProcessBuilder("java", "-version");pb.redirectErrorStream(true); //デフォルトはfalse:マージしない(標準出力と標準エラーは別々)Process process = pb.start();//Processの使い方は後述標準出力と標準エラーの内容が順不同で入り混じる可能性はあるが…
どちらかしか出力されないと分かっているような場合や、出力内容は問わないというような場合なら使い道があるかも。
ProcessBuilder#start()(あるいはRuntime#exec())によって 別プロセスが起動し、非同期で実行される。
(そのstart()やexec()の戻り値である)Processを使うことによって、起動したプロセスの実行結果を取得することが出来る。
起動したプロセスが終了するのを待つには、以下のようにする。
ProcessBuilder pb = new ProcessBuilder(〜);Process process = pb.start();int ret = process.waitFor();System.out.println("戻り値:" + ret);process.waitFor();int ret = process.exitValue();System.out.println("戻り値:" + ret);戻り値は、起動したプロセス(コマンド)の戻り値(終了コード)。一般的に、正常終了なら0が返る。
起動したプロセスが標準出力や標準エラーに書き込んだ内容は、InputStreamを介して取得することが出来る。
Process process = pb.start();前待ちprocess.waitFor();InputStream is = process.getInputStream();//標準出力printInputStream(is);InputStream es = process.getErrorStream();//標準エラーprintInputStream(es);後待ちprocess.waitFor();
public static voidprintInputStream(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 process = pb.start();process.waitFor();printInputStream(process.getInputStream());printInputStream(process.getErrorStream());
しかし、別プロセスからの出力量が多いときは、このプログラムは止まってしまう。
(試してみた感じでは、出力側(起動したプロセス)が標準出力or標準エラーのどちらかに512文字(たぶんUNICODE(UTF16)なので、1024バイト)を超えて出力するとNG)
外部プロセスは標準出力(や標準エラー)にがんがん書き込みたいのだが、その受け側であるProcessのInputStreamはバッファーサイズに限りがある。
したがって、データ量がそのバッファー内に収まっている間はいいのだが、ストリームから読み出してやらないとバッファーが足りなくなって、それ以上読み込めなくなる。
InputStreamが読み込めないと、書き込み側(外部プロセス)がブロッキング(一時停止)される。したがって、そのプロセスは終了できないことになる。
でも上記のプログラムでは、プロセスの終了待ち(waitFor())をしているだけで、その間にInputStreamから読み込む処理は無い。
したがって永久にバッファーは空かず、起動したプロセスも終了せず、永久待ち=プログラム停止状態になる。(いわゆるデッドロック状態)
では、プロセス終了待ちをする前に読み込めば大丈夫か。
Process process = pb.start();printInputStream(process.getInputStream());printInputStream(process.getErrorStream());process.waitFor();
これも、標準エラーにがんがん書き込まれると、同じ状態になる。
先に標準出力が全て来るまでループしているが、その間に標準エラーに書かれてバッファーが一杯になると、外部プロセスはブロッキングされる。
すると標準出力にもそれ以上データは吐かれなくなり、待ち状態に入ってしまう。
じゃあ読み込み側で、InputStreamとErrorStreamを交互に読むようにしてみるか。
public static voidprint2(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 process = pb.start();print2(process.getInputStream(), process.getErrorStream());process.waitFor();
しかしこれでも、ダメなケースがある。
標準入力と標準エラーが交互に出力されてくるならこれでいいが、どちらか片方が出力されないと、そこでreadLine()が入力待ちに入ってしまう。
また、改行抜きで大量のデータが来た場合もreadLine()で止まってしまう模様。
readLine()==nullという判定でなくready()を使えば、前者に対しては解決できるか?
public static voidprint2_(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 process = pb.start();InputStream is = process.getInputStream();try {while(is.read() >= 0);//標準出力だけ読み込めばよい} finally {is.close();}//process.waitFor();System.out.println("戻り値:" + process.exitValue());
この場合、waitFor()で待つ必要は無い。なぜなら、is.read()が-1を返すのはストリームがクローズされた場合のみ。すなわち、起動したプロセスが終了したことを意味するから。
という訳で、標準出力・標準エラーの内容が両方とも欲しいのであれば、一番確実なのは、標準出力・標準エラーからの読み込みをスレッド化してしまう方法かもしれない。
import java.io.*;import java.util.*;/** * InputStreamを読み込むスレッド */public classInputStreamThread extendsThread {private BufferedReader br;private List<String> list = new ArrayList<String>();/** コンストラクター */publicInputStreamThread(InputStream is) {br = new BufferedReader(new InputStreamReader(is));}/** コンストラクター */publicInputStreamThread(InputStream is, String charset) {try {br = new BufferedReader(new InputStreamReader(is, charset));} catch (UnsupportedEncodingException e) {throw new RuntimeException(e);}}@Overridepublic voidrun() {try {for (;;) {String line = br.readLine();if (line == null) break;list.add(line);}} catch (IOException e) {throw new RuntimeException(e);} finally {try {br.close();} catch (IOException e) {e.printStackTrace();}}}/** 文字列取得 */public List<String>getStringList() {return list;}}Process process = pb.start();//InputStreamのスレッド開始InputStreamThreadit = newInputStreamThread(process.getInputStream());InputStreamThreadet = newInputStreamThread(process.getErrorStream());it.start();et.start();//プロセスの終了待ちprocess.waitFor();//InputStreamのスレッド終了待ちit.join();et.join();System.out.println("戻り値:" + process.exitValue());//標準出力の内容を出力for (String s :it.getStringList()) {System.out.println(s);}//標準エラーの内容を出力for (String s :et.getStringList()) {System.err.println(s);}
この方法なら、標準出力用のbr.readLine()がブロッキングされても、別スレッドで標準エラー用のbr.readLine()が処理されるので問題ない。
また、並行してプロセス終了待ち(process.waitFor())も実行されているので、起動したプロセスが終了したら、InputStreamも終了する。
するとbr.readLine()はnullを返すので、スレッドも無事に終了する。
一応タイムラグはあるかもしれないので、Thread#join()を使ってスレッドの終了待ちも行う。
※Buffered系のBufferedReaderやBufferedInputStreamを使うと、起動したプロセスによっては停止しなくなることがあるらしいので注意。[2015-04-18]
標準出力・標準エラーをスレッドで読み込む方法の欠点は、ずばり「スレッドを使用する」こと。
例えばWebLogic8.1ではアプリケーションがスレッドを生成することが許されていないので、プロセス起動時にスレッドを起こす方法は使えない。
そういう時は、標準出力・標準エラーの内容を ファイルにリダイレクトしてしまう方法も考えられる。
そうすればプロセスを起動したアプリ側のInputStreamには何も渡ってこないので、ブロックがどうこうという問題から解放される。
→DOSのリダイレクション・UNIXのリダイレクション
//ダメな例1ProcessBuilder pb = new ProcessBuilder("javac", "-version",">", "c:/t.txt");//ダメな例2ProcessBuilder 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 process = pb.start();//標準出力・標準エラーのストリームを読み込む必要は無いint ret = process.waitFor();System.out.println("戻り値" + ret);
//全て標準出力へ統合する例 …素直にredirectErrorStream()を使うべきだと思うが^^;ProcessBuilder pb = new ProcessBuilder("cmd", "/c","javac", "-help","2>&1");Process process = pb.start();printInputStream(process.getInputStream()); //標準出力だけ読み込むint ret = process.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 process = pb.start();printInputStream(process.getInputStream()); //標準出力だけ読み込むint ret = process.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はJDK1.8で導入されたが、[2014-03-19]
JDK1.8より前だったら、プロセスを強制終了させるにはProcess#destroy()を呼び出せばよいので、自分でタイマーを起動して、一定時間後にdestroy()するしかないようだ。
class class ProcessDestroyer extendsTimerTask {privateProcess process;publicProcessDestroyer(Process process) {this.process = process;}@Overridepublic voidrun() {process.destroy(); //プロセスを強制終了}}ProcessBuilder pb = new ProcessBuilder("calc"); //Windowsの「電卓」Process process = pb.start();TimerTask task = newProcessDestroyer(p);Timer timer = newTimer("プロセス停止タイマー");timer.schedule(task,TimeUnit.SECONDS.toMillis(3));//3秒後にProcessDestroyer#run()が呼ばれるfor (;;) {try {process.waitFor();//プロセスの終了待ちbreak;} catch (InterruptedException e) {e.printStackTrace();//waitFor()はInterruptedExceptionが発生する可能性があるが、//今回の例では、その場合もプロセスの終了待ちを繰り返す。//(プロセスの強制終了とInterruptedExceptionは無関係)}}timer.cancel();//タイマーのキャンセル(必須)System.out.println("戻り値:" + process.exitValue());WindowsXPの場合、destroy()による強制終了は戻り値が1になるようだ。(例外が発生するわけではない)
ちなみに、開いているウィンドウ(この例で言えば「電卓」)をタスクマネージャから「プロセスの終了」で強制終了させると、同じく1が返ってきた。
タイマー(Timerクラス)を使ってタイムアウトさせる方法は理に適っていると思うけれども、やはりスレッドを使う点がちょっとネック。
泥臭いけど、exitValue()を使ってポーリングするという方法もあるようだ。(JDK1.7以前)
exitValue()はプロセスの戻り値を返すメソッドだが、プロセスが終了していないと例外が発生する。なので、例外の有無でもってプロセスが終了したかどうか判定できる。
Process process = pb.start();longbegin = System.currentTimeMillis();//System.out.println(newDate(begin));for (;;) {try { Thread.sleep(100); } catch (InterruptedException e) {}try {process.exitValue();} catch (IllegalThreadStateException e) {//exitValue()でプロセスが終了していないと この例外が発生するlongnow = System.currentTimeMillis();if (TimeUnit.MILLISECONDS.toSeconds(now -begin) < 10) {//10秒以上経過したかどうかcontinue;//forループを繰り返す}//System.out.println("タイムアウト!" + newDate(now));process.destroy(); //プロセスを強制終了break;//forループを抜ける}//System.out.println("正常終了");break;//forループを抜ける}
JDK1.8ではisAlive()というメソッドが追加されたので、ポーリングするならそちらを使った方が良いだろう。[2014-03-19]
JDK1.8ではisAlive()というメソッドが追加されたので、(exitValue()の代わりに)それを使って判定することが出来る。[2014-03-19]
Process process = pb.start();longbegin = System.currentTimeMillis()();for (;;) {try { Thread.sleep(100); } catch (InterruptedException e) {}if (!process.isAlive()) {System.out.println("正常終了 " + process.exitValue());break; // forループを抜ける}long now = System.currentTimeMillis()();if (TimeUnit.MILLISECONDS.toSeconds(now -begin) >= 10) { // 10秒以上経過したかどうかSystem.out.println("タイムアウト!" + newDate(now));process.destroy(); // プロセスを強制終了break; // forループを抜ける}}が、isAlive()を使ってポーリングするより、タイムアウト付きのwaitFor()を使う方が断然良いだろう。
JDK1.5で導入されたFutureを使うと、Futureのタイムアウト機能を使うことが出来る。[2014-03-18]
ProcessTask task = newProcessTask();ExecutorService pool = Executors.newSingleThreadExecutor();try {Future<Integer> future = pool.submit(task);int exitValue = future.get(10,TimeUnit.SECONDS);//10秒でタイムアウトSystem.out.println(exitValue);} catch (InterruptedException| ExecutionException| TimeoutException e) {e.printStackTrace();} finally {pool.shutdownNow();}classProcessTask implementsCallable<Integer> {@Overridepublic Integercall() throws Exception {ProcessBuilder pb = newProcessBuilder(〜);Process process = pb.start();try {process.waitFor();return process.exitValue();} catch (Exception e) {System.out.println(e.getClass() + "\t" + e.getMessage());process.destroy(); // プロセスを強制終了throw e;}}}CallableでProcessを実行するタスクを作成し、ExecutorServiceで実行(submit)する。
submitするとFutureが返ってくるので、それに対してタイムアウト付きのget()を呼び出す。
Futureがタイムアウトすると、get()からTimeoutExceptionがスローされるので、それをキャッチしてタイムアウト処理を行えばよい。
それとは別に、FutureでタイムアウトするとCallable内部で実行しているprocess.waitFor()でInterruptedExceptionが発生する。
ここでprocess.destroy()を呼んでおかないと、Process自身は終了しないので注意。
JDK1.8から、タイムアウト時間が指定できるwaitForメソッドが追加された。[2014-03-19]
Process process = pb.start();boolean end = process.waitFor(10,TimeUnit.SECONDS);//10秒でタイムアウトif (end) {System.out.println("正常終了 " + process.exitValue());} else {System.out.println("タイムアウト");process.destroy(); // プロセスを強制終了}タイムアウト指定版waitForでは、戻り値がtrueだったら正常終了、falseだったらタイムアウト。
タイムアウトした際にプロセスを強制終了させたいなら、自分でprocess.destroy()を呼び出す必要がある。