fix(desktop): await remaining shutdown cleanup after rejection
This commit is contained in:
1 parent
e722ee6869
commit
39efcc36b5
5 files changed
+69
-7
No files matched your search
@@ -36,8 +36,8 @@
|
||||
en: 22153a298a6662e1
|
||||
zh: 3600803c8c9d0274
|
||||
/deepseek-harness-desktop/installation-ownership/runtime-and-plugin-activation:
|
||||
en: b80deb2229d886d9
|
||||
zh: fa0b82b1064f127d
|
||||
en: 64fe5fce4786ff17
|
||||
zh: d804df3714aa6b23
|
||||
/deepseek-harness-desktop/installation-ownership/host-environment:
|
||||
en: 98cf848762482d48
|
||||
zh: 74f9100a39ec8c2f
|
||||
|
||||
@@ -180,7 +180,7 @@ Before the first fatal dialog opens, Electron writes a crash report to the platf
|
||||
|
||||
Operation records and the application single-instance lock are isolated by `userData`; the shared platform logs directory does not store operation records. History from older test builds under `logs/plugin-operations` is not imported automatically because it may contain other profiles. Repairing an unfinished append preserves complete lines in the current segment and the previous segment. Host `transportDropped` is a lower bound as of the last successful delivery; Desktop rejects malformed or mismatched-run records without adding them to that counter.
|
||||
|
||||
After saving the basic report, the shell attempts to append recent plugin-management records in an isolated Worker packaged as `lib/operation-report-worker.js` inside `app.asar`. The completion marker and omission counters distinguish a finished collection from missing or partial material. The Host announces its run ID before profile loading; reported Host errors carry that ID as `hostRunId`, matching operation records from the same lifetime. Failure before this announcement leaves the run identity unknown. Recent operations are context, not proof of cause. Collection reads only two owned files under `userData/plugin-operations`, at most 64 KiB and 100 records; the complete appended section is limited to 64 KiB. The 500 ms collection budget reserves its final 100 ms for cooperative cancellation before requesting forced Worker termination; completion waits for its exit. Application exit, recovery exit or restart, and confirmed update installation also cancel collection and wait at most 500 ms for Worker exit. Shutdown prevents late basic-report saves from starting another collector. An update installation failure permits future collection again. Worker messages interrupt collection between bounded records; an in-flight file open, stat, read or close must settle, and synchronous parsing cannot be preempted within one record. The final append uses an abort signal but can leave partial bytes. Forced termination remains the fallback when cooperative cleanup does not finish. The basic report remains available independently. Only one collector runs at a time; concurrent reports keep their basic content without an auxiliary section. A rejected termination request does not release that slot until exit is observed. Get diagnostic report can reveal the basic file before the append finishes. The completion marker confirms only that collection ended, not that all operations were captured; queued bytes and omission counters describe missing material.
|
||||
After saving the basic report, the shell attempts to append recent plugin-management records in an isolated Worker packaged as `lib/operation-report-worker.js` inside `app.asar`. The completion marker and omission counters distinguish a finished collection from missing or partial material. The Host announces its run ID before profile loading; reported Host errors carry that ID as `hostRunId`, matching operation records from the same lifetime. Failure before this announcement leaves the run identity unknown. Recent operations are context, not proof of cause. Collection reads only two owned files under `userData/plugin-operations`, at most 64 KiB and 100 records; the complete appended section is limited to 64 KiB. The 500 ms collection budget reserves its final 100 ms for cooperative cancellation before requesting forced Worker termination; completion waits for its exit. Application exit, recovery exit or restart, and confirmed update installation also cancel collection and wait at most 500 ms for Worker exit. A rejected shutdown cleanup is logged without skipping the remaining cleanup waits. Shutdown prevents late basic-report saves from starting another collector. An update installation failure permits future collection again. Worker messages interrupt collection between bounded records; an in-flight file open, stat, read or close must settle, and synchronous parsing cannot be preempted within one record. The final append uses an abort signal but can leave partial bytes. Forced termination remains the fallback when cooperative cleanup does not finish. The basic report remains available independently. Only one collector runs at a time; concurrent reports keep their basic content without an auxiliary section. A rejected termination request does not release that slot until exit is observed. Get diagnostic report can reveal the basic file before the append finishes. The completion marker confirms only that collection ended, not that all operations were captured; queued bytes and omission counters describe missing material.
|
||||
|
||||
The journal retains two 256 KiB segments, accepts records up to 4 KiB, and bounds queued writes to 64 KiB. Exit, recovery restart and installer preparation wait at most 500 ms for queued writes; a timeout disables further writes, while an already-issued OS request may still settle. Records cover manager operations observed after Host startup, not direct CLI commands or manual file changes. Package-manager output remains in its existing detailed logs and is not copied into this summary. Missing records, a start without an end, and damaged history do not establish a successful operation. This collection does not diagnose kernel hangs, OOM or native crashes. Persistence failures also disable the writer for the rest of this shell lifetime, including after recovery or installer failure. This bounds repeated failing I/O and avoids appending behind an uncertain partial write. Restarting the application creates a new writer. Omission and repair counters cover only the current shell lifetime. Retained segments provide a larger local troubleshooting window than the deliberately smaller report attachment.
|
||||
|
||||
|
||||
@@ -180,7 +180,7 @@ macOS 上自定义菜单保留 Electron 的标准 Window 菜单及应用隐藏
|
||||
|
||||
操作记录目录与应用单实例锁均按 `userData` 隔离;共享平台日志目录不用于保存操作记录。旧测试包的 `logs/plugin-operations` 不自动导入,避免混入其他 profile 的历史。修复未写完的尾行时保留当前段的完整行与上一段。Host 的 `transportDropped` 只表示最近一次成功发送时的计数下界;Desktop 拒绝的非法或运行 ID 不匹配记录不计入该值。
|
||||
|
||||
基本报告保存后,壳会尝试通过独立 Worker 追加近期插件管理记录;该 Worker 以 `lib/operation-report-worker.js` 打包在 `app.asar` 中。完成标记和遗漏计数用于区分已完成收集与缺失或不完整的材料。Host 在加载 profile 前报告运行 ID;Host 错误中的 `hostRunId` 与该次运行的操作记录使用同一标识。在此通知之前发生的失败,其运行标识仍为未知。近期操作只是上下文,不是因果证据。收集只读取 `userData/plugin-operations` 下归其管理的两个文件,最多读取 64 KiB 和 100 条记录;完整追加内容不超过 64 KiB。500 ms 收集预算中的最后 100 ms 用于协作取消,随后才请求强制终止 Worker;只有 Worker 退出后才确认收集结束。应用退出、恢复退出或重启、确认安装更新也会取消收集,并最多等待 500 ms 确认 Worker 退出。退出过程中,延迟保存完成的基本报告不会再启动收集器。更新安装失败后可再次收集。Worker 在有界记录之间处理取消消息;已经发出的文件打开、状态读取、读取或关闭操作必须等待完成,单条记录的同步解析不能被抢占。最后的追加写入传入取消信号,但仍可能留下部分字节。协作清理未及时完成时仍需强制终止。基本报告独立可用。同时只运行一个收集器;并发报告保留基本内容,不追加辅助部分。终止请求被拒绝时,必须观察到退出才能释放该名额。“获取诊断报告”可能在追加完成前定位基本文件。完成标记仅表示收集结束,不表示捕获了全部操作;排队字节数和遗漏计数说明缺失材料。
|
||||
基本报告保存后,壳会尝试通过独立 Worker 追加近期插件管理记录;该 Worker 以 `lib/operation-report-worker.js` 打包在 `app.asar` 中。完成标记和遗漏计数用于区分已完成收集与缺失或不完整的材料。Host 在加载 profile 前报告运行 ID;Host 错误中的 `hostRunId` 与该次运行的操作记录使用同一标识。在此通知之前发生的失败,其运行标识仍为未知。近期操作只是上下文,不是因果证据。收集只读取 `userData/plugin-operations` 下归其管理的两个文件,最多读取 64 KiB 和 100 条记录;完整追加内容不超过 64 KiB。500 ms 收集预算中的最后 100 ms 用于协作取消,随后才请求强制终止 Worker;只有 Worker 退出后才确认收集结束。应用退出、恢复退出或重启、确认安装更新也会取消收集,并最多等待 500 ms 确认 Worker 退出。退出清理中的拒绝会被记录,但不会跳过其他清理等待。退出过程中,延迟保存完成的基本报告不会再启动收集器。更新安装失败后可再次收集。Worker 在有界记录之间处理取消消息;已经发出的文件打开、状态读取、读取或关闭操作必须等待完成,单条记录的同步解析不能被抢占。最后的追加写入传入取消信号,但仍可能留下部分字节。协作清理未及时完成时仍需强制终止。基本报告独立可用。同时只运行一个收集器;并发报告保留基本内容,不追加辅助部分。终止请求被拒绝时,必须观察到退出才能释放该名额。“获取诊断报告”可能在追加完成前定位基本文件。完成标记仅表示收集结束,不表示捕获了全部操作;排队字节数和遗漏计数说明缺失材料。
|
||||
|
||||
操作历史保留两个 256 KiB 文件段,单条记录不超过 4 KiB,排队写入不超过 64 KiB。退出、恢复重启和安装准备最多等待排队写入 500 ms;超时会停用后续写入,但已经发出的操作系统请求仍可能随后结束。记录覆盖 Host 启动后观察到的管理器操作,不包含直接执行的 CLI 命令或手工文件修改。包管理器输出保留在原有详细日志中,不复制到摘要。缺失记录、只有开始没有结束的操作以及损坏的历史,都不能说明操作已成功。此收集不诊断内核卡死、OOM 或原生崩溃。持久化失败也会在本次壳生命周期内停用写入,包括恢复或安装器失败后继续运行的情况,以限制重复失败的 I/O,并避免在可能残缺的写入后追加记录。重启应用会创建新写入器。遗漏和修复计数仅覆盖当前壳生命周期。保留文件段提供比报告附件更大的本地排障历史窗口。
|
||||
|
||||
|
||||
@@ -1328,9 +1328,9 @@ async function main(): Promise<void> {
|
||||
await operationJournal?.drain(500)
|
||||
throw error
|
||||
}),
|
||||
// A Platform cleanup failure is logged without cutting the remaining Host shutdown short.
|
||||
platformView.dispose().catch((error: unknown) => { console.error(error) })])
|
||||
.catch((error: unknown) => { console.error(error) }).finally(() => { app.quit() })
|
||||
platformView.dispose(),
|
||||
].map(cleanup => cleanup.catch((error: unknown) => { console.error(error) })))
|
||||
.then(() => { app.quit() })
|
||||
}
|
||||
app.on('before-quit', (event) => {
|
||||
if (shellInstallerOwnsQuit) {
|
||||
|
||||
@@ -2211,6 +2211,68 @@ describe('desktop main startup', () => {
|
||||
await harness.quitCompleted.promise
|
||||
})
|
||||
|
||||
it.each([{ rejectCleanup: false, stuck: false }, { rejectCleanup: true, stuck: false },
|
||||
{ rejectCleanup: true, stuck: true }])('waits for the diagnostic Worker (policy rejects=$rejectCleanup, stuck=$stuck)', async ({ rejectCleanup, stuck }) => {
|
||||
harness.embeddedPolicy = { origin: 'https://policy.example.com', authentication: 'feishu-test',
|
||||
allowedAuthOrigins: ['https://login.example.com'], allowedPageOrigins: ['https://downloads.example.com'] }
|
||||
vi.stubGlobal('fetch', vi.fn<typeof fetch>().mockImplementation(async () =>
|
||||
Response.json({ error: { code: 'UNAUTHENTICATED' } }, { status: 401 })))
|
||||
let finishReport!: () => void
|
||||
let reportSignal: AbortSignal | undefined
|
||||
let workerExited = false
|
||||
const report = new Promise<{ appended: boolean; timedOut: boolean; cancelled: boolean }>((resolve) => {
|
||||
finishReport = () => { workerExited = true; resolve({ appended: false, timedOut: false, cancelled: true }) }
|
||||
})
|
||||
vi.mocked(runOperationReportJob).mockImplementationOnce((_input, _options, signal) => {
|
||||
reportSignal = signal
|
||||
return report
|
||||
})
|
||||
let finishPolicy!: () => void
|
||||
let failPolicy!: (error: Error) => void
|
||||
const policy = new Promise<void>((resolve, reject) => { finishPolicy = resolve; failPolicy = reject })
|
||||
testAuth.dispose.mockImplementationOnce(() => policy)
|
||||
const failure = new Error('policy Session storage cleanup failed')
|
||||
const host = await readyForUpdate()
|
||||
let quitCompleted = false
|
||||
void harness.quitCompleted.promise.then(() => { quitCompleted = true })
|
||||
try {
|
||||
const window = harness.windows[0]!
|
||||
window.webContents.emit('render-process-gone', {}, { reason: 'crashed' })
|
||||
await harness.dialogShown.promise
|
||||
expect(runOperationReportJob).toHaveBeenCalledOnce()
|
||||
host.exited.resolve()
|
||||
window.emit('session-end', { reasons: ['shutdown'] })
|
||||
harness.app.quit()
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(testAuth.dispose).toHaveBeenCalledOnce()
|
||||
expect(reportSignal?.aborted).toBe(true)
|
||||
expect(quitCompleted).toBe(false)
|
||||
if (rejectCleanup) failPolicy(failure)
|
||||
else finishPolicy()
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
if (rejectCleanup) expect(console.error).toHaveBeenCalledWith(failure)
|
||||
expect(workerExited).toBe(false)
|
||||
expect(quitCompleted).toBe(false)
|
||||
if (stuck) {
|
||||
await vi.advanceTimersByTimeAsync(499)
|
||||
expect(quitCompleted).toBe(false)
|
||||
await vi.advanceTimersByTimeAsync(1)
|
||||
expect(workerExited).toBe(false)
|
||||
expect(console.error).toHaveBeenCalledWith('dsh desktop: operation report Worker has not exited; continuing shutdown')
|
||||
} else {
|
||||
finishReport()
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
}
|
||||
expect(quitCompleted).toBe(true)
|
||||
} finally {
|
||||
finishPolicy()
|
||||
finishReport()
|
||||
host.exited.resolve()
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
await harness.quitCompleted.promise
|
||||
}
|
||||
})
|
||||
|
||||
it.each([{ response: 0, stuck: false }, { response: 1, stuck: false }, { response: 2, stuck: false },
|
||||
{ response: 0, stuck: true }])('waits for Host exit and bounds Worker cleanup for recovery $response (stuck=$stuck)', async ({ response, stuck }) => {
|
||||
let finishReport!: (result: { appended: boolean; timedOut: boolean; cancelled: boolean }) => void
|
||||
|
||||
Reference in new issue
Block a user