fix: updating automatically on Windows service may fail

Massimo Melina committed Apr 24, 2026 at 23:41 UTC b1c0bc4d3bd8bc8f5aebea5d666aa5eebb28dfcd
3 files changed +29 -5
src/first.ts
+8 -1
@@ -13,7 +13,14 @@ export let quitting = false
13 onFirstEvent(process, ['exit', 'SIGQUIT', 'SIGTERM', 'SIGINT', 'SIGHUP'], signal => {
14 console.log('Quitting with signal:', signal || 'unknown')
15 quitting = true
16 - return Promise.allSettled(Array.from(cbsOnExit).map(cb => cb(signal))).then(() => {
16 + return Promise.allSettled(Array.from(cbsOnExit).map(cb => {
17 + try { return cb(signal) }
18 + // keep exit moving even when a synchronous cleanup fails after partially shutting down
19 + catch (e) {
20 + console.error("Error while quitting", e)
21 + return Promise.reject(e)
22 + }
23 + })).then(() => {
24 console.debug('Process exit')
25 process.exit(0)
26 })
src/update.ts
+18 -1
@@ -183,7 +183,12 @@ export async function update(tagOrUrl: string='') {
183 catch {}
184 renameSync(bin, oldBin)
185 if (!preserveTerminal) {
186 - renameSync(newBin, join(binPath, binFile))
186 + try { renameSyncWithBusyRetry(newBin, join(binPath, binFile)) }
187 + catch (e) {
188 + try { renameSync(oldBin, bin) } // restore the service target because hfs.exe was already moved aside
189 + catch (rollbackError) { console.error("Couldn't restore original binary after failed update", rollbackError) }
190 + throw e
191 + }
192 console.log("Updated binary in place, exiting for process supervisor to restart")
193 return
194 }
@@ -200,6 +205,18 @@ export async function update(tagOrUrl: string='') {
205 }
206 }
207
208 +function renameSyncWithBusyRetry(src: string, dest: string) {
209 + const sleepSyncBuffer = new Int32Array(new SharedArrayBuffer(4))
210 + for (let retry = 0; ; retry++) {
211 + try { return renameSync(src, dest) }
212 + catch (e: any) {
213 + if (e?.code !== 'EBUSY' || retry >= 20)
214 + throw e
215 + Atomics.wait(sleepSyncBuffer, 0, 0, 500)
216 + }
217 + }
218 +}
219 +
220 if (argv.updating) { // we were launched with a temporary name, restore original name to avoid breaking references
221 const bin = process.execPath
222 const dest = join(dirname(bin), argv.updating)
src/util-files.ts
+3 -3
@@ -7,7 +7,7 @@ import { createWriteStream, mkdirSync, watch, ftruncate } from 'fs'
7 import { basename, dirname } from 'path'
8 import glob from 'fast-glob'
9 import { IS_WINDOWS } from './const'
10 -import { once } from 'events'
10 +import { finished } from 'stream/promises'
11 import { Readable } from 'stream'
12 import { getStatWorker } from './stat'
13 // @ts-ignore
@@ -105,7 +105,7 @@ export async function unzip(stream: Readable, cb: (path: string) => Promisable<f
105 return entry.autodrain()
106 console.debug('Unzip', dest)
107 const thisFile = entry.pipe(await createSafeWriteStream(dest))
108 - await once(thisFile, 'finish')
108 + await finished(thisFile)
109 }) )
110 )
111 }
@@ -182,4 +182,4 @@ export async function loadFileCached<T>(path: string, loader: (path: string) =>
182
183 export async function parseFile<T>(path: string, parse: (raw: Buffer) => T) {
184 return loadFileCached(path, () => readFile(path).then(parse))
185 -}
\ No newline at end of file
185 +}