actionhero
Version:
The reusable, scalable, and quick node.js API server for stateless and stateful applications
355 lines (354 loc) • 15.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.Process = void 0;
const path = require("path");
const glob = require("glob");
const fs = require("fs");
const config_1 = require("./../modules/config");
const log_1 = require("../modules/log");
const utils_1 = require("../modules/utils");
const id_1 = require("./process/id");
const env_1 = require("./process/env");
const pid_1 = require("./process/pid");
const index_1 = require("../index");
let config = {};
class Process {
constructor() {
this.initialized = false;
this.started = false;
this.stopped = false;
this.initializers = {};
this.loadInitializers = [];
this.startInitializers = [];
this.stopInitializers = [];
this.stopReasons = [];
this.startCount = 0;
index_1.api.commands.initialize = async (...args) => {
return this.initialize(...args);
};
index_1.api.commands.start = async (...args) => {
return this.start(...args);
};
index_1.api.commands.stop = async () => {
return this.stop();
};
index_1.api.commands.restart = async () => {
return this.restart();
};
index_1.api.process = this;
}
async initialize(params = {}) {
this._startingParams = params;
const loadInitializerRankings = {};
const startInitializerRankings = {};
const stopInitializerRankings = {};
let initializerFiles = [];
// rebuild config with startingParams
config = config_1.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) => {
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 = 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_1.utils.arrayUnique(initializerFiles);
initializerFiles = utils_1.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 Promise.resolve().then(() => require(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;
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_1.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_1.log(`Loading initializer: ${initializer.name}`, "debug", file);
try {
await initializer.initialize(config);
try {
log_1.log(`Loaded initializer: ${initializer.name}`, "debug", file);
}
catch (e) { }
}
catch (error) {
const message = `Exception occurred in initializer \`${initializer.name}\` during load`;
try {
log_1.log(message, "emerg", error.toString());
}
catch (_error) {
console.error(message);
}
throw error;
}
}
};
const startFunction = async () => {
if (typeof initializer.start === "function") {
log_1.log(`Starting initializer: ${initializer.name}`, "debug", file);
try {
await initializer.start(config);
log_1.log(`Started initializer: ${initializer.name}`, "debug", file);
}
catch (error) {
log_1.log(`Exception occurred in initializer \`${initializer.name}\` during start`, "emerg", error.toString());
throw error;
}
}
};
const stopFunction = async () => {
if (typeof initializer.stop === "function") {
log_1.log(`Stopping initializer: ${initializer.name}`, "debug", file);
try {
await initializer.stop(config);
log_1.log(`Stopped initializer: ${initializer.name}`, "debug", file);
}
catch (error) {
log_1.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_1.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);
pid_1.writePidFile();
this.running = true;
index_1.api.running = true;
log_1.log(`environment: ${env_1.env}`, "notice");
log_1.log(`*** Starting ${config.general.serverName} ***`, "info");
this.startInitializers.push(() => {
this.bootTime = new Date().getTime();
if (this.startCount === 0) {
log_1.log(`server ID: ${id_1.id}`, "notice");
log_1.log(`*** ${config.general.serverName} Started ***`, "notice");
this.startCount++;
}
else {
log_1.log(`*** ${config.general.serverName} Restarted ***`, "notice");
}
});
try {
await utils_1.utils.asyncWaterfall(this.startInitializers);
}
catch (error) {
return this.fatalError(error, "start");
}
this.started = true;
}
async stop(stopReasons = []) {
var _a;
if (this.running) {
this.shuttingDown = true;
this.running = false;
this.initialized = false;
this.started = false;
this.stopReasons = Array.isArray(stopReasons)
? stopReasons
: [stopReasons];
log_1.log("stopping process...", "notice");
if (((_a = this.stopReasons) === null || _a === void 0 ? void 0 : _a.length) > 0) {
log_1.log(`stop reasons: ${this.stopReasons.join(", ")}`, "debug");
}
await utils_1.utils.sleep(100);
this.stopInitializers.push(async () => {
pid_1.clearPidFile();
log_1.log(`*** ${config.general.serverName} Stopped ***`, "notice");
delete this.shuttingDown;
// reset initializers to prevent duplicate check on restart
this.initializers = {};
index_1.api.running = false;
await utils_1.utils.sleep(100);
});
try {
await utils_1.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_1.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) => { }) {
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) => {
log_1.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) => {
log_1.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_1.log(`[ SIGNAL ] - SIGINT`, "notice");
let timer = awaitHardStop();
await this.stop();
if (!this.shuttingDown) {
clearTimeout(timer);
stopCallback(0);
}
});
process.on("SIGTERM", async () => {
log_1.log(`[ SIGNAL ] - SIGTERM`, "notice");
let timer = awaitHardStop();
await this.stop();
if (!this.shuttingDown) {
clearTimeout(timer);
stopCallback(0);
}
});
process.on("SIGUSR2", async () => {
log_1.log(`[ SIGNAL ] - SIGUSR2`, "notice");
let timer = awaitHardStop();
await this.restart();
clearTimeout(timer);
});
}
// HELPERS
async fatalError(errors = [], type) {
if (!(errors instanceof Array))
errors = [errors];
if (errors) {
log_1.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) => {
var _a, _b;
log_1.log(showStack
? (_a = error.stack) !== null && _a !== void 0 ? _a : error.toString()
: (_b = error.message) !== null && _b !== void 0 ? _b : error.toString(), "emerg");
});
await this.stop(errors.map((e) => { var _a; return (_a = e.message) !== null && _a !== void 0 ? _a : e.toString(); }));
await utils_1.utils.sleep(1000); // allow time for console.log to print
process.exit(1);
}
}
flattenOrderedInitializer(collection) {
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;
}
}
exports.Process = Process;
function sortNumber(a, b) {
return a - b;
}