Начальная структура директорий, каркас проекта
This commit is contained in:
15
build/linux.js
Normal file
15
build/linux.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
const fs = require('fs-extra');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const distDir = path.resolve(__dirname, '../dist');
|
||||||
|
const publicDir = `${distDir}/tmp/public`;
|
||||||
|
const outDir = `${distDir}/linux`;
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
await fs.emptyDir(outDir);
|
||||||
|
// перемещаем public на место
|
||||||
|
if (await fs.pathExists(publicDir))
|
||||||
|
await fs.move(publicDir, `${outDir}/public`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
72
build/webpack.base.config.js
Normal file
72
build/webpack.base.config.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
const path = require('path');
|
||||||
|
const DefinePlugin = require('webpack').DefinePlugin;
|
||||||
|
const { VueLoaderPlugin } = require('vue-loader');
|
||||||
|
|
||||||
|
const clientDir = path.resolve(__dirname, '../client');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
ws: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
entry: [`${clientDir}/main.js`],
|
||||||
|
output: {
|
||||||
|
publicPath: '/app/',
|
||||||
|
},
|
||||||
|
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.vue$/,
|
||||||
|
loader: 'vue-loader',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
resourceQuery: /^\?vue/,
|
||||||
|
use: path.resolve(__dirname, 'includer.js')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.js$/,
|
||||||
|
loader: 'babel-loader',
|
||||||
|
exclude: /node_modules/,
|
||||||
|
options: {
|
||||||
|
presets: [['@babel/preset-env', { targets: { esmodules: true } }]],
|
||||||
|
plugins: [
|
||||||
|
['@babel/plugin-proposal-decorators', { legacy: true }]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(gif|png)$/,
|
||||||
|
type: 'asset/inline',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.jpg$/,
|
||||||
|
type: 'asset/resource',
|
||||||
|
generator: {
|
||||||
|
filename: 'images/[name]-[hash:6][ext]'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
test: /\.(ttf|eot|woff|woff2)$/,
|
||||||
|
type: 'asset/resource',
|
||||||
|
generator: {
|
||||||
|
filename: 'fonts/[name]-[hash:6][ext]'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
plugins: [
|
||||||
|
new DefinePlugin({
|
||||||
|
__VUE_OPTIONS_API__: true,
|
||||||
|
__VUE_PROD_DEVTOOLS__: false,
|
||||||
|
__QUASAR_SSR__: false,
|
||||||
|
__QUASAR_SSR_SERVER__: false,
|
||||||
|
__QUASAR_SSR_CLIENT__: false,
|
||||||
|
__QUASAR_VERSION__: false,
|
||||||
|
}),
|
||||||
|
new VueLoaderPlugin(),
|
||||||
|
]
|
||||||
|
};
|
||||||
43
build/webpack.dev.config.js
Normal file
43
build/webpack.dev.config.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
const path = require('path');
|
||||||
|
const webpack = require('webpack');
|
||||||
|
|
||||||
|
const { merge } = require('webpack-merge');
|
||||||
|
const baseWpConfig = require('./webpack.base.config');
|
||||||
|
|
||||||
|
baseWpConfig.entry.unshift('webpack-hot-middleware/client');
|
||||||
|
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
|
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||||
|
|
||||||
|
const publicDir = path.resolve(__dirname, '../server/public');
|
||||||
|
const clientDir = path.resolve(__dirname, '../client');
|
||||||
|
|
||||||
|
module.exports = merge(baseWpConfig, {
|
||||||
|
mode: 'development',
|
||||||
|
devtool: 'inline-source-map',
|
||||||
|
output: {
|
||||||
|
path: `${publicDir}/app`,
|
||||||
|
filename: 'bundle.js'
|
||||||
|
},
|
||||||
|
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.css$/,
|
||||||
|
use: [
|
||||||
|
'vue-style-loader',
|
||||||
|
'css-loader'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
plugins: [
|
||||||
|
new webpack.HotModuleReplacementPlugin(),
|
||||||
|
new webpack.NoEmitOnErrorsPlugin(),
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
template: `${clientDir}/index.html.template`,
|
||||||
|
filename: `${publicDir}/index.html`
|
||||||
|
}),
|
||||||
|
new CopyWebpackPlugin({patterns: [{from: `${clientDir}/assets/*`, to: `${publicDir}/`}]})
|
||||||
|
]
|
||||||
|
});
|
||||||
58
build/webpack.prod.config.js
Normal file
58
build/webpack.prod.config.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
const path = require('path');
|
||||||
|
//const webpack = require('webpack');
|
||||||
|
|
||||||
|
const { merge } = require('webpack-merge');
|
||||||
|
const baseWpConfig = require('./webpack.base.config');
|
||||||
|
const TerserPlugin = require('terser-webpack-plugin');
|
||||||
|
const CssMinimizerWebpackPlugin = require('css-minimizer-webpack-plugin');
|
||||||
|
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||||
|
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
|
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||||
|
|
||||||
|
const publicDir = path.resolve(__dirname, '../dist/tmp/public');
|
||||||
|
const clientDir = path.resolve(__dirname, '../client');
|
||||||
|
|
||||||
|
module.exports = merge(baseWpConfig, {
|
||||||
|
mode: 'production',
|
||||||
|
output: {
|
||||||
|
path: `${publicDir}/app_new`,
|
||||||
|
filename: 'bundle.[contenthash].js'
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.css$/,
|
||||||
|
use: [
|
||||||
|
MiniCssExtractPlugin.loader,
|
||||||
|
'css-loader'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
optimization: {
|
||||||
|
minimizer: [
|
||||||
|
new TerserPlugin({
|
||||||
|
parallel: true,
|
||||||
|
terserOptions: {
|
||||||
|
format: {
|
||||||
|
comments: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
new CssMinimizerWebpackPlugin()
|
||||||
|
]
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
//new CleanWebpackPlugin({ cleanOnceBeforeBuildPatterns: [`${publicDir}/**`] }),
|
||||||
|
new MiniCssExtractPlugin({
|
||||||
|
filename: "[name].[contenthash].css"
|
||||||
|
}),
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
template: `${clientDir}/index.html.template`,
|
||||||
|
filename: `${publicDir}/index.html`
|
||||||
|
}),
|
||||||
|
new CopyWebpackPlugin({patterns:
|
||||||
|
[{from: `${clientDir}/assets/*`, to: `${publicDir}/`, context: `${clientDir}/assets` }]
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
});
|
||||||
15
build/win.js
Normal file
15
build/win.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
const fs = require('fs-extra');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const distDir = path.resolve(__dirname, '../dist');
|
||||||
|
const publicDir = `${distDir}/tmp/public`;
|
||||||
|
const outDir = `${distDir}/win`;
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
await fs.emptyDir(outDir);
|
||||||
|
// перемещаем public на место
|
||||||
|
if (await fs.pathExists(publicDir))
|
||||||
|
await fs.move(publicDir, `${outDir}/public`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
15249
package-lock.json
generated
Normal file
15249
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
1
server/config/application_env
Normal file
1
server/config/application_env
Normal file
@@ -0,0 +1 @@
|
|||||||
|
development
|
||||||
28
server/config/base.js
Normal file
28
server/config/base.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
const path = require('path');
|
||||||
|
const pckg = require('../../package.json');
|
||||||
|
|
||||||
|
const execDir = path.resolve(__dirname, '..');
|
||||||
|
const dataDir = `${execDir}/.${pckg.name}/data`;
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
branch: 'unknown',
|
||||||
|
version: pckg.version,
|
||||||
|
name: pckg.name,
|
||||||
|
|
||||||
|
dataDir,
|
||||||
|
tempDir: `${dataDir}/tmp`,
|
||||||
|
logDir: `${dataDir}/log`,
|
||||||
|
publicDir: `${dataDir}/public`,
|
||||||
|
|
||||||
|
loggingEnabled: true,
|
||||||
|
|
||||||
|
maxFilesDirSize: 1024*1024*1024,//1Gb
|
||||||
|
|
||||||
|
webConfigParams: ['name', 'version', 'branch'],
|
||||||
|
|
||||||
|
server: {
|
||||||
|
ip: '0.0.0.0',
|
||||||
|
port: '22380',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
5
server/config/development.js
Normal file
5
server/config/development.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
const base = require('./base');
|
||||||
|
|
||||||
|
module.exports = Object.assign({}, base, {
|
||||||
|
branch: 'development',
|
||||||
|
});
|
||||||
89
server/config/index.js
Normal file
89
server/config/index.js
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
const _ = require('lodash');
|
||||||
|
const fs = require('fs-extra');
|
||||||
|
|
||||||
|
const branchFilename = __dirname + '/application_env';
|
||||||
|
|
||||||
|
const propsToSave = [
|
||||||
|
'loggingEnabled',
|
||||||
|
'maxFilesDirSize',
|
||||||
|
'server',
|
||||||
|
];
|
||||||
|
|
||||||
|
let instance = null;
|
||||||
|
|
||||||
|
//singleton
|
||||||
|
class ConfigManager {
|
||||||
|
constructor() {
|
||||||
|
if (!instance) {
|
||||||
|
this.inited = false;
|
||||||
|
|
||||||
|
instance = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
if (this.inited)
|
||||||
|
throw new Error('already inited');
|
||||||
|
|
||||||
|
this.branch = 'production';
|
||||||
|
try {
|
||||||
|
await fs.access(branchFilename);
|
||||||
|
this.branch = (await fs.readFile(branchFilename, 'utf8')).trim();
|
||||||
|
} catch (err) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
process.env.NODE_ENV = this.branch;
|
||||||
|
|
||||||
|
this.branchConfigFile = __dirname + `/${this.branch}.js`;
|
||||||
|
this._config = require(this.branchConfigFile);
|
||||||
|
|
||||||
|
await fs.ensureDir(this._config.dataDir);
|
||||||
|
this._userConfigFile = `${this._config.dataDir}/config.json`;
|
||||||
|
|
||||||
|
this.inited = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
get config() {
|
||||||
|
if (!this.inited)
|
||||||
|
throw new Error('not inited');
|
||||||
|
return _.cloneDeep(this._config);
|
||||||
|
}
|
||||||
|
|
||||||
|
set config(value) {
|
||||||
|
Object.assign(this._config, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
get userConfigFile() {
|
||||||
|
return this._userConfigFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
set userConfigFile(value) {
|
||||||
|
if (value)
|
||||||
|
this._userConfigFile = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
if (!this.inited)
|
||||||
|
throw new Error('not inited');
|
||||||
|
if (!await fs.pathExists(this.userConfigFile)) {
|
||||||
|
await this.save();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await fs.readFile(this.userConfigFile, 'utf8');
|
||||||
|
this.config = JSON.parse(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async save() {
|
||||||
|
if (!this.inited)
|
||||||
|
throw new Error('not inited');
|
||||||
|
|
||||||
|
const dataToSave = _.pick(this._config, propsToSave);
|
||||||
|
await fs.writeFile(this.userConfigFile, JSON.stringify(dataToSave, null, 4));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ConfigManager;
|
||||||
20
server/config/production.js
Normal file
20
server/config/production.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
const path = require('path');
|
||||||
|
const pckg = require('../../package.json');
|
||||||
|
const base = require('./base');
|
||||||
|
|
||||||
|
const execDir = path.dirname(process.execPath);
|
||||||
|
const dataDir = `${execDir}/.${pckg.name}/data`;
|
||||||
|
|
||||||
|
module.exports = Object.assign({}, base, {
|
||||||
|
branch: 'production',
|
||||||
|
dataDir,
|
||||||
|
tempDir: `${dataDir}/tmp`,
|
||||||
|
logDir: `${dataDir}/log`,
|
||||||
|
publicDir: `${dataDir}/public`,
|
||||||
|
|
||||||
|
server: {
|
||||||
|
ip: '0.0.0.0',
|
||||||
|
port: '12380',
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
105
server/controllers/WebSocketController.js
Normal file
105
server/controllers/WebSocketController.js
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
const WebSocket = require ('ws');
|
||||||
|
const _ = require('lodash');
|
||||||
|
|
||||||
|
const log = new (require('../core/AppLogger'))().log;//singleton
|
||||||
|
//const utils = require('../core/utils');
|
||||||
|
|
||||||
|
const cleanPeriod = 1*60*1000;//1 минута
|
||||||
|
const closeSocketOnIdle = 5*60*1000;//5 минут
|
||||||
|
|
||||||
|
class WebSocketController {
|
||||||
|
constructor(wss, config) {
|
||||||
|
this.config = config;
|
||||||
|
this.isDevelopment = (config.branch == 'development');
|
||||||
|
|
||||||
|
this.wss = wss;
|
||||||
|
|
||||||
|
wss.on('connection', (ws) => {
|
||||||
|
ws.on('message', (message) => {
|
||||||
|
this.onMessage(ws, message.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('error', (err) => {
|
||||||
|
log(LM_ERR, err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => { this.periodicClean(); }, cleanPeriod);
|
||||||
|
}
|
||||||
|
|
||||||
|
periodicClean() {
|
||||||
|
try {
|
||||||
|
const now = Date.now();
|
||||||
|
this.wss.clients.forEach((ws) => {
|
||||||
|
if (!ws.lastActivity || now - ws.lastActivity > closeSocketOnIdle - 50) {
|
||||||
|
ws.terminate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setTimeout(() => { this.periodicClean(); }, cleanPeriod);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async onMessage(ws, message) {
|
||||||
|
let req = {};
|
||||||
|
try {
|
||||||
|
if (this.isDevelopment) {
|
||||||
|
log(`WebSocket-IN: ${message.substr(0, 4000)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
req = JSON.parse(message);
|
||||||
|
|
||||||
|
ws.lastActivity = Date.now();
|
||||||
|
|
||||||
|
//pong for WebSocketConnection
|
||||||
|
this.send({_rok: 1}, req, ws);
|
||||||
|
|
||||||
|
switch (req.action) {
|
||||||
|
case 'test':
|
||||||
|
await this.test(req, ws); break;
|
||||||
|
case 'get-config':
|
||||||
|
await this.getConfig(req, ws); break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Action not found: ${req.action}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.send({error: e.message}, req, ws);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
send(res, req, ws) {
|
||||||
|
if (ws.readyState == WebSocket.OPEN) {
|
||||||
|
ws.lastActivity = Date.now();
|
||||||
|
let r = res;
|
||||||
|
if (req.requestId)
|
||||||
|
r = Object.assign({requestId: req.requestId}, r);
|
||||||
|
|
||||||
|
const message = JSON.stringify(r);
|
||||||
|
ws.send(message);
|
||||||
|
|
||||||
|
if (this.isDevelopment) {
|
||||||
|
log(`WebSocket-OUT: ${message.substr(0, 4000)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Actions ------------------------------------------------------------------
|
||||||
|
async test(req, ws) {
|
||||||
|
this.send({message: `${this.config.name} project is awesome`}, req, ws);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getConfig(req, ws) {
|
||||||
|
if (Array.isArray(req.params)) {
|
||||||
|
const paramsSet = new Set(req.params);
|
||||||
|
|
||||||
|
this.send(_.pick(this.config, this.config.webConfigParams.filter(x => paramsSet.has(x))), req, ws);
|
||||||
|
} else {
|
||||||
|
throw new Error('params is not an array');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = WebSocketController;
|
||||||
3
server/controllers/index.js
Normal file
3
server/controllers/index.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
WebSocketController: require('./WebSocketController'),
|
||||||
|
}
|
||||||
60
server/core/AppLogger.js
Normal file
60
server/core/AppLogger.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
const fs = require('fs-extra');
|
||||||
|
const Logger = require('./Logger');
|
||||||
|
|
||||||
|
let instance = null;
|
||||||
|
|
||||||
|
//singleton
|
||||||
|
class AppLogger {
|
||||||
|
constructor() {
|
||||||
|
if (!instance) {
|
||||||
|
this.inited = false;
|
||||||
|
this.logFileName = '';
|
||||||
|
this.errLogFileName = '';
|
||||||
|
this.fatalLogFileName = '';
|
||||||
|
|
||||||
|
instance = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
async init(config) {
|
||||||
|
if (this.inited)
|
||||||
|
throw new Error('already inited');
|
||||||
|
|
||||||
|
let loggerParams = null;
|
||||||
|
|
||||||
|
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: 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
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
this._logger = new Logger(loggerParams);
|
||||||
|
|
||||||
|
this.inited = true;
|
||||||
|
return this.logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
get logger() {
|
||||||
|
if (!this.inited)
|
||||||
|
throw new Error('not inited');
|
||||||
|
return this._logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
get log() {
|
||||||
|
const l = this.logger;
|
||||||
|
return l.log.bind(l);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = AppLogger;
|
||||||
105
server/core/AsyncExit.js
Normal file
105
server/core/AsyncExit.js
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
let instance = null;
|
||||||
|
|
||||||
|
const defaultTimeout = 15*1000;//15 sec
|
||||||
|
const exitSignals = ['SIGINT', 'SIGTERM', 'SIGBREAK', 'SIGHUP', 'uncaughtException'];
|
||||||
|
|
||||||
|
//singleton
|
||||||
|
class AsyncExit {
|
||||||
|
constructor(signals = exitSignals, codeOnSignal = 2) {
|
||||||
|
if (!instance) {
|
||||||
|
this.onSignalCallbacks = new Map();
|
||||||
|
this.callbacks = new Map();
|
||||||
|
this.afterCallbacks = new Map();
|
||||||
|
this.exitTimeout = defaultTimeout;
|
||||||
|
|
||||||
|
this._init(signals, codeOnSignal);
|
||||||
|
|
||||||
|
instance = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
_init(signals, codeOnSignal) {
|
||||||
|
const runSingalCallbacks = async(signal, err, origin) => {
|
||||||
|
for (const signalCallback of this.onSignalCallbacks.keys()) {
|
||||||
|
try {
|
||||||
|
await signalCallback(signal, err, origin);
|
||||||
|
} catch(e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const signal of signals) {
|
||||||
|
process.once(signal, async(err, origin) => {
|
||||||
|
await runSingalCallbacks(signal, err, origin);
|
||||||
|
this.exit(codeOnSignal);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
232
server/core/Logger.js
Normal file
232
server/core/Logger.js
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
/*
|
||||||
|
Журналирование с буферизацией вывода
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
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 = 1000000;
|
||||||
|
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.error(`Logger error: ${e}`); ayncExit.exit(1); } );
|
||||||
|
this.flushing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
log(msgType, message) {
|
||||||
|
if (this.closed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async close() {
|
||||||
|
if (this.closed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (this.iid)
|
||||||
|
clearInterval(this.iid);
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (this.outputBufferLength) {
|
||||||
|
await this.flush();
|
||||||
|
await sleep(1);
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
console.log(e);
|
||||||
|
ayncExit.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
async close() {
|
||||||
|
if (this.closed)
|
||||||
|
return;
|
||||||
|
await super.close();
|
||||||
|
if (this.fd) {
|
||||||
|
await fs.close(this.fd);
|
||||||
|
this.fd = null;
|
||||||
|
}
|
||||||
|
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.access(tn).then(() => true).catch(() => false);
|
||||||
|
if (exists) {
|
||||||
|
if (i >= LOG_ROTATE_FILE_DEPTH - 1) {
|
||||||
|
await fs.unlink(tn);
|
||||||
|
} else {
|
||||||
|
await this.rotateFile(fileName, i + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await fs.rename(fn, tn);
|
||||||
|
}
|
||||||
|
|
||||||
|
async doFileRotationIfNeeded() {
|
||||||
|
this.rcid = 0;
|
||||||
|
|
||||||
|
let stat = await fs.fstat(this.fd);
|
||||||
|
if (stat.size > LOG_ROTATE_FILE_LENGTH) {
|
||||||
|
await fs.close(this.fd);
|
||||||
|
await this.rotateFile(this.fileName, 0);
|
||||||
|
this.fd = await fs.open(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);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.fd)
|
||||||
|
await fs.write(this.fd, Buffer.from(data.join('')));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConsoleLog extends BaseLog {
|
||||||
|
async flushImpl(data) {
|
||||||
|
process.stdout.write(data.join(''));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//------------------------------------------------------------------
|
||||||
|
const factory = {
|
||||||
|
ConsoleLog,
|
||||||
|
FileLog,
|
||||||
|
};
|
||||||
|
|
||||||
|
class Logger {
|
||||||
|
|
||||||
|
constructor(params = null) {
|
||||||
|
this.handlers = [];
|
||||||
|
if (params) {
|
||||||
|
params.forEach((logParams) => {
|
||||||
|
let className = logParams.log;
|
||||||
|
let loggerClass = factory[className];
|
||||||
|
this.handlers.push(new loggerClass(logParams));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.closed = false;
|
||||||
|
ayncExit.onSignal((signal, err) => {
|
||||||
|
this.log(LM_FATAL, `Signal "${signal}" received, error: "${(err.stack ? err.stack : err)}", 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 this.formatDate(new Date()) + ` ${msgTypeToStr[msgType]}: ${message}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
log(msgType, message) {
|
||||||
|
if (message == null) {
|
||||||
|
message = msgType;
|
||||||
|
msgType = LM_INFO;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mes = this.prepareMessage(msgType, message);
|
||||||
|
|
||||||
|
if (!this.closed) {
|
||||||
|
for (let i = 0; i < this.handlers.length; i++)
|
||||||
|
this.handlers[i].log(msgType, mes);
|
||||||
|
} else {
|
||||||
|
console.log(mes);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mes;
|
||||||
|
}
|
||||||
|
|
||||||
|
async close() {
|
||||||
|
for (let i = 0; i < this.handlers.length; i++)
|
||||||
|
await this.handlers[i].close();
|
||||||
|
this.closed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Logger;
|
||||||
7
server/core/utils.js
Normal file
7
server/core/utils.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
function versionText(config) {
|
||||||
|
return `${config.name} v${config.version}, Node.js ${process.version}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
versionText,
|
||||||
|
};
|
||||||
43
server/dev.js
Normal file
43
server/dev.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
const log = new (require('./core/AppLogger'))().log;//singleton
|
||||||
|
|
||||||
|
function webpackDevMiddleware(app) {
|
||||||
|
const webpack = require('webpack');
|
||||||
|
const wpConfig = require('../build/webpack.dev.config');
|
||||||
|
|
||||||
|
const compiler = webpack(wpConfig);
|
||||||
|
const devMiddleware = require('webpack-dev-middleware');
|
||||||
|
app.use(devMiddleware(compiler, {
|
||||||
|
publicPath: wpConfig.output.publicPath,
|
||||||
|
stats: {colors: true}
|
||||||
|
}));
|
||||||
|
|
||||||
|
let hotMiddleware = require('webpack-hot-middleware');
|
||||||
|
app.use(hotMiddleware(compiler, {
|
||||||
|
log: log
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function logQueries(app) {
|
||||||
|
app.use(function(req, res, next) {
|
||||||
|
const start = Date.now();
|
||||||
|
log(`${req.method} ${req.originalUrl} ${JSON.stringify(req.body).substr(0, 4000)}`);
|
||||||
|
//log(`${JSON.stringify(req.headers, null, 2)}`)
|
||||||
|
res.once('finish', () => {
|
||||||
|
log(`${Date.now() - start}ms`);
|
||||||
|
});
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function logErrors(app) {
|
||||||
|
app.use(function(err, req, res, next) {// eslint-disable-line no-unused-vars
|
||||||
|
log(LM_ERR, err.stack);
|
||||||
|
res.status(500).send(err.stack);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
webpackDevMiddleware,
|
||||||
|
logQueries,
|
||||||
|
logErrors
|
||||||
|
};
|
||||||
121
server/index.js
Normal file
121
server/index.js
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
const fs = require('fs-extra');
|
||||||
|
const argv = require('minimist')(process.argv.slice(2));
|
||||||
|
const express = require('express');
|
||||||
|
const compression = require('compression');
|
||||||
|
const http = require('http');
|
||||||
|
const WebSocket = require ('ws');
|
||||||
|
|
||||||
|
const ayncExit = new (require('./core/AsyncExit'))();
|
||||||
|
const utils = require('./core/utils');
|
||||||
|
|
||||||
|
let log = null;
|
||||||
|
let config = null;
|
||||||
|
|
||||||
|
const maxPayloadSize = 50;//in MB
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
//config
|
||||||
|
const configManager = new (require('./config'))();//singleton
|
||||||
|
await configManager.init();
|
||||||
|
//configManager.userConfigFile = argv.config;
|
||||||
|
await configManager.load();
|
||||||
|
config = configManager.config;
|
||||||
|
|
||||||
|
//logger
|
||||||
|
const appLogger = new (require('./core/AppLogger'))();//singleton
|
||||||
|
await appLogger.init(config);
|
||||||
|
log = appLogger.log;
|
||||||
|
|
||||||
|
if (!argv.help) {
|
||||||
|
log(utils.versionText(config));
|
||||||
|
log('Initializing');
|
||||||
|
}
|
||||||
|
|
||||||
|
//dirs
|
||||||
|
await fs.ensureDir(config.dataDir);
|
||||||
|
await fs.ensureDir(config.tempDir);
|
||||||
|
await fs.emptyDir(config.tempDir);
|
||||||
|
|
||||||
|
const appDir = `${config.publicDir}/app`;
|
||||||
|
const appNewDir = `${config.publicDir}/app_new`;
|
||||||
|
if (await fs.pathExists(appNewDir)) {
|
||||||
|
await fs.remove(appDir);
|
||||||
|
await fs.move(appNewDir, appDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showHelp() {
|
||||||
|
console.log(utils.versionText(config));
|
||||||
|
console.log(
|
||||||
|
`Usage: ${config.name} [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--help Print ${config.name} command line options
|
||||||
|
`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
if (argv.help) {
|
||||||
|
showHelp();
|
||||||
|
ayncExit.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const log = new (require('./core/AppLogger'))().log;//singleton
|
||||||
|
|
||||||
|
//server
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
const server = http.createServer(app);
|
||||||
|
const wss = new WebSocket.Server({ server, maxPayload: maxPayloadSize*1024*1024 });
|
||||||
|
|
||||||
|
const serverConfig = Object.assign({}, config, config.server);
|
||||||
|
|
||||||
|
let devModule = undefined;
|
||||||
|
if (serverConfig.branch == 'development') {
|
||||||
|
const devFileName = './dev.js'; //require ignored by pkg -50Mb executable size
|
||||||
|
devModule = require(devFileName);
|
||||||
|
devModule.webpackDevMiddleware(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use(compression({ level: 1 }));
|
||||||
|
//app.use(express.json({limit: `${maxPayloadSize}mb`}));
|
||||||
|
if (devModule)
|
||||||
|
devModule.logQueries(app);
|
||||||
|
|
||||||
|
initStatic(app, config);
|
||||||
|
|
||||||
|
const { WebSocketController } = require('./controllers');
|
||||||
|
new WebSocketController(wss, config);
|
||||||
|
|
||||||
|
if (devModule) {
|
||||||
|
devModule.logErrors(app);
|
||||||
|
} else {
|
||||||
|
app.use(function(err, req, res, next) {// eslint-disable-line no-unused-vars
|
||||||
|
log(LM_ERR, err.stack);
|
||||||
|
res.sendStatus(500);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
server.listen(serverConfig.port, serverConfig.ip, function() {
|
||||||
|
log(`Server is ready on http://${serverConfig.ip}:${serverConfig.port}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initStatic(app, config) {// eslint-disable-line
|
||||||
|
//загрузка файлов в /files
|
||||||
|
//TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
(async() => {
|
||||||
|
try {
|
||||||
|
await init();
|
||||||
|
await main();
|
||||||
|
} catch (e) {
|
||||||
|
if (log)
|
||||||
|
log(LM_FATAL, e.stack);
|
||||||
|
else
|
||||||
|
console.error(e.stack);
|
||||||
|
ayncExit.exit(1);
|
||||||
|
}
|
||||||
|
})();
|
||||||
Reference in New Issue
Block a user