# 2026-04-21 桌面单实例续接记录

## 目标

本轮要落实的口径是：

- 已有客户端在运行时，再次启动不应继续跑第二个主流程
- 首实例应恢复/显示/聚焦原窗口
- 不应新增一轮 runtime/gateway 启动或冲突日志

对应自动验证命令：

```powershell
corepack pnpm smoke:desktop-single-instance
```

## 已完成

代码侧已落地以下改动：

- `apps/desktop/src/main/index.ts`
  - 增加 `app.requestSingleInstanceLock()`
  - 增加 `second-instance` 处理
  - 增加主窗口跟踪与唤回逻辑：
    - `mainWindow`
    - `focusMainWindow()`
    - `restoreOrCreateMainWindow()`
    - `createTrackedMainWindow()`
  - 增加单实例 smoke 辅助状态与输出：
    - `QJCLAW_SMOKE_SINGLE_INSTANCE`
    - `QJCLAW_SMOKE_SINGLE_INSTANCE_READY_PATH`
    - `QJCLAW_SMOKE_SINGLE_INSTANCE_EVENT_PATH`
    - `runSingleInstanceSmoke()`
    - `secondInstanceEventCount`
    - `lastSecondInstanceEventSnapshot`
  - `activate` 事件改为统一走 `restoreOrCreateMainWindow("activate")`

- `build/scripts/desktop-single-instance-smoke.ps1`
  - 已新增单实例 smoke 脚本
  - 流程是：
    - 起第一个 Electron 实例
    - 等首实例写 `ready`
    - 起第二个 Electron 实例
    - 校验第二个实例快速退出
    - 校验首实例收到 `second-instance`
    - 校验窗口数仍为 `1`
    - 校验 `Launching bundled runtime command` 次数不增加
    - 校验日志中没有：
      - `gateway already running`
      - `Port 18889 is already in use`
      - `schtasks /End /TN`

- `package.json`
  - 已新增脚本：
    - `smoke:desktop-single-instance`

- `build/scripts/README.md`
  - 已登记 `desktop-single-instance-smoke.ps1`

## 已验证

- 已通过：

```powershell
corepack pnpm typecheck
```

结果：通过。

## 当前阻塞

自动 smoke 目前还没有跑通，阻塞点不是 TypeScript 编译，而是本机环境残留了多个无法在当前权限下结束的 `electron.exe`。

已观察到的残留 PID：

- `10420`
- `19736`
- `23484`
- `23564`
- `23588`
- `23820`
- `24384`

现象：

- `corepack pnpm smoke:desktop-single-instance` 失败
- 失败点是首实例迟迟没有写出 `ready` 文件
- 手工尝试结束残留进程时，`taskkill` / `Stop-Process` 都报 `Access is denied`
- 在残留进程存在时，单实例锁环境不干净，自动验证结果不可信

失败输出里最关键的信息是：

- `First Electron instance never reported single-instance readiness.`

相关失败产物路径：

- `D:\qjclaw\.tmp\desktop-single-instance-smoke\result.json`
- `D:\qjclaw\.tmp\desktop-single-instance-smoke\result.json.trace.log`
- `D:\qjclaw\.tmp\desktop-single-instance-smoke\logs\startup\startup-latest.log`

## 已尝试但未完成

已执行过这些命令，但因为权限问题未能清掉残留进程：

```powershell
taskkill /IM electron.exe /F /T
```

```powershell
Stop-Process -Id 10420,19736,23484,23564,23588,23820,24384 -Force
```

校验残留进程的命令：

```powershell
Get-Process electron -ErrorAction SilentlyContinue
```

当前结论：

- 代码改动已经在仓库里
- 当前最先要解决的是机器上的残留 Electron 进程
- 在这个问题解决前，不要把 smoke 失败直接归因到单实例代码本身

## 下次继续时先做什么

优先顺序：

1. 先清掉所有残留 `electron.exe`
2. 再确认 `Get-Process electron -ErrorAction SilentlyContinue` 没有输出
3. 再跑单实例 smoke
4. 若 smoke 仍失败，再根据 `.tmp` 里的 trace/log 继续定位

推荐命令：

```powershell
Get-Process electron -ErrorAction SilentlyContinue
```

如果仍杀不掉，直接重启机器。

重启或清理完成后先跑：

```powershell
corepack pnpm smoke:desktop-single-instance
```

如果还失败，再补跑：

```powershell
powershell -ExecutionPolicy Bypass -File build/scripts/startup-binding-smoke.ps1
```

目的：

- `desktop-single-instance-smoke` 用来验证单实例口径本身
- `startup-binding-smoke` 用来判断是不是更基础的桌面启动链路也有问题

## 继续排查时要看的文件

- `D:\qjclaw\apps\desktop\src\main\index.ts`
- `D:\qjclaw\build\scripts\desktop-single-instance-smoke.ps1`
- `D:\qjclaw\package.json`
- `D:\qjclaw\build\scripts\README.md`

失败时优先看：

- `D:\qjclaw\.tmp\desktop-single-instance-smoke\result.json`
- `D:\qjclaw\.tmp\desktop-single-instance-smoke\result.json.trace.log`
- `D:\qjclaw\.tmp\desktop-single-instance-smoke\logs\startup\startup-latest.log`
- `D:\qjclaw\.tmp\desktop-single-instance-smoke\logs\runtime-manager.log`

## 当前工作区事实

和这次单实例相关的工作区改动包括：

- `apps/desktop/src/main/index.ts`
- `build/scripts/desktop-single-instance-smoke.ps1`
- `package.json`
- `build/scripts/README.md`

其中：

