iobroker.js-controller
Version:
Updated by reinstall.js on 2018-06-11T15:19:56.688Z
2,954 lines (2,732 loc) • 106 kB
JavaScript
'use strict';
const fs = require('fs-extra');
const path = require('path');
const semver = require('semver');
const os = require('os');
const forge = require('node-forge');
const deepClone = require('deep-clone');
const cpPromise = require('promisify-child-process');
const jwt = require('jsonwebtoken');
const { createInterface } = require('readline');
// @ts-ignore
require('events').EventEmitter.prototype._maxListeners = 100;
let request;
let extend;
let password;
let npmVersion;
let crypto;
let diskusage;
const randomID = Math.round(Math.random() * 10000000000000); // Used for creation of User-Agent
const VENDOR_FILE = '/etc/iob-vendor.json';
let lastCalculationOfIps;
let ownIpArr = [];
// Here we define all characters that are forbidden in IDs. Since we want to allow multiple
// unicode character classes, we do that by OR-ing the character classes and negating the result.
// Also, we can easily whitelist characters this way.
//
// We allow:
// · Ll = lowercase letters
// · Lu = uppercase letters
// · Nd = numbers
// · ".", "_", "-" (common in IDs)
// · "/" (required for designs)
// · " :!#$%&()+=@^{}|~" (for legacy reasons)
//
/** All characters that may not appear in an object ID. */
const FORBIDDEN_CHARS = /[^._\-/ :!#$%&()+=@^{}|~\p{Ll}\p{Lu}\p{Nd}]+/gu;
/**
* recursively copy values from old object to new one
*
* @alias copyAttributes
* @memberof tools
* @param {object} oldObj source object
* @param {object} newObj destination object
* @param {object} [originalObj] optional object for read __no_change__ values
* @param {boolean} [isNonEdit] optional indicator if copy is in nonEdit part
*
*/
function copyAttributes(oldObj, newObj, originalObj, isNonEdit) {
for (const attr of Object.keys(oldObj)) {
if (typeof oldObj[attr] !== 'object' || oldObj[attr] instanceof Array) {
if (oldObj[attr] === '__no_change__' && originalObj && !isNonEdit) {
if (originalObj[attr] !== undefined) {
newObj[attr] = deepClone(originalObj[attr]);
} else {
console.log(`Attribute ${attr} ignored by copying`);
}
} else
if (oldObj[attr] === '__delete__' && !isNonEdit) {
if (newObj[attr] !== undefined) {
delete newObj[attr];
}
} else {
newObj[attr] = oldObj[attr];
}
} else {
newObj[attr] = newObj[attr] || {};
copyAttributes(oldObj[attr], newObj[attr], originalObj && originalObj[attr], isNonEdit || attr === 'nonEdit');
}
}
}
/**
* Checks the flag nonEdit and restores non-changeable values if required
*
* @alias checkNonEditable
* @memberof tools
* @param {object} oldObject source object
* @param {object} newObject destination object
*
*/
function checkNonEditable(oldObject, newObject) {
if (!oldObject) {
return true;
}
if (!oldObject.nonEdit && !newObject.nonEdit) {
return true;
}
// if nonEdit is protected with password
if (oldObject.nonEdit && oldObject.nonEdit.passHash) {
// If new Object wants to update the nonEdit information
if (newObject.nonEdit && newObject.nonEdit.password) {
crypto = crypto || require('crypto');
const hash = crypto.createHash('sha256').update(newObject.nonEdit.password.toString()).digest('base64');
if (oldObject.nonEdit.passHash !== hash) {
delete newObject.nonEdit;
return false;
} else {
oldObject.nonEdit = deepClone(newObject.nonEdit);
delete oldObject.nonEdit.password;
delete newObject.nonEdit.password;
oldObject.nonEdit.passHash = hash;
newObject.nonEdit.passHash = hash;
}
copyAttributes(newObject.nonEdit, newObject, newObject);
if (newObject.passHash) {
delete newObject.passHash;
}
if (newObject.nonEdit && newObject.nonEdit.password) {
delete newObject.nonEdit.password;
}
return true;
} else {
newObject.nonEdit = oldObject.nonEdit;
}
} else if (newObject.nonEdit) {
oldObject.nonEdit = deepClone(newObject.nonEdit);
if (newObject.nonEdit.password) {
crypto = crypto || require('crypto');
const hash = crypto.createHash('sha256').update(newObject.nonEdit.password.toString()).digest('base64');
delete oldObject.nonEdit.password;
delete newObject.nonEdit.password;
oldObject.nonEdit.passHash = hash;
newObject.nonEdit.passHash = hash;
}
}
// restore settings
copyAttributes(oldObject.nonEdit, newObject, oldObject);
if (newObject.passHash) {
delete newObject.passHash;
}
if (newObject.nonEdit && newObject.nonEdit.password) {
delete newObject.nonEdit.password;
}
return true;
}
/**
* @param {string} repoVersion
* @param {string} installedVersion
*/
function upToDate(repoVersion, installedVersion) {
// Check if the installed version is at least the repo version
return semver.gte(installedVersion, repoVersion);
}
// TODO: this is only here for backward compatibility, if MULTIHOST password was still setup with old decryption
function decryptPhrase(password, data, callback) {
crypto = crypto || require('crypto');
const decipher = crypto.createDecipher('aes192', password);
try {
let decrypted = '';
decipher.on('readable', () => {
const data = decipher.read();
if (data) {
decrypted += data.toString('utf8');
}
});
decipher.on('error', error => {
console.error('Cannot decode secret: ' + error);
callback(null);
});
decipher.on('end', () => callback(decrypted));
decipher.write(data, 'hex');
decipher.end();
} catch (e) {
console.error('Cannot decode secret: ' + e.message);
callback(null);
}
}
function getAppName() {
const parts = __dirname.replace(/\\/g, '/').split('/');
return parts[parts.length - 2].split('.')[0];
}
function rmdirRecursiveSync(path) {
if (fs.existsSync(path)) {
fs.readdirSync(path).forEach(file => {
const curPath = path + '/' + file;
if (fs.statSync(curPath).isDirectory()) {
// recurse
rmdirRecursiveSync(curPath);
} else {
// delete file
fs.unlinkSync(curPath);
}
});
// delete (hopefully) empty folder
try {
fs.rmdirSync(path);
} catch (e) {
console.log('Cannot delete directory ' + path + ': ' + e.message);
}
}
}
function findIPs() {
if (!lastCalculationOfIps || Date.now() - lastCalculationOfIps > 10000) {
lastCalculationOfIps = Date.now();
ownIpArr = [];
try {
const ifaces = require('os').networkInterfaces();
Object.keys(ifaces).forEach(dev =>
ifaces[dev].forEach(details =>
// noinspection JSUnresolvedVariable
!details.internal && ownIpArr.push(details.address)));
} catch (e) {
console.error(`Can not find local IPs: ${e.message}`);
}
}
return ownIpArr;
}
function findPath(path, url) {
if (!url) {
return '';
}
if (url.substring(0, 'http://'.length) === 'http://' ||
url.substring(0, 'https://'.length) === 'https://') {
return url;
} else {
if (path.substring(0, 'http://'.length) === 'http://' ||
path.substring(0, 'https://'.length) === 'https://') {
return (path + url).replace(/\/\//g, '/').replace('http:/', 'http://').replace('https:/', 'https://');
} else {
if (url[0] === '/') {
return __dirname + '/..' + url;
} else {
return __dirname + '/../' + path + url;
}
}
}
}
function getMac(callback) {
const macRegex = /(?:[a-z0-9]{2}[:-]){5}[a-z0-9]{2}/ig;
const zeroRegex = /(?:[0]{2}[:-]){5}[0]{2}/;
const command = (process.platform.indexOf('win') === 0) ? 'getmac' : 'ifconfig || ip link';
require('child_process').exec(command, {windowsHide: true}, (err, stdout, _stderr) => {
if (err) {
callback(err);
} else {
let macAddress;
let match;
let result = null;
while (true) {
match = macRegex.exec(stdout);
if (!match) {
break;
}
macAddress = match[0];
if (!zeroRegex.test(macAddress) && !result) {
result = macAddress;
}
}
if (result === null) {
callback(new Error('could not determine the mac address from:\n' + stdout));
} else {
callback(null, result.replace(/-/g, ':').toLowerCase());
}
}
});
}
// Is docker environment
function isDocker() {
let _isDocker = false;
try {
fs.statSync('/.dockerenv');
_isDocker = true;
} catch {
// ignore error
}
let _isDockerGroup = false;
try {
_isDockerGroup = fs.readFileSync('/proc/self/cgroup', 'utf8').includes('docker');
} catch {
// ignore error
}
return _isDocker || _isDockerGroup;
}
// Build unique uuid based on MAC address if possible
function uuid(givenMac, callback) {
if (typeof givenMac === 'function') {
callback = givenMac;
givenMac = '';
}
const _isDocker = isDocker();
// return constant UUID for all CI environments to keep the statistics clean
if (require('ci-info').isCI) {
return callback('55travis-pipe-line-cior-githubaction');
}
let mac = givenMac !== null ? (givenMac || '') : null;
let u;
if (!_isDocker && mac === '') {
const ifaces = require('os').networkInterfaces();
// Find first not empty MAC
for (const n of Object.keys(ifaces)) {
for (let c = 0; c < ifaces[n].length; c++) {
if (ifaces[n][c].mac && ifaces[n][c].mac !== '00:00:00:00:00:00') {
mac = ifaces[n][c].mac;
break;
}
}
if (mac) {
break;
}
}
}
if (!_isDocker && mac === '') {
return getMac((_err, mac) => uuid(mac || null, callback));
}
if (!_isDocker && mac) {
const md5sum = require('crypto').createHash('md5');
md5sum.update(mac);
mac = md5sum.digest('hex');
u = mac.substring(0, 8) + '-' + mac.substring(8, 12) + '-' + mac.substring(12, 16) + '-' + mac.substring(16, 20) + '-' + mac.substring(20);
} else {
// Returns a RFC4122 compliant v4 UUID https://gist.github.com/LeverOne/1308368 (DO WTF YOU WANT TO PUBLIC LICENSE)
/** @type {any} */
let a;
let b;
b = a = '';
while (a++ < 36) {
b += ((a * 51) & 52) ? (a ^ 15 ? 8 ^ Math.random() * (a ^ 20 ? 16 : 4) : 4).toString(16) : '-';
}
u = b;
}
callback(u);
}
function updateUuid(newUuid, _objects, callback) {
uuid('', _uuid => {
_uuid = newUuid || _uuid;
// Add vendor prefix to UUID
if (fs.existsSync(VENDOR_FILE)) {
try {
const vendor = require(VENDOR_FILE);
if (vendor.vendor && vendor.vendor.uuidPrefix && vendor.vendor.uuidPrefix.length === 2) {
_uuid = vendor.vendor.uuidPrefix + _uuid;
}
} catch {
console.error(`Cannot parse ${VENDOR_FILE}`);
}
}
_objects.setObject('system.meta.uuid', {
type: 'meta',
common: {
name: 'uuid',
type: 'uuid'
},
ts: new Date().getTime(),
from: 'system.host.' + getHostName() + '.tools',
native: {
uuid: _uuid
}
}, err => {
if (err) {
console.error('object system.meta.uuid cannot be updated: ' + err);
callback();
} else {
_objects.getObject('system.meta.uuid', (err, obj) => {
if (obj.native.uuid !== _uuid) {
console.error('object system.meta.uuid cannot be updated: write protected');
} else {
console.log('object system.meta.uuid created: ' + _uuid);
}
callback(_uuid);
});
}
});
});
}
function createUuid(_objects, callback) {
const promiseCheckPassword = new Promise(resolve =>
_objects.getObject('system.user.admin', (err, obj) => {
if (err || !obj) {
password = password || require('./password');
// Default Password for user 'admin' is application name in lower case
password(getAppName()).hash(null, null, (err, res) => {
err && console.error(err);
// Create user here and not in io-package.js because of hash password
_objects.setObject('system.user.admin', {
type: 'user',
common: {
name: 'admin',
password: res,
dontDelete: true,
enabled: true
},
ts: new Date().getTime(),
from: 'system.host.' + getHostName() + '.tools',
native: {}
}, () => {
console.log('object system.user.admin created');
resolve();
});
});
} else {
resolve();
}
})
);
const promiseCheckUuid = new Promise(resolve =>
_objects.getObject('system.meta.uuid', (err, obj) => {
if (!err && obj && obj.native && obj.native.uuid) {
const PROBLEM_UUIDS = [
'ab265f4a-67f9-a46a-c0b2-61e4b95cefe5',
'7abd3182-d399-f7bd-da19-9550d8babede',
'deb6f2a8-fe69-5491-0a50-a9f9b8f3419c',
'ec66c85e-fc36-f6f9-f1c9-f5a2882d23c7',
'e6203b03-f5f4-253a-e4f6-b295fc543ab7',
'd659fa3d-7ef9-202a-ea23-acd0aff67b24'
];
// if COMMON invalid docker uuid
if (PROBLEM_UUIDS.find(u => u === obj.native.uuid)) {
// Read vis license
_objects.getObject('system.adapter.vis.0', (err, licObj) => {
if (!licObj || !licObj.native || !licObj.native.license) {
// generate new UUID
updateUuid('', _objects, _uuid => resolve(_uuid));
} else {
// decode obj.native.license
let data;
try {
data = jwt.decode(licObj.native.license);
} catch {
data = null;
}
if (!data || !data.uuid) {
// generate new UUID
updateUuid('', _objects, __uuid => resolve(__uuid));
} else {
if (data.uuid !== obj.native.uuid) {
updateUuid(data.correct ? data.uuid : '', _objects, _uuid => resolve(_uuid));
} else {
// Show error
console.warn(`Your iobroker.vis license must be updated. Please contact info@iobroker.net to get a new license!`);
console.warn(`Provide following information in email: ${data.email}, invoice: ${data.invoice}`);
resolve();
}
}
}
});
} else {
resolve();
}
} else {
// generate new UUID
updateUuid('', _objects, _uuid => resolve(_uuid));
}
})
);
return Promise.all([promiseCheckPassword, promiseCheckUuid])
.then(result => callback && callback(result[1]));
}
// Download file to tmp or return file name directly
function getFile(urlOrPath, fileName, callback) {
request = request || require('request');
// If object was read
if (urlOrPath.substring(0, 'http://'.length) === 'http://' ||
urlOrPath.substring(0, 'https://'.length) === 'https://') {
const tmpFile = `${__dirname}/../tmp/${fileName || Math.floor(Math.random() * 0xFFFFFFE) + '.zip'}`;
// Add some information to user-agent, like chrome, IE and Firefox do
request({url: urlOrPath, gzip: true, headers: {'User-Agent': `${module.exports.appName}, RND: ${randomID}, N: ${process.version}`}}).on('error', error => {
console.log(`Cannot download "${tmpFile}": ${error}`);
if (callback) {
callback(tmpFile);
}
}).pipe(fs.createWriteStream(tmpFile)).on('close', () => {
console.log('downloaded ' + tmpFile);
if (callback) {
callback(tmpFile);
}
});
} else {
if (fs.existsSync(urlOrPath)) {
if (callback) {
callback(urlOrPath);
}
} else
if (fs.existsSync(__dirname + '/../' + urlOrPath)) {
if (callback) {
callback(__dirname + '/../' + urlOrPath);
}
} else if (fs.existsSync(__dirname + '/../tmp/' + urlOrPath)) {
if (callback) {
callback(__dirname + '/../tmp/' + urlOrPath);
}
} else {
console.log('File not found: ' + urlOrPath);
process.exit(1);
}
}
}
// Return content of the json file. Download it or read directly
function getJson(urlOrPath, agent, callback) {
if (typeof agent === 'function') {
callback = agent;
agent = '';
}
agent = agent || '';
request = request || require('request');
let sources = {};
// If object was read
if (urlOrPath && typeof urlOrPath === 'object') {
if (callback) {
callback(urlOrPath);
}
} else
if (!urlOrPath) {
console.log('Empty url!');
if (callback) {
callback(null);
}
} else {
if (urlOrPath.substring(0, 'http://'.length) === 'http://' ||
urlOrPath.substring(0, 'https://'.length) === 'https://') {
request({url: urlOrPath, timeout: 10000, gzip: true, headers: {'User-Agent': agent}}, (error, response, body) => {
if (error || !body || response.statusCode !== 200) {
console.warn('Cannot download json from ' + urlOrPath + '. Error: ' + (error || body));
if (callback) {
callback(null, urlOrPath);
}
return;
}
try {
sources = JSON.parse(body);
} catch {
console.error('Json file is invalid on ' + urlOrPath);
if (callback) {
callback(null, urlOrPath);
}
return;
}
if (callback) {
callback(sources, urlOrPath);
}
}).on('error', _error => {
//console.log('Cannot download json from ' + urlOrPath + '. Error: ' + error);
//if (callback) callback(null, urlOrPath);
});
} else {
if (fs.existsSync(urlOrPath)) {
try {
sources = fs.readJSONSync(urlOrPath);
} catch (e) {
console.log('Cannot parse json file from ' + urlOrPath + '. Error: ' + e.message);
if (callback) {
callback(null, urlOrPath);
}
return;
}
if (callback) {
callback(sources, urlOrPath);
}
} else
if (fs.existsSync(__dirname + '/../' + urlOrPath)) {
try {
sources = fs.readJSONSync(__dirname + '/../' + urlOrPath);
} catch (e) {
console.log('Cannot parse json file from ' + __dirname + '/../' + urlOrPath + '. Error: ' + e.message);
if (callback) {
callback(null, urlOrPath);
}
return;
}
if (callback) {
callback(sources, urlOrPath);
}
} else if (fs.existsSync(__dirname + '/../tmp/' + urlOrPath)) {
try {
sources = fs.readJSONSync(__dirname + '/../tmp/' + urlOrPath);
} catch (e) {
console.log('Cannot parse json file from ' + __dirname + '/../tmp/' + urlOrPath + '. Error: ' + e.message);
if (callback) {
callback(null, urlOrPath);
}
return;
}
if (callback) {
callback(sources, urlOrPath);
}
} else {
//if (urlOrPath.indexOf('/example/') === -1) console.log('Json file not found: ' + urlOrPath);
if (callback) {
callback(null, urlOrPath);
}
}
}
}
}
function scanDirectory(dirName, list, regExp) {
if (fs.existsSync(dirName)) {
let dirs;
try {
dirs = fs.readdirSync(dirName);
} catch (e) {
console.log(`Cannot read or parse ${dirName}: ${e.message}`);
return;
}
for (let i = 0; i < dirs.length; i++) {
try {
const fullPath = path.join(dirName, dirs[i]);
const fileIoName = path.join(fullPath, 'io-package.json');
const fileName = path.join(fullPath, 'package.json');
if (regExp.test(dirs[i]) && fs.existsSync(fileIoName)) {
const ioPackage = fs.readJSONSync(fileIoName);
const package_ = fs.existsSync(fileName)
? fs.readJSONSync(fileName)
: {};
const localIcon = (ioPackage.common.icon ? `/adapter/${dirs[i].substring(module.exports.appName.length + 1)}/${ioPackage.common.icon}` : '');
//noinspection JSUnresolvedVariable
list[ioPackage.common.name] = {
controller: ioPackage.common.controller || false,
version: ioPackage.common.version,
icon: ioPackage.common.extIcon || localIcon,
localIcon,
title: ioPackage.common.title, // deprecated 2021.04.18 BF
titleLang: ioPackage.common.titleLang,
desc: ioPackage.common.desc,
platform: ioPackage.common.platform,
keywords: ioPackage.common.keywords,
readme: ioPackage.common.readme,
type: ioPackage.common.type,
license: ioPackage.common.license ? ioPackage.common.license : ((package_.licenses && package_.licenses.length) ? package_.licenses[0].type : ''),
licenseUrl: (package_.licenses && package_.licenses.length) ? package_.licenses[0].url : ''
};
}
} catch (e) {
console.log(`Cannot read or parse ${__dirname}/../node_modules/${dirs[i]}/io-package.json: ${e.message}`);
}
}
}
}
/**
* Get list of all installed adapters and controller version on this host
* @param {string} [hostRunningVersion] Version of the running js-controller, will be included in the returned information if provided
* @returns {object} object containing information about installed host
*/
function getInstalledInfo(hostRunningVersion) {
const result = {};
const fullPath = path.join(__dirname, '..');
// Get info about host
let ioPackage;
try {
ioPackage = fs.readJSONSync(path.join(fullPath, 'io-package.json'));
} catch (e) {
console.error(`Cannot get installed host information: ${e.message}`);
}
const package_ = fs.existsSync(path.join(fullPath, 'package.json')) ? fs.readJSONSync(path.join(fullPath, 'package.json')) : {};
const regExp = new RegExp(`^${module.exports.appName}\\.`, 'i');
if (ioPackage) {
result[ioPackage.common.name] = {
controller: true,
version: ioPackage.common.version,
icon: ioPackage.common.extIcon || ioPackage.common.icon,
title: ioPackage.common.title, // deprecated 2021.04.18 BF
titleLang: ioPackage.common.titleLang,
desc: ioPackage.common.desc,
platform: ioPackage.common.platform,
keywords: ioPackage.common.keywords,
readme: ioPackage.common.readme,
runningVersion: hostRunningVersion,
license: ioPackage.common.license ? ioPackage.common.license : ((package_.licenses && package_.licenses.length) ? package_.licenses[0].type : ''),
licenseUrl: (package_.licenses && package_.licenses.length) ? package_.licenses[0].url : ''
};
}
scanDirectory(path.join(__dirname, '../node_modules'), result, regExp);
scanDirectory(path.join(__dirname, '../../node_modules'), result, regExp);
if (
fs.existsSync(path.join(__dirname, `../../../node_modules/${module.exports.appName.toLowerCase()}.js-controller`)) ||
fs.existsSync(path.join(__dirname, `../../../node_modules/${module.exports.appName}.js-controller`))
) {
scanDirectory(path.join(__dirname, '../..'), result, regExp);
}
return result;
}
/**
* Reads an adapter's npm version
* @param {string | null} adapter The adapter to read the npm version from. Null for the root ioBroker packet
* @param {(err: Error | null, version?: string) => void} [callback]
*/
function getNpmVersion(adapter, callback) {
adapter = adapter ? module.exports.appName + '.' + adapter : module.exports.appName;
adapter = adapter.toLowerCase();
const cliCommand = `npm view ${adapter} version`;
const exec = require('child_process').exec;
exec(cliCommand, {timeout: 2000, windowsHide: true}, (error, stdout, _stderr) => {
let version;
if (error) {
// command failed
if (typeof callback === 'function') {
callback(error);
return;
}
} else if (stdout) {
version = semver.valid(stdout.trim());
}
if (typeof callback === 'function') {
callback(null, version);
}
});
}
function getIoPack(sources, name, callback) {
getJson(sources[name].meta, ioPack => {
const packUrl = sources[name].meta.replace('io-package.json', 'package.json');
if (!ioPack) {
if (sources._helper) {
sources._helper.failCounter.push(name);
}
if (callback) {
callback(sources, name);
}
} else {
setImmediate(() => {
getJson(packUrl, pack => {
const version = sources[name].version;
const type = sources[name].type;
// If installed from git or something else
// js-controller is exception, because can be installed from npm and from git
if (sources[name].url && name !== 'js-controller') {
if (ioPack && ioPack.common) {
sources[name] = extend(true, sources[name], ioPack.common);
// overwrite type of adapter from repository
if (type) {
sources[name].type = type;
}
if (pack && pack.licenses && pack.licenses.length) {
if (!sources[name].license) {
sources[name].license = pack.licenses[0].type;
}
if (!sources[name].licenseUrl) {
sources[name].licenseUrl = pack.licenses[0].url;
}
}
}
if (callback) {
callback(sources, name);
}
} else {
if (ioPack && ioPack.common) {
sources[name] = extend(true, sources[name], ioPack.common);
if (pack && pack.licenses && pack.licenses.length) {
if (!sources[name].license) {
sources[name].license = pack.licenses[0].type;
}
if (!sources[name].licenseUrl) {
sources[name].licenseUrl = pack.licenses[0].url;
}
}
}
// overwrite type of adapter from repository
if (type) {
sources[name].type = type;
}
if (version) {
sources[name].version = version;
if (callback) {
callback(sources, name);
}
} else {
if (sources[name].meta.substring(0, 'http://'.length) === 'http://' ||
sources[name].meta.substring(0, 'https://'.length) === 'https://') {
//installed from npm
getNpmVersion(name, (_err, version) => {
if (version) {
sources[name].version = version;
} else {
sources[name].version = 'npm error';
}
if (callback) {
callback(sources, name);
}
});
} else {
if (callback) {
callback(sources, name);
}
}
}
}
});
});
}
});
}
function _getRepositoryFile(sources, path, callback) {
if (!sources._helper) {
let count = 0;
for (const _name in sources) {
if (!Object.prototype.hasOwnProperty.call(sources, _name)) {
continue;
}
count++;
}
sources._helper = {failCounter: []};
sources._helper.timeout = setTimeout(() => {
if (sources._helper) {
delete sources._helper;
for (const __name of Object.keys(sources)) {
if (sources[__name].processed !== undefined) {
delete sources[__name].processed;
}
}
if (callback) {
callback('Timeout by read all package.json (' + count + ') seconds', sources);
}
callback = null;
}
}, count * 1000);
}
for (const name of Object.keys(sources)) {
if (sources[name].processed || name === '_helper') {
continue;
}
sources[name].processed = true;
if (sources[name].url) {
sources[name].url = findPath(path, sources[name].url);
}
if (sources[name].meta) {
sources[name].meta = findPath(path, sources[name].meta);
}
if (sources[name].icon) {
sources[name].icon = findPath(path, sources[name].icon);
}
if (!sources[name].name && sources[name].meta) {
getIoPack(sources, name, _ignore => {
if (sources._helper) {
if (sources._helper.failCounter.length > 10) {
clearTimeout(sources._helper.timeout);
delete sources._helper;
for (const _name of Object.keys(sources)) {
if (sources[_name].processed !== undefined) {
delete sources[_name].processed;
}
}
if (callback) {
callback('Looks like there is no internet.', sources);
}
callback = null;
} else {
// process next
setImmediate(() =>
_getRepositoryFile(sources, path, callback));
}
}
});
return;
}
}
// all packages are processed
if (sources._helper) {
let err;
if (sources._helper.failCounter.length) {
err = 'Following packages cannot be read: ' + sources._helper.failCounter.join(', ');
}
clearTimeout(sources._helper.timeout);
delete sources._helper;
for (const __name of Object.keys(sources)) {
if (sources[__name].processed !== undefined) {
delete sources[__name].processed;
}
}
if (callback) {
callback(err, sources);
}
callback = null;
}
}
function _checkRepositoryFileHash(urlOrPath, additionalInfo, callback) {
request = request || require('request');
// read hash of file
if (urlOrPath.startsWith('http://') || urlOrPath.startsWith('https://')) {
urlOrPath = urlOrPath.replace(/\.json$/, '-hash.json');
let json = null;
request({url: urlOrPath, timeout: 10000, gzip: true}, (error, response, body) => {
if (error || !body || response.statusCode !== 200) {
console.warn('Cannot download json from ' + urlOrPath + '. Error: ' + (error || body));
} else {
try {
json = JSON.parse(body);
} catch {
console.error('Json file is invalid on ' + urlOrPath);
}
}
if (json && json.hash) {
// The hash download was successful
if (additionalInfo && additionalInfo.sources && json.hash === additionalInfo.hash) {
// The hash is the same as for the cached sources
console.log('hash unchanged, use cached sources');
callback(null, additionalInfo.sources, json.hash);
} else {
// Either we have no sources cached or the hash changed
// => force download of new sources
console.log('hash changed or no sources cached => force download of new sources');
callback(null, null, json.hash);
}
} else {
// Could not download new sources, use the old ones
console.log('failed to download new sources, use cached sources');
callback(null, additionalInfo.sources, '');
}
}).on('error', _error => {
//console.log('Cannot download json from ' + urlOrPath + '. Error: ' + error);
//if (callback) callback(null, urlOrPath);
});
} else {
// it is a file and file has not hash
callback(null, null, 0);
}
}
/**
* Get list of all adapters and controller in some repository file or in /conf/source-dist.json
*
* @alias getRepositoryFile
* @memberof tools
* @param {string} urlOrPath URL stargin with http:// or https:// or local file link
* @param {object} additionalInfo destination object
* @param {function} callback function (err, sources, actualHash) { }
*
*/
function getRepositoryFile(urlOrPath, additionalInfo, callback) {
let sources = {};
let path = '';
if (typeof additionalInfo === 'function') {
callback = additionalInfo;
additionalInfo = {};
}
additionalInfo = additionalInfo || {};
extend = extend || require('node.extend');
if (urlOrPath) {
const parts = urlOrPath.split('/');
path = parts.splice(0, parts.length - 1).join('/') + '/';
}
// If object was read
if (urlOrPath && typeof urlOrPath === 'object') {
if (typeof callback === 'function') {
callback(null, urlOrPath);
}
} else
if (!urlOrPath) {
try {
sources = fs.readJSONSync(getDefaultDataDir() + 'sources.json');
} catch {
sources = {};
}
try {
const sourcesDist = fs.readJSONSync(__dirname + '/../conf/sources-dist.json');
sources = extend(true, sourcesDist, sources);
} catch {
// continue regardless of error
}
for (const s of Object.keys(sources)) {
if (additionalInfo[s] && additionalInfo[s].published) {
sources[s].published = additionalInfo[s].published;
}
}
_getRepositoryFile(sources, path, err => {
if (err) {
console.error(`[${new Date()}] ${err}`);
}
if (typeof callback === 'function') {
callback(err, sources);
}
});
} else {
let agent = '';
if (additionalInfo) {
// Add some information to user-agent, like chrome, IE and Firefox do
agent = `${additionalInfo.name}, RND: ${additionalInfo.randomID || randomID}, Node:${additionalInfo.node}, V:${additionalInfo.controller}`;
}
// load hash of file first to not load the whole 1MB of sources
_checkRepositoryFileHash(urlOrPath, additionalInfo, (err, sources, actualSourcesHash) => {
if (!err && sources) {
// Source file was not changed
typeof callback === 'function' && callback(err, sources, actualSourcesHash);
} else {
getJson(urlOrPath, agent, sources => {
if (sources) {
for (const s of Object.keys(sources)) {
if (additionalInfo[s] && additionalInfo[s].published) {
sources[s].published = additionalInfo[s].published;
}
}
setImmediate(() =>
_getRepositoryFile(sources, path, err => {
err && console.error(`[${new Date()}] ${err}`);
typeof callback === 'function' && callback(err, sources, actualSourcesHash);
}));
} else {
// return cached sources, because no sources found
console.log(`failed to download new sources, ${additionalInfo.sources ? 'use cached sources' : 'no cached sources available'}`);
return maybeCallbackWithError(callback, `Cannot read "${urlOrPath}"`, additionalInfo.sources, '');
}
});
}
});
}
}
function sendDiagInfo(obj, callback) {
request = request || require('request');
console.log(`Send diag info: ${JSON.stringify(obj)}`);
request.post({
url: 'http://download.' + module.exports.appName + '.net/diag.php',
method: 'POST',
gzip: true,
headers: {'content-type': 'application/x-www-form-urlencoded'},
body: 'data=' + JSON.stringify(obj),
timeout: 2000
}, (_err, _response, _body) => {
/*if (err || !body || response.statusCode !== 200) {
}*/
if (typeof callback === 'function') {
callback();
}
}).on('error', error => {
console.log('Cannot send diag info: ' + error.message);
if (typeof callback === 'function') {
callback(error);
}
});
}
/**
* Finds the adapter directory of a given adapter
*
* @alias getAdapterDir
* @memberof tools
* @param {string} adapter name of the adapter, e.g. hm-rpc
* @returns {string|null} path to adapter directory or null if no directory found
*/
function getAdapterDir(adapter) {
const appName = module.exports.appName;
// snip off 'iobroker.'
if (adapter.startsWith(appName + '.')) {
adapter = adapter.substring(appName.length + 1);
}
// snip off instance id
if (/\.\d+$/.test(adapter)) {
adapter = adapter.substr(0, adapter.lastIndexOf('.'));
}
const possibilities = [
`${appName.toLowerCase()}.${adapter}/package.json`,
`${appName}.${adapter}/package.json`
];
/** @type {string} */
let adapterPath;
for (const possibility of possibilities) {
// special case to not read adapters from js-controller/node_module/adapter adn check first in parent directory
if (fs.existsSync(`${__dirname}/../../${possibility}`)) {
adapterPath = `${__dirname}/../../${possibility}`;
} else {
try {
adapterPath = require.resolve(possibility);
break;
} catch { /* not found */ }
}
}
if (!adapterPath) {
return null; // inactive
} else {
const parts = path.normalize(adapterPath).split(/[\\/]/g);
parts.pop();
return parts.join('/');
}
}
/**
* Returns the hostname of this host
* @alias getHostName
* @returns {string}
*/
function getHostName() {
// for tests purposes
if (process.env.IOB_HOSTNAME) {
return process.env.IOB_HOSTNAME;
}
try {
const configName = getConfigFileName();
const config = fs.readJSONSync(configName);
return config.system ? config.system.hostname || require('os').hostname() : require('os').hostname();
} catch {
return require('os').hostname();
}
}
/**
* Read version of system npm
*
* @alias getSystemNpmVersion
* @memberof Tools
* @param {function} callback return result
* <pre><code>
* function (err, version) {
* adapter.log.debug('NPM version is: ' + version);
* }
* </code></pre>
*/
function getSystemNpmVersion(callback) {
const exec = require('child_process').exec;
// remove local node_modules\.bin dir from path
// or we potentially get a wrong npm version
const newEnv = Object.assign({}, process.env);
newEnv.PATH = (newEnv.PATH || newEnv.Path || newEnv.path)
.split(path.delimiter)
.filter(dir => {
dir = dir.toLowerCase();
return !(dir.indexOf('iobroker') > -1 && dir.indexOf(path.join('node_modules', '.bin')) > -1);
})
.join(path.delimiter);
try {
let timeout = setTimeout(() => {
timeout = null;
if (callback) {
callback('timeout');
callback = null;
}
}, 10000);
exec('npm -v', {encoding: 'utf8', env: newEnv, windowsHide: true}, (error, stdout) => {//, stderr) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
if (stdout) {
stdout = semver.valid(stdout.trim());
}
if (callback) {
callback(error, stdout);
callback = null;
}
});
} catch (e) {
if (callback) {
callback(e);
callback = null;
}
}
}
/**
* Read disk free space
*
* @alias getDiskInfo
* @memberof Tools
* @param {string} platform result of os.platform() (win32 => Windows, darwin => OSX)
* @param {function} callback return result
* <pre><code>
* function (err, infos) {
* adapter.log.debug('Disks sizes is: ' + info['Disk size'] + ' - ' + info['Disk free']);
* }
* </code></pre>
*/
function getDiskInfo(platform, callback) {
platform = platform || require('os').platform();
if (diskusage) {
try {
const path = platform === 'win32' ? __dirname.substring(0, 2) : '/';
const info = diskusage.checkSync(path);
return callback && callback(null, {'Disk size': info.total, 'Disk free': info.free});
} catch (err) {
console.log(err);
}
} else {
const exec = require('child_process').exec;
try {
if (platform === 'Windows' || platform === 'win32') {
// Caption FreeSpace Size
// A:
// C: 66993807360 214640357376
// D:
// Y: 116649795584 148368257024
// Z: 116649795584 148368257024
const disk = __dirname.substring(0, 2).toUpperCase();
exec('wmic logicaldisk get size,freespace,caption', {encoding: 'utf8', windowsHide: true}, (error, stdout) => {//, stderr) {
if (stdout) {
const lines = stdout.split('\n');
const line = lines.find(line => {
const parts = line.split(/\s+/);
return parts[0].toUpperCase() === disk;
});
if (line) {
const parts = line.split(/\s+/);
return callback && callback(error, {'Disk size': parseInt(parts[2]), 'Disk free': parseInt(parts[1])});
}
}
callback && callback(error, null);
});
} else {
exec('df -k /', {encoding: 'utf8', windowsHide: true}, (error, stdout) => {//, stderr) {
// Filesystem 1K-blocks Used Available Use% Mounted on
// /dev/mapper/vg00-lv01 162544556 9966192 145767152 7% /
try {
if (stdout) {
const parts = stdout.split('\n')[1].split(/\s+/);
return callback && callback(error, {'Disk size': parseInt(parts[1]) * 1024, 'Disk free': parseInt(parts[3]) * 1024});
}
} catch {
// continue regardless of error
}
callback && callback(error, null);
});
}
} catch (e) {
callback && callback(e, null);
}
}
}
/**
* Returns information about a certificate
*
*
* Following info will be returned:
* - certificate: the certificate itself
* - serialNumber: serial number
* - signature: type of signature as text like "RSA",
* - keyLength: bits used for encryption key like 2048
* - issuer: issuer of the certificate
* - subject: subject that is signed
* - dnsNames: server name this certificate belong to
* - keyUsage: this certificate can be used for the followinf puposes
* - extKeyUsage: usable or client, server or ...
* - validityNotBefore: certificate validity start datetime
* - validityNotAfter: certificate validity end datetime
*
* @alias getCertificateInfo
* @memberof Tools
* @param {string} cert
* @return certificate information object
*/
function getCertificateInfo(cert) {
let info = null;
if (!cert) {
return null;
}
// https://github.com/digitalbazaar/forge
forge.options.usePureJavaScript = false;
const pki = forge.pki;
let certFile = null;
try {
if (typeof cert === 'string' && cert.length < 1024 && fs.existsSync(cert)) {
certFile = cert;
cert = fs.readFileSync(cert, 'utf8');
}
const crt = pki.certificateFromPem(cert);
info = {
certificateFilename: certFile,
certificate: cert,
serialNumber: crt.serialNumber,
signature: pki.oids[crt.signatureOid],
keyLength: crt.publicKey.n.toString(2).length,
issuer: crt.issuer,
subject: crt.subject,
dnsNames: crt.getExtension('subjectAltName').altNames,
keyUsage: crt.getExtension('keyUsage'),
extKeyUsage: crt.getExtension('extKeyUsage'),
validityNotBefore: crt.validity.notBefore,
validityNotAfter: crt.validity.notAfter
};
// do not return info about values
delete info.keyUsage.value;
delete info.extKeyUsage.value;
return info;
} catch {
return null;
}
}
/**
* Returns default SSL certificates (private and public)
*
*
* Following info will be returned:
* - defaultPrivate: private RSA key
* - defaultPublic: public certificate
*
* @alias generateDefaultCertificates
* @memberof Tools
* @returns {{ defaultPrivate: any[];defaultPublic: any[] }}
* <pre><code>
* const certificates = tools.generateDefaultCertificates();
* </code></pre>
*/
function generateDefaultCertificates() {
// If at any time you wish to disable the use of native code, where available, for particular forge features
// like its secure random number generator, you may set the forge.options.usePureJavaScript flag to true. It
// is not recommended that you set this flag as native code is typically more performant and may have stronger
// security properties. It may be useful to set this flag to test certain features that you plan to run in
// environments that are different from your testing environment.
// https://github.com/digitalbazaar/forge
forge.options.usePureJavaScript = false;
const pki = forge.pki;
const keys = pki.rsa.generateKeyPair({ bits: 2048, e: 0x10001 });
const cert = pki.createCertificate();
cert.publicKey = keys.publicKey;
cert.serialNumber = '0' + makeid(17);
cert.validity.notBefore = new Date();
cert.validity.notAfter = new Date();
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1);
const subAttrs = [
{ name: 'commonName', value: getHostName() },
{ name: 'organizationName', value: 'ioBroker GmbH' },
{ shortName: 'OU', value: 'iobroker' }
];
const issAttrs = [
{ name: 'commonName', value: 'iobroker' },
{ name: 'organizationName', value: 'ioBroker GmbH' },
{ shortName: 'OU', value: 'iobroker' }
];
cert.setSubject(subAttrs);
cert.setIssuer(issAttrs);
cert.setExtensions([
{
name: 'basicConstraints',
critical: true,
cA: false
},
{
name: 'keyUsage',
critical: true,
digitalSignature: true,
contentCommitment: true,
keyEncipherment: true,
dataEncipherment: true,
keyAgreement: true,
keyCertSign: true,
cRLSign: true,
encipherOnly: true,
decipherOnly: true
},
{
name: 'subjectAltName',
altNames: [{
type: 2,
value: os.hostname()
}]
},
{
name: 'subjectKeyIdentifier'
},
{
name: 'extKeyUsage',
serverAuth: true,
clientAuth: true,
codeSigning: false,
emailProtection: false,
timeStamping: false
},
{
name: 'authorityKeyIdentifier'
}
]);
cert.sign(keys.privateKey, forge.md.sha256.create());
const pem_pkey = pki.privateKeyToPem(keys.privateKey);
const pem_cert = pki.certificateToPem(cert);
//console.log(pem_pkey);
//console.log(pem_cert);
return {
defaultPrivate: pem_pkey,
defaultPublic: pem_cert
};
}
function makeid(length) {
let result = '';
const characters = 'abcdef0123456789';
const charactersLength = characters.length;
for ( let i = 0; i < length; i++ ) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
/**
* Collects information about host and available adapters
*
* Following info will be collected:
* - available adapters
* - node.js --version
* - npm --version
*
* @alias getHostInfo
* @memberof Tools
* @param {object} objects
* @param {function} callback return result
* <pre><code>
* function (err, result) {
* adapter.log.debug('Info about host: ' + JSON.stringify(result, null, 2);
* }
* </code></pre>
*/
function getHostInfo(objects, callback) {
const os = require('os');
if (diskusage !== false) {
try {
diskusage = diskusage || require('diskusage');
} catch {
diskusage = false;
}
}
const cpus = os.cpus();
const data = {
Platform: os.platform(),
os: process.platform,
Architecture: os.arch(),
CPUs: cpus && Array.isArray(cpus) ? cpus.length : null,
Speed: cpus && Array.isArray(cpus) ? cpus[0].speed : null,
Model: cpus && Array.isArray(cpus) ? cpus[0].model : null,
RAM: os.totalmem(),
'System uptime': Math.round(os.uptime()),
'Node.js': process.version
};
if (data.Platform === 'win32') {
data.Platform = 'Windows';
} else
if (data.Platform === 'darwin') {
data.Platform = 'OSX';
}
let task = 0;
task++;
objects.getObject('system.config', (_err, systemConfig) => {
objects.getObject('system.repositories', (err, repos) => {
// Check if repositories exists
if (!err && repos && repos.native && repos.native.repositories) {
const repo = repos.native.repositories[systemConfig.common.activeRepo];
if (repo && repo.json) {
data['adapters count'] = Object.keys(repo.json).length;
}
}
if (!--task) {
callback(err, data);
}
});
});
if (!npmVersion) {
task++;
getSystemNpmVersion((err, version) => {
data['NPM'] = 'v' + (version || ' ---');
npmVersion = version;
if (!--task) {
callback(err, data);
}
});
} else {
data['NPM'] = npmVersion;
if (!task) {
callback(null, data);
}
}
task++;
getDiskInfo(data.Platform, (err, info) => {
if (info) {
Object.assign(data, info);
}
if (!--task) {
callback(err, data);
}
});
}
// All paths are returned always relative to /node_modules/' + module.exports.appName + '.js-controller
// the result has always "/" as last symbol
function getDefaultDataDir() {
//var dataDir = __dirname.replace(/\\/g, '/');
//dataDir = dataDir.split('/');
const appName = module.exports.appName.toLowerCase();
// if debugging with npm5
if (fs.existsSync(__dirname + '/../../node_modules/' + appName + '.js-controller')) {
return '../' + appName + '-data/';
} else // If installed with npm
if (fs.existsSync(__dirname + '/../../../node_modules/' + appName + '.js-controller')) {
return '../../' + appName + '-data/';
} else {
//dataDir.splice(dataDir.length - 1, 1);
//dataDir = dataDir.join('/');
return './data/';
}
}
/**
* Returns the path of the config file
*
* @returns {string}
*/
function getConfigFileName() {
/** @type {string|string[]} */
let configDir = __dirname.replace(/\\/g, '/');
configDir = configDir.split('/');
const appName = module.exports.appName.toLowerCase();
// If installed with npm
if (fs.existsSync(__dirname + '/../../../node_modules/' + appName.toLowerCase() + '.js-controller') ||
fs.existsSync(__dirname + '/../../../node_modules/' + appName + '.js-controller')) {
// remove /node_modules/' + appName + '.js-controller/lib
configDir.splice(configDir.length - 3, 3);
configDir = configDir.join('/');
return configDir + '/' + appName + '-data/' + appName + '.json';
} else
// if debugging with npm5
if (fs.existsSync(__dirname + '/../../node_modules/' + appName.toLowerCase() + '.js-controller') ||
fs.existsSync(__dirname + '/../../node_modules/' + appName + '.js-controller')) {
// remove /node_modules/' + appName + '.js-controller/lib
configDir.splice(configDir.length - 2, 2);
configDir = configDir.join('/');
return configDir + '/' + appName + '-data/' + appName + '.json';
} else {
// Remove /lib
configDir.splice(configDir.length - 1, 1);
configDir = configDir.join('/');
if (fs.existsSync(__dirname + '/../conf/' + appName + '.json')) {
return configDir + '/conf/' + appName + '.json';
} else {
return configDir + '/data/' + appName + '.json';
}
}
}
/**
* Puts all values from an `arguments` object into an array, starting at the given index
* @param {IArguments} argsObj An `arguments` object as passed to a function
* @param {number} [startIndex=0] The optional index to start taking the arguments from
*/
function sliceArgs(argsObj, startIndex) {
if (startIndex === null || startIndex === undefined) {
startIndex = 0;
}
const ret = [];
for (let i = startIndex; i < argsObj.length; i++) {
ret.push(argsObj[i]);
}
return ret;
}
/**
* Promisifies a function which returns an error as the first argument in its callback
* @param {Function} fn The function to promisify
* @param {any} [context=this] (optional) The context (value of `this` to bind the function to)
* @param {string[]} [returnArgNames] (optional) If the callback contains multiple arguments,
* you can combine them into one object by passing the names as an array.
* Otherwise the Promise will resolve with an array
* @returns {(...args: any[]) => Promise<any>}
*/
function promisify(fn, context, returnArgNames) {
return function () {
const args = sliceArgs(arguments);
// @ts-ignore we cannot know the type of `this`
context = context || this;
return new Promise((resolve, reject) => {
fn.apply(context, args.concat([
function (error, result) {
if (error) {
return reject(error instanceof Error ? error : new Error(error));
} else {
// decide on how we want to return the callback arguments
switch (arguments.length) {
case 1: // only an error was given
return resolve(); // Promise<void>
case 2: // a single value (result) was returned
return resolve(result);
default: {// multiple values should be returned
/** @type {{} | any[]} */
let ret;
const extraArgs = sliceArgs(arguments, 1);
if (returnArgNames && returnArgNames.length === extraArgs.length) {
// we can build an object
ret = {};
for (let i = 0; i < returnArgNames.length; i++) {
ret[returnArgNames[i]] = extraArgs[i];
}
} else {
// we return the raw array
ret = extraArgs;
}
return resolve(ret);
}
}
}
}
]));
});
};
}
/**
* Promisifies a function which does not provide an error as the first argument in its callback
* @param {Function} fn The function to promisify
* @param {any} [context] (optional) The context (value of `this` to bind the function to)
* @param {string[]} [returnArgNames] (optional) If the callback contains multiple arguments,
* you can combine them into one object by passing the names as an array.
* Otherwise the Promise will resolve with an array
* @returns {(...args: any[]) => Promise<any>}
*/
function promisifyNoError(fn, context, returnArgNames) {
return function () {
const args = sliceArgs(arguments);
// @ts-ignore we cannot know the type of `this`
context = context || this;
return new Promise((resolve, _reject) => {
fn.apply(context, args.concat([
function (result) {
// decide on how we want to return the callback arguments
switch (arguments.length) {
case 0: // no arguments were given
return resolve(); // Promise<void>
case 1: // a single value (result) was returned
return resolve(result);
default: {// multiple values should be returned
/** @type {{} | any[]} */
let ret;
const extraArgs = sliceArgs(arguments, 0);
if (returnArgNames && returnArgNames.length === extraArgs.length) {
// we can build an object
ret = {};
for (let i = 0; i < returnArgNames.length; i++) {
ret[returnArgNames[i]] = extraArgs[i];
}
} else {
// we return the raw array
ret = extraArgs;
}
return resolve(ret);
}
}
}
]));
});
};
}
/**
* Creates and executes an array of promises in sequence
* @param {((...args: any[]) => Promise<any>)[]} promiseFactories An array of promise-returning functions
*/
function promiseSequence(promiseFactories) {
return promiseFactories.reduce((promise, factory) => {
return promise.then(result => factory().then(Array.prototype.concat.bind(result)));
}, Promise.resolve([]));
}
function _setQualityForStates(states, keys, quality, cb) {
if (!keys || !states || !keys.length) {
cb();
} else {
states.setState(keys.shift(), {ack: null, q: quality}, () => setImmediate(_setQualityForStates, states, keys, quality, cb));
}
}
function setQualityForInstance(objects, states, namespace, q) {
return new Promise((resolve, reject) => {
objects.getObjectView('system', 'state', {startkey: namespace + '.', endkey: namespace + '.\u9999', include_docs: false}, (err, _states) => {
if (err) {
reject(err);
} else {
let keys = [];
if (_states && _states.rows) {
for (let s = 0; s < _states.rows.length; s++) {
const id = _states.rows[s].id;
// if instance still active, but device is offline
if (!(q & 0x10) && id.match(/\.info\.connection$/)) {
continue;
}
keys.push(id);
}
}
// read all values for IDs
states.getStates(keys, (_err, values) => {
// Get only states, that have ack = true
keys = keys.filter((_id, i) => values[i] && values[i].ack);
// update quality code of the states to new one
_setQualityForStates(states, keys, q, err => err ? reject(err) : resolve());
});
}
});
});
}
/**
* Converts ioB pattern into regex.
* @param {string} pattern - Regex string to use it in new RegExp(pattern)
* @returns {string}
*/
function pattern2RegEx(pattern) {
pattern = (pattern || '').toString();
const startsWithWildcard = pattern[0] === '*';
const endsWithWildcard = pattern[pattern.length - 1] === '*';
pattern = pattern.replace(/[-/\\^$+?.()|[\]{}]/g, '\\$&').replace(/\*/g, '.*');
return (startsWithWildcard ? '' : '^') + pattern + (endsWithWildcard ? '' : '$');
}
/**
* Generates a stack trace that can be added to log outputs to trace their source
* @param {string} [wrapperName = 'captureStackTrace'] The wrapper function after which the stack trace should begin
* @returns {string}
*/
function captureStackTrace(wrapperName) {
if (typeof wrapperName !== 'string') {
wrapperName = 'captureStackTrace';
}
const ret = new Error();
if (ret.stack) {
let foundSelf = false;
const lines = ret.stack.split('\n')
.filter(line => {
// keep all lines after this function's
if (foundSelf) {
return true;
}
if (line.indexOf(wrapperName) > -1) {
foundSelf = true;
}
return false;
});
return lines.join('\n');
}
return '';
}
/**
* Appends the stack trace generated by `captureStackTrace` to the given string
* @param {string} str - The string to append the stack trace to
* @returns {string}
*/
function appendStackTrace(str) {
// Convert anything that isn't a string into a string
if (typeof str !== 'string') {
str = String(str);
}
if (str.substr(-1) !== '\n') {
str += '\n';
}
return str + captureStackTrace('appendStackTrace');
}
/**
* @template T
* @typedef {{new (...args: any[]): T}} ES6Class<T>
*/
/**
* @template T
* @typedef {{new (...args: any[]): T; (...args: any[]): T; prototype: Function}} Class<T>
*/
/**
* Wraps an ES6 class so it can be called from legacy code without the new keyword.
* Usage e.g.:
* ```js
// filename: foo.js
class Foo {
constructor() { this.bar = 1; }
}
module.exports = wrapES6Class(Foo);
// filename: index.js
const Foo = require("./foo");
var x = new Foo(); // works!
var y = Foo(); // works too!
```
* @template T
* @param {ES6Class<T>} Class
* @returns {Class<T>}
*/
function wrapES6Class(Class) {
const _Class = function _Class() {
const args = sliceArgs(arguments);
return new (Function.prototype.bind.apply(Class, [null].concat(args)))();
};
_Class.prototype = Class.prototype;
// @ts-ignore
return _Class;
}
/**
* Encrypt the password/value with given key
* @param {string} key - Secret key
* @param {string} value - value to encrypt
* @returns {string}
*/
function encryptLegacy(key, value) {
let result = '';
for(let i = 0; i < value.length; i++) {
result += String.fromCharCode(key[i % key.length].charCodeAt(0) ^ value.charCodeAt(i));
}
return result;
}
/**
* Decrypt the password/value with given key
* @param {string} key - Secret key
* @param {string} value - value to decrypt
* @returns {string}
*/
function decryptLegacy(key, value) {
let result = '';
for(let i = 0; i < value.length; i++) {
result += String.fromCharCode(key[i % key.length].charCodeAt(0) ^ value.charCodeAt(i));
}
return result;
}
/**
* encrypts a value by a given key via AES-192-CBC
*
* @param {string} key - Secret key
* @param {string} value - value to decrypt
* @returns {string}
*/
function encrypt(key, value) {
if (!/^[0-9a-f]{48}$/.test(key)) {
// key length is not matching for AES-192-CBC or key is no valid hex - fallback to old encryption
return encryptLegacy(key, value);
}
crypto = crypto || require('crypto');
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-192-cbc', Buffer.from(key, 'hex'), iv);
const encrypted = Buffer.concat([cipher.update(value), cipher.final()]);
return `$/aes-192-cbc:${iv.toString('hex')}:${encrypted.toString('hex')}`;
}
/**
* encrypts a value by a given key via AES-192-CBC
*
* @param {string} key - Secret key
* @param {string} value - value to decrypt
* @returns {string}
*/
function decrypt(key, value) {
// if not encrypted as aes-192 or key not a valid 48 digit hex -> fallback
if (!value.startsWith(`$/aes-192-cbc:`) || !/^[0-9a-f]{48}$/.test(key)) {
return decryptLegacy(key, value);
}
crypto = crypto || require('crypto');
const textParts = value.split(':');
const iv = Buffer.from(textParts[1], 'hex');
const encryptedText = Buffer.from(textParts.pop(), 'hex');
const decipher = crypto.createDecipheriv('aes-192-cbc', Buffer.from(key, 'hex'), iv);
const decrypted = Buffer.concat([decipher.update(encryptedText), decipher.final()]);
return decrypted.toString();
}
/**
* Tests whether the given variable is a real object and not an Array
* @param {any} it The variable to test
* @returns {it is Record<string, any>}
*/
function isObject(it) {
// This is necessary because:
// typeof null === 'object'
// typeof [] === 'object'
// [] instanceof Object === true
return Object.prototype.toString.call(it) === '[object Object]'; // this code is 25% faster then below one
// return it && typeof it === 'object' && !(it instanceof Array);
}
/**
* Tests whether the given variable is really an Array
* @param {any} it The variable to test
*/
function isArray(it) {
return Array.isArray(it); // from node 0.1 is a part of engine
}
/**
* Measure the Node.js event loop lag and repeatedly call the provided callback function with the updated results
* @param {number} ms The number of milliseconds for monitoring
* @param {function} cb Callback function to call for each new value
*/
function measureEventLoopLag(ms, cb) {
let start = hrtime();
let timeout = setTimeout(check, ms);
timeout.unref();
function check() {
// workaround for https://github.com/joyent/node/issues/8364
clearTimeout(timeout);
// how much time has actually elapsed in the loop beyond what
// setTimeout says is supposed to happen. we use setTimeout to
// cover multiple iterations of the event loop, getting a larger
// sample of what the process is working on.
const t = hrtime();
// we use Math.max to handle case where timers are running efficiently
// and our callback executes earlier than `ms` due to how timers are
// implemented. this is ok. it means we're healthy.
cb && cb(Math.max(0, t - start - ms));
start = t;
timeout = setTimeout(check, ms);
timeout.unref();
}
function hrtime() {
const t = process.hrtime();
return (t[0] * 1e3) + (t[1] / 1e6);
}
}
/**
* This function convert state values by read and write of aliases. Function is synchron.
*
* @param {object} sourceObj
* @param {object} targetObj
* @param {object} state Object with val, ack and so on
* @param {object} logger Logging object
* @param {string} logNamespace optional Logging namespace
*/
function formatAliasValue(sourceObj, targetObj, state, logger, logNamespace) {
logNamespace = logNamespace ? logNamespace + ' ' : '';
if (!state) {
return;
}
if (state.val === undefined) {
state.val = null;
return state;
}
if (targetObj && targetObj.alias && targetObj.alias.read) {
try {
// process the value here
const func = new Function('val', 'type', 'min', 'max', 'sType', 'sMin', 'sMax', 'return ' + targetObj.alias.read);
state.val = func(state.val, targetObj.type, targetObj.min, targetObj.max, sourceObj.type, sourceObj.min, sourceObj.max);
} catch (e) {
logger.error(`${logNamespace}Invalid read function for ${targetObj._id}: ${targetObj.alias.read} => ${e}`);
return null;
}
}
if (sourceObj && sourceObj.alias && sourceObj.alias.write) {
try {
// process the value here
const func = new Function('val', 'type', 'min', 'max', 'tType', 'tMin', 'tMax', 'return ' + sourceObj.alias.write);
state.val = func(state.val, sourceObj.type, sourceObj.min, sourceObj.max, targetObj.type, targetObj.min, targetObj.max);
} catch (e) {
logger.error(`${logNamespace}Invalid write function for ${sourceObj._id}: ${sourceObj.alias.write} => ${e}`);
return null;
}
}
if (targetObj && typeof state.val !== targetObj.type && state.val !== null) {
if (targetObj.type === 'boolean') {
const lowerVal = typeof state.val === 'string' ? state.val.toLowerCase() : state.val;
if (lowerVal === 'off' || lowerVal === 'aus' || state.val === '0') {
state.val = false;
} else {
// this also handles strings like "EIN" or such that will be true
state.val = !!state.val;
}
} else if (targetObj.type === 'number') {
state.val = parseFloat(state.val);
} else if (targetObj.type === 'string') {
state.val = state.val.toString();
}
}
// auto-scaling, only if val not null and unit for target (x)or source is %
if (((targetObj && targetObj.alias && !targetObj.alias.read) || (sourceObj && sourceObj.alias && !sourceObj.alias.write)) && state.val !== null) {
if (targetObj && targetObj.type === 'number' && targetObj.unit === '%' && sourceObj &&
sourceObj.type === 'number' && sourceObj.unit !== '%' && sourceObj.min !== undefined && sourceObj.max !== undefined) {
// scale target between 0 and 100 % based on sources min/max
state.val = (state.val - sourceObj.min) / (sourceObj.max - sourceObj.min) * 100;
} else if (sourceObj && sourceObj.type === 'number' && sourceObj.unit === '%' && targetObj &&
targetObj.unit !== '%' && targetObj.type === 'number' && targetObj.min !== undefined && targetObj.max !== undefined) {
// scale target based on its min/max by its source (assuming source is meant to be 0 - 100 %)
state.val = ((targetObj.max - targetObj.min) * state.val / 100) + targetObj.min;
}
}
return state;
}
/**
* remove given id from all enums
*
* @alias removeIdFromAllEnums
* @memberof tools
* @param {object} objects object to access objects db
* @param {string} id the object id which will be deleted from enums
* @returns {Promise}
*
*/
function removeIdFromAllEnums(objects, id) {
return new Promise((resolve, reject) => {
objects.getObjectView('system', 'enum', {startkey: '', endkey: '\u9999'}, (err, res) => {
if (err) {
reject(err);
} else {
const promises = [];
for (const obj of res.rows) {
const idx = obj.value && obj.value.common && obj.value.common.members ? obj.value.common.members.indexOf(id) : -1;
if (idx !== -1) {
// the id is in the enum now we have to remove it
obj.value.common.members.splice(idx, 1);
promises.push(new Promise(resolve => {
objects.setObject(obj.value._id, obj.value, err => {
err ? reject(err) : resolve();
});
}));
} // endIf
} // endFor
Promise.all(promises).then(resolve);
} // endElse
});
});
}
/**
* Parses dependencies to standardized object of form
*
* @alias parseDependencies
* @memberof tools
* @param {string[]|object[]|string} dependencies dependencies array or single dependency
* @returns {object} parsed dependencies
*/
function parseDependencies(dependencies) {
let adapters = {};
if (Array.isArray(dependencies)) {
dependencies.forEach(rule => {
if (typeof rule === 'string') {
// No version given, all are okay
adapters[rule] = '*';
} else if (isObject(rule)) {
// can be object containing single adapter or multiple
Object.keys(rule).filter(adapter => !adapters[adapter]).forEach(adapter => adapters[adapter] = rule[adapter]);
}
});
} else if (typeof dependencies === 'string') {
// its a single string without version requirement
adapters[dependencies] = '*';
} else if (isObject(dependencies)) {
// if dependencies is already an object, just use it
adapters = dependencies;
}
return adapters;
}
/**
* Validates types of obj.common properties and object.type, if invalid types are used, an error is thrown.
* If attributes of obj.common are not provided, no error is thrown. obj.type has to be there and has to be valid.
*
* @param {object} obj an object which will be validated
* @param {boolean} [extend] (optional) if true checks will allow more optional cases for extendObject calls
* @throws Error if a property has the wrong type or obj.type is non existing
*/
function validateGeneralObjectProperties(obj, extend) {
// designs have no type but have attribute views
if (obj && obj.type === undefined && obj.views !== undefined) {
return;
}
if (!obj || (obj.type === undefined && !extend)) {
throw new Error(`obj.type has to exist`);
}
if (obj.type !== undefined && typeof obj.type !== 'string') {
throw new Error(`obj.type has an invalid type! Expected "string", received "${typeof obj.type}"`);
}
const allowedObjectTypes = ['state', 'channel', 'device', 'enum', 'host', 'adapter', 'instance', 'meta', 'config', 'script', 'user', 'group', 'chart', 'folder'];
if (obj.type !== undefined && !allowedObjectTypes.includes(obj.type)) {
throw new Error(`obj.type has an invalid value (${obj.type}) but has to be one of ${allowedObjectTypes.join(', ')}`);
}
// obj.common is optional in general check
if (!obj.common) {
return;
}
if (obj.common.name !== undefined && typeof obj.common.name !== 'string' && typeof obj.common.name !== 'object') {
throw new Error(`obj.common.name has an invalid type! Expected "string" or "object", received "${typeof obj.common.name}"`);
}
if (obj.common.type !== undefined) {
if (typeof obj.common.type !== 'string') {
throw new Error(`obj.common.type has an invalid type! Expected "string", received "${typeof obj.common.type}"`);
}
// if object type indicates a state, check that common.type matches
const allowedStateTypes = ['number', 'string', 'boolean', 'array', 'object', 'mixed', 'file', 'json'];
if (obj.type === 'state' && !allowedStateTypes.includes(obj.common.type)) {
throw new Error(`obj.common.type has an invalid value (${obj.common.type}) but has to be one of ${allowedStateTypes.join(', ')}`);
}
}
if (obj.common.read !== undefined && typeof obj.common.read !== 'boolean') {
throw new Error(`obj.common.read has an invalid type! Expected "boolean", received "${typeof obj.common.read}"`);
}
if (obj.common.write !== undefined && typeof obj.common.write !== 'boolean') {
throw new Error(`obj.common.write has an invalid type! Expected "boolean", received "${typeof obj.common.write}"`);
}
if (obj.common.role !== undefined && typeof obj.common.role !== 'string') {
throw new Error(`obj.common.role has an invalid type! Expected "string", received "${typeof obj.common.role}"`);
}
if (obj.common.desc !== undefined && typeof obj.common.desc !== 'string' && typeof obj.common.desc !== 'object') {
throw new Error(`obj.common.desc has an invalid type! Expected "string" or "object", received "${typeof obj.common.desc}"`);
}
if (obj.type === 'state' && obj.common.custom !== undefined && obj.common.custom !== null && !isObject(obj.common.custom)) {
throw new Error(`obj.common.custom has an invalid type! Expected "object", received "${typeof obj.common.custom}"`);
}
}
/**
* get all instances of all adapters in the list
*
* @alias getAllInstances
* @memberof tools
* @param {string[]} adapters list of adapter names to get instances for
* @param {function} callback callback to be executed
*/
function getAllInstances(adapters, objects, callback) {
const instances = [];
let count = 0;
for (let k = 0; k < adapters.length; k++) {
if (!adapters[k]) {
continue;
}
if (adapters[k].indexOf('.') === -1) {
count++;
}
}
for (let i = 0; i < adapters.length; i++) {
if (!adapters[i]) {
continue;
}
if (adapters[i].indexOf('.') === -1) {
getInstances(adapters[i], objects, false, (err, inst) => {
for (let j = 0; j < inst.length; j++) {
if (instances.indexOf(inst[j]) === -1) {
instances.push(inst[j]);
}
}
if (!--count && callback) {
callback(null, instances);
callback = null;
}
});
} else {
if (instances.indexOf(adapters[i]) === -1) {
instances.push(adapters[i]);
}
}
}
if (!count && callback) {
callback(null, instances);
callback = null;
}
}
/**
* Promise-version of getAllInstances
*/
const getAllInstancesAsync = promisify(getAllInstances);
/**
* get all instances of one adapter
*
* @alias getInstances
* @param {string }adapter name of the adapter
* @param {object }objects objects DB
* @param {boolean} withObjects return objects instead of only ids
* @param {function} callback callback to be executed
*/
function getInstances(adapter, objects, withObjects, callback) {
if (typeof withObjects === 'function') {
callback = withObjects;
withObjects = false;
}
objects.getObjectList({startkey: 'system.adapter.' + adapter + '.', endkey: 'system.adapter.' + adapter + '.\u9999'}, (err, arr) => {
const instances = [];
if (!err && arr && arr.rows) {
for (let i = 0; i < arr.rows.length; i++) {
if (arr.rows[i].value.type !== 'instance') {
continue;
}
if (withObjects) {
instances.push(arr.rows[i].value);
} else {
instances.push(arr.rows[i].value._id);
}
}
}
callback(null, instances);
});
}
/**
* Checks if the given callback is a function and if so calls it with the given parameter immediately, else a resolved Promise is returned
*
* @param {(...args: any[]) => void | null | undefined} callback - callback function to be executed
* @param {any[]} args - as many arguments as needed, which will be returned by the callback function or by the Promise
* @returns {Promise<any>} - if Promise is resolved with multiple arguments, an array is returned
*/
function maybeCallback(callback, ...args) {
if (typeof callback === 'function') {
// if function we call it with given param
setImmediate(callback, ...args);
} else {
return Promise.resolve(args.length > 1 ? args : args[0]);
}
}
/**
* Checks if the given callback is a function and if so calls it with the given error and parameter immediately, else a resolved or rejected Promise is returned. Error ERROR_DB_CLOSED are not rejecting the promise
*
* @param {((error: Error | null | undefined, ...args: any[]) => void) | null | undefined} callback - callback function to be executed
* @param {Error | string | null | undefined} error - error which will be used by the callback function. If callback is not a function and
* error is given, a rejected Promise is returned. If error is given but it is not an instance of Error, it is converted into one.
* @param {any[]} args - as many arguments as needed, which will be returned by the callback function or by the Promise
* @returns {Promise<any>} - if Promise is resolved with multiple arguments, an array is returned
*/
function maybeCallbackWithError(callback, error, ...args) {
if (error !== undefined && error !== null && !(error instanceof Error)) {
// if it's not a real Error, we convert it into one
error = new Error(error);
}
const isDbError = error ? error.message === ERROR_DB_CLOSED : false;
if (typeof callback === 'function') {
setImmediate(callback, error, ...args);
} else if (error && !isDbError) {
return Promise.reject(error);
} else {
return Promise.resolve(args.length > 1 ? args : args[0]);
}
}
/**
* Executes a command asynchronously. On success, the promise resolves with stdout and stderr.
* On error, the promise rejects with the exit code or signal, as well as stdout and stderr.
* @param {string} command The command to execute
* @param {import("child_process").ExecOptions} [execOptions] The options for child_process.exec
* @returns {import("child_process").ChildProcess & Promise<{stdout?: string; stderr?: string}>}
*/
function execAsync(command, execOptions) {
const defaultOptions = {
// we do not want to show the node.js window on Windows
windowsHide: true,
// And we want to capture stdout/stderr
encoding: 'utf8'
};
// @ts-ignore We set the encoding, so stdout/stdrr must be a string
return cpPromise.exec(command, { ...defaultOptions, ...execOptions });
}
/**
* Takes input from one stream and writes it to another as soon as a complete line was read.
* @param {NodeJS.ReadableStream} input The stream to read from
* @param {NodeJS.WritableStream} output The stream to write into
*/
function pipeLinewise(input, output) {
const rl = createInterface({
input,
crlfDelay: Infinity
});
rl.on('line', line => output.write(line + os.EOL));
}
/**
* Find the adapter main file as full path
*
* @memberof tools
* @param {string} adapter - adapter name of the adapter, e.g. hm-rpc
* @returns {Promise<string>}
*/
async function resolveAdapterMainFile(adapter) {
const adapterDir = getAdapterDir(adapter);
if (!adapterDir) {
throw new Error(`Could not find adapter dir of ${adapter}`);
}
const possibleMainFiles = [
'main.js',
`${adapter}.js`
];
// Add package.json -> main as the 2nd choice
try {
const pack = JSON.parse(
await fs.readFile(
path.join(adapterDir, 'package.json'),
'utf8'
)
);
if (pack && typeof pack.main === 'string') {
possibleMainFiles.unshift(pack.main);
}
} catch {
// Ignore, we have fallback solutions
}
// Add io-package.json -> common.main as the preferred choice
try {
const ioPack = JSON.parse(
await fs.readFile(
path.join(adapterDir, 'io-package.json'),
'utf8'
)
);
if (ioPack && ioPack.common && typeof ioPack.common.main === 'string') {
possibleMainFiles.unshift(ioPack.common.main);
}
} catch {
// Ignore, we have fallback solutions
}
// Try all possible main files
for (const mainFile of possibleMainFiles) {
const fullFileName = path.join(adapterDir, mainFile);
if (await fs.pathExists(fullFileName)) {
return fullFileName;
}
}
throw new Error(`Could not find main file of ${adapter}`);
}
/**
* Returns the default nodeArgs required to execute the main file, e.g. transpile hooks for TypeScript
* @param {string} mainFile
* @returns {string[]}
*/
function getDefaultNodeArgs(mainFile) {
if (mainFile.endsWith('.ts')) {
return ['-r', '@alcalzone/esbuild-register'];
}
return [];
}
/** This is used for the short github URL format that NPM accepts (<githubname>/<githubrepo>[#<commit-ish>]) */
const shortGithubUrlRegex = /^(?<user>[^/]+)\/(?<repo>[^#]+)(?:#(?<commit>.+))?$/;
/**
* Tests if the given URL matches the format <githubname>/<githubrepo>[#<commit-ish>]
* @param {string} url The URL to parse
*/
function isShortGithubUrl(url) {
return shortGithubUrlRegex.test(url);
}
/**
* Tries to parse an URL in the format <githubname>/<githubrepo>[#<commit-ish>] into its separate parts
* @param {string} url The URL to parse
* @returns {{user: string; repo: string; commit?: string} | null}
*/
function parseShortGithubUrl(url) {
const match = shortGithubUrlRegex.exec(url);
if (!match || !match.groups) {
return null;
}
return {
user: match.groups.user,
repo: match.groups.repo,
commit: match.groups.commit
};
}
/** This is used to parse the pathname of a github URL */
const githubPathnameRegex = /^\/(?<user>[^/]+)\/(?<repo>[^/]*?)(?:\.git)?(?:\/(?:tree|tarball|archive)\/(?<commit>.*?)(?:\.(?:zip|gz|tar\.gz))?)?$/;
/**
* Tests if the given pathname matches the format /<githubname>/<githubrepo>[.git][/<tarball|tree|archive>/<commit-ish>[.zip|.gz]]
* @param {string} pathname The pathname part of a Github URL
*/
function isGithubPathname(pathname) {
return githubPathnameRegex.test(pathname);
}
/**
* Tries to a github pathname format /<githubname>/<githubrepo>[.git][/<tarball|tree|archive>/<commit-ish>[.zip|.gz|.tar.gz]] into its separate parts
* @param {string} pathname The pathname part of a Github URL
* @returns {{user: string; repo: string; commit: string} | null}
*/
function parseGithubPathname(pathname) {
const match = githubPathnameRegex.exec(pathname);
if (!match || !match.groups) {
return null;
}
return {
user: match.groups.user,
repo: match.groups.repo,
commit: match.groups.commit
};
}
/**
* Removes properties which are given by preserve
* @param {object} preserve - object which has true entries (or array of selected attributes) for all attributes which should be removed from currObj
* @param {object} oldObj - old object
* @param {object} newObj - new object
*/
function removePreservedProperties(preserve, oldObj, newObj) {
for (const prop of Object.keys(preserve)) {
if (isObject(preserve[prop]) && isObject(newObj[prop])) {
// we have to go one step deeper
removePreservedProperties(preserve[prop], oldObj[prop], newObj[prop]);
} else if (newObj && newObj[prop] !== undefined && (oldObj && oldObj[prop] !== undefined)) {
// we only need to remove something if its in the old object and in the new one
if (typeof preserve[prop] === 'boolean') {
delete newObj[prop];
} else if (Array.isArray(preserve[prop])) {
// array, only rm selected subattributes instead of whole attribute
for (const rmProp of preserve[prop]) {
if (oldObj[prop][rmProp] !== undefined && newObj[prop][rmProp] !== undefined) {
// only delete if conflicting
delete newObj[prop][rmProp];
}
}
}
}
}
}
/**
* Returns the array of system.adapter.<namespace>.* objects which are created for every instance
*
* @param {string} namespace - adapter namespace + id, e.g. hm-rpc.0
* @param {boolean} createWakeup - indicator to create wakeup object too
* @returns {object[]}
*/
function getInstanceIndicatorObjects(namespace, createWakeup) {
const id = `system.adapter.${namespace}`;
const objs = [
{
_id: `${id}.alive`,
type: 'state',
common: {
name: `${namespace} alive`,
type: 'boolean',
read: true,
write: true,
role: 'indicator.state'
},
native: {}
},
{
_id: `${id}.connected`,
type: 'state',
common: {
name: `${namespace} is connected`,
type: 'boolean',
read: true,
write: false,
role: 'indicator.state'
},
native: {}
},
{
_id: `${id}.compactMode`,
type: 'state',
common: {
name: `${namespace}.compactMode`,
type: 'boolean',
read: true,
write: false,
role: 'indicator.state'
},
native: {}
},
{
_id: `${id}.cpu`,
type: 'state',
common: {
name: `${namespace}.cpu`,
type: 'number',
read: true,
write: false,
role: 'indicator.state',
unit: '% of one core'
},
native: {}
}, {
_id: `${id}.cputime`,
type: 'state',
common: {
name: namespace + '.cputime',
type: 'number',
read: true,
write: false,
role: 'indicator.state',
unit: 'seconds'
},
native: {}
},
{
_id: `${id}.memHeapUsed`,
type: 'state',
common: {
name: `${namespace} heap actually Used`,
type: 'number',
read: true,
write: false,
role: 'indicator.state',
unit: 'MB'
},
native: {}
},
{
_id: `${id}.memHeapTotal`,
type: 'state',
common: {
name: `${namespace} total Size of the Heap`,
read: true,
write: false,
type: 'number',
role: 'indicator.state',
unit: 'MB'
},
native: {}
},
{
_id: `${id}.memRss`,
type: 'state',
common: {
name: `${namespace} resident Set Size`,
desc: 'Resident set size',
read: true,
write: false,
type: 'number',
role: 'indicator.state',
unit: 'MB'
},
native: {}
},
{
_id: `${id}.uptime`,
type: 'state',
common: {
name: `${namespace} uptime`,
type: 'number',
read: true,
write: false,
role: 'indicator.state',
unit: 'seconds'
},
native: {}
},
{
_id: `${id}.inputCount`,
type: 'state',
common: {
name: `${namespace} events input counter`,
desc: 'State\'s inputs in 15 seconds',
type: 'number',
read: true,
write: false,
role: 'state',
unit: 'events/15 seconds'
},
native: {}
},
{
_id: `${id}.outputCount`,
type: 'state',
common: {
name: `${namespace} events output counter`,
desc: 'State\'s outputs in 15 seconds',
type: 'number',
read: true,
write: false,
role: 'state',
unit: 'events/15 seconds'
},
native: {}
},
{
_id: `${id}.eventLoopLag`,
type: 'state',
common: {
name: `${namespace} Node.js event loop lag`,
desc: 'Node.js event loop lag in ms averaged over 15 seconds',
type: 'number',
read: true,
write: false,
role: 'state',
unit: 'ms'
},
native: {}
},
{
_id: `${id}.sigKill`,
type: 'state',
common: {
name: `${namespace} kill signal`,
type: 'number',
read: true,
write: false,
desc: 'Process id that must survive. All other IDs must terminate itself',
role: 'state'
},
native: {}
},
{
_id: `${id}.logLevel`,
type: 'state',
common: {
name: `${namespace} loglevel`,
type: 'string',
read: true,
write: true,
desc: 'Loglevel of the adapter. Will be set on start with defined value but can be overridden during runtime',
role: 'state'
},
native: {}
}
];
if (createWakeup) {
objs.push({
_id: `${id}.wakeup`,
type: 'state',
common: {
name: `${namespace}.wakeup`,
read: true,
write: true,
type: 'boolean',
role: 'adapter.wakeup'
},
native: {}
});
}
return objs;
}
function getLogger(log) {
if (!log) {
log = {
silly: function (_msg) {/*console.log(msg);*/},
debug: function (_msg) {/*console.log(msg);*/},
info: function (_msg) {/*console.log(msg);*/},
warn: function (msg) {
console.log(msg);
},
error: function (msg) {
console.log(msg);
}
};
} else if (!log.silly) {
log.silly = log.debug;
}
return log;
}
/**
* Allows to find out if a given objects dbType offers a server or not
* @param dbType {string} database type
* @returns {boolean} true if a server class is available
*/
function objectsDbHasServer(dbType) {
try {
const path = require.resolve(` /db-objects-${dbType}`);
return !!require(path).Server;
} catch {
throw new Error(`Installation error or unknown objects database type: ${dbType}`);
}
}
/**
* Allows to find out if a given objects dbType offers a server which runs on this host and listens (locally or globally/by IP)
* @param dbType {string} database type
* @param host {string} configured db host
* @param [checkIfLocalOnly=false] {boolean} optional if try the method checks if the server listens to local connections only; else also external connection options are checked
* @returns {boolean} true if a server listens on this host (locally or globally/by IP)
*/
function isLocalObjectsDbServer(dbType, host, checkIfLocalOnly) {
const ownIps = findIPs();
if (!objectsDbHasServer(dbType)) {
return false; // if no server it can not be a local server
}
let result = host === 'localhost' || host === '127.0.0.1'; // reachable locally only
if (!checkIfLocalOnly) {
result = result || host === '0.0.0.0' || ownIps.includes(host);
}
return result;
}
/**
* Allows to find out if a given states dbType offers a server or not
* @param dbType {string} database type
* @returns {boolean} true if a server class is available
*/
function statesDbHasServer(dbType) {
try {
const path = require.resolve(` /db-states-${dbType}`);
return !!require(path).Server;
} catch {
throw new Error(`Installation error or unknown states database type: ${dbType}`);
}
}
/**
* Allows to find out if a given states dbType offers a server which runs on this host and listens (locally or globally/by IP)
* @param dbType {string} database type
* @param host {string} configured db host
* @param [checkIfLocalOnly=false] {boolean} if try the method checks if the server listens to local connections only; else also external connection options are checked
* @returns {boolean} true if a server listens on this host (locally or globally/by IP)
*/
function isLocalStatesDbServer(dbType, host, checkIfLocalOnly) {
const ownIps = findIPs();
if (!statesDbHasServer(dbType)) {
return false; // if no server it can not be a local server
}
let result = host === 'localhost' || host === '127.0.0.1'; // reachable locally only
if (!checkIfLocalOnly) {
result = result || host === '0.0.0.0' || ownIps.includes(host);
}
return result;
}
/**
* Get ordered instances according to tier level
*
* @param {object} objects - Objects DB
* @param {object} logger - logger object
* @param {string} [logPrefix] - prefix for logging
* @return {Promise<object[]>}
*/
async function getInstancesOrderedByStartPrio(objects, logger, logPrefix='') {
const instances = {'1': [], '2': [], '3': [], 'admin': []};
const allowedTiers = [1, 2, 3];
if (logPrefix) {
// append space if we have a prefix
logPrefix += ' ';
}
let doc = {};
try {
doc = await objects.getObjectViewAsync('system', 'instance');
} catch (e) {
if (e.message.startsWith('Cannot find ')) {
logger.error(`${logPrefix}_design/system missing - call node ${getAppName()}.js setup`);
} else {
logger.error(`${logPrefix}Can not get instances: ${e.message}`);
}
}
if (!doc.rows || doc.rows.length === 0) {
logger.info(`${logPrefix}no instances found`);
}
for (const row of doc.rows) {
if (row && row.value) {
if (row.value._id.startsWith('system.adapter.admin')) {
instances.admin.push(row.value);
} else if (row.value.common && allowedTiers.includes(parseInt(row.value.common.tier))) {
instances[row.value.common.tier].push(row.value);
} else {
// no valid tier so put it in the last one
instances['3'].push(row.value);
}
}
}
return [...instances.admin, ...instances['1'], ...instances['2'], ...instances['3']];
}
const ERROR_NOT_FOUND = 'Not exists';
const ERROR_EMPTY_OBJECT = 'null object';
const ERROR_NO_OBJECT = 'no object';
const ERROR_DB_CLOSED = 'DB closed';
module.exports = {
appName: getAppName(),
createUuid,
decryptPhrase,
execAsync,
findIPs,
generateDefaultCertificates,
getAdapterDir,
getInstances,
getAllInstances,
getAllInstancesAsync,
getCertificateInfo,
getConfigFileName,
getDefaultDataDir,
getDefaultNodeArgs,
getFile,
getHostInfo,
getHostName,
getInstalledInfo,
getInstanceIndicatorObjects,
getIoPack,
getJson,
getInstancesOrderedByStartPrio,
getRepositoryFile,
getSystemNpmVersion,
isObject,
isArray,
maybeCallback,
maybeCallbackWithError,
promisify,
promisifyNoError,
promiseSequence,
removeIdFromAllEnums,
resolveAdapterMainFile,
rmdirRecursiveSync,
parseDependencies,
sendDiagInfo,
upToDate,
validateGeneralObjectProperties,
checkNonEditable,
copyAttributes,
getDiskInfo,
wrapES6Class,
setQualityForInstance,
appendStackTrace,
captureStackTrace,
pattern2RegEx,
encrypt,
decrypt,
measureEventLoopLag,
formatAliasValue,
pipeLinewise,
isShortGithubUrl,
parseShortGithubUrl,
isGithubPathname,
parseGithubPathname,
removePreservedProperties,
FORBIDDEN_CHARS,
getLogger,
objectsDbHasServer,
isLocalObjectsDbServer,
statesDbHasServer,
isLocalStatesDbServer,
ERRORS: {
ERROR_NOT_FOUND: ERROR_NOT_FOUND,
ERROR_EMPTY_OBJECT: ERROR_EMPTY_OBJECT,
ERROR_NO_OBJECT: ERROR_NO_OBJECT,
ERROR_DB_CLOSED: ERROR_DB_CLOSED
}
};