UNPKG

iobroker.js-controller

Version:

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

1,067 lines (944 loc) 82.1 kB
/** * Install adapter * * Copyright 2013-2020 bluefox <dogafox@gmail.com> * * MIT License * */ 'use strict'; /** @class */ function Install(options) { const EXIT_CODES = require('../exitCodes'); const fs = require('fs-extra'); const tools = require('../tools.js'); const hostname = tools.getHostName(); const path = require('path'); const semver = require('semver'); const child_process = require('child_process'); const request = require('request'); 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; let JSZip; /** @type {Install} */ const that = this; 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.installNpm) { throw new Error('Invalid arguments: installNpm 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 installNpm = options.installNpm; const getRepository = options.getRepository; const params = options.params || {}; let mime; let packetManager; // TODO: promisify States and Objects at some point /** @type {(stateId: string) => Promise<void>} */ const delStateAsync = tools.promisify(states.delState, states); /** @type {(objId: string) => Promise<void>} */ const delObjectAsync = tools.promisify(objects.delObject, objects); /** @type {(id: string, name: string) => Promise<void>} */ const unlinkAsync = tools.promisify(objects.unlink, objects); /** @type {(design: string, search: string, params: any, options?: any) => Promise<{rows: {id: string, value: any}[]}>} */ const getObjectViewAsync = tools.promisify(objects.getObjectView, objects); /** @type {(params: any | null) => Promise<{rows: {id: string, value: any}[]}>} */ const getObjectListAsync = tools.promisify(objects.getObjectList, objects); /** @type {(objId: string) => Promise<any>} */ const getObjectAsync = tools.promisify(objects.getObject, objects); /** @type {(objId: string, newObj: any) => Promise<void>} */ const setObjectAsync = tools.promisify(objects.setObject, objects); /** @type {(pattern: string) => Promise<string[]>} */ const getKeysAsync = tools.promisify(states.getKeys, states); const tarballRegex = /\/tarball\/[^/]+$/; let installCount = 0; const Upload = require('./setupUpload'); const upload = new Upload(options); function enableAdapters(adapters, isEnable, callback) { let count = 0; if (adapters && adapters.length) { count = adapters.length; const ts = Date.now(); for (let i = 0; i < adapters.length; i++) { const updatedObj = { common: { enabled: isEnable }, from: 'system.host.' + tools.getHostName() + '.cli', ts: ts }; console.log('host.' + hostname + ' Adapter "' + adapters[i]._id + '" is ' + (isEnable ? 'started' : 'stopped.')); objects.extendObject(adapters[i]._id, updatedObj, () => { if (!--count) { callback(); } }); } } if (!count) { callback(); } } function _writeOneFile(zip, targetName, fileName, callback) { zip.files[fileName].async('nodebuffer').then(data => { fs.writeFileSync(path.join(targetName, fileName), data); callback(); }, err => callback(err)); } function extractFiles(fileName, targetName, callback) { JSZip = JSZip || require('jszip'); const zip = new JSZip(); zip.loadAsync(fs.readFileSync(fileName)).then(() => { let count = 0; for (const fName of Object.keys(zip.files)) { if (!fName || fName[fName.length - 1] === '/') { continue; } count++; _writeOneFile(zip, targetName, fName, err => { if (!--count) { callback(err); } }); } if (!count) { callback(); } }); } this.downloadPacket = function (repoUrl, packetName, options, stoppedList, callback) { let url; let name; if (!options || typeof options !== 'object') { options = {}; } if (typeof stoppedList === 'function') { callback = stoppedList; stoppedList = null; } if (!repoUrl || typeof repoUrl !== 'object') { return getRepository(repoUrl, params, (err, sources) => { if (err) { processExit(err); } else { this.downloadPacket(sources, packetName, options, stoppedList, callback); } }); } let debug = false; for (let i = 0; i < process.argv.length; i++) { if (process.argv[i] === '--debug') { debug = true; break; } } 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 if (sources[packetName] && sources[packetName].stopBeforeUpdate && !stoppedList) { return objects.getObjectList({startkey: `system.adapter.${packetName}.`, endkey: `system.adapter.${packetName}.香`}, (err, arr) => { stoppedList = []; if (!err && arr) { for (let id = 0; id < arr.rows.length; id++) { // stop only started instances on this host if (arr.rows[id].value.common.enabled && hostname === arr.rows[id].value.common.host) { stoppedList.push(arr.rows[id].value); } } } enableAdapters(stoppedList, false, () => that.downloadPacket(sources, packetName + '@' + version, options, stoppedList, callback)); }); } // try to extract the information from local sources-dist.json if (!sources[packetName]) { try { const sourcesDist = fs.readJSONSync(__dirname + '/../../conf/sources-dist.json'); sources[packetName] = sourcesDist[packetName]; } catch { // OK } } if (sources[packetName]) { url = sources[packetName].url; if (url && packetName === 'js-controller' && fs.existsSync(`${__dirname}/../../../../node_modules/${tools.appName}.js-controller`)) { url = null; } if (!url && packetName !== 'example') { // Install node modules that.npmInstallWithCheck(`${tools.appName.toLowerCase()}.${packetName}${version ? '@' + version : ''}`, options, debug, () => { // command succeeded typeof callback === 'function' && callback(_callback => enableAdapters(stoppedList, true, _callback), packetName); }); return; } if (url && url.match(tarballRegex)) { // Install node modules return that.npmInstallWithCheck(url, options, debug, () => { // command succeeded typeof callback === 'function' && callback(_callback => enableAdapters(stoppedList, true, _callback), packetName); }); } // Adapter if (!url) { console.warn(`host.${hostname} Adapter "${packetName}" can be updated only together with ${tools.appName}.js-controller`); return typeof callback === 'function' && callback(_callback => typeof _callback === 'function' && _callback(), packetName); } name = packetName.replace(/[/ $&*\\]/g, '_'); } else { url = packetName; if (!url.includes('http://') && !url.includes('https://') && !url.includes('file://')) { console.error('host.' + hostname + ' Unknown packetName ' + packetName); processExit(EXIT_CODES.UNKNOWN_PACKET_NAME); } name = Math.floor(Math.random() * 0xFFFFFFE).toString(); } const { ncp } = require('ncp'); ncp.limit = 16; console.log(`host.${hostname} download ${url}`); tools.getFile(url, name + '.zip', tmpFile => { tmpFile = path.normalize(tmpFile); console.log(`host.${hostname} unzip ${tmpFile}`); // Extract files into tmp/ extractFiles(tmpFile, path.join(__dirname + '/../../tmp/', name), error => { if (error) { console.error(error); processExit(EXIT_CODES.CANNOT_EXTRACT_FROM_ZIP); } // Find out the first directory const dirs = fs.readdirSync(__dirname + '/../../tmp/' + name); if (dirs.length) { const source = __dirname + '/../../tmp/' + name + ((dirs.length === 1) ? '/' + dirs[0] : ''); // Copy files into adapter or controller if (fs.existsSync(source + '/io-package.json')) { let packetIo; try { packetIo = fs.readJSONSync(source + '/io-package.json'); } catch { console.error('host.' + hostname + ' io-package.json has invalid format! Installation terminated.'); typeof callback === 'function' && callback(_callback => _callback && _callback(), name, 'Invalid io-package.json!'); processExit(EXIT_CODES.INVALID_IO_PACKAGE_JSON); } packetIo.common = packetIo.common || {}; packetIo.common.installedFrom = url; fs.writeFileSync(source + '/io-package.json', JSON.stringify(packetIo, null, 2), 'utf8'); let destination = __dirname + '/../..'; if (!packetIo.common.controller) { if (fs.existsSync(destination + '/../../node_modules')) { destination += '/../' + tools.appName + '.' + packetIo.common.name; } else { destination += '/node_modules/' + tools.appName + '.' + packetIo.common.name; } } destination = path.normalize(destination); console.log(`host.${hostname} copying ${source} to ${destination}(Version: ${packetIo.common.version})`); ncp(source, destination, err => { if (err) { console.error(`host.${hostname} ncp error: ${err}`); processExit(EXIT_CODES.CANNOT_COPY_DIR); } if (tmpFile.substring(0, (path.normalize(__dirname + '/../../tmp/')).length) === path.normalize(__dirname + '/../../tmp/')) { console.log(`host.${hostname} delete ${tmpFile}`); fs.unlinkSync(tmpFile); } console.log(`host.${hostname} delete ${path.normalize(__dirname + '/../../tmp/' + name)}`); tools.rmdirRecursiveSync(__dirname + '/../../tmp/' + name); // Call npm install if (typeof callback === 'function') { typeof callback === 'function' && callback(_callback => enableAdapters(stoppedList, true, _callback), name, packetIo); } }); } else { console.error(`host.${hostname} io-package.json not found in ${source}/io-package.json. Invalid packet! Installation terminated.`); typeof callback === 'function' && callback(_callback => _callback && _callback(), name, 'Invalid packet!'); processExit(EXIT_CODES.INVALID_IO_PACKAGE_JSON); } } else { console.error(`host.${hostname} Packet is empty! Installation terminated.`); typeof callback === 'function' && callback(_callback => _callback && _callback(), name, 'Packet is empty'); processExit(EXIT_CODES.MISSING_ADAPTER_FILES); } }); }); }; this.npmInstallWithCheck = function (npmUrl, options, debug, callback) { // 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 (e) { console.error('Error trying to check npm version: ' + e); } 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('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!'); processExit(EXIT_CODES.INVALID_NPM_VERSION); return; } 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('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!'); processExit(EXIT_CODES.INVALID_NPM_VERSION); return; } this.npmInstall(npmUrl, options, debug, callback); } catch (e) { console.error('Could not check npm version: ' + e); console.error('Assuming that correct version is installed.'); } }; this.npmInstall = function (npmUrl, options, debug, callback) { if (typeof options !== 'object') { options = {}; } // Install node modules /** @type {string|string[]} */ let cwd = __dirname.replace(/\\/g, '/'); if (fs.existsSync(__dirname + '/../../../../node_modules/' + tools.appName + '.js-controller')) { // js-controller installed as npm cwd = cwd.split('/'); cwd.splice(cwd.length - 4, 4); cwd = cwd.join('/'); } else { // remove lib cwd = cwd.split('/'); cwd.pop(); cwd.pop(); cwd = cwd.join('/'); } // zwave for example requires always unsafe-perm option if (unsafePermAlways.some(adapter => npmUrl.indexOf(adapter) > -1)) { 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; } // We don't need --production and --save here. // --production doesn't do anything when installing a specific package (which we do here) // --save is the default since npm 3 // Don't use --prefix on Windows, because that has ugly bugs const cmd = [ 'npm install', npmUrl, debug ? '' : '--loglevel error', options.unsafePerm ? '--unsafe-perm' : '', osPlatform !== 'win32' ? `--prefix "${cwd}"` : '' ].filter(arg => !!arg).join(' '); console.log(`${cmd} (System call)`); // Install node modules as system call // System call used for update of js-controller itself, // because during installation npm packet will be deleted too, but some files must be loaded even during the install process. const exec = require('child_process').exec; const child = exec(cmd, { windowsHide: true, cwd }); tools.pipeLinewise(child.stderr, process.stdout); if (debug || params.debug) { tools.pipeLinewise(child.stdout, process.stdout); } // Determine where the packet would be installed if npm succeeds /** @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.indexOf('#') > -1) { packetDirName = packetDirName.substr(0, packetDirName.indexOf('#')); } if (packetDirName.indexOf('/') > -1 && !packetDirName.startsWith('@')) { // only scoped packages (e.g. @types/node ) may have a slash in their path packetDirName = packetDirName.substr(packetDirName.lastIndexOf('/') + 1); } } const installDir = path.join(cwd, 'node_modules', packetDirName); child.on('exit', code => { // code 1 is strange error that cannot be explained. Everything is installed but error :( if (code && code !== 1) { console.error('host.' + hostname + ' Cannot install ' + npmUrl + ': ' + code); processExit(EXIT_CODES.CANNOT_INSTALL_NPM_PACKET); return; } // 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 { console.error('host.' + hostname + ' Cannot install ' + npmUrl + ': ' + code); processExit(EXIT_CODES.CANNOT_INSTALL_NPM_PACKET); return; } // create file that indicates, that npm was called there fs.writeFileSync(path.join(installDir, 'iob_npm.done'), ' '); // command succeeded typeof callback === 'function' && callback(npmUrl, cwd + '/node_modules'); }); }; this.npmUninstall = function (packageName, options, debug, callback) { // TODO: find a nicer way to find the root directory // Install node modules /** @type {string|string[]} */ let cwd = __dirname.replace(/\\/g, '/'); if (fs.existsSync(`${__dirname}/../../../../node_modules/${tools.appName}.js-controller`)) { // js-controller installed as npm cwd = cwd.split('/'); cwd.splice(cwd.length - 4, 4); cwd = cwd.join('/'); } else { // remove lib cwd = cwd.split('/'); cwd.pop(); cwd.pop(); cwd = cwd.join('/'); } // Don't use --prefix on Windows, because that has ugly bugs // Instead set the working directory (cwd) of the process const cmd = [ 'npm uninstall', packageName, debug ? '' : '--loglevel error', osPlatform !== 'win32' ? `--prefix "${cwd}"` : '' ].filter(arg => !!arg).join(' '); console.log(`${cmd} (System call)`); // Install node modules as system call // System call used for update of js-controller itself, // because during installation npm packet will be deleted too, but some files must be loaded even during the install process. const exec = require('child_process').exec; const child = exec(cmd, { windowsHide: true, cwd }); tools.pipeLinewise(child.stderr, process.stdout); if (debug || params.debug) { tools.pipeLinewise(child.stdout, process.stdout); } child.on('exit', code => { // code 1 is strange error that cannot be explained. Everything is installed but error :( if (code) { if (typeof callback === 'function') { callback(`host.${hostname}: Cannot uninstall ${packageName}: ${code}`); } } // command succeeded if (callback) { callback(); } }); }; /** @type {(packageName: string, options: any, debug: boolean) => Promise<void>} */ this.npmUninstallAsync = tools.promisify(this.npmUninstall, this); // this command is executed always on THIS host function checkDependencies(adapter, deps, globalDeps, _options, callback) { if (!deps && !globalDeps) { return callback && callback(adapter); } deps = tools.parseDependencies(deps); globalDeps = tools.parseDependencies(globalDeps); // combine both dependencies const allDeps = {...deps, ...globalDeps}; let cnt = 0; // Get all installed adapters objects.getObjectView('system', 'instance', {}, null, (err, objs) => { err && console.error(err); 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) { cnt++; that.createInstance(dName, _options, name => upload.uploadAdapter(name, true, false, () => upload.uploadAdapter(name, false, false, () => !--cnt && callback && callback(dName)))); } } } !cnt && callback && callback(adapter); }); } function setObjects(adapter, _objs, _callback) { if (!_objs || _objs.length === 0) { _callback(null, adapter); } else { const obj = _objs.pop(); obj.from = 'system.host.' + tools.getHostName() + '.cli'; obj.ts = Date.now(); objects.extendObject(obj._id, obj, err => { if (err) { console.error('host.' + hostname + ' error setObject ' + obj._id + ' ' + err); _callback(EXIT_CODES.CANNOT_SET_OBJECT, adapter); } else { console.log('host.' + hostname + ' object ' + obj._id + ' created/updated'); setImmediate(setObjects, adapter, _objs, _callback); } }); } } this.uploadStaticObjects = function (adapter, adapterConf, callback) { if (typeof adapterConf === 'function') { callback = adapterConf; adapterConf = null; } if (!adapterConf) { const adapterDir = tools.getAdapterDir(adapter); if (!fs.existsSync(adapterDir + '/io-package.json')) { console.error('host.' + hostname + ' Adapter directory "' + adapterDir + '" does not exists'); callback(EXIT_CODES.CANNOT_FIND_ADAPTER_DIR, adapter); return; } try { adapterConf = fs.readJSONSync(adapterDir + '/io-package.json'); } catch (e) { console.error('host.' + hostname + ' error: reading io-package.json ' + e, adapter); callback(EXIT_CODES.CANNOT_FIND_ADAPTER_DIR, adapter); return; } } 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 checkDependencies(adapter, adapterConf.common.dependencies, adapterConf.common.globalDependencies, params, () => { adapterConf.common.installedVersion = adapterConf.common.version; objs.push({ _id: `system.adapter.${adapterConf.common.name}`, type: 'adapter', common: adapterConf.common, native: adapterConf.native }); setObjects(adapter, objs, callback); }); }; this.installAdapter = async (adapter, repoUrl, callback) => { if (typeof repoUrl === 'function') { callback = repoUrl; repoUrl = null; } const fullName = adapter; if (adapter.indexOf('@') !== -1) { 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}`); processExit(EXIT_CODES.CANNOT_INSTALL_NPM_PACKET); return; } installCount++; that.downloadPacket(repoUrl, fullName, null, enableAdapterCallback => that.installAdapter(adapter, () => enableAdapterCallback(callback))); return; } installCount = 0; let adapterConf; try { adapterConf = fs.readJSONSync(adapterDir + '/io-package.json'); } catch (e) { console.error(`host.${hostname} error: reading io-package.json ${e}`); 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}`); 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}`); 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}`); 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 (e) { console.error(`host.${hostname} Could not install required OS packages: ${e.message}`); } } if (!fs.existsSync(adapterDir + '/node_modules')) { // Install node modules installNpm(adapter, (err, _adapter) => { if (err) { processExit(err); } else { upload.uploadAdapter(_adapter, true, true, null, null, () => upload.uploadAdapter(_adapter, false, true, null, null,() => callInstallOfAdapter(_adapter, adapterConf, () => that.uploadStaticObjects(adapter, _err => upload.upgradeAdapterObjects(adapter, () => callback(adapter)))))); } }); } else { upload.uploadAdapter(adapter, true, true, () => upload.uploadAdapter(adapter, false, true, () => callInstallOfAdapter(adapter, adapterConf, () => that.uploadStaticObjects(adapter, _err => upload.upgradeAdapterObjects(adapter, () => callback(adapter)))))); } }; async function callInstallOfAdapter(adapter, config, callback) { 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 void callback(adapter); } cmd += `"${fileFullName}" --install`; console.log(`host.${hostname} command: ${cmd}`); const child = exec(cmd, {windowsHide: true}); tools.pipeLinewise(child.stderr, process.stdout); child.on('exit', () => callback && callback(adapter)); } else if (typeof callback === 'function') { callback(adapter); } } //options = enabled, host, port this.createInstance = function (adapter, options, callback) { const adapterDir = tools.getAdapterDir(adapter); if (typeof options === 'function') { callback = options; options = null; } 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'); objects.getObject('system.adapter.' + adapter, (err, doc) => { // Adapter is not installed - install it now if (err || !doc || !doc.common.installedVersion) { return that.installAdapter(adapter, () => that.createInstance(adapter, options, callback)); } // Check if some web pages should be uploaded upload.uploadAdapter(adapter, true, false, () => { upload.uploadAdapter(adapter, false, false, () => { objects.getObjectView('system', 'instance', {startkey: 'system.adapter.' + adapter + '.', endkey: 'system.adapter.' + adapter + '.\u9999'}, null, (err, res) => { objects.getObject('system.config', (err, systemConfig) => { const defaultLogLevel = systemConfig && systemConfig.common && systemConfig.common.defaultLogLevel; let a; if (err || !res) { console.error('host.' + hostname + ' error: view instanceStats ' + err); processExit(EXIT_CODES.CANNOT_READ_INSTANCES); return; } // Count started instances if (doc.common.singleton && res.rows.length) { if (ignoreIfExists) { callback && callback(); return; } console.error(`host.${hostname} error: this adapter does not allow multiple instances`); processExit(EXIT_CODES.NO_MULTIPLE_INSTANCES_ALLOWED); return; } // check singletonHost one on host if (doc.common.singletonHost) { for (a = 0; a < res.rows.length; a++) { if (res.rows[a].value.common.host === hostname) { if (ignoreIfExists) { callback && callback(); return; } console.error(`host.${hostname} error: this adapter does not allow multiple instances on one host`); processExit(EXIT_CODES.NO_MULTIPLE_INSTANCES_ALLOWED_ON_HOST); return; } } } 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`); processExit(EXIT_CODES.INSTANCE_ALREADY_EXISTS); return; } } else { // find max instance for (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 = doc; doc = deepClone(doc); instanceObj._id = `system.adapter.${adapter}.${instance}`; instanceObj.type = 'instance'; 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.indexOf('%INSTANCE%') !== -1) { 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 = []; } 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 (e) { console.error(`host.${hostname} error: reading io-package.json ${e}`); return void 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]); } } */ function setObjs() { if (objs.length > 0) { const obj = objs.pop(); try { tools.validateGeneralObjectProperties(obj); } catch (e) { // todo: in the future we will not create this object console.warn(`host.${hostname} Object ${obj._id} is invalid: ${e.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(); objects.setObject(obj._id, obj, err => { if (err) { console.error(`host.${hostname} error: ${err}`); } else { console.log(`host.${hostname} object ${obj._id} created`); } setTimeout(setObjs, 25); }); } else { setStates(); } } // sets the default states if any given function setStates() { if (defStates.length > 0) { const defState =