Windows: host process crashes with ENOENT in OutputCollector.spillAll after a temp cleaner removes %TEMP%\dsh-subprocess-* #2252
Replies: 3 comments
|
Confirming this still reproduces at function privateSpillDir() {
defaultSpillDir ??= mkdtempSync(join(tmpdir(), "dsh-subprocess-"));
return defaultSpillDir;
}spillAll(chunk) {
...
if (this.spillFd === void 0) {
this.spillFile = join(this.spillDir, `dsh-subprocess-${process.pid}-${++spillCounter}-...`);
this.spillFd = openSync(this.spillFile, "wx", 384);
for (const prior of this.chunks) writeSync(this.spillFd, prior);
}
writeSync(this.spillFd, chunk);
}The Two details from having reimplemented this same collector for The failure really is confined to the first open after deletion, which corroborates your The throw site is what makes it fatal rather than annoying. On your suggested fix, agreed on both parts, with one adjustment to the second. For what it is worth, One honest difference in ours. We use a fixed per-pid directory name rather than |
|
Thanks — adopted, with your adjustment replacing our original second half entirely. The implementation (in our local fork; happy to hand over the patch) now does exactly what you describe: attempt the open, and on Your repro nuance is now encoded in the tests as well: a cleaner-removes-dir case healing through the refresh with exactly one retry, a refreshed-dir-also-gone case disabling spilling, a non- On the fixed per-pid name in |
|
This is the same unguarded
One delta from your final design: I recreate the cached Your "delete must precede the first overflow" repro nuance is exactly why this needs a long quiet session to show: the |
Uh oh!
There was an error while loading. Please reload this page.
Reporting a host-crash bug here since issues are disabled. Environment:
@deepseek-ai/dsh0.1.0-rc.5 installed globally from npm, Windows 10/11. Trigger: a%TEMP%cleaning tool running during an active session (Huorong / 火绒 in the original case; CCleaner and Windows Storage Sense are the same class).Crash
The whole dsh host process dies mid-task.
Root cause
As read from
packages/subprocess/subprocess-local/src/spawn.tsat rc.5:privateSpillDir()creates the private spill directory withmkdtempSynconce per process and caches it for the process lifetime — it is never revalidated.openSync(file, 'wx')fails withENOENT— the file name is freshly random, soENOENTcan only mean the parent directory is gone.'data'listener (stream.on('data', chunk => collector.push(chunk))), so it becomes an uncaught exception and kills the host. The-1-in the file name above is the spill counter: this was the first spill of that process, i.e. the directory sat unused through a long session and was cleaned in between.Every neighboring path already degrades gracefully — a spill past its cap is discarded, a failed final close withholds the spill path, and
dsh-spill-policylogs a failedsaveTextand keeps the inline result. This open/write was the one unguarded surface.Reproduction
Start a session, delete
%TEMP%\dsh-subprocess-*(or run a temp cleaner), then run any command whose collected output exceeds the in-memory cap. The host crashes on the first spill open.Suggested fix
Two small changes in
spawn.ts:spillAll()in a catch that permanently disables spilling for that stream via the existingdiscardSpill()state — the failure never propagates out ofpush(), and the truncated tail keeps serving reads.privateSpillDir()withexistsSyncon each spawn and recreate it withmkdtempSyncwhen missing, so one deletion costs only the collectors racing it instead of every later spawn's spill.Sibling-pattern audit:
dsh-spill-local'sprivateRoot()is not affected —saveTextFile()recreates the session directory via recursivemkdiron every save, and its async failures are already contained bydsh-spill-policy.中文小结:Windows 临时文件清理工具(火绒等)删除
%TEMP%\dsh-subprocess-*后,会话中第一条输出超过内存上限的命令会让整个 dsh 宿主进程以未捕获异常崩溃;根因与最小修复如上,两处改动都在dsh-subprocess-local的spawn.ts。All reactions