Search code examples
javajava-process-runtime

process.waitFor() throws IllegalThreadStateException


Environment

Windows 10
Java 1.8

Process

I am running a 7zip's zip task.
The process takes 2 to 3 hours to complete.

Exception

java.lang.IllegalThreadStateException: process has not exited
at java.lang.ProcessImpl.exitValue(ProcessImpl.java:443)
at java.lang.ProcessImpl.waitFor(ProcessImpl.java:452at

My code

int exitValue = -1;
Process start = null;
try
{
        ProcessBuilder processBuilder = new ProcessBuilder(commands);
        start = processBuilder.start();
        try(BufferedReader ipBuf = new BufferedReader(new InputStreamReader(start.getInputStream())))
        {
            String line = null;
            while ((line = ipBuf.readLine()) != null)
            {
                LOGGER.info(line);
            }
        }
        try(BufferedReader errBuf = new BufferedReader(new InputStreamReader(start.getErrorStream())))
        {
            String line;
            while ((line = errBuf.readLine()) != null)
            {
                LOGGER.warning(line);
            }
        }
        start.waitFor();
        exitValue = start.exitValue();
}
finally
{
        if (start != null)
        {
            start.destroy();
        }
}
return exitValue;

I'm unable to find the root cause of this issue.

Note: I've tried this process with a similar demo instance on the same machine and it works fine.

Please help me resolve this, Thanks.


Solution

  • If your problem is caused by the Windows ProcessBuilder exit code 259 bug then there are workarounds available: all you need to do to make sure that your sub-process does not exit with status code 259 and Windows JRE won't report java.lang.IllegalThreadStateException.

    UPDATE

    The status 259 bug on Windows was fixed in JDK20, see JDK-8294899, so answer below is only helpful for earlier JDK.

    You can easily reproduce this issue by executing the following command with Runtime.getRuntime().exec(cmd) or ProcessBuilder(cmd):

    String[] cmd = {"cmd.exe /c exit /b 259"};
    

    If you have written the code for the sub-process then just edit your code so that exit code is never set to 259.

    If you have not written the code for the sub-process then a rather hacky workaround is to wrap your Java sub-process launch with a "CMD.EXE" and mini-script which adapts non-zero sub-process exit back to exit codes 0 or 1:

    String[] fixed = new String[] { "cmd.exe", "/c", 
            "(call "+String.join(" ", cmd)+ ") || (echo !!! DETECTED ERROR!!! && exit 1)" };
    

    Note: I'm no expert on CMD. The above fix definitely won't work for certain commands or punctuation (eg those with quotes / spaces etc), and because it runs under CMD.EXE environment settings the outcome might be different to direct launch from the calling JVM.

    Here is an example class you could test with:

    /** Examples to test with and without the fix:
    
     java Status259 "cmd.exe /c exit /b 0"
     java Status259 "cmd.exe /c exit /b 25"
     java Status259 "cmd.exe /c exit /b 259"
    
     java Status259 %JAVA_HOME%\bin\java -cp your.jar Status259$StatusXXX 0
     java Status259 %JAVA_HOME%\bin\java -cp your.jar Status259$StatusXXX 33
     java Status259 %JAVA_HOME%\bin\java -cp your.jar Status259$StatusXXX 259
     */
    public class Status259 {
    
        public static class StatusXXX {
            public static void main(String ... args) {
                int status = args.length > 0 ? Integer.parseInt(args[0]) : 0;
                System.out.println("StatusXXX exit code: "+status);
                System.exit(status);
            }
        }
    
        public static int exec(String[] cmd) throws IOException, InterruptedException {
    
            System.out.println("exec "+Arrays.toString(Objects.requireNonNull(cmd)));
    
            ProcessBuilder pb = new ProcessBuilder(cmd);
            // No STDERR => merge to STDOUT - or call redirectError(File)
            pb.redirectErrorStream(true);
            Process p = pb.start();
    
            // send sub-process STDOUT to the Java stdout stream
            try(var stdo = p.getInputStream()) {
                stdo.transferTo(System.out);
            }
    
            int rc = p.waitFor();
    
            System.out.println("exec() END pid="+p.pid()+" CODE "+rc +' '+(rc == 0 ? "OK":"**** ERROR ****"));
    
            return rc;
        }
    
        public static void main(String ... args) throws IOException, InterruptedException {    
            // COMMENT OUT NEXT LINE TO SEE EFFECT OF DIRECT LAUNCH:
            args = fixStatus259(args);
    
            int rc = exec(args);
            System.exit(rc);
        }
    
        private static String[] fixStatus259(String[] cmd) {
            System.out.println("fixStatus259 "+Arrays.toString(cmd));
            return new String[] {
                "cmd.exe", "/c",
                "(call "+String.join(" ", cmd)+ ") || (echo !!! DETECTED ERROR!!! && exit 1)"
            };
        }
    }