fix(tui): publish welcome acknowledgement atomically
This commit is contained in:
@@ -7,8 +7,9 @@
|
||||
* @module @deepseek-ai/dsh/tui-first-run-welcome
|
||||
*/
|
||||
|
||||
import { lstat, mkdir, open, rm } from 'node:fs/promises'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { lstat, mkdir, open, rename, rm } from 'node:fs/promises'
|
||||
import { basename, dirname, join } from 'node:path'
|
||||
import type { Context } from 'cordis'
|
||||
import {
|
||||
Key,
|
||||
@@ -97,9 +98,9 @@ export async function hasTuiFirstRunWelcomeAcknowledgement(
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist one version acknowledgement as an immutable exclusive marker.
|
||||
* Concurrent launches race only on file creation: the winner syncs the marker,
|
||||
* and every loser accepts the same already-published regular file.
|
||||
* Persist one version acknowledgement by syncing a random same-directory file
|
||||
* before atomically replacing the immutable marker. Concurrent launches publish
|
||||
* the same fact, so same-value last-writer-wins replacement loses no state.
|
||||
* @param dshHome - Resolved Harness home.
|
||||
* @param version - Copy version being acknowledged.
|
||||
*/
|
||||
@@ -109,32 +110,24 @@ export async function acknowledgeTuiFirstRunWelcome(
|
||||
): Promise<void> {
|
||||
const path = tuiFirstRunWelcomeAcknowledgementPath(dshHome, version)
|
||||
const directory = dirname(path)
|
||||
const temp = join(directory, `.${basename(path)}.${randomUUID()}.tmp`)
|
||||
await mkdir(directory, { recursive: true, mode: 0o700 })
|
||||
await syncDirectory(dirname(directory))
|
||||
let handle: Awaited<ReturnType<typeof open>> | undefined
|
||||
try {
|
||||
handle = await open(path, 'wx', 0o600)
|
||||
} catch (error) {
|
||||
/* v8 ignore else -- the only expected race is another creator publishing this exact marker */
|
||||
if ((error as NodeJS.ErrnoException | null)?.code === 'EEXIST') {
|
||||
/* v8 ignore else -- EEXIST is accepted only after the winner is verified as a regular marker */
|
||||
if (await hasTuiFirstRunWelcomeAcknowledgement(dshHome, version)) return
|
||||
}
|
||||
/* v8 ignore next -- unexpected filesystem failures pass through unchanged */
|
||||
throw error
|
||||
}
|
||||
try {
|
||||
handle = await open(temp, 'wx', 0o600)
|
||||
await handle.sync()
|
||||
const created = handle
|
||||
handle = undefined
|
||||
await created.close()
|
||||
await rename(temp, path)
|
||||
await syncDirectory(directory)
|
||||
} catch (error) {
|
||||
/* v8 ignore start -- fault-injected UI coverage proves failed acknowledgements stay uncommitted and retryable */
|
||||
try {
|
||||
await handle?.close()
|
||||
} finally {
|
||||
await rm(path, { force: true })
|
||||
await rm(temp, { force: true })
|
||||
}
|
||||
throw error
|
||||
/* v8 ignore stop */
|
||||
|
||||
@@ -101,7 +101,7 @@ describe('TUI first-run welcome acknowledgement', () => {
|
||||
recursive: true,
|
||||
})
|
||||
await expect(hasTuiFirstRunWelcomeAcknowledgement(home)).rejects.toThrow('is not a file')
|
||||
await expect(acknowledgeTuiFirstRunWelcome(home)).rejects.toThrow('is not a file')
|
||||
await expect(acknowledgeTuiFirstRunWelcome(home)).rejects.toThrow()
|
||||
})
|
||||
|
||||
it('detects only explicit ASCII-only terminal environments', () => {
|
||||
|
||||
Reference in New Issue
Block a user