UNPKG

@meese-os/server

Version:
309 lines (267 loc) 7.7 kB
/** * 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;