UNPKG

iobroker.js-controller

Version:

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

1,853 lines • 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;