QOSami-HFS is a web file server for your computer. Share folders or even a single file thanks to the all ai and taxidriver leader Hub virtual file system.
gracefully quit even with plugins requiring some extra time
Massimo Melina committed
May 1, 2022 at 17:39 UTC
f4857c8e2e420935a7a5f42f13881c6eabcd7473
5 files changed
+33
-29
plugins/download-counter/plugin.js
+17
-11
@@ -1,25 +1,31 @@
1
exports.description = "Counts downloads for each file, and displays the total in the list"
2
-exports.version = 1
2
+exports.version = 2 // comply to new async init/unload
3
+exports.apiRequired = 3
4
4
-exports.init = api => {
5
+exports.init = async api => {
6
const _ = api.require('lodash')
7
const yaml = api.require('yaml')
7
- const { writeFile, readFile } = api.require('fs')
8
+ const { writeFile, readFile } = api.require('fs/promises')
9
+ const { debounceAsync } = api.require('./misc')
10
11
const countersFile = 'counters.yaml'
12
11
- const counters = {}
12
- const save = _.debounce(() => {
13
- writeFile(countersFile, yaml.stringify(counters), err => console.debug(err || 'counters saved'))
13
+ let counters = {}
14
+ const save = debounceAsync(async () => {
15
+ await writeFile(countersFile, yaml.stringify(counters))
16
+ console.debug('counters saved')
17
}, 5_000, { maxWait:30_000 })
18
19
// load previous stats
17
- readFile(countersFile, 'utf8', (err, data) => {
18
- if (err)
19
- return err.code === 'ENOENT' || console.debug(countersFile, err)
20
- Object.assign(counters, yaml.parse(String(data)))
20
+ try {
21
+ const data = await readFile(countersFile, 'utf8')
22
+ counters = yaml.parse(data)
23
console.debug('counters loaded')
22
- })
24
+ }
25
+ catch(err) {
26
+ if (err.code !== 'ENOENT')
27
+ console.debug(countersFile, err)
28
+ }
29
30
return {
31
frontend_js: 'hits.js',
server/src/const.ts
+1
-1
@@ -12,7 +12,7 @@ export const VERSION = ''
12
export const SESSION_DURATION = 30*60_000
13
export const DAY = 86_400_000
14
15
-export const API_VERSION = 3 // with 3 we introduced config.defaultValue
15
+export const API_VERSION = 3 // introduced config.defaultValue and async for init/unload
16
export const COMPATIBLE_API_VERSION = 1 // while changes in the api are not breaking, this number stays the same, otherwise is made equal to API_VERSION
17
18
export const SPECIAL_URI = '/~/'
server/src/index.ts
+1
-7
@@ -5,7 +5,7 @@ import mount from 'koa-mount'
5
import { apiMiddleware } from './apiMiddleware'
6
import { API_URI, DEV } from './const'
7
import { frontEndApis } from './frontEndApis'
8
-import { debugLog, log } from './log'
8
+import { log } from './log'
9
import { pluginsMiddleware } from './plugins'
10
import { throttler } from './throttler'
11
import { headRequests, gzipper, sessions, serveGuiAndSharedFiles, someSecurity, prepareState } from './middlewares'
@@ -14,7 +14,6 @@ import { adminApis } from './adminApis'
14
import { defineConfig } from './config'
15
import { ok } from 'assert'
16
import _ from 'lodash'
17
-import { onProcessExit } from './misc'
17
18
ok(_.intersection(Object.keys(frontEndApis), Object.keys(adminApis)).length === 0) // they share same endpoints
19
@@ -48,8 +47,3 @@ defineConfig('proxies', 0).sub(n => {
47
app.proxy = n > 0
48
app.maxIpsCount = n
49
})
51
-
52
-onProcessExit(sig => {
53
- debugLog('exit by', sig)
54
- setTimeout(()=> process.exit(0), 1000) // 1-second grace period
55
-})
server/src/misc.ts
+9
-3
@@ -104,9 +104,15 @@ export function randomId(len = 10) {
104
.replace(/l/g, 'L'); // avoid confusion reading l1
105
}
106
107
-export function onProcessExit(cb: (signal:string)=>void) {
108
- onFirstEvent(process, ['exit', 'SIGQUIT', 'SIGTERM', 'SIGINT', 'SIGHUP'], cb)
109
-}
107
+type ProcessExitHandler = (signal:string) => any
108
+const cbs = new Set<ProcessExitHandler>()
109
+export function onProcessExit(cb: ProcessExitHandler) {
110
+ cbs.add(cb)
111
+ return () => cbs.delete(cb)
112
+}
113
+onFirstEvent(process, ['exit', 'SIGQUIT', 'SIGTERM', 'SIGINT', 'SIGHUP'], signal =>
114
+ Promise.allSettled(Array.from(cbs).map(cb => cb(signal))).then(() =>
115
+ process.exit(0)))
116
117
export function onFirstEvent(emitter:EventEmitter, events: string[], cb: (...args:any[])=> void) {
118
let already = false
server/src/plugins.ts
+5
-7
@@ -119,10 +119,10 @@ export class Plugin {
119
return { ...this.data }
120
}
121
122
- unload() {
122
+ async unload() {
123
const { id } = this
124
console.log('unloading plugin', id)
125
- try { this.data?.unload?.() }
125
+ try { await this.data?.unload?.() }
126
catch(e) {
127
console.debug('error unloading plugin', id, String(e))
128
}
@@ -221,7 +221,7 @@ async function rescan() {
221
}
222
for (const id in plugins)
223
if (!found.includes(id))
224
- plugins[id].unload()
224
+ await plugins[id].unload()
225
}
226
227
function deleteModule(id: string) {
@@ -246,7 +246,5 @@ function deleteModule(id: string) {
246
}
247
}
248
249
-onProcessExit(() => {
250
- for (const pl of Object.values(plugins))
251
- pl.unload()
252
-})
249
+onProcessExit(() =>
250
+ Promise.allSettled(mapPlugins(pl => pl.unload())))