#!/usr/bin/env node 'use strict'; /** * This module is started with src/bin/run.sh. It sets up a Express HTTP and a Socket.IO Server. * Static file Requests are answered directly from this module, Socket.IO messages are passed * to MessageHandler and minfied requests are passed to minified. */ /* * 2011 Peter 'Pita' Martischka (Primary Technology Ltd) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS-IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const log4js = require('log4js'); log4js.replaceConsole(); // wtfnode should be loaded after log4js.replaceConsole() so that it uses log4js for logging, and it // should be above everything else so that it can hook in before resources are used. const wtfnode = require('wtfnode'); /* * early check for version compatibility before calling * any modules that require newer versions of NodeJS */ const NodeVersion = require('./utils/NodeVersion'); NodeVersion.enforceMinNodeVersion('10.17.0'); NodeVersion.checkDeprecationStatus('10.17.0', '1.8.8'); const UpdateCheck = require('./utils/UpdateCheck'); const db = require('./db/DB'); const express = require('./hooks/express'); const hooks = require('../static/js/pluginfw/hooks'); const npm = require('npm/lib/npm.js'); const pluginDefs = require('../static/js/pluginfw/plugin_defs'); const plugins = require('../static/js/pluginfw/plugins'); const settings = require('./utils/Settings'); const util = require('util'); const logger = log4js.getLogger('server'); const State = { INITIAL: 1, STARTING: 2, RUNNING: 3, STOPPING: 4, STOPPED: 5, EXITING: 6, WAITING_FOR_EXIT: 7, STATE_TRANSITION_FAILED: 8, }; let state = State.INITIAL; class Gate extends Promise { constructor() { let res; super((resolve) => { res = resolve; }); this.resolve = res; } } const removeSignalListener = (signal, listener) => { logger.debug(`Removing ${signal} listener because it might interfere with shutdown tasks. ` + `Function code:\n${listener.toString()}\n` + `Current stack:\n${(new Error()).stack.split('\n').slice(1).join('\n')}`); process.off(signal, listener); }; let startDoneGate; exports.start = async () => { switch (state) { case State.INITIAL: break; case State.STARTING: await startDoneGate; // Retry. Don't fall through because it might have transitioned to STATE_TRANSITION_FAILED. return await exports.start(); case State.RUNNING: return express.server; case State.STOPPING: case State.STOPPED: case State.EXITING: case State.WAITING_FOR_EXIT: case State.STATE_TRANSITION_FAILED: throw new Error('restart not supported'); default: throw new Error(`unknown State: ${state.toString()}`); } logger.info('Starting Etherpad...'); startDoneGate = new Gate(); state = State.STARTING; try { // Check if Etherpad version is up-to-date UpdateCheck.check(); // start up stats counting system const stats = require('./stats'); stats.gauge('memoryUsage', () => process.memoryUsage().rss); stats.gauge('memoryUsageHeap', () => process.memoryUsage().heapUsed); process.on('uncaughtException', (err) => exports.exit(err)); // As of v14, Node.js does not exit when there is an unhandled Promise rejection. Convert an // unhandled rejection into an uncaught exception, which does cause Node.js to exit. process.on('unhandledRejection', (err) => { throw err; }); for (const signal of ['SIGINT', 'SIGTERM']) { // Forcibly remove other signal listeners to prevent them from terminating node before we are // done cleaning up. See https://github.com/andywer/threads.js/pull/329 for an example of a // problematic listener. This means that exports.exit is solely responsible for performing all // necessary cleanup tasks. for (const listener of process.listeners(signal)) { removeSignalListener(signal, listener); } process.on(signal, exports.exit); // Prevent signal listeners from being added in the future. process.on('newListener', (event, listener) => { if (event !== signal) return; removeSignalListener(signal, listener); }); } await util.promisify(npm.load)(); await db.init(); await plugins.update(); const installedPlugins = Object.values(pluginDefs.plugins) .filter((plugin) => plugin.package.name !== 'ep_etherpad-lite') .map((plugin) => `${plugin.package.name}@${plugin.package.version}`) .join(', '); logger.info(`Installed plugins: ${installedPlugins}`); logger.debug(`Installed parts:\n${plugins.formatParts()}`); logger.debug(`Installed hooks:\n${plugins.formatHooks()}`); await hooks.aCallAll('loadSettings', {settings}); await hooks.aCallAll('createServer'); } catch (err) { logger.error('Error occurred while starting Etherpad'); state = State.STATE_TRANSITION_FAILED; startDoneGate.resolve(); return await exports.exit(err); } logger.info('Etherpad is running'); state = State.RUNNING; startDoneGate.resolve(); // Return the HTTP server to make it easier to write tests. return express.server; }; let stopDoneGate; exports.stop = async () => { switch (state) { case State.STARTING: await exports.start(); // Don't fall through to State.RUNNING in case another caller is also waiting for startup. return await exports.stop(); case State.RUNNING: break; case State.STOPPING: await stopDoneGate; // fall through case State.INITIAL: case State.STOPPED: case State.EXITING: case State.WAITING_FOR_EXIT: case State.STATE_TRANSITION_FAILED: return; default: throw new Error(`unknown State: ${state.toString()}`); } logger.info('Stopping Etherpad...'); const stopDoneGate = new Gate(); state = State.STOPPING; try { let timeout = null; await Promise.race([ hooks.aCallAll('shutdown'), new Promise((resolve, reject) => { timeout = setTimeout(() => reject(new Error('Timed out waiting for shutdown tasks')), 3000); }), ]); clearTimeout(timeout); } catch (err) { logger.error('Error occurred while stopping Etherpad'); state = State.STATE_TRANSITION_FAILED; stopDoneGate.resolve(); return await exports.exit(err); } logger.info('Etherpad stopped'); state = State.STOPPED; stopDoneGate.resolve(); }; let exitGate; let exitCalled = false; exports.exit = async (err = null) => { /* eslint-disable no-process-exit */ if (err === 'SIGTERM') { // Termination from SIGTERM is not treated as an abnormal termination. logger.info('Received SIGTERM signal'); err = null; } else if (err != null) { logger.error(err.stack || err.toString()); process.exitCode = 1; if (exitCalled) { logger.error('Error occurred while waiting to exit. Forcing an immediate unclean exit...'); process.exit(1); } } exitCalled = true; switch (state) { case State.STARTING: case State.RUNNING: case State.STOPPING: await exports.stop(); // Don't fall through to State.STOPPED in case another caller is also waiting for stop(). // Don't pass err to exports.exit() because this err has already been processed. (If err is // passed again to exit() then exit() will think that a second error occurred while exiting.) return await exports.exit(); case State.INITIAL: case State.STOPPED: case State.STATE_TRANSITION_FAILED: break; case State.EXITING: await exitGate; // fall through case State.WAITING_FOR_EXIT: return; default: throw new Error(`unknown State: ${state.toString()}`); } logger.info('Exiting...'); exitGate = new Gate(); state = State.EXITING; exitGate.resolve(); // Node.js should exit on its own without further action. Add a timeout to force Node.js to exit // just in case something failed to get cleaned up during the shutdown hook. unref() is called on // the timeout so that the timeout itself does not prevent Node.js from exiting. setTimeout(() => { logger.error('Something that should have been cleaned up during the shutdown hook (such as ' + 'a timer, worker thread, or open connection) is preventing Node.js from exiting'); wtfnode.dump(); logger.error('Forcing an unclean exit...'); process.exit(1); }, 5000).unref(); logger.info('Waiting for Node.js to exit...'); state = State.WAITING_FOR_EXIT; /* eslint-enable no-process-exit */ }; if (require.main === module) exports.start();