enh: add exit-hook support

This commit is contained in:
Ben Allfree 2023-10-20 02:53:45 -07:00
parent 89e8d96c16
commit 4085a73c25
11 changed files with 54 additions and 58 deletions

View File

@ -31,6 +31,7 @@
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"event-source-polyfill": "^1.0.31", "event-source-polyfill": "^1.0.31",
"eventsource": "^2.0.2", "eventsource": "^2.0.2",
"exit-hook": "^4.0.0",
"ftp-srv": "^4.6.2", "ftp-srv": "^4.6.2",
"get-port": "^6.1.2", "get-port": "^6.1.2",
"http-proxy": "^1.18.1", "http-proxy": "^1.18.1",

View File

@ -110,19 +110,5 @@ global.EventSource = require('eventsource')
info(`Hooking into process exit event`) 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() await (await rpcService()).initRpcs()
process.on('SIGTERM', shutdown)
process.on('SIGINT', shutdown)
process.on('SIGHUP', shutdown)
})() })()

View File

@ -7,6 +7,7 @@ import {
SSL_KEY, SSL_KEY,
} from '$constants' } from '$constants'
import { clientService, createPbClient } from '$services' import { clientService, createPbClient } from '$services'
import { exitHook } from '$util'
import { SingletonBaseConfig, mkSingleton } from '@pockethost/common' import { SingletonBaseConfig, mkSingleton } from '@pockethost/common'
import { readFileSync } from 'fs' import { readFileSync } from 'fs'
import { FtpSrv } from 'ftp-srv' import { FtpSrv } from 'ftp-srv'
@ -81,10 +82,10 @@ export const ftpService = mkSingleton((config: FtpConfig) => {
info('Ftp server started...') info('Ftp server started...')
}) })
const shutdown = () => { exitHook(() => {
info(`Closing FTP server`) info(`Closing FTP server`)
ftpServer.close() ftpServer.close()
} })
return { shutdown } return {}
}) })

View File

@ -11,7 +11,7 @@ import {
port, port,
proxyService, proxyService,
} from '$services' } from '$services'
import { mkInternalUrl, now } from '$util' import { asyncExitHook, mkInternalUrl, now } from '$util'
import { import {
assertTruthy, assertTruthy,
CLEANUP_PRIORITY_LAST, CLEANUP_PRIORITY_LAST,
@ -165,6 +165,7 @@ export const instanceService = mkSingleton(
return startRequest() return startRequest()
}, },
shutdown: async (reason) => { shutdown: async (reason) => {
dbg(`Shutting down`)
if (reason) { if (reason) {
_shutdownReason = reason _shutdownReason = reason
error(`Panic shutdown for ${reason}`) error(`Panic shutdown for ${reason}`)
@ -462,14 +463,14 @@ export const instanceService = mkSingleton(
`InstanceService`, `InstanceService`,
) )
const shutdown = async () => { asyncExitHook(async () => {
dbg(`Shutting down instance manager`) dbg(`Shutting down instance manager`)
const p = Promise.all(map(instanceApis, (api) => api.shutdown())) const p = Promise.all(map(instanceApis, (api) => api.shutdown()))
await p await p
} })
const getInstanceApiIfExistsById = (id: InstanceId) => instanceApis[id] const getInstanceApiIfExistsById = (id: InstanceId) => instanceApis[id]
return { shutdown, getInstanceApiIfExistsById } return { getInstanceApiIfExistsById }
}, },
) )

View File

