Skip to content

Commit

Permalink
Stop forked exec and javaexec on build termination
Browse files Browse the repository at this point in the history
Terminating a single process does not terminate its children automatically,
so when Gradle terminates .bat file, it leaves the forked application running.

The problem is often visible as "process exec process keeps running after build termination".
The root cause is that in Windows exec uses cmd -> process.exe sequence,
so aborting the build terminates "cmd.exe", and "process.exe" keeps running.

The workaround is to use taskkill /PID $pid /T /F command that terminates the process tree

For other operating systems, alternative solutions are needed.
This commit uses Process#descendants if Gradle runs with Java 9+
Otherwise it resorts to the previous "process.destroy" sequence.

fixes gradle#18716 gradle#7603 gradle#6114 gradle#3093 gradle#1128 gradle#1109

Signed-off-by: Vladimir Sitnikov <[email protected]>
  • Loading branch information
vlsi committed Apr 9, 2022
1 parent 8d89e13 commit f0a2207
Show file tree
Hide file tree
Showing 2 changed files with 134 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,20 @@
package org.gradle.process.internal;

import net.rubygrapefruit.platform.ProcessLauncher;
import org.apache.commons.lang.StringUtils;
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import org.gradle.internal.os.OperatingSystem;

import javax.annotation.Nullable;
import java.lang.management.ManagementFactory;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Stream;

public class ExecHandleRunner implements Runnable {
private static final Logger LOGGER = Logging.getLogger(ExecHandleRunner.class);
Expand Down Expand Up @@ -57,14 +65,116 @@ public void abortProcess() {
aborted = true;
if (process != null) {
streamsHandler.disconnect();
LOGGER.debug("Abort requested. Destroying process: {}.", execHandle.getDisplayName());
process.destroy();
LOGGER.info("Abort requested. Destroying process: {}.", execHandle.getDisplayName());
if (!OperatingSystem.current().isWindows()) {
destroyProcessTree();
} else {
Long pid = getProcessPid();
if (pid == null) {
LOGGER.info("Aborting {}", execHandle.getDisplayName());
process.destroy();
} else {
// taskkill requires pid
destroyWindowsProcessTree(pid);
}
}
}
} finally {
lock.unlock();
}
}

private @Nullable Long getProcessPid() {
long pid;
try {
// Java 9+
Method pidMethod = Process.class.getMethod("pid");
pid = (Long) pidMethod.invoke(process);
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
LOGGER.debug("Process#pid() is not available, so will try deduce process id from RuntimeMXBean.name", e);
String runtimeMXBeanName = ManagementFactory.getRuntimeMXBean().getName();
try {
pid = Long.parseLong(StringUtils.substringBefore(runtimeMXBeanName, "@"));
} catch (NumberFormatException nfe) {
LOGGER.info("Native-platform process: failed to parse PID from Runtime MX bean name: {} " +
" (expecting pid@.. format) when terminating {}", runtimeMXBeanName, execHandle.getDisplayName());
return null;
}
}
return pid;
}

/**
* By default, {@link Process#destroy()} does not terminate children processes in Windows,
* so it causes non-terminated processes when user cancels the build (e.g. via Ctrl+C).
* {@code taskkill} utility allows to terminate all the processes in the tree.
*/
private void destroyWindowsProcessTree(long pid) {
// TODO: is this ok? Should it use ExecActionFactory somehow?
ProcessBuilder taskkillProcessBuilder = new ProcessBuilder("cmd", "/K", "taskkill", "/PID", Long.toString(pid), "/T", "/F");
Process taskkill = processLauncher.start(taskkillProcessBuilder);
streamsHandler.connectStreams(taskkill, "terminate " + execHandle.getDisplayName() + " via taskkill " + pid, executor);
try {
// taskkill should be fast, however, do not allow it to hang Gradle
taskkill.waitFor(15, TimeUnit.SECONDS);
} catch (InterruptedException e) {
LOGGER.info("taskkill took more than 15 seconds to terminate {}", execHandle.getDisplayName());
process.destroy();
}
}

/**
* By default, {@link Process#destroy()} does not terminate children processes in Windows,
* so it causes non-terminated processes when user cancels the build (e.g. via Ctrl+C).
* The method destroys all descendants first, and then destroys the process itself.
*/
@SuppressWarnings("unchecked")
private void destroyProcessTree() {
// Destroy descendants when running with Java 9+
// TODO: remove reflection when code is Java 9+
@SuppressWarnings("rawtypes")
Stream/*<ProcessHandle>*/ descendantsProcesses = Stream.empty();

try {
Method descendantsMethod = Process.class.getMethod("descendants");
//noinspection rawtypes
descendantsProcesses = (Stream) descendantsMethod.invoke(process);
} catch (NoSuchMethodException ignore) {
LOGGER.info("Can't call terminate process tree for {} since Process#descendants method does not exist. Running with Java 8?", execHandle.getDisplayName());
} catch (InvocationTargetException e) {
LOGGER.info("Can't query descendants process tree for {}", execHandle.getDisplayName(), e.getCause());
} catch (IllegalAccessException e) {
LOGGER.info("Can't query descendants process tree for {}", execHandle.getDisplayName(), e);
}

//noinspection unchecked
descendantsProcesses.forEach(processHandle -> { // ProcessHandle is Java 9+
Class<?> processHandleClass = processHandle.getClass();
Long childPid;
try {
Method pidMethod = processHandleClass.getMethod("pid");
childPid = (Long) pidMethod.invoke(processHandle);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ignore) {
return;
}

try {
Method destroyMethod = processHandleClass.getMethod("destroy");
LOGGER.info("Destroying process {}", childPid);
destroyMethod.invoke(processHandle);
} catch (NoSuchMethodException ignore) {
// can't happen
} catch (InvocationTargetException e) {
LOGGER.info("Error while destroying process {}", childPid, e.getCause());
} catch (IllegalAccessException e) {
LOGGER.info("Error while destroying process {}", childPid, e);
}
});

LOGGER.info("Aborting {}", execHandle.getDisplayName());
process.destroy();
}

@Override
public void run() {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,27 @@ class DefaultExecHandleSpec extends ConcurrentSpec {
execHandle.abort()
}

@Ignore //not yet implemented
void "aborts process tree"() {
def output = new ByteArrayOutputStream()
def execHandle = handle().setStandardOutput(output).setTimeout(5000).args(args(SlowDaemonApp.class)).build()

when:
execHandle.start()
// 2.5 sec should be enough for the process to print "'m the daemon"
Thread.sleep(2500)
// Now we abort it while it still runs
execHandle.abort()
execHandle.waitForFinish()

then:
output.toString().contains "I'm the daemon"
execHandle.state == ExecHandleState.ABORTED

cleanup:
execHandle.abort()
}

@Ignore //not yet implemented: see "when" comment
void "aborts daemon"() {
def output = new ByteArrayOutputStream()
def execHandle = handle().setDaemon(true).setStandardOutput(output).args(args(SlowDaemonApp.class)).build()
Expand All @@ -308,6 +328,7 @@ class DefaultExecHandleSpec extends ConcurrentSpec {

when:
execHandle.abort()
// .abort() does destroy the process, however, DETACHED is treated as "terminal" state, so the state is not updated to ABORTED
def result = execHandle.waitForFinish()

then:
Expand Down

0 comments on commit f0a2207

Please sign in to comment.