fix(browser-control): stop idle sessions, and stop close-all from bricking the browser - #242
fix(browser-control): stop idle sessions, and stop close-all from bricking the browser#242SandroHub013 wants to merge 3 commits into
Conversation
…cking the browser Two ways a browser outlived its use. A session nobody stops holds its browser - eleven chrome.exe processes on Windows - for as long as the daemon lives. The daemon's idle shutdown cannot help, because it waits for zero *running* sessions and a forgotten `start` is exactly the case where that count is one. Sessions now carry the time they were last driven, and the daemon's existing sweep stops the ones nobody has touched for 30 minutes, which is what finally lets the daemon go and the browser with it. A session streaming a live view or sampling a recording is never reaped: neither shows up as an operation. NIKCLI_BROWSER_IDLE_MINUTES overrides the window, and 0 disables reaping. The sweep is silent on purpose. `ensureDaemon` spawns the daemon with `stderr: "pipe"` and the CLI exits straight after, closing the read end, so logging a reap there kills the daemon on a broken pipe - taking its sessions with it. The regression test drives the real CLI for that reason: a test that spawns the daemon itself keeps the pipe open and cannot reproduce it. `SessionManager.closeAll()` called `Bun.WebView.closeAll()`, which SIGKILLs the browser subprocess the whole Bun process shares. Bun does not recover: every later view fails with "Chrome process closed the pipe" and then "Failed to spawn Chrome", so one `close-all` left the browser tool dead for the life of the daemon - the whole nikcli session when it is hosted in-process. Closing each view is enough; the subprocess goes away with its parent. Closes #240 Closes #241
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
Reviewing the reaper turned up a hole in it: every per-session call went through require(), which marked the session as used - including the status reads a client makes while watching a session it is not driving. Anything polling info/isRecording/recordingData on a timer would have pinned a browser forever, which is exactly the leak the reaper exists to close. require() is a lookup again, and drive() is the one that marks use.
|
/nikcli review and be sure all is legit and work |
|
Unable to connect. Is the computer able to access the url? |
|
@cursoragent review pr and be sure all is legit and work |
|
Reviewed #242 against #240/#241. The two bugs are real and the approach is sound: I found two problems in the posted diff and fixed them on this branch:
|
… view ends The previous commit saved daemon.ts and manager.ts with a UTF-8 BOM and mojibake em dashes. A live HTTP stream also called Screencast.stop() without clearing the session, so isBusy kept the browser alive after the client disconnected.
nikomatt69
left a comment
There was a problem hiding this comment.
Review of #242 — the two bugs are real, the approach is sound, and the suite now passes here.
Checked against #240 / #241, the daemon/manager/session path, and a full bun test in packages/browser-control.
What holds up
- #241
close-all: droppingBun.WebView.closeAll()is the right fix. CLIclose-allgoes throughshutdownDaemon→manager.closeAll(); the regression test opens a second session in the same process and it staysrunning. - #240 idle sessions:
drive()vsrequire()is the correct split. Status reads (info,isRecording,recordingData,rawConsole,list) do not keep a session alive.NIKCLI_BROWSER_IDLE_MINUTES=0disables reaping; garbage values fall back to 30 instead of disabling it. The sweep is silent, which is load-bearing (stderr: "pipe"+ closed client). - Tests actually ran (Chrome is present here, so they did not skip): 19 pass, 47
expect()calls, including the CLI-left-behind daemon test and the daemon self-reap test. - Pre-push
typecheck: 35/35 packages.
What I changed on this branch
Two real problems in the posted diff:
- UTF-8 BOM + mojibake em dashes in
daemon.ts/manager.ts(the review comment on line 1). Stripped; diffs now show—correctly. - A finished live view could pin the browser.
GET /screencastcalledScreencast.stop()but leftsession.screencastset, soisBusy()stayed true after the client disconnected.isScreencasting()now requires an active stream, andfinish()detaches that object (withoutstopScreencast(name), which would kill a replacement view). Covered by a new test.
Also fixed a stale SESSION_IDLE_MS reference (that identifier does not exist) and documented the 30-minute session window in the CLI help.
Out of scope, still true
--record/ an in-progress recording is never reaped (isBusy). Intentional, called out in the PR.- Polling
/healthstill resets the daemon idle clock. Session reaping is independent, so the browser is still bounded.
I would merge this.
|
Review fatta. I due bug della PR sono veri e il fix regge. Cosa è ok
Cosa ho trovato e sistemato su questo branch (
Verifica Fuori scope, ma vero
Per me è mergeabile. |
|
/nikcli analyze and validate pr |
|
Transport |


Issue for this PR
Closes #240
Closes #241
Type of change
What does this PR do?
Two ways a browser outlived its use.
#240 — a forgotten session never lets go. A running session holds its browser (11
chrome.exeprocesses on Windows) for as long as the daemon lives, and the daemon's idle shutdown cannot help:
it waits for zero running sessions, which is exactly what a forgotten
startis not. Nothing putan upper bound on it. Sessions now record when they were last driven —
require()inSessionManageris the single place every operation on a session goes through — and the daemon'sexisting sweep stops the ones untouched for 30 minutes. That is what finally lets the daemon reach
its own idle shutdown and take the browser with it. A session streaming a live view or sampling a
recording is never reaped, since neither shows up as an operation.
NIKCLI_BROWSER_IDLE_MINUTESoverrides the window and0disables reaping.#241 —
close-allbricked the browser.SessionManager.closeAll()ended withBun.WebView.closeAll(), which SIGKILLs the browser subprocess the whole Bun process shares. Bundoes not recover from it: every later view fails with
Chrome process closed the pipe, thenFailed to spawn Chrome, even from a freshSessionManager. In a daemon — which is the entirenikcli session when hosted in-process — one
close-allleft the browser tool dead until a restart.Closing each view is already enough, and the subprocess goes away with its parent.
The two are in one PR because the second blocks the first: with
closeAll()poisoning the process,no test could open a second session, so the reaper could not be covered.
Two things I got wrong on the way, both now encoded in tests:
ensureDaemonspawns the daemon withstderr: "pipe"and the CLI exits immediately after, closing the read end — so the first reapkilled the daemon on a broken pipe, and the browser processes went away only because the daemon
had died. The sweep is silent now, and the comment says why.
require()caught too much: the status reads a client makes whilewatching a session (
info,isRecording,recordingData,rawConsole) counted as use, soanything polling on a timer would have pinned a browser forever — the very leak this closes.
require()is a lookup again;drive()marks use, and only the calls that actually drive thepage go through it.
How did you verify your code works?
Windows 11 / Bun 1.4.0, Chrome 151.
Counting real OS processes through the CLI, with the window shortened via the env var:
The daemon is still answering there — the session is stopped, not lost with it. Watching the whole
chain with the daemon's own idle shutdown temporarily shortened to 5s (untouched, because polling
/healthresets its idle clock): session reaped → daemon exits →chrome19 → 6.bun testinpackages/browser-control: 17 pass, 41 expect() calls, no skipsbun typecheckat the repo root: 35/35 packagesEach new test was checked against the unfixed code, not just the fixed code:
Bun.WebView.closeAll()back incloseAll()(fail) leaves the process able to open another session—Chrome process closed the pipereapIdleneutered to a no-op(fail)both the manager and the daemon reaping tests(fail) survives reaping a session after the client process has exited—ECONNRESETtouch()back inrequire()(fail) watching a session does not keep it aliveThe last one is why that test drives the real
cli.ts: a test that spawns the daemon itself keepsthe stderr pipe open and passes even with the bug present.
Screenshots / recordings
Not a UI change.
Checklist
Notes for reviewers
/healthresets the daemon's idleclock, so a caller that health-checks on a timer keeps the daemon alive indefinitely. The session
reaper here is independent of that, so the browser is still bounded. Happy to file it separately.