1

I'm trying to execute a Batch Script with ProcessBuilder and can't figure out why it's not working.

I built a small PoC to show my problem, in order for this to work you would need to create some folders on C Drive:

22840c1a

22840c1a\subfolder

Then copy your calc.exe into the subfolder. Next create start.bat inside of 22840c1a. Paste the following content into start.bat

@echo off
echo "Set WorkingDirectory"
cd /d C:\22840c1a\subfolder
echo "Start"
C:\22840c1a\subfolder\calc.exe

With that you're able to run the following unit test and reproduce the problem:

Update: Added working PoC

import org.junit.jupiter.api.Test;

import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;

class BatchExecutionTest {
    @Test
    void pocWorking() {
        execute(Arrays.asList("C:\\22840c1a\\start.bat"), "C:\\22840c1a");
    }
    @Test
    void pocNotWorking() {
        execute(Arrays.asList("start.bat"), "C:\\22840c1a");
    }
    @Test
    void pocWorkingCmd1() {
        execute(Arrays.asList("cmd", "/c", "C:\\22840c1a\\start.bat"), "C:\\22840c1a");
    }
    @Test
    void pocWorkingCmd2() {
        execute(Arrays.asList("cmd", "/c", "start.bat"), "C:\\22840c1a");
    }

    public static void execute(List<String> commands, String workingDirectory) {
        try {
            final ProcessBuilder pb = new ProcessBuilder(commands);
            pb.redirectErrorStream(true);
            pb.directory(new File(workingDirectory));
            pb.start();
        } catch (IOException e) {
            throw new IllegalStateException(e);
        }
    }
}

If everything works as expected the Unit Test is not able to execute start.bat because the File could not be found. However you can run start.bat from cmd and its working fine. Why is ProcessBuilder not able to execute the Batch Script?

