iobroker.js-controller
Version:
Updated by reinstall.js on 2018-06-11T15:19:56.688Z
1,711 lines (1,536 loc) • 82.1 kB
JavaScript
/**
* Install adapter
*
* Copyright 2013-2020 bluefox <dogafox@gmail.com>
*
* MIT License
*
*/
;
/** @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 = defStates.pop();
defState.ack = true;
defState.from = `system.host.${tools.getHostName()}.cli`;
states.setState(defState.id, defState, err => {
if (err) {
console.error(`host.${hostname} error: ${err}`);
} else {
console.log(`host.${hostname} Set default value of ${defState.id}: ${defState.val}`);
}
setTimeout(setStates, 25);
});
} else {
instanceObj.from = 'system.host.' + tools.getHostName() + '.cli';
instanceObj.ts = Date.now();
objects.setObject(instanceObj._id, instanceObj, err => {
if (err) {
console.error(`host.${hostname} error: ${err}`);
} else {
console.log(`host.${hostname} object ${instanceObj._id} created`);
}
if (callback) {
callback(adapter);
} else {
processExit(EXIT_CODES.NO_ERROR);
}
});
}
}
setObjs();
});
});
});
});
});
};
/**
* Enumerate all instances of an adapter
* @type {(knownObjIDs: string[], notDeleted: any[], adapter: string, instance?: string) => Promise<void>}
*/
this.enumerateAdapterInstances = function enumerateInstances(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+$`);
return getObjectViewAsync('system', 'instance', {
startkey: `system.adapter.${adapter}${instance !== undefined ? `.${instance}` : ''}`,
endkey: `system.adapter.${adapter}${instance !== undefined ? `.${instance}` : ''}\u9999`
}, null).then(doc => {
// add non-duplicates to the list (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))
// only the ones on this host
.filter(row => {
if (!row.value.common || !row.value.common.host || row.value.common.host === hostname) {
return true;
} else {
if (notDeleted.indexOf(row.value._id) === -1) {
notDeleted.push(row.value._id);
}
return false;
}
})
.map(row => row.value._id)
.filter(id => knownObjIDs.indexOf(id) === -1)
;
knownObjIDs.push.apply(knownObjIDs, newObjIDs);
if (newObjIDs.length > 0) {
console.log(`host.${hostname} Counted ${newObjIDs.length} instances of ${adapter}${instance !== undefined ? `.${instance}` : ''}`);
}
}).catch(e =>
e !== tools.ERRORS.ERROR_NOT_FOUND && e.message !== tools.ERRORS.ERROR_NOT_FOUND && console.error('host.' + hostname + ' error: ' + e.message));
};
/**
* Enumerate all meta objects of an adapter
* @type {(knownObjIDs: string[], adapter: string, metaFilesToDelete: string[]) => Promise<void>}
*/
this.enumerateAdapterMeta = async function enumerateMeta(knownObjIDs, adapter, metaFilesToDelete) {
try {
const doc = await 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 (e) {
e !== tools.ERRORS.ERROR_NOT_FOUND && e.message !== tools.ERRORS.ERROR_NOT_FOUND && console.error(`host.${hostname} error: ${e.message}`);
}
};
/**
* @type {(knownObjIDs: string[], adapter: string) => Promise<number>}
* @returns 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 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 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 (e) {
console.error(e);
}
};
/**
* 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 = function enumerateAdapterDevices(knownObjIDs, adapter, instance) {
const adapterRegex = new RegExp(`^${adapter}${instance ? `\\.${instance}` : ''}\\.`);
return getObjectViewAsync('system', 'device', {
startkey: `${adapter}${instance !== undefined ? `.${instance}` : ''}`,
endkey: `${adapter}${instance !== undefined ? `.${instance}` : ''}\u9999`
}, null).then(doc => {
if (doc.rows.length !== 0) {
// 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);
if (newObjs.length > 0) {
console.log(`host.${hostname} Counted ${newObjs.length} devices of ${adapter}${instance !== undefined ? `.${instance}` : ''}`);
}
}
}).catch(e =>
e !== tools.ERRORS.ERROR_NOT_FOUND && e.message !== tools.ERRORS.ERROR_NOT_FOUND && console.error('host.' + hostname + ' error: ' + e.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 = function enumerateAdapterChannels(knownObjIDs, adapter, instance) {
const adapterRegex = new RegExp(`^${adapter}${instance ? `\\.${instance}` : ''}\\.`);
return getObjectViewAsync('system', 'channel', {
startkey: `${adapter}${instance !== undefined ? `.${instance}` : ''}`,
endkey: `${adapter}${instance !== undefined ? `.${instance}` : ''}\u9999`
}, null).then(doc => {
if (doc.rows.length !== 0) {
// 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);
if (newObjs.length > 0) {
console.log(`host.${hostname} Counted ${newObjs.length} channels of ${adapter}${instance ? `.${instance}` : ''}`);
}
}
}).catch(e =>
e !== tools.ERRORS.ERROR_NOT_FOUND && e.message !== tools.ERRORS.ERROR_NOT_FOUND && console.error('host.' + hostname + ' error: ' + e.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 = function enumerateAdapterStateObjects(knownObjIDs, adapter, instance) {
const adapterRegex = new RegExp(`^${adapter}${instance ? `\\.${instance}` : ''}\\.`);
const sysAdapterRegex = new RegExp(`^system\\.adapter\\.${adapter}${instance ? `\\.${instance}` : ''}\\.`);
return getObjectViewAsync('system', 'state', {
startkey: `${adapter}${instance !== undefined ? `.${instance}` : ''}`,
endkey: `${adapter}${instance !== undefined ? `.${instance}` : ''}\u9999`
}, null).then(doc => {
if (doc.rows.length !== 0) {
// 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);
if (newObjs.length > 0) {
console.log(`host.${hostname} Counted ${newObjs.length} states of ${adapter}${instance ? `.${instance}` : ''}`);
}
}
return getObjectViewAsync('system', 'state', {
startkey: `system.adapter.${adapter}${instance !== undefined ? `.${instance}` : ''}`,
endkey: `system.adapter.${adapter}${instance !== undefined ? `.${instance}` : ''}\u9999`
}, null).then(doc => {
if (doc.rows.length !== 0) {
// 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.indexOf(id) === -1);
knownObjIDs.push.apply(knownObjIDs, newObjs);
if (newObjs.length > 0) {
console.log(`host.${hostname} Counted ${newObjs.length} states of system.adapter.${adapter}${instance ? `.${instance}` : ''}`);
}
}
}).catch(e =>
e !== tools.ERRORS.ERROR_NOT_FOUND && e.message !== tools.ERRORS.ERROR_NOT_FOUND && console.error('host.' + hostname + ' error: ' + e.message));
}).catch(e =>
e !== tools.ERRORS.ERROR_NOT_FOUND && e.message !== tools.ERRORS.ERROR_NOT_FOUND && console.error('host.' + hostname + ' error: ' + e.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 = function enumerateAdapterDocs(knownObjIDs, adapter, instance) {
const adapterRegex = new RegExp(`^${adapter}${instance ? `\\.${instance}` : ''}\\.`);
const sysAdapterRegex = new RegExp(`^system\\.adapter\\.${adapter}${instance ? `\\.${instance}` : ''}\\.`);
return getObjectListAsync({include_docs: true}).then(doc => {
if (doc.rows.length !== 0) {
// 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.indexOf(id) === -1)
;
knownObjIDs.push.apply(knownObjIDs, newObjs);
if (newObjs.length > 0) {
console.log(`host.${hostname} Counted ${newObjs.length} objects of ${adapter}${instance ? `.${instance}` : ''}`);
}
}
}).catch(e =>
e !== tools.ERRORS.ERROR_NOT_FOUND && e.message !== tools.ERRORS.ERROR_NOT_FOUND && console.error('host.' + hostname + ' error: ' + e.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.indexOf(id) === -1);
knownStateIDs.push.apply(knownStateIDs, newStates);
if (newStates.length > 0) {
console.log(`host.${hostname} Counted ${newStates.length} states (${pattern}) from states`);
}
}
} catch (e) {
console.error(e);
}
}
};
/**
* 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 unlinkAsync(id, file.name || '');
console.log(`host.${hostname} file ${id + (file.name ? `/${file.name}` : '')} deleted`);
} catch (e) {
e !== tools.ERRORS.ERROR_NOT_FOUND && e.message !== tools.ERRORS.ERROR_NOT_FOUND && console.error(`Cannot delete ${id} files folder: ${e}`);
}
}
for (const objId of [adapter, `${adapter}.admin`]) {
try {
await delObjectAsync(objId);
console.log(`host.${hostname} object ${objId} deleted`);
} catch (e) {
e !== tools.ERRORS.ERROR_NOT_FOUND && e.message !== tools.ERRORS.ERROR_NOT_FOUND && console.error(`host.${hostname} error: ${e}`);
}
}
};
/**
* @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 (e) { // yep that works!
e !== tools.ERRORS.ERROR_NOT_FOUND && e.message !== tools.ERRORS.ERROR_NOT_FOUND && console.error(e);
}
}
};
/**
* @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).');
}
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 delObjectAsync(id);
await tools.removeIdFromAllEnums(objects, id);
} catch (e) {
e !== tools.ERRORS.ERROR_NOT_FOUND && e.message !== tools.ERRORS.ERROR_NOT_FOUND && console.error('host.' + hostname + ' error: ' + e);
}
}
};
this.deleteAdapter = function (adapter, callback) {
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 that.npmUninstallAsync(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.getAllInstancesAsync(ioPack.common.restartAdapters, objects);
if (instances && instances.length) {
for (const instance of instances) {
const obj = await 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 setObjectAsync(obj._id, obj);
obj.common.enabled = true; // enable instance
obj.from = `system.host.${tools.getHostName()}.cli`;
obj.ts = Date.now();
await setObjectAsync(obj._id, obj);
console.log(`Adapter "${obj._id}" restarted.`);
} catch (err) {
console.error(`Cannot restart adapter "${obj._id}": ${err}`);
}
}
}
}
}
}
}
} catch (e) {
console.error(`Error deleting adapter ${adapter} from disk: ${e}`);
console.error(`You might have to delete it yourself!`);
}
};
// detect if all instances on this host, if not so the www and admin must not be deleted
return that.enumerateAdapterInstances(knownObjectIDs, notDeletedObjectIDs, adapter).then(() => {
if (notDeletedObjectIDs.length) {
// just delete all instances on this host and then delete npm
const tasks = knownObjectIDs.map(id => that.deleteInstance(adapter, id.split('.').pop()));
return Promise.all(tasks)
.then(uninstallNpm)
.catch(err => console.error(`There was an error uninstalling ${adapter} on ${hostname}: ${err.message}`))
.then(() => callback(adapter, resultCode));
} else {
return that.enumerateAdapterMeta(knownObjectIDs, adapter, metaFilesToDelete)
.then(() => that.enumerateAdapters(knownObjectIDs, adapter).then(ret => resultCode = ret))
.then(() => that.enumerateAdapterDevices(knownObjectIDs, adapter))
.then(() => that.enumerateAdapterChannels(knownObjectIDs, adapter))
.then(() => that.enumerateAdapterStateObjects(knownObjectIDs, adapter))
.then(() => that.enumerateAdapterStates(knownStateIDs, adapter))
.then(() => that.enumerateAdapterDocs(knownObjectIDs, adapter))
.then(() => that.deleteAdapterFiles(adapter, metaFilesToDelete))
.then(() => that.deleteAdapterObjects(knownObjectIDs))
.then(() => that.deleteAdapterStates(knownStateIDs))
.then(uninstallNpm)
.catch(err => console.error(`There was an error uninstalling ${adapter}: ${err.message}`))
.then(() => callback && callback(adapter, resultCode));
}
});
};
this.deleteInstance = function (adapter, instance, callback) {
const knownObjectIDs = [];
const knownStateIDs = [];
return that.enumerateAdapterInstances(knownObjectIDs, null, adapter, instance)
.then(() => that.enumerateAdapterDevices(knownObjectIDs, adapter, instance))
.then(() => that.enumerateAdapterChannels(knownObjectIDs, adapter, instance))
.then(() => that.enumerateAdapterStateObjects(knownObjectIDs, adapter, instance))
.then(() => that.enumerateAdapterStates(knownStateIDs, adapter, instance))
.then(() => that.enumerateAdapterDocs(knownObjectIDs, adapter, instance))
.then(() => that.deleteAdapterObjects(knownObjectIDs))
.then(() => that.deleteAdapterStates(knownStateIDs))
.then(() => callback && callback(adapter, instance))
;
// TODO delete meta objects - i think a recursive deletion of all child object would be less effort.
};
this.installAdapterFromUrl = /**
* @param {string} url
* @param {string} name
* @param {() => any} callback
*/
function(url, name, callback) {
// 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 */ }
let debug = false;
for (let i = 0; i < process.argv.length; i++) {
if (process.argv[i] === '--debug') {
debug = true;
break;
}
}
if (parsedUrl && parsedUrl.hostname === 'github.com') {
if (!tools.isGithubPathname(parsedUrl.pathname)) {
console.log(`Cannot install from GitHub. Invalid URL ${url}`);
return void tools.maybeCallback(callback);
}
// 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
request.get(
{
url: `http://api.github.com/repos/${user}/${repo}/commits`,
json: true,
headers: { 'User-Agent': 'ioBroker Adapter install' }
},
(err, res, data) => {
if (err) {
console.log(`Info: Can not get current GitHub commit, only remember that we installed from GitHub: ${err}`);
// Install using the npm Github URL syntax `npm i user/repo_name`:
return void that.installAdapterFromUrl(`${user}/${repo}`, name, callback);
} else if (res.statusCode !== 200) {
console.log(`Info: Can not get current GitHub commit, only remember that we installed from GitHub. Status: ${res.statusCode}${data && data.message? ` (${data.message})`: ''}`);
// Install using the npm Github URL syntax `npm i user/repo_name`:
return void that.installAdapterFromUrl(`${user}/${repo}`, name, callback);
} else if (data && Array.isArray(data) && data.length >= 1 && data[0].sha) {
// Install using the npm Github URL syntax `npm i user/repo_name#commit-ish`:
return void that.installAdapterFromUrl(`${user}/${repo}#${data[0].sha}`, name, callback);
} else {
console.log('Info: Can not get current GitHub commit, only remember that we installed from GitHub. No SHA available');
// Install using the npm Github URL syntax `npm i user/repo_name`:
return void that.installAdapterFromUrl(`${user}/${repo}`, name, callback);
}
}
);
return;
} else {
// We've extracted all we need from the URL
return void that.installAdapterFromUrl(`${user}/${repo}#${commit}`, name, callback);
}
}
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
};
that.npmInstallWithCheck(url, options, debug, (_url, installDir) => {
if (name) {
upload.uploadAdapter(name, true, true, () =>
upload.uploadAdapter(name, false, true, () =>
upload.upgradeAdapterObjects(name, callback)));
} else {
// Try to find io-package.json with newest date
const dirs = fs.readdirSync(installDir);
let date = null;
let dir = null;
for (let i = 0; i < dirs.length; i++) {
if (fs.existsSync(installDir + '/' + dirs[i] + '/io-package.json')) {
const stat = fs.statSync(installDir + '/' + dirs[i] + '/io-package.json');
if (!date || stat.mtime.getTime() > date.getTime()) {
dir = dirs[i];
date = stat.mtime;
}
}
}
// if modify time is not older than one hour
if (dir && (new Date()).getTime() - date.getTime() < 3600000) {
name = dir.substring(tools.appName.length + 1);
upload.uploadAdapter(name, true, true, () =>
upload.uploadAdapter(name, false, true, () =>
upload.upgradeAdapterObjects(name, callback)));
} else {
return void callback();
}
}
});
};
}
module.exports = Install;