@ -5,7 +5,13 @@ import {
PUBLIC_DEBUG, PUBLIC_DEBUG,
} from '$constants' } from '$constants'
import { port as getPort, InstanceLogger, updaterService } from '$services' import { port as getPort, InstanceLogger, updaterService } from '$services'
import { assert, AsyncContext, mkInternalUrl, tryFetch } from '$util' import {
assert,
AsyncContext,
asyncExitHook,
mkInternalUrl,
tryFetch,
} from '$util'
import { import {
createCleanupManager, createCleanupManager,
createTimerManager, createTimerManager,
@ -58,10 +64,10 @@ export const createPocketbaseService = async (
const { getLatestVersion, getVersion } = await updaterService() const { getLatestVersion, getVersion } = await updaterService()
const maxVersion = getLatestVersion() const maxVersion = getLatestVersion()
const cm = createCleanupManager()
const tm = createTimerManager({}) const tm = createTimerManager({})
const _spawn = async (cfg: SpawnConfig, context?: AsyncContext) => { const _spawn = async (cfg: SpawnConfig, context?: AsyncContext) => {
const cm = createCleanupManager()
const logger = (context?.logger || _serviceLogger).create('spawn') const logger = (context?.logger || _serviceLogger).create('spawn')
const { dbg, warn, error } = logger const { dbg, warn, error } = logger
const defaultPort = await (async () => { const defaultPort = await (async () => {
@ -195,6 +201,8 @@ export const createPocketbaseService = async (
iLogger.info(`${slug} closed with code ${StatusCode}`) iLogger.info(`${slug} closed with code ${StatusCode}`)
dbg({ err, data }) dbg({ err, data })
isRunning = false isRunning = false
container = undefined
unsub()
if (StatusCode > 0 || err) { if (StatusCode > 0 || err) {
iLogger.error( iLogger.error(
`Unexpected stop with code ${StatusCode} and error ${err}`, `Unexpected stop with code ${StatusCode} and error ${err}`,
@ -212,24 +220,7 @@ export const createPocketbaseService = async (
resolve(container) resolve(container)
}) })
}) })
if (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 {
iLogger.error(`Could not start container`) iLogger.error(`Could not start container`)
error(`${slug} could not start container`) error(`${slug} could not start container`)
onUnexpectedStop?.(999) onUnexpectedStop?.(999)
@ -244,6 +235,10 @@ export const createPocketbaseService = async (
logger: _serviceLogger, logger: _serviceLogger,
}) })
} }
const unsub = asyncExitHook(async () => {
dbg(`Exiting process ${slug}`)
await api.kill()
})
const api: PocketbaseProcess = { const api: PocketbaseProcess = {
url, url,
pid: () => { pid: () => {
@ -252,26 +247,33 @@ export const createPocketbaseService = async (
}, },
exited, exited,
kill: async () => { kill: async () => {
unsub()
if (!container) { if (!container) {
throw new Error( throw new Error(
`Attempt to kill a PocketBase process that was never running.`, `Attempt to kill a PocketBase process that was never running.`,
) )
} }
iLogger.info(`Stopping instance`)
await container.stop() await container.stop()
iLogger.info(`Instance stopped`)
stderr.off('data', _stdErrData)
stdout.off('data', _stdoutData)
container = undefined
await cm.shutdown()
}, },
} }
return api return api
} }
const shutdown = () => { asyncExitHook(async () => {
dbg(`Shutting down pocketbaseService`) dbg(`Shutting down pocketbaseService`)
tm.shutdown() tm.shutdown()
cm.shutdown() })
}
return { return {
spawn: _spawn, spawn: _spawn,
shutdown,
} }
} }

View File

@ -1,4 +1,5 @@
import { DAEMON_PORT, PUBLIC_EDGE_APEX_DOMAIN } from '$constants' import { DAEMON_PORT, PUBLIC_EDGE_APEX_DOMAIN } from '$constants'
import { asyncExitHook } from '$util'
import { Logger, SingletonBaseConfig, mkSingleton } from '@pockethost/common' import { Logger, SingletonBaseConfig, mkSingleton } from '@pockethost/common'
import { isFunction } from '@s-libs/micro-dash' import { isFunction } from '@s-libs/micro-dash'
import { import {
@ -10,7 +11,6 @@ import {
import { default as Server, default as httpProxy } from 'http-proxy' import { default as Server, default as httpProxy } from 'http-proxy'
import { AsyncReturnType, SetReturnType } from 'type-fest' import { AsyncReturnType, SetReturnType } from 'type-fest'
import UrlPattern from 'url-pattern' import UrlPattern from 'url-pattern'
export type ProxyServiceApi = AsyncReturnType<typeof proxyService> export type ProxyServiceApi = AsyncReturnType<typeof proxyService>
export type ProxyMiddleware = ( export type ProxyMiddleware = (
@ -88,7 +88,7 @@ export const proxyService = mkSingleton(async (config: ProxyServiceConfig) => {
info(`daemon on port ${DAEMON_PORT}`) info(`daemon on port ${DAEMON_PORT}`)
server.listen(DAEMON_PORT) server.listen(DAEMON_PORT)
const shutdown = async () => { asyncExitHook(() => {
info(`Shutting down proxy server`) info(`Shutting down proxy server`)
return new Promise<void>((resolve) => { return new Promise<void>((resolve) => {
server.close((err) => { server.close((err) => {
@ -97,7 +97,7 @@ export const proxyService = mkSingleton(async (config: ProxyServiceConfig) => {
}) })
server.closeAllConnections() server.closeAllConnections()
}) })
} })
type MiddlewareListener = SetReturnType< type MiddlewareListener = SetReturnType<
RequestListener, RequestListener,
@ -169,5 +169,5 @@ export const proxyService = mkSingleton(async (config: ProxyServiceConfig) => {
}) })
} }
return { shutdown, use } return { use }
}) })

View File

@ -118,9 +118,5 @@ export const realtimeLog = mkSingleton(async (config: RealtimeLogConfig) => {
`RealtimeLogService`, `RealtimeLogService`,
) )
return { return {}
shutdown() {
dbg(`shutdown`)
},
}
}) })

