fix: wait for the browser to actually exit when stopping it - #84
Merged
Conversation
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
nathanfallet
force-pushed
the
fix/wait-for-the-browser-to-actually-exit
branch
from
August 26, 2026 18:19
c62bd5b to
22759b7
Compare
stop() called process.destroy() and moved on. destroy() only requests termination — it returns immediately, and it does not reach the browser's child processes. On Windows the renderer and GPU children outlive it and keep open handles on the profile directory. A browser started on the same --user-data-dir shortly after therefore cannot take ownership of the profile: it hangs before opening its debug port, and the start times out after the full connect window with no visible cause. Observed in production as a restart loop — close, reopen one second later, fail for 30s, repeat — that only a reboot could clear. stop() now uses destroyAndAwaitExit(): grace period after destroy(), escalation to killTree(), then a second wait. Same shape as zendriver's Browser.stop, which terminates, polls for up to 3 seconds, kills, then waits. The waiting loop is common code since isAlive() exists on every target; only the forceful kill is per-platform. On JVM killTree() also takes down descendants, snapshotting them first because killing the parent detaches them. On POSIX it sends SIGKILL to the process alone, which is enough there — unlike on Windows, children do not outlive their parent.
The Windows native and JS source sets had no actual, so :core:compileKotlinMingwX64 failed with 'Expected killTree has no actual declaration'. Only the JVM and POSIX ones had been written. On mingw, TerminateProcess already ends the process outright, so there is nothing gentler to escalate from. It does not reach the children: enumerating those on Windows means walking a CreateToolhelp32Snapshot by parent id, which this target does not do. That gap is documented rather than papered over — the JVM target is the one running browsers in production, and it uses ProcessHandle.descendants(). On JS it throws UnsupportedOperationException, like every other process function there.
Moves killTree, destroyAndAwaitExit and its polling helper to their own file: Process.kt was over the per-file function threshold once both killTree and readStderrSnapshot were added. Also folds one early return and wraps a long line. No behaviour change. Lint issues on these files are back to the same count as main.
nathanfallet
force-pushed
the
fix/wait-for-the-browser-to-actually-exit
branch
from
August 27, 2026 08:35
58c99b3 to
c7fbfa2
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
stop()asked the browser to terminate and moved on:destroy()only requests termination — it returns immediately — and it does not reach the browser's child processes. On Windows the renderer and GPU children outlive it and keep open handles on the profile directory.So a browser started on the same
--user-data-dirshortly afterwards cannot take ownership of the profile: it hangs before opening its debug port, and the start times out after the full connect window with no visible cause.We hit this in production as a restart loop: close the browser, reopen it one second later, fail for 30s, repeat — recoverable only by rebooting the machine, which is simply what finally kills the leftover children.
Fix
stop()now waits:destroyAndAwaitExit()follows the same shape as zendriver'sBrowser.stop(terminate → poll up to 3s → kill → wait):destroy(), then pollisAlive()for a grace period (3s by default);killTree();The waiting loop is common code —
isAlive()exists on every target — so only the forceful kill needed a per-platformactual:killTree()also takes down descendants. They are snapshot before killing the parent, since killing it detaches them and they can no longer be enumerated.SIGKILLto the process itself, which is enough there — unlike Windows, children do not outlive their parent.Notes
No API change beyond the new
Process.killTree()expect/actual and thedestroyAndAwaitExit()helper. Timeouts are parameters with sane defaults.:core:jvmTestpasses.:core:buildfails onkotlinStoreYarnLock, as onmain— unrelated.