覚えたら書く

IT関係のデベロッパとして日々覚えたことを書き残したいです。twitter: @yyoshikaw

Javaで外部プロセスを実行する

Java8以上の世の中だと思いますので、外部プロセスを実行する場合はProcessBuilderクラスを使いましょう。

今回は、外部プロセスが出力する標準出力や標準エラー出力の内容は無視して、終了コードだけを取得する例となっています。

Javaで?外部プロセスを実行する場合、よく出る話ですが以下の考慮が必要です。

外部プロセスは標準出力(や標準エラー)に書き込みたいしたいのに、受け側のProcessのInputStreamがいっぱいになってしまいます。
そのため、ストリームから読み出してやらないとバッファーが不足して、書き込み側(外部プロセス)がブロッキング(一時停止)してしまい、
そのプロセスは終了しないことになります。


実行用クラスは以下のような感じになるかと思います。

import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;

public final class CommandExecutor {

    public static int execute(String cmd, List<String> params, long timeoutSec) {
        List<String> cmdAndParams = new LinkedList<>();
        cmdAndParams.add(cmd);
        cmdAndParams.addAll(params);

        return execute(cmdAndParams, timeoutSec);
    }

    public static int execute(String cmd, long timeoutSec) {
        return execute(Arrays.asList(cmd), timeoutSec);
    }

    private static int execute(List<String> cmdAndParams, long timeoutSec) {
        ProcessBuilder builder = new ProcessBuilder(cmdAndParams);
        builder.redirectErrorStream(true);  // 標準エラー出力の内容を標準出力にマージする

        Process process;
        try {
            process = builder.start();
        } catch (IOException e) {
            throw new CommandExecuteFailedException("Command launch failed. [cmd: " + cmdAndParams + "]", e);
        }

        int exitCode;
        try {
            // 標準出力をすべて読み込む
            new Thread(() -> {
                try (InputStream is = process.getInputStream()) {
                    while (is.read() >= 0); 
                } catch (IOException e) {
                    throw new UncheckedIOException(e);
                }
            }).start();

            boolean end = process.waitFor(timeoutSec, TimeUnit.SECONDS);
            if (end) {
                exitCode = process.exitValue();
            } else {
                throw new CommandExecuteFailedException("Command timeout. [CommandPath: " + cmdAndParams + "]");
            }

        } catch (InterruptedException e) {
            throw new CommandExecuteFailedException("Command interrupted. [CommandPath: " + cmdAndParams + "]", e);
        } finally {
            if (process.isAlive()) {
                process.destroy(); // プロセスを強制終了
            }
        }

        return exitCode;
    }

    private CommandExecutor() {
    }
}


独自の例外クラスも一応定義

public final class CommandExecuteFailedException extends RuntimeException {

    public CommandExecuteFailedException(String message) {
        super(message);
    }

    public CommandExecuteFailedException(String message, Throwable cause) {
        super(message, cause);
    }
}


実行例は以下の通りです


存在するコマンドの実行

java -version

■実行用コード

import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        System.out.println("Command start [java -version]");
        int exitCode = CommandExecutor.execute("java", Arrays.asList("-version"), 3);

        System.out.printf("Command end. [exitCode: %d]\n", exitCode);
    }
}


■実行結果

Command start [java -version]
Command end. [exitCode: 0]


正常終了しています


存在しないオプションを指定して実行

java -unknown

■実行用コード

(上記と同様でmainで実行していますが、同様の箇所のコードをかなり省略しています)

System.out.println("Command start [java -unknown]");
int exitCode = CommandExecutor.execute("java", Arrays.asList("-unknown"), 3);

System.out.printf("Command end. [exitCode: %d]\n", exitCode);


■実行結果

Command start [java -unknown]
Command end. [exitCode: 1]


exitCode = 1 で異常終了しています


タイムアウトするコマンドを実行

ping 127.0.0.1 -c 100

■実行用コード

タイムアウト時間を3秒にして実行しています

System.out.println("Command start [ping 127.0.0.1 -c 100]");
int exitCode = CommandExecutor.execute("ping", Arrays.asList("127.0.0.1", "-c", "100"), 3);

System.out.printf("Command end. [exitCode: %d]\n", exitCode);


■実行結果

Command start [ping 127.0.0.1 -c 100]
Exception in thread "main" net.yyuki.cmd.CommandExecuteFailedException: Command timeout. [CommandPath: [ping, 127.0.0.1, -c, 100]]
    at net.yyuki.cmd.CommandExecutor.execute(CommandExecutor.java:51)
    at net.yyuki.cmd.CommandExecutor.execute(CommandExecutor.java:18)
    at net.yyuki.cmd.trial.Main3.main(Main3.java:10)


タイムアウトして、CommandExecuteFailedExceptionがスローされています


存在しないマンドを実行

unknown_cmd -h

■実行用コード

存在しないunknown_cmdというコマンドを実行しています

System.out.println("Command start [unknown_cmd -h]");
int exitCode = CommandExecutor.execute("unknown_cmd", Arrays.asList("-h"), 3);

System.out.printf("Command end. [exitCode: %d]\n", exitCode);


■実行結果

Command start [unknown_cmd -h]
Exception in thread "main" net.yyuki.cmd.CommandExecuteFailedException: Command launch failed. [cmd: [unknown_cmd, -h]]
    at net.yyuki.cmd.CommandExecutor.execute(CommandExecutor.java:33)
    at net.yyuki.cmd.CommandExecutor.execute(CommandExecutor.java:18)
    at net.yyuki.cmd.trial.Main5.main(Main5.java:10)
Caused by: java.io.IOException: Cannot run program "unknown_cmd": error=2, No such file or directory
    at java.lang.ProcessBuilder.start(ProcessBuilder.java:1048)
    at net.yyuki.cmd.CommandExecutor.execute(CommandExecutor.java:31)
    ... 2 more
Caused by: java.io.IOException: error=2, No such file or directory
    at java.lang.UNIXProcess.forkAndExec(Native Method)
    at java.lang.UNIXProcess.<init>(UNIXProcess.java:247)
    at java.lang.ProcessImpl.start(ProcessImpl.java:134)
    at java.lang.ProcessBuilder.start(ProcessBuilder.java:1029)
    ... 3 more


対象のコマンドが存在せず、CommandExecuteFailedExceptionがスローされています


まとめ

ProcessBuilderクラスを利用して外部プロセスを実行してみました