fix(desktop): exe identity stamping stops retrying when the rcedit binary itself is missing

apps/desktop/scripts/set-exe-identity.mjs::stampExeIdentity retried every rcedit
rejection with 0.5/1/2 s backoff. A failure to SPAWN rcedit (bin/rcedit-x64.exe
missing or not executable) is permanent, so the only effect was a 3.5 s delay
before after-pack.mjs's warning.

The npm rcedit wrapper (@malept/cross-spawn-promise) reports a spawn failure as
CrossSpawnError with the errno on `originalError.code`; a non-zero rcedit exit
("Unable to commit changes") is an ExitCodeError with a numeric `code`. Skip the
retry loop when `originalError.code ?? code` is ENOENT/EACCES and rethrow as-is.

Probe: stub rcedit rejecting with that shape -> before 4 attempts, sleeps
[500,1000,2000]; after 1 attempt, no sleeps. Transient-lock retry unchanged.

Part of #112544 (optional review atom).
This commit is contained in:
teknium1
2026-09-16 22:10:12 -07:00
committed by Teknium
parent bbdb279a68
commit 2bbff2a405
2 changed files with 42 additions and 5 deletions

View File

@@ -44,12 +44,20 @@ import { isMain } from './utils.mjs'
// A real-time file scanner (AV/EDR) holds a short exclusive handle on a freshly
// written exe; rcedit's resource commit then fails with "Unable to commit
// changes" and succeeds seconds later on identical input. Retrying on ANY
// rcedit failure keeps the shape simple — a permanent failure costs 3.5 s more
// before after-pack.mjs swallows it. Delays sized to the field report: the
// lock was still held 5 s after a first attempt in some runs.
// changes" and succeeds seconds later on identical input. Delays sized to the
// field report: the lock was still held 5 s after a first attempt in some runs.
const RCEDIT_COMMIT_RETRY_DELAYS_MS = [500, 1000, 2000]
// A failure to spawn the rcedit binary itself (missing or not executable) is
// permanent; waiting 3.5 s on it only delays after-pack.mjs's warning. The npm
// rcedit wrapper surfaces the spawn error as `originalError` on its rejection,
// while a non-zero rcedit exit carries a numeric `code`.
const RCEDIT_PERMANENT_SPAWN_CODES = new Set(['ENOENT', 'EACCES'])
function isPermanentRceditFailure(err) {
return RCEDIT_PERMANENT_SPAWN_CODES.has(err?.originalError?.code ?? err?.code)
}
function wait(delay) {
return new Promise(resolve => setTimeout(resolve, delay))
}
@@ -91,7 +99,7 @@ async function stampExeIdentity(
break
} catch (err) {
const delay = RCEDIT_COMMIT_RETRY_DELAYS_MS[attempt]
if (delay === undefined) {
if (delay === undefined || isPermanentRceditFailure(err)) {
throw err
}
console.warn(`[set-exe-identity] rcedit failed; retrying in ${delay}ms (${err.message})`)

View File

@@ -56,3 +56,32 @@ test('stops retrying after the bounded rcedit retry budget is exhausted', async
fs.rmSync(root, { recursive: true, force: true })
}
})
test('does not retry when the rcedit binary itself cannot be spawned (ENOENT/EACCES)', async () => {
// #112544 follow-up: the npm rcedit wrapper reports a spawn failure via
// `originalError`; that toolchain breakage is permanent, so the 3.5 s
// transient-lock retry budget must not be spent on it.
const { exe, root } = makeDesktopRoot()
let attempts = 0
const delays = []
const spawnFailure = Object.assign(new Error('Error executing command (rcedit-x64.exe):\nspawn rcedit-x64.exe ENOENT'), {
originalError: Object.assign(new Error('spawn rcedit-x64.exe ENOENT'), { code: 'ENOENT' })
})
try {
await assert.rejects(
stampExeIdentity(exe, root, {
rcedit: async () => {
attempts += 1
throw spawnFailure
},
sleep: async delay => delays.push(delay)
}),
/ENOENT/
)
assert.equal(attempts, 1)
assert.deepEqual(delays, [])
} finally {
fs.rmSync(root, { recursive: true, force: true })
}
})