Начата работа над логикой сервера
This commit is contained in:
255
server/core/Logger.js
Normal file
255
server/core/Logger.js
Normal file
@@ -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
server/core/SqliteConnectionPool.js
Normal file
95
server/core/SqliteConnectionPool.js
Normal file
@@ -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
server/core/loggerInit.js
Normal file
16
server/core/loggerInit.js
Normal file
@@ -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
server/core/utils.js
Normal file
29
server/core/utils.js
Normal file
@@ -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,
|
||||
};
|
||||
Reference in New Issue
Block a user