fix(desktop): retry transient rcedit commit failures
Windows desktop rebuilds intermittently hit rcedit's `Fatal error: Unable to commit changes` while a real-time file scanner holds the freshly packed Hermes.exe; `stampExeIdentity` called rcedit exactly once, so the stamp failed on the first lock. Retry the commit with bounded backoff before surfacing the original error. Trimmed from #112552: the after-pack.mjs injection seam and its test — the hook's swallow-and-warn is pre-existing behaviour on main and needs no change (the reporter's "Could not install the rebuilt desktop app" comes from hermes_cli/main_desktop.py::_swap_staged_desktop_app's directory rename, not from the stamp).
This commit is contained in:
@@ -42,10 +42,20 @@ import { rcedit } from 'rcedit'
|
||||
|
||||
import { isMain } from './utils.mjs'
|
||||
|
||||
const RCEDIT_COMMIT_RETRY_DELAYS_MS = [100, 300]
|
||||
|
||||
function wait(delay) {
|
||||
return new Promise(resolve => setTimeout(resolve, delay))
|
||||
}
|
||||
|
||||
// Stamp the Hermes icon + identity onto `exe`. Resolves on success, throws on
|
||||
// failure. `desktopRoot` defaults to this script's package root so the icon and
|
||||
// the rcedit dependency resolve regardless of cwd.
|
||||
async function stampExeIdentity(exe, desktopRoot = resolve(import.meta.dirname, '..')) {
|
||||
async function stampExeIdentity(
|
||||
exe,
|
||||
desktopRoot = resolve(import.meta.dirname, '..'),
|
||||
{ rcedit: runRcedit = rcedit, sleep = wait } = {}
|
||||
) {
|
||||
if (!exe || !existsSync(exe)) {
|
||||
throw new Error(`target exe not found: ${exe}`)
|
||||
}
|
||||
@@ -59,7 +69,7 @@ async function stampExeIdentity(exe, desktopRoot = resolve(import.meta.dirname,
|
||||
console.log(`[set-exe-identity] stamping ${exe}`)
|
||||
console.log(`[set-exe-identity] icon: ${icon}`)
|
||||
|
||||
await rcedit(exe, {
|
||||
const options = {
|
||||
icon,
|
||||
'version-string': {
|
||||
ProductName: 'Hermes',
|
||||
@@ -67,12 +77,26 @@ async function stampExeIdentity(exe, desktopRoot = resolve(import.meta.dirname,
|
||||
CompanyName: 'Nous Research',
|
||||
LegalCopyright: 'Copyright (c) 2026 Nous Research'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
for (let attempt = 0; ; attempt += 1) {
|
||||
try {
|
||||
await runRcedit(exe, options)
|
||||
break
|
||||
} catch (err) {
|
||||
const delay = RCEDIT_COMMIT_RETRY_DELAYS_MS[attempt]
|
||||
if (!/unable to commit changes/i.test(err?.message) || delay === undefined) {
|
||||
throw err
|
||||
}
|
||||
console.warn(`[set-exe-identity] rcedit commit failed; retrying in ${delay}ms (${err.message})`)
|
||||
await sleep(delay)
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[set-exe-identity] done — Hermes icon + identity stamped')
|
||||
}
|
||||
|
||||
export { stampExeIdentity }
|
||||
export { RCEDIT_COMMIT_RETRY_DELAYS_MS, stampExeIdentity }
|
||||
|
||||
// CLI entry point: `node scripts/set-exe-identity.mjs <exe>`.
|
||||
if (isMain(import.meta.url)) {
|
||||
|
||||
58
apps/desktop/scripts/set-exe-identity.test.mjs
Normal file
58
apps/desktop/scripts/set-exe-identity.test.mjs
Normal file
@@ -0,0 +1,58 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import { test } from 'vitest'
|
||||
|
||||
import { stampExeIdentity } from './set-exe-identity.mjs'
|
||||
|
||||
function makeDesktopRoot() {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'hermes-exe-identity-'))
|
||||
fs.mkdirSync(path.join(root, 'assets'))
|
||||
fs.writeFileSync(path.join(root, 'assets', 'icon.ico'), 'icon')
|
||||
const exe = path.join(root, 'Hermes.exe')
|
||||
fs.writeFileSync(exe, 'exe')
|
||||
return { exe, root }
|
||||
}
|
||||
|
||||
test('retries transient rcedit commit failures with bounded backoff', async () => {
|
||||
const { exe, root } = makeDesktopRoot()
|
||||
const delays = []
|
||||
let attempts = 0
|
||||
|
||||
try {
|
||||
await stampExeIdentity(exe, root, {
|
||||
rcedit: async () => {
|
||||
attempts += 1
|
||||
if (attempts < 3) throw new Error('Unable to commit changes')
|
||||
},
|
||||
sleep: async delay => delays.push(delay)
|
||||
})
|
||||
|
||||
assert.equal(attempts, 3)
|
||||
assert.deepEqual(delays, [100, 300])
|
||||
} finally {
|
||||
fs.rmSync(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
test('stops retrying after the bounded rcedit retry budget is exhausted', async () => {
|
||||
const { exe, root } = makeDesktopRoot()
|
||||
let attempts = 0
|
||||
|
||||
try {
|
||||
await assert.rejects(
|
||||
stampExeIdentity(exe, root, {
|
||||
rcedit: async () => {
|
||||
attempts += 1
|
||||
throw new Error('Unable to commit changes')
|
||||
},
|
||||
sleep: async () => {}
|
||||
}),
|
||||
/Unable to commit changes/
|
||||
)
|
||||
assert.equal(attempts, 3)
|
||||
} finally {
|
||||
fs.rmSync(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user