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