{"record":{"id":"8854b653a259d99d","repo":"gradle/gradle","slug":"cannot-send-signal-signal-the-process-has-not-s","errorCode":null,"errorMessage":"Cannot send signal {signal}: the process has not started yet","messagePattern":"Cannot send signal (.+?): the process has not started yet","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"platforms/core-runtime/process-services-base/src/main/java/org/gradle/process/internal/ExecHandleRunner.java","lineNumber":77,"sourceCode":"        if (execHandle == null) {\n            throw new IllegalArgumentException(\"execHandle == null!\");\n        }\n        this.execHandle = execHandle;\n        this.streamsHandler = streamsHandler;\n        this.processLauncher = processLauncher;\n        this.executor = executor;\n        this.associatedBuildOperation = associatedBuildOperation;\n        this.processBuilderFactory = new ProcessBuilderFactory();\n    }\n\n    public void sendSignal(int signal) {\n        if (OperatingSystem.current().isWindows()) {\n            throw new UnsupportedOperationException(\"Sending signals is not supported on Windows\");\n        }\n        lock.lock();\n        try {\n            if (process == null) {\n                throw new IllegalStateException(\"Cannot send signal \" + signal + \": the process has not started yet\");\n            }\n            try {\n                long pid = getProcessId(process);\n                String[] command = {\"kill\", \"-\" + signal, String.valueOf(pid)};\n                Process kill = new ProcessBuilder(command)\n                    .redirectErrorStream(true)\n                    .start();\n                int exitCode = kill.waitFor();\n                if (exitCode != 0) {\n                    String output = CharStreams.toString(new InputStreamReader(kill.getInputStream(), UTF_8)).trim();\n                    String message = StringUtils.join(command, \" \") + \" failed with exit code \" + exitCode;\n                    throw new RuntimeException(message + (output.isEmpty() ? \"\" : output));\n                }\n            } catch (RuntimeException e) {\n                throw e;\n            } catch (Exception e) {\n                throw new RuntimeException(\"Failed to send signal \" + signal + \" to process\", e);\n            }","sourceCodeStart":59,"sourceCodeEnd":95,"githubUrl":"https://github.com/gradle/gradle/blob/534f27719b66953f95cc907aae7f2c1b12f5482d/platforms/core-runtime/process-services-base/src/main/java/org/gradle/process/internal/ExecHandleRunner.java#L59-L95","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nExecHandleRunner runner = ...; // handed out before start\nnew Thread(() -> runner.sendSignal(15)).start(); // process may still be null\n\n// after\nExecHandle handle = builder.build().start(); // waits until the process is running\nif (handle.getState() == ExecHandleState.STARTED) {\n    handle.abort(); // or sendSignal via the started runner\n}","handlingStrategy":"validation","validationCode":"// Only signal once the handle confirms the process is running\nif (handle.getState() == ExecHandleState.STARTED || handle.getState() == ExecHandleState.DETACHED) {\n    runner.sendSignal(signal);\n} else {\n    LOG.debug(\"process not started yet (state={}), skipping signal\", handle.getState());\n}","typeGuard":null,"tryCatchPattern":"try {\n    runner.sendSignal(signal);\n} catch (IllegalStateException e) {\n    if (e.getMessage().contains(\"has not started yet\")) {\n        // startup race - retry after the handle reaches STARTED or give up\n    } else {\n        throw e;\n    }\n}","preventionTips":["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."],"tags":["gradle","java","signals","exec","lifecycle","concurrency"],"backgroundTag":"process-state-machine-violation","analyzedSha":"534f27719b66953f95cc907aae7f2c1b12f5482d","analyzedAt":"2026-08-22T08:09:12.375Z","schemaVersion":2},"datasetVersion":"2026-08-22T09:17:25.309Z"}