🔒 fix(desktop): 单实例锁——双开不再双 sidecar 并发写 ~/.lume - #417
Conversation
settings 有 lockfile 但 sessions/memory/audit 的 JSONL 与 sqlite 均无跨进程锁, 双开应用即双 sidecar 并发追加同一数据目录,交错写入与事件 seq 续读错乱 难以归因。标准 requestSingleInstanceLock:第二实例直接退出,首实例经 second-instance 聚焦/恢复既有窗口承接。锁在 app ready 前申请;未获锁时 whenReady 回调短路,避免窗口创建竞速。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Code Review 结论:✅ 实现正确,可合并(附一条健壮性建议)已验证的点
建议(P2):锁申请位置上移到模块顶部当前锁申请在 main.ts:3208+(模块尾部),保护是"位置敏感"的——今天恰好前置顶层代码无副作用,但未来任何人在 main.ts 前部加顶层初始化(开 sqlite、spawn sidecar 等)都会静默绕过单实例锁。注释写着"锁必须在 app ready 前申请",建议代码位置也体现这一约束:import 区之后立即申请(如 备注:CI 6/6 全绿、MERGEABLE。 |
锁原位于 main.ts 尾部(child-process-gone 注册之后),恰因前置顶层 语句均无盘写副作用而侥幸安全;上移到 import 区之后、任何顶层初始化 之前,使"锁必须最早申请"的约束由代码位置保证——后续在文件前部新增 顶层副作用(开库、spawn 等)不再可能绕过单实例保护。注释同步说明 该位置约束的原因。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Code Review可合。验证过程:锁请求位于 main.ts:161(import 之后、第一条语句之前);全 desktop src grep 模块顶层磁盘 I/O 零命中(无模块在拿锁前做盘写);唯一一处 两个 P3 记录: [P3] 锁粒度不感知 LUME_CONFIG_DIR,不同数据目录的实例被过度互斥 [P3] 「早于任何顶层初始化副作用」的承诺依赖一条未强制的不变量 |
Closes #290
问题
settings 有自己的 lockfile(fail-closed),但 sessions/memory/audit 的 JSONL 与 sqlite 均无跨进程锁。双开应用即双 sidecar 并发写同一
~/.lume数据目录:JSONL 追加交错、事件 seq 按文件续读错乱、sqlite 锁冲突——用户视角是"偶尔丢消息",损坏难以归因。方案
标准 Electron 单实例模式:
app.requestSingleInstanceLock()在 app ready 前申请;未获锁直接app.quit(),且whenReady回调短路,避免窗口/sidecar 创建竞速second-instance:无窗口则重建,最小化则 restore,最后聚焦——复用activate分支的既有模式已知边界:锁按 Electron 默认 userData 作用域,不区分 launcher config dir。多 profile 并存是开发者场景且各写各的目录、本无并发写冲突;如未来需要 config-dir 粒度可在锁前同步解析 launcher config 后
setPath('userData')再申请。测试
真实双开行为需打包后手工验证一次(
bun dev双进程不走同一构建产物路径,行为可能与打包态不同)。