Diferença entre ProcessBuilder e Runtime.exec ()

96

Estou tentando executar um comando externo de código java, mas há uma diferença que notei entre Runtime.getRuntime().exec(...)e new ProcessBuilder(...).start().

Ao usar Runtime:

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

o exitValue é 0 e o comando é encerrado ok.

No entanto, com ProcessBuilder:

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

o valor de saída é 1001 e o comando termina no meio, embora waitForretorne.

O que devo fazer para corrigir o problema ProcessBuilder?

garota
fonte

Respostas:

99

As várias sobrecargas de Runtime.getRuntime().exec(...)levam uma matriz de strings ou uma única string. As sobrecargas de string única de exec()irão transformar a string em uma matriz de argumentos, antes de passar a matriz de string para uma das exec()sobrecargas que recebe uma matriz de string. Os ProcessBuilderconstrutores, por outro lado, usam apenas um array varargs de strings ou um Listde strings, em que cada string no array ou lista é considerado um argumento individual. De qualquer forma, os argumentos obtidos são então reunidos em uma string que é passada ao sistema operacional para execução.

Então, por exemplo, no Windows,

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

irá executar um DoStuff.exeprograma com os dois argumentos fornecidos. Nesse caso, a linha de comando é convertida em tokens e recomposta. Contudo,

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

irá falhar, a menos que haja um programa cujo nome esteja DoStuff.exe -arg1 -arg2em C:\. Isso ocorre porque não há tokenização: presume-se que o comando a ser executado já tenha sido tokenizado. Em vez disso, você deve usar

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

ou alternativamente

List<String> params = java.util.Arrays.asList("C:\DoStuff.exe", "-arg1", "-arg2");
ProcessBuilder b = new ProcessBuilder(params);
Luke Woodward
fonte
ainda não funciona: List <String> params = java.util.Arrays.asList (installation_path + uninstall_path + uninstall_command, uninstall_arguments); Processo qq = new ProcessBuilder (params) .start ();
gal
7
Eu não posso acreditar que esta concatanação de string faça algum sentido: "installation_path + uninstall_path + uninstall_command".
Angel O'Sphere
8
Runtime.getRuntime (). Exec (...) NÃO invoca um shell a menos que seja explicitamente especificado pelo comando. Isso é uma coisa boa em relação ao recente problema de bug "Shellshock". Essa resposta é enganosa, porque afirma que cmd.exe ou equivalente (ou seja, / bin / bash no unix) seria executado, o que não parece ser o caso. Em vez disso, a tokenização é feita dentro do ambiente Java.
Stefan Paul Noack,
@ noah1989: obrigado pelo feedback. Atualizei minha resposta para (espero) esclarecer as coisas e, em particular, remover qualquer menção a shells ou cmd.exe.
Luke Woodward,
o analisador para exec também não funciona exatamente como a versão parametrizada, o que me levou alguns dias para descobrir ...
Drew Delano
18

Veja como Runtime.getRuntime().exec()passa o comando String para o ProcessBuilder. Ele usa um tokenizer e explode o comando em tokens individuais e, em seguida, invoca exec(String[] cmdarray, ......)que constrói um ProcessBuilder.

Se você construir o ProcessBuildercom uma matriz de strings em vez de uma única, obterá o mesmo resultado.

O ProcessBuilderconstrutor recebe um String...vararg, portanto, passar o comando inteiro como uma única String tem o mesmo efeito que invocar esse comando entre aspas em um terminal:

shell$ "command with args"
Costi Ciudatu
fonte
14

Não há diferença entre ProcessBuilder.start()e Runtime.exec()porque a implementação de 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();
}

Então codifique:

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();

deve ser o mesmo que:

Runtime.exec(command)

Obrigado dave_thompson_085 pelo comentário

Eugene Lopatkin
fonte
2
Mas o Q não chama esse método. Ele (indiretamente) chama public Process exec(String command, String[] envp, File dir)- StringNÃO String[]- que chama StringTokenizere coloca os tokens em uma matriz que é então passada (indiretamente) para ProcessBuilder, o que É uma diferença conforme declarado corretamente pelas três respostas de 7 anos atrás.
dave_thompson_085
Não importa a idade da pergunta. Mas tento consertar a resposta.
Eugene Lopatkin
Não consigo definir o ambiente para ProcessBuilder. Eu só posso obter o ambiente ...
ilke Muhtaroglu
consulte docs.oracle.com/javase/7/docs/api/java/lang/… para definir o ambiente após obtê-los por meio do método de ambiente ...
ilke Muhtaroglu
Se você olhar com mais cuidado, verá que o ambiente por padrão é nulo.
Eugene Lopatkin
14

Sim, há uma diferença.

  • O Runtime.exec(String)método usa uma única string de comando que é dividida em um comando e uma sequência de argumentos.

  • O ProcessBuilderconstrutor leva um (varargs) array de strings. A primeira string é o nome do comando e o restante são os argumentos. (Há um construtor alternativo que leva uma lista de strings, mas nenhum que leva uma única string consistindo no comando e nos argumentos.)

Então, o que você está dizendo ao ProcessBuilder para fazer é executar um "comando" cujo nome contém espaços e outras coisas inúteis. Obviamente, o sistema operacional não consegue encontrar um comando com esse nome e a execução do comando falha.

Stephen C
fonte
Não, não há diferença. Runtime.exec (String) é um atalho para ProcessBuilder. Existem outros construtores suportados.
marcolopes
2
Você está incorreto. Leia o código-fonte! Runtime.exec(cmd)é efetivamente um atalho para Runtime.exec(cmd.split("\\s+")). A ProcessBuilderclasse não tem um construtor que seja equivalente direto a Runtime.exec(cmd). Este é o ponto que estou defendendo em minha resposta.
Stephen C
1
Na verdade, se você instanciar um ProcessBuilder assim:, new ProcessBuilder("command arg1 arg2")a start()chamada não fará o que você espera. Provavelmente irá falhar e só terá sucesso se você tiver um comando com espaços no nome. Este é exatamente o problema que o OP está perguntando!
Stephen C