View File

@ -11,6 +11,7 @@ import {
import { isObject } from '@s-libs/micro-dash' import { isObject } from '@s-libs/micro-dash'
import Ajv, { JSONSchemaType, ValidateFunction } from 'ajv' import Ajv, { JSONSchemaType, ValidateFunction } from 'ajv'
import Bottleneck from 'bottleneck' import Bottleneck from 'bottleneck'
import exitHook from 'exit-hook'
import { default as knexFactory } from 'knex' import { default as knexFactory } from 'knex'
import pocketbaseEs, { ClientResponseError } from 'pocketbase' import pocketbaseEs, { ClientResponseError } from 'pocketbase'
import { AsyncReturnType, JsonObject } from 'type-fest' import { AsyncReturnType, JsonObject } from 'type-fest'
@ -107,9 +108,7 @@ export const rpcService = mkSingleton(async (config: RpcServiceConfig) => {
const unsub = await client.onNewRpc(run) const unsub = await client.onNewRpc(run)
const shutdown = () => { exitHook(unsub)
unsub()
}
const ajv = new Ajv() const ajv = new Ajv()
@ -133,6 +132,5 @@ export const rpcService = mkSingleton(async (config: RpcServiceConfig) => {
return { return {
registerCommand, registerCommand,
initRpcs, initRpcs,
shutdown,
} }
}) })

View File

@ -0,0 +1,5 @@
import exitHook, { asyncExitHook as _ } from 'exit-hook'
const asyncExitHook = (cb: () => Promise<void>) => _(cb, { wait: 1000 })
export { asyncExitHook, exitHook }

View File

@ -4,6 +4,7 @@ export * from './assert'
export * from './downloadAndExtract' export * from './downloadAndExtract'
export * from './ensureDirExists' export * from './ensureDirExists'
export * from './env' export * from './env'
export * from './exit'
export * from './internal' export * from './internal'
export * from './now' export * from './now'
export * from './smartFetch' export * from './smartFetch'

View File

@ -2724,6 +2724,11 @@ executable@^4.1.0:
dependencies: dependencies:
pify "^2.2.0" 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: expand-template@^2.0.3:
version "2.0.3" version "2.0.3"
resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c"