Unterschied zwischen ProcessBuilder und Runtime.exec ()

96

Ich versuche, einen externen Befehl aus Java-Code auszuführen, aber es gibt einen Unterschied zwischen Runtime.getRuntime().exec(...)und new ProcessBuilder(...).start().

Bei Verwendung von Runtime:

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

Der exitValue ist 0 und der Befehl wird ok beendet.

Mit ProcessBuilder:

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

Der Exit-Wert ist 1001 und der Befehl endet in der Mitte, obwohl er waitForzurückkehrt.

Was soll ich tun, um das Problem zu beheben ProcessBuilder?

gal
quelle

Antworten:

99

Die verschiedenen Überladungen von Runtime.getRuntime().exec(...)nehmen entweder ein Array von Zeichenfolgen oder eine einzelne Zeichenfolge. Die einzelnen Zeichenfolgenüberladungen von exec()werden die Zeichenfolge in ein Array von Argumenten umwandeln, bevor das Zeichenfolgenarray an eine der exec()Überladungen übergeben wird, die ein Zeichenfolgenarray benötigt. Die ProcessBuilderKonstruktoren verwenden dagegen nur ein varargs-Array von Zeichenfolgen oder ein Listvon Zeichenfolgen, wobei angenommen wird, dass jede Zeichenfolge im Array oder in der Liste ein einzelnes Argument ist. In beiden Fällen werden die erhaltenen Argumente zu einer Zeichenfolge zusammengefügt, die zur Ausführung an das Betriebssystem übergeben wird.

So zum Beispiel unter Windows,

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

führt ein DoStuff.exeProgramm mit den beiden angegebenen Argumenten aus. In diesem Fall wird die Befehlszeile mit einem Token versehen und wieder zusammengesetzt. Jedoch,

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

fehl, es sei denn , es Name , ein Programm zu sein , dessen geschieht DoStuff.exe -arg1 -arg2in C:\. Dies liegt daran, dass es keine Tokenisierung gibt: Es wird davon ausgegangen, dass der auszuführende Befehl bereits tokenisiert wurde. Stattdessen sollten Sie verwenden

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

oder alternativ

List<String> params = java.util.Arrays.asList("C:\DoStuff.exe", "-arg1", "-arg2");
ProcessBuilder b = new ProcessBuilder(params);
Luke Woodward
quelle
es funktioniert immer noch nicht: List <String> params = java.util.Arrays.asList (Installationspfad + Deinstallationspfad + Deinstallationsbefehl, Deinstallationsargumente); Prozess qq = neuer ProcessBuilder (params) .start ();
gal
7
Ich kann nicht glauben, dass diese String-Konkatanation Sinn macht: "Installationspfad + Deinstallationspfad + Deinstallationsbefehl".
Angel O'Sphere
8
Runtime.getRuntime (). Exec (...) ruft KEINE Shell auf, es sei denn, dies wird vom Befehl explizit angegeben. Das ist eine gute Sache in Bezug auf das aktuelle "Shellshock" -Fehlerproblem. Diese Antwort ist irreführend, da sie besagt, dass cmd.exe oder ein gleichwertiges Element (dh / bin / bash unter Unix) ausgeführt wird, was anscheinend nicht der Fall ist. Stattdessen erfolgt die Tokenisierung in der Java-Umgebung.
Stefan Paul Noack
@ noah1989: danke für das Feedback. Ich habe meine Antwort aktualisiert, um (hoffentlich) Dinge zu klären und insbesondere jegliche Erwähnung von Muscheln oder Muscheln zu entfernen cmd.exe.
Luke Woodward
Der Parser für exec funktioniert auch nicht ganz so wie die parametrisierte Version, was mich einige Tage gekostet hat, um herauszufinden ...
Drew Delano
18

