UNPKG

@freemework/hosting

Version:

Hosting library of the Freemework Project.

320 lines (292 loc) 9.29 kB
import { FException, FConfiguration, FConfigurationChain, FExecutionContext, FCancellationTokenSource, FCancellationTokenSourceManual, FLogger, FCancellationException, FCancellationExecutionContext, FDisposable, } from "@freemework/common"; import { FConfigurationDirectory, FConfigurationEnv, FConfigurationProperties, FConfigurationToml, } from "../configuration/index.js"; import { FLauncherException } from "./f_launcher_exception.js"; import { FLauncherRestartRequiredException } from "./f_launcher_restart_required_exception.js"; export function flauncher(runtimeFactory: FConfiglessRuntimeFactory): void; /** * Launch an application using `defaultConfigurationLoader` * @param configurationParser User's function that provides configuration parser * @param runtimeFactory User's function that compose and start runtime */ export function flauncher<TConfiguration>( configurationParser: ConfigurationParser<TConfiguration>, runtimeFactory: FLauncherRuntimeFactory<TConfiguration> ): void; /** * Launch an application * @param configurationLoader User's function that provides configuration loader * @param configurationParser User's function that provides configuration parser * @param runtimeFactory User's function that compose and start runtime */ export function flauncher<TConfiguration>( configurationLoader: RawConfigurationLoader, configurationParser: ConfigurationParser<TConfiguration>, runtimeFactory: FLauncherRuntimeFactory<TConfiguration> ): void; export function flauncher<TConfiguration>(...args: Array<any>): void { const log: FLogger = FLogger.create("FLauncher"); const cancellationTokenSource: FCancellationTokenSource = new FCancellationTokenSourceManual(); const executionContext: FExecutionContext = new FCancellationExecutionContext( FExecutionContext.Empty, cancellationTokenSource.token ); async function run() { process.on("unhandledRejection", (e) => { const ex: FException = FException.wrapIfNeeded(e); log.debug(executionContext, "Unhandled Rejection", ex); log.fatal( executionContext, `Unhandled Rejection. ${ex.constructor.name}: ${ex.message}` ); fireShutdownHooks().finally(function () { process.exit(255); }); }); let destroyRequestCount = 0; const shutdownSignals: Array<NodeJS.Signals> = ["SIGTERM", "SIGINT"]; let runtimeStuff: | { readonly loader: RawConfigurationLoader; readonly parser: ConfigurationParser<TConfiguration>; readonly runtimeFactory: FLauncherRuntimeFactory<TConfiguration>; } | { readonly parser: null; readonly runtimeFactory: FConfiglessRuntimeFactory; }; if ( args.length === 3 && typeof args[0] === "function" && typeof args[1] === "function" ) { runtimeStuff = Object.freeze({ loader: args[0], parser: args[1], runtimeFactory: args[2], }); } else if ( args.length === 2 && typeof args[0] === "function" && typeof args[1] === "function" ) { runtimeStuff = Object.freeze({ loader: defaultConfigurationLoader, parser: args[0], runtimeFactory: args[1], }); } else if (args.length === 1 && typeof args[0] === "function") { runtimeStuff = Object.freeze({ parser: null, runtimeFactory: args[0], }); } else { throw new Error("Wrong arguments"); } let runtime: FDisposable; if (runtimeStuff.parser === null) { runtime = await runtimeStuff.runtimeFactory(executionContext); } else { const rawConfiguration: FConfiguration = await runtimeStuff.loader( executionContext ); const parsedConfiguration: TConfiguration = runtimeStuff.parser(rawConfiguration); runtime = await runtimeStuff.runtimeFactory( executionContext, parsedConfiguration ); } shutdownSignals.forEach((signal: NodeJS.Signals) => process.on(signal, () => gracefulShutdown(signal)) ); async function gracefulShutdown(signal: string) { if (destroyRequestCount++ === 0) { cancellationTokenSource.cancel(); if (log.isInfoEnabled) { log.info(executionContext, `Interrupt signal received: ${signal}`); } try { await runtime.dispose(); } catch (e) { const ex: FException = FException.wrapIfNeeded(e); const msg = "Unexpected exception from the dispose() method of your runtime object."; log.debug(executionContext, msg, ex); log.error(executionContext, () => `${msg} ${ex.message}`); } await fireShutdownHooks(); process.exit(0); } else { if (log.isInfoEnabled) { log.info( executionContext, `Interrupt signal (${destroyRequestCount}) received: ${signal}` ); } } } } log.info(executionContext, "Starting application..."); run() .then(() => { if (log.isInfoEnabled) { log.info( executionContext, `Application was started. Process ID: ${process.pid}` ); } }) .catch((e) => { const ex: FException = FException.wrapIfNeeded(e); if (ex instanceof FCancellationException) { log.warn( executionContext, "Runtime initialization was cancelled by user" ); fireShutdownHooks().finally(function () { process.exit(0); }); } if (log.isFatalEnabled) { if (e instanceof FLauncherException) { log.fatal( executionContext, `Runtime initialization failed with ${e.constructor.name}: ${e.message}` ); } else { log.fatal( executionContext, `Runtime initialization failed with error: ${ex.message}` ); log.debug(executionContext, ex.message, ex); } } const exitCode: number = ex instanceof FLauncherRestartRequiredException ? ex.exitCode : 127; if (process.env["NODE_ENV"] === "development") { setTimeout(() => { fireShutdownHooks().finally(function () { process.exit(exitCode); }); }, 1000); } else { fireShutdownHooks().finally(function () { process.exit(exitCode); }); } }); } export type RawConfigurationLoader = ( executionContext: FExecutionContext ) => Promise<FConfiguration>; export type ConfigurationParser<TConfiguration> = ( rawConfiguration: FConfiguration ) => TConfiguration; export type FLauncherRuntimeFactory<TConfiguration> = ( executionContext: FExecutionContext, configuration: TConfiguration ) => Promise<FDisposable>; export type FConfiglessRuntimeFactory = ( executionContext: FExecutionContext ) => Promise<FDisposable>; export async function defaultConfigurationLoader( _executionContext: FExecutionContext ): Promise<FConfiguration> { const chainItems: Array<FConfiguration> = []; for (const arg of process.argv) { if (arg.startsWith(defaultConfigurationLoader.CONFIG_FILE_ARG)) { const configFile = arg.substring( defaultConfigurationLoader.CONFIG_FILE_ARG.length ); const fileConf: FConfiguration = await FConfigurationProperties.fromFile( configFile ); chainItems.push(fileConf); } else if ( arg.startsWith(defaultConfigurationLoader.CONFIG_TOML_FILE_ARG) ) { const configFile = arg.substring( defaultConfigurationLoader.CONFIG_TOML_FILE_ARG.length ); const fileConf: FConfiguration = await FConfigurationToml.fromFile( configFile ); chainItems.push(fileConf); } else if ( arg.startsWith(defaultConfigurationLoader.CONFIG_SECRET_DIR_ARG) ) { const secretsDir = arg.substring( defaultConfigurationLoader.CONFIG_SECRET_DIR_ARG.length ); const secretsConfiguration = await FConfigurationDirectory.read( secretsDir ); chainItems.push(secretsConfiguration); } else if (arg === defaultConfigurationLoader.CONFIG_ENV_ARG) { const envConf = new FConfigurationEnv(); chainItems.push(envConf); } } if (chainItems.length === 0) { throw new FLauncherException( "Missing configuration. Please provide at least one of: " + `${defaultConfigurationLoader.CONFIG_ENV_ARG}, ${defaultConfigurationLoader.CONFIG_FILE_ARG}, ${defaultConfigurationLoader.CONFIG_TOML_FILE_ARG}, ${defaultConfigurationLoader.CONFIG_SECRET_DIR_ARG}` ); } chainItems.reverse(); const rawConfiguration: FConfiguration = new FConfigurationChain( ...chainItems ); return rawConfiguration; } export namespace defaultConfigurationLoader { export const CONFIG_ENV_ARG = "--config-env"; export const CONFIG_FILE_ARG = "--config-file="; export const CONFIG_TOML_FILE_ARG = "--config-toml-file="; export const CONFIG_SECRET_DIR_ARG = "--config-secrets-dir="; } export function registerShutdownHook(cb: () => Promise<void>): void { shutdownHooks.push(cb); } const shutdownHooks: Array<() => Promise<void>> = []; async function fireShutdownHooks(): Promise<void> { if (shutdownHooks.length > 0) { const log = FLogger.create("FLauncher.fireShutdownHooks"); log.debug(FExecutionContext.Empty, "Executing shutdown hooks..."); const shutdownHooksCopy = [...shutdownHooks]; do { const cb: () => Promise<void> = shutdownHooksCopy.pop()!; try { await cb(); } catch (e) { const ex: FException = FException.wrapIfNeeded(e); log.warn( FExecutionContext.Empty, `An shutdown hook was finished with error: ${ex.message}` ); log.debug( FExecutionContext.Empty, "An shutdown hook was finished with error", ex ); } } while (shutdownHooksCopy.length > 0); } }