CodeGym /コース /JAVA 25 SELF /プロセスとのインタラクティブなやり取り

プロセスとのインタラクティブなやり取り

JAVA 25 SELF
レベル 61 , レッスン 3
使用可能

1. はじめに

インタラクティブなプロセスとは、単に自律的に何かをするだけでなく、あなたがそれと「会話」するのを待つプログラムのことです。ユーザーからの入力を受け取り、それに応答します。

代表的な例は、PythonbashPowerShell のようなインタプリタです。これらはコマンドが来るのを辛抱強く待ち、実行して結果を表示します。ほかにも、電卓のようなインタラクティブなユーティリティ、コンソール型データベース(psqlsqlite3)、さらには vinano のようなテキストエディタもあります。通常のスクリプトでも、実行中にパラメータや回答を尋ねるならインタラクティブになります。

この種のプロセスを Java から起動するのは、散歩のように簡単ではありません。単に「コマンドを渡して結果を得る」だけでは不十分です。実際の会話のように、タイミングよくデータを送り、応答を読み取る対話を構築する必要があります。

メッセンジャーで友人にメッセージを書くイメージです。あなたはメッセージを送り、返事を待ち、また送ります。スレッドを正しく扱えば、Java と外部プロセスのやり取りもまさにこのようになります。

双方向のやり取りの構成

各プロセスには 3 つの「通信チャネル」があります。入力(stdin)、出力(stdout)、エラー(stderr)です。最初のチャネルでプロセスにデータを渡し、2 番目で結果を受け取り、3 番目はエラーメッセージ用です。

重要なのは、プロセスを待機状態のまま「固まらせない」ことです。通常出力だけを読み、エラーストリームを無視すると、プロセスはまるであなたがその不満に気づくのを待っているかのようにハングすることがあります。逆も同様です。入力を送るだけで応答を読まないと、プロセスはたまったデータのやり場に困って停止することがあります。

そのため、インタラクティブなプロセスを扱う際は、双方向のやり取りを維持することが重要です。実際の会話のように、双方が聞いて応えるのであって、虚空に向かって話さないようにします。

インタラクティブなやり取りの概略

+---------------------+
|   Java program      |
+---------------------+
   |           ^
   v           |
stdin      stdout/stderr
   |           ^
+---------------------+
| External process    |
+---------------------+
  • Java はプロセスの入力ストリーム(stdin)に書き込みます。
  • Java はプロセスの stdoutstderr を読み取ります。
  • これらは同時に進行することがあります!

2. 実践: 外部プロセスとのインタラクティブなやり取り

実用的な例を見ていきましょう。外部プロセス(たとえば Python のインタプリタや単純な echo スクリプト)を起動し、行を送って応答を読み取ります。

例 1: 入力を待つ Python スクリプトの起動

まず簡単な Python スクリプトを作成します(名前は echo_bot.py とします)。

# echo_bot.py
while True:
    try:
        line = input()
        if line == "exit":
            print("Bye!")
            break
        print("Echo:", line)
    except EOFError:
        break

このスクリプトは入力を待ち、各行に対して "Echo: ..." と返答します。"exit" を入力すると終了します。

このスクリプトを Java から起動して「会話」するには?

1. プロセスを起動する
ProcessBuilder builder = new ProcessBuilder("python", "echo_bot.py");
Process process = builder.start();
2. やり取りのためのストリームを用意する
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(process.getOutputStream()));
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
3. 対話を実装する
// プロセスに行を送信
writer.write("Hello, process!\n");
writer.flush();

// 応答を読む
String response = reader.readLine();
System.out.println("プロセスからの応答: " + response);
4. 飽きるまで繰り返す

これをシンプルなループにして、ユーザーがコンソールに入力し、Java がそれをプロセスに送って返答を表示できるようにします。

完全な例: 外部プロセスとの Java チャット

import java.io.*;