Sehen Sie sich an, wie Runtime.getRuntime().exec()der String-Befehl an das übergeben wird ProcessBuilder. Es verwendet einen Tokenizer und zerlegt den Befehl in einzelne Token. Anschließend wird aufgerufen, exec(String[] cmdarray, ......)welches a erstellt wird ProcessBuilder.

Wenn Sie die ProcessBuildermit einem Array von Zeichenfolgen anstelle einer einzelnen erstellen, erhalten Sie das gleiche Ergebnis.

Der ProcessBuilderKonstruktor nimmt ein String...vararg an. Die Übergabe des gesamten Befehls als einzelne Zeichenfolge hat also den gleichen Effekt wie das Aufrufen dieses Befehls in Anführungszeichen in einem Terminal:

shell$ "command with args"
Costi Ciudatu
quelle
14

Es gibt keinen Unterschied zwischen ProcessBuilder.start()und Runtime.exec()weil die Implementierung von 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();
}

Also Code:

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

sollte das gleiche sein wie:

Runtime.exec(command)

Danke dave_thompson_085 für den Kommentar

Eugene Lopatkin
quelle
2
Aber das Q nennt diese Methode nicht. Es ruft (indirekt) public Process exec(String command, String[] envp, File dir)- StringNICHT String[]- auf, was StringTokenizerdie Token aufruft und in ein Array legt, an das sie dann (indirekt) übergeben ProcessBuilderwerden. Dies ist ein Unterschied, wie in den drei Antworten von vor 7 Jahren korrekt angegeben.
Dave_thompson_085
Es spielt keine Rolle, wie alt die Frage ist. Aber ich versuche die Antwort zu korrigieren.
Eugene Lopatkin
Ich kann die Umgebung für ProcessBuilder nicht festlegen. Ich kann nur die Umwelt bekommen ...
Ilke Muhtaroglu
Siehe docs.oracle.com/javase/7/docs/api/java/lang/… , um die Umgebung
festzulegen,
Wenn Sie genauer hinschauen, können Sie feststellen, dass die Umgebung standardmäßig null ist.
Eugene Lopatkin
14

Ja, da gibt es einen Unterschied.

  • Die Runtime.exec(String)Methode verwendet eine einzelne Befehlszeichenfolge, die in einen Befehl und eine Folge von Argumenten aufgeteilt wird.

  • Der ProcessBuilderKonstruktor verwendet ein (varargs) Array von Zeichenfolgen. Die erste Zeichenfolge ist der Befehlsname und der Rest sind die Argumente. (Es gibt einen alternativen Konstruktor, der eine Liste von Zeichenfolgen verwendet, aber keinen, der eine einzelne Zeichenfolge enthält, die aus dem Befehl und den Argumenten besteht.)

Sie fordern ProcessBuilder also auf, einen "Befehl" auszuführen, dessen Name Leerzeichen und anderen Müll enthält. Natürlich kann das Betriebssystem keinen Befehl mit diesem Namen finden und die Befehlsausführung schlägt fehl.

Stephen C.
quelle
Nein, es gibt keinen Unterschied. Runtime.exec (String) ist eine Verknüpfung für ProcessBuilder. Es werden andere Konstruktoren unterstützt.
marcolopes
2
Du bist falsch. Lesen Sie den Quellcode! Runtime.exec(cmd)ist effektiv eine Abkürzung für Runtime.exec(cmd.split("\\s+")). Die ProcessBuilderKlasse hat keinen Konstruktor, der direkt äquivalent zu ist Runtime.exec(cmd). Dies ist der Punkt, den ich in meiner Antwort anspreche.
Stephen C
1
Wenn Sie einen ProcessBuilder wie folgt instanziieren: new ProcessBuilder("command arg1 arg2")Der start()Aufruf wird nicht das tun, was Sie erwarten. Es wird wahrscheinlich fehlschlagen und nur erfolgreich sein, wenn Sie einen Befehl mit Leerzeichen im Namen haben. Dies ist genau das Problem, nach dem das OP fragt!
Stephen C