シェルの Wrapper のようなものを Java で実装するときに 15 年くらい悩んで解決していない問題なんだけど、良いアイディアがある人が居たらコメントに残しておいて頂けると助かります。
要件は Java で子プロセスを起動して親の標準入力に流し込まれたデータをすべて子に渡す & 子が出力したデータをすべて親の標準出力に流す、という処理。
import java.io.*;
import java.util.concurrent.*;
class Foo {
// 入力から読み込んだデータをそのまま出力するだけの処理
public static void rw(InputStream in, OutputStream out) {
try {
while(true){
int ch = in.read();
if(ch < 0) break;
out.write(ch);
}
} catch(IOException ex){ /* */ }
}
public static void main(String[] args) throws IOException, InterruptedException {
ExecutorService threads = Executors.newFixedThreadPool(3);
Process proc = new ProcessBuilder(args).start();
final InputStream in = proc.getInputStream();
final InputStream err = proc.getErrorStream();
final OutputStream out = proc.getOutputStream();
threads.submit(new Runnable(){ public void run(){ rw(in, System.out); }});
threads.submit(new Runnable(){ public void run(){ rw(err, System.err); }});
// threads.submit(new Runnable(){ public void run(){ rw(System.in, out); }}); // ※このスレッドが終了できない
proc.waitFor();
threads.shutdown();
System.out.printf("-- Exit: %d%n", proc.exitValue());
}
}
これ、標準入力から読み込んで子に渡すスレッドが標準入力に対しての I/O ブロックに入っているので、子プロセスが終了しても終了できないんですね。
そもそも同期で読み込んでいることが問題なので、System.in ではなく FileDescriptor.in
を使用して非同期 I/O でとも考えましたが FileChannle
はそもそも SelectableChannel
ではないので非同期 I/O 化は行えませんでした。また AsynchronousFileChannel
も Path
指定なので FileDescriptor
を指定することは出来ません (デバイスファイルとかある環境なら良いかもしれませんが)。
無理矢理実装するなら入力をデーモン化してオレオレ非同期化ですかね。
- System.in に対して Java プロセス全体で共通の読み出し用 daemon スレッドを張り付ける。
- そのスレッドが読み込んだデータを非同期で受け取る Listener 的なものを add したり remove したり。
- プログラム終了時にそのスレッドは消えるのでそのまま終了 (標準入力の
close()
は意図通りに機能しない環境があるかも)。
メリット: 親プロセスから何種類かの子プロセスを起動する場合でも Listener の attach/detach でうまく対処できる。
デメリット: 子プロセスが読み込みを行わなずどんどん入力するといずれパイプ用のバッファがいっぱいになり読み込みスレッドが停止する、InputStream の抽象性が生かせず実装が非標準的なことになる、他のライブラリで System.in から読み込むものがいたら競合する、ちょっとした処理でやるには大げさ、等々。
うーん、あまり綺麗ではないですがどこを妥協するかですね。
あるいは:
- 子プロセスが終了したら無理繰り
Thread.stop()
する。
以後の System.in の状態が保障されない事になると思いますが、子プロセスが終了したら親もすぐ終了するからという場合なら選択肢にはなりそう。
そもそも標準入出力と子プロセスの入出力が非同期 I/O に対応した SelectableChannel
として使えるなら無駄なスレッドを張り付けることをしなくても良いんですけどー。難しいですね。