public class InteractiveProcessDemo {
    public static void main(String[] args) throws IOException {
        ProcessBuilder builder = new ProcessBuilder("python", "echo_bot.py");
        Process process = builder.start();

        // プロセスとやり取りするためのストリーム
        BufferedWriter toProcess = new BufferedWriter(new OutputStreamWriter(process.getOutputStream()));
        BufferedReader fromProcess = new BufferedReader(new InputStreamReader(process.getInputStream()));
        BufferedReader userInput = new BufferedReader(new InputStreamReader(System.in));

        System.out.println("プロセスに送る文字列を入力してください(終了するには exit と入力):");

        while (true) {
            // ユーザーから1行読み取る
            String line = userInput.readLine();
            if (line == null) break;

            // それをプロセスに送る
            toProcess.write(line + "\n");
            toProcess.flush();

            // プロセスの応答を読む
            String response = fromProcess.readLine();
            System.out.println("プロセスからの応答: " + response);

            if ("exit".equals(line)) break;
        }

        // プロセスを終了する
        process.destroy();
    }
}

コードに関するコメント:

  • 3 つのストリームを使います。ユーザーから読むため(System.in)、プロセスへ書くため(process.getOutputStream())、応答を読むため(process.getInputStream())。
  • 各行をプロセスへ送ったら、すぐに readLine() で応答を読みます。
  • ユーザーが "exit" を入力すると、プログラムはループを終了し、プロセスを破棄します(process.destroy())。

3. 同時に読み書きすることが重要な理由

実際のタスクでは、プロセスが大量のデータを出力したり、連続して入力を求めたりすることがあります。プロセスの出力をタイムリーに読まないと内部バッファがいっぱいになり、誰かが読み取るまでプロセスが「停止」することがあります。逆に、入力を送らないとプロセスが待ち続け、同様に「固まる」ことがあります。

デッドロック: すべてが固まるとき

Deadlock は、2 つのプロセス(またはスレッドとプロセス)が互いを待ち続け、誰も先へ進めなくなる状況です。

デッドロックの例:

  • Java はプロセスが stdout に何かを書くのを待っている。
  • プロセスは Java が stdin に何か書くのを待っている。
  • 双方が待機し、誰も動かない。

解決策: 読み取りと書き込みを別スレッドで行う

デッドロックを避けるために、プロセスの stdoutstderr を同時に読むための別スレッド(Thread)を使うのが一般的です。たとえば、stdout を読むスレッドと stderr を読むスレッドの 2 本を用意し、メインスレッドは stdin への書き込みを担当します。

最小例: stderr を別スレッドで読む

