Różnica między ProcessBuilder i Runtime.exec ()

96

Próbuję wykonać zewnętrzne polecenie z kodu Java, ale zauważyłem różnicę między Runtime.getRuntime().exec(...)i new ProcessBuilder(...).start().

Podczas używania Runtime:

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

wartość exitValue wynosi 0, a polecenie zostało zakończone poprawnie.

Jednak z ProcessBuilder:

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

wartość wyjścia to 1001, a polecenie kończy się w środku, chociaż waitForzwraca.

Co mam zrobić, aby rozwiązać problem ProcessBuilder?

gal
źródło

Odpowiedzi:

99

Różne przeciążenia Runtime.getRuntime().exec(...)przyjmują tablicę ciągów lub pojedynczy ciąg. Przeciążenia pojedynczego ciągu funkcji exec()spowodują tokenizację ciągu na tablicę argumentów przed przekazaniem tablicy ciągów do jednego z exec()przeciążeń, które pobiera tablicę ciągów. Z ProcessBuilderdrugiej strony konstruktory pobierają tylko tablicę łańcuchów varargs lub Listłańcuch ciągów, w których każdy ciąg w tablicy lub liście jest traktowany jako indywidualny argument. Tak czy inaczej, uzyskane argumenty są następnie łączone w ciąg, który jest przekazywany do systemu operacyjnego w celu wykonania.

Na przykład w systemie Windows

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

uruchomi DoStuff.exeprogram z dwoma podanymi argumentami. W takim przypadku wiersz poleceń zostanie poddany tokenizacji i ponownie złożony. Jednak,

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

zakończy się niepowodzeniem, chyba że jest program, którego nazwa jest DoStuff.exe -arg1 -arg2w C:\. Dzieje się tak, ponieważ nie ma tokenizacji: zakłada się, że polecenie do uruchomienia zostało już tokenizowane. Zamiast tego powinieneś użyć

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

lub alternatywnie

List<String> params = java.util.Arrays.asList("C:\DoStuff.exe", "-arg1", "-arg2");
ProcessBuilder b = new ProcessBuilder(params);
Luke Woodward
źródło
nadal nie działa: List <String> params = java.util.Arrays.asList (ścieżka_instalacji + ścieżka_odinstalowania + polecenie_odinstalowania, argument_dinstalacyjny); Process qq = new ProcessBuilder (params) .start ();
gal
7
Nie mogę uwierzyć, że ta konkatanacja ciągów ma jakikolwiek sens: „ścieżka_instalacji + ścieżka_instalacji + polecenie_odinstalowania”.
Angel O'Sphere,
8
Runtime.getRuntime (). Exec (...) NIE wywołuje powłoki, chyba że jest to wyraźnie określone w poleceniu. To dobra rzecz, biorąc pod uwagę niedawny problem z błędem „Shellshock”. Ta odpowiedź jest myląca, ponieważ stwierdza, że ​​zostanie uruchomiony cmd.exe lub jego odpowiednik (np. / Bin / bash na unixie), co nie wydaje się mieć miejsca. Zamiast tego tokenizacja odbywa się w środowisku Java.
Stefan Paul Noack
@ noah1989: dzięki za informację zwrotną. Zaktualizowałem moją odpowiedź, aby (miejmy nadzieję) wyjaśnić rzeczy, aw szczególności usunąć wszelkie wzmianki o muszlach lub cmd.exe.
Luke Woodward
parser dla Exec nie działa zupełnie tak samo jak wersja parametryzowane albo, co zajęło mi kilka dni, aby dowiedzieć ...
Drew Delano
18

Zobacz, jak Runtime.getRuntime().exec()przekazuje polecenie String do ProcessBuilder. Używa tokenizera i rozbija polecenie na pojedyncze tokeny, a następnie wywołuje, exec(String[] cmdarray, ......)który konstruuje plik ProcessBuilder.

Jeśli skonstruujesz ProcessBuildertablicę ciągów zamiast jednego, uzyskasz ten sam wynik.

ProcessBuilderKonstruktor bierze String...vararg, więc przechodząc całą komendę jako pojedynczy ciąg ma taki sam efekt jak wywoływanie tego polecenia w cudzysłów w terminalu:

shell$ "command with args"
Costi Ciudatu
źródło
14

Nie ma różnicy między ProcessBuilder.start()i, Runtime.exec()ponieważ implementacja Runtime.exec()to:

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

Więc kod:

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

powinno być takie samo jak:

Runtime.exec(command)

Dzięki dave_thompson_085 za komentarz

Eugene Lopatkin
źródło
2
Ale Q nie wywołuje tej metody. To (pośrednio) wywołuje public Process exec(String command, String[] envp, File dir)- StringNOT String[]- które wywołuje StringTokenizeri umieszcza tokeny w tablicy, która jest następnie przekazywana (pośrednio) do ProcessBuilder, co JEST różnicą, jak poprawnie stwierdzono w trzech odpowiedziach sprzed 7 lat.
dave_thompson_085
Nie ma znaczenia, ile lat ma to pytanie. Ale staram się naprawić odpowiedź.
Eugene Lopatkin
Nie mogę ustawić środowiska dla ProcessBuilder. Mogę tylko uzyskać środowisko ...
ilke Muhtaroglu
zobacz docs.oracle.com/javase/7/docs/api/java/lang/... aby ustawić środowisko po pobraniu ich metodą środowiskową ...
ilke Muhtaroglu
Jeśli przyjrzysz się uważniej, zobaczysz, że domyślnie środowisko jest puste.
Eugene Lopatkin
14

Tak, jest różnica.

  • Runtime.exec(String)Metoda przyjmuje pojedynczy łańcuch dowodzenia, że dzieli się na komendzie i sekwencja argumentów.

  • ProcessBuilderKonstruktor bierze (varargs) szereg strun. Pierwszy ciąg to nazwa polecenia, a pozostałe to argumenty. (Istnieje alternatywny konstruktor, który pobiera listę ciągów, ale żaden z nich nie przyjmuje pojedynczego ciągu składającego się z polecenia i argumentów).

To, co każesz ProcessBuilderowi zrobić, to wykonanie „polecenia”, którego nazwa zawiera spacje i inne śmieci. Oczywiście system operacyjny nie może znaleźć polecenia o tej nazwie, a jego wykonanie kończy się niepowodzeniem.

Stephen C.
źródło
Nie, nie ma różnicy. Runtime.exec (String) to skrót do ProcessBuilder. Obsługiwane są inne konstruktory.
marcolopes
2
Mylisz się. Przeczytaj kod źródłowy! Runtime.exec(cmd)jest efektywnym skrótem do Runtime.exec(cmd.split("\\s+")). ProcessBuilderKlasa nie posiada konstruktora, który jest bezpośrednim odpowiednikiem Runtime.exec(cmd). O to właśnie chodzi w mojej odpowiedzi.
Stephen C
1
W rzeczywistości, jeśli instancję ProcessBuilder tak: new ProcessBuilder("command arg1 arg2")The start()wezwanie nie będzie robić to, czego oczekują. Prawdopodobnie zakończy się niepowodzeniem i powiedzie się tylko wtedy, gdy masz polecenie ze spacjami w nazwie. Właśnie o to pyta PO!
Stephen C