gradle/gradle · error · IllegalStateException
Cannot send signal {signal}: the process has not started yet
Error message
Cannot send signal {signal}: the process has not started yet What it means
ExecHandleRunner.sendSignal(int) requires the field 'process' to be set, which happens only in startProcess() while holding the runner's lock. If sendSignal is invoked before that (handle still INIT/STARTING, process == null), it throws IllegalStateException('Cannot send signal N: the process has not started yet'). The runner schedules asynchronously, so 'start() returned' does not imply the runner thread already spawned the OS process unless start() was awaited.
Source
Thrown at platforms/core-runtime/process-services-base/src/main/java/org/gradle/process/internal/ExecHandleRunner.java:77
if (execHandle == null) {
throw new IllegalArgumentException("execHandle == null!");
}
this.execHandle = execHandle;
this.streamsHandler = streamsHandler;
this.processLauncher = processLauncher;
this.executor = executor;
this.associatedBuildOperation = associatedBuildOperation;
this.processBuilderFactory = new ProcessBuilderFactory();
}
public void sendSignal(int signal) {
if (OperatingSystem.current().isWindows()) {
throw new UnsupportedOperationException("Sending signals is not supported on Windows");
}
lock.lock();
try {
if (process == null) {
throw new IllegalStateException("Cannot send signal " + signal + ": the process has not started yet");
}
try {
long pid = getProcessId(process);
String[] command = {"kill", "-" + signal, String.valueOf(pid)};
Process kill = new ProcessBuilder(command)
.redirectErrorStream(true)
.start();
int exitCode = kill.waitFor();
if (exitCode != 0) {
String output = CharStreams.toString(new InputStreamReader(kill.getInputStream(), UTF_8)).trim();
String message = StringUtils.join(command, " ") + " failed with exit code " + exitCode;
throw new RuntimeException(message + (output.isEmpty() ? "" : output));
}
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException("Failed to send signal " + signal + " to process", e);
}View on GitHub (pinned to 534f27719b)
Solutions
- Signal via the ExecHandle after start() completes (start() waits out of STARTING), not via a hand-held runner.
- Guard with the handle's state: only send when getState() == STARTED/DETACHED.
- Make the watchdog tolerant: retry sendSignal briefly or treat not-yet-started as 'nothing to kill'.
- Catch IllegalStateException around sendSignal when racing is unavoidable.
Example fix
// before
ExecHandleRunner runner = ...; // handed out before start
new Thread(() -> runner.sendSignal(15)).start(); // process may still be null
// after
ExecHandle handle = builder.build().start(); // waits until the process is running
if (handle.getState() == ExecHandleState.STARTED) {
handle.abort(); // or sendSignal via the started runner
} Defensive patterns
Strategy: validation
Validate before calling
// Only signal once the handle confirms the process is running
if (handle.getState() == ExecHandleState.STARTED || handle.getState() == ExecHandleState.DETACHED) {
runner.sendSignal(signal);
} else {
LOG.debug("process not started yet (state={}), skipping signal", handle.getState());
} Try / catch
try {
runner.sendSignal(signal);
} catch (IllegalStateException e) {
if (e.getMessage().contains("has not started yet")) {
// startup race - retry after the handle reaches STARTED or give up
} else {
throw e;
}
} Prevention
- Call start() (which blocks until STARTED) before any signalling.
- Make watchdogs tolerate the not-yet-started window instead of assuming a live process.
- Keep signalling on one thread to avoid racing the runner's own startup.
When it happens
Trigger: Calling sendSignal from a watchdog/timeout thread that fires before the fork completes (heavy JVM fork, cold daemon), or keeping an ExecHandleRunner and signalling it directly instead of going through the started ExecHandle.
Common situations: Timeout watchdogs racing a slow process spawn on loaded CI machines; retry loops that signal immediately after scheduling the runner; signalling a runner whose handle was built but never started.
Related errors
- Cannot abort process '%s' because it is not in started or de
- Process has already been aborted
- Process '%s' finished with non-zero exit value %d%s
- Sending signals is not supported on Windows
- {command} failed with exit code {exitCode}
AI-assisted analysis of gradle/gradle@534f27719b (2026-08-22).
Data as JSON: /api/errors/8854b653a259d99d.
Report an issue: GitHub.