ProcessBuilder behaviour on different platforms

I’m writing a Minecraft server wrapper, and so part of that involves starting the minecraft process via my webapp. The minecraft command line looks like this:

java -Xmx1024M -Xms1024M -jar /path/to/minecraft.jar nogui

Unfortunately I’m having problems dealing with the case where the path to the jar file contains spaces. If I surround the path with “double quotes” then it works on windows, but fails on OSX. If I escape the spaces with backslashes (eg. “C:/Program\ Files/foo.jar”) then it works on OSX but fails on windows. In both cases it fails because it’s splitting the path in half on the space.

Annoyingly, both methods work just fine if they’re actually pasted into a command prompt, it’s just via ProcessBuilder that they cause issues. And it doesn’t make a difference if I use the ProcessBuilder(List) constructor or the ProcessBuilder(String…) constructor.

Does anyone know what’s going on here? Is there a single ‘proper’ way to pass a path with spaces to the command line via ProcessBuilder?

Thanks.

I don’t have a direct answer unfortunately, but failing all else you could probably just pass the complete string into Runtime.exec()

A bit more work to deal with the out and err streams seems to be the only penalty.

Sorry if this is a daft question, but wasn’t sure from what you wrote. Are you passing in each argument as a separate String - “java” “-jar” “/path/to/minecraft.jar”, etc.?

Indeed. You must always use a String[] to pass the arguments, quoting simply isn’t reliable cross platform, there have been cases where I simply could not get it to work.

I normally make a List and call toArray(new String[]) on it.

A second solution (just for reference) would be just to check if your on Windows and use the Windows version and otherwise use your MacOS version (as I’d presume it would work on other Unix like systems).

Here’s my current code:

List<String> commands = new ArrayList<String>();
			commands.add("java");
			commands.add("-Xmx1024M");
			commands.add("-Xms1024M");
			commands.add("-jar");
			
			String exePath = "";
			if (OsDetect.isMac())
			{
				exePath = exe.file.getAbsolutePath().replace(" ", "\\ ");
			}
			else
			{
				exePath = "\"" + exe.file.getAbsolutePath() + "\"";
			}
			commands.add(exePath);
			
			if (!showGui)
				commands.add("nogui");
			ProcessBuilder builder = new ProcessBuilder(commands);

ryanm: the whole point of process builder (as far as I know) is that by passing an array of args rather than one string they’ll be correctly separated, rather than relying on platform-specific parsing. I suspect that going with Runtime.exec will be even more quirky between platforms.

nsigma, Riven: I should have been more specific. :slight_smile: Hopefully my code above shows you that I’m already doing that (either that or I’ve misunderstood you).

JL235: Yeah, that’s the method I’ve currently gone with. However it’s pretty hacky - it reminds me of generating different html for different browsers, and once you start down that road it gets ugly fast. :frowning: It’ll do for the time being though.

No need for that, just throw in file.getAbsolutePath() directly.

Um, did you read the thread properly? Without escaping the spaces it’ll fail to get the right jar path on OSX.

That was in the original post, when you were still passing in a single string. I have used Runtime.exec with JAR paths with spaces in them for years, I haven’t actually tested the same code with ProcessBuilder but I would be surprised if ProcessBuilder and Runtime.exec behaved differently in this area. And the reason why they take a String[] as input is so that platform-specific stuff such as putting spaces between options, quoting, and escaping are done for you.

This is what happens behind the scenes, in java.lang.ProcessImpl:


   private ProcessImpl(String cmd[],
			String envblock,
			String path,
			boolean redirectErrorStream)
	throws IOException
    {
	// Win32 CreateProcess requires cmd[0] to be normalized
	cmd[0] = new File(cmd[0]).getPath();

	StringBuilder cmdbuf = new StringBuilder(80);
	for (int i = 0; i < cmd.length; i++) {
            if (i > 0) {
                cmdbuf.append(' ');
            }
	    String s = cmd[i];
	    if (s.indexOf(' ') >= 0 || s.indexOf('\t') >= 0) {
	        if (s.charAt(0) != '"') {
		    cmdbuf.append('"');
		    cmdbuf.append(s);
		    if (s.endsWith("\\")) {
			cmdbuf.append("\\");
		    }
		    cmdbuf.append('"');
                } else if (s.endsWith("\"")) {
		    /* The argument has already been quoted. */
		    cmdbuf.append(s);
		} else {
		    /* Unmatched quote for the argument. */
		    throw new IllegalArgumentException();
		}
	    } else {
	        cmdbuf.append(s);
	    }
	}
	String cmdstr = cmdbuf.toString();

	stdin_fd  = new FileDescriptor();
	stdout_fd = new FileDescriptor();
	stderr_fd = new FileDescriptor();

	handle = create(cmdstr, envblock, path, redirectErrorStream,
			stdin_fd, stdout_fd, stderr_fd);

	java.security.AccessController.doPrivileged(
	    new java.security.PrivilegedAction() {
	    public Object run() {
		stdin_stream =
		    new BufferedOutputStream(new FileOutputStream(stdin_fd));
		stdout_stream =
		    new BufferedInputStream(new FileInputStream(stdout_fd));
		stderr_stream =
		    new FileInputStream(stderr_fd);
		return null;
	    }
	});
    }

It shows how quoting your arguments on Windows is not required.

I wouldn’t know why Mac OS X requires the additional space escaping.

I should have been more clear - the code I’ve tested has always been passing an array of strings, never as one big string.

[quote]I haven’t actually tested the same code with ProcessBuilder
[/quote]
I have. And as described, it doesn’t work without escaping or quotes.

[quote]And the reason why they take a String[] as input is so that platform-specific stuff such as putting spaces between options, quoting, and escaping are done for you.
[/quote]
Well yes. I know this. However as I’ve said, I’ve tested it and it doesn’t work in practice.

I feel like i’m repeating myself here - I know how it should work in theory, and I know that because i’m passing it as an array of strings it should take care of paths with spaces automatically, but please believe me when I say I’ve actually tested this on multiple machines and it doesn’t work.