@meese-os/server
Version:
meeseOS Server
309 lines (267 loc) • 7.7 kB
JavaScript
/**
* OS.js - JavaScript Cloud/Web Desktop Platform
*
* Copyright (c) 2011-Present, 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 { consola } = require("consola");
const deepmerge = require("deepmerge");
const express = require("express");
// eslint-disable-next-line no-unused-vars
const express_ws = require("express-ws");
const fs = require("fs-extra");
const http = require("http");
const https = require("https");
const morgan = require("morgan");
const minimist = require("minimist");
const path = require("path");
const { CoreBase } = require("@meese-os/common");
const {
argvToConfig,
createSession,
createWebsocket,
parseJson,
} = require("./utils/core.js");
const { defaultConfiguration } = require("./config.js");
const logger = consola.withTag("Core");
let _instance;
/**
* MeeseOS Server Core
*/
class Core extends CoreBase {
/**
* Creates a new Core 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 {express.RequestHandler}
*/
this.session = createSession(this.configuration);
/**
* @type {express_ws.Instance}
*/
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
* @returns {Promise<undefined>}
*/
async destroy(done = () => {}) {
if (this.destroyed) return;
this.emit("meeseOS/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.
* @returns {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.
* @returns {Promise<Boolean>}
*/
async boot() {
if (this.booted) return true;
this.emit("meeseOS/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("meeseOS/core:started");
return true;
}
/**
* Opens HTTP server.
*/
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) return;
this.wss.clients // This is a Set
.forEach((client) => {
if (!client._meeseOS_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._meeseOS_client.username === username;
});
}
/**
* Gets the server instance.
* @returns {Core}
*/
static getInstance() {
return _instance;
}
}
module.exports = Core;