From 43f41b814caded688876a07b1fc2a2aa9ea58a17 Mon Sep 17 00:00:00 2001 From: Moritz Heusinger Date: Thu, 18 Mar 2021 12:11:26 +0100 Subject: [PATCH] sort adapters by their dependencies - upgrade adapters w/o dependecies and with irrelevant dependencies first - then the dependent adapters - then the adapters which need the dependent adapters - no longer list 'is-up-to-date' + 'not installed' infos for all adapters anymore, use 'iob update' if you need this - use debug module as described in #309 - maybe we can also close this issue if we spread debug like here in more places in the future - implements #1015 --- lib/setup.js | 2 +- lib/setup/setupUpgrade.js | 126 ++++++++++++++++++++++++++++++++------ 2 files changed, 108 insertions(+), 20 deletions(-) diff --git a/lib/setup.js b/lib/setup.js index 31c6341f94..d17be25ae6 100644 --- a/lib/setup.js +++ b/lib/setup.js @@ -987,7 +987,7 @@ function processCommand(command, args, params, callback) { } if (links) { result.sort(); - upgrade.upgradeAdapterHelper(links, result, 0, false, params.y || params.yes, callback); + upgrade.upgradeAdapterHelper(links, result, false, params.y || params.yes, callback); } else { // No information return void callback(EXIT_CODES.INVALID_REPO); diff --git a/lib/setup/setupUpgrade.js b/lib/setup/setupUpgrade.js index 1841985b91..6a3a4f53d5 100644 --- a/lib/setup/setupUpgrade.js +++ b/lib/setup/setupUpgrade.js @@ -8,39 +8,40 @@ */ 'use strict'; +const debug = require('debug')('iobroker:cli'); /** @class */ function Upgrade(options) { - const fs = require('fs-extra'); - const tools = require('../tools.js'); + const fs = require('fs-extra'); + const tools = require('../tools.js'); options = options || {}; - if (!options.processExit) { + if (!options.processExit) { throw new Error('Invalid arguments: processExit is missing'); } - if (!options.installNpm) { + if (!options.installNpm) { throw new Error('Invalid arguments: installNpm is missing'); } if (!options.restartController) { throw new Error('Invalid arguments: restartController is missing'); } - if (!options.getRepository) { + if (!options.getRepository) { throw new Error('Invalid arguments: getRepository is missing'); } - const processExit = options.processExit; - const installNpm = options.installNpm; + const processExit = options.processExit; + const installNpm = options.installNpm; const restartController = options.restartController; - const getRepository = options.getRepository; - const params = options.params; - const objects = options.objects; + 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 hostname = tools.getHostName(); const EXIT_CODES = require('../exitCodes'); const Upload = require('./setupUpload.js'); @@ -50,29 +51,116 @@ function Upgrade(options) { const install = new Install(options); /** - * Upgrade multiple adapters from the given repository url + * Sorts the adapters by their dependencies and then upgrades multiple adapters from the given repository url * - * @param {string} repoUrl url of the selected repository + * @param {object} repo the repository content * @param {string[]} list list of adapters to upgrade - * @param {number} i current index used by list * @param {boolean} forceDowngrade flag to force downgrade * @param {boolean} autoConfirm automatically confirm the tty questions (bypass) * @param {function} callback callback to be executed after function is finished */ - this.upgradeAdapterHelper = function (repoUrl, list, i, forceDowngrade, autoConfirm, callback) { + this.upgradeAdapterHelper = (repo, list, forceDowngrade, autoConfirm, callback) => { if (typeof autoConfirm === 'function') { callback = autoConfirm; autoConfirm = false; } - this.upgradeAdapter(repoUrl, list[i], forceDowngrade, autoConfirm, true, () => { + 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) { + 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); + } else { + const deps = repo[relAdapter].dependencies || []; + const globalDeps = repo[relAdapter].globalDependencies || []; + + // we have to check if the deps are there + let conflict = false; + for (const dep of [...deps, ...globalDeps]) { + debug(`adapter "${relAdapter}" has dependency "${JSON.stringify(dep)}"`); + if (tools.isObject(dep)) { + // object -> dependency is important, because it affects version range + for (const depName of Object.keys(dep)) { + 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; + } + } + } + // else it is a string, so doesn't matter because dep is also there before upgrade + } + // 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(', ')}"`); + this._upgradeAdapterHelper(repo, sortedAdapters, 0, forceDowngrade, autoConfirm, callback); + } else { + console.log('All adapters are up to date'); + if (typeof callback === 'function') { + callback(); + } + } + }; + + /** + * Upgrade multiple adapters from the given repository url + * + * @param {object} repo the repository content + * @param {string[]} list list of adapters to upgrade + * @param {number} i current index used by list + * @param {boolean} forceDowngrade flag to force downgrade + * @param {boolean} autoConfirm automatically confirm the tty questions (bypass) + * @param {function} callback callback to be executed after function is finished + */ + this._upgradeAdapterHelper = (repo, list, i, forceDowngrade, autoConfirm, callback) => { + this.upgradeAdapter(repo, list[i], forceDowngrade, autoConfirm, true, () => { i++; - while (repoUrl[list[i]] && repoUrl[list[i]].controller) { + while (repo[list[i]] && repo[list[i]].controller) { i++; } if (list[i]) { - setImmediate(() => this.upgradeAdapterHelper(repoUrl, list, i, forceDowngrade, autoConfirm, callback)); + setImmediate(() => this._upgradeAdapterHelper(repo, list, i, forceDowngrade, autoConfirm, callback)); } else if (callback) { callback(); } @@ -129,7 +217,7 @@ function Upgrade(options) { 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) { + if (locInstances.length === 0) { return Promise.reject(new Error(`Required dependency "${dName}" not found on this host.`)); } }