Преглед на файлове

Начата работа над логикой сервера

Book Pauk преди 6 години
родител
ревизия
3c5d32cd72
променени са 4 файла, в които са добавени 395 реда и са изтрити 0 реда
  1. 255 0
      server/core/Logger.js
  2. 95 0
      server/core/SqliteConnectionPool.js
  3. 16 0
      server/core/loggerInit.js
  4. 29 0
      server/core/utils.js

+ 255 - 0
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;

+ 95 - 0
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;

+ 16 - 0
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);
+}

+ 29 - 0
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,
+};