Terminate jobs gracefully when the runner holds no console on Windows
A runner installed as a Windows service holds no console, and cancelling a job then killed the job process instead of asking it to stop. Two separate defects caused that. This fixes both and adds the coverage that let them survive since 2024.
- Report only a failed delivery of the console control event, and log the console restore failures instead of folding them into that error. A failed restore no longer force-kills a job that is shutting down gracefully.
- Fall back to the console of the job process when the direct control event fails, so a runner with no console can still reach the job. The direct path stays first, so a runner started from a console is unaffected.
- Guard the console swaps with a mutex, since
FreeConsoleandAttachConsoleapply to the whole runner process. - Group the kernel32 console calls behind
consoleAPI, so the termination logic can be tested without a real console. - Add Windows unit tests for every delivery, restore and setup outcome, and an integration test that drives the real entry point from a process with no console.
Closes #39720 (closed) Closes #39721 (closed) Relates to #39297 (closed) Unblocks #39684 (closed)
Verification
TestKillerWithoutConsole reaches the state the Service Control Manager starts a runner in, without installing a service. It starts the terminating process with DETACHED_PROCESS, two levels deep, so that process holds no console and its parent holds none either. AttachConsole(ATTACH_PARENT_PROCESS) then fails exactly as it does under a service.
The test drives process.NewOSKillWait(...).KillAndWait, the real entry point, and asserts three things: the process under test kept no console, the job process exited with its graceful stop code, and the fallback warning appeared. The last one means the direct path cannot satisfy the test by accident. It covers both process strategies and runs in the existing windows 1809/21H2 integration tests jobs. No new infrastructure.
Not covered: session 0. A service runs there, a detached process does not. The console state is faithful, and every symptom on #39297 (closed) follows from the console state, so confirmation on a real service install is still welcome.
The approach rests on one observation from that issue: AttachConsole against the job process succeeds even when the runner runs as a service, which means the job process has a console of its own.
Known gap
A failed console takeover still leaves the runner's console state unrestored. Tracked separately in #39724.