diff --git a/apps/desktop/scripts/set-exe-identity.mjs b/apps/desktop/scripts/set-exe-identity.mjs index 4e19999c75..a1e67fab5d 100644 --- a/apps/desktop/scripts/set-exe-identity.mjs +++ b/apps/desktop/scripts/set-exe-identity.mjs @@ -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 `. if (isMain(import.meta.url)) { diff --git a/apps/desktop/scripts/set-exe-identity.test.mjs b/apps/desktop/scripts/set-exe-identity.test.mjs new file mode 100644 index 0000000000..0f02e77bf9 --- /dev/null +++ b/apps/desktop/scripts/set-exe-identity.test.mjs @@ -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 }) + } +})