From 4085a73c25d53d0b6bdbe493f7dd7a4f8fbf04fc Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Fri, 20 Oct 2023 02:53:45 -0700 Subject: [PATCH] enh: add exit-hook support --- packages/daemon/package.json | 1 + packages/daemon/src/server.ts | 14 ------ .../daemon/src/services/FtpService/index.ts | 7 +-- .../src/services/InstanceService/index.ts | 9 ++-- .../src/services/PocketBaseService/index.ts | 50 ++++++++++--------- packages/daemon/src/services/ProxyService.ts | 8 +-- packages/daemon/src/services/RealtimeLog.ts | 6 +-- .../daemon/src/services/RpcService/index.ts | 6 +-- packages/daemon/src/util/exit.ts | 5 ++ packages/daemon/src/util/index.ts | 1 + yarn.lock | 5 ++ 11 files changed, 54 insertions(+), 58 deletions(-) create mode 100644 packages/daemon/src/util/exit.ts diff --git a/packages/daemon/package.json b/packages/daemon/package.json index ecb4d1bb..129609ab 100644 --- a/packages/daemon/package.json +++ b/packages/daemon/package.json @@ -31,6 +31,7 @@ "dotenv": "^16.3.1", "event-source-polyfill": "^1.0.31", "eventsource": "^2.0.2", + "exit-hook": "^4.0.0", "ftp-srv": "^4.6.2", "get-port": "^6.1.2", "http-proxy": "^1.18.1", diff --git a/packages/daemon/src/server.ts b/packages/daemon/src/server.ts index 1b4ce628..6be7e4a3 100644 --- a/packages/daemon/src/server.ts +++ b/packages/daemon/src/server.ts @@ -110,19 +110,5 @@ global.EventSource = require('eventsource') info(`Hooking into process exit event`) - const shutdown = async (signal: NodeJS.Signals) => { - info(`Got signal ${signal}`) - info(`Shutting down`) - ftpService().shutdown() - ;(await realtimeLog()).shutdown() - ;(await proxyService()).shutdown() - ;(await instanceService()).shutdown() - ;(await rpcService()).shutdown() - pbService.shutdown() - } - await (await rpcService()).initRpcs() - process.on('SIGTERM', shutdown) - process.on('SIGINT', shutdown) - process.on('SIGHUP', shutdown) })() diff --git a/packages/daemon/src/services/FtpService/index.ts b/packages/daemon/src/services/FtpService/index.ts index b18bcc4d..be55b868 100644 --- a/packages/daemon/src/services/FtpService/index.ts +++ b/packages/daemon/src/services/FtpService/index.ts @@ -7,6 +7,7 @@ import { SSL_KEY, } from '$constants' import { clientService, createPbClient } from '$services' +import { exitHook } from '$util' import { SingletonBaseConfig, mkSingleton } from '@pockethost/common' import { readFileSync } from 'fs' import { FtpSrv } from 'ftp-srv' @@ -81,10 +82,10 @@ export const ftpService = mkSingleton((config: FtpConfig) => { info('Ftp server started...') }) - const shutdown = () => { + exitHook(() => { info(`Closing FTP server`) ftpServer.close() - } + }) - return { shutdown } + return {} }) diff --git a/packages/daemon/src/services/InstanceService/index.ts b/packages/daemon/src/services/InstanceService/index.ts index c0443b4d..68e83660 100644 --- a/packages/daemon/src/services/InstanceService/index.ts +++ b/packages/daemon/src/services/InstanceService/index.ts @@ -11,7 +11,7 @@ import { port, proxyService, } from '$services' -import { mkInternalUrl, now } from '$util' +import { asyncExitHook, mkInternalUrl, now } from '$util' import { assertTruthy, CLEANUP_PRIORITY_LAST, @@ -165,6 +165,7 @@ export const instanceService = mkSingleton( return startRequest() }, shutdown: async (reason) => { + dbg(`Shutting down`) if (reason) { _shutdownReason = reason error(`Panic shutdown for ${reason}`) @@ -462,14 +463,14 @@ export const instanceService = mkSingleton( `InstanceService`, ) - const shutdown = async () => { + asyncExitHook(async () => { dbg(`Shutting down instance manager`) const p = Promise.all(map(instanceApis, (api) => api.shutdown())) await p - } + }) const getInstanceApiIfExistsById = (id: InstanceId) => instanceApis[id] - return { shutdown, getInstanceApiIfExistsById } + return { getInstanceApiIfExistsById } }, ) diff --git a/packages/daemon/src/services/PocketBaseService/index.ts b/packages/daemon/src/services/PocketBaseService/index.ts index d53edb18..dcdebbe7 100644 --- a/packages/daemon/src/services/PocketBaseService/index.ts +++ b/packages/daemon/src/services/PocketBaseService/index.ts @@ -5,7 +5,13 @@ import { PUBLIC_DEBUG, } from '$constants' import { port as getPort, InstanceLogger, updaterService } from '$services' -import { assert, AsyncContext, mkInternalUrl, tryFetch } from '$util' +import { + assert, + AsyncContext, + asyncExitHook, + mkInternalUrl, + tryFetch, +} from '$util' import { createCleanupManager, createTimerManager, @@ -58,10 +64,10 @@ export const createPocketbaseService = async ( const { getLatestVersion, getVersion } = await updaterService() const maxVersion = getLatestVersion() - const cm = createCleanupManager() const tm = createTimerManager({}) const _spawn = async (cfg: SpawnConfig, context?: AsyncContext) => { + const cm = createCleanupManager() const logger = (context?.logger || _serviceLogger).create('spawn') const { dbg, warn, error } = logger const defaultPort = await (async () => { @@ -195,6 +201,8 @@ export const createPocketbaseService = async ( iLogger.info(`${slug} closed with code ${StatusCode}`) dbg({ err, data }) isRunning = false + container = undefined + unsub() if (StatusCode > 0 || err) { iLogger.error( `Unexpected stop with code ${StatusCode} and error ${err}`, @@ -212,24 +220,7 @@ export const createPocketbaseService = async ( resolve(container) }) }) - if (container) { - cm.add(async () => { - dbg(`Stopping ${slug} for cleanup`) - iLogger.info(`Stopping instance`) - await container - ?.stop() - .then(() => { - iLogger.info(`Instance stopped`) - }) - .catch((err) => { - iLogger.error(`Error stopping instance`) - warn(`Possible error stopping container: ${err}`) - }) - - stderr.off('data', _stdErrData) - stdout.off('data', _stdoutData) - }) - } else { + if (!container) { iLogger.error(`Could not start container`) error(`${slug} could not start container`) onUnexpectedStop?.(999) @@ -244,6 +235,10 @@ export const createPocketbaseService = async ( logger: _serviceLogger, }) } + const unsub = asyncExitHook(async () => { + dbg(`Exiting process ${slug}`) + await api.kill() + }) const api: PocketbaseProcess = { url, pid: () => { @@ -252,26 +247,33 @@ export const createPocketbaseService = async ( }, exited, kill: async () => { + unsub() if (!container) { throw new Error( `Attempt to kill a PocketBase process that was never running.`, ) } + iLogger.info(`Stopping instance`) await container.stop() + iLogger.info(`Instance stopped`) + stderr.off('data', _stdErrData) + stdout.off('data', _stdoutData) + + container = undefined + await cm.shutdown() }, } + return api } - const shutdown = () => { + asyncExitHook(async () => { dbg(`Shutting down pocketbaseService`) tm.shutdown() - cm.shutdown() - } + }) return { spawn: _spawn, - shutdown, } } diff --git a/packages/daemon/src/services/ProxyService.ts b/packages/daemon/src/services/ProxyService.ts index 2047a92e..479f3745 100644 --- a/packages/daemon/src/services/ProxyService.ts +++ b/packages/daemon/src/services/ProxyService.ts @@ -1,4 +1,5 @@ import { DAEMON_PORT, PUBLIC_EDGE_APEX_DOMAIN } from '$constants' +import { asyncExitHook } from '$util' import { Logger, SingletonBaseConfig, mkSingleton } from '@pockethost/common' import { isFunction } from '@s-libs/micro-dash' import { @@ -10,7 +11,6 @@ import { import { default as Server, default as httpProxy } from 'http-proxy' import { AsyncReturnType, SetReturnType } from 'type-fest' import UrlPattern from 'url-pattern' - export type ProxyServiceApi = AsyncReturnType export type ProxyMiddleware = ( @@ -88,7 +88,7 @@ export const proxyService = mkSingleton(async (config: ProxyServiceConfig) => { info(`daemon on port ${DAEMON_PORT}`) server.listen(DAEMON_PORT) - const shutdown = async () => { + asyncExitHook(() => { info(`Shutting down proxy server`) return new Promise((resolve) => { server.close((err) => { @@ -97,7 +97,7 @@ export const proxyService = mkSingleton(async (config: ProxyServiceConfig) => { }) server.closeAllConnections() }) - } + }) type MiddlewareListener = SetReturnType< RequestListener, @@ -169,5 +169,5 @@ export const proxyService = mkSingleton(async (config: ProxyServiceConfig) => { }) } - return { shutdown, use } + return { use } }) diff --git a/packages/daemon/src/services/RealtimeLog.ts b/packages/daemon/src/services/RealtimeLog.ts index d1365752..65e89dfa 100644 --- a/packages/daemon/src/services/RealtimeLog.ts +++ b/packages/daemon/src/services/RealtimeLog.ts @@ -118,9 +118,5 @@ export const realtimeLog = mkSingleton(async (config: RealtimeLogConfig) => { `RealtimeLogService`, ) - return { - shutdown() { - dbg(`shutdown`) - }, - } + return {} }) diff --git a/packages/daemon/src/services/RpcService/index.ts b/packages/daemon/src/services/RpcService/index.ts index c27f253c..acf3c54d 100644 --- a/packages/daemon/src/services/RpcService/index.ts +++ b/packages/daemon/src/services/RpcService/index.ts @@ -11,6 +11,7 @@ import { import { isObject } from '@s-libs/micro-dash' import Ajv, { JSONSchemaType, ValidateFunction } from 'ajv' import Bottleneck from 'bottleneck' +import exitHook from 'exit-hook' import { default as knexFactory } from 'knex' import pocketbaseEs, { ClientResponseError } from 'pocketbase' import { AsyncReturnType, JsonObject } from 'type-fest' @@ -107,9 +108,7 @@ export const rpcService = mkSingleton(async (config: RpcServiceConfig) => { const unsub = await client.onNewRpc(run) - const shutdown = () => { - unsub() - } + exitHook(unsub) const ajv = new Ajv() @@ -133,6 +132,5 @@ export const rpcService = mkSingleton(async (config: RpcServiceConfig) => { return { registerCommand, initRpcs, - shutdown, } }) diff --git a/packages/daemon/src/util/exit.ts b/packages/daemon/src/util/exit.ts new file mode 100644 index 00000000..2ff4bba1 --- /dev/null +++ b/packages/daemon/src/util/exit.ts @@ -0,0 +1,5 @@ +import exitHook, { asyncExitHook as _ } from 'exit-hook' + +const asyncExitHook = (cb: () => Promise) => _(cb, { wait: 1000 }) + +export { asyncExitHook, exitHook } diff --git a/packages/daemon/src/util/index.ts b/packages/daemon/src/util/index.ts index 4e8ae049..490ca261 100644 --- a/packages/daemon/src/util/index.ts +++ b/packages/daemon/src/util/index.ts @@ -4,6 +4,7 @@ export * from './assert' export * from './downloadAndExtract' export * from './ensureDirExists' export * from './env' +export * from './exit' export * from './internal' export * from './now' export * from './smartFetch' diff --git a/yarn.lock b/yarn.lock index 64200c1b..48c609cf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2724,6 +2724,11 @@ executable@^4.1.0: dependencies: pify "^2.2.0" +exit-hook@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-4.0.0.tgz#c1e16ebd03d3166f837b1502dac755bb5c460d58" + integrity sha512-Fqs7ChZm72y40wKjOFXBKg7nJZvQJmewP5/7LtePDdnah/+FH9Hp5sgMujSCMPXlxOAW2//1jrW9pnsY7o20vQ== + expand-template@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c"