k4200’s notes and thoughts

Programmer side of k4200

Commons Exec

概要

Java(やScala等のJVM系の言語)から外部のプロセスを起動するには、ProcessBuilderというクラスを使う事が多いと思うけど、Apache CommonsにCommons Execというライブラリ(Processクラスとかのラッパーのはず)があり、そちらの方が若干便利そうなのでそれを使ってみる事にした。

一応チュートリアルがあるが、ごく基本的な内容なので、もう少し踏み込んだ内容を書いてみる。

入出力の扱い

Javaから外部プロセスを呼び出す際の注意事項として、呼び出したプロセスの入出力(よく問題になるのは出力)を適切に扱う必要があるという事。

例えば、呼び出したプロセスが標準出力にログを吐く場合、それを適宜読み出してあげないとバッファーに溜まっていき、バッファーが一杯になった時点で処理がブロックされる。これはProcessクラスのJavadocにも書かれている、FAQと言ってもいい内容だけど、具体的にはどのように扱えばいいのか、というのは中々難しい問題。

java プロセス ブロック」で検索すると色々情報が見つかるけど、こちらのページが具体例なども載っていて分かりやすかった(英語)。

ここに載っているような処理が、Commons Execを使うともう少し簡単に書ける。以下、サンプルコードを交えつつ簡単に説明。なお、サンプルコードはScalaだけど、文法が異なる以外はJavaでも全く一緒一緒。

標準出力を捨てる

標準入出力を扱うにはStreamHandlerというインターフェースの実装クラスであるPumpStreamHandlerを使う。

起動したプロセスの標準出力(あるいは標準エラー出力)を使わない場合、以下のようにする。

// 実行するコマンド
val cmdLine = new CommandLine("ls -l")

// コマンド実行のメインクラス
val executor = new DefaultExecutor

// 現在ディレクトリを変更
executor.setWorkingDirectory(new File("/var/log"))

// 標準出力、標準エラー出力を捨てる設定にする
val streamHandler = new PumpStreamHandler(null)
executor.setStreamHandler(streamHandler)

//実行
executor.execute(cmdLine)
標準出力をファイルに出力

PumpStreamHandlerにnullの代わりに出力先のファイルを渡せばOK。該当部分だけ。

val streamHandler = new PumpStreamHandler(new FileOutputStream(new File("/tmp/foo.txt")))
executor.setStreamHandler(streamHandler)
標準出力をプログラムで読み込む

PipedInputStreamPipedOutputStreamを使う。

// プロセスの出力をPipedOutputStreamに吐くように設定
val os = new PipedOutputStream()
val streamHandler = new PumpStreamHandler(os)
executor.setStreamHandler(streamHandler)

// 実行は非同期モードにする(ResultHandlerを第2引数に渡す)
val resultHandler = new DefaultExecuteResultHandler()
executor.execute(cmdLine, resultHandler) // asynchronous

// PipedInputStreamに接続して、それを読み出す
val is = new PipedInputStream(os);
//val br = new BufferedReader(new InputStreamReader(is)) // Javaならこちら
val inSource = scala.io.Source.fromInputStream(is)  // Scalaならscala.io.Sourceを使う方が便利

自分の場合、呼び出した子プロセスはシェルスクリプトで、そこから更に別のプロセスをバックグラウンドで動かそうとしたんだけど、リダイレクトとか標準入出力のオープン・クローズ等色々面倒だったので、最終的には子プロセスの出力は全てログファイルに落として、そのファイルを後から読むようにした。パイプの処理って面倒。

タイムアウトを設定

一定時間経っても終了しない場合はプロセスを殺す等の処理にはWatchDogを使用。

//タイムアウトを20秒に設定
val watchdog = new ExecuteWatchdog(20000)
executor.setWatchdog(watchdog)

//実行
executor.execute(cmdLine)

内部的には別Threadが動いて、一定時間が経ったらProcess#destroyを呼び出しているだけ。

Process#destroyは、子プロセスが終了することを必ずしも保証していないはずので、必要に応じてkill -9を呼び出すとかする必要がある。