- `build/scripts/desktop-single-instance-smoke.ps1` 当前是新增文件
- 其余几个文件已有未提交改动

## 一句话结论

单实例代码和 smoke 脚本已经落地，`typecheck` 已通过；下次续接前先解决本机残留的高权限 `electron.exe`，否则自动验证不会有可信结果。
## 2026-04-21 最新进展补充

### 本轮已继续完成的代码侧工作

- `apps/desktop/src/main/index.ts`
  - 已把单实例入口继续接完整：
    - `app.requestSingleInstanceLock()`
    - `second-instance` 接管并聚焦现有主窗口
    - `mainWindow` 跟踪
    - `focusMainWindow()`
    - `restoreOrCreateMainWindow()`
    - `createTrackedMainWindow()`
  - 已补单实例 smoke 专用信号：
    - `QJCLAW_SMOKE_SINGLE_INSTANCE`
    - `QJCLAW_SMOKE_SINGLE_INSTANCE_READY_PATH`
    - `QJCLAW_SMOKE_SINGLE_INSTANCE_EVENT_PATH`
    - `runSingleInstanceSmoke()`
  - 已补窗口状态与日志辅助信息：
    - `secondInstanceEventCount`
    - `lastSecondInstanceEventSnapshot`
    - `mainWindowLoadState`
    - `window.load-state` 启动日志
  - 已在 smoke 模式下加：
    - `app.disableHardwareAcceleration()`

- `build/scripts/desktop-single-instance-smoke.ps1`
  - 已去掉对 `Start-Process` 返回 PID 的“提前失败”判断
  - 现在不会因为 Electron 首个 PID 变化而误判首实例退出

### 本轮已执行的验证

已执行并通过：

```powershell
corepack pnpm --filter @qjclaw/desktop build
```

```powershell
corepack pnpm typecheck
```

已执行但失败：

```powershell
corepack pnpm smoke:desktop-single-instance
```

```powershell
powershell -ExecutionPolicy Bypass -File build/scripts/startup-binding-smoke.ps1
```

### 关键新结论

- 当前问题已经不能再只归因到“单实例 smoke 本身”
- `startup-binding-smoke` 也失败了，说明更底层的桌面启动链路本身就不稳定
- `startup-binding-smoke` 的失败点是：
  - Electron 还没写出 smoke output 就退出了
  - 退出码是 `-2147483645`
- `desktop-single-instance-smoke` 仍然失败，但现在可以确认：
  - 不再是脚本盯着首个 PID 导致的误判
  - 更像是首实例窗口/渲染链路没稳定进入可用状态

### 这轮看到的最关键日志现象

- 单实例 smoke 的 trace 只走到：
  - `runSingleInstanceSmoke:start`
- 基础启动 smoke 的 trace 走到：
  - `runSmokeTest:start`
  - `runSmokeTest:renderer-loading`
  - `runSmokeTest:loading-renderer-state`
- `startup-latest.log` 里，主窗口 load-state 目前只看到：
  - `created`
- 没有继续看到：
  - `did-start-loading`
  - `dom-ready`
  - `did-finish-load`
  - `did-fail-load`
  - `render-process-gone`

这说明：

- 主窗口创建是发生了的
- 但后续页面加载事件没有稳定落到日志里
- 现象更接近 Electron / Chromium 级别的启动异常或系统态残留干扰

### 运行时相关补充观察

- `runtime-manager.log` 里持续可见：
  - `Launching bundled runtime command`
  - `Bundled runtime direct spawn was blocked with EPERM; retrying via PowerShell wrapper.`
  - `Bundled runtime failed to spawn: spawn EPERM`

这不是本轮唯一问题，但说明当前环境里还有额外权限限制噪音，不能把 smoke 失败简单归因到单实例代码。

### 当前机器状态的新结论

当前机器上确认有一颗高权限残留 `electron.exe`：

- PID：`7216`
- Parent PID：`12892`
- 但父进程已经不存在
- `Get-Process` 结果里：
  - `HandleCount = 0`
  - `Path` 为空
  - `StartTime = 2026/4/21 15:30:31`

已尝试但仍失败：

```powershell
taskkill /PID 7216 /F /T
```

```powershell
Stop-Process -Id 7216 -Force
```

以及通过 `schtasks` 以 `SYSTEM` 身份触发 `taskkill`

当前判断：

- 这颗进程已经接近“残留/僵尸进程”
- 继续在当前系统态下跑 smoke，结果不可信
- 最稳妥做法仍然是先重启机器

### 更新后的下次继续顺序

1. 先重启机器，清掉 PID `7216`
2. 重启后先确认：

```powershell
Get-Process electron -ErrorAction SilentlyContinue
```

3. 如果没有输出，先跑：

```powershell
powershell -ExecutionPolicy Bypass -File build/scripts/startup-binding-smoke.ps1
```

4. 如果基础启动 smoke 通过，再跑：

```powershell
corepack pnpm smoke:desktop-single-instance
```

5. 如果基础启动 smoke 仍失败，优先看：
  - `D:\qjclaw\.tmp\startup-binding-smoke\result.json`
  - `D:\qjclaw\.tmp\startup-binding-smoke\result.json.trace.log`
  - `D:\qjclaw\.tmp\startup-binding-smoke\logs\startup\startup-latest.log`
  - `D:\qjclaw\.tmp\startup-binding-smoke\logs\runtime-manager.log`

### 当前这轮最准确的一句话结论

单实例代码和单实例 smoke 已继续补强，但新的验证结果表明当前主要阻塞已经下沉到更底层的 Electron 启动链路与机器残留进程状态；在重启清掉高权限残留 `electron.exe` 之前，继续跑 smoke 不会有可信结果。
