src/core.js
/*
* OS.js - JavaScript Cloud/Web Desktop Platform
*
* Copyright (c) Anders Evenrud <andersevenrud@gmail.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* @author Anders Evenrud <andersevenrud@gmail.com>
* @licence Simplified BSD License
*/
const fs = require('fs-extra');
const http = require('http');
const https = require('https');
const path = require('path');
const morgan = require('morgan');
const express = require('express');
const minimist = require('minimist');
const deepmerge = require('deepmerge');
const consola = require('consola');
const {CoreBase} = require('@osjs/common');
const {argvToConfig, createSession, createWebsocket, parseJson} = require('./utils/core.js');
const {defaultConfiguration} = require('./config.js');
const logger = consola.withTag('Core');
let _instance;
/**
* OS.js Server Core
*/
class Core extends CoreBase {
/**
* Creates a new instance
* @param {Object} cfg Configuration tree
* @param {Object} [options] Options
*/
constructor(cfg, options = {}) {
options = {
argv: process.argv.splice(2),
root: process.cwd(),
...options
};
const argv = minimist(options.argv);
const val = k => argvToConfig[k](parseJson(argv[k]));
const keys = Object.keys(argvToConfig).filter(k => Object.prototype.hasOwnProperty.call(argv, k));
const argvConfig = keys.reduce((o, k) => {
logger.info(`CLI argument '--${k}' overrides`, val(k));
return {...o, ...deepmerge(o, val(k))};
}, {});
super(defaultConfiguration, deepmerge(cfg, argvConfig), options);
this.logger = consola.withTag('Internal');
/**
* @type {Express}
*/
this.app = express();
if (!this.configuration.public) {
throw new Error('The public option is required');
}
/**
* @type {http.Server|https.Server}
*/
this.httpServer = this.config('https.enabled')
? https.createServer(this.config('https.options'), this.app)
: http.createServer(this.app);
/**
* @type {object}
*/
this.session = createSession(this.app, this.configuration);
/**
* @type {object}
*/
this.ws = createWebsocket(this.app, this.configuration, this.session, this.httpServer);
/**
* @type {object}
*/
this.wss = this.ws.getWss();
_instance = this;
}
/**
* Destroys the instance
* @param {Function} [done] Callback when done
* @return {Promise<undefined>}
*/
async destroy(done = () => {}) {
if (this.destroyed) {
return;
}
this.emit('osjs/core:destroy');
logger.info('Shutting down...');
if (this.wss) {
this.wss.close();
}
const finish = (error) => {
if (error) {
logger.error(error);
}
if (this.httpServer) {
this.httpServer.close(done);
} else {
done();
}
};
try {
await super.destroy();
finish();
} catch (e) {
finish(e);
}
}
/**
* Starts the server
* @return {Promise<boolean>}
*/
async start() {
if (!this.started) {
logger.info('Starting services...');
await super.start();
logger.success('Initialized!');
await this.listen();
}
return true;
}
/**
* Initializes the server
* @return {Promise<boolean>}
*/
async boot() {
if (this.booted) {
return true;
}
this.emit('osjs/core:start');
if (this.configuration.logging) {
this.wss.on('connection', (c) => {
logger.log('WebSocket connection opened');
c.on('close', () => logger.log('WebSocket connection closed'));
});
if (this.configuration.morgan) {
this.app.use(morgan(this.configuration.morgan));
}
}
logger.info('Initializing services...');
await super.boot();
this.emit('init');
await this.start();
this.emit('osjs/core:started');
return true;
}
/**
* Opens HTTP server
*/
async listen() {
const httpPort = this.config('port');
const httpHost = this.config('bind');
const wsPort = this.config('ws.port') || httpPort;
const pub = this.config('public');
const session = path.basename(path.dirname(this.config('session.store.module')));
const dist = pub.replace(process.cwd(), '');
const secure = this.config('https.enabled', false);
const proto = prefix => `${prefix}${secure ? 's' : ''}://`;
const host = port => `${httpHost}:${port}`;
logger.info('Opening server connection');
const checkFile = path.join(pub, this.configuration.index);
if (!fs.existsSync(checkFile)) {
logger.warn('Missing files in "dist/" directory. Did you forget to run "npm run build" ?');
}
return new Promise((resolve, reject) => {
try {
this.httpServer.listen(httpPort, httpHost, (e) => {
if (e) {
reject(e);
} else {
logger.success(`Using '${session}' sessions`);
logger.success(`Serving '${dist}'`);
logger.success(`WebSocket listening on ${proto('ws')}${host(wsPort)}`);
logger.success(`Server listening on ${proto('http')}${host(httpPort)}`);
resolve();
}
});
} catch (e) {
reject(e);
}
});
}
/**
* Broadcast given event to client
* @param {string} name Event name
* @param {Array} params A list of parameters to send to client
* @param {Function} [filter] A function to filter clients
*/
broadcast(name, params, filter) {
filter = filter || (() => true);
if (this.ws) {
this.wss.clients // This is a Set
.forEach(client => {
if (!client._osjs_client) {
return;
}
if (filter(client)) {
client.send(JSON.stringify({
params,
name
}));
}
});
}
}
/**
* Broadcast given event to all clients
* @param {string} name Event name
* @param {Array} ...params A list of parameters to send to client
*/
broadcastAll(name, ...params) {
return this.broadcast(name, params);
}
/**
* Broadcast given event to client filtered by username
* @param {String} username Username to send to
* @param {string} name Event name
* @param {Array} ...params A list of parameters to send to client
*/
broadcastUser(username, name, ...params) {
return this.broadcast(name, params, client => {
return client._osjs_client.username === username;
});
}
/**
* Gets the server instance
* @return {Core}
*/
static getInstance() {
return _instance;
}
}
module.exports = Core;