UNPKG

@egeria/plugin-manager

Version:

Plugin loader and updater

194 lines (171 loc) 7.29 kB
/* .--. .-'. .--. .--. .--. .--. .`-. .--. :::::.\::::::::.\::::::::.\::::::::.\::::::::.\::::::::.\::::::::.\::::::::.\ ' `--' `.-' `--' `--' `--' `-.' `--' ` Egeria - She bestows Knowledge and Wisdom Copyright (C) 2016-2019 MySidesTheyAreGone <mysidestheyaregone@protonmail.com> This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. .--. .-'. .--. .--. .--. .--. .`-. .--. :::::.\::::::::.\::::::::.\::::::::.\::::::::.\::::::::.\::::::::.\::::::::.\ ' `--' `.-' `--' `--' `--' `-.' `--' ` */ module.exports = function (state) { const R = require('ramda') const T = require('@egeria/tools') const W = require('@egeria/httplib') const FS = require('@egeria/fslib') const semver = require('semver') const path = require('path') const _spawn = require('child_process').spawn const prefix = 'PLUGIN MANAGER |' state.set('prefix', prefix) const err = (message, origin, data) => T.err(prefix, message, origin, data) const { logSilly, logInfo, logWarning, logError } = T.setupLogging() const rethrow = (action, e, data) => { throw T.err(prefix, action, e, data) } const moduleBaseDir = state.get('moduleBaseDir') const moduleInstallDir = 'lib/node_modules' const pluginRoster = state.select('plugins') const installCmd = 'install --silent --prefix=' + moduleBaseDir + ' --global --production ' let loadedPlugins = {} function cleanOutput (d) { return R.replace(/\r?\n|\r/, '', d.toString()) } const versionSatisfies = R.flip(R.binary(semver.satisfies)) async function spawn (cmd, args) { return new Promise((resolve, reject) => { var proc = _spawn(cmd, args, { stdio: ['ignore', 'ignore', 'pipe'] }) proc.stderr.on('data', (d) => { logWarning(state, 'npm warns: ' + cleanOutput(d)) }) proc.on('error', (code) => { var e = new Error('npm terminated abnormally with code "' + code + '"') reject(e) }) proc.on('close', (code, signal) => { if (code !== 0 || signal === 'SIGTERM') { var reason = (R.isNil(signal) ? ' - exit code ' + code : ' - received signal ' + signal) var e = new Error('npm terminated abnormally' + reason) reject(e) } else { resolve(true) } }) }) } function getDirectory (module) { return path.resolve(moduleBaseDir, moduleInstallDir, module) } async function getLatestVersion (module) { let { body } = await W.httpreq({ uri: 'https://registry.npmjs.org/' + encodeURIComponent(module).replace('%40', '@'), json: true }) let versions = R.keys(body.versions) let allowedVersions = R.filter(versionSatisfies('^0.x'), versions) let latest = R.last(allowedVersions) logSilly(state, 'Latest allowed ' + module + ' version is v' + latest) return latest } async function getInstalledVersion (module) { let dir = getDirectory(module) let version try { let pkgfile = await FS.read(path.resolve(dir, 'package.json')) version = JSON.parse(pkgfile).version } catch (e) { version = null } logSilly(state, 'Installed ' + module + ' version is v' + version) return version } function remove (module) { return FS.erase(getDirectory(module)) } async function install (module) { var action = 'While installing module ' + module try { await spawn('npm', R.split(' ', installCmd + module)) } catch (e) { rethrow(action, e) } } async function load (name) { logSilly(state, 'Attempting to load ' + name) let action = 'While attempting to load plugin ' + name let module = pluginRoster.get(name) if (R.isNil(module)) { logInfo(state, `Plugin "${name}" is not official`) if (!R.isNil(loadedPlugins[name])) { return loadedPlugins[name][name] } try { loadedPlugins[name] = require(path.resolve('..', `plugin-${name}`)) return loadedPlugins[name][name] } catch (e) { logSilly(state, e.message) throw err(action, new Error(`Plugin ${name} doesn't exist and can't be found.`)) } } else if (!R.isNil(loadedPlugins[module])) { return loadedPlugins[module][name] } else { try { let repoPluginDir = path.resolve('..', 'plugin-' + R.replace(/@egeria\/(.*)-plugin/, '$1', module)) loadedPlugins[module] = require(repoPluginDir) return loadedPlugins[module][name] } catch (e) { logSilly(state, e.message) } let moduleDir = getDirectory(module) let moduleIsOutdated let registryUnreachable = false let current = await getInstalledVersion(module) let latest logSilly(state, 'Getting latest version of ' + module) try { latest = await getLatestVersion(module) moduleIsOutdated = R.isNil(current) || semver.gt(latest, current) } catch (e) { logError(state, `Couldn't get the latest version number of ${module} from the npm registry`, e) latest = null moduleIsOutdated = false registryUnreachable = true } let moduleDirExists = await FS.exists(moduleDir) try { if (moduleIsOutdated) { if (moduleDirExists) { logInfo(state, 'Upgrading ' + module + ' to v' + latest) await remove(module) } else { logInfo(state, 'Installing ' + module + '@v' + latest) } await install(module) } logSilly(state, 'Requiring ' + module) let plugin = require(moduleDir) logInfo(state, 'Plugin ' + name + ' loaded') loadedPlugins[module] = plugin return plugin[name] } catch (e) { if (moduleIsOutdated) { throw err(`The installation of ${module} failed or resulted in a corrupted module: giving up`, e) } else if (registryUnreachable) { throw err(`The module ${module} is either missing or corrupted; since it was also impossibile to fetch anything from the npm registry, I'm giving up. Check your Internet connection.`, e) } else { logError(state, 'You have the latest version of ' + module + ', but it seems to be corrupted. Attempting reinstallation.') await remove(module) return load(name) } } } } return { load } }