mirror of
https://gh-proxy.org/https://github.com/klinsmaya/workdsh.git
synced 2026-11-10 18:36:56 +08:00
feat(desktop): track unclean process exits
This commit is contained in:
@@ -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()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user