actionhero
Version:
The reusable, scalable, and quick node.js API server for stateless and stateful applications
480 lines (416 loc) • 13.9 kB
text/typescript
import * as path from "path";
import * as fs from "fs";
import { config } from "..";
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";
import { rebuildConfig } from "../modules/config";
import { safeGlobSync } from "../modules/utils/safeGlob";
export const fatalErrorCode = "FATAL_ACTIONHERO_ERROR";
export class Process {
running: boolean;
initialized: boolean;
started: boolean;
stopped: boolean;
stopReasons?: string[];
shuttingDown: boolean;
bootTime: number;
initializers: Initializers;
startCount: number;
loadInitializers: Initializer["initialize"][];
startInitializers: Initializer["start"][];
stopInitializers: Initializer["stop"][];
constructor() {
this.initialized = false;
this.started = false;
this.stopped = false;
this.initializers = {};
this.loadInitializers = [];
this.startInitializers = [];
this.stopInitializers = [];
this.stopReasons = [];
this.startCount = 0;
api.process = this;
api.commands.initialize = this.initialize;
api.commands.start = this.start;
api.commands.stop = this.stop;
api.commands.restart = this.restart;
}
async initialize() {
const loadInitializerRankings: {
[rank: number]: Initializer["initialize"][];
} = {};
const startInitializerRankings: {
[rank: number]: Initializer["start"][];
} = {};
const stopInitializerRankings: {
[rank: number]: Initializer["stop"][];
} = {};
let initializerFiles: Array<string> = [];
// load initializers from core
initializerFiles = initializerFiles.concat(
safeGlobSync(
path.join(__dirname, "..", "initializers", "**", "**/*(*.js|*.ts)"),
),
);
// load initializers from project
config.general.paths.initializer.forEach((startPath: string) => {
initializerFiles = initializerFiles.concat(
safeGlobSync(path.join(startPath, "**", "**/*(*.js|*.ts)")),
);
});
// load initializers from plugins
for (const plugin of Object.values(config.plugins)) {
const pluginPath: string = path.normalize(plugin.path);
if (!fs.existsSync(pluginPath)) {
throw new Error(`plugin path does not exist: ${pluginPath}`);
}
if (plugin.initializers !== false) {
// old style at the root of the project
initializerFiles = initializerFiles.concat(
safeGlobSync(path.join(pluginPath, "initializers", "**", "*.js")),
);
// new TS dist files
initializerFiles = initializerFiles.concat(
safeGlobSync(
path.join(pluginPath, "dist", "initializers", "**", "*.js"),
),
);
}
}
initializerFiles = utils.arrayUnique(initializerFiles);
initializerFiles = utils.ensureNoTsHeaderOrSpecFiles(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);
}
function decorateInitError(
error: NodeJS.ErrnoException & Record<string, any>,
type: string,
) {
error["data"] = error["data"] ?? {};
error["data"].name = initializer.name;
error["data"].file = file;
error["data"].type = type;
}
const initializeFunction = async () => {
if (typeof initializer.initialize === "function") {
log(`Loading initializer: ${initializer.name}`, "debug", file);
try {
await initializer.initialize();
log(`Loaded initializer: ${initializer.name}`, "debug", file);
} catch (error) {
decorateInitError(error, "initialize");
throw error;
}
}
};
const startFunction = async () => {
if (typeof initializer.start === "function") {
log(`Starting initializer: ${initializer.name}`, "debug", file);
try {
await initializer.start();
log(`Started initializer: ${initializer.name}`, "debug", file);
} catch (error) {
decorateInitError(error, "start");
throw error;
}
}
};
const stopFunction = async () => {
if (typeof initializer.stop === "function") {
log(`Stopping initializer: ${initializer.name}`, "debug", file);
try {
await initializer.stop();
log(`Stopped initializer: ${initializer.name}`, "debug", file);
} catch (error) {
decorateInitError(error, "stop");
throw error;
}
}
};
if (!loadInitializerRankings[initializer.loadPriority]) {
loadInitializerRankings[initializer.loadPriority] = [];
}
if (!startInitializerRankings[initializer.startPriority]) {
startInitializerRankings[initializer.startPriority] = [];
}
if (!stopInitializerRankings[initializer.stopPriority]) {
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 {
for (const loader of this.loadInitializers) await loader();
} catch (error) {
return this.fatalError(error, "initialize");
}
this.initialized = true;
}
/**
* Start the Actionhero Process
*/
async start() {
if (!this.initialized) await this.initialize();
const serverName = config.general.serverName;
writePidFile();
this.running = true;
api.running = true;
log(`environment: ${env}`, "notice");
log(`*** Starting ${serverName} ***`, "info");
this.startInitializers.push(async () => {
this.bootTime = new Date().getTime();
if (this.startCount === 0) {
log(`server ID: ${id}`, "notice");
log(`*** ${serverName} Started ***`, "notice");
this.startCount++;
} else {
log(`*** ${serverName} Restarted ***`, "notice");
}
});
try {
for (const starter of this.startInitializers) await starter();
} catch (error) {
return this.fatalError(error, "start");
}
this.started = true;
}
/**
* Stop the Actionhero Process
*/
async stop(stopReasons: string | string[] = []) {
const serverName = config.general.serverName;
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(`*** ${serverName} Stopped ***`, "notice");
delete this.shuttingDown;
// reset initializers to prevent duplicate check on restart
this.initializers = {};
api.running = false;
await utils.sleep(100);
});
try {
for (const stopper of this.stopInitializers) await stopper();
} 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 ${serverName}, not running`;
log(message, "crit");
}
}
/**
* Restart the Actionhero Process
*/
async restart() {
if (this.running === true) {
await this.stop();
rebuildConfig();
await this.start();
} else {
await this.start();
}
}
/**
* 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: NodeJS.ErrnoException) => {
if (error.code !== fatalErrorCode) {
if (api.exceptionHandlers) {
api.exceptionHandlers.report(
error,
"uncaught",
"Exception",
{},
"emerg",
);
} else {
console.error(error);
}
}
if (this.shuttingDown !== true) {
let timer = awaitHardStop();
if (this.running) await this.stop();
clearTimeout(timer);
stopCallback(1);
}
});
process.once(
"unhandledRejection",
async (rejection: NodeJS.ErrnoException) => {
if (rejection.code !== fatalErrorCode) {
if (api.exceptionHandlers) {
api.exceptionHandlers.report(
rejection,
"uncaught",
"Rejection",
{},
"emerg",
);
} else {
console.error(rejection);
}
}
if (!this.shuttingDown) {
let timer = awaitHardStop();
if (this.running) await this.stop();
clearTimeout(timer);
stopCallback(1);
}
},
);
// handle signals
process.on("SIGINT", async () => {
log(`[ SIGNAL ] - SIGINT`, "notice");
let timer = awaitHardStop();
if (this.running) await this.stop();
if (!this.shuttingDown) {
clearTimeout(timer);
stopCallback(0);
}
});
process.on("SIGTERM", async () => {
log(`[ SIGNAL ] - SIGTERM`, "notice");
let timer = awaitHardStop();
if (this.running) 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: NodeJS.ErrnoException | NodeJS.ErrnoException[] = [],
type: string,
) {
if (!(errors instanceof Array)) errors = [errors];
if (errors) {
const showStack = process.env.ACTIONHERO_FATAL_ERROR_STACK_DISPLAY
? process.env.ACTIONHERO_FATAL_ERROR_STACK_DISPLAY === "true"
: true;
errors.forEach((error) => {
if (!showStack) delete error.stack;
if (api.exceptionHandlers) {
api.exceptionHandlers.report(error, "initializer", type);
} else {
console.error(error);
}
});
if (this.running) {
await this.stop(errors.map((e) => e.message ?? e.toString())); // stop and set the stopReasons
}
await utils.sleep(100); // allow time for console.log to print
if (!errors[0].code) errors[0].code = fatalErrorCode;
throw errors[0];
}
}
flattenOrderedInitializer<T>(collection: { [rank: number]: T[] }) {
const output: T[] = [];
const keys = [];
for (const key in collection) keys.push(parseInt(key, 10));
keys.sort(sortNumber);
keys.forEach((key) => {
collection[key].forEach((d) => {
output.push(d);
});
});
return output;
}
}
function sortNumber(a: number, b: number) {
return a - b;
}