@meese-os/server
Version:
meeseOS Server
262 lines (227 loc) • 6.52 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 fs = require("fs-extra");
const fg = require("fast-glob");
const path = require("path");
const Package = require("./package.js");
const consola = require("consola");
const logger = consola.withTag("Packages");
const relative = (filename) => filename.replace(process.cwd(), "");
const readOrDefault = (filename) =>
fs.existsSync(filename) ? fs.readJsonSync(filename) : [];
/**
* Package Service Options
* @typedef {Object} PackagesOptions
* @property {String} [manifestFile] Manifest filename
* @property {String} [discoveredFile] Discovery output file
*/
/**
* MeeseOS Package Management
*/
class Packages {
/**
* Create new instance.
* @param {Core} core MeeseOS Core instance reference
* @param {PackagesOptions} [options={}] Instance options
*/
constructor(core, options = {}) {
/**
* @type {Core}
*/
this.core = core;
/**
* @type {Package[]}
*/
this.packages = [];
this.hotReloading = {};
/**
* @type {PackagesOptions}
*/
this.options = {
manifestFile: null,
discoveredFile: null,
...options,
};
}
/**
* Initializes packages.
*/
init() {
this.core.on("meeseOS/application:socket:message", (ws, ...params) => {
this.handleMessage(ws, params);
});
return this.load();
}
/**
* Loads package manager.
* @returns {Promise<Boolean>}
*/
load() {
return this.createLoader().then((packages) => {
this.packages = this.packages.concat(packages);
return true;
});
}
/**
* Loads all packages.
* @returns {Promise<Package[]>}
*/
createLoader() {
const result = [];
const { discoveredFile, manifestFile } = this.options;
const discovered = readOrDefault(discoveredFile);
const manifest = readOrDefault(manifestFile);
const sources = discovered.map((d) => path.join(d, "metadata.json"));
logger.info("Using package discovery file", relative(discoveredFile));
logger.info("Using package manifest file", relative(manifestFile));
const stream = fg.stream(sources, {
extension: false,
brace: false,
deep: 1,
case: false,
});
stream.on("error", (error) => logger.error(error));
stream.on("data", (filename) => {
result.push(this.loadPackage(filename, manifest));
});
return new Promise((resolve, reject) => {
stream.once("end", () => {
Promise.all(result)
.then((result) => result.filter((iter) => Boolean(iter.handler)))
.then(resolve)
.catch(reject);
});
});
}
/**
* When a package dist has changed.
* @param {Package} pkg Package instance
*/
onPackageChanged(pkg) {
clearTimeout(this.hotReloading[pkg.metadata.name]);
this.hotReloading[pkg.metadata.name] = setTimeout(() => {
logger.debug("Sending reload signal for", pkg.metadata.name);
this.core.broadcast("meeseOS/packages:package:changed", [
pkg.metadata.name,
]);
}, 500);
}
/**
* Loads package data.
* @param {String} filename Filename
* @param {PackageMetadata} manifest Manifest
* @returns {Promise<Package>}
*/
loadPackage(filename, manifest) {
const done = (pkg, error) => {
if (error) {
logger.warn(error);
}
return Promise.resolve(pkg);
};
return fs.readJson(filename).then((metadata) => {
const pkg = new Package(this.core, {
filename,
metadata,
});
return this.initializePackage(pkg, manifest, done);
});
}
/**
* Initializes a package.
* @returns {Promise<Package>}
*/
initializePackage(pkg, manifest, done) {
if (pkg.validate(manifest)) {
logger.info(`Loading ${relative(pkg.script)}`);
try {
if (this.core.configuration.development) {
pkg.watch(() => {
this.onPackageChanged(pkg);
});
}
return pkg
.init()
.then(() => done(pkg))
.catch((e) => done(pkg, e));
} catch (e) {
return done(pkg, e);
}
}
return done(pkg);
}
/**
* Starts packages.
*/
start() {
this.packages.forEach((pkg) => pkg.start());
}
/**
* Destroys packages.
* @returns {Promise<undefined>}
*/
async destroy() {
await Promise.all(this.packages.map((pkg) => pkg.destroy()));
this.packages = [];
}
/**
* Handles an incoming message and signals an application.
*
* This will call the 'onmessage' event in your application server script.
*
* @param {WebSocket} ws Websocket Connection client
* @param {Array} params A list of incoming parameters
*/
handleMessage(ws, params) {
const { pid, name, args } = params[0];
const found = this.packages.findIndex(
({ metadata }) => metadata.name === name
);
if (found !== -1) {
const { handler } = this.packages[found];
if (handler && typeof handler.onmessage === "function") {
const respond = (...respondParams) =>
ws.send(
JSON.stringify({
name: "meeseOS/application:socket:message",
params: [
{
pid,
args: respondParams,
},
],
})
);
handler.onmessage(ws, respond, args);
}
}
}
}
module.exports = Packages;