@samitouri / QOSami-HFS / commits / 6c39538c

fix: (regression 3.1.2) faulty update procedure

Massimo Melina committed May 3, 2026 at 15:10 UTC 6c39538ca87760b2d84537596b6f069b0ccd50d9
2 files changed +18 -7
src/first.ts
+14 -4
@@ -13,13 +13,14 @@ export function onProcessExit(cb: ProcessExitHandler, order=10) {
13 }
14
15 export let quitting = false
16 +export let exitCode = 0
17 // 'exit' event is handled as the last resort, but it's not compatible with async callbacks
17 -onFirstEvent(process, ['exit', 'SIGQUIT', 'SIGTERM', 'SIGINT', 'SIGHUP'], async signal => {
18 +onFirstEvent(process, ['exit', 'SIGQUIT', 'SIGTERM', 'SIGINT', 'SIGHUP', 'beforeExit'], async signal => {
19 console.log('Quitting with signal:', signal || 'unknown')
20 quitting = true
21 const byOrder = _.groupBy(Array.from(cbsOnExit), 'order') // this will be inherently ordered because keys are positive integers
21 - for (const recs of Object.values(byOrder))
22 - await Promise.allSettled(recs.map(({ cb }) => {
22 + for (const recs of Object.values(byOrder)) {
23 + const ret = Promise.allSettled(recs.map(({ cb }) => {
24 try { return cb(signal) }
25 // keep exit moving even when a synchronous cleanup fails after partially shutting down
26 catch (e) {
@@ -27,10 +28,19 @@ onFirstEvent(process, ['exit', 'SIGQUIT', 'SIGTERM', 'SIGINT', 'SIGHUP'], async
28 return Promise.reject(e)
29 }
30 }))
31 + if (signal !== 'exit') // exit is sync
32 + await ret
33 + }
34 + cbsOnExit.clear()
35 console.debug('Process exit')
31 - process.exit(0)
36 + process.exit(exitCode)
37 })
38
39 +export function quit(code=0) {
40 + exitCode = code
41 + process.emit('SIGINT')
42 +}
43 +
44 // keep calling cb in a sync fashion – returning a promise instead would break the code for argv.updating (update.ts)
45 export function onFirstEvent(emitter:EventEmitter, events: string[], cb: (...args:any[]) => void) {
46 let already = false
src/update.ts
+4 -3
@@ -11,7 +11,7 @@ import { chmod, rename, writeFile, rm } from 'fs/promises'
11 import open from 'open'
12 import { configReady, currentVersion, defineConfig, versionToScalar } from './config'
13 import { cmdEscape, runningAsWindowsService } from './util-os'
14 -import { onProcessExit } from './first'
14 +import { onProcessExit, quit } from './first'
15 import { storedMap } from './persistence'
16 import _ from 'lodash'
17 import { argv } from './argv'
@@ -64,7 +64,7 @@ const ReleaseAssetKeys = ['name', 'browser_download_url'] satisfies (keyof Relea
64 const curV = currentVersion.scalar
65 function prepareRelease(r: Release) {
66 const v = versionToScalar(r.name)
67 - return Object.assign(_.pick(r, ReleaseKeys), { // prune a bit, as it will be serialized and it has a lot of unused data
67 + return Object.assign(_.pick(r, ReleaseKeys), { // prune a bit, as it will be serialized, and it has a lot of unused data
68 versionScalar: v,
69 isNewer: v > curV, // make easy to know what's newer
70 assets: r.assets.map((a: any) => _.pick(a, ReleaseAssetKeys))
@@ -197,7 +197,8 @@ export async function update(tagOrUrl: string='') {
197 spawnSync(cmdEscape(newBin), ['--updating', binFile, '--cwd .'], { shell: true, stdio: [0,1,2] }) // sync necessary to work on Mac by double-click
198 })
199 console.log("Quitting")
200 - setTimeout(() => process.exit(100)) // non-zero, otherwise some service managers (like Shawl) won't restart the process
200 + setTimeout(() => quit(100), // non-zero, otherwise some service managers (like Shawl) won't restart the process.
201 + 200) // give time to return (and caller to complete, eg: rest api to reply)
202 }
203 catch (e: any) {
204 pluginsWatcher.unpause()