Repository navigation
Replies: 3 comments
|
Independent measurements on 0.2.0-rc.2 → 0.2.1-rc.2 (Windows 11 x64), plus a second leak path, plus a fix that is correct no matter which handle is guilty. 1. What I could confirmSame symptom, 100% reproducible, and it is definitely not task-related (all agents/jobs idle): 2. koffi at load time is not sufficient hereOn this build, loading koffi and actually calling through it exits immediately, under both plain Node and So the module load alone does not create referenced pipes on this combination (Electron 44 / Node 22 / the 3. A second, independent way to pin the loop: MCP stdio childrenI booted the real Mechanism:
With tempad disabled the survivor simply becomes figwright, i.e. the handle follows whichever server forks a helper — it is not one specific server. In my controlled boot that handle cleared about a second later, so it is not sufficient on its own — but when the helper is long-lived (a shared hub that outlives its parent, as on my machine) the handle never goes away, and it composes with whatever koffi leaves behind. 4. What my controlled boot does not reproduceA fresh profile boot + So the 10 s hang needs something the real workspace has and a bare boot does not (a live session, a terminal, a background job, a long-lived MCP helper, koffi's callback pipes once a consumer registers one). I am not claiming to have named the single guilty handle — the point below is that the fix should not depend on naming it. 5. Suggested fix: bound the exit, do not rely on the loop drainingThe invariant the update handshake actually needs is "dispose finished and the process is gone". Right now only the first half is bounded: // @deepseek-ai/dsh/profile-boot — createProcessShutdown
timeout = setTimeout(() => { forceExitOnce(code); }, timeoutMs); // 5 s
pending = Promise.resolve().then(dispose).then(() => {
if (forceAfterDispose) forceExitOnce(code);
else completeOnce(code); // sets process.exitCode AND clears the watchdog
}, () => { forceExitOnce(code); });Proposed (keeps healthy quits byte-identical, makes a stuck handle cost 5 s instead of forever): pending = Promise.resolve().then(dispose).then(() => {
if (forceAfterDispose) forceExitOnce(code);
- else completeOnce(code);
+ else {
+ completeOnce(code);
+ // dispose success does not mean the loop drained: a native handle or a
+ // grandchild holding inherited stdio can keep it alive forever.
+ // Re-arm unref'd so a healthy process is never held open by the guard,
+ // and log what survived when it fires.
+ timeout = setTimeout(() => {
+ process.stderr.write(`dsh: shutdown left active resources: ${JSON.stringify(process.getActiveResourcesInfo?.() ?? [])}\n`);
+ forceExitOnce(code);
+ }, timeoutMs);
+ timeout.unref();
+ }
}, () => { forceExitOnce(code); });And in const stop = () => stopping ??= (async () => {
await (await application.catch(() => void 0))?.shutdown.shutdown(0);
await send({ type: "shutdown-complete" });
if (process.connected) process.disconnect();
+ setTimeout(() => process.exit(0), 1_500).unref();
})();And the root-cause half for the MCP path, in 6. Why this also covers #8198#8198 is the same missing boundary from the other side: there the 5 s watchdog fires before dispose finishes ( Happy to open a PR against |
|
Confirming this on 0.1.5-rc.1 -> 0.2.1-rc.2, Windows 10, desktop profile. Environment
5 hypotheses were tested and all disproven:
Workaround that worked: run the already-downloaded installer manually This matches the koffi theory: no JS-level suspect reproduced it, and shutdown was (Evidence gathered with an AI assistant assisting log collection; report posted by the user.) |
|
Confirming the same failure on 0.2.0-rc.2 → 0.2.1-rc.2 (Windows x64, nightly), desktop update: Host did not complete graceful task teardown Environment
What happened
Additional observation (may help narrow the mechanism)In the 0.2.0-rc.2 app.asar, the only code reference to WorkaroundManual install from the pending installer: [worked / not tried yet — update after trying] Happy to collect |
Uh oh!
There was an error while loading. Please reload this page.
Summary
On Windows, clicking Check for Updates → Update in the desktop app always fails with:
The Host does complete its graceful shutdown and acknowledges it (
shutdown acknowledged true), but the Host process never exits, so the shell SIGTERMs it after the 10 s grace window and refuses the handoff. Manual installers work fine; only the in-app update path is affected.Root cause: the Host process loads
koffi(native FFI, pulled in by@deepseek-ai/dsh-fs-local/win32and the Windows console-signal installer).kofficreates two referenced libuv pipe handles at load time for its cross-thread callback channel and never releases them.desktop-hostnever callsprocess.exit()aftershutdown-complete, so the Host relies on the event loop draining — which it never does oncekoffiis loaded.Environment
0.2.1-rc.2(also reproduced on0.2.1-rc.1; the failed update was rc.2 → rc.1)DeepSeek Harness.exe)$DSH_HOME(moved to another drive via directory junctions, unrelated to the bug — reproduced with default layout too)Reproduction
The same thing happens on any normal quit (silently): the window closes but the process tree survives ~10–15 s before being force-killed.
Evidence
1.
koffialone pins a Node process forever2.
koffiadds two referenced pipesprocess.report.getReport().libuv, before/after requiringkoffi.node:They persist indefinitely:
3. Inside the real Host: teardown finishes, the process still doesn't exit
A temporary diagnostic plugin dumped
process._getActiveHandles()/process.getActiveResourcesInfo()from itsctx.effectdispose callback (unref'd sampling timer) during a real quit:{Pipe:1, Socket:13, Server:1, FSWatcher:5, ChildProcess:3}(3 MCP servers, 5 file watchers, the web server)So all plugin-owned resources are released within 500 ms; the residual liveness comes from non-JS libuv handles created by
koffi.4.
koffiexposes no way to release itGrepping the JS layer of the installed
koffipackage forunload|dispose|shutdown|unref|finalizereturns nothing. The pipes are created in the N-API layer and are not exposed.Code pointers
app.asar/lib/main.js—DesktopHost.stop(requireGraceful):app.asar/dsh/node_modules/@deepseek-ai/dsh-desktop-host/lib/index.js—stop()never exits explicitly:kofficonsumers:@deepseek-ai/dsh-fs-local/win32(Win32 ACL/DACL helpers) and the CLI’sinstallWindowsCliSignals()(SetConsoleCtrlHandler; its comment even says “The registration lasts until process exit”).Suggested fix (any one is enough)
dsh-desktop-hoststop(), callprocess.exit(0)right aftersend({ type: 'shutdown-complete' })(after the IPC flush). Everything has already been torn down at that point, so there is nothing to flush afterwards.koffi's wake-up pipes on shutdown (uv_unref/napi_finalize), or expose an explicitkoffi.unref()/koffi.unload().requireGraceful: a Host that acknowledged shutdown and whose teardown released all resources should not be rejected because of a native handle it cannot release.User-side workaround (for other users hitting this)
A tiny profile plugin can force the exit after teardown settles:
With this plugin, quits go from “hang 10 s then SIGTERM” to “exit in ~1 s, exitCode 0”, and in-app updates succeed.
(中文摘要)
Windows 桌面端点「更新」必定失败并报
Host did not complete graceful task teardown。原因是宿主进程加载了koffi(原生 FFI,由dsh-fs-local/win32与控制台信号安装器引入),它在初始化时创建两条 referenced 的 libuv pipe 用于跨线程回调唤醒,且从不释放;而desktop-host的stop()在发完shutdown-complete后没有调用process.exit(),只依赖事件循环排空,于是进程永远不退,10 秒后被 SIGTERM,更新器判定交接不干净而拒绝安装。实测:所有插件资源在 teardown 后 500ms 内全部释放;单测
require koffi.node后进程 8 秒仍不退出,而同样环境下其他原生模块与 Electron-as-node 都在百毫秒级正常退出。建议在stop()里补一行process.exit(0)。All reactions