actionhero
Version:
The reusable, scalable, and quick node.js API server for stateless and stateful applications
453 lines (387 loc) • 13.3 kB
text/typescript
import * as path from "path";
import * as glob from "glob";
import * as fs from "fs";
import { buildConfig, ConfigInterface } from "./../modules/config";
import { log } from "../modules/log";
import { Initializer } from "./initializer";
import { Initializers } from "./initializers";
import { utils } from "../modules/utils";
import { id } from "./process/id";
import { env } from "./process/env";
import { writePidFile, clearPidFile } from "./process/pid";
import { api } from "../index";
let config: ConfigInterface = {};
export class Process {
running: boolean;
initialized: boolean;
started: boolean;
stopped: boolean;
stopReasons?: string[];
shuttingDown: boolean;
bootTime: number;
initializers: Initializers;
startCount: number;
loadInitializers: Array<Function>;
startInitializers: Array<Function>;
stopInitializers: Array<Function>;
_startingParams: {
[key: string]: any;
};
constructor() {
this.initialized = false;
this.started = false;
this.stopped = false;
this.initializers = {};
this.loadInitializers = [];
this.startInitializers = [];
this.stopInitializers = [];
this.stopReasons = [];
this.startCount = 0;
api.commands.initialize = async (...args) => {
return this.initialize(...args);
};
api.commands.start = async (...args) => {
return this.start(...args);
};
api.commands.stop = async () => {
return this.stop();
};
api.commands.restart = async () => {
return this.restart();
};
api.process = this;
}
async initialize(params: object = {}) {
this._startingParams = params;
const loadInitializerRankings = {};
const startInitializerRankings = {};
const stopInitializerRankings = {};
let initializerFiles: Array<string> = [];
// rebuild config with startingParams
config = buildConfig(this._startingParams);
// load initializers from core
initializerFiles = initializerFiles.concat(
glob.sync(
path.join(__dirname, "..", "initializers", "**", "**/*(*.js|*.ts)")
)
);
// load initializers from project
config.general.paths.initializer.forEach((startPath: string) => {
initializerFiles = initializerFiles.concat(
glob.sync(path.join(startPath, "**", "**/*(*.js|*.ts)"))
);
});
// load initializers from plugins
for (const pluginName in config.plugins) {
if (config.plugins[pluginName] !== false) {
const pluginPath: string = path.normalize(
config.plugins[pluginName].path
);
if (!fs.existsSync(pluginPath)) {
throw new Error(`plugin path does not exist: ${pluginPath}`);
}
// old style at the root of the project
initializerFiles = initializerFiles.concat(
glob.sync(path.join(pluginPath, "initializers", "**", "*.js"))
);
// new TS dist files
initializerFiles = initializerFiles.concat(
glob.sync(path.join(pluginPath, "dist", "initializers", "**", "*.js"))
);
}
}
initializerFiles = utils.arrayUnique(initializerFiles);
initializerFiles = utils.ensureNoTsHeaderFiles(initializerFiles);
for (const i in initializerFiles) {
const f = initializerFiles[i];
const file = path.normalize(f);
if (require.cache[require.resolve(file)]) {
delete require.cache[require.resolve(file)];
}
let exportedClasses = await import(file);
// allow for old-js style single default exports
if (typeof exportedClasses === "function") {
exportedClasses = { default: exportedClasses };
}
if (Object.keys(exportedClasses).length === 0) {
this.fatalError(
new Error(`no exported initializers found in ${file}`),
file
);
}
for (const exportKey in exportedClasses) {
let initializer: Initializer;
let InitializerClass = exportedClasses[exportKey];
try {
initializer = new InitializerClass();
// check if initializer already exists (exclude utils and config)
if (this.initializers[initializer.name]) {
const warningMessage = `an existing initializer with the same name \`${initializer.name}\` will be overridden by the file ${file}`;
log(warningMessage, "warning");
} else {
initializer.validate();
this.initializers[initializer.name] = initializer;
}
} catch (error) {
this.fatalError(error, file);
}
const initializeFunction = async () => {
if (typeof initializer.initialize === "function") {
log(`Loading initializer: ${initializer.name}`, "debug", file);
try {
await initializer.initialize(config);
try {
log(`Loaded initializer: ${initializer.name}`, "debug", file);
} catch (e) {}
} catch (error) {
const message = `Exception occurred in initializer \`${initializer.name}\` during load`;
try {
log(message, "emerg", error.toString());
} catch (_error) {
console.error(message);
}
throw error;
}
}
};
const startFunction = async () => {
if (typeof initializer.start === "function") {
log(`Starting initializer: ${initializer.name}`, "debug", file);
try {
await initializer.start(config);
log(`Started initializer: ${initializer.name}`, "debug", file);
} catch (error) {
log(
`Exception occurred in initializer \`${initializer.name}\` during start`,
"emerg",
error.toString()
);
throw error;
}
}
};
const stopFunction = async () => {
if (typeof initializer.stop === "function") {
log(`Stopping initializer: ${initializer.name}`, "debug", file);
try {
await initializer.stop(config);
log(`Stopped initializer: ${initializer.name}`, "debug", file);
} catch (error) {
log(
`Exception occurred in initializer \`${initializer.name}\` during stop`,
"emerg",
error.toString()
);
throw error;
}
}
};
if (loadInitializerRankings[initializer.loadPriority] === undefined) {
loadInitializerRankings[initializer.loadPriority] = [];
}
if (startInitializerRankings[initializer.startPriority] === undefined) {
startInitializerRankings[initializer.startPriority] = [];
}
if (stopInitializerRankings[initializer.stopPriority] === undefined) {
stopInitializerRankings[initializer.stopPriority] = [];
}
if (initializer.loadPriority > 0) {
loadInitializerRankings[initializer.loadPriority].push(
initializeFunction
);
}
if (initializer.startPriority > 0) {
startInitializerRankings[initializer.startPriority].push(
startFunction
);
}
if (initializer.stopPriority > 0) {
stopInitializerRankings[initializer.stopPriority].push(stopFunction);
}
}
}
// flatten all the ordered initializer methods
this.loadInitializers = this.flattenOrderedInitializer(
loadInitializerRankings
);
this.startInitializers = this.flattenOrderedInitializer(
startInitializerRankings
);
this.stopInitializers = this.flattenOrderedInitializer(
stopInitializerRankings
);
try {
await utils.asyncWaterfall(this.loadInitializers);
} catch (error) {
return this.fatalError(error, "initialize");
}
this.initialized = true;
}
async start(params = {}) {
if (this.initialized !== true) await this.initialize(params);
writePidFile();
this.running = true;
api.running = true;
log(`environment: ${env}`, "notice");
log(`*** Starting ${config.general.serverName} ***`, "info");
this.startInitializers.push(() => {
this.bootTime = new Date().getTime();
if (this.startCount === 0) {
log(`server ID: ${id}`, "notice");
log(`*** ${config.general.serverName} Started ***`, "notice");
this.startCount++;
} else {
log(`*** ${config.general.serverName} Restarted ***`, "notice");
}
});
try {
await utils.asyncWaterfall(this.startInitializers);
} catch (error) {
return this.fatalError(error, "start");
}
this.started = true;
}
async stop(stopReasons: string | string[] = []) {
if (this.running) {
this.shuttingDown = true;
this.running = false;
this.initialized = false;
this.started = false;
this.stopReasons = Array.isArray(stopReasons)
? stopReasons
: [stopReasons];
log("stopping process...", "notice");
if (this.stopReasons?.length > 0) {
log(`stop reasons: ${this.stopReasons.join(", ")}`, "debug");
}
await utils.sleep(100);
this.stopInitializers.push(async () => {
clearPidFile();
log(`*** ${config.general.serverName} Stopped ***`, "notice");
delete this.shuttingDown;
// reset initializers to prevent duplicate check on restart
this.initializers = {};
api.running = false;
await utils.sleep(100);
});
try {
await utils.asyncWaterfall(this.stopInitializers);
} catch (error) {
return this.fatalError(error, "stop");
}
this.stopped = true;
} else if (this.shuttingDown === true) {
// double sigterm; ignore it
} else {
const message = `Cannot shut down ${config.general.serverName}, not running`;
log(message, "crit");
}
}
async restart() {
if (this.running === true) {
await this.stop();
await this.start(this._startingParams);
} else {
await this.start(this._startingParams);
}
}
/**
* Register listeners for process signals and uncaught exceptions & rejections.
* Try to gracefully shut down when signaled to do so
*/
registerProcessSignals(stopCallback = (exitCode?: number) => {}) {
const timeout = process.env.ACTIONHERO_SHUTDOWN_TIMEOUT
? parseInt(process.env.ACTIONHERO_SHUTDOWN_TIMEOUT)
: 1000 * 30;
function awaitHardStop() {
return setTimeout(() => {
console.error(
`Process did not terminate within ${timeout}ms. Stopping now!`
);
process.nextTick(process.exit(1));
}, timeout);
}
// handle errors & rejections
process.once("uncaughtException", async (error: Error) => {
log(`UNCAUGHT EXCEPTION: ` + error.stack, "fatal");
if (!this.shuttingDown === true) {
let timer = awaitHardStop();
await this.stop();
clearTimeout(timer);
stopCallback(1);
}
});
process.once("unhandledRejection", async (rejection: Error) => {
log(`UNHANDLED REJECTION: ` + rejection.stack, "fatal");
if (!this.shuttingDown === true) {
let timer = awaitHardStop();
await this.stop();
clearTimeout(timer);
stopCallback(1);
}
});
// handle signals
process.on("SIGINT", async () => {
log(`[ SIGNAL ] - SIGINT`, "notice");
let timer = awaitHardStop();
await this.stop();
if (!this.shuttingDown) {
clearTimeout(timer);
stopCallback(0);
}
});
process.on("SIGTERM", async () => {
log(`[ SIGNAL ] - SIGTERM`, "notice");
let timer = awaitHardStop();
await this.stop();
if (!this.shuttingDown) {
clearTimeout(timer);
stopCallback(0);
}
});
process.on("SIGUSR2", async () => {
log(`[ SIGNAL ] - SIGUSR2`, "notice");
let timer = awaitHardStop();
await this.restart();
clearTimeout(timer);
});
}
// HELPERS
async fatalError(errors: Error | Error[] = [], type: any) {
if (!(errors instanceof Array)) errors = [errors];
if (errors) {
log(`Error with initializer step: ${JSON.stringify(type)}`, "emerg");
const showStack = process.env.ACTIONHERO_FATAL_ERROR_STACK_DISPLAY
? process.env.ACTIONHERO_FATAL_ERROR_STACK_DISPLAY === "true"
: true;
errors.forEach((error) => {
log(
showStack
? error.stack ?? error.toString()
: error.message ?? error.toString(),
"emerg"
);
});
await this.stop(errors.map((e) => e.message ?? e.toString()));
await utils.sleep(1000); // allow time for console.log to print
process.exit(1);
}
}
flattenOrderedInitializer(collection: any) {
const output = [];
const keys = [];
for (const key in collection) {
keys.push(parseInt(key));
}
keys.sort(sortNumber);
keys.forEach((key) => {
collection[key].forEach((d) => {
output.push(d);
});
});
return output;
}
}
function sortNumber(a: number, b: number) {
return a - b;
}