Разница между ProcessBuilder и Runtime.exec()

Я пытаюсь выполнить внешнюю команду из кода Java, но я заметил разницу между Runtime.getRuntime().exec(...) и new ProcessBuilder(...).start().

При использовании Runtime:

Process p = Runtime.getRuntime().exec(installation_path + 
                                       uninstall_path + 
                                       uninstall_command + 
                                       uninstall_arguments);
p.waitFor();

значение выхода равно 0, и команда завершена нормально.

Однако с ProcessBuilder:

Process p = (new ProcessBuilder(installation_path +    
                                 uninstall_path +
                                 uninstall_command,
                                 uninstall_arguments)).start();
p.waitFor();

значение выхода равно 1001, и команда завершается посередине, хотя waitFor возвращает.

Что я должен сделать, чтобы исправить проблему с ProcessBuilder?

Ответ 1

Различные перегрузки Runtime.getRuntime().exec(...) принимают либо массив строк, либо одну строку. Однострочные перегрузки exec() будут помечать строку в массив аргументов, прежде чем передавать строковый массив на одну из перегрузок exec(), которая принимает строковый массив. С другой стороны, конструкторы ProcessBuilder принимают только массив varargs строк или строку List, где каждая строка в массиве или списке считается отдельным аргументом. В любом случае полученные аргументы затем соединяются в строку, которая передается ОС для выполнения.

Так, например, в Windows,

Runtime.getRuntime().exec("C:\DoStuff.exe -arg1 -arg2");

будет запускать программу DoStuff.exe с двумя заданными аргументами. В этом случае командная строка получает токенизацию и возвращает обратно. Тем не менее,

ProcessBuilder b = new ProcessBuilder("C:\DoStuff.exe -arg1 -arg2");

завершится с ошибкой, если не будет программы с именем DoStuff.exe -arg1 -arg2 в C:\. Это связано с тем, что нет токенизации: предполагается, что команда для запуска уже была маркирована. Вместо этого вы должны использовать

ProcessBuilder b = new ProcessBuilder("C:\DoStuff.exe", "-arg1", "-arg2");

или, альтернативно,

List<String> params = java.util.Arrays.asList("C:\DoStuff.exe", "-arg1", "-arg2");
ProcessBuilder b = new ProcessBuilder(params);

Ответ 2

Посмотрите, как Runtime.getRuntime().exec() передает команду String в ProcessBuilder. Он использует токенизатор и взрывает команду в отдельные токены, затем вызывает exec(String[] cmdarray, ......), который строит a ProcessBuilder.

Если вы построите ProcessBuilder с массивом строк вместо одного, вы получите тот же результат.

Конструктор ProcessBuilder принимает String... vararg, поэтому передача всей команды в виде отдельной строки имеет тот же эффект, что и вызов этой команды в кавычках в терминале:

shell$ "command with args"

Ответ 3

Да, есть разница.

Итак, что вы говорите, что ProcessBuilder должен выполнить, это выполнить "команду", чье имя содержит пробелы и другие нежелательные файлы. Конечно, операционная система не может найти команду с этим именем, и выполнение команды не выполняется.

Ответ 4

Нет разницы между ProcessBuilder.start() и Runtime.exec(), потому что реализация Runtime.exec():

public Process exec(String command) throws IOException {
    return exec(command, null, null);
}

public Process exec(String command, String[] envp, File dir)
    throws IOException {
    if (command.length() == 0)
        throw new IllegalArgumentException("Empty command");

    StringTokenizer st = new StringTokenizer(command);
    String[] cmdarray = new String[st.countTokens()];
    for (int i = 0; st.hasMoreTokens(); i++)
        cmdarray[i] = st.nextToken();
    return exec(cmdarray, envp, dir);
}

public Process exec(String[] cmdarray, String[] envp, File dir)
    throws IOException {
    return new ProcessBuilder(cmdarray)
        .environment(envp)
        .directory(dir)
        .start();
}

Итак, код:

List<String> list = new ArrayList<>();
new StringTokenizer(command)
.asIterator()
.forEachRemaining(str -> list.add((String) str));
new ProcessBuilder(String[])list.toArray())
            .environment(envp)
            .directory(dir)
            .start();

должно быть таким же, как:

Runtime.exec(command)

Спасибо dave_thompson_085 за комментарий