UNPKG

6.34 kBJavaScriptView Raw
1/*
2 * OS.js - JavaScript Cloud/Web Desktop Platform
3 *
4 * Copyright (c) 2011-2020, Anders Evenrud <andersevenrud@gmail.com>
5 * All rights reserved.
6 *
7 * Redistribution and use in source and binary forms, with or without
8 * modification, are permitted provided that the following conditions are met:
9 *
10 * 1. Redistributions of source code must retain the above copyright notice, this
11 * list of conditions and the following disclaimer
12 * 2. Redistributions in binary form must reproduce the above copyright notice,
13 * this list of conditions and the following disclaimer in the documentation
14 * and/or other materials provided with the distribution
15 *
16 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
20 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
21 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
22 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
23 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 *
27 * @author Anders Evenrud <andersevenrud@gmail.com>
28 * @licence Simplified BSD License
29 */
30
31const fs = require('fs-extra');
32const fg = require('fast-glob');
33const path = require('path');
34const Package = require('./package.js');
35const consola = require('consola');
36const logger = consola.withTag('Packages');
37
38const relative = filename => filename.replace(process.cwd(), '');
39
40const readOrDefault = filename => fs.existsSync(filename)
41 ? fs.readJsonSync(filename)
42 : [];
43
44/**
45 * OS.js Package Management
46 */
47class Packages {
48
49 /**
50 * Create new instance
51 * @param {Core} core Core reference
52 * @param {object} [options] Instance options
53 */
54 constructor(core, options = {}) {
55 this.core = core;
56 this.packages = [];
57 this.hotReloading = {};
58 this.options = {
59 manifestFile: null,
60 discoveredFile: null,
61 ...options
62 };
63 }
64
65 /**
66 * Initializes packages
67 */
68 init() {
69 this.core.on('osjs/application:socket:message', (ws, ...params) => {
70 this.handleMessage(ws, params);
71 });
72
73 return this.load();
74 }
75
76 /**
77 * Loads package manager
78 */
79 load() {
80 return this.createLoader()
81 .then(packages => {
82 this.packages = this.packages.concat(packages);
83
84 return true;
85 });
86 }
87
88 /**
89 * Loads all packages
90 * @return {Promise<Package[]>}
91 */
92 createLoader() {
93 let result = [];
94 const {discoveredFile, manifestFile} = this.options;
95 const discovered = readOrDefault(discoveredFile);
96 const manifest = readOrDefault(manifestFile);
97 const sources = discovered.map(d => path.join(d, 'metadata.json'));
98
99 logger.info('Using package discovery file', relative(discoveredFile));
100 logger.info('Using package manifest file', relative(manifestFile));
101
102 const stream = fg.stream(sources, {
103 extension: false,
104 brace: false,
105 deep: 1,
106 case: false
107 });
108
109 stream.on('error', error => logger.error(error));
110 stream.on('data', filename => {
111 result.push(this.loadPackage(filename, manifest));
112 });
113
114 return new Promise((resolve, reject) => {
115 stream.once('end', () => {
116 Promise.all(result)
117 .then(result => result.filter(iter => !!iter.handler))
118 .then(resolve)
119 .catch(reject);
120 });
121 });
122 }
123
124 /**
125 * When a package dist has changed
126 * @param {Package} pkg Package instance
127 */
128 onPackageChanged(pkg) {
129 clearTimeout(this.hotReloading[pkg.metadata.name]);
130
131 this.hotReloading[pkg.metadata.name] = setTimeout(() => {
132 logger.debug('Sending reload signal for', pkg.metadata.name);
133 this.core.broadcast('osjs/packages:package:changed', [pkg.metadata.name]);
134 }, 500);
135 }
136
137 /**
138 * Loads package data
139 * @param {string} filename Filename
140 * @param {object} manifest Manifest
141 * @return {Promise<Package>}
142 */
143 loadPackage(filename, manifest) {
144 const done = (pkg, error) => {
145 if (error) {
146 logger.warn(error);
147 }
148
149 return Promise.resolve(pkg);
150 };
151
152 return fs.readJson(filename)
153 .then(metadata => {
154 const pkg = new Package(this.core, {
155 filename,
156 metadata
157 });
158
159 return this.initializePackage(pkg, manifest, done);
160 });
161 }
162
163 /**
164 * Initializes a package
165 * @return {Promise<Package>}
166 */
167 initializePackage(pkg, manifest, done) {
168 if (pkg.validate(manifest)) {
169 logger.info(`Loading ${relative(pkg.script)}`);
170
171 try {
172 if (this.core.configuration.development) {
173 pkg.watch(() => {
174 this.onPackageChanged(pkg);
175 });
176 }
177
178 return pkg.init()
179 .then(() => done(pkg))
180 .catch(e => done(pkg, e));
181 } catch (e) {
182 return done(pkg, e);
183 }
184 }
185
186 return done(pkg);
187 }
188
189 /**
190 * Starts packages
191 */
192 start() {
193 this.packages.forEach(pkg => pkg.start());
194 }
195
196 /**
197 * Destroys packages
198 */
199 async destroy() {
200 await Promise.all(this.packages.map(pkg => pkg.destroy()));
201
202 this.packages = [];
203 }
204
205 /**
206 * Handles an incoming message and signals an application
207 * @desc This will call the 'onmessage' event in your application server script
208 * @param {Object} ws Websocket Connection client
209 * @param {Array} params A list of incoming parameters
210 */
211 handleMessage(ws, params) {
212 const {pid, name, args} = params[0];
213 const found = this.packages.findIndex(({metadata}) => metadata.name === name);
214
215 if (found !== -1) {
216 const {handler} = this.packages[found];
217 if (handler && typeof handler.onmessage === 'function') {
218 const respond = (...respondParams) => ws.send(JSON.stringify({
219 name: 'osjs/application:socket:message',
220 params: [{
221 pid,
222 args: respondParams
223 }]
224 }));
225
226 handler.onmessage(ws, respond, args);
227 }
228 }
229 }
230}
231
232module.exports = Packages;