kSp
  • 224
  • 2
  • 13
  • The first mistake is naming the batch file `start.bat`. Batch files should have never the name of any [Windows command](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/windows-commands). __START__ is an internal command of `cmd.exe`. No batch file should have ever `start` as file name. The batch file could be named `StartCalc.cmd`. Note: The file extension should be also nowadays `.cmd` and not `.bat` although the difference in processing the lines inside the batch file by `cmd.exe` depending on file extension of the batch file is minimal. – Mofi Aug 22 '23 at 17:22
  • The Java [Class ProcessBuilder](https://docs.oracle.com/en/java/javase/20/docs/api/java.base/java/lang/ProcessBuilder.html) is on Windows a Java wrapper class for the the Windows kernel library function [CreateProcess](https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw). There can be run executables. A batch file is not an executable, it is a text file containing lines which `cmd.exe` can process and execute. – Mofi Aug 22 '23 at 17:27
  • A double click on a batch file results in `explorer.exe` calling `CreateProcess` for starting `%ComSpec%` with the option `/c` and with full qualified batch file name appended as command to execute by `cmd.exe` and then close. `ComSpec` is an environment variable which is defined by Windows default with `%SystemRoot%\System32\cmd.exe` which expands usually to `C:\Windows\System32\cmd.exe`. The Java code should therefore get the string value of the environment variable `SystemRoot` (better than getting `ComSpec` value for several reasons). – Mofi Aug 22 '23 at 17:30
  • `SystemRoot` is usually always defined as Windows defines it already at a very early state on booting Windows because of many Windows services depend on `SystemRoot`. The only possibility existing for a not (correct) defined `SystemRoot` is that a user opens a console (command prompt or PowerShell console or Windows Terminal) and explicitly undefines or redefines `SystemRoot`. I have never seen that a user really did that as nearly no Windows program works anymore on starting next a program like `java.exe`. However, if there is no or an empty string returned, use `"C:\\Windows"` as replacement – Mofi Aug 22 '23 at 17:34
  • Then concatenate the Windows system directory path string with `"\\System32\\cmd.exe"` for the fully qualified file name of the *Windows Command Processor*. There could be used next a simple check if this file really exist and inform the user if it does not. This might be useless at the moment but who knows if Windows in 20 years will still have `cmd.exe`. Then can be used `ProcessBuilder` with most likely the string `C:\Windows\System32\cmd.exe` for the application to run with the arguments `/D` and `/C` and the fully qualified batch file name, best enclosed in `"`. – Mofi Aug 22 '23 at 17:38
  • Run in a command prompt window `cmd /?` and read the output usage help. I recommend using `/D` as too many users corrupt their *Command Processor* configuration by adding an `AutoRun` registry value which is not used __only__ by `cmd.exe` on the user opens a command prompt by running `cmd` from Windows shell, as most users think but on __every__ execution of `cmd.exe` from any process and not only `explorer.exe` running as Windows shell without usage of option `/D`. Then your Java application can at least run `cmd.exe` as you want it even on a misconfigured Windows. – Mofi Aug 22 '23 at 17:41
  • I have not installed Java on the computer which I use at the moment and can't verify the Java code for that reason if there is something else in the Java code responsible for failed execution of the *Windows Command Processor* for processing the batch file `C:\\22840c1a\start.bat`. BTW: In the batch file can be used `cd /d "%~dp0subfolder" || (echo There is no "subfolder" in "%~dp0"& exit /B)` and `"%~dp0subfolder\calc.exe"` to get the batch file itself working independent on the name of the batch file folder (as long as not using a shared folder on a network resource and its UNC path). – Mofi Aug 22 '23 at 17:54
  • I agree with you on the proper naming, but I don't think that it's the cause of the Problem. Also one thing to mention, it works with an absolute path but not with a relative path. For me it feels like ProcessBuilder is not using the working directory correctly. – kSp Aug 23 '23 at 04:40
  • I just updated the PoCs. As you can see both PoCs that use cmd /c (with and without absolute path work) also the PoC to start a batch file directly from an absolute path works as well. Only the version starting a batch file from a relative path with properly set working directory is not working. – kSp Aug 23 '23 at 05:03
  • There can be used the free Windows Sysinternals (Microsoft) tool [Process Monitor](https://learn.microsoft.com/en-us/sysinternals/downloads/procmon) to view how `java.exe` starts `cmd.exe` with which command line. Double click on first recorded `cmd.exe` line in log of Process Monitor after a line with `java.exe` and select the tab __Process__. It can be seen in the log in which directory `cmd.exe` searches first for `Start.bat` which is the current working directory set by `CreateProcess` which must not be the directory set by `ProcessBuilder` on starting `Start.bat` instead of `cmd.exe`. – Mofi Aug 23 '23 at 06:28
  • I just tried it myself, were you able to record how the Process is started in the not working example? What Filters do you set? But if i understand you correctly, ProcessBuilder tries to execute my command using Java built-ins, therefore one of these tries to call CreateProcess to execute my actual command. CreateProcess takes directory as a parameter. And normally I would expect that this parameter is set to the working directory. I just tried some other variations and it shows that only BatchScripts relative to their working directory don't work, executables are fine. – kSp Aug 24 '23 at 02:55
  • For me the fix is to use absolute paths, but i'm still curious where the difference is between starting a batch script with absolute path or with relative path. I think if absolute works, relative should work as well. Unfortunately I wasn't able to monitor with Process Monitor, I think I set the wrong filters that's why I asked about yours. – kSp Aug 24 '23 at 03:00
  • In this case use in addition to the default filters __Process Name is cmd.exe__ then __Include__ and __Process Name is java.exe__ then __Include__ and __Path ends with Start.bat__ then __Include__. It is advisable to toggle off in the toolbar the last five symbols with exception of the filing cabinet icon to get only the file system activity shown in the *Process Monitor* window and not the other activities (registry, network, process and thread, profiling events). – Mofi Aug 24 '23 at 06:04

1 Answers1

0

The ProcessBuilder command line launch using a relative path as you've used in pocNotWorking() isn't implemented as you think.

ProcessBuilder is applying a normal search using Path of your JVM. It does not - rightly or wrongly - consider checking in proposed subprocess workingDirectory to resolve a relative pathname to the command.

You can verify this by appending C:\22840c1a to the path BEFORE launching your JVM for the unit tests, and then I would expect all 4 tests to work:

set Path=%Path%;C:\22840c1a

I also tested pocWorking/pocNotWorking on Linux with a shell script start.sh and it gives consistent results to Windows when calling pocWorking/pocNotWorking. The relative path version won't run unless you adjust PATH beforehand so that start.sh is resolved in PATH rather than the working directory:

export PATH=$PATH:/somepathto/22840c1a

Note that you cannot fix this by attempting to edit path for the subprocess via pb.environment().set("Path", xxx), the path edit must be to the JVM parent process.

Overall, it seems good practice to use absolute path to ProcessBuilder or resolve command as workingDirectory + File.separator + relativeExeName

DuncG
  • 12,137
  • 2
  • 21
  • 33
  • I really ask myself why they implement it like that. I mean I come from DOS times and I don't really understand what the workingDirectory from ProcessBuilder is used for then, as it always was like, workingDirectory was the actual directory of your CommandProcessor and commands would be executed relative to this. Anway I tested and it's working as you said. If I add the Directory to the PATH-Variable the tests are working. Thanks, I see this as the answer to my question. – kSp Sep 01 '23 at 13:29
  • The only reason I can think of for this behaviour is that it avoids having to choose which script to run if there is a "start.bat" in both the `Path` and `workingDirectory`. By only using Path, the decision is made. – DuncG Sep 01 '23 at 13:40