Process execution hangs on Windows if children processes are still alive


(William Lichtenberger) #1

I have a problem where a JavaExec task runs, creates the new JVM, that JVM launches child processes, the JVM then terminates, returns an exit value, and hangs forever.

Here is a very simple snippet to reproduce the problem:

task runNotepad(type: Exec) {
        description 'Demonstrate the problem independent of java'
        commandLine 'cmd', '/c', 'cmd /c start notepad.exe'
    }

Gradle gets stuck while notepad is open. Even though the parent process of notepad.exe is terminated.

A working demo of the problem, including a gradle file to reproduce the issue, as well as an executable unit test to see exactly where in gradle it gets stuckare posted here https://gist.github.com/skazzyy/9536507

I’ve reproduced this with Gradle versions 1.8,1.9,1.10, and 1.11. I’ve verified this is NOT a problem on Linux (tried both RedHat and CentOS)

I posted this issue, before learning more information, on stackoverflow here: http://stackoverflow.com/questions/21918965/why-does-a-java-process-hang-from-gradle-when-sub-process-is-still-open

The long story: I’m using gradle to drive automated system tests… some of those system tests are UI tests, and the poorly behaving ones sometimes leave processes hanging out there. This causes the gradle task to freeze, forever. I’m even okay with the gradle task FAILING, just not hanging indefinitely. I’m stuck running on Windows, because that’s where the UI tests need to run.


(Jesper Skov) #2

We experienced the same after a Java or Windows7 fixpack update. I don’t remember which.

The apparent behavior is that a DOS command executed from Java does not properly terminate to Java, if it has started another process.

My solution to the problem is the (Groovy) class below. I hope you can use it.

Cheers, Jesper

package dk.jyskebank.tools.system.impl
  import java.util.regex.Matcher
import java.io.File;
import java.util.regex.Pattern
  import dk.jyskebank.tools.system.ExecResult;
  /**
 * Runs script on Windows 7 where CMD does not appear to return until all child processes have completed.
 *
  * For WAS6 startServer.bat this means that the script will launch the server, and then hang.
 * This code works around this by looking for a special marker in the output, which is used to
 * communicate exit value - and the order to kill the process.
 */
class Win7ScriptRunner {
 private static final int PROCESS_OK = 0
 private static final String TERMINATION_MARKER = "#### WIN7 PROCESS EXIT %ERRORLEVEL% ####"
 private static final Pattern TERMINATION_MARKER_PATTERN = Pattern.compile("(?s)#### WIN7 PROCESS EXIT (-?\d+) ####.*")
 private boolean processSucceeded = true
 private int exitValue = 0
 private boolean terminatedByMarker = false
 private boolean echoOutput = false
    /**
  * Executes DOS script with magic marker exit handling.
  * Note that stdout and stderr output is merged.
  *
   * @param processCurrentDirectory directory to run process in.
  * @param args command arguments.
  * @param messageOnError message to print if script failed.
  *
   * @return execution result.
  */
 public static ExecResult executeShellCommandAbortOnError(File processCurrentDirectory, List<String> args, String messageOnError) {
  return new Win7ScriptRunner().executeScriptAbortOnError(processCurrentDirectory, args, messageOnError)
 }
    /**
  * Executes DOS script with magic marker exit handling.
  *
   * @param processCurrentDirectory directory to run process in.
  * @param args command arguments.
  * @param messageOnError message to print if script failed.
  *
   * @return integer result from script
  */
 public static int executeShellCommandIntResult(File processCurrentDirectory, List<String> args, String messageOnError) {
  return new Win7ScriptRunner().executeScriptIntResult(processCurrentDirectory, args, messageOnError)
 }
   private int executeScriptIntResult(File processCurrentDirectory, List<String> args, String messageOnError) {
  echoOutput = true
  executeScriptAbortOnError(processCurrentDirectory, args, messageOnError)
  return exitValue
 }
    public ExecResult executeScriptAbortOnError(File processCurrentDirectory, List<String> args, String messageOnError) {
  List<String> cmdWrapped = getWindowsShellExecutionPrefix() + args + [ "&", "echo ${TERMINATION_MARKER}"]
  File wrapperScript = makeWrapperScript(args)
  Process proc = new ProcessBuilder(wrapperScript.getAbsolutePath()).directory(processCurrentDirectory).redirectErrorStream(true).start()
  InputStreamReader isr = new InputStreamReader(new BufferedInputStream(proc.getInputStream()))
  String output = consumeProcessOutputLookingForTermintationMarker(isr)
  if (terminatedByMarker) {
   proc.destroy()
  } else {
   recordExitValue(proc.exitValue())
  }
  wrapperScript.delete()
  return new ExecResult(processSucceeded, output, "", cmdWrapped)
 }
    private File makeWrapperScript(List<String> args) {
  File wrapper = File.createTempFile("win7.wrapper.", ".bat")
  wrapper << """@echo off
@call ${args.join(' ')}
@echo ${TERMINATION_MARKER}
"""
  return wrapper
 }
    private String consumeProcessOutputLookingForTermintationMarker(Reader reader) {
  StringBuilder sb = new StringBuilder()
  char[] buffer = new char[1024]
  String builtString = ""
  while (true) {
   int readCount = reader.read(buffer, 0, buffer.length)
   if (readCount == -1){
    return builtString
   }
   sb.append(buffer, 0, readCount)
   builtString = sb.toString()
     String tailString = getTailLinePreservingNewline(builtString)
   Matcher m = TERMINATION_MARKER_PATTERN.matcher(tailString)
   if (m.matches()) {
    recordExitValue(Integer.parseInt(m[0][1]))
    terminatedByMarker = true
    return builtString.replace(tailString, "")
   }
   if (echoOutput) {
    System.out.print(new String(buffer, 0, readCount))
   }
  }
      return sb.toString()
 }
    private void recordExitValue(int exitValue) {
  this.exitValue = exitValue
  processSucceeded = (exitValue == PROCESS_OK)
 }
    private String getTailLinePreservingNewline(String str) {
  String tailString = str.split("\n").last()
  if (str.endsWith("\n")) {
   tailString += "\n"
  }
  return tailString
   }
    private getWindowsShellExecutionPrefix() {
  return [ System.getenv('ComSpec'), "/c" ]
 }
    public static final void main(String[] args) {
  int res = executeShellCommandIntResult(new File(System.getProperty("user.dir")), args as List, "Failed to run script: ${args}")
  System.exit(res)
 }
}
package dk.jyskebank.tools.system
    /**
 * Represents execution result.
  * The boolean value of the object itself reflects the execution success/failure.
  */
class ExecResult {
  final boolean executionResult
  final String stdOutput
  final String errOutput
  final List<String> command
      ExecResult(boolean executionResult, String stdOutput, String errOutput, List<String> command) {
    this.executionResult = executionResult
    this.stdOutput = stdOutput
    this.errOutput = errOutput
 this.command = command
  }
      boolean asBoolean() {
    return executionResult
  }
}

(William Lichtenberger) #3

Is it possible to get a JIRA created with regards to this issue such that I can submit a pull request to gradle?


(Peter Niederwieser) #4

You shouldn’t need a JIRA to submit a pull request, but I’ve filed GRADLE-3047.