UNPKG

iobroker.js-controller

Version:

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

1,853 lines (1,657 loc) • 77.5 kB
/** * Install adapter * * Copyright 2013-2022 bluefox <dogafox@gmail.com> * * MIT License * */ 'use strict'; /** @class */ function Install(options) { const { tools, EXIT_CODES } = require('@iobroker/js-controller-common'); const fs = require('fs-extra'); const hostname = tools.getHostName(); const path = require('path'); const semver = require('semver'); const child_process = require('child_process'); const axios = require('axios'); const PacketManager = require('./setupPacketManager'); const osPlatform = require('os').platform(); const deepClone = require('deep-clone'); const { URL } = require('url'); // todo solve it somehow const unsafePermAlways = [ `${tools.appName.toLowerCase()}.zwave`, `${tools.appName.toLowerCase()}.amazon-dash`, `${tools.appName.toLowerCase()}.xbox` ]; const isRootOnUnix = typeof process.getuid === 'function' && process.getuid() === 0; options = options || {}; if (!options.states) { throw new Error('Invalid arguments: states is missing'); } if (!options.objects) { throw new Error('Invalid arguments: objects is missing'); } if (!options.processExit) { throw new Error('Invalid arguments: processExit is missing'); } if (!options.getRepository) { throw new Error('Invalid arguments: getRepository is missing'); } const objects = options.objects; const states = options.states; const processExit = options.processExit; const getRepository = options.getRepository; const params = options.params || {}; let mime; let packetManager; // TODO: promisify States at some point /** @type {(stateId: string) => Promise<void>} */ const delStateAsync = tools.promisify(states.delState, states); /** @type {(pattern: string) => Promise<string[]>} */ const getKeysAsync = tools.promisify(states.getKeys, states); const tarballRegex = /\/tarball\/[^/]+$/; const Upload = require('./setupUpload'); const upload = new Upload(options); /** * Enables or disables given instances * * @param {ioBroker.InstanceObject[]} instances * @param {boolean} enabled * @return {Promise<void>} */ this.enableInstances = async function (instances, enabled) { if (instances && instances.length) { const ts = Date.now(); for (const instance of instances) { const updatedObj = { common: { enabled }, from: `system.host.${tools.getHostName()}.cli`, ts }; console.log(`host.${hostname} Adapter "${instance._id}" is ${enabled ? 'started' : 'stopped.'}`); await objects.extendObjectAsync(instance._id, updatedObj); } } }; /** * Download given packet * * @param {string} repoUrl * @param {string} packetName * @param {Record<string, any>?} options, { stopDb: true } - will stop the db before upgrade ONLY use it for controller upgrade - * db is gone afterwards, does not work with stoppedList * @param {ioBroker.InstanceObject[]?} stoppedList * @return {Promise<Record<string, any>>} */ this.downloadPacket = async function (repoUrl, packetName, options, stoppedList) { let url; if (!options || typeof options !== 'object') { options = {}; } stoppedList = stoppedList || []; if (!repoUrl || typeof repoUrl !== 'object') { try { repoUrl = await getRepository(repoUrl, params); } catch (err) { return processExit(err); } } if (options.stopDb && stoppedList.length) { console.warn('[downloadPacket] stoppedList cannot be used if stopping of databases is requested'); stoppedList = []; } const debug = process.argv.includes('--debug'); let version; // check if the adapter has format adapter@1.0.0 if (packetName.includes('@')) { const parts = packetName.split('@'); packetName = parts[0]; version = parts[1]; } else { // always take version from repository if (repoUrl[packetName] && repoUrl[packetName].version) { version = repoUrl[packetName].version; } else { version = ''; } } options.packetName = packetName; const sources = repoUrl; options.unsafePerm = sources[packetName] && sources[packetName].unsafePerm; // Check if flag stopBeforeUpdate is true or on windows we stop because of issue #1436 if ( ((sources[packetName] && sources[packetName].stopBeforeUpdate) || process.platform === 'win32') && !stoppedList.length ) { stoppedList = await this._getInstancesOfAdapter(packetName); await this.enableInstances(stoppedList, false); } // try to extract the information from local sources-dist.json if (!sources[packetName]) { try { const sourcesDist = fs.readJsonSync(`${tools.getControllerDir()}/conf/sources-dist.json`); sources[packetName] = sourcesDist[packetName]; } catch { // OK } } if (sources[packetName]) { url = sources[packetName].url; if ( url && packetName === 'js-controller' && fs.pathExistsSync(`${tools.getControllerDir()}/../../node_modules/${tools.appName}.js-controller`) ) { url = null; } if (!url && packetName !== 'example') { if (options.stopDb) { if (objects && objects.destroy) { await objects.destroy(); console.log('Stopped Objects DB'); } if (states && states.destroy) { await states.destroy(); console.log('Stopped States DB'); } } // Install node modules await this._npmInstallWithCheck( `${tools.appName.toLowerCase()}.${packetName}${version ? `@${version}` : ''}`, options, debug ); return { packetName, stoppedList }; } else if (url && url.match(tarballRegex)) { if (options.stopDb) { if (objects && objects.destroy) { await objects.destroy(); console.log('Stopped Objects DB'); } if (states && states.destroy) { await states.destroy(); console.log('Stopped States DB'); } } // Install node modules await this._npmInstallWithCheck(url, options, debug); return { packetName, stoppedList }; } else if (!url) { // Adapter console.warn( `host.${hostname} Adapter "${packetName}" can be updated only together with ${tools.appName}.js-controller` ); return { packetName, stoppedList }; } } console.error( `host.${hostname} Unknown packetName ${packetName}. Please install packages from outside the repository using npm!` ); return processExit(EXIT_CODES.UNKNOWN_PACKET_NAME); }; /** * Install npm module from url * * @param {string} npmUrl * @param {object} options * @param {boolean} debug * @return {Promise<undefined|{installDir: string, _url: string}>} * @private */ this._npmInstallWithCheck = async function (npmUrl, options, debug) { // Get npm version try { let npmVersion; try { npmVersion = child_process.execSync('npm -v', { encoding: 'utf8' }); if (npmVersion) { npmVersion = semver.valid(npmVersion.trim()); } console.log('NPM version: ' + npmVersion); } catch (err) { console.error(`Error trying to check npm version: ${err.message}`); } if (!npmVersion) { console.error('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!'); console.error('Aborting install because the npm version could not be checked!'); console.error('Please check that npm is installed correctly.'); console.error( 'Use "npm install -g npm@4" or "npm install -g npm@latest" to install a supported version.' ); console.error( 'You need to make sure to repeat this step after installing an update to NodeJS and/or npm' ); console.error('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!'); return processExit(EXIT_CODES.INVALID_NPM_VERSION); } else if (semver.gte(npmVersion, '5.0.0') && semver.lt(npmVersion, '5.7.1')) { console.error('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!'); console.error('NPM 5 is only supported starting with version 5.7.1!'); console.error('Please use "npm install -g npm@4" to downgrade npm to 4.x or '); console.error('use "npm install -g npm@latest" to install a supported version of npm!'); console.error( 'You need to make sure to repeat this step after installing an update to NodeJS and/or npm' ); console.error('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!'); return processExit(EXIT_CODES.INVALID_NPM_VERSION); } } catch (err) { console.error(`Could not check npm version: ${err.message}`); console.error('Assuming that correct version is installed.'); } try { return await this._npmInstall(npmUrl, options, debug); } catch (err) { console.error(`Could not install ${npmUrl}: ${err.message}`); } }; this._npmInstall = async function (npmUrl, options, debug) { if (typeof options !== 'object') { options = {}; } // zwave for example requires always unsafe-perm option if (unsafePermAlways.some(adapter => npmUrl.includes(adapter))) { options.unsafePerm = true; } else if (isRootOnUnix) { // If ioBroker or the CLI is executed as root on unix platforms, // not providing the --unsafe-perm options means that every pre/postinstall // script fails when it uses npm commands. options.unsafePerm = true; } console.log(`Installing ${npmUrl}... (System call)`); const result = await tools.installNodeModule(npmUrl, { debug: !!debug, unsafePerm: !!options.unsafePerm }); if (result.success || result.exitCode === 1) { // code 1 is strange error that cannot be explained. Everything is installed but error :( // Determine where the packet would be installed if npm succeeds // TODO: There's probably a better way to figure this out /** @type {string} */ let packetDirName; if (options.packetName) { packetDirName = tools.appName.toLowerCase() + '.' + options.packetName; } else { packetDirName = npmUrl.toLowerCase(); // If the user installed a git commit-ish, the url contains stuff that doesn't belong in a folder name // e.g. iobroker/iobroker.javascript#branch-name if (packetDirName.includes('#')) { packetDirName = packetDirName.substr(0, packetDirName.indexOf('#')); } if (packetDirName.includes('/') && !packetDirName.startsWith('@')) { // only scoped packages (e.g. @types/node ) may have a slash in their path packetDirName = packetDirName.substr(packetDirName.lastIndexOf('/') + 1); } } const installDir = tools.getAdapterDir(packetDirName); // inject the installedFrom information in io-package if (fs.existsSync(installDir)) { const ioPackPath = path.join(installDir, 'io-package.json'); let iopack; try { iopack = fs.readJSONSync(ioPackPath); } catch { iopack = null; } if (iopack) { iopack.common = iopack.common || {}; iopack.common.installedFrom = npmUrl; try { fs.writeFileSync(ioPackPath, JSON.stringify(iopack, null, 2), 'utf8'); } catch { // OK } } } else { // TODO: revisit this - this should not happen console.error(`host.${hostname} Cannot install ${npmUrl}: ${result.exitCode}`); return processExit(EXIT_CODES.CANNOT_INSTALL_NPM_PACKET); } // create file that indicates, that npm was called there fs.writeFileSync(path.join(installDir, 'iob_npm.done'), ' '); // command succeeded return { _url: npmUrl, installDir: path.dirname(installDir) }; } else { console.error(`host.${hostname} Cannot install ${npmUrl}: ${result.exitCode}`); return processExit(EXIT_CODES.CANNOT_INSTALL_NPM_PACKET); } }; /** @type {(packageName: string, options: any, debug: boolean, callback?: (err?: Error) => void) => Promise<void>} */ this._npmUninstall = async function (packageName, options, debug) { const result = await tools.uninstallNodeModule(packageName, { debug: !!debug }); if (!result.success) { throw new Error(`host.${hostname}: Cannot uninstall ${packageName}: ${result.exitCode}`); } }; // this command is executed always on THIS host this._checkDependencies = async (adapter, deps, globalDeps, _options) => { if (!deps && !globalDeps) { return adapter; } deps = tools.parseDependencies(deps); globalDeps = tools.parseDependencies(globalDeps); // combine both dependencies const allDeps = { ...deps, ...globalDeps }; // Get all installed adapters const objs = await objects.getObjectViewAsync('system', 'instance', { startkey: 'system.adapter.', endkey: 'system.adapter.\u9999' }); if (objs && objs.rows && objs.rows.length) { for (const dName in allDeps) { let isFound = false; 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`); if (!semver.satisfies(iopkg_.version, version, { includePrerelease: true })) { console.error( `host.${hostname} Invalid version of "${dName}". Installed "${iopkg_.version}", required "${version}"` ); return processExit(EXIT_CODES.INVALID_DEPENDENCY_VERSION); } else { isFound = true; } } else { isFound = true; } } if (!isFound) { 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) { console.error(`host.${hostname} Required dependency "${dName}" not found on this host.`); } } // we check, that all existing instances match - respect different versions for local and global deps for (const instance of locInstances) { if ( !semver.satisfies(instance.value.common.version, deps[dName], { includePrerelease: true }) ) { console.error( `host.${hostname} Invalid version of "${dName}". Installed "${instance.value.common.version}", required "${deps[dName]}"` ); return processExit(EXIT_CODES.INVALID_DEPENDENCY_VERSION); } else { isFound = true; } } for (const instance of gInstances) { if ( !semver.satisfies(instance.value.common.version, globalDeps[dName], { includePrerelease: true }) ) { console.error( `host.${hostname} Invalid version of "${dName}". Installed "${instance.value.common.version}", required "${globalDeps[dName]}"` ); return processExit(EXIT_CODES.INVALID_DEPENDENCY_VERSION); } else { isFound = true; } } } // if required dependency not found => install it if (!isFound) { await this.createInstance(dName, _options); } } } }; this._uploadStaticObjects = async function (adapter, adapterConf) { if (!adapterConf) { const adapterDir = tools.getAdapterDir(adapter); if (!fs.existsSync(`${adapterDir}/io-package.json`)) { console.error(`host.${hostname} Adapter directory "${adapterDir}" does not exists`); throw new Error(EXIT_CODES.CANNOT_FIND_ADAPTER_DIR); } try { adapterConf = fs.readJSONSync(adapterDir + '/io-package.json'); } catch (err) { console.error(`host.${hostname} error: reading io-package.json ${err.message}`, adapter); throw new Error(EXIT_CODES.CANNOT_FIND_ADAPTER_DIR); } } let objs; if (adapterConf.objects && adapterConf.objects.length > 0) { objs = adapterConf.objects; } else { objs = []; } // check if some dependencies are missing and install them if some found await this._checkDependencies( adapter, adapterConf.common.dependencies, adapterConf.common.globalDependencies, params ); adapterConf.common.installedVersion = adapterConf.common.version; if (adapterConf.common.news) { delete adapterConf.common.news; // remove this information as it will be taken from repo } objs.push({ _id: `system.adapter.${adapterConf.common.name}`, type: 'adapter', common: adapterConf.common, native: adapterConf.native }); if (objs && objs.length) { for (let i = 0; i < objs.length; i++) { const obj = objs[i]; obj.from = `system.host.${tools.getHostName()}.cli`; obj.ts = Date.now(); try { await objects.extendObjectAsync(obj._id, obj); } catch (err) { console.error(`host.${hostname} error setObject ${obj._id} ${err.message}`); return EXIT_CODES.CANNOT_SET_OBJECT; } console.log(`host.${hostname} object ${obj._id} created/updated`); } } }; /** * Installs given adapter * * @param {string} adapter * @param {string?} repoUrl * @param {number?} _installCount * @return {Promise<string>} */ this.installAdapter = async (adapter, repoUrl, _installCount) => { _installCount = _installCount || 0; const fullName = adapter; if (adapter.includes('@')) { adapter = adapter.split('@')[0]; } const adapterDir = tools.getAdapterDir(adapter); console.log(`host.${hostname} install adapter ${fullName}`); if (!fs.existsSync(adapterDir + '/io-package.json')) { if (_installCount === 2) { console.error(`host.${hostname} Cannot install ${adapter}`); return processExit(EXIT_CODES.CANNOT_INSTALL_NPM_PACKET); } _installCount++; const { stoppedList } = await this.downloadPacket(repoUrl, fullName); await this.installAdapter(adapter, repoUrl, _installCount); await this.enableInstances(stoppedList, true); // even if unlikely make sure to reenable disabled instances return adapter; } let adapterConf; try { adapterConf = fs.readJSONSync(adapterDir + '/io-package.json'); } catch (err) { console.error(`host.${hostname} error: reading io-package.json ${err.message}`); return processExit(EXIT_CODES.INVALID_IO_PACKAGE_JSON); } // Check if the operation system is ok if (adapterConf.common && adapterConf.common.os) { if (typeof adapterConf.common.os === 'string' && adapterConf.common.os !== osPlatform) { console.error( `host.${hostname} Adapter does not support current os. Required ${adapterConf.common.os}. Actual platform: ${osPlatform}` ); return processExit(EXIT_CODES.INVALID_OS); } else { if (!adapterConf.common.os.includes(osPlatform)) { console.error( `host.${hostname} Adapter does not support current os. Required one of ${adapterConf.common.os.join( ', ' )}. Actual platform: ${osPlatform}` ); return processExit(EXIT_CODES.INVALID_OS); } } } let engineVersion; try { // read directly from disk and not via require to allow "on the fly" updates of adapters. const p = JSON.parse(fs.readFileSync(adapterDir + '/package.json', 'utf8')); engineVersion = p && p.engines && p.engines.node; } catch { console.error(`host.${hostname}: Cannot read and parse "${adapterDir}/package.json"`); } // check node.js version if defined in package.json if (engineVersion) { if (!semver.satisfies(process.version.replace(/^v/, ''), engineVersion)) { console.error( `host.${hostname} Adapter does not support current nodejs version. Required ${engineVersion}. Actual version: ${process.version}` ); return processExit(EXIT_CODES.INVALID_NODE_VERSION); } } if (adapterConf.common.osDependencies && adapterConf.common.osDependencies[process.platform]) { // install linux/osx libraries try { packetManager = packetManager || new PacketManager(); await packetManager.install(adapterConf.common.osDependencies[process.platform]); } catch (err) { console.error(`host.${hostname} Could not install required OS packages: ${err.message}`); } } await upload.uploadAdapter(adapter, true, true); await upload.uploadAdapter(adapter, false, true); await callInstallOfAdapter(adapter, adapterConf); await this._uploadStaticObjects(adapter); await upload.upgradeAdapterObjects(adapter); return adapter; }; async function callInstallOfAdapter(adapter, config) { if (config.common.install) { // Install node modules const { exec } = require('child_process'); let cmd = 'node '; let fileFullName; try { fileFullName = await tools.resolveAdapterMainFile(adapter); } catch { return; } return new Promise(resolve => { cmd += `"${fileFullName}" --install`; console.log(`host.${hostname} command: ${cmd}`); const child = exec(cmd, { windowsHide: true }); tools.pipeLinewise(child.stderr, process.stdout); child.on('exit', () => resolve(adapter)); }); } } /** * Create adapter instance * * @param {string} adapter * @param {object?} options - enabled, host, port * @return {Promise<void>} */ this.createInstance = async function (adapter, options) { let ignoreIfExists = false; options = options || {}; options.host = options.host || tools.getHostName(); if (options.enabled === 'true') { options.enabled = true; } if (options.enabled === 'false') { options.enabled = false; } if (options.ignoreIfExists !== undefined) { ignoreIfExists = !!options.ignoreIfExists; delete options.ignoreIfExists; } mime = mime || require('mime'); let doc; let err; try { doc = await objects.getObjectAsync(`system.adapter.${adapter}`); } catch (_err) { err = _err; } // Adapter is not installed - install it now if (err || !doc || !doc.common.installedVersion) { await this.installAdapter(adapter); doc = await objects.getObjectAsync(`system.adapter.${adapter}`); } // Check if some web pages should be uploaded await upload.uploadAdapter(adapter, true, false); await upload.uploadAdapter(adapter, false, false); const res = await objects.getObjectViewAsync('system', 'instance', { startkey: `system.adapter.${adapter}.`, endkey: `system.adapter.${adapter}.\u9999` }); const systemConfig = await objects.getObjectAsync('system.config'); const defaultLogLevel = systemConfig && systemConfig.common && systemConfig.common.defaultLogLevel; if (!res) { console.error(`host.${hostname} error: view instanceStats`); return processExit(EXIT_CODES.CANNOT_READ_INSTANCES); } // Count started instances if (doc.common.singleton && res.rows.length) { if (ignoreIfExists) { return; } console.error(`host.${hostname} error: this adapter does not allow multiple instances`); return processExit(EXIT_CODES.NO_MULTIPLE_INSTANCES_ALLOWED); } // check singletonHost one on host if (doc.common.singletonHost) { for (let a = 0; a < res.rows.length; a++) { if (res.rows[a].value.common.host === hostname) { if (ignoreIfExists) { return; } console.error(`host.${hostname} error: this adapter does not allow multiple instances on one host`); return processExit(EXIT_CODES.NO_MULTIPLE_INSTANCES_ALLOWED_ON_HOST); } } } let instance = null; if (options.instance !== undefined) { instance = options.instance; // find max instance if (res.rows.find(obj => parseInt(obj.id.split('.').pop(), 10) === instance)) { console.error(`host.${hostname} error: instance yet exists`); return processExit(EXIT_CODES.INSTANCE_ALREADY_EXISTS); } } else { // find max instance for (let a = 0; a < res.rows.length; a++) { const iInstance = parseInt(res.rows[a].id.split('.').pop(), 10); if (instance === null || iInstance > instance) { instance = iInstance; } } if (instance === null) { instance = 0; } else { instance++; } } const instanceObj = deepClone(doc); instanceObj._id = `system.adapter.${adapter}.${instance}`; instanceObj.type = 'instance'; if (instanceObj.common.news) { delete instanceObj.common.news; // remove this information as it could be big, but it will be taken from repo } if (instanceObj._rev) { delete instanceObj._rev; } instanceObj.common.enabled = options.enabled === true || options.enabled === false ? options.enabled : instanceObj.common.enabled === true || instanceObj.common.enabled === false ? instanceObj.common.enabled : false; instanceObj.common.host = options.host; if (options.port) { instanceObj.native = instanceObj.native || {}; instanceObj.native.port = options.port; } if (instanceObj.common.dataFolder && instanceObj.common.dataFolder.includes('%INSTANCE%')) { instanceObj.common.dataFolder = instanceObj.common.dataFolder.replace(/%INSTANCE%/g, instance); } if (defaultLogLevel) { instanceObj.common.loglevel = defaultLogLevel; } else if (!instanceObj.common.loglevel) { instanceObj.common.loglevel = 'info'; } console.log(`host.${hostname} create instance ${adapter}`); let objs; if (!instanceObj.common.onlyWWW && instanceObj.common.mode !== 'once') { objs = tools.getInstanceIndicatorObjects(`${adapter}.${instance}`, instanceObj.common.wakeup); } else { objs = []; } const adapterDir = tools.getAdapterDir(adapter); if (fs.existsSync(path.join(adapterDir, 'www'))) { objs.push({ _id: `system.adapter.${adapter}.upload`, type: 'state', common: { name: adapter + '.upload', type: 'number', read: true, write: false, role: 'indicator.state', unit: '%', def: 0, desc: 'Upload process indicator' }, native: {} }); } let adapterConf; try { adapterConf = fs.readJSONSync(`${adapterDir}/io-package.json`); } catch (err) { console.error(`host.${hostname} error: reading io-package.json ${err.message}`); return processExit(EXIT_CODES.INVALID_IO_PACKAGE_JSON); } adapterConf.instanceObjects = adapterConf.instanceObjects || []; adapterConf.objects = adapterConf.objects || []; const defStates = []; // Create only for this instance the predefined in io-package.json objects // It is not necessary to write "system.adapter.name.N." in the object '_id' for (let i = 0; i < adapterConf.instanceObjects.length; i++) { adapterConf.instanceObjects[i]._id = `${adapter}.${instance}${ adapterConf.instanceObjects[i]._id ? '.' + adapterConf.instanceObjects[i]._id : '' }`; if (adapterConf.instanceObjects[i].common) { if (adapterConf.instanceObjects[i].common.name) { // if name has many languages if (typeof adapterConf.instanceObjects[i].common.name === 'object') { Object.keys(adapterConf.instanceObjects[i].common.name).forEach( lang => (adapterConf.instanceObjects[i].common.name[lang] = adapterConf.instanceObjects[ i ].common.name[lang].replace('%INSTANCE%', instance)) ); } else { adapterConf.instanceObjects[i].common.name = adapterConf.instanceObjects[i].common.name.replace( '%INSTANCE%', instance ); } } if (adapterConf.instanceObjects[i].common.desc) { // if name has many languages if (typeof adapterConf.instanceObjects[i].common.desc === 'object') { Object.keys(adapterConf.instanceObjects[i].common.desc).forEach( lang => (adapterConf.instanceObjects[i].common.desc[lang] = adapterConf.instanceObjects[ i ].common.desc[lang].replace('%INSTANCE%', instance)) ); } else { adapterConf.instanceObjects[i].common.desc = adapterConf.instanceObjects[i].common.desc.replace( '%INSTANCE%', instance ); } } } objs.push(adapterConf.instanceObjects[i]); if (adapterConf.instanceObjects[i].common && adapterConf.instanceObjects[i].common.def !== undefined) { defStates.push({ id: adapterConf.instanceObjects[i]._id, val: adapterConf.instanceObjects[i].common.def }); } } /* these are already created on adapter install if (adapterConf.objects && adapterConf.objects.length > 0) { for (var j = 0, l = adapterConf.objects.length; j < l; j++) { objs.push(adapterConf.objects[j]); } } */ // upload all objects for (let i = 0; i < objs.length; i++) { const obj = objs[i]; try { tools.validateGeneralObjectProperties(obj); } catch (err) { // todo: in the future we will not create this object console.warn(`host.${hostname} Object ${obj._id} is invalid: ${err.message}`); console.warn( `host.${hostname} This object will not be created in future versions. Please report this to the developer.` ); } obj.from = 'system.host.' + tools.getHostName() + '.cli'; obj.ts = Date.now(); try { await objects.setObjectAsync(obj._id, obj); console.log(`host.${hostname} object ${obj._id} created`); } catch (err) { console.error(`host.${hostname} error: ${err.message}`); } } // sets the default states if any given for (let d = 0; d < defStates.length; d++) { const defState = defStates[d]; defState.ack = true; defState.from = `system.host.${tools.getHostName()}.cli`; try { await states.setStateAsync(defState.id, defState); console.log(`host.${hostname} Set default value of ${defState.id}: ${defState.val}`); } catch (err) { console.error(`host.${hostname} error: ${err.message}`); } } instanceObj.from = `system.host.${tools.getHostName()}.cli`; instanceObj.ts = Date.now(); try { await objects.setObjectAsync(instanceObj._id, instanceObj); console.log(`host.${hostname} object ${instanceObj._id} created`); } catch (err) { console.error(`host.${hostname} error: ${err.message}`); } }; /** * Enumerate all instances of an adapter * @type {(knownObjIDs: string[], notDeleted: any[], adapter: string, instance?: string) => Promise<void>} */ this._enumerateAdapterInstances = async (knownObjIDs, notDeleted, adapter, instance) => { if (!notDeleted) { notDeleted = []; } // We need to filter the instances using RegExp, because the naive approach with startkey/endkey // startkey: system.adapter.mqtt // endkey: system.adapter.mqtt.\u9999 // matches system.adapter.mqtt AND system.adapter.mqtt-client const instanceRegex = instance !== undefined ? new RegExp(`^system\\.adapter\\.${adapter}\\.${instance}$`) : new RegExp(`^system\\.adapter\\.${adapter}\\.\\d+$`); try { const doc = await objects.getObjectView('system', 'instance', { startkey: `system.adapter.${adapter}${instance !== undefined ? `.${instance}` : ''}`, endkey: `system.adapter.${adapter}${instance !== undefined ? `.${instance}` : ''}\u9999` }); // add non-duplicates to the list (if instance not given -> only for this host) const newObjIDs = doc.rows // only the ones with an ID that matches the pattern .filter(row => row && row.value && row.value._id) .filter(row => instanceRegex.test(row.value._id)) // if instance given also delete from foreign host else only instance on this host .filter(row => { if ( instance !== undefined || !row.value.common || !row.value.common.host || row.value.common.host === hostname ) { return true; } else { if (!notDeleted.includes(row.value._id)) { notDeleted.push(row.value._id); } return false; } }) .map(row => row.value._id) .filter(id => !knownObjIDs.includes(id)); knownObjIDs.push.apply(knownObjIDs, newObjIDs); if (newObjIDs.length > 0) { console.log( `host.${hostname} Counted ${newObjIDs.length} instances of ${adapter}${ instance !== undefined ? `.${instance}` : '' }` ); } } catch (err) { err !== tools.ERRORS.ERROR_NOT_FOUND && err.message !== tools.ERRORS.ERROR_NOT_FOUND && console.error(`host.${hostname} error: ${err.message}`); } }; /** * Enumerate all meta objects of an adapter * @type {(knownObjIDs: string[], adapter: string, metaFilesToDelete: string[]) => Promise<void>} */ this._enumerateAdapterMeta = async function (knownObjIDs, adapter, metaFilesToDelete) { try { const doc = await objects.getObjectViewAsync('system', 'meta', { startkey: `${adapter}.`, endkey: `${adapter}.\u9999` }); if (doc.rows.length !== 0) { const adapterRegex = new RegExp(`^${adapter}\\.`); // add non-duplicates to the list const newObjs = doc.rows .filter(row => row && row.value && row.value._id) .map(row => row.value._id) .filter(id => adapterRegex.test(id)) .filter(id => knownObjIDs.indexOf(id) === -1); knownObjIDs.push.apply(knownObjIDs, newObjs); // meta ids can also be present as files metaFilesToDelete.push.apply(metaFilesToDelete, newObjs); if (newObjs.length) { console.log(`host.${hostname} Counted ${newObjs.length} meta of ${adapter}`); } } } catch (err) { err !== tools.ERRORS.ERROR_NOT_FOUND && err.message !== tools.ERRORS.ERROR_NOT_FOUND && console.error(`host.${hostname} error: ${err.message}`); } }; /** * @type {(knownObjIDs: string[], adapter: string) => Promise<number>} * @returns {Promise<number>} 22 if the adapter could not be deleted, 0 otherwise */ this._enumerateAdapters = async (knownObjIDs, adapter) => { // This does not really enumerate the adapters, but finds the adapter object // if it exists and adds it to the list try { const obj = await objects.getObjectAsync(`system.adapter.${adapter}`); if (obj) { if (obj.common && obj.common.nondeletable) { // If the adapter is non-deletable, mark it as not installed console.log( 'host.' + hostname + ' Adapter ' + adapter + ' cannot be deleted completely, because it is marked non-deletable.' ); obj.installedVersion = ''; obj.from = 'system.host.' + tools.getHostName() + '.cli'; obj.ts = Date.now(); await objects.setObjectAsync(obj._id, obj); return EXIT_CODES.CANNOT_DELETE_NON_DELETABLE; } else { // The adapter is deletable, remember it for deletion knownObjIDs.push(obj._id); console.log(`host.${hostname} Counted 1 adapter for ${adapter}`); return EXIT_CODES.NO_ERROR; } } } catch (err) { console.error(`host.${hostname} Cannot enumerate adapters: ${err.message}`); } }; /** * Enumerates the devices of an adapter (or instance) * @param {string[]} knownObjIDs The already known object ids * @param {string} adapter The adapter to enumerate the devices for * @param {string} [instance] The instance to enumerate the devices for (optional) */ this._enumerateAdapterDevices = async (knownObjIDs, adapter, instance) => { const adapterRegex = new RegExp(`^${adapter}${instance ? `\\.${instance}` : ''}\\.`); try { const doc = await objects.getObjectViewAsync('system', 'device', { startkey: `${adapter}${instance !== undefined ? `.${instance}` : ''}`, endkey: `${adapter}${instance !== undefined ? `.${instance}` : ''}\u9999` }); if (doc && doc.rows && doc.rows.length) { // add non-duplicates to the list const newObjs = doc.rows .filter(row => row && row.value && row.value._id) .map(row => row.value._id) .filter(id => adapterRegex.test(id)) .filter(id => !knownObjIDs.includes(id)); knownObjIDs.push.apply(knownObjIDs, newObjs); if (newObjs.length > 0) { console.log( `host.${hostname} Counted ${newObjs.length} devices of ${adapter}${ instance !== undefined ? `.${instance}` : '' }` ); } } } catch (err) { err !== tools.ERRORS.ERROR_NOT_FOUND && err.message !== tools.ERRORS.ERROR_NOT_FOUND && console.error(`host.${hostname} error: ${err.message}`); } }; /** * Enumerates the channels of an adapter (or instance) * @param {string[]} knownObjIDs The already known object ids * @param {string} adapter The adapter to enumerate the channels for * @param {string} [instance] The instance to enumerate the channels for (optional) */ this._enumerateAdapterChannels = async (knownObjIDs, adapter, instance) => { const adapterRegex = new RegExp(`^${adapter}${instance ? `\\.${instance}` : ''}\\.`); try { const doc = await objects.getObjectViewAsync('system', 'channel', { startkey: `${adapter}${instance !== undefined ? `.${instance}` : ''}`, endkey: `${adapter}${instance !== undefined ? `.${instance}` : ''}\u9999` }); if (doc && doc.rows && doc.rows.length) { // add non-duplicates to the list const newObjs = doc.rows .filter(row => row && row.value && row.value._id) .map(row => row.value._id) .filter(id => adapterRegex.test(id)) .filter(id => !knownObjIDs.includes(id)); knownObjIDs.push.apply(knownObjIDs, newObjs); if (newObjs.length > 0) { console.log( `host.${hostname} Counted ${newObjs.length} channels of ${adapter}${ instance ? `.${instance}` : '' }` ); } } } catch (err) { err !== tools.ERRORS.ERROR_NOT_FOUND && err.message !== tools.ERRORS.ERROR_NOT_FOUND && console.error(`host.${hostname} error: ${err.message}`); } }; /** * Enumerates the states of an adapter (or instance) * @param {string[]} knownObjIDs The already known object ids * @param {string} adapter The adapter to enumerate the states for * @param {string} [instance] The instance to enumerate the states for (optional) */ this._enumerateAdapterStateObjects = async (knownObjIDs, adapter, instance) => { const adapterRegex = new RegExp(`^${adapter}${instance ? `\\.${instance}` : ''}\\.`); const sysAdapterRegex = new RegExp(`^system\\.adapter\\.${adapter}${instance ? `\\.${instance}` : ''}\\.`); try { let doc = await objects.getObjectViewAsync('system', 'state', { startkey: `${adapter}${instance !== undefined ? `.${instance}` : ''}`, endkey: `${adapter}${instance !== undefined ? `.${instance}` : ''}\u9999` }); if (doc && doc.rows && doc.rows.length) { // add non-duplicates to the list const newObjs = doc.rows .filter(row => row && row.value && row.value._id) .map(row => row.value._id) .filter(id => adapterRegex.test(id)) .filter(id => !knownObjIDs.includes(id)); knownObjIDs.push.apply(knownObjIDs, newObjs); if (newObjs.length > 0) { console.log( `host.${hostname} Counted ${newObjs.length} states of ${adapter}${ instance ? `.${instance}` : '' }` ); } } doc = await objects.getObjectViewAsync('system', 'state', { startkey: `system.adapter.${adapter}${instance !== undefined ? `.${instance}` : ''}`, endkey: `system.adapter.${adapter}${instance !== undefined ? `.${instance}` : ''}\u9999` }); if (doc && doc.rows && doc.rows.length) { // add non-duplicates to the list const newObjs = doc.rows .filter(row => row && row.value && row.value._id) .map(row => row.value._id) .filter(id => sysAdapterRegex.test(id)) .filter(id => !knownObjIDs.includes(id)); knownObjIDs.push.apply(knownObjIDs, newObjs); if (newObjs.length > 0) { console.log( `host.${hostname} Counted ${newObjs.length} states of system.adapter.${adapter}${ instance ? `.${instance}` : '' }` ); } } } catch (err) { err !== tools.ERRORS.ERROR_NOT_FOUND && err.message !== tools.ERRORS.ERROR_NOT_FOUND && console.error(`host.${hostname} error: ${err.message}`); } }; // TODO: is enumerateAdapterDocs the correct name??? /** * Enumerates the docs of an adapter (or instance) * @param {string[]} knownObjIDs The already known object ids * @param {string} adapter The adapter to enumerate the states for * @param {string} [instance] The instance to enumerate the states for (optional) */ this._enumerateAdapterDocs = async (knownObjIDs, adapter, instance) => { const adapterRegex = new RegExp(`^${adapter}${instance ? `\\.${instance}` : ''}\\.`); const sysAdapterRegex = new RegExp(`^system\\.adapter\\.${adapter}${instance ? `\\.${instance}` : ''}\\.`); try { const doc = await objects.getObjectListAsync({ include_docs: true }); if (doc && doc.rows && doc.rows.length) { // add non-duplicates to the list const newObjs = doc.rows .filter(row => row && row.value && row.value._id) .map(row => row.value._id) .filter(id => adapterRegex.test(id) || sysAdapterRegex.test(id)) .filter(id => !knownObjIDs.includes(id)); knownObjIDs.push.apply(knownObjIDs, newObjs); if (newObjs.length > 0) { console.log( `host.${hostname} Counted ${newObjs.length} objects of ${adapter}${ instance ? `.${instance}` : '' }` ); } } } catch (err) { err !== tools.ERRORS.ERROR_NOT_FOUND && err.message !== tools.ERRORS.ERROR_NOT_FOUND && console.error(`host.${hostname} error: ${err.message}`); } }; /** * Enumerate all state IDs of an adapter (or instance) * @type {(knownStateIDs: string[], adapter: string, instance?: string) => Promise<void>} */ this._enumerateAdapterStates = async (knownStateIDs, adapter, instance) => { for (const pattern of [ `io.${adapter}.${instance ? instance + '.' : ''}*`, `messagebox.${adapter}.${instance ? instance + '.' : ''}*`, `log.${adapter}.${instance ? instance + '.' : ''}*`, `${adapter}.${instance ? instance + '.' : ''}*`, `system.adapter.${adapter}.${instance ? instance + '.' : ''}*` ]) { try { const ids = await getKeysAsync(pattern); if (ids && ids.length) { // add non-duplicates to the list const newStates = ids.filter(id => !knownStateIDs.includes(id)); knownStateIDs.push.apply(knownStateIDs, newStates); if (newStates.length) { console.log(`host.${hostname} Counted ${newStates.length} states (${pattern}) from states`); } } } catch (err) { console.error(`host.${hostname} Cannot get keys async: ${err.message}`); } } }; /** * delete WWW pages, objects and meta files * @type {(adapter: string, metaFilesToDelete: string[]) => Promise<void>} */ this._deleteAdapterFiles = async (adapter, metaFilesToDelete) => { // special files, which are not meta (vis widgets), combined with meta object ids const filesToDelete = [ { id: `vis`, name: `widgets/${adapter}` }, { id: `vis`, name: `widgets/${adapter}.html` }, { id: adapter }, { id: `${adapter}.admin` }, ...metaFilesToDelete.map(id => ({ id })) ]; for (const file of filesToDelete) { const id = typeof file === 'object' ? file.id : file; try { await objects.unlinkAsync(id, file.name || ''); console.log(`host.${hostname} file ${id + (file.name ? `/${file.name}` : '')} deleted`); } catch (err) { err !== tools.ERRORS.ERROR_NOT_FOUND && err.message !== tools.ERRORS.ERROR_NOT_FOUND && console.error(`host.${hostname} Cannot delete ${id} files folder: ${err.message}`); } } for (const objId of [adapter, `${adapter}.admin`]) { try { await objects.delObjectAsync(objId); console.log(`host.${hostname} object ${objId} deleted`); } catch (err) { err !== tools.ERRORS.ERROR_NOT_FOUND && err.message !== tools.ERRORS.ERROR_NOT_FOUND && console.error(`host.${hostname} cannot delete objects: ${err.message}`); } } }; /** * @type {(stateIDs: string[]) => Promise<void>} */ this._deleteAdapterStates = async stateIDs => { if (stateIDs.length > 1000) { console.log(`host.${hostname} Deleting ${stateIDs.length} state(s). Be patient...`); } else if (stateIDs.length) { console.log(`host.${hostname} Deleting ${stateIDs.length} state(s).`); } while (stateIDs.length > 0) { if (stateIDs.length % 200 === 0) { // write progress report console.log(`host.${hostname}: Only ${stateIDs.length} states left to be deleted.`); } // try to delete the current state try { await delStateAsync(stateIDs.pop()); } catch (err) { // yep that works! err !== tools.ERRORS.ERROR_NOT_FOUND && err.message !== tools.ERRORS.ERROR_NOT_FOUND && console.error(`host.${hostname} Cannot delete states: ${err.message}`); } } }; /** * @type {(objIDs: string[]) => Promise<void>} */ this._deleteAdapterObjects = async objIDs => { if (objIDs.length > 1000) { console.log(`host.${hostname} Deleting ${objIDs.length} object(s). Be patient...`); } else if (objIDs.length) { console.log(`host.${hostname} Deleting ${objIDs.length} object(s).`); } let allEnums; if (objIDs.length > 1) { try { // cache all enums, else it will be slow to delete many objects allEnums = await tools.getAllEnums(objects); } catch (e) { console.error(`host.${hostname}: Could not retrieve all enums: ${e.message}`); } } while (objIDs.length > 0) { if (objIDs.length % 200 === 0) { // write progress report console.log(`host.${hostname}: Only ${objIDs.length} objects left to be deleted.`); } // try to delete the current object try { const id = objIDs.pop(); await objects.delObjectAsync(id); await tools.removeIdFromAllEnums(objects, id, allEnums); } catch (err) { err !== tools.ERRORS.ERROR_NOT_FOUND && err.message !== tools.ERRORS.ERROR_NOT_FOUND && console.error(`host.${hostname} cannot delete objects: ${err.message}`); } } }; /** * Deltes given adapter from filesystem and removes all instances * * @param {string} adapter * @return {Promise<number>} */ this.deleteAdapter = async adapter => { const knownObjectIDs = []; const metaFilesToDelete = []; const notDeletedObjectIDs = []; const knownStateIDs = []; let resultCode = EXIT_CODES.NO_ERROR; const _uninstallNpm = async () => { try { // find the adapter's io-package.json const adapterNpm = `${tools.appName}.${adapter}`; const ioPack = require(`${adapterNpm}/io-package.json`); // yep, it's that easy if (!ioPack.common || !ioPack.common.nondeletable) { await this._npmUninstall(adapterNpm, null, false); // after uninstalling we have to restart the defined adapters if (ioPack.common.restartAdapters) { if (!Array.isArray(ioPack.common.restartAdapters)) { // its not an array, now it can only be a single adapter as string if (typeof ioPack.common.restartAdapters !== 'string') { return; } ioPack.common.restartAdapters = [ioPack.common.restartAdapters]; } if (ioPack.common.restartAdapters.length && ioPack.common.restartAdapters[0]) { const instances = await tools.getAllInstances(ioPack.common.restartAdapters, objects); if (instances && instances.length) { for (const instance of instances) { const obj = await objects.getObjectAsync(instance); // if instance is enabled if (obj && obj.common && obj.common.enabled) { try { obj.common.enabled = false; // disable instance obj.from = `system.host.${tools.getHostName()}.cli`; obj.ts = Date.now(); await objects.setObjectAsync(obj._id, obj); obj.common.enabled = true; // enable instance obj.from = `system.host.${tools.getHostName()}.cli`; obj.ts = Date.now(); await objects.setObjectAsync(obj._id, obj); console.log(`Adapter "${obj._id}" restarted.`); } catch (err) { console.error(`Cannot restart adapter "${obj._id}": ${err.message}`); } } } } } } } } catch (err) { console.error(`Error deleting adapter ${adapter} from disk: ${err.message}`); console.error(`You might have to delete it yourself!`); } }; try { // detect if all instances on this host, if not so the www and admin must not be deleted await this._enumerateAdapterInstances(knownObjectIDs, notDeletedObjectIDs, adapter); if (notDeletedObjectIDs.length) { // just delete all instances on this host and then delete npm for (const knownObjectID of knownObjectIDs) { await this.deleteInstance(adapter, knownObjectID.split('.').pop()); } // remove adapter from custom await this._removeCustomFromObjects([adapter]); await _uninstallNpm(); } else { // we are not allowed to delete last instance if another instance depends on us const dependentInstance = await this._hasDependentInstances(adapter); if (dependentInstance) { console.log( `Cannot remove adapter "${adapter}", because instance "${dependentInstance}" depends on it!` ); return EXIT_CODES.CANNOT_DELETE_DEPENDENCY; } const instances = knownObjectIDs.map(id => `${adapter}.${id.split('.').pop()}`); await this._enumerateAdapterMeta(knownObjectIDs, adapter, metaFilesToDelete); resultCode = await this._enumerateAdapters(knownObjectIDs, adapter); await this._enumerateAdapterDevices(knownObjectIDs, adapter); await this._enumerateAdapterChannels(knownObjectIDs, adapter); await this._enumerateAdapterStateObjects(knownObjectIDs, adapter); await this._enumerateAdapterStates(knownStateIDs, adapter); await this._enumerateAdapterDocs(knownObjectIDs, adapter); await this._deleteAdapterFiles(adapter, metaFilesToDelete); await this._deleteAdapterObjects(knownObjectIDs); await this._deleteAdapterStates(knownStateIDs); if (params.custom) { // remove adapter from custom await this._removeCustomFromObjects([...instances, adapter]); } await _uninstallNpm(); } } catch (err) { console.error(`There was an error uninstalling ${adapter} on ${hostname}: ${err.message}`); } return resultCode; }; /** * Deletes given instance of an adapter * * @param {string} adapter adapter name like hm-rpc * @param {string?} instance e.g. 1 * @return {Promise<void>} */ this.deleteInstance = async (adapter, instance) => { const knownObjectIDs = []; const knownStateIDs = []; // we are not allowed to delete last instance if another instance depends on us const dependentInstance = await this._hasDependentInstances(adapter, instance); if (dependentInstance) { console.log( `Cannot remove instance "${adapter}.${instance}", because instance "${dependentInstance}" depends on it!` ); return EXIT_CODES.CANNOT_DELETE_DEPENDENCY; } await this._enumerateAdapterInstances(knownObjectIDs, null, adapter, instance); await this._enumerateAdapterDevices(knownObjectIDs, adapter, instance); await this._enumerateAdapterChannels(knownObjectIDs, adapter, instance); await this._enumerateAdapterStateObjects(knownObjectIDs, adapter, instance); await this._enumerateAdapterStates(knownStateIDs, adapter, instance); await this._enumerateAdapterDocs(knownObjectIDs, adapter, instance); await this._deleteAdapterObjects(knownObjectIDs); await this._deleteAdapterStates(knownStateIDs); if (params.custom) { // delete instance from custom await this._removeCustomFromObjects([`${adapter}.${instance}`]); } // TODO delete meta objects - I think a recursive deletion of all child object would be less effort. }; /** * Removes the custom attribute of the provided adapter/instance * * @param {string[]} ids - id of the adapter/instance to check for * @returns {Promise<void>} */ this._removeCustomFromObjects = async ids => { // get all objects which have a custom attribute const res = await objects.getObjectViewAsync('system', 'custom', { startkey: '', endkey: '\u9999' }); if (res && res.rows) { for (const row of res.rows) { let obj; for (const id of ids) { if (Object.prototype.hasOwnProperty.call(row.value, id)) { if (!obj) { obj = await objects.getObjectAsync(row.id); } obj.common.custom[id] = null; } } if (obj) { // if we have removed a custom attribute, set it to db await objects.setObjectAsync(row.id, obj); } } } }; /** * Installs an adapter from given url * @param {string} url * @param {string} name * @return {Promise<void>} */ this.installAdapterFromUrl = async function (url, name) { // If the user provided an URL, try to parse it into known ways to represent a Github URL let parsedUrl; try { parsedUrl = new URL(url); } catch { /* ignore, not a valid URL */ } const debug = process.argv.includes('--debug'); if (parsedUrl && parsedUrl.hostname === 'github.com') { if (!tools.isGithubPathname(parsedUrl.pathname)) { return console.log(`Cannot install from GitHub. Invalid URL ${url}`); } // This is a URL we can parse const { repo, user, commit } = tools.parseGithubPathname(parsedUrl.pathname); if (!commit) { // No commit given, try to get it from the API try { const result = await axios(`http://api.github.com/repos/${user}/${repo}/commits`, { headers: { 'User-Agent': 'ioBroker Adapter install', validateStatus: status => status === 200 } }); if (result.data && Array.isArray(result.data) && result.data.length >= 1 && result.data[0].sha) { url = `${user}/${repo}#${result.data[0].sha}`; } else { console.log( `Info: Can not get current GitHub commit, only remember that we installed from GitHub.` ); url = `${user}/${repo}`; } } catch (err) { console.log( `Info: Can not get current GitHub commit, only remember that we installed from GitHub: ${err.message}` ); // Install using the npm Github URL syntax `npm i user/repo_name`: url = `${user}/${repo}`; } } else { // We've extracted all we need from the URL url = `${user}/${repo}#${commit}`; } } console.log(`install ${url}`); // Try to extract name from URL if (!name) { const reNpmPacket = new RegExp('^' + tools.appName + '\\.([-_\\w\\d]+)(@.*)?$', 'i'); const match = reNpmPacket.exec(url); // we have iobroker.adaptername@1.2.3 if (match) { name = match[1]; } else if (url.match(/\.(tgz|gz|zip|tar\.gz)$/)) { const parts = url.split('/'); const last = parts.pop(); const mm = last.match(/\.([-_\w\d]+)-[.\d]+/); if (mm) { name = mm[1]; } } else { const githubUrlParts = tools.parseShortGithubUrl(url); // Try to extract the adapter name from the github url if possible // Otherwise fall back to the complete URL if (githubUrlParts) { name = githubUrlParts.repo; } else { name = url; } // Remove the leading `iobroker.` from the name const reG = new RegExp(tools.appName + '\\.([-_\\w\\d]+)$', 'i'); const match = reG.exec(name); if (match) { name = match[1]; } } } const options = { packetName: name }; /** list of stopped instances for windows */ let stoppedList = []; if (process.platform === 'win32') { stoppedList = await this._getInstancesOfAdapter(name); await this.enableInstances(stoppedList, false); } const res = await this._npmInstallWithCheck(url, options, debug); // if we have no installDir, the method has called processExit itself if (!res || !res.installDir) { return; } const { installDir } = res; if (name) { await upload.uploadAdapter(name, true, true); await upload.uploadAdapter(name, false, true); await upload.upgradeAdapterObjects(name); } else { // Try to find io-package.json with newest date const dirs = fs.readdirSync(installDir); let date = null; let dir = null; for (const _dir of dirs) { if (fs.existsSync(`${installDir}/${_dir}/io-package.json`)) { const stat = fs.statSync(`${installDir}/${_dir}/io-package.json`); if (!date || stat.mtime.getTime() > date.getTime()) { dir = _dir; date = stat.mtime; } } } // if modify time is not older than one hour if (dir && Date.now() - date.getTime() < 3600000) { name = dir.substring(tools.appName.length + 1); await upload.uploadAdapter(name, true, true); await upload.uploadAdapter(name, false, true); await upload.upgradeAdapterObjects(name); } } // re-enable stopped instances await this.enableInstances(stoppedList, true); }; /** * Checks if other adapters depend on this adapter * * @param {string} adapter adapter name * @param {string?} instance instance, like 1 * @return {Promise<void|string>} if dependent exists returns adapter name * @private */ this._hasDependentInstances = async (adapter, instance) => { try { // lets get all instances const doc = await objects.getObjectViewAsync('system', 'instance', { startkey: 'system.adapter.', endkey: 'system.adapter.\u9999' }); let scopedHostname; if (instance) { // we need to respect host relative to the instance [scopedHostname] = doc.rows .filter(row => row.id === `system.adapter.${adapter}.${instance}`) .map(row => row.value.common.host); } // fallback is this host scopedHostname = scopedHostname || hostname; for (const row of doc.rows) { if (!row.value.common) { // this object seems to be corrupted so it will not need our adapter continue; } const localDeps = tools.parseDependencies(row.value.common.dependencies); for (const localDep of Object.keys(localDeps)) { if (row.value.common.host === scopedHostname && localDep === adapter) { if (!instance) { // this adapter needs us locally and all instances should be deleted return `${row.value.common.name}.${row.id.split('.').pop()}`; } else { // check if other instance of us exists on this host if (this._checkDependencyFulfilledThisHost(adapter, instance, doc.rows, scopedHostname)) { // there are other instances of our adapter - ok break; } else { return `${row.value.common.name}.${row.id.split('.').pop()}`; } } } } const globalDeps = tools.parseDependencies(row.value.common.globalDependencies); for (const globalDep of Object.keys(globalDeps)) { if (globalDep === adapter) { if (!instance) { // all instances on this host should be removed so check if there are some on other hosts if (this._checkDependencyFulfilledForeignHosts(adapter, doc.rows, scopedHostname)) { break; } else { return row.value.common.name; } } else if ( this._checkDependencyFulfilledForeignHosts(adapter, doc.rows, scopedHostname) || this._checkDependencyFulfilledThisHost(adapter, instance, doc.rows, scopedHostname) ) { // another instance of our adapter is on another host or on ours, no need to search further break; } else { return row.value.common.name; } } } } } catch (e) { console.error(`Could not check dependent instances for "${adapter}": ${e.message}`); } }; /** * Checks if adapter can also be found on another host than this * * @param {string} adapter adapter name * @param {object[]} instancesRows all instances objects view rows * @param {string} scopedHostname hostname which should be assumed as local * @return {boolean} true if an instance is present on other host * @private */ this._checkDependencyFulfilledForeignHosts = (adapter, instancesRows, scopedHostname) => { for (const row of instancesRows) { if (row.value.common.name === adapter && row.value.common.host !== scopedHostname) { return true; } } return false; }; /** * Checks if another instance then the given is present on this host * * @param {string} adapter adapter name * @param {string} instance instance number like 1 * @param {object[]} instancesRows all instances objects view rows * @param {string} scopedHostname hostname which should be assumed as local * @return {boolean} true if another instance is present on this host * @private */ this._checkDependencyFulfilledThisHost = (adapter, instance, instancesRows, scopedHostname) => { for (const row of instancesRows) { if ( row.value.common.name === adapter && row.value.common.host === scopedHostname && row.value._id.split('.').pop() !== instance ) { return true; } } return false; }; /** * Get all instances of an adapter which are on the current host * * @param {string} adapter * @return {Promise<ioBroker.InstanceObject[]>} * @private */ this._getInstancesOfAdapter = async adapter => { const instances = []; const doc = await objects.getObjectListAsync({ startkey: `system.adapter.${adapter}.`, endkey: `system.adapter.${adapter}.\u9999` }); if (doc) { for (const row of doc.rows) { // stop only started instances on this host if (row.value.common.enabled && hostname === row.value.common.host) { instances.push(row.value); } } } return instances; }; } module.exports = Install;