feat(desktop): track unclean process exits

This commit is contained in:
Qiuner
2026-08-18 00:26:03 +08:00
parent 4d54ab8696
commit 9bb5a1cff7
2 changed files with 115 additions and 0 deletions
+52
View File
@@ -0,0 +1,52 @@
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs'
import { dirname } from 'node:path'
/** Identity persisted while one desktop process is active. */
export interface DesktopRunRecord {
readonly startedAt: string
readonly pid: number
readonly version: string
}
export interface UnreadableDesktopRun {
readonly unreadable: true
}
/** Active run marker and evidence recovered from the previous launch. */
export interface DesktopRun {
readonly previousRun: DesktopRunRecord | UnreadableDesktopRun | undefined
/** Remove this process's active marker before a controlled exit. */
markClean(): void
}
function readPreviousRun(statePath: string): DesktopRunRecord | UnreadableDesktopRun | undefined {
if (!existsSync(statePath)) return undefined
try {
const value: unknown = JSON.parse(readFileSync(statePath, 'utf8'))
if (typeof value !== 'object' || value === null) return { unreadable: true }
const record = value as Partial<DesktopRunRecord>
if (typeof record.startedAt !== 'string'
|| typeof record.pid !== 'number'
|| typeof record.version !== 'string') return { unreadable: true }
return { startedAt: record.startedAt, pid: record.pid, version: record.version }
} catch (cause) {
if (cause instanceof SyntaxError) return { unreadable: true }
throw cause
}
}
/** Persist this launch and return a marker left behind by an unclean exit. */
export function beginDesktopRun(statePath: string, currentRun: DesktopRunRecord): DesktopRun {
const previousRun = readPreviousRun(statePath)
mkdirSync(dirname(statePath), { recursive: true })
writeFileSync(statePath, `${JSON.stringify(currentRun)}\n`, 'utf8')
let clean = false
return {
previousRun,
markClean() {
if (clean) return
clean = true
unlinkSync(statePath)
},
}
}
@@ -0,0 +1,63 @@
import { mkdtempSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import { beginDesktopRun } from '../src/crash-evidence.ts'
describe('desktop crash evidence', () => {
it('reports the previous run when it did not shut down cleanly', () => {
const statePath = join(mkdtempSync(join(tmpdir(), 'dsh-run-')), 'active-run.json')
beginDesktopRun(statePath, {
startedAt: '2026-08-18T00:00:00.000Z',
pid: 41,
version: '2.0.1',
})
const next = beginDesktopRun(statePath, {
startedAt: '2026-08-18T00:01:00.000Z',
pid: 42,
version: '2.0.1',
})
expect(next.previousRun).toEqual({
startedAt: '2026-08-18T00:00:00.000Z',
pid: 41,
version: '2.0.1',
})
})
it('does not report a run that marked its shutdown clean', () => {
const statePath = join(mkdtempSync(join(tmpdir(), 'dsh-run-')), 'active-run.json')
const run = beginDesktopRun(statePath, {
startedAt: '2026-08-18T00:00:00.000Z',
pid: 41,
version: '2.0.1',
})
expect(() => {
run.markClean()
run.markClean()
}).not.toThrow()
const next = beginDesktopRun(statePath, {
startedAt: '2026-08-18T00:01:00.000Z',
pid: 42,
version: '2.0.1',
})
expect(next.previousRun).toBeUndefined()
})
it('reports an unreadable previous marker without blocking the next run', () => {
const statePath = join(mkdtempSync(join(tmpdir(), 'dsh-run-')), 'active-run.json')
writeFileSync(statePath, '{partial', 'utf8')
const run = beginDesktopRun(statePath, {
startedAt: '2026-08-18T00:01:00.000Z',
pid: 42,
version: '2.0.1',
})
expect(run.previousRun).toEqual({ unreadable: true })
expect(() => run.markClean()).not.toThrow()
})
})