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:
KoNit-K
2026-09-16 10:21:30 -07:00
committed by Teknium
parent ca396e0f7f
commit 8ca920b52a
2 changed files with 86 additions and 4 deletions

View File

@@ -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)) {

View 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 })
}
})