// 標準エラーを別スレッドで読む
Thread errorThread = new Thread(() -> {
    try (BufferedReader errorReader = new BufferedReader(
            new InputStreamReader(process.getErrorStream()))) {
        String line;
        while ((line = errorReader.readLine()) != null) {
            System.err.println("stderr: " + line);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
});
errorThread.start();

4. 実践: 電卓とのインタラクティブなやり取り

常に Python があるとは限りません。ほぼどこにでもある標準コマンド、たとえばコマンドライン電卓を起動してみましょう。Linux/Mac なら bc(basic calculator)、Windows なら cmdpowershell が使えます。

例: bc と対話(Linux/Mac)

ProcessBuilder builder = new ProcessBuilder("bc");
Process process = builder.start();

BufferedWriter toProcess = new BufferedWriter(new OutputStreamWriter(process.getOutputStream()));
BufferedReader fromProcess = new BufferedReader(new InputStreamReader(process.getInputStream()));

toProcess.write("2 + 2\n");
toProcess.flush();

String result = fromProcess.readLine();
System.out.println("電卓の応答: " + result);

toProcess.write("quit\n");
toProcess.flush();

process.destroy();

Windows の場合: cmd を起動してコマンドを渡すこともできますが、構文が異なるため少し複雑です。通常は、両方のプラットフォームにあるユーティリティを使うか、自作のミニスクリプトでデモを行います。

5. 問題と落とし穴

ストリームのバッファリング
プロセスはすぐにデータを出力せず、内部バッファに溜め込み、バッファが満杯になったときや改行("\n")を受け取ったときにだけ「吐き出す」ことがあります。その結果、プロセスが「用意はしている」のに、あなたには応答がしばらく見えないことがあります。

アドバイス:

  • プロセスの stdin に書き込むときは、常に行末に "\n" を付ける。
  • 自作スクリプトでは、各出力の後に flush() を呼ぶ。

スレッド構成の不備によるデッドロック
stderr を読まず、プロセスがそこに大量のデータを書き込むと、誰かがエラーを読むまでプロセスが停止することがあります。

アドバイス:
プロセスの stdoutstderr は、できれば別スレッドで常に読むようにしましょう。

プロセスは終了したのに、まだ書き込んでいる
すでに終了したプロセスに入力を書き込み続けると、IOException: "Stream closed" が発生します。

7. ベストプラクティス: インタラクティブなやり取り

ExecutorService を用いた並行読み取り

出力とエラー読み取りのスレッドを手作業で立てる代わりに、ExecutorService(たとえば 2 スレッドのプール)を使うと便利です。

import java.util.concurrent.*;

ExecutorService executor = Executors.newFixedThreadPool(2);

executor.submit(() -> {
    try (BufferedReader out = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
        String line;
        while ((line = out.readLine()) != null) {
            System.out.println("stdout: " + line);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
});

executor.submit(() -> {
    try (BufferedReader err = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
        String line;
        while ((line = err.readLine()) != null) {
            System.err.println("stderr: " + line);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
});

ストリームを正しく閉じる

プロセスの処理が終わったら、すべてのストリーム(stdinstdoutstderr)を必ず閉じて、リソースリークを防ぎましょう。

toProcess.close();
fromProcess.close();
process.destroy();
executor.shutdown();

8. 最終例: 外部プロセスとのインタラクティブなやり取り

すべてをまとめましょう。以下の例は、ユーザーと外部プロセス(たとえば Python スクリプトや電卓)との間のインタラクティブな「チャット」として動作します。

import java.io.*;
import java.util.concurrent.*;

public class InteractiveProcessUniversal {
    public static void main(String[] args) throws IOException {
        // 自分のコマンドに置き換えてください — 例: "python echo_bot.py" または "bc"
        ProcessBuilder builder = new ProcessBuilder("python", "echo_bot.py");
        Process process = builder.start();

        ExecutorService executor = Executors.newFixedThreadPool(2);

        // プロセスの stdout を読む
        executor.submit(() -> {
            try (BufferedReader out = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
                String line;
                while ((line = out.readLine()) != null) {
                    System.out.println("[process] " + line);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        });

        // プロセスの stderr を読む
        executor.submit(() -> {
            try (BufferedReader err = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
                String line;
                while ((line = err.readLine()) != null) {
                    System.err.println("[process-err] " + line);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        });

        // メインスレッド: プロセスの stdin に書き込む
        try (BufferedWriter toProcess = new BufferedWriter(new OutputStreamWriter(process.getOutputStream()));
             BufferedReader userInput = new BufferedReader(new InputStreamReader(System.in))) {

            System.out.println("プロセスへの入力行を入力してください(終了するには exit):");
            String line;
            while ((line = userInput.readLine()) != null) {
                toProcess.write(line + "\n");
                toProcess.flush();
                if ("exit".equals(line)) break;
            }
        }

        process.destroy();
        executor.shutdown();
    }
}

9. インタラクティブなプロセス操作での典型的なミス

エラー No.1: プロセスの stderr を読んでいない。 プロセスがエラーに大量出力するのに stderr を読まないと、プロセスが停止することがあります。エラーが出ないと確信していても、stderr は読みましょう!

エラー No.2: ストリームを閉じない。 プロセスのストリームを閉じないと、リソースリークやプロセス終了のブロックが発生することがあります。

エラー No.3: コマンドのプラットフォーム差異。 コマンドとその構文は Windows と Unix 系で異なります。OS を確認し、適切なコマンドを選びましょう。

エラー No.4: プログラムが「固まる」— flush を呼んでいない。 ストリームに書き込んだ後で flush() を呼び忘れると、データがバッファに残ってプロセスに届かないことがあります。

エラー No.5: 外部プロセスの出力バッファリング。 プロセスは十分な文字数がたまるか "\n" を受け取るまで出力しないことがあります。自作スクリプトでは flush() を使いましょう。

エラー No.6: 入出力の同時待機によるデッドロック。 メインスレッドがプロセスの出力を待ち、プロセスが入力を待つと、双方が「固まる」ことがあります。読み書きは別スレッドで行いましょう。

エラー No.7: 終了したプロセスへの書き込みで「Stream closed」例外。 プロセスの stdin に書く前に、生きているか確認しましょう。失敗すると IOException: "Stream closed" になります。

コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION