-
-
Notifications
You must be signed in to change notification settings - Fork 3k
Expand file tree
/
Copy pathserver.ts
More file actions
executable file
·317 lines (282 loc) · 10.6 KB
/
server.ts
File metadata and controls
executable file
·317 lines (282 loc) · 10.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
#!/usr/bin/env node
/**
* 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.
*/
import {PluginType} from "./types/Plugin";
import {ErrorCaused} from "./types/ErrorCaused";
import log4js from 'log4js';
import pkg from '../package.json';
import {checkForMigration} from "../static/js/pluginfw/installer";
import axios from "axios";
const settings = require('./utils/Settings');
let wtfnode: any;
if (settings.dumpOnUncleanExit) {
// 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.
wtfnode = require('wtfnode');
}
const addProxyToAxios = (url: URL) => {
axios.defaults.proxy = {
host: url.hostname,
auth: {
username: url.username,
password: url.password,
},
port: Number(url.port),
protocol: url.protocol,
}
}
if(process.env['http_proxy']) {
console.log("Using proxy: " + process.env['http_proxy'])
addProxyToAxios(new URL(process.env['http_proxy']));
}
if (process.env['https_proxy']) {
console.log("Using proxy: " + process.env['https_proxy'])
addProxyToAxios(new URL(process.env['https_proxy']));
}
/*
* early check for version compatibility before calling
* any modules that require newer versions of NodeJS
*/
const NodeVersion = require('./utils/NodeVersion');
NodeVersion.enforceMinNodeVersion(pkg.engines.node.replace(">=", ""));
NodeVersion.checkDeprecationStatus(pkg.engines.node.replace(">=", ""), '2.1.0');
const UpdateCheck = require('./utils/UpdateCheck');
const db = require('./db/DB');
const express = require('./hooks/express');
const hooks = require('../static/js/pluginfw/hooks');
const pluginDefs = require('../static/js/pluginfw/plugin_defs');
const plugins = require('../static/js/pluginfw/plugins');
import {Gate} from './utils/promises';
const stats = require('./stats')
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;
const removeSignalListener = (signal: NodeJS.Signals, listener: NodeJS.SignalsListener) => {
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: Gate<unknown>
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();
// @ts-ignore
stats.gauge('memoryUsage', () => process.memoryUsage().rss);
// @ts-ignore
stats.gauge('memoryUsageHeap', () => process.memoryUsage().heapUsed);
process.on('uncaughtException', (err: ErrorCaused) => {
logger.debug(`uncaught exception: ${err.stack || err}`);
// eslint-disable-next-line promise/no-promise-in-callback
exports.exit(err)
.catch((err: ErrorCaused) => {
logger.error('Error in process exit', err);
// eslint-disable-next-line n/no-process-exit
process.exit(1);
});
});
// 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: ErrorCaused) => {
logger.debug(`unhandled rejection: ${err.stack || err}`);
throw err;
});
for (const signal of ['SIGINT', 'SIGTERM'] as NodeJS.Signals[]) {
// 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 db.init();
await checkForMigration();
await plugins.update();
const installedPlugins = (Object.values(pluginDefs.plugins) as PluginType[])
.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 server-side hooks:\n${plugins.formatHooks('hooks', false)}`);
await hooks.aCallAll('loadSettings', {settings});
await hooks.aCallAll('createServer');
} catch (err) {
logger.error('Error occurred while starting Etherpad');
state = State.STATE_TRANSITION_FAILED;
// @ts-ignore
startDoneGate.resolve();
return await exports.exit(err);
}
logger.info('Etherpad is running');
state = State.RUNNING;
// @ts-ignore
startDoneGate.resolve();
// Return the HTTP server to make it easier to write tests.
return express.server;
};
const stopDoneGate = new Gate();
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...');
state = State.STOPPING;
try {
let timeout: NodeJS.Timeout = null as unknown as NodeJS.Timeout;
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;
// @ts-ignore
stopDoneGate.resolve();
return await exports.exit(err);
}
logger.info('Etherpad stopped');
state = State.STOPPED;
// @ts-ignore
stopDoneGate.resolve();
};
let exitGate: any;
let exitCalled = false;
exports.exit = async (err: ErrorCaused|string|null = 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 (typeof err == "object" && err != null) {
logger.error(`Metrics at time of fatal error:\n${JSON.stringify(stats.toJSON(), null, 2)}`);
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);
}
}
if (!exitCalled) logger.info('Exiting...');
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()}`);
}
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');
if (settings.dumpOnUncleanExit) {
wtfnode.dump();
} else {
logger.error('Enable `dumpOnUncleanExit` setting to get a dump of objects preventing a ' +
'clean exit');
}
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();
// @ts-ignore
if (typeof(PhusionPassenger) !== 'undefined') exports.start();