Repository navigation
Replies: 1 comment
独立验证:根因确认无误;补量化上界,并对修复方案 #1 提出一个修正我在另一台机器、另一个安装上做了独立静态分析,先给结论:#9078 的根因分析完全正确,逐条源码位置在 0.2.0-rc.2 上行号级命中。下面补三条原文未覆盖的信息,其中第 3 条是我想请官方注意的方案修正。 0. 环境与交叉验证
复核原文引用位置,全部命中:
→ 这不是个别安装问题,是版本通用行为。 1. 补充:窗口期上界约 20 秒,不是"几秒"原文说"收尾非瞬时"但未量化。超时链在 async stop(requireGraceful = false) { // :3790
const graceful = await exitsWithin(exited, 1e4); // ① 优雅退出 10s
if (!graceful) child.kill("SIGTERM");
if (!await exitsWithin(exited, 5e3)) { // ② 再等 5s
child.kill("SIGKILL");
if (!await exitsWithin(exited, 5e3)) throw ... // ③ 再等 5s
}
}最坏 10 + 5 + 5 = 20 秒锁被持有。另加 2. 补充:进程树是嵌套的,规模随会话增长实测退出前同一棵树有 9 个进程,且 Host 下面还有孙进程: Host 收到 3.
|
| 优先级 | 措施 | 理由 |
|---|---|---|
| P0 | 抢锁失败分支不要静默(原文 #2) | 零风险,直接消除困惑。把静默 application.quit() 换成提示"DSH 正在退出,请稍候再试" |
| P1 | 收窄视觉不一致窗口(原文 #3) | 把 tray?.dispose()(:12209) 与 mainWindow.hide()(:12212) 移到 .finally() 里跟 app.quit() 一起执行。用户"看到图标消失"是唯一的退出完成信号,而当前这个信号是假的。这是我最想要的一条 |
| P2 | releaseSingleInstanceLock |
仅在与 session/writer-held 的友好降级同时做才有意义 |
4. 建议与 #8899 / #9040 一起收口
#8899(托盘加「重启 Desktop」)、#9040(生产模式加「重启」菜单项)想做的是 app.relaunch(),而 :10984、:11909 已有 app.relaunch()。这两条的实现路径会原样撞上本文的单实例锁时序——"重启"会复现"点了没反应"。建议官方把 #9078 / #8899 / #9040 作为同一个收尾时序问题一起处理。
5. 修正后的用户侧规避
- 不要只等 2-3 秒。确认整棵进程树消失再启动:
无输出才算退干净。
Get-Process "DeepSeek Harness" -ErrorAction SilentlyContinue | Select-Object Id,ProcessName
- 仍推荐用托盘菜单「退出」(
:10863,走app.quit()真退出路径),点窗口 X 只是 hide 到托盘。
方法学声明:以上为对已发布只读产物的静态分析(app.asar 未混淆、保留 JSDoc),未修改任何文件;"20 秒"为源码超时常量的算术上界推导,非端到端实测计时。第 3 条的风险判断基于 lease 模块的自述语义,若官方能确认旧进程在 quitting 阶段是否会主动释放 session.lock,这条修正的适用性即可定论——我在产物里没有找到主动释放逻辑(锁随进程退出由内核释放)。
Uh oh!
There was an error while loading. Please reload this page.
概述
在 Windows 上退出 DSH 桌面端后,再次点击桌面图标有时无任何反应,必须点击第二次才会打开窗口。用户侧观感是"要连点两次",实际根因是旧进程尚未完全退出、单实例锁仍未释放,此时的新进程会静默自杀。
环境与构建版本
@deepseek-ai/dsh-desktop0.2.0-rc.204f392c9ddd144fa426da2045178797da6db6c11<install>\resources\app.asar(默认安装位置)https://download.deepseek.com/dsh-desk/feeds/win-x64/)%APPDATA%\@deepseek-ai根因
1. 单实例锁的生命周期 = 进程生命周期
lib/main.js:6871-6878(lib/types/single-instance.js):Electron 的
requestSingleInstanceLock()持有的锁在整个进程生命周期内有效。旧进程只要还有一个 PID 未退出,后继启动就抢锁失败,随后application.quit()—— 不弹窗、不报错、无任何视觉反馈,与"点了没反应"完全一致。2. 退出路径是异步的,且窗口先于进程消失
lib/main.js:12204-12228:注意执行顺序:在步骤 ②(窗口隐藏)和步骤 ④(进程退出)之间存在一个时间窗口。这段时间里:
hide(),托盘图标已destroy()→ 用户视觉上判定"已经退出了"backend.close()还在等 PTY / MCP 子进程 / SQLite / 会话落盘收尾 → 进程仍存活 → 锁未释放用户此刻点击图标,正好落进这个窗口期,于是命中上面的抢锁失败路径。
实测该会话中
DeepSeek Harness.exe同时存在 7 个进程(1 main + 1 renderer + 1 gpu-process + 1 utility + 3 个 node 后端),可见后端进程组规模不小,收尾并非瞬时完成。(进程数量随打开的会话数变化,此处仅为量级参考。)3. 加重因素 A:点 X 默认不是退出,而是隐藏到托盘
lib/main.js:12029-12041:托盘图标是常驻的,
lib/main.js:10834注释明确写着:这解释了另一类表现:如果用户点 X"退出"后进程其实还活着(藏在托盘),此时点击桌面图标会走
second-instance→focusPrimaryWindow(),一次就成功(窗口被调出,可能还带着之前的会话状态)。用户因此很难分辨自己遇到的是哪一种情况。4. 加重因素 B:退出过程中的
second-instance会被直接吞掉lib/main.js:12166-12189:即使
second-instance事件被成功投递到旧进程,if (quitting) return也会让它什么都不做。这与 #1 的锁竞争是同一个根因的两种表现:无论事件是没送达(新进程抢锁失败自杀)还是送达了被忽略(quitting早退),用户看到的都是"点了没反应"。建议修复方向
按侵入性从低到高:
提前释放单实例锁(推荐,改动最小)
在
finishQuit()开头、启动异步收尾之前调用app.releaseSingleInstanceLock()。这样旧进程仍在收尾时,后继启动即可正常接管,不再抢锁失败自杀。让"无反应"变得可见
在
claimDesktopSingleInstance抢锁失败的分支里,不要静默application.quit();可以短暂提示"DSH 正在退出,请稍候再试",把静默失败变成可理解的反馈。收窄视觉与进程状态的不一致窗口
将
mainWindow.hide()/tray.dispose()从finishQuit()开头移到backend.close()完成之后,让"窗口消失"与"进程退出"在时间上尽量对齐(代价是用户观感上退出变慢)。兜底:退出中收到
second-instance时给出反馈在
focusPrimaryWindow的if (quitting) return分支里,至少不要完全静默。其中 #1 单独就能消除主要症状,#2 可作为体验补强。
用户侧临时规避
lib/main.js:10863-10866的 quit 项,走app.quit()真退出路径),而不是点窗口 X定位方法备查
本文结论来自对已发布产物的静态分析,非行为猜测。
app.asar可直接解包读取lib/main.js(490,173 字节,未混淆,保留原始 JSDoc 注释):涉及的源码位置汇总:
lib/main.js:6871-6878claimDesktopSingleInstance— 单实例锁,抢不到即静默退出lib/main.js:12204-12228finishQuit— 异步收尾,隐藏窗口与释放锁之间的窗口期lib/main.js:12229-12262before-quit— 退出确认流程lib/main.js:12029-12041window.on("close")— 点 X 拦成 hidelib/main.js:12166-12189focusPrimaryWindow—if (quitting) return吞掉启动请求lib/main.js:10832-10876DesktopTray— 托盘常驻与手动退出入口lib/main.js:10879-10933DesktopBackgroundNotice— 首次隐藏到托盘的确认弹窗lib/main.js:12319-12321claimDesktopSingleInstance(app, () => focusPrimaryWindow())All reactions