diff --git a/server/core/AppLogger.js b/server/core/AppLogger.js index af6799ca..68c8e1fb 100644 --- a/server/core/AppLogger.js +++ b/server/core/AppLogger.js @@ -7,10 +7,14 @@ let instance = null; class AppLogger { constructor() { if (!instance) { + this.inited = false; + this.logFileName = ''; + this.errLogFileName = ''; + this.fatalLogFileName = ''; + instance = this; } - this.inited = false; return instance; } @@ -22,11 +26,16 @@ class AppLogger { if (config.loggingEnabled) { await fs.ensureDir(config.logDir); + + this.logFileName = `${config.logDir}/${config.name}.log`; + this.errLogFileName = `${config.logDir}/${config.name}.err.log`; + this.fatalLogFileName = `${config.logDir}/${config.name}.fatal.log`; + loggerParams = [ {log: 'ConsoleLog'}, - {log: 'FileLog', fileName: `${config.logDir}/${config.name}.log`}, - {log: 'FileLog', fileName: `${config.logDir}/${config.name}.err.log`, exclude: [LM_OK, LM_INFO, LM_TOTAL]}, - {log: 'FileLog', fileName: `${config.logDir}/${config.name}.fatal.log`, exclude: [LM_OK, LM_INFO, LM_WARN, LM_ERR, LM_TOTAL]},//LM_FATAL only + {log: 'FileLog', fileName: this.logFileName}, + {log: 'FileLog', fileName: this.errLogFileName, exclude: [LM_OK, LM_INFO, LM_TOTAL]}, + {log: 'FileLog', fileName: this.fatalLogFileName, exclude: [LM_OK, LM_INFO, LM_WARN, LM_ERR, LM_TOTAL]},//LM_FATAL only ]; } diff --git a/server/core/AsyncExit.js b/server/core/AsyncExit.js new file mode 100644 index 00000000..d2b61594 --- /dev/null +++ b/server/core/AsyncExit.js @@ -0,0 +1,111 @@ +let instance = null; + +const defaultTimeout = 15*1000;//15 sec +const exitSignals = ['SIGINT', 'SIGTERM', 'SIGBREAK', 'SIGHUP', 'uncaughtException']; + +//singleton +class AsyncExit { + constructor() { + if (!instance) { + this.onSignalCallbacks = new Map(); + this.callbacks = new Map(); + this.afterCallbacks = new Map(); + this.exitTimeout = defaultTimeout; + this.inited = false; + instance = this; + } + + return instance; + } + + init(signals = null, codeOnSignal = 2) { + if (this.inited) + throw new Error('AsyncExit: initialized already'); + + if (!signals) + signals = exitSignals; + + const runSingalCallbacks = async(signal) => { + for (const signalCallback of this.onSignalCallbacks.keys()) { + try { + await signalCallback(signal); + } catch(e) { + console.error(e); + } + } + }; + + for (const signal of signals) { + process.once(signal, async() => { + await runSingalCallbacks(signal); + this.exit(codeOnSignal); + }); + } + + this.inited = true; + } + + onSignal(signalCallback) { + if (!this.onSignalCallbacks.has(signalCallback)) { + this.onSignalCallbacks.set(signalCallback, true); + } + } + + add(exitCallback) { + if (!this.callbacks.has(exitCallback)) { + this.callbacks.set(exitCallback, true); + } + } + + addAfter(exitCallback) { + if (!this.afterCallbacks.has(exitCallback)) { + this.afterCallbacks.set(exitCallback, true); + } + } + + remove(exitCallback) { + if (this.callbacks.has(exitCallback)) { + this.callbacks.delete(exitCallback); + } + if (this.afterCallbacks.has(exitCallback)) { + this.afterCallbacks.delete(exitCallback); + } + } + + setExitTimeout(timeout) { + this.exitTimeout = timeout; + } + + exit(code = 0) { + if (this.exiting) + return; + + this.exiting = true; + + const timer = setTimeout(() => { process.exit(code); }, this.exitTimeout); + + (async() => { + for (const exitCallback of this.callbacks.keys()) { + try { + await exitCallback(); + } catch(e) { + console.error(e); + } + } + + for (const exitCallback of this.afterCallbacks.keys()) { + try { + await exitCallback(); + } catch(e) { + console.error(e); + } + } + + clearTimeout(timer); + //console.log('Exited gracefully'); + process.exit(code); + })(); + } +} + +module.exports = AsyncExit; diff --git a/server/core/LibSharedStorage/MegaStorage.js b/server/core/LibSharedStorage/MegaStorage.js index 718534d3..08091f2b 100644 --- a/server/core/LibSharedStorage/MegaStorage.js +++ b/server/core/LibSharedStorage/MegaStorage.js @@ -25,7 +25,6 @@ class MegaStorage { this.debouncedSaveStats = _.debounce(() => { this.saveStats().catch((e) => { log(LM_ERR, `MegaStorage::saveStats ${e.message}`); - //process.exit(1); }); }, 5000, {'maxWait':6000}); diff --git a/server/core/Logger.js b/server/core/Logger.js index db5d122b..c095bdfa 100644 --- a/server/core/Logger.js +++ b/server/core/Logger.js @@ -2,6 +2,9 @@ Журналирование с буферизацией вывода */ const fs = require('fs-extra'); +const ayncExit = new (require('./AsyncExit'))(); + +const sleep = (ms) => { return new Promise(resolve => setTimeout(resolve, ms)) }; global.LM_OK = 0; global.LM_INFO = 1; @@ -46,12 +49,13 @@ class BaseLog { this.outputBuffer = []; await this.flushImpl(this.data) - .catch(e => { console.log(e); process.exit(1); } ); + .catch(e => { console.log(e); ayncExit.exit(1); } ); this.flushing = false; } log(msgType, message) { - if (this.closed) { console.log(`Logger fatal error: log was closed (message to log: ${message}})`); process.exit(1); } + if (this.closed) + return; if (!this.exclude.has(msgType)) { this.outputBuffer.push(message); @@ -73,7 +77,7 @@ class BaseLog { } } - close() { + async close() { if (this.closed) return; @@ -81,12 +85,13 @@ class BaseLog { clearInterval(this.iid); try { - if (this.flushing) - this.flushImplSync(this.data); - this.flushImplSync(this.outputBuffer); + while (this.outputBufferLength) { + await this.flush(); + await sleep(1); + } } catch(e) { console.log(e); - process.exit(1); + ayncExit.exit(1); } this.outputBufferLength = 0; this.outputBuffer = []; @@ -103,12 +108,14 @@ class FileLog extends BaseLog { this.rcid = 0; } - close() { + async close() { if (this.closed) return; - super.close(); - if (this.fd) - fs.closeSync(this.fd); + await super.close(); + if (this.fd) { + await fs.close(this.fd); + this.fd = null; + } if (this.rcid) clearTimeout(this.rcid); } @@ -151,23 +158,15 @@ class FileLog extends BaseLog { }, LOG_ROTATE_FILE_CHECK_INTERVAL); } - await fs.write(this.fd, Buffer.from(data.join(''))); + if (this.fd) + await fs.write(this.fd, Buffer.from(data.join(''))); } - - flushImplSync(data) { - fs.writeSync(this.fd, Buffer.from(data.join(''))); - } - } class ConsoleLog extends BaseLog { async flushImpl(data) { process.stdout.write(data.join('')); } - - flushImplSync(data) { - process.stdout.write(data.join('')); - } } //------------------------------------------------------------------ @@ -178,7 +177,7 @@ const factory = { class Logger { - constructor(params = null, cleanupCallback = null) { + constructor(params = null) { this.handlers = []; if (params) { params.forEach((logParams) => { @@ -187,12 +186,22 @@ class Logger { this.handlers.push(new loggerClass(logParams)); }); } - cleanupCallback = cleanupCallback || (() => {}); - this.cleanup(cleanupCallback); + + this.closed = false; + ayncExit.onSignal((signal) => { + this.log(LM_FATAL, `Signal ${signal} received, exiting...`); + }); + ayncExit.addAfter(this.close.bind(this)); + } + + formatDate(date) { + return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ` + + `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}:${date.getSeconds().toString().padStart(2, '0')}.` + + `${date.getMilliseconds().toString().padStart(3, '0')}`; } prepareMessage(msgType, message) { - return (new Date().toISOString()) + ` ${msgTypeToStr[msgType]}: ${message}\n`; + return this.formatDate(new Date()) + ` ${msgTypeToStr[msgType]}: ${message}\n`; } log(msgType, message) { @@ -203,47 +212,18 @@ class Logger { const mes = this.prepareMessage(msgType, message); - for (let i = 0; i < this.handlers.length; i++) - this.handlers[i].log(msgType, mes); + if (!this.closed) { + for (let i = 0; i < this.handlers.length; i++) + this.handlers[i].log(msgType, mes); + } else { + console.log(mes); + } } - close() { + async close() { for (let i = 0; i < this.handlers.length; i++) - this.handlers[i].close(); - } - - cleanup(callback) { - // attach user callback to the process event emitter - // if no callback, it will still exit gracefully on Ctrl-C - callback = callback || (() => {}); - process.on('cleanup', callback); - - // do app specific cleaning before exiting - process.on('exit', () => { - this.close(); - process.emit('cleanup'); - }); - - // catch ctrl+c event and exit normally - process.on('SIGINT', () => { - this.log(LM_FATAL, 'Ctrl-C pressed, exiting...'); - process.exit(2); - }); - - process.on('SIGTERM', () => { - this.log(LM_FATAL, 'Kill signal, exiting...'); - process.exit(2); - }); - - //catch uncaught exceptions, trace, then exit normally - process.on('uncaughtException', e => { - try { - this.log(LM_FATAL, e.stack); - } catch (e) { - console.log(e.stack); - } - process.exit(99); - }); + await this.handlers[i].close(); + this.closed = true; } } diff --git a/server/index.js b/server/index.js index 4990a86c..c4b8a408 100644 --- a/server/index.js +++ b/server/index.js @@ -7,6 +7,11 @@ const compression = require('compression'); const http = require('http'); const WebSocket = require ('ws'); +const ayncExit = new (require('./core/AsyncExit'))(); +ayncExit.init(); + +let log = null; + async function init() { //config const configManager = new (require('./config'))();//singleton @@ -18,7 +23,7 @@ async function init() { //logger const appLogger = new (require('./core/AppLogger'))();//singleton await appLogger.init(config); - const log = appLogger.log; + log = appLogger.log; //dirs log(`${config.name} v${config.version}, Node.js ${process.version}`); @@ -96,13 +101,15 @@ async function main() { } } - (async() => { try { await init(); await main(); } catch (e) { - console.error(e); - process.exit(1); + if (log) + log(LM_FATAL, e); + else + console.error(e); + ayncExit.exit(1); } -})(); \ No newline at end of file +})();