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
+}