resolve conflict

This commit is contained in:
John McLear 2021-01-30 08:11:10 +00:00
commit 96520a3f31
13 changed files with 458 additions and 394 deletions

View file

@ -7,12 +7,14 @@
// unhandled rejection into an uncaught exception, which does cause Node.js to exit. // unhandled rejection into an uncaught exception, which does cause Node.js to exit.
process.on('unhandledRejection', (err) => { throw err; }); process.on('unhandledRejection', (err) => { throw err; });
const npm = require('ep_etherpad-lite/node_modules/npm');
const util = require('util');
if (process.argv.length !== 2) throw new Error('Use: node bin/checkAllPads.js'); if (process.argv.length !== 2) throw new Error('Use: node bin/checkAllPads.js');
// load and initialize NPM (async () => {
const npm = require('ep_etherpad-lite/node_modules/npm'); await util.promisify(npm.load)({});
npm.load({}, async () => {
try {
// initialize the database // initialize the database
require('ep_etherpad-lite/node/utils/Settings'); require('ep_etherpad-lite/node/utils/Settings');
const db = require('ep_etherpad-lite/node/db/DB'); const db = require('ep_etherpad-lite/node/db/DB');
@ -30,7 +32,7 @@ npm.load({}, async () => {
const pad = await padManager.getPad(padId); const pad = await padManager.getPad(padId);
// check if the pad has a pool // check if the pad has a pool
if (pad.pool === undefined) { if (pad.pool == null) {
console.error(`[${pad.id}] Missing attribute pool`); console.error(`[${pad.id}] Missing attribute pool`);
continue; continue;
} }
@ -66,13 +68,13 @@ npm.load({}, async () => {
} }
// check if there is a atext in the keyRevisions // check if there is a atext in the keyRevisions
if (revisions[keyRev].meta === undefined || revisions[keyRev].meta.atext === undefined) { let {meta: {atext} = {}} = revisions[keyRev];
if (atext == null) {
console.error(`[${pad.id}] Missing atext in revision ${keyRev}`); console.error(`[${pad.id}] Missing atext in revision ${keyRev}`);
continue; continue;
} }
const apool = pad.pool; const apool = pad.pool;
let atext = revisions[keyRev].meta.atext;
for (let rev = keyRev + 1; rev <= keyRev + 100 && rev <= head; rev++) { for (let rev = keyRev + 1; rev <= keyRev + 100 && rev <= head; rev++) {
try { try {
const cs = revisions[rev].changeset; const cs = revisions[rev].changeset;
@ -88,8 +90,4 @@ npm.load({}, async () => {
throw new Error('No revisions tested'); throw new Error('No revisions tested');
} }
console.log(`Finished: Tested ${revTestedCount} revisions`); console.log(`Finished: Tested ${revTestedCount} revisions`);
} catch (err) { })();
console.trace(err);
throw err;
}
});

View file

@ -7,16 +7,18 @@
// unhandled rejection into an uncaught exception, which does cause Node.js to exit. // unhandled rejection into an uncaught exception, which does cause Node.js to exit.
process.on('unhandledRejection', (err) => { throw err; }); process.on('unhandledRejection', (err) => { throw err; });
const npm = require('ep_etherpad-lite/node_modules/npm');
const util = require('util');
if (process.argv.length !== 3) throw new Error('Use: node bin/checkPad.js $PADID'); if (process.argv.length !== 3) throw new Error('Use: node bin/checkPad.js $PADID');
// get the padID // get the padID
const padId = process.argv[2]; const padId = process.argv[2];
let checkRevisionCount = 0; let checkRevisionCount = 0;
// load and initialize NPM; (async () => {
const npm = require('ep_etherpad-lite/node_modules/npm'); await util.promisify(npm.load)({});
npm.load({}, async () => {
try {
// initialize database // initialize database
require('ep_etherpad-lite/node/utils/Settings'); require('ep_etherpad-lite/node/utils/Settings');
const db = require('ep_etherpad-lite/node/db/DB'); const db = require('ep_etherpad-lite/node/db/DB');
@ -59,18 +61,16 @@ npm.load({}, async () => {
} }
// check if the pad has a pool // check if the pad has a pool
if (pad.pool === undefined) throw new Error('Attribute pool is missing'); if (pad.pool == null) throw new Error('Attribute pool is missing');
// check if there is an atext in the keyRevisions // check if there is an atext in the keyRevisions
if (revisions[keyRev] === undefined || let {meta: {atext} = {}} = revisions[keyRev] || {};
revisions[keyRev].meta === undefined || if (atext == null) {
revisions[keyRev].meta.atext === undefined) {
console.error(`No atext in key revision ${keyRev}`); console.error(`No atext in key revision ${keyRev}`);
continue; continue;
} }
const apool = pad.pool; const apool = pad.pool;
let atext = revisions[keyRev].meta.atext;
for (let rev = keyRev + 1; rev <= keyRev + 100 && rev <= head; rev++) { for (let rev = keyRev + 1; rev <= keyRev + 100 && rev <= head; rev++) {
checkRevisionCount++; checkRevisionCount++;
@ -84,8 +84,4 @@ npm.load({}, async () => {
} }
console.log(`Finished: Checked ${checkRevisionCount} revisions`); console.log(`Finished: Checked ${checkRevisionCount} revisions`);
} }
} catch (err) { })();
console.trace(err);
throw err;
}
});

View file

@ -12,12 +12,14 @@ if (process.argv.length !== 3) throw new Error('Use: node bin/checkPadDeltas.js
// get the padID // get the padID
const padId = process.argv[2]; const padId = process.argv[2];
// load and initialize NPM;
const expect = require('../tests/frontend/lib/expect'); const expect = require('../tests/frontend/lib/expect');
const diff = require('ep_etherpad-lite/node_modules/diff'); const diff = require('ep_etherpad-lite/node_modules/diff');
const npm = require('ep_etherpad-lite/node_modules/npm'); const npm = require('ep_etherpad-lite/node_modules/npm');
const util = require('util');
(async () => {
await util.promisify(npm.load)({});
npm.load({}, async () => {
// initialize database // initialize database
require('ep_etherpad-lite/node/utils/Settings'); require('ep_etherpad-lite/node/utils/Settings');
const db = require('ep_etherpad-lite/node/db/DB'); const db = require('ep_etherpad-lite/node/db/DB');
@ -54,10 +56,8 @@ npm.load({}, async () => {
// console.log('Fetching', revNum) // console.log('Fetching', revNum)
const revision = await db.get(`pad:${padId}:revs:${revNum}`); const revision = await db.get(`pad:${padId}:revs:${revNum}`);
// check if there is a atext in the keyRevisions // check if there is a atext in the keyRevisions
if (~keyRevisions.indexOf(revNum) && const {meta: {atext: revAtext} = {}} = revision || {};
(revision === undefined || if (~keyRevisions.indexOf(revNum) && revAtext == null) {
revision.meta === undefined ||
revision.meta.atext === undefined)) {
console.error(`No atext in key revision ${revNum}`); console.error(`No atext in key revision ${revNum}`);
continue; continue;
} }
@ -104,4 +104,4 @@ npm.load({}, async () => {
} }
})); }));
} }
}); })();

