Search code examples
javaprocessprocessbuilder

Java Process Builder Run locally installed program


I am working on a program that I want to run a console command and save the output to a string. This program is installed by the user before running my program. However, I have found I keep getting the error Cannot run program "XXXX" (in directory "C:\Users\accou\Downloads"): CreateProcess error=2, The system cannot find the file specified For example, I have the program "Maui" that I have added to my system path: Running in my Command Prompt:

C:\Users\accou\Downloads>maui
HELLO

C:\Users\accou\Downloads>

If I run the following code in Java I get the exception above

ProcessBuilder pb =
        new ProcessBuilder("maui");
pb.directory(new File("C:\\Users\\accou\\Downloads"));
File log = new File("C:\\Users\\accou\\Downloads\\log.txt");
pb.redirectErrorStream(true);
pb.redirectOutput(ProcessBuilder.Redirect.appendTo(log));
Process p = pb.start();
assert pb.redirectInput() == ProcessBuilder.Redirect.PIPE;
assert pb.redirectOutput().file() == log;
assert p.getInputStream().read() == -1;

Exception:

Exception in thread "main" java.io.IOException: Cannot run program "maui" (in directory "C:\Users\accou\Downloads"): CreateProcess error=2, The system cannot find the file specified
    at java.base/java.lang.ProcessBuilder.start(ProcessBuilder.java:1142)
    at java.base/java.lang.ProcessBuilder.start(ProcessBuilder.java:1073)
    at Application.main(Application.java:13)
Caused by: java.io.IOException: CreateProcess error=2, The system cannot find the file specified
    at java.base/java.lang.ProcessImpl.create(Native Method)
    at java.base/java.lang.ProcessImpl.<init>(ProcessImpl.java:483)
    at java.base/java.lang.ProcessImpl.start(ProcessImpl.java:158)
    at java.base/java.lang.ProcessBuilder.start(ProcessBuilder.java:1109)
    ... 2 more

I will get the same error if I try commands such as "dir" which I know 100% is available in windows.

However, if I can the command to the following it workes perfectly fine.

new ProcessBuilder("cmd.exe", "/c", "maui");

The following also works

new ProcessBuilder("maui.cmd");

Why is it that I cannot run "dir" or more importantly "maui" directly?


Solution

  • You're confusing ProcessBuilder with a shell. ProcessBuilder is not 'ask the dos box cmd shell to act as if we ran this statement', and when you type something in that black box, such as 'dir', that is not the same as 'ask the OS to execute this process'.

    The shell does all sorts of intriguing transformations and interpretations of what you typed. That's cmd.exe (or, on other OSes, e.g. /usr/bin/bash), it is not the OS doing that, and java does not ship with a bash or cmd.exe baked in. It just asks the OS to do what you asked.

    Thus, you can't use all these shellisms. Unfortunately, java tries to do some very basic shellisms, which just adds confusion. In particular:

    1. The single-string argument variant will split on spaces to attempt to extract the executable to run + the arguments to pass.
    2. On windows, *.txt and friends probably works. On other OSes it probably does not, as expanding a star into all matching files is a bashism. Best not use it.
    3. built-ins, such as cd, pwd, test, dir, and others definitely do not work.
    4. Relying on $PATH probably doesn't work, but this is one that java does sometimes try to apply. Still, best not rely on it.
    5. Any fancy redirects and the like are all shellisms and do not work. You can't ask java to redirect to printer using foo.exe >PRN or /usr/bin/whatnot >/dev/printer. You can't write an if or a loop, or use %foo% or $foo or any other such things.

    This leads to the following conclusion:

    1. Do not ever use relative paths for anything ProcessBuilder related.
    2. Do not ever try to squeeze the executable and the arguments into a single string; always use the multiple strings variant.
    3. Don't rely on ANYTHING other than an absolute path, and absolute arguments.
    4. If you need any shellisms of any sort, run cmd.exe /c C:\absolute\path\to\something.bat, or on posixy systems, /bin/bash -c /abs/path/to/shellscript, and strongly consider on windows to use System.getenv to get the installed location of windows, so you can absolute-path cmd to thatPath + "cmd.exe", so that you end up with "C:\\Windows\\cmd.exe" or equivalent (windows may not actually be installed there, hence, use env).

    Unwieldy? Yeah. Don't use ProcessBuilder unless you know what you are doing.

    In this case, if type maui on the command line, it's cmd.exe that figures out: Oh, hey, there is a maui.cmd in this dir on the path, surely they meant that. That's a shellism. Not baked into the OS itself, and java does not ship with cmd.exe built in, so that does not work. dir is not an executable but a built-in feature of cmd.exe. Same rule.

    Java can do all these baked in things. There is no need to call dir - java can walk paths and give you all relevant info with e.g. the java.nio.file.Files API.