@samitouri / QOSami-HFS-2 / commits / f4857c8e
Fork 0

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())))