Allow plugins to specify "exit" functions

This commit is contained in:
valadaptive 2023-12-17 12:26:34 -05:00
parent 4fcb7b5ea4
commit 7ae0e05946
2 changed files with 31 additions and 13 deletions

View File

@ -608,9 +608,12 @@ const setupTasks = async function () {
await loadTokenizers(); await loadTokenizers();
await statsEndpoint.init(); await statsEndpoint.init();
const cleanupPlugins = await loadPlugins();
const exitProcess = () => { const exitProcess = () => {
statsEndpoint.onExit(); statsEndpoint.onExit();
process.exit(); process.exit();
cleanupPlugins();
}; };
// Set up event listeners for a graceful shutdown // Set up event listeners for a graceful shutdown
@ -621,7 +624,6 @@ const setupTasks = async function () {
exitProcess(); exitProcess();
}); });
await loadPlugins();
console.log('Launching...'); console.log('Launching...');

View File

@ -22,9 +22,12 @@ const isESModule = (file) => path.extname(file) === '.mjs';
* Load and initialize server plugins from a directory if they are enabled. * Load and initialize server plugins from a directory if they are enabled.
* @param {import('express').Express} app Express app * @param {import('express').Express} app Express app
* @param {string} pluginsPath Path to plugins directory * @param {string} pluginsPath Path to plugins directory
* @returns {Promise<any>} Promise that resolves when all plugins are loaded * @returns {Promise<Function>} Promise that resolves when all plugins are loaded. Resolves to a "cleanup" function to
* be called before the server shuts down.
*/ */
async function loadPlugins(app, pluginsPath) { async function loadPlugins(app, pluginsPath) {
const exitHooks = [];
// Server plugins are disabled. // Server plugins are disabled.
if (!enableServerPlugins) { if (!enableServerPlugins) {
return; return;
@ -46,7 +49,7 @@ async function loadPlugins(app, pluginsPath) {
const pluginFilePath = path.join(pluginsPath, file); const pluginFilePath = path.join(pluginsPath, file);
if (fs.statSync(pluginFilePath).isDirectory()) { if (fs.statSync(pluginFilePath).isDirectory()) {
await loadFromDirectory(app, pluginFilePath); await loadFromDirectory(app, pluginFilePath, exitHooks);
continue; continue;
} }
@ -55,11 +58,14 @@ async function loadPlugins(app, pluginsPath) {
continue; continue;
} }
await loadFromFile(app, pluginFilePath); await loadFromFile(app, pluginFilePath, exitHooks);
}
} }
async function loadFromDirectory(app, pluginDirectoryPath) { // Call all plugin "exit" functions at once and wait for them to finish
return () => Promise.all(exitHooks.map(exitFn => exitFn()));
}
async function loadFromDirectory(app, pluginDirectoryPath, exitHooks) {
const files = fs.readdirSync(pluginDirectoryPath); const files = fs.readdirSync(pluginDirectoryPath);
// No plugins to load. // No plugins to load.
@ -70,7 +76,7 @@ async function loadFromDirectory(app, pluginDirectoryPath) {
// Plugin is an npm package. // Plugin is an npm package.
const packageJsonFilePath = path.join(pluginDirectoryPath, 'package.json'); const packageJsonFilePath = path.join(pluginDirectoryPath, 'package.json');
if (fs.existsSync(packageJsonFilePath)) { if (fs.existsSync(packageJsonFilePath)) {
if (await loadFromPackage(app, packageJsonFilePath)) { if (await loadFromPackage(app, packageJsonFilePath, exitHooks)) {
return; return;
} }
} }
@ -78,7 +84,7 @@ async function loadFromDirectory(app, pluginDirectoryPath) {
// Plugin is a CommonJS module. // Plugin is a CommonJS module.
const cjsFilePath = path.join(pluginDirectoryPath, 'index.js'); const cjsFilePath = path.join(pluginDirectoryPath, 'index.js');
if (fs.existsSync(cjsFilePath)) { if (fs.existsSync(cjsFilePath)) {
if (await loadFromFile(app, cjsFilePath)) { if (await loadFromFile(app, cjsFilePath, exitHooks)) {
return; return;
} }
} }
@ -86,7 +92,7 @@ async function loadFromDirectory(app, pluginDirectoryPath) {
// Plugin is an ECMAScript module. // Plugin is an ECMAScript module.
const esmFilePath = path.join(pluginDirectoryPath, 'index.mjs'); const esmFilePath = path.join(pluginDirectoryPath, 'index.mjs');
if (fs.existsSync(esmFilePath)) { if (fs.existsSync(esmFilePath)) {
if (await loadFromFile(app, esmFilePath)) { if (await loadFromFile(app, esmFilePath, exitHooks)) {
return; return;
} }
} }
@ -96,14 +102,16 @@ async function loadFromDirectory(app, pluginDirectoryPath) {
* Loads and initializes a plugin from an npm package. * Loads and initializes a plugin from an npm package.
* @param {import('express').Express} app Express app * @param {import('express').Express} app Express app
* @param {string} packageJsonPath Path to package.json file * @param {string} packageJsonPath Path to package.json file
* @param {Array<Function>} exitHooks Array of functions to be run on plugin exit. Will be pushed to if the plugin has
* an "exit" function.
* @returns {Promise<boolean>} Promise that resolves to true if plugin was loaded successfully * @returns {Promise<boolean>} Promise that resolves to true if plugin was loaded successfully
*/ */
async function loadFromPackage(app, packageJsonPath) { async function loadFromPackage(app, packageJsonPath, exitHooks) {
try { try {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
if (packageJson.main) { if (packageJson.main) {
const pluginFilePath = path.join(path.dirname(packageJsonPath), packageJson.main); const pluginFilePath = path.join(path.dirname(packageJsonPath), packageJson.main);
return await loadFromFile(app, pluginFilePath); return await loadFromFile(app, pluginFilePath, exitHooks);
} }
} catch (error) { } catch (error) {
console.error(`Failed to load plugin from ${packageJsonPath}: ${error}`); console.error(`Failed to load plugin from ${packageJsonPath}: ${error}`);
@ -115,13 +123,15 @@ async function loadFromPackage(app, packageJsonPath) {
* Loads and initializes a plugin from a file. * Loads and initializes a plugin from a file.
* @param {import('express').Express} app Express app * @param {import('express').Express} app Express app
* @param {string} pluginFilePath Path to plugin directory * @param {string} pluginFilePath Path to plugin directory
* @param {Array.<Function>} exitHooks Array of functions to be run on plugin exit. Will be pushed to if the plugin has
* an "exit" function.
* @returns {Promise<boolean>} Promise that resolves to true if plugin was loaded successfully * @returns {Promise<boolean>} Promise that resolves to true if plugin was loaded successfully
*/ */
async function loadFromFile(app, pluginFilePath) { async function loadFromFile(app, pluginFilePath, exitHooks) {
try { try {
const plugin = await getPluginModule(pluginFilePath); const plugin = await getPluginModule(pluginFilePath);
console.log(`Initializing plugin from ${pluginFilePath}`); console.log(`Initializing plugin from ${pluginFilePath}`);
return await initPlugin(app, plugin); return await initPlugin(app, plugin, exitHooks);
} catch (error) { } catch (error) {
console.error(`Failed to load plugin from ${pluginFilePath}: ${error}`); console.error(`Failed to load plugin from ${pluginFilePath}: ${error}`);
return false; return false;
@ -141,9 +151,11 @@ function isValidPluginID(id) {
* Initializes a plugin module. * Initializes a plugin module.
* @param {import('express').Express} app Express app * @param {import('express').Express} app Express app
* @param {any} plugin Plugin module * @param {any} plugin Plugin module
* @param {Array.<Function>} exitHooks Array of functions to be run on plugin exit. Will be pushed to if the plugin has
* an "exit" function.
* @returns {Promise<boolean>} Promise that resolves to true if plugin was initialized successfully * @returns {Promise<boolean>} Promise that resolves to true if plugin was initialized successfully
*/ */
async function initPlugin(app, plugin) { async function initPlugin(app, plugin, exitHooks) {
if (typeof plugin.info !== 'object') { if (typeof plugin.info !== 'object') {
console.error('Failed to load plugin module; plugin info not found'); console.error('Failed to load plugin module; plugin info not found');
return false; return false;
@ -179,6 +191,10 @@ async function initPlugin(app, plugin) {
app.use(`/plugins/${id}`, router); app.use(`/plugins/${id}`, router);
} }
if (typeof plugin.exit === 'function') {
exitHooks.push(plugin.exit);
}
return true; return true;
} }