refactor(desktop): deepen crash evidence lifecycle ownership

This commit is contained in:
Qiuner
2026-08-19 17:10:00 +08:00
parent db56201aa7
commit 8464e9e823
2 changed files with 140 additions and 12 deletions
+104 -11
View File
@@ -1,5 +1,21 @@
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs'
import { dirname } from 'node:path'
import { randomUUID } from 'node:crypto'
import {
chmodSync,
closeSync,
constants,
fstatSync,
lstatSync,
mkdirSync,
openSync,
readFileSync,
renameSync,
unlinkSync,
writeFileSync,
} from 'node:fs'
import { basename, dirname, join } from 'node:path'
const PRIVATE_DIRECTORY_MODE = 0o700
const PRIVATE_FILE_MODE = 0o600
/** Minimal Electron crash reporter surface used before the app is ready. */
export interface DesktopCrashReporter {
@@ -51,34 +67,111 @@ export interface DesktopRun {
markClean(): void
}
function readPreviousRun(statePath: string): DesktopRunRecord | UnreadableDesktopRun | undefined {
if (!existsSync(statePath)) return undefined
interface StoredDesktopRun extends DesktopRunRecord {
readonly ownerId?: string
}
function lstatOptional(filename: string): ReturnType<typeof lstatSync> | undefined {
try {
const value: unknown = JSON.parse(readFileSync(statePath, 'utf8'))
return lstatSync(filename)
} catch (cause) {
if ((cause as NodeJS.ErrnoException).code === 'ENOENT') return undefined
throw cause
}
}
function assertOwnedMarker(stats: NonNullable<ReturnType<typeof lstatSync>>): void {
if (stats.isSymbolicLink() || !stats.isFile() || stats.nlink > 1) {
throw new Error('dsh-plugin-desktop: active run marker is invalid')
}
}
function readStoredRun(statePath: string): StoredDesktopRun | UnreadableDesktopRun | undefined {
const pathStats = lstatOptional(statePath)
if (pathStats === undefined) return undefined
assertOwnedMarker(pathStats)
const descriptor = openSync(statePath, constants.O_RDONLY | noFollowFlag())
try {
assertOwnedMarker(fstatSync(descriptor))
const value: unknown = JSON.parse(readFileSync(descriptor, 'utf8'))
if (typeof value !== 'object' || value === null) return { unreadable: true }
const record = value as Partial<DesktopRunRecord>
const record = value as Partial<StoredDesktopRun>
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 }
return {
startedAt: record.startedAt,
pid: record.pid,
version: record.version,
...(typeof record.ownerId === 'string' ? { ownerId: record.ownerId } : {}),
}
} catch (cause) {
if (cause instanceof SyntaxError) return { unreadable: true }
throw cause
} finally {
closeSync(descriptor)
}
}
function unlinkTemporary(filename: string): void {
try {
unlinkSync(filename)
} catch (cause) {
if ((cause as NodeJS.ErrnoException).code !== 'ENOENT') throw cause
}
}
function writeCurrentRun(statePath: string, currentRun: StoredDesktopRun): void {
const directory = dirname(statePath)
mkdirSync(directory, { recursive: true, mode: PRIVATE_DIRECTORY_MODE })
const directoryStats = lstatSync(directory)
if (directoryStats.isSymbolicLink() || !directoryStats.isDirectory()) {
throw new Error('dsh-plugin-desktop: active run directory is invalid')
}
try { chmodSync(directory, PRIVATE_DIRECTORY_MODE) } catch {}
const temporary = join(directory, `.${basename(statePath)}.${process.pid}.${randomUUID()}.tmp`)
try {
writeFileSync(temporary, `${JSON.stringify(currentRun)}\n`, {
encoding: 'utf8',
flag: 'wx',
mode: PRIVATE_FILE_MODE,
})
try { chmodSync(temporary, PRIVATE_FILE_MODE) } catch {}
renameSync(temporary, statePath)
} finally {
unlinkTemporary(temporary)
}
}
/** 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')
const storedPreviousRun = readStoredRun(statePath)
const previousRun = storedPreviousRun === undefined || 'unreadable' in storedPreviousRun
? storedPreviousRun
: {
startedAt: storedPreviousRun.startedAt,
pid: storedPreviousRun.pid,
version: storedPreviousRun.version,
}
const ownerId = randomUUID()
writeCurrentRun(statePath, { ...currentRun, ownerId })
let clean = false
return {
previousRun,
markClean() {
if (clean) return
clean = true
const storedRun = readStoredRun(statePath)
if (storedRun === undefined || 'unreadable' in storedRun || storedRun.ownerId !== ownerId) {
clean = true
return
}
unlinkSync(statePath)
clean = true
},
}
}
function noFollowFlag(): number {
return process.platform === 'win32' ? 0 : constants.O_NOFOLLOW
}
@@ -1,4 +1,4 @@
import { mkdtempSync, writeFileSync } from 'node:fs'
import { existsSync, linkSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { describe, expect, it, vi } from 'vitest'
@@ -81,4 +81,39 @@ describe('desktop crash evidence', () => {
expect(run.previousRun).toEqual({ unreadable: true })
expect(() => run.markClean()).not.toThrow()
})
it('only clears the marker owned by the current run', () => {
const statePath = join(mkdtempSync(join(tmpdir(), 'dsh-run-')), 'active-run.json')
const first = beginDesktopRun(statePath, {
startedAt: '2026-08-18T00:00:00.000Z',
pid: 41,
version: '2.0.1',
})
const second = beginDesktopRun(statePath, {
startedAt: '2026-08-18T00:01:00.000Z',
pid: 42,
version: '2.0.1',
})
first.markClean()
expect(JSON.parse(readFileSync(statePath, 'utf8'))).toMatchObject({ pid: 42 })
expect(() => second.markClean()).not.toThrow()
expect(existsSync(statePath)).toBe(false)
})
it('does not read or overwrite a linked marker', () => {
const directory = mkdtempSync(join(tmpdir(), 'dsh-run-'))
const target = join(directory, 'outside.json')
const statePath = join(directory, 'active-run.json')
writeFileSync(target, '{"outside":true}\n', 'utf8')
linkSync(target, statePath)
expect(() => beginDesktopRun(statePath, {
startedAt: '2026-08-18T00:01:00.000Z',
pid: 42,
version: '2.0.1',
})).toThrow('active run marker is invalid')
expect(readFileSync(target, 'utf8')).toBe('{"outside":true}\n')
})
})