mirror of
https://gh-proxy.org/https://github.com/klinsmaya/workdsh.git
synced 2026-11-09 09:57:05 +08:00
refactor(desktop): deepen crash evidence lifecycle ownership
This commit is contained in:
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user