UNPKG

iobroker.js-controller

Version:

Updated by reinstall.js on 2018-06-11T15:19:56.688Z

685 lines (635 loc) • 30.7 kB
/** * Upgrade command * * Copyright 2013-2022 bluefox <dogafox@gmail.com> * * MIT License * */ 'use strict'; const debug = require('debug')('iobroker:cli'); /** @class */ function Upgrade(options) { const fs = require('fs-extra'); const { tools } = require('@iobroker/js-controller-common'); options = options || {}; if (!options.processExit) { throw new Error('Invalid arguments: processExit is missing'); } if (!options.restartController) { throw new Error('Invalid arguments: restartController is missing'); } if (!options.getRepository) { throw new Error('Invalid arguments: getRepository is missing'); } const processExit = options.processExit; const getRepository = options.getRepository; const params = options.params; const objects = options.objects; /** @type {import("semver")} */ const semver = require('semver'); let rl; let tty; const hostname = tools.getHostName(); const { EXIT_CODES } = require('@iobroker/js-controller-common'); const Upload = require('./setupUpload.js'); const upload = new Upload(options); const Install = require('./setupInstall.js'); const install = new Install(options); /** * Sorts the adapters by their dependencies and then upgrades multiple adapters from the given repository url * * @param {object} repo the repository content * @param {string[]} list list of adapters to upgrade * @param {boolean} forceDowngrade flag to force downgrade * @param {boolean} autoConfirm automatically confirm the tty questions (bypass) */ this.upgradeAdapterHelper = async (repo, list, forceDowngrade, autoConfirm) => { const relevantAdapters = []; // check which adapters are upgradeable and sort them according to their dependencies for (const adapter of list) { if (repo[adapter].controller) { // skip controller continue; } const adapterDir = tools.getAdapterDir(adapter); if (fs.existsSync(`${adapterDir}/io-package.json`)) { const ioInstalled = require(`${adapterDir}/io-package.json`); if (!tools.upToDate(repo[adapter].version, ioInstalled.common.version)) { // not up to date, we need to put it into account for our dependency check relevantAdapters.push(adapter); } } } if (relevantAdapters.length) { const sortedAdapters = []; while (relevantAdapters.length) { let oneAdapterAdded = false; // create ordered list for upgrades for (let i = relevantAdapters.length - 1; i >= 0; i--) { const relAdapter = relevantAdapters[i]; // if new version has no dependencies we can upgrade if (!repo[relAdapter].dependencies && !repo[relAdapter].globalDependencies) { // no deps, simply add it sortedAdapters.push(relAdapter); relevantAdapters.splice(relevantAdapters.indexOf(relAdapter), 1); oneAdapterAdded = true; } else { /** @type {Record<string, string>} */ const allDeps = { ...tools.parseDependencies(repo[relAdapter].dependencies), ...tools.parseDependencies(repo[relAdapter].globalDependencies) }; // we have to check if the deps are there let conflict = false; for (const [depName, version] of Object.entries(allDeps)) { debug(`adapter "${relAdapter}" has dependency "${depName}": "${version}"`); if (version !== '*') { // dependency is important, because it affects version range if (relevantAdapters.includes(depName)) { // the dependency is also in the upgrade list and not previously added, we should add the dependency first debug(`conflict for dependency "${depName}" at adapter "${relAdapter}"`); conflict = true; break; } } } // we reached here and no conflict so every dep is satisfied if (!conflict) { sortedAdapters.push(relAdapter); relevantAdapters.splice(relevantAdapters.indexOf(relAdapter), 1); oneAdapterAdded = true; } } } if (!oneAdapterAdded) { // no adapter during this loop -> circular dependency console.warn(`Circular dependency detected between adapters "${relevantAdapters.join(', ')}"`); sortedAdapters.concat(relevantAdapters); break; // however, break and try to update } } debug(`upgrade order is "${sortedAdapters.join(', ')}"`); for (let i = 0; i < sortedAdapters.length; i++) { if (repo[sortedAdapters[i]] && repo[sortedAdapters[i]].controller) { continue; } await this.upgradeAdapter(repo, sortedAdapters[i], forceDowngrade, autoConfirm, true); } } else { console.log('All adapters are up to date'); } }; /** * Checks that local and global deps are fulfilled else rejects promise * @param {string[]|object[]|object} deps local dependencies - required on this host * @param {string[]|object[]|object} globalDeps global dependencies - required on one of the hosts * @return {Promise<void>} */ async function checkDependencies(deps, globalDeps) { if (!deps && !globalDeps) { return Promise.resolve(); } deps = tools.parseDependencies(deps); globalDeps = tools.parseDependencies(globalDeps); // combine both dependencies const allDeps = { ...deps, ...globalDeps }; // Get all installed adapters let objs; try { objs = await objects.getObjectViewAsync( 'system', 'instance', { startkey: 'system.adapter.', endkey: 'system.adapter.\u9999' }, null ); } catch (err) { return Promise.reject(err); } if (objs && objs.rows && objs.rows.length) { for (const dName in allDeps) { if (dName === 'js-controller') { const version = allDeps[dName]; // Check only if version not *, else we dont have to read io-pack unnecessarily if (version !== '*') { const iopkg_ = fs.readJSONSync(`${__dirname}/../../package.json`); try { if (!semver.satisfies(iopkg_.version, version, { includePrerelease: true })) { return Promise.reject( new Error( `Invalid version of "${dName}". Installed "${iopkg_.version}", required "${version}` ) ); } } catch (err) { console.log(`Can not check js-controller dependency requirement: ${err.message}`); return Promise.reject( new Error( `Invalid version of "${dName}". Installed "${iopkg_.version}", required "${version}` ) ); } } } else { let gInstances = []; let locInstances = []; // if global dep get all instances of adapter if (globalDeps[dName] !== undefined) { gInstances = objs.rows.filter( obj => obj && obj.value && obj.value.common && obj.value.common.name === dName ); } if (deps[dName] !== undefined) { // local dep get all instances on same host locInstances = objs.rows.filter( obj => obj && obj.value && obj.value.common && obj.value.common.name === dName && obj.value.common.host === hostname ); if (locInstances.length === 0) { return Promise.reject(new Error(`Required dependency "${dName}" not found on this host.`)); } } let isFound = false; // we check, that all instances match - respect different local and global dep versions for (const instance of locInstances) { try { if ( !semver.satisfies(instance.value.common.version, deps[dName], { includePrerelease: true }) ) { return Promise.reject( new Error( `Invalid version of "${dName}". Installed "${instance.value.common.version}", required "${deps[dName]}` ) ); } } catch (err) { console.log(`Can not check dependency requirement: ${err.message}`); return Promise.reject( new Error( `Invalid version of "${dName}". Installed "${instance.value.common.version}", required "${deps[dName]}` ) ); } isFound = true; } for (const instance of gInstances) { try { if ( !semver.satisfies(instance.value.common.version, globalDeps[dName], { includePrerelease: true }) ) { return Promise.reject( new Error( `Invalid version of "${dName}". Installed "${instance.value.common.version}", required "${globalDeps[dName]}` ) ); } } catch (err) { console.log(`Can not check dependency requirement: ${err.message}`); return Promise.reject( new Error( `Invalid version of "${dName}". Installed "${instance.value.common.version}", required "${globalDeps[dName]}` ) ); } isFound = true; } if (isFound === false) { return Promise.reject(new Error(`Required dependency "${dName}" not found.`)); } } } } } /** * Try to async upgrade adapter from given source with some checks * * @param {string|object} repoUrl url of the selected repository or parsed repo * @param {string} adapter name of the adapter * @param {boolean} forceDowngrade flag to force downgrade * @param {boolean} autoConfirm automatically confirm the tty questions (bypass) * @param {boolean} upgradeAll if true, this is an upgrade all call, we don't do major upgrades if no tty */ this.upgradeAdapter = async function (repoUrl, adapter, forceDowngrade, autoConfirm, upgradeAll) { if (!repoUrl || typeof repoUrl !== 'object') { try { repoUrl = await getRepository(repoUrl, params); } catch (e) { return processExit(e); } } const finishUpgrade = async (name, ioPack) => { if (!ioPack) { const adapterDir = tools.getAdapterDir(name); try { ioPack = fs.readJSONSync(`${adapterDir}/io-package.json`); } catch { console.error(`Cannot find io-package.json in ${adapterDir}`); return processExit(EXIT_CODES.MISSING_ADAPTER_FILES); } } // Upload www and admin files of adapter into CouchDB await upload.uploadAdapter(name, false, true); // extend all adapter instance default configs with current config // (introduce potentially new attributes while keeping current settings) await upload.upgradeAdapterObjects(name, ioPack); await upload.uploadAdapter(name, true, true); }; const sources = repoUrl; let version; if (adapter.includes('@')) { const parts = adapter.split('@'); adapter = parts[0]; version = parts[1]; } else { version = ''; } if (version) { forceDowngrade = true; } const adapterDir = tools.getAdapterDir(adapter); // Read actual description of installed adapter with version if (!version && !fs.existsSync(`${adapterDir}/io-package.json`)) { return console.log( `Adapter "${adapter}"${ adapter.length < 15 ? new Array(15 - adapter.length).join(' ') : '' } is not installed.` ); } // Get the url of io-package.json or direct the version if (!repoUrl[adapter]) { console.log(`Adapter "${adapter}" is not in the repository and cannot be updated.`); } if (repoUrl[adapter].controller) { return console.log( `Cannot update ${adapter} using this command. Please use "iobroker upgrade self" instead!` ); } let ioInstalled; if (fs.existsSync(`${adapterDir}/io-package.json`)) { ioInstalled = require(`${adapterDir}/io-package.json`); } if (!ioInstalled) { ioInstalled = { common: { version: '0.0.0' } }; } /** * We show changelog (news) and ask user if he really wants to upgrade but only if fd is associated with a tty, returns true if upgrade desired * @param {string} installedVersion - installed version of adapter * @param {string} targetVersion - target version of adapter * @param {string} adapterName - name of the adapter * @return {boolean} */ const showUpgradeDialog = (installedVersion, targetVersion, adapterName) => { // major upgrade or downgrade const isMajor = semver.major(installedVersion) !== semver.major(targetVersion); tty = tty || require('tty'); if (autoConfirm || (!tty.isatty(process.stdout.fd) && (!isMajor || !upgradeAll))) { // force flag or script on non major or single adapter upgrade -> always upgrade return true; } if (!tty.isatty(process.stdout.fd) && isMajor && upgradeAll) { // no tty and not forced and multiple adapters, do not upgrade console.log(`Skip major upgrade of ${adapterName} from ${installedVersion} to ${targetVersion}`); return false; } const isUpgrade = semver.gt(targetVersion, installedVersion); const isDowngrade = semver.lt(targetVersion, installedVersion); // if information in repo files -> show news if (repoUrl[adapter] && repoUrl[adapter].news) { const news = repoUrl[adapter].news; let first = true; // check if upgrade or downgrade if (isUpgrade) { for (const version in news) { try { if (semver.lte(version, targetVersion) && semver.gt(version, installedVersion)) { if (first === true) { const noMissingNews = news[targetVersion] && news[installedVersion]; console.log( `\nThis upgrade of "${adapter}" will ${ noMissingNews ? '' : 'at least ' }introduce the following changes:` ); console.log( '==========================================================================' ); first = false; } else if (first === false) { console.log(); } console.log(`-> ${version}:`); console.log(news[version].en); } } catch { // ignore } } } else if (isDowngrade) { for (const version in news) { try { if (semver.gt(version, targetVersion) && semver.lte(version, installedVersion)) { if (first === true) { const noMissingNews = news[targetVersion] && news[installedVersion]; console.log( `\nThis downgrade of "${adapter}" will ${ noMissingNews ? '' : 'at least ' }remove the following changes:` ); console.log( '==========================================================================' ); first = false; } else if (first === false) { console.log(); } console.log(`-> ${version}`); console.log(news[version].en); } } catch { // ignore } } } if (first === false) { console.log('==========================================================================\n'); } } rl = rl || require('readline-sync'); let answer; // ask user if he really wants to upgrade/downgrade/reinstall - repeat until (y)es or (n)o given do { if (isUpgrade || isDowngrade) { if (isMajor) { console.log( `BE CAREFUL: THIS IS A MAJOR ${ isUpgrade ? 'UPGRADE' : 'DOWNGRADE' }, WHICH WILL MOST LIKELY INTRODUCE BREAKING CHANGES!` ); } answer = rl.question( `Would you like to ${isUpgrade ? 'upgrade' : 'downgrade'} ${adapter} from @${ ioInstalled.common.version } to @${version || repoUrl[adapter].version} now? [(y)es, (n)o]: `, { defaultInput: 'n' } ); } else { answer = rl.question( `Would you like to reinstall version ${ version || repoUrl[adapter].version } of ${adapter} now? [(y)es, (n)o]: `, { defaultInput: 'n' } ); } answer = answer.toLowerCase(); if (answer === 'n' || answer === 'no') { return false; } } while (answer !== 'y' && answer !== 'yes'); return true; }; // If version is included in repository if (repoUrl[adapter].version) { if (!forceDowngrade) { try { await checkDependencies(repoUrl[adapter].dependencies, repoUrl[adapter].globalDependencies); } catch (err) { return console.error(`Cannot check dependencies: ${err.message}`); } } if ( !forceDowngrade && (repoUrl[adapter].version === ioInstalled.common.version || tools.upToDate(repoUrl[adapter].version, ioInstalled.common.version)) ) { return console.log( `Adapter "${adapter}"${ adapter.length < 15 ? new Array(15 - adapter.length).join(' ') : '' } is up to date.` ); } else { const targetVersion = version || repoUrl[adapter].version; try { if (!showUpgradeDialog(ioInstalled.common.version, targetVersion, adapter)) { return console.log(`No upgrade of "${adapter}" desired.`); } } catch (err) { console.log(`Can not check version information to display upgrade infos: ${err.message}`); } console.log(`Update ${adapter} from @${ioInstalled.common.version} to @${targetVersion}`); // Get the adapter from web site const { packetName, stoppedList } = await install.downloadPacket( sources, `${adapter}@${targetVersion}` ); await finishUpgrade(packetName); await install.enableInstances(stoppedList, true); } } else if (repoUrl[adapter].meta) { // Read repository from url or file const ioPack = await tools.getJsonAsync(repoUrl[adapter].meta); if (!ioPack) { return console.error(`Cannot parse file${repoUrl[adapter].meta}`); } if (!forceDowngrade) { try { await checkDependencies( ioPack.common && ioPack.common.dependencies, ioPack.common && ioPack.common.globalDependencies ); } catch (err) { return console.error(`Cannot check dependencies: ${err.message}`); } } if ( !version && (ioPack.common.version === ioInstalled.common.version || (!forceDowngrade && tools.upToDate(ioPack.common.version, ioInstalled.common.version))) ) { console.log( `Adapter "${adapter}"${ adapter.length < 15 ? new Array(15 - adapter.length).join(' ') : '' } is up to date.` ); } else { // Get the adapter from web site const targetVersion = version || ioPack.common.version; try { if (!showUpgradeDialog(ioInstalled.common.version, targetVersion, adapter)) { return console.log(`No upgrade of "${adapter}" desired.`); } } catch (err) { console.log(`Can not check version information to display upgrade infos: ${err.message}`); } console.log(`Update ${adapter} from @${ioInstalled.common.version} to @${targetVersion}`); const { packetName, stoppedList } = await install.downloadPacket( sources, `${adapter}@${targetVersion}` ); await finishUpgrade(packetName, ioPack); await install.enableInstances(stoppedList, true); } } else { if (forceDowngrade) { try { if (!showUpgradeDialog(ioInstalled.common.version, version, adapter)) { return console.log(`No upgrade of "${adapter}" desired.`); } } catch (err) { console.log(`Can not check version information to display upgrade infos: ${err.message}`); } console.warn(`Unable to get version for "${adapter}". Update anyway.`); console.log(`Update ${adapter} from @${ioInstalled.common.version} to @${version}`); // Get the adapter from web site const { packetName, stoppedList } = await install.downloadPacket(sources, `${adapter}@${version}`); await finishUpgrade(packetName); await install.enableInstances(stoppedList, true); } else { return console.error(`Unable to get version for "${adapter}".`); } } }; /** * Upgrade the js-controller * * @param {string} repoUrl * @param {boolean} forceDowngrade * @param {boolean} controllerRunning * @return {Promise<void>} */ this.upgradeController = async function (repoUrl, forceDowngrade, controllerRunning) { if (!repoUrl || typeof repoUrl !== 'object') { try { const result = await getRepository(repoUrl, params); if (!result) { return console.warn(`Cannot get repository under "${repoUrl}"`); } repoUrl = result; } catch (err) { return processExit(err); } } const installed = fs.readJSONSync(`${__dirname}/../../io-package.json`); if (!installed || !installed.common || !installed.common.version) { return console.error( `Host "${hostname}"${hostname.length < 15 ? ''.padStart(15 - hostname.length) : ''} is not installed.` ); } if (!repoUrl[installed.common.name]) { // no info for controller return console.error(`Cannot find this controller "${installed.common.name}" in repository.`); } if (repoUrl[installed.common.name].version) { if ( !forceDowngrade && (repoUrl[installed.common.name].version === installed.common.version || tools.upToDate(repoUrl[installed.common.name].version, installed.common.version)) ) { console.log( `Host "${hostname}"${ hostname.length < 15 ? new Array(15 - hostname.length).join(' ') : '' } is up to date.` ); } else if (controllerRunning) { console.warn(`Controller is running. Please stop ioBroker first.`); } else { console.log( `Update ${installed.common.name} from @${installed.common.version} to @${ repoUrl[installed.common.name].version }` ); // Get the controller from web site await install.downloadPacket( repoUrl, `${installed.common.name}@${repoUrl[installed.common.name].version}`, { stopDb: true } ); } } else { const ioPack = await tools.getJsonAsync(repoUrl[installed.common.name].meta); if ((!ioPack || !ioPack.common) && !forceDowngrade) { return console.warn( `Cannot read version. Write "${tools.appName} upgrade self --force" to upgrade controller anyway.` ); } let version = ioPack && ioPack.common ? ioPack.common.version : ''; if (version) { version = `@${version}`; } if ( (ioPack && ioPack.common && ioPack.common.version === installed.common.version) || (!forceDowngrade && ioPack && ioPack.common && tools.upToDate(ioPack.common.version, installed.common.version)) ) { console.log( `Host "${hostname}"${ hostname.length < 15 ? new Array(15 - hostname.length).join(' ') : '' } is up to date.` ); } else if (controllerRunning) { console.warn(`Controller is running. Please stop ioBroker first.`); } else { const name = ioPack && ioPack.common && ioPack.common.name ? ioPack.common.name : installed.common.name; console.log(`Update ${name} from @${installed.common.version} to ${version}`); // Get the controller from web site await install.downloadPacket(repoUrl, name + version, { stopDb: true }); } } }; } module.exports = Upgrade;