View file

@ -16,11 +16,11 @@ if (process.argv.length !== 3) throw new Error('Use: node extractPadData.js $PAD
const padId = process.argv[2]; const padId = process.argv[2];
const npm = require('ep_etherpad-lite/node_modules/npm'); const npm = require('ep_etherpad-lite/node_modules/npm');
const util = require('util');
npm.load({}, async (err) => { (async () => {
if (err) throw err; await util.promisify(npm.load)({});
try {
// initialize database // initialize database
require('ep_etherpad-lite/node/utils/Settings'); require('ep_etherpad-lite/node/utils/Settings');
const db = require('ep_etherpad-lite/node/db/DB'); const db = require('ep_etherpad-lite/node/db/DB');
@ -29,7 +29,6 @@ npm.load({}, async (err) => {
// load extra modules // load extra modules
const dirtyDB = require('ep_etherpad-lite/node_modules/dirty'); const dirtyDB = require('ep_etherpad-lite/node_modules/dirty');
const padManager = require('ep_etherpad-lite/node/db/PadManager'); const padManager = require('ep_etherpad-lite/node/db/PadManager');
const util = require('util');
// initialize output database // initialize output database
const dirty = dirtyDB(`${padId}.db`); const dirty = dirtyDB(`${padId}.db`);
@ -67,8 +66,4 @@ npm.load({}, async (err) => {
} }
console.log('finished'); console.log('finished');
} catch (err) { })();
console.error(err);
throw err;
}
});

View file

@ -4,6 +4,9 @@
// unhandled rejection into an uncaught exception, which does cause Node.js to exit. // unhandled rejection into an uncaught exception, which does cause Node.js to exit.
process.on('unhandledRejection', (err) => { throw err; }); process.on('unhandledRejection', (err) => { throw err; });
const npm = require('ep_etherpad-lite/node_modules/npm');
const util = require('util');
const startTime = Date.now(); const startTime = Date.now();
const log = (str) => { const log = (str) => {
@ -43,10 +46,10 @@ const unescape = (val) => {
return val; return val;
}; };
(async () => {
await util.promisify(npm.load)({});
require('ep_etherpad-lite/node_modules/npm').load({}, (er, npm) => {
const fs = require('fs'); const fs = require('fs');
const ueberDB = require('ep_etherpad-lite/node_modules/ueberdb2'); const ueberDB = require('ep_etherpad-lite/node_modules/ueberdb2');
const settings = require('ep_etherpad-lite/node/utils/Settings'); const settings = require('ep_etherpad-lite/node/utils/Settings');
const log4js = require('ep_etherpad-lite/node_modules/log4js'); const log4js = require('ep_etherpad-lite/node_modules/log4js');
@ -68,11 +71,7 @@ require('ep_etherpad-lite/node_modules/npm').load({}, (er, npm) => {
if (!sqlFile) throw new Error('Use: node importSqlFile.js $SQLFILE'); if (!sqlFile) throw new Error('Use: node importSqlFile.js $SQLFILE');
log('initializing db'); log('initializing db');
db.init((err) => { await util.promisify(db.init.bind(db))();
// there was an error while initializing the database, output it and stop
if (err) {
throw err;
} else {
log('done'); log('done');
log('open output file...'); log('open output file...');
@ -101,9 +100,6 @@ require('ep_etherpad-lite/node_modules/npm').load({}, (er, npm) => {
process.stdout.write('done. waiting for db to finish transaction. ' + process.stdout.write('done. waiting for db to finish transaction. ' +
'depended on dbms this may take some time..\n'); 'depended on dbms this may take some time..\n');
db.close(() => { await util.promisify(db.close.bind(db))();
log(`finished, imported ${keyNo} keys.`); log(`finished, imported ${keyNo} keys.`);
}); })();
}
});
});

View file

@ -4,9 +4,12 @@
// unhandled rejection into an uncaught exception, which does cause Node.js to exit. // unhandled rejection into an uncaught exception, which does cause Node.js to exit.
process.on('unhandledRejection', (err) => { throw err; }); process.on('unhandledRejection', (err) => { throw err; });
const npm = require('ep_etherpad-lite/node_modules/npm');
const util = require('util'); const util = require('util');
require('ep_etherpad-lite/node_modules/npm').load({}, async (er, npm) => { (async () => {
await util.promisify(npm.load)({});
process.chdir(`${npm.root}/..`); process.chdir(`${npm.root}/..`);
// This script requires that you have modified your settings.json file // This script requires that you have modified your settings.json file
@ -56,4 +59,4 @@ require('ep_etherpad-lite/node_modules/npm').load({}, async (er, npm) => {
await util.promisify(db.close.bind(db))(); await util.promisify(db.close.bind(db))();
console.log('Finished.'); console.log('Finished.');
}); })();

View file

@ -13,7 +13,6 @@ if (process.argv.length !== 4 && process.argv.length !== 5) {
throw new Error('Use: node bin/repairPad.js $PADID $REV [$NEWPADID]'); throw new Error('Use: node bin/repairPad.js $PADID $REV [$NEWPADID]');
} }
const async = require('ep_etherpad-lite/node_modules/async');
const npm = require('ep_etherpad-lite/node_modules/npm'); const npm = require('ep_etherpad-lite/node_modules/npm');
const util = require('util'); const util = require('util');
@ -21,50 +20,33 @@ const padId = process.argv[2];
const newRevHead = process.argv[3]; const newRevHead = process.argv[3];
const newPadId = process.argv[4] || `${padId}-rebuilt`; const newPadId = process.argv[4] || `${padId}-rebuilt`;
let db, oldPad, newPad; (async () => {
let Pad, PadManager; await util.promisify(npm.load)({});
async.series([ const db = require('ep_etherpad-lite/node/db/DB');
(callback) => npm.load({}, callback), await db.init();
(callback) => {
// Get a handle into the database const PadManager = require('ep_etherpad-lite/node/db/PadManager');
db = require('ep_etherpad-lite/node/db/DB'); const Pad = require('ep_etherpad-lite/node/db/Pad').Pad;
db.init(callback);
},
(callback) => {
Pad = require('ep_etherpad-lite/node/db/Pad').Pad;
PadManager = require('ep_etherpad-lite/node/db/PadManager');
// Get references to the original pad and to a newly created pad
// HACK: This is a standalone script, so we want to write everything
// out to the database immediately. The only problem with this is
// that a driver (like the mysql driver) can hardcode these values.
db.db.db.settings = {cache: 0, writeInterval: 0, json: true};
// Validate the newPadId if specified and that a pad with that ID does // Validate the newPadId if specified and that a pad with that ID does
// not already exist to avoid overwriting it. // not already exist to avoid overwriting it.
if (!PadManager.isValidPadId(newPadId)) { if (!PadManager.isValidPadId(newPadId)) {
throw new Error('Cannot create a pad with that id as it is invalid'); throw new Error('Cannot create a pad with that id as it is invalid');
} }
PadManager.doesPadExists(newPadId, (err, exists) => { const exists = await PadManager.doesPadExist(newPadId);
if (exists) throw new Error('Cannot create a pad with that id as it already exists'); if (exists) throw new Error('Cannot create a pad with that id as it already exists');
});
PadManager.getPad(padId, (err, pad) => { const oldPad = await PadManager.getPad(padId);
oldPad = pad; const newPad = new Pad(newPadId);
newPad = new Pad(newPadId);
callback();
});
},
(callback) => {
// Clone all Chat revisions // Clone all Chat revisions
const chatHead = oldPad.chatHead; const chatHead = oldPad.chatHead;
for (let i = 0, curHeadNum = 0; i <= chatHead; i++) { await Promise.all([...Array(chatHead + 1).keys()].map(async (i) => {
db.db.get(`pad:${padId}:chat:${i}`, (err, chat) => { const chat = await db.get(`pad:${padId}:chat:${i}`);
db.db.set(`pad:${newPadId}:chat:${curHeadNum++}`, chat); await db.set(`pad:${newPadId}:chat:${i}`, chat);
console.log(`Created: Chat Revision: pad:${newPadId}:chat:${curHeadNum}`); console.log(`Created: Chat Revision: pad:${newPadId}:chat:${i}`);
}); }));
}
callback();
},
(callback) => {
// Rebuild Pad from revisions up to and including the new revision head // Rebuild Pad from revisions up to and including the new revision head
const AuthorManager = require('ep_etherpad-lite/node/db/AuthorManager'); const AuthorManager = require('ep_etherpad-lite/node/db/AuthorManager');
const Changeset = require('ep_etherpad-lite/static/js/Changeset'); const Changeset = require('ep_etherpad-lite/static/js/Changeset');
@ -73,23 +55,18 @@ async.series([
// and, AFAICT, cannot be recreated any other way // and, AFAICT, cannot be recreated any other way
newPad.pool.numToAttrib = oldPad.pool.numToAttrib; newPad.pool.numToAttrib = oldPad.pool.numToAttrib;
for (let curRevNum = 0; curRevNum <= newRevHead; curRevNum++) { for (let curRevNum = 0; curRevNum <= newRevHead; curRevNum++) {
db.db.get(`pad:${padId}:revs:${curRevNum}`, (err, rev) => { const rev = await db.get(`pad:${padId}:revs:${curRevNum}`);
if (rev.meta) { if (!rev || !rev.meta) throw new Error('The specified revision number could not be found.');
throw new Error('The specified revision number could not be found.');
}
const newRevNum = ++newPad.head; const newRevNum = ++newPad.head;
const newRevId = `pad:${newPad.id}:revs:${newRevNum}`; const newRevId = `pad:${newPad.id}:revs:${newRevNum}`;
db.db.set(newRevId, rev); await Promise.all([
AuthorManager.addPad(rev.meta.author, newPad.id); db.set(newRevId, rev),
AuthorManager.addPad(rev.meta.author, newPad.id),
]);
newPad.atext = Changeset.applyToAText(rev.changeset, newPad.atext, newPad.pool); newPad.atext = Changeset.applyToAText(rev.changeset, newPad.atext, newPad.pool);
console.log(`Created: Revision: pad:${newPad.id}:revs:${newRevNum}`); console.log(`Created: Revision: pad:${newPad.id}:revs:${newRevNum}`);
if (newRevNum === newRevHead) {
callback();
} }
});
}
},
(callback) => {
// Add saved revisions up to the new revision head // Add saved revisions up to the new revision head
console.log(newPad.head); console.log(newPad.head);
const newSavedRevisions = []; const newSavedRevisions = [];
@ -100,16 +77,13 @@ async.series([
} }
} }
newPad.savedRevisions = newSavedRevisions; newPad.savedRevisions = newSavedRevisions;
callback();
},
(callback) => {
// Save the source pad // Save the source pad
db.db.set(`pad:${newPadId}`, newPad, (err) => { await db.set(`pad:${newPadId}`, newPad);
console.log(`Created: Source Pad: pad:${newPadId}`); console.log(`Created: Source Pad: pad:${newPadId}`);
util.callbackify(newPad.saveToDatabase.bind(newPad))(callback); await newPad.saveToDatabase();
});
}, await db.shutdown();
], (err) => {
if (err) throw err;
console.info('finished'); console.info('finished');
}); })();

View file

@ -18,8 +18,10 @@ const padId = process.argv[2];
let valueCount = 0; let valueCount = 0;
const npm = require('ep_etherpad-lite/node_modules/npm'); const npm = require('ep_etherpad-lite/node_modules/npm');
npm.load({}, async (err) => { const util = require('util');
if (err) throw err;
(async () => {
await util.promisify(npm.load)({});
// intialize database // intialize database
require('ep_etherpad-lite/node/utils/Settings'); require('ep_etherpad-lite/node/utils/Settings');
@ -56,4 +58,4 @@ npm.load({}, async (err) => {
} }
console.info(`Finished: Replaced ${valueCount} values in the database`); console.info(`Finished: Replaced ${valueCount} values in the database`);
}); })();

8
package-lock.json generated
View file

@ -870,7 +870,8 @@
"tinycon": "0.0.1", "tinycon": "0.0.1",
"ueberdb2": "^1.2.5", "ueberdb2": "^1.2.5",
"underscore": "1.8.3", "underscore": "1.8.3",
"unorm": "1.4.1" "unorm": "1.4.1",
"wtfnode": "^0.8.4"
}, },
"dependencies": { "dependencies": {
"@apidevtools/json-schema-ref-parser": { "@apidevtools/json-schema-ref-parser": {
@ -10577,6 +10578,11 @@
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="
}, },
"wtfnode": {
"version": "0.8.4",
"resolved": "https://registry.npmjs.org/wtfnode/-/wtfnode-0.8.4.tgz",
"integrity": "sha512-64GEKtMt/MUBuAm+8kHqP74ojjafzu00aT0JKsmkIwYmjRQ/odO0yhbzKLm+Z9v1gMla+8dwITRKzTAlHsB+Og=="
},
"yallist": { "yallist": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",

View file

@ -27,6 +27,10 @@
const log4js = require('log4js'); const log4js = require('log4js');
log4js.replaceConsole(); 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 * early check for version compatibility before calling
* any modules that require newer versions of NodeJS * any modules that require newer versions of NodeJS
@ -44,31 +48,81 @@ const plugins = require('../static/js/pluginfw/plugins');
const settings = require('./utils/Settings'); const settings = require('./utils/Settings');
const util = require('util'); const util = require('util');
let started = false; const State = {
let stopped = false; INITIAL: 1,
STARTING: 2,
RUNNING: 3,
STOPPING: 4,
STOPPED: 5,
EXITING: 6,
WAITING_FOR_EXIT: 7,
};
let state = State.INITIAL;
const removeSignalListener = (signal, listener) => {
console.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);
};
const runningCallbacks = [];
exports.start = async () => { exports.start = async () => {
if (started) return express.server; switch (state) {
started = true; case State.INITIAL:
if (stopped) throw new Error('restart not supported'); break;
case State.STARTING:
await new Promise((resolve) => runningCallbacks.push(resolve));
// fall through
case State.RUNNING:
return express.server;
case State.STOPPING:
case State.STOPPED:
case State.EXITING:
case State.WAITING_FOR_EXIT:
throw new Error('restart not supported');
default:
throw new Error(`unknown State: ${state.toString()}`);
}
console.log('Starting Etherpad...');
state = State.STARTING;
// Check if Etherpad version is up-to-date // Check if Etherpad version is up-to-date
UpdateCheck.check(); UpdateCheck.check();
// start up stats counting system // start up stats counting system
const stats = require('./stats'); const stats = require('./stats');
let startDurations = {}; const startDurations = {};
stats.gauge('startDurations', () => startDurations); stats.gauge('startDurations', () => startDurations);
stats.gauge('memoryUsage', () => process.memoryUsage().rss); stats.gauge('memoryUsage', () => process.memoryUsage().rss);
stats.gauge('memoryUsageHeap', () => process.memoryUsage().heapUsed); stats.gauge('memoryUsageHeap', () => process.memoryUsage().heapUsed);
// Performance stats gauges process.on('uncaughtException', exports.exit);
// We use gauges because a reload might replace the value // 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);
});
}
const preNpmLoad = Date.now(); const preNpmLoad = Date.now();
await util.promisify(npm.load)(); await util.promisify(npm.load)();
startDurations.npmLoad = Date.now() - preNpmLoad; startDurations.npmLoad = Date.now() - preNpmLoad;
try {
const preDbInit = Date.now(); const preDbInit = Date.now();
await db.init(); await db.init();
startDurations.dbInit = Date.now() - preDbInit; startDurations.dbInit = Date.now() - preDbInit;
@ -85,70 +139,104 @@ exports.start = async () => {
await hooks.aCallAll('loadSettings', {settings}); await hooks.aCallAll('loadSettings', {settings});
startDurations.loadSettings = Date.now() - preLoadSettings; startDurations.loadSettings = Date.now() - preLoadSettings;
const preCreateServer = Date.now();
await hooks.aCallAll('createServer'); await hooks.aCallAll('createServer');
startDurations.createSettings = Date.now() - preCreateServer;
} catch (e) {
console.error(`exception thrown: ${e.message}`);
if (e.stack) console.log(e.stack);
process.exit(1);
}
process.on('uncaughtException', exports.exit); console.log('Etherpad is running');
state = State.RUNNING;
/* while (runningCallbacks.length > 0) setImmediate(runningCallbacks.pop());
* Connect graceful shutdown with sigint and uncaught exception
*
* Until Etherpad 1.7.5, process.on('SIGTERM') and process.on('SIGINT') were
* not hooked up under Windows, because old nodejs versions did not support
* them.
*
* According to nodejs 6.x documentation, it is now safe to do so. This
* allows to gracefully close the DB connection when hitting CTRL+C under
* Windows, for example.
*
* Source: https://nodejs.org/docs/latest-v6.x/api/process.html#process_signal_events
*
* - SIGTERM is not supported on Windows, it can be listened on.
* - SIGINT from the terminal is supported on all platforms, and can usually
* be generated with <Ctrl>+C (though this may be configurable). It is not
* generated when terminal raw mode is enabled.
*/
process.on('SIGINT', exports.exit);
// When running as PID1 (e.g. in docker container) allow graceful shutdown on SIGTERM c.f. #3265.
// Pass undefined to exports.exit because this is not an abnormal termination.
process.on('SIGTERM', () => exports.exit());
// Return the HTTP server to make it easier to write tests. // Return the HTTP server to make it easier to write tests.
return express.server; return express.server;
}; };
const stoppedCallbacks = [];
exports.stop = async () => { exports.stop = async () => {
if (stopped) return; switch (state) {
stopped = true; 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 new Promise((resolve) => stoppedCallbacks.push(resolve));
// fall through
case State.INITIAL:
case State.STOPPED:
case State.EXITING:
case State.WAITING_FOR_EXIT:
return;
default:
throw new Error(`unknown State: ${state.toString()}`);
}
console.log('Stopping Etherpad...'); console.log('Stopping Etherpad...');
await new Promise(async (resolve, reject) => { state = State.STOPPING;
const id = setTimeout(() => reject(new Error('Timed out waiting for shutdown tasks')), 3000); let timeout = null;
await hooks.aCallAll('shutdown'); await Promise.race([
clearTimeout(id); hooks.aCallAll('shutdown'),
resolve(); new Promise((resolve, reject) => {
}); timeout = setTimeout(() => reject(new Error('Timed out waiting for shutdown tasks')), 3000);
}),
]);
clearTimeout(timeout);
console.log('Etherpad stopped');
state = State.STOPPED;
while (stoppedCallbacks.length > 0) setImmediate(stoppedCallbacks.pop());
}; };
exports.exit = async (err) => { const exitCallbacks = [];
let exitCode = 0; let exitCalled = false;
if (err) { exports.exit = async (err = null) => {
exitCode = 1; /* eslint-disable no-process-exit */
console.error(err.stack ? err.stack : err); if (err === 'SIGTERM') {
// Termination from SIGTERM is not treated as an abnormal termination.
console.log('Received SIGTERM signal');
err = null;
} else if (err != null) {
console.error(err.stack || err.toString());
process.exitCode = 1;
if (exitCalled) {
console.error('Error occurred while waiting to exit. Forcing an immediate unclean exit...');
process.exit(1);
} }
try { }
exitCalled = true;
switch (state) {
case State.STARTING:
case State.RUNNING:
case State.STOPPING:
await exports.stop(); await exports.stop();
} catch (err) { // Don't fall through to State.STOPPED in case another caller is also waiting for stop().
exitCode = 1; // Don't pass err to exports.exit() because this err has already been processed. (If err is
console.error(err.stack ? err.stack : err); // 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:
break;
case State.EXITING:
await new Promise((resolve) => exitCallbacks.push(resolve));
// fall through
case State.WAITING_FOR_EXIT:
return;
default:
throw new Error(`unknown State: ${state.toString()}`);
} }
process.exit(exitCode); console.log('Exiting...');
state = State.EXITING;
while (exitCallbacks.length > 0) setImmediate(exitCallbacks.pop());
// 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(() => {
console.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();
console.error('Forcing an unclean exit...');
process.exit(1);
}, 5000).unref();
console.log('Waiting for Node.js to exit...');
state = State.WAITING_FOR_EXIT;
/* eslint-enable no-process-exit */
}; };
if (require.main === module) exports.start(); if (require.main === module) exports.start();

5
src/package-lock.json generated
View file

@ -8697,6 +8697,11 @@
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.2.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.2.tgz",
"integrity": "sha512-T4tewALS3+qsrpGI/8dqNMLIVdq/g/85U98HPMa6F0m6xTbvhXU6RCQLqPH3+SlomNV/LdY6RXEbBpMH6EOJnA==" "integrity": "sha512-T4tewALS3+qsrpGI/8dqNMLIVdq/g/85U98HPMa6F0m6xTbvhXU6RCQLqPH3+SlomNV/LdY6RXEbBpMH6EOJnA=="
}, },
"wtfnode": {
"version": "0.8.4",
"resolved": "https://registry.npmjs.org/wtfnode/-/wtfnode-0.8.4.tgz",
"integrity": "sha512-64GEKtMt/MUBuAm+8kHqP74ojjafzu00aT0JKsmkIwYmjRQ/odO0yhbzKLm+Z9v1gMla+8dwITRKzTAlHsB+Og=="
},
"xml2js": { "xml2js": {
"version": "0.4.23", "version": "0.4.23",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",

View file

@ -72,7 +72,8 @@
"tinycon": "0.0.1", "tinycon": "0.0.1",
"ueberdb2": "^1.2.5", "ueberdb2": "^1.2.5",
"underscore": "1.8.3", "underscore": "1.8.3",
"unorm": "1.4.1" "unorm": "1.4.1",
"wtfnode": "^0.8.4"
}, },
"bin": { "bin": {
"etherpad-lite": "node/server.js" "etherpad-lite": "node/server.js"

View file

@ -50,10 +50,10 @@ exports.init = async function () {
after(async function () { after(async function () {
webaccess.authnFailureDelayMs = backups.authnFailureDelayMs; webaccess.authnFailureDelayMs = backups.authnFailureDelayMs;
await server.stop();
// Note: This does not unset settings that were added. // Note: This does not unset settings that were added.
Object.assign(settings, backups.settings); Object.assign(settings, backups.settings);
log4js.setGlobalLogLevel(logLevel); log4js.setGlobalLogLevel(logLevel);
await server.exit();
}); });
return exports.agent; return exports.agent;