signalk-server
Version:
An implementation of a [Signal K](http://signalk.org) server for boats.
338 lines (337 loc) • 14.2 kB
JavaScript
;
/*
* Copyright 2017 Scott Bender <scott@scottbender.net>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
const debug_1 = require("../debug");
const debug = (0, debug_1.createDebug)('signalk-server:interfaces:appstore');
const _ = require('lodash');
const { gt } = require('semver');
const { installModule, removeModule } = require('../modules');
const { isTheServerModule, findModulesWithKeyword, getLatestServerVersion, getAuthor, getKeywords } = require('../modules');
const { SERVERROUTESPREFIX } = require('../constants');
const { getCategories, getAvailableCategories } = require('../categories');
const npmServerInstallLocations = [
'/usr/bin/signalk-server',
'/usr/lib/node_modules/signalk-server/bin/signalk-server',
'/usr/local/bin/signalk-server',
'/usr/local/lib/node_modules/signalk-server/bin/signalk-server'
];
module.exports = function (app) {
let moduleInstalling;
const modulesInstalledSinceStartup = {};
const moduleInstallQueue = [];
return {
start: function () {
app.post([
`${SERVERROUTESPREFIX}/appstore/install/:name/:version`,
`${SERVERROUTESPREFIX}/appstore/install/:org/:name/:version`
], (req, res) => {
let name = req.params.name;
const version = req.params.version;
if (req.params.org) {
name = req.params.org + '/' + name;
}
findPluginsAndWebapps()
.then(([plugins, webapps]) => {
if (!isTheServerModule(name, app.config) &&
!plugins.find(packageNameIs(name)) &&
!webapps.find(packageNameIs(name))) {
res.status(404);
res.json('No such webapp or plugin available:' + name);
}
else {
if (moduleInstalling) {
moduleInstallQueue.push({ name: name, version: version });
sendAppStoreChangedEvent();
}
else {
installSKModule(name, version);
}
res.json(`Installing ${name}...`);
}
})
.catch((error) => {
console.log(error.message);
debug(error.stack);
res.status(500);
res.json(error.message);
});
});
app.post([
`${SERVERROUTESPREFIX}/appstore/remove/:name`,
`${SERVERROUTESPREFIX}/appstore/remove/:org/:name`
], (req, res) => {
let name = req.params.name;
if (req.params.org) {
name = req.params.org + '/' + name;
}
findPluginsAndWebapps()
.then(([plugins, webapps]) => {
if (!plugins.find(packageNameIs(name)) &&
!webapps.find(packageNameIs(name))) {
res.status(404);
res.json('No such webapp or plugin available:' + name);
}
else {
if (moduleInstalling) {
moduleInstallQueue.push({ name: name, isRemove: true });
sendAppStoreChangedEvent();
}
else {
removeSKModule(name);
}
res.json(`Removing ${name}...`);
}
})
.catch((error) => {
console.log(error.message);
debug(error.stack);
res.status(500);
res.json(error.message);
});
});
app.get(`${SERVERROUTESPREFIX}/appstore/available/`, (req, res) => {
findPluginsAndWebapps()
.then(([plugins, webapps]) => {
getLatestServerVersion(app.config.version)
.then((serverVersion) => {
const result = getAllModuleInfo(plugins, webapps, serverVersion);
res.json(result);
})
.catch(() => {
//could be that npmjs is down, so we can not get
//server version, but we have app store data
const result = getAllModuleInfo(plugins, webapps, '0.0.0');
res.json(result);
});
})
.catch((error) => {
console.log(error.message);
debug(error.stack);
res.json(emptyAppStoreInfo(false));
});
});
},
stop: () => undefined
};
function findPluginsAndWebapps() {
return Promise.all([
findModulesWithKeyword('signalk-node-server-plugin'),
findModulesWithKeyword('signalk-embeddable-webapp'),
findModulesWithKeyword('signalk-webapp')
]).then(([plugins, embeddableWebapps, webapps]) => {
const allWebapps = [].concat(embeddableWebapps).concat(webapps);
return [
plugins,
_.uniqBy(allWebapps, (plugin) => {
return plugin.package.name;
})
];
});
}
function getPlugin(id) {
return app.plugins.find((plugin) => plugin.packageName === id);
}
function getWebApp(id) {
return ((app.webapps && app.webapps.find((webapp) => webapp.name === id)) ||
(app.addons && app.addons.find((webapp) => webapp.name === id)) ||
(app.embeddablewebapps &&
app.embeddablewebapps.find((webapp) => webapp.name === id)));
}
function emptyAppStoreInfo(storeAvailable = true) {
return {
available: [],
installed: [],
updates: [],
installing: [],
categories: getAvailableCategories(),
storeAvailable: storeAvailable,
isInDocker: process.env.IS_IN_DOCKER === 'true'
};
}
function getAllModuleInfo(plugins, webapps, serverVersion) {
const all = emptyAppStoreInfo();
if (process.argv.length > 1 &&
(npmServerInstallLocations.includes(process.argv[1]) ||
process.env.SIGNALK_SERVER_IS_UPDATABLE) &&
!process.env.SIGNALK_DISABLE_SERVER_UPDATES) {
all.canUpdateServer = !all.isInDocker && true;
if (gt(serverVersion, app.config.version)) {
all.serverUpdate = serverVersion;
const info = {
name: app.config.name,
version: serverVersion,
description: app.config.description,
author: getAuthor(app.config),
npmUrl: null,
isPlugin: false,
isWebapp: false
};
if (moduleInstallQueue.find((p) => p.name === info.name)) {
info.isWaiting = true;
all.installing.push(info);
}
else if (modulesInstalledSinceStartup[info.name]) {
if (moduleInstalling && moduleInstalling.name === info.name) {
info.isInstalling = true;
}
else if (modulesInstalledSinceStartup[info.name].code !== 0) {
info.installFailed = true;
}
all.installing.push(info);
}
}
}
else {
all.canUpdateServer = false;
}
getModulesInfo(plugins, getPlugin, all);
getModulesInfo(webapps, getWebApp, all);
if (process.env.PLUGINS_WITH_UPDATE_DISABLED) {
let disabled = process.env.PLUGINS_WITH_UPDATE_DISABLED.split(',');
all.updates = all.updates.filter((info) => !disabled.includes(info.name));
}
return all;
}
function getModulesInfo(modules, existing, result) {
modules.forEach((plugin) => {
const name = plugin.package.name;
const version = plugin.package.version;
const pluginInfo = {
name: name,
version: version,
description: plugin.package.description,
author: getAuthor(plugin.package),
categories: getCategories(plugin.package),
updated: plugin.package.date,
keywords: getKeywords(plugin.package),
npmUrl: getNpmUrl(plugin),
isPlugin: plugin.package.keywords.some((v) => v === 'signalk-node-server-plugin'),
isWebapp: plugin.package.keywords.some((v) => v === 'signalk-webapp'),
isEmbeddableWebapp: plugin.package.keywords.some((v) => v === 'signalk-embeddable-webapp')
};
const installedModule = existing(name);
if (installedModule) {
pluginInfo.installedVersion = installedModule.version;
}
if (moduleInstallQueue.find((p) => p.name === name)) {
pluginInfo.isWaiting = true;
addIfNotDuplicate(result.installing, pluginInfo);
}
else if (modulesInstalledSinceStartup[name]) {
if (moduleInstalling && moduleInstalling.name === name) {
if (moduleInstalling.isRemove) {
pluginInfo.isRemoving = true;
}
else {
pluginInfo.isInstalling = true;
}
}
else if (modulesInstalledSinceStartup[name].code !== 0) {
pluginInfo.installFailed = true;
addIfNotDuplicate(result.available, pluginInfo);
}
pluginInfo.isRemove = modulesInstalledSinceStartup[name].isRemove;
addIfNotDuplicate(result.installing, pluginInfo);
}
else if (installedModule) {
if (gt(version, installedModule.version)) {
addIfNotDuplicate(result.updates, pluginInfo);
}
addIfNotDuplicate(result.installed, pluginInfo);
}
addIfNotDuplicate(result.available, pluginInfo);
return result;
});
}
function addIfNotDuplicate(theArray, moduleInfo) {
if (!theArray.find((p) => p.name === moduleInfo.name)) {
theArray.push(moduleInfo);
}
}
function getNpmUrl(moduleInfo) {
const npm = _.get(moduleInfo.package, 'links.npm');
return npm || null;
}
function sendAppStoreChangedEvent() {
findPluginsAndWebapps().then(([plugins, webapps]) => {
getLatestServerVersion(app.config.version).then((serverVersion) => {
const result = getAllModuleInfo(plugins, webapps, serverVersion);
app.emit('serverevent', {
type: 'APP_STORE_CHANGED',
from: 'signalk-server',
data: result
});
});
});
}
function installSKModule(module, version) {
if (isTheServerModule(module, app.config)) {
try {
app.providers.forEach((providerHolder) => {
if (typeof providerHolder.pipeElements[0].pipeline[0].options
.filename !== 'undefined') {
debug('close file connection:', providerHolder.id);
providerHolder.pipeElements[0].end();
}
});
}
catch (err) {
debug(err);
}
}
updateSKModule(module, version, false);
}
function removeSKModule(module) {
updateSKModule(module, null, true);
}
function updateSKModule(module, version, isRemove) {
moduleInstalling = {
name: module,
output: [],
version: version,
isRemove: isRemove
};
modulesInstalledSinceStartup[module] = moduleInstalling;
sendAppStoreChangedEvent();
const fn = isRemove ? removeModule : installModule;
fn(app.config, module, version, (output) => {
modulesInstalledSinceStartup[module].output.push(output);
console.log(`stdout: ${output}`);
}, (output) => {
modulesInstalledSinceStartup[module].output.push(output);
console.error(`stderr: ${output}`);
}, (code) => {
debug('close: ' + module);
modulesInstalledSinceStartup[module].code = code;
moduleInstalling = undefined;
debug(`child process exited with code ${code}`);
if (moduleInstallQueue.length) {
const next = moduleInstallQueue.splice(0, 1)[0];
if (next.isRemove) {
removeSKModule(next.name);
}
else {
installSKModule(next.name, next.version);
}
}
sendAppStoreChangedEvent();
});
}
};
function packageNameIs(name) {
return (x) => x.package.name === name;
}
//# sourceMappingURL=appstore.js.map