diff --git a/server/core/Logger.js b/server/core/Logger.js new file mode 100644 index 00000000..8c74e2d0 --- /dev/null +++ b/server/core/Logger.js @@ -0,0 +1,255 @@ +/* + Журналирование с буферизацией вывода +*/ + +const Promise = require('bluebird'); +const fs = Promise.promisifyAll(require('fs')); + +global.LM_OK = 0; +global.LM_INFO = 1; +global.LM_WARN = 2; +global.LM_ERR = 3; +global.LM_FATAL = 4; +global.LM_TOTAL = 5; + +const LOG_CACHE_BUFFER_SIZE = 8192; +const LOG_BUFFER_FLUSH_INTERVAL = 200; + +const LOG_ROTATE_FILE_LENGTH = 10000000; +const LOG_ROTATE_FILE_DEPTH = 9; +const LOG_ROTATE_FILE_CHECK_INTERVAL = 60000; + +let msgTypeToStr = { + [LM_OK]: ' OK', + [LM_INFO]: ' INFO', + [LM_WARN]: ' WARN', + [LM_ERR]: 'ERROR', + [LM_FATAL]: 'FATAL ERROR', + [LM_TOTAL]: 'TOTAL' +}; + +class BaseLog { + + constructor(params) { + this.params = params; + this.exclude = new Set(params.exclude); + this.outputBufferLength = 0; + this.outputBuffer = []; + this.flushing = false; + } + + async flush() { + if (this.flushing || !this.outputBufferLength) + return; + this.flushing = true; + + this.data = this.outputBuffer; + this.outputBufferLength = 0; + this.outputBuffer = []; + + await this.flushImpl(this.data) + .catch(e => { console.log(e); process.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.exclude.has(msgType)) { + this.outputBuffer.push(message); + this.outputBufferLength += message.length; + + if (this.outputBufferLength >= LOG_CACHE_BUFFER_SIZE && !this.flushing) { + this.flush(); + } + + if (!this.iid) { + this.iid = setInterval(() => { + if (!this.flushing) { + clearInterval(this.iid); + this.iid = 0; + this.flush(); + } + }, LOG_BUFFER_FLUSH_INTERVAL); + } + } + } + + close() { + if (this.closed) + return; + + if (this.iid) + clearInterval(this.iid); + + try { + if (this.flushing) + this.flushImplSync(this.data); + this.flushImplSync(this.outputBuffer); + } catch(e) { + console.log(e); + process.exit(1); + } + this.outputBufferLength = 0; + this.outputBuffer = []; + this.closed = true; + } +} + +class FileLog extends BaseLog { + + constructor(params) { + super(params); + this.fileName = params.fileName; + this.fd = fs.openSync(this.fileName, 'a'); + this.rcid = 0; + } + + close() { + if (this.closed) + return; + super.close(); + if (this.fd) + fs.closeSync(this.fd); + if (this.rcid) + clearTimeout(this.rcid); + } + + async rotateFile(fileName, i) { + let fn = fileName; + if (i > 0) + fn += `.${i}`; + let tn = fileName + '.' + (i + 1); + let exists = await fs.accessAsync(tn).then(() => true).catch(() => false); + if (exists) { + if (i >= LOG_ROTATE_FILE_DEPTH - 1) { + await fs.unlinkAsync(tn); + } else { + await this.rotateFile(fileName, i + 1); + } + } + await fs.renameAsync(fn, tn); + } + + async doFileRotationIfNeeded() { + this.rcid = 0; + + let stat = await fs.fstatAsync(this.fd); + if (stat.size > LOG_ROTATE_FILE_LENGTH) { + await fs.closeAsync(this.fd); + await this.rotateFile(this.fileName, 0); + this.fd = await fs.openAsync(this.fileName, "a"); + } + } + + async flushImpl(data) { + if (this.closed) + return; + + if (!this.rcid) { + await this.doFileRotationIfNeeded(); + this.rcid = setTimeout(() => { + this.rcid = 0; + }, LOG_ROTATE_FILE_CHECK_INTERVAL); + }; + + await fs.writeAsync(this.fd, new Buffer(data.join(''))); + } + + flushImplSync(data) { + fs.writeSync(this.fd, new Buffer(data.join(''))); + } + +} + +class ConsoleLog extends BaseLog { + async flushImpl(data) { + process.stdout.write(data.join('')); + } + + flushImplSync(data) { + process.stdout.write(data.join('')); + } +} + +//------------------------------------------------------------------ +const factory = { + ConsoleLog, + FileLog, +}; + +class Logger { + + constructor(params = null, cleanupCallback = null) { + this.handlers = []; + if (params) { + params.forEach((logParams) => { + let className = logParams.log; + let loggerClass = factory[className]; + this.handlers.push(new loggerClass(logParams)); + }); + } else { + this.handlers.push(new DummyLog); + } + + cleanupCallback = cleanupCallback || (() => {}); + this.cleanup(cleanupCallback); + } + + prepareMessage(msgType, message) { + return (new Date().toISOString()) + ` ${msgTypeToStr[msgType]}: ${message}\n`; + } + + log(msgType, message) { + if (message == null) { + message = msgType; + msgType = LM_INFO; + } + + const mes = this.prepareMessage(msgType, message); + + for (let i = 0; i < this.handlers.length; i++) + this.handlers[i].log(msgType, mes); + } + + 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_WARN, 'Ctrl-C pressed, exiting...'); + process.exit(2); + }); + + process.on('SIGTERM', () => { + this.log(LM_WARN, '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); + }); + } +} + +module.exports = Logger; \ No newline at end of file diff --git a/server/core/SqliteConnectionPool.js b/server/core/SqliteConnectionPool.js new file mode 100644 index 00000000..b3d64d0f --- /dev/null +++ b/server/core/SqliteConnectionPool.js @@ -0,0 +1,95 @@ +const Promise = require('bluebird'); +const utils = require('./utils'); +const sqlite = require('sqlite'); + +const waitingDelay = 100; //ms + +class SqliteConnectionPool { + constructor(connCount, logger, config) { + this.config = config; + this.connCount = connCount; + } + + async init() { + this.logger.log('Opening database'); + + utils.mkDirIfNotExistsSync(config.dataDir); + const dbFileName = config.dataDir + '/' + config.dbFileName; + + this.connections = []; + this.taken = new Set(); + this.freed = new Set(); + + for (let i = 0; i < this.connCount; i++) { + + let client = await sqlite.open(dbFileName); + client.configure('busyTimeout', 10000); //ms + + client.ret = () => { + this.taken.delete(i); + this.freed.add(i); + }; + + this.freed.add(i); + this.connections[i] = client; + } + } + + _setImmediate() { + return new Promise((resolve) => { + setImmediate(() => { + return resolve(); + }); + }); + } + + async get() { + if (this.closed) + return; + + let freeConnIndex = this.freed.values().next().value; + if (freeConnIndex == null) { + if (waitingDelay) + await utils.sleep(waitingDelay); + return await this._setImmediate().then(() => this.get()); + } + + this.freed.delete(freeConnIndex); + this.taken.add(freeConnIndex); + + return this.connections[freeConnIndex]; + } + + async run(query) { + const dbh = await this.get(); + try { + let result = await dbh.run(query); + dbh.ret(); + return result; + } catch (e) { + dbh.ret(); + throw e; + } + } + + async all(query) { + const dbh = await this.get(); + try { + let result = await dbh.all(query); + dbh.ret(); + return result; + } catch (e) { + dbh.ret(); + throw e; + } + } + + async close() { + for (let i = 0; i < this.connections.length; i++) { + await this.connections[i].close(); + } + this.closed = true; + } +} + +module.exports = SqliteConnectionPool; \ No newline at end of file diff --git a/server/core/loggerInit.js b/server/core/loggerInit.js new file mode 100644 index 00000000..200cf86e --- /dev/null +++ b/server/core/loggerInit.js @@ -0,0 +1,16 @@ +const utils = require('./utils'); +const Logger = require('./Logger'); + +module.exports = function(config) { + let loggerParams = null; + + if (config.loggingEnabled) { + utils.mkDirIfNotExistsSync(config.logDir); + loggerParams = [ + {log: 'ConsoleLog'}, + {log: 'FileLog', fileName: `${config.logDir}/${config.name}.log`}, + ]; + } + + return new Logger(loggerParams); +} \ No newline at end of file diff --git a/server/core/utils.js b/server/core/utils.js new file mode 100644 index 00000000..38f793b6 --- /dev/null +++ b/server/core/utils.js @@ -0,0 +1,29 @@ +const Promise = require('bluebird'); +const fs = require('fs'); + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +function statPathSync(path) { + try { + return fs.statSync(path); + } catch (ex) {} + return false; +} + +function mkDirIfNotExistsSync(path) { + console.log(path); + let exists = statPathSync(path); + if (!exists) { + fs.mkdirSync(path, {recursive: true, mode: 0o755}); + } else if (!exists.isDirectory()) { + throw new Error(`Not a directory: ${path}`); + } +} + +module.exports = { + sleep, + statPathSync, + mkDirIfNotExistsSync, +}; \ No newline at end of file