Skip to content

[Windows] Any non-daemon bsk command hangs forever when no daemon is running -- automatic daemon startup never returns, while bsk daemon start alone takes ~190 ms #320

Description

@xiajiajun516

Environment

item value
bsk CLI 0.3.0 (bsk --version), ~/.local/bin/bsk.exe, installed with the official install.ps1
bsk daemon / protocol 0.3.0 / 1.3
extension / browser 0.3.0 / Microsoft Edge 153.0.0.0 — connected (bsk browsers lists it, session_count: 0)
OS Windows 11 Pro, build 26200, x64
callers used to reproduce Windows PowerShell 5.1.26100.7705, Node.js v24.13.0 (child_process.spawn), WMI Win32_Process.Create + cmd.exe
bsk update --check up_to_date (0.3.0 is the current release)

Summary

With no daemon running, every bsk command that is not a daemon subcommand hangs forever and prints nothing — browsers, status, session start, all of them. The daemon is started in the background (.bsk/daemon.json is written, the daemon log shows daemon ready and then browser connected), and a command issued later from another shell returns in ~100 ms — but the first process never returns and can only be killed.

  • bsk daemon start invoked directly returns in 191 ms, exit 0, and leaves a healthy daemon.
  • With BSK_AUTO_START=0 the same cold command returns in 86 ms, exit 2, with the proper actionable error:
    error: ensure daemon is running: automatic daemon startup is disabled (BSK_AUTO_START=0); run 'bsk daemon start' in the owning host environment with the same BSK_HOME, then retry

So the hang lives exactly in the automatic daemon startup path (ensure daemon is running) — the code that starts the daemon on demand and then waits for it.

Steps to reproduce

bsk daemon stop        # make sure no daemon is running
bsk browsers           # <-- hangs forever, no output
  1. bsk daemon stop — afterwards Get-Process bsk is empty.
  2. Run any non-daemon command (bsk browsers, bsk status, bsk session start --no-focus). It blocks with zero stdout and zero stderr; -vv also produces nothing, so it blocks before anything is logged to the caller.
  3. In another shell, bsk status returns immediately and reports a healthy daemon with the browser connected — the daemon was started successfully, only the waiting client is stuck.
  4. The first process stays stuck indefinitely (observed 60 s+, see the table); killing the daemon is the only thing that releases it.

Evidence (all observed on the machine above)

cold-daemon invocation caller stdio result
bsk browsers from PowerShell 5.1 (& bsk ... 2>&1 | Out-String) pipe hang, 0 output, > 60 s
bsk -vv browsers from PowerShell pipe hang, 0 bytes on stderr
bsk browsers from Node spawn (stdio default pipe, and ['ignore','pipe','pipe']) pipes hang, 0 output, > 20 s
bsk browsers from a WMI-created cmd.exe with > out.txt 2> err.txt (outside any job object) file redirection hang, both files 0 bytes, > 19 s
bsk browsers via Start-Process -WindowStyle Hidden (own console) real console hang, > 20 s
bsk browsers from Node with a minimal clean environment (only PATH/SystemRoot/TEMP/USERPROFILE/...) pipe hang, > 20 s
bsk browsers with BSK_AUTO_START=0 pipe 86 ms, exit 2, clear error
bsk daemon start directly pipe 191 ms, exit 0, daemon healthy afterwards
bsk browsers with a warm daemon pipe 108-180 ms, exit 0

Daemon log of one cold hang (these lines and then nothing more):

INFO daemon lock acquired                                      (t0 + 0 ms)
INFO ws server listening  127.0.0.1:52800
INFO daemon ready         pid=... ws_port=52800
INFO ipc named-pipe server listening  \.\pipe\bsk-daemon-<hash>
INFO browser connected    id=<browser-id> name=edge generation=1   (t0 + ~4 s)

Even after the extension is connected and the daemon is fully ready, the waiting client never proceeds.

Impact — it also freezes the caller

The auto-started daemon appears to inherit the caller stdio handles and keeps them open. For any consumer that spawns bsk <cmd> with piped stdio and waits for the stream (Node close event, child_process.execFileSync, Python subprocess.communicate()), the pipe never reaches EOF — so even after the consumer own timeout kills the CLI, the consumer stays blocked forever, an uncancellable hanging tool call.

Real incident in a downstream agent harness (DeepSeek Harness plugin @wxg-prc-cpg/browser-skill-dsh-plugin, which runs bsk <cmd> --json with piped stdio):

time event
T+0 s harness spawns bsk session start --json; no daemon running
T+0 s daemon starts (log lines above); extension connects at T+4 s
T+120 s harness command timeout fires and cancels the CLI (the CLI process is gone afterwards), but the pipe stays open, so the harness keeps waiting
T+1168 s an operator kills the daemon; the pipes close and the harness unblocks in the same second

This is the classic Windows wait-for-EOF-instead-of-exit trap: a descendant holds the write end of the pipe. The same pattern is documented for another daemon CLI in open-gsd/gsd-browser#62 (daemon start leaves the caller inherited stdio handles open, hanging any consumer that uses a pipe).

Hypothesized cause (outside-in — I have not read the sources)

The automatic-start path seems to wait on the spawned daemon child stdio rather than on its exit or on a readiness handshake — e.g. a Rust wait_with_output() / Command::output() on the bsk daemon start child. That returns on pipe EOF, not on process exit; the daemon keeps the inherited write handle, so EOF never happens. Using bsk daemon start directly does not take that path, which is why it returns in ~190 ms.

Suggested directions:

  1. Keep the daemon child off the caller stdio (Stdio::null()) and/or use a readiness handshake over the existing IPC named pipe; wait on that or on process exit — never on pipe EOF.
  2. Bound the auto-start wait with a timeout and reuse the error text that the BSK_AUTO_START=0 path already produces.
  3. Because of the inherited handles this also leaks into the caller, so keeping every descendant off the caller stdio would stop consumers from hanging even before the wait itself is fixed.

Workarounds

  • Consumer side: make sure a daemon is running before the first real command — bsk daemon start returns in ~200 ms and leaves a detached daemon, so pre-warm once per session (this is what I have patched into the DSH plugin).
  • Or set BSK_AUTO_START=0 and manage the daemon yourself: you get a bounded, actionable error instead of a hang.
  • Consumers should settle on process exit rather than pipe EOF; to unblock a caller that is already stuck, kill the daemon.

Related reports

中文摘要

Windows 11(build 26200)+ bsk/扩展 0.3.0:没有 daemon 在跑时,任何非 daemon 子命令(browsers / status / session start)都会永久挂起且零输出——daemon 其实已经在后台起来了(日志有 daemon ready / browser connected),另一个 shell 的 bsk status 能秒回,但第一个进程永不返回。直接跑 bsk daemon start 191 ms 返回并留下健康 daemon;BSK_AUTO_START=0 时同一条命令 86 ms 返回 exit 2 且错误信息清晰。所以卡死点就在「自动拉起 daemon」这条路径的等待逻辑里。已排除:stdio 类型(管道 / 文件重定向 / 真控制台都复现)、环境变量(最小干净环境同样复现)、job object(WMI 外挂 cmd 同样复现)。

更严重的是它会连带冻住调用方:自动拉起的 daemon 继承了调用方的 stdio 句柄,调用方即使超时杀掉 CLI,管道也到不了 EOF(典型的 Windows「等 EOF 而不是等进程退出」陷阱,参见 open-gsd/gsd-browser#62)。实测事故:调用方 120 s 超时杀掉 CLI 后又被额外阻塞 19 分 28 秒,直到有人手动杀掉 daemon 才在同一秒解冻。建议:让 daemon 子进程完全不碰调用方 stdio(Stdio::null())并改为等待进程退出 / IPC 就绪握手,同时给自动拉起路径加超时,并复用 BSK_AUTO_START=0 那句错误提示。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions