iobroker.js-controller
Version:
Updated by reinstall.js on 2018-06-11T15:19:56.688Z
1,140 lines (1,054 loc) • 214 kB
JavaScript
/**
* application.controller
*
* Controls Adapter-Processes
*
* Copyright 2013-2020 bluefox <dogafox@gmail.com>,
* 2013-2014 hobbyquaker <hq@ccu.io>
* MIT License
*
*/
'use strict';
const schedule = require('node-schedule');
const os = require('os');
const fs = require('fs-extra');
const path = require('path');
const cp = require('child_process');
const ioPackage = require('./io-package.json');
const tools = require('./lib/tools');
const version = ioPackage.common.version;
const pidUsage = require('pidusage');
const deepClone = require('deep-clone');
const { isDeepStrictEqual } = require('util');
const EXIT_CODES = require('./lib/exitCodes');
const { PluginHandler } = require('@iobroker/plugin-base');
const NotificationHandler = require('./lib/notificationHandler');
let pluginHandler;
let notificationHandler;
const exec = cp.exec;
const spawn = cp.spawn;
let zipFiles;
let upload; // will be used only once by upload of adapter
/* Use require('loadavg-windows') to enjoy os.loadavg() on Windows OS.
Currently Node.js on Windows platform do not implements os.loadavg() functionality - it returns [0,0,0]
Expect first results after 1 min from application start (before 1 min runtime it will return [0,0,0])
Requiring it on other operating systems have NO influence.*/
if (os.platform() === 'win32') {
require('loadavg-windows');
}
let title = tools.appName + '.js-controller';
let Objects;
let States;
let decache;
const semver = require('semver');
let logger;
let isDaemon = false;
let callbackId = 1;
let callbacks = {};
const hostname = tools.getHostName();
let hostObjectPrefix = 'system.host.' + hostname;
let hostLogPrefix = 'host.' + hostname;
const compactGroupObjectPrefix = '.compactgroup';
const logList = [];
let detectIpsCount = 0;
let objectsDisconnectTimeout= null;
let statesDisconnectTimeout = null;
let connected = null; // not false, because want to detect first connection
let lastDiskSizeCheck = 0;
let restartTimeout = null;
let connectTimeout = null;
let reportInterval = null;
const procs = {};
const hostAdapter = {};
const subscribe = {};
const stopTimeouts = {};
let states = null;
let objects = null;
let storeTimer = null;
let mhTimer = null;
let isStopping = null;
let allInstancesStopped = true;
let stopTimeout = 10000;
let uncaughtExceptionCount = 0;
const installQueue = [];
let started = false;
let inputCount = 0;
let outputCount = 0;
let eventLoopLags = [];
let mhService = null; // multihost service
const uptimeStart = Date.now();
let compactGroupController = false;
let compactGroup = null;
const compactProcs = {};
const scheduledInstances = {};
const VENDOR_BOOTSTRAP_FILE = '/opt/iobroker/iob-vendor-secret.json';
const VENDOR_FILE = '/etc/iob-vendor.json';
let updateIPsTimer = null;
const uploadTasks = [];
const config = getConfig();
function getErrorText(code) {
const texts = Object.keys(EXIT_CODES);
for (let i = 0; i < texts.length; i++) {
if (EXIT_CODES[texts[i]] === code) {
return texts[i];
}
}
return code;
}
/**
* Get the config directly from fs - never cached
*
* @returns {null|object}
*/
function getConfig() {
const configFile = tools.getConfigFileName();
if (!fs.existsSync(configFile)) {
if (process.argv.indexOf('start') !== -1) {
isDaemon = true;
logger = require('./lib/logger')('info', [tools.appName], true);
} else {
logger = require('./lib/logger')('info', [tools.appName]);
}
logger.error(`${hostLogPrefix} conf/${tools.appName}.json missing - call node ${tools.appName}.js setup`);
process.exit(EXIT_CODES.MISSING_CONFIG_JSON);
return null;
} else {
const _config = fs.readJSONSync(configFile);
if (!_config.states) {
_config.states = {type: 'file'};
}
if (!_config.objects) {
_config.objects = {type: 'file'};
}
if (!_config.system) {
_config.system = {};
}
return _config;
}
}
function _startMultihost(_config, secret) {
const MHService = require('./lib/multihostServer.js');
const cpus = os.cpus();
mhService = new MHService(hostname, logger, _config, {
node: process.version,
arch: os.arch(),
model: cpus && cpus[0] && cpus[0].model ? cpus[0].model : 'unknown',
cpus: cpus ? cpus.length : 1,
mem: os.totalmem(),
ostype: os.type()
}, tools.findIPs(), secret);
}
/**
* Starts or stops the multihost discovery server, depending on the config and temp information
*
* @param {object} __config - the iobroker config object
* @returns {boolean|void}
*/
function startMultihost(__config) {
if (compactGroupController) {
return;
}
if (mhTimer) {
clearTimeout(mhTimer);
mhTimer = null;
}
const _config = __config || getConfig();
if ((_config.multihostService && _config.multihostService.enabled)) {
if (mhService) {
try {
mhService.close(() => {
mhService = null;
setImmediate(() => startMultihost(_config));
});
return;
} catch (e) {
logger.warn(`${hostLogPrefix} Cannot stop multihost discovery server: ${e}`);
}
}
if (!_config.objects.host || tools.isLocalObjectsDbServer(_config.objects.type, _config.objects.host, true)) {
logger.warn(`${hostLogPrefix} Multihost Master on this system is not possible, because IP address for objects is ${_config.objects.host}. Please allow remote connections to the server by adjusting the IP.`);
return false;
} else if (!_config.states.host || tools.isLocalObjectsDbServer(_config.states.type, _config.states.host, true)) {
logger.warn(`${hostLogPrefix} Multihost Master on this system is not possible, because IP address for states is ${_config.states.host}. Please allow remote connections to the server by adjusting the IP.`);
return false;
}
if (_config.multihostService.secure) {
if (typeof _config.multihostService.password === 'string' && _config.multihostService.password.length) {
objects.getObject('system.config', (err, obj) => {
if (obj && obj.native && obj.native.secret) {
if (!_config.multihostService.password.startsWith(`$/aes-192-cbc:`)) {
// if old encryption was used, we need to decrypt in old fashion
tools.decryptPhrase(obj.native.secret, _config.multihostService.password, secret =>
_startMultihost(_config, secret));
} else {
const secret = tools.decrypt(obj.native.secret, _config.multihostService.password);
_startMultihost(_config, secret);
}
} else {
logger.error(`${hostLogPrefix} Cannot start multihost discovery server: no system.config found (err:${err})`);
}
});
} else {
logger.error(`${hostLogPrefix} Cannot start multihost discovery server: secure mode was configured, but no secret was set. Please check the configuration!`);
}
} else {
_startMultihost(_config, false);
}
if (!_config.multihostService.persist) {
mhTimer = setTimeout(async () => {
if (mhService) {
try {
mhService.close();
mhService = null;
logger.info(`${hostLogPrefix} Multihost discovery server stopped after 15 minutes, because only temporarily activated`);
_config.multihostService.persist = false;
_config.multihostService.enabled = false;
const configFile = tools.getConfigFileName();
await fs.writeFile(configFile, JSON.stringify(_config, null, 2));
} catch (e) {
logger.warn(`${hostLogPrefix} Cannot stop multihost discovery: ${e}`);
}
}
mhTimer = null;
}, 15 * 60000);
}
return true;
} else if (mhService) {
try {
mhService.close();
mhService = null;
} catch (e) {
logger.warn(`${hostLogPrefix} Cannot stop multihost discovery: ${e}`);
}
return false;
}
}
/**
* Starts cyclic update of IP interfaces.
* At start every 30 seconds and after 5 minutes, every hour.
* Because DHCP could change the IPs.
*/
function startUpdateIPs() {
if (!updateIPsTimer) {
updateIPsTimer = setInterval(() => {
if (Date.now() - uptimeStart > 5 * 60000) {// 5 minutes at start check every 30 seconds because of DHCP
clearInterval(updateIPsTimer);
updateIPsTimer = setInterval(() => setIPs(), 3600000); // update IPs every hour
}
setIPs();
}, 30000);
}
}
// subscribe or unsubscribe loggers
function logRedirect(isActive, id, reason) {
console.log(`================================== > LOG REDIRECT ${id} => ${isActive} [${reason}]`);
if (isActive) {
if (logList.indexOf(id) === -1) {
logList.push(id);
}
} else {
const pos = logList.indexOf(id);
if (pos !== -1) {
logList.splice(pos, 1);
}
}
}
function handleDisconnect() {
if (!connected || restartTimeout || isStopping) {
return;
}
if (statesDisconnectTimeout) {
clearTimeout(statesDisconnectTimeout);
statesDisconnectTimeout = null;
}
if (objectsDisconnectTimeout) {
clearTimeout(objectsDisconnectTimeout);
objectsDisconnectTimeout = null;
}
connected = false;
logger.warn(hostLogPrefix + ' Slave controller detected disconnection. Stop all instances.');
if (compactGroupController) {
stop(true);
} else {
stop(true, () => {
restartTimeout = setTimeout(() => {
processMessage({command: 'cmdExec', message: {data: '_restart'}});
setTimeout(() => process.exit(EXIT_CODES.JS_CONTROLLER_STOPPED), 1000);
}, 10000);
});
}
}
function createStates(onConnect) {
states = new States({
namespace: hostLogPrefix,
connection: config.states,
logger: logger,
hostname: hostname,
change: (id, state) => {
inputCount++;
if (!id) {
return logger.error(hostLogPrefix + ' change event with no ID: ' + JSON.stringify(state));
}
// If some log transporter activated or deactivated
if (id.match(/.logging$/)) {
logRedirect(state ? state.val : false, id.substring(0, id.length - '.logging'.length), id);
} else
// If this is messagebox, only the main controller is handling the host messages
if (!compactGroupController && id === 'messagebox.' + hostObjectPrefix) {
const obj = state;
if (obj) {
// If callback stored for this request
if (obj.callback &&
obj.callback.ack &&
obj.callback.id &&
callbacks &&
callbacks['_' + obj.callback.id]) {
// Call callback function
if (callbacks['_' + obj.callback.id].cb) {
callbacks['_' + obj.callback.id].cb(obj.message);
delete callbacks['_' + obj.callback.id];
}
// delete too old callbacks IDs
const now = Date.now();
for (const _id of Object.keys(callbacks)) {
if (now - callbacks[_id].time > 3600000) {
delete callbacks[_id];
}
}
} else {
processMessage(obj);
}
}
} else
// If this NAME.0.info.connection, only main controller is handling this
if (!compactGroupController && id.match(/^[^.]+\.\d+\.info\.connection$/)) {
// Disabled in 1.5.x
// if (state && !state.val) {
// tools.setQualityForInstance(objects, states, id.substring(0, id.length - /* '.info.connection'.length*/ 16), 0x42)
// .then(() => {
// logger.debug(hostLogPrefix + ' set all states quality to 0x42 (device not connected');
// }).catch(e => {
// logger.error(hostLogPrefix + ' cannot set all states quality: ' + e);
// });
// }
} else
// If this system.adapter.NAME.0.alive, only main controller is handling this
if (!compactGroupController && id.match(/^system.adapter.[^.]+\.\d+\.alive$/)) {
if (state && !state.ack) {
const enabled = state.val;
setImmediate(() => {
objects.getObject(id.substring(0, id.length - 6/*'.alive'.length*/), (err, obj) => {
if (err) {
logger.error(hostLogPrefix + ' Cannot read object: ' + err);
}
if (obj && obj.common) {
// IF adapter enabled => disable it
if ((obj.common.enabled && !enabled) || (!obj.common.enabled && enabled)) {
obj.common.enabled = !!enabled;
logger.info(hostLogPrefix + ' instance "' + obj._id + '" ' + (obj.common.enabled ? 'enabled' : 'disabled') + ' via .alive');
setImmediate(() => {
obj.from = hostObjectPrefix;
obj.ts = Date.now();
objects.setObject(obj._id, obj);
});
}
}
});
});
} else if (state && state.ack && !state.val) {
// Disabled in 1.5.x
// id = id.substring(0, id.length - /*.alive*/ 6);
// if (procs[id] && procs[id].config.common.host === hostname && procs[id].config.common.mode === 'daemon') {
// tools.setQualityForInstance(objects, states, id.substring(15 /*'system.adapter.'.length*/), 0x12)
// .then(() => {
// logger.debug(hostLogPrefix + ' set all states quality to 0x12 (instance not connected');
// }).catch(e => {
// logger.error(hostLogPrefix + ' cannot set all states quality: ' + e);
// });
// }
}
} else
if (subscribe[id]) {
for (let i = 0; i < subscribe[id].length; i++) {
// wake up adapter
if (procs[subscribe[id][i]]) {
console.log('Wake up ' + id + ' ' + JSON.stringify(state));
startInstance(subscribe[id][i], true);
} else {
logger.warn(hostLogPrefix + ' controller Adapter subscribed on ' + id + ' does not exist!');
}
}
} else
if (id === hostObjectPrefix + '.logLevel') {
if (! config || !config.log || !state || state.ack) {
return;
}
let currentLevel = config.log.level;
if (state.val && state.val !== currentLevel && ['silly','debug', 'info', 'warn', 'error'].includes(state.val)) {
config.log.level = state.val;
for (const transport of Object.keys(logger.transports)) {
if (logger.transports[transport].level === currentLevel) {
logger.transports[transport].level = state.val;
}
}
logger.info(hostLogPrefix + ' Loglevel changed from "' + currentLevel + '" to "' + state.val + '"');
currentLevel = state.val;
} else if (state.val && state.val !== currentLevel) {
logger.info(hostLogPrefix + ' Got invalid loglevel "' + state.val + '", ignoring');
}
states.setState(hostObjectPrefix + '.logLevel', {val: currentLevel, ack: true, from: hostObjectPrefix});
} else
if (id.startsWith(hostObjectPrefix + '.plugins.') && id.endsWith('.enabled')) {
if (!config || !config.log || !state || state.ack) {
return;
}
const pluginStatesIndex = (hostObjectPrefix + '.plugins.').length;
let nameEndIndex = id.indexOf('.', pluginStatesIndex + 1);
if (nameEndIndex === -1) {
nameEndIndex = undefined;
}
const pluginName = id.substring(pluginStatesIndex, nameEndIndex);
if (!pluginHandler.pluginExists(pluginName)) {
return;
}
if (pluginHandler.isPluginActive(pluginName) !== state.val) {
if (state.val) {
if (!pluginHandler.isPluginInstanciated(pluginName)) {
pluginHandler.instanciatePlugin(pluginName, pluginHandler.getPluginConfig(pluginName), __dirname);
pluginHandler.setDatabaseForPlugin(pluginName, objects, states);
pluginHandler.initPlugin(pluginName, ioPackage);
}
} else {
if (!pluginHandler.destroy(pluginName)) {
logger.info(`${hostLogPrefix} Plugin ${pluginName} could not be disabled. Please restart ioBroker to disable it.`);
}
}
}
}
/* it is not used because of code before
else
// Monitor activity of the adapter and restart it if stopped
if (!isStopping && id.substring(id.length - '.alive'.length) === '.alive') {
let adapter = id.substring(0, id.length - '.alive'.length);
if (procs[adapter] &&
!procs[adapter].stopping &&
!procs[adapter].process &&
procs[adapter].config &&
procs[adapter].config.common.enabled &&
procs[adapter].config.common.mode === 'daemon') {
startInstance(adapter, false);
}
}
*/
},
connected: () => {
if (statesDisconnectTimeout) {
clearTimeout(statesDisconnectTimeout);
statesDisconnectTimeout = null;
}
// logs and cleanups are only handled by the main controller process
if (!compactGroupController) {
states.clearAllLogs && states.clearAllLogs();
deleteAllZipPackages();
}
initMessageQueue();
startAliveInterval();
initializeController();
onConnect && onConnect();
},
disconnected: (/*error*/) => {
if (restartTimeout) {
return;
}
statesDisconnectTimeout && clearTimeout(statesDisconnectTimeout);
statesDisconnectTimeout = setTimeout(() => {
statesDisconnectTimeout = null;
handleDisconnect();
}, (config.states.connectTimeout || 2000) + (!compactGroupController ? 500 : 0));
}
});
return true;
}
async function initializeController() {
if (!states || !objects || connected) {
return;
}
logger.info(`${hostLogPrefix} connected to Objects and States`);
// initialize notificationHandler
const notificationSettings = {
states: states,
objects: objects,
log: logger,
logPrefix: hostLogPrefix,
host: hostname
};
notificationHandler = new NotificationHandler(notificationSettings);
if (ioPackage.notifications) {
try {
await notificationHandler.addConfig(ioPackage.notifications);
logger.info(`${hostLogPrefix} added notifications configuration of host`);
// load setup of all adapters to class, to remember messages even of non-running hosts
await notificationHandler.getSetupOfAllAdaptersFromHost();
} catch (e) {
logger.error(`${hostLogPrefix} Could not add notifications config of this host: ${e.message}`);
}
}
if (connected === null) {
connected = true;
if (!isStopping) {
pluginHandler.setDatabaseForPlugins(objects, states);
pluginHandler.initPlugins(ioPackage, () => {
states.subscribe(hostObjectPrefix + '.plugins.*');
// Do not start if we still stopping the instances
checkHost(() => {
startMultihost(config);
setMeta();
started = true;
getInstances();
});
});
}
} else {
connected = true;
started = true;
// Do not start if we still stopping the instances
if (!isStopping) {
getInstances();
}
}
}
// create "objects" object
function createObjects(onConnect) {
objects = new Objects({
namespace: hostLogPrefix,
connection: config.objects,
controller: true,
logger: logger,
hostname: hostname,
connected: () => {
// stop disconnect timeout
if (objectsDisconnectTimeout) {
clearTimeout(objectsDisconnectTimeout);
objectsDisconnectTimeout = null;
}
initializeController();
onConnect && onConnect();
},
disconnected: (/*error*/) => {
if (restartTimeout) {
return;
}
objectsDisconnectTimeout && clearTimeout(objectsDisconnectTimeout);
objectsDisconnectTimeout = setTimeout(() => {
objectsDisconnectTimeout = null;
handleDisconnect();
}, (config.objects.connectTimeout || 2000) + (!compactGroupController ? 500 : 0));
// give main controller a bit longer, so that adapter and compact processes can exit before
},
change: async (id, obj) => {
if (!started || !id.match(/^system\.adapter\.[a-zA-Z0-9-_]+\.[0-9]+$/)) {
return;
}
try {
logger.debug(hostLogPrefix + ' object change ' + id + ' (from: ' + (obj ? obj.from : null) + ')');
// known adapter
if (procs[id]) {
// if adapter deleted
if (!obj) {
// deleted: also remove from instance list of compactGroup
if (!compactGroupController && procs[id].config.common.compactGroup && compactProcs[procs[id].config.common.compactGroup] && compactProcs[procs[id].config.common.compactGroup].instances && compactProcs[procs[id].config.common.compactGroup].instances.includes(id)) {
compactProcs[procs[id].config.common.compactGroup].instances.splice(compactProcs[procs[id].config.common.compactGroup].instances.indexOf(id), 1);
}
// instance removed -> remove all notifications
await notificationHandler.clearNotifications(null, null, id);
procs[id].config.common.enabled = false;
procs[id].config.common.host = null;
procs[id].config.deleted = true;
delete hostAdapter[id];
logger.info(hostLogPrefix + ' object deleted ' + id);
} else {
if (procs[id].config.common.enabled && !obj.common.enabled) {
logger.info(hostLogPrefix + ' "' + id + '" disabled');
}
if (!procs[id].config.common.enabled && obj.common.enabled) {
logger.info(hostLogPrefix + ' "' + id + '" enabled');
procs[id].downloadRetry = 0;
}
// Check if compactgroup or compact mode changed
if (!compactGroupController &&
procs[id].config.common.compactGroup &&
(procs[id].config.common.compactGroup !== obj.common.compactGroup || procs[id].config.common.runAsCompactMode !== obj.common.runAsCompactMode) &&
compactProcs[procs[id].config.common.compactGroup] &&
compactProcs[procs[id].config.common.compactGroup].instances &&
compactProcs[procs[id].config.common.compactGroup].instances.includes(id)
) {
compactProcs[procs[id].config.common.compactGroup].instances.splice(compactProcs[procs[id].config.common.compactGroup].instances.indexOf(id), 1);
}
procs[id].config = obj;
hostAdapter[id] = hostAdapter[id] || {};
hostAdapter[id].config = obj;
}
if (procs[id].process || procs[id].config.common.mode === 'schedule' || procs[id].config.common.mode === 'subscribe') {
procs[id].restartExpected = true;
stopInstance(id, async () => {
const _ipArr = tools.findIPs();
if (checkAndAddInstance(procs[id].config, _ipArr)) {
if (procs[id].config.common.enabled && (procs[id].config.common.mode !== 'extension' || !procs[id].config.native.webInstance)) {
if (procs[id].restartTimer) {
clearTimeout(procs[id].restartTimer);
}
const restartTimeout = (procs[id].config.common.stopTimeout || 500) + 2500;
procs[id].restartTimer = setTimeout(_id => startInstance(_id), restartTimeout, id);
}
} else {
// moved: also remove from instance list of compactGroup
if (!compactGroupController && procs[id].config.common.compactGroup && compactProcs[procs[id].config.common.compactGroup] && compactProcs[procs[id].config.common.compactGroup].instances && compactProcs[procs[id].config.common.compactGroup].instances.includes(id)) {
compactProcs[procs[id].config.common.compactGroup].instances.splice(compactProcs[procs[id].config.common.compactGroup].instances.indexOf(id), 1);
}
if (procs[id].restartTimer) {
clearTimeout(procs[id].restartTimer);
delete procs[id].restartTimer;
}
// instance moved -> remove all notifications, new host has to take care
await notificationHandler.clearNotifications(null, null, id);
delete procs[id];
delete hostAdapter[id];
}
});
} else if (installQueue.find(obj => obj.id === id)) { // ignore object changes when still in install queue
logger.debug(`${hostLogPrefix} ignore object change because the adapter is still in installation/rebuild queue`);
} else {
const _ipArr = tools.findIPs();
if (procs[id].config && checkAndAddInstance(procs[id].config, _ipArr)) {
if (procs[id].config.common.enabled && (procs[id].config.common.mode !== 'extension' || !procs[id].config.native.webInstance)) {
startInstance(id);
}
} else {
// moved: also remove from instance list of compactGroup
if (!compactGroupController && procs[id].config.common.compactGroup && compactProcs[procs[id].config.common.compactGroup] && compactProcs[procs[id].config.common.compactGroup].instances && compactProcs[procs[id].config.common.compactGroup].instances.includes(id)) {
compactProcs[procs[id].config.common.compactGroup].instances.splice(compactProcs[procs[id].config.common.compactGroup].instances.indexOf(id), 1);
}
if (procs[id].restartTimer) {
clearTimeout(procs[id].restartTimer);
delete procs[id].restartTimer;
}
delete procs[id];
delete hostAdapter[id];
}
}
} else if (obj && obj.common) {
const _ipArr = tools.findIPs();
// new adapter
if (checkAndAddInstance(obj, _ipArr) &&
procs[id].config.common.enabled &&
(procs[id].config.common.mode !== 'extension' || !procs[id].config.native.webInstance)
) {
// We should give is a slight delay to allow an pot. former existing process on other host to exit
const restartTimeout = (procs[id].config.common.stopTimeout || 500) + 2500;
procs[id].restartTimer = setTimeout(_id => startInstance(_id), restartTimeout, id);
}
}
} catch (err) {
if (!compactGroupController || (obj && obj.common && obj.common.runAsCompactMode && obj.common.compactGroup === compactGroup)) {
logger.error(hostLogPrefix + ' cannot process: ' + id + ': ' + err + ' / ' + err.stack);
}
}
}
});
return true;
}
function startAliveInterval() {
config.system = config.system || {};
config.system.statisticsInterval = parseInt(config.system.statisticsInterval, 10) || 15000;
config.system.checkDiskInterval = (config.system.checkDiskInterval !== 0) ? parseInt(config.system.checkDiskInterval, 10) || 300000 : 0;
if (!compactGroupController) {
// Provide info to see for each host if compact is enabled or not and be able to use in Admin or such
states.setState(hostObjectPrefix + '.compactModeEnabled', {
ack: true,
from: hostObjectPrefix,
val: config.system.compact || false
});
}
reportInterval = setInterval(reportStatus, config.system.statisticsInterval);
reportStatus();
tools.measureEventLoopLag(1000, lag => eventLoopLags.push(lag));
}
function reportStatus() {
if (!states) {
return;
}
const id = hostObjectPrefix;
outputCount += 10;
states.setState(id + '.alive', {val: true, ack: true, expire: Math.floor(config.system.statisticsInterval / 1000) + 10, from: id});
// provide infos about current process
// pidUsage([pid,pid,...], function (err, stats) {
// => {
// cpu: 10.0, // percentage (from 0 to 100*vcore)
// memory: 357306368, // bytes
// ppid: 312, // PPID
// pid: 727, // PID
// ctime: 867000, // ms user + system time
// elapsed: 6650000, // ms since the start of the process
// timestamp: 864000000 // ms since epoch
// }
pidUsage(process.pid, (err, stats) => {
// controller.s might be stopped, but this is still running
if (!err && states && states.setState && stats) {
states.setState(id + '.cpu', {ack: true, from: id, val: Math.round(100 * parseFloat(stats.cpu)) / 100});
states.setState(id + '.cputime', {ack: true, from: id, val: stats.ctime / 1000});
outputCount+=2;
}
});
const mem = process.memoryUsage();
states.setState(id + '.memRss', {val: Math.round(mem.rss / 10485.76/* 1MB / 100 */) / 100, ack: true, from: id});
states.setState(id + '.memHeapTotal', {val: Math.round(mem.heapTotal / 10485.76/* 1MB / 100 */) / 100, ack: true, from: id});
states.setState(id + '.memHeapUsed', {val: Math.round(mem.heapUsed / 10485.76/* 1MB / 100 */) / 100, ack: true, from: id});
// provide machine infos
states.setState(id + '.load', {val: Math.round(os.loadavg()[0] * 100) / 100, ack: true, from: id}); //require('loadavg-windows')
states.setState(id + '.uptime', {val: Math.round(process.uptime()), ack: true, from: id});
states.setState(id + '.mem', {val: Math.round(1000 * os.freemem() / os.totalmem()) / 10, ack: true, from: id});
states.setState(id + '.freemem', {val: Math.round(os.freemem() / 1048576/* 1MB */), ack: true, from: id});
if (fs.existsSync('/proc/meminfo')) {
try {
const text = fs.readFileSync('/proc/meminfo', 'utf8');
const m = text && text.match(/MemAvailable:\s*(\d+)/);
if (m && m[1]) {
states.setState(id + '.memAvailable', {val: Math.round(parseInt(m[1], 10) * 0.001024), ack: true, from: id});
outputCount++;
}
} catch (err) {
logger.error(hostLogPrefix + ' Cannot read /proc/meminfo: ' + err);
}
}
if (config.system.checkDiskInterval && Date.now() - lastDiskSizeCheck >= config.system.checkDiskInterval) {
lastDiskSizeCheck = Date.now();
tools.getDiskInfo(os.platform(), (err, info) => {
if (err) {
logger.error(hostLogPrefix + ' Cannot read disk size: ' + err);
}
try {
if (info) {
states.setState(id + '.diskSize', {val: Math.round((info['Disk size'] || 0) / (1024 * 1024)), ack: true, from: id});
states.setState(id + '.diskFree', {val: Math.round((info['Disk free'] || 0) / (1024 * 1024)), ack: true, from: id});
outputCount+=2;
}
} catch (e) {
logger.error(hostLogPrefix + ' Cannot read disk information: ' + e);
}
});
}
// some statistics
states.setState(id + '.inputCount', {val: inputCount, ack: true, from: id});
states.setState(id + '.outputCount', {val: outputCount, ack: true, from: id});
if (eventLoopLags.length) {
const eventLoopLag = Math.ceil(eventLoopLags.reduce((a, b) => (a + b)) / eventLoopLags.length);
states.setState(id + '.eventLoopLag', {val: eventLoopLag, ack: true, from: id}); // average of measured values
eventLoopLags = [];
}
states.setState(id + '.compactgroupProcesses', {val: Object.keys(compactProcs).length, ack: true, from: id});
let realProcesses = 0;
let compactProcesses = 0;
Object.keys(procs).forEach(proc => {
if (procs[proc].process) {
if (procs[proc].startedInCompactMode) {
compactProcesses++;
} else {
realProcesses++;
}
}
});
states.setState(id + '.instancesAsProcess', {val: realProcesses, ack: true, from: id});
states.setState(id + '.instancesAsCompact', {val: compactProcesses, ack: true, from: id});
inputCount = 0;
outputCount = 0;
if (!isStopping && compactGroupController && started && compactProcesses === 0 && realProcesses === 0) {
logger.info(`${hostLogPrefix} Compact group controller ${compactGroup} does not own any processes, stop`);
stop(false);
}
}
function changeHost(objs, oldHostname, newHostname, callback) {
if (!objs || !objs.length) {
typeof callback === 'function' && callback();
} else {
const row = objs.shift();
if (row && row.value && row.value.common && row.value.common.host === oldHostname) {
const obj = row.value;
obj.common.host = newHostname;
logger.info(`${hostLogPrefix} Reassign instance ${obj._id.substring('system.adapter.'.length)} from ${oldHostname} to ${newHostname}`);
obj.from = 'system.host.' + tools.getHostName();
obj.ts = Date.now();
objects.setObject(obj._id, obj, (/* err */) =>
setImmediate(() => changeHost(objs, oldHostname, newHostname, callback)));
} else {
setImmediate(() => changeHost(objs, oldHostname, newHostname, callback));
}
}
}
function cleanAutoSubscribe(instance, autoInstance, callback) {
inputCount++;
states.getState(autoInstance + '.subscribes', (err, state) => {
if (!state || !state.val) {
return typeof callback === 'function' && setImmediate(() => callback());
}
let subs;
try {
subs = JSON.parse(state.val);
} catch {
logger.error(`${hostLogPrefix} Cannot parse subscribes: ${state.val}`);
return typeof callback === 'function' && setImmediate(() => callback());
}
let modified = false;
// look for all subscribes from this instance
for (const pattern of Object.keys(subs)) {
for (const id of Object.keys(subs[pattern])) {
if (id === instance) {
modified = true;
delete subs[pattern][id];
}
}
// check if array is now empty
if (!Object.keys(subs[pattern]).length) {
modified = true;
delete subs[pattern];
}
}
if (modified) {
outputCount++;
states.setState(`${autoInstance}.subscribes`, subs, () => (typeof callback === 'function') && callback());
} else if (typeof callback === 'function') {
setImmediate(() => callback());
}
});
}
function cleanAutoSubscribes(instance, callback) {
// instance = 'system.adapter.name.0'
instance = instance.substring(15); // get name.0
// read all instances
objects.getObjectView('system', 'instance', {startkey: 'system.adapter.', endkey: 'system.adapter.\u9999'}, (err, res) => {
let count = 0;
if (res && res.rows) {
for (let c = res.rows.length - 1; c >= 0; c--) {
// remove this instance from autoSubscribe
if (res.rows[c].value && res.rows[c].value.common.subscribable) {
count++;
cleanAutoSubscribe(instance, res.rows[c].id, () =>
!--count && callback && callback());
}
}
}
!count && callback && callback();
});
}
function delObjects(objs, callback) {
if (!objs || !objs.length) {
typeof callback === 'function' && callback();
} else {
const row = objs.shift();
if (row && row.id) {
logger.info(hostLogPrefix + ' Delete state "' + row.id + '"');
if (row.value && row.value.type === 'state') {
states.delState(row.id, (/* err */) =>
objects.delObject(row.id, (/* err */) =>
setImmediate(() => delObjects(objs, callback))));
} else {
objects.delObject(row.id, (/* err */) =>
setImmediate(() => delObjects(objs, callback)));
}
} else {
setImmediate(() => delObjects(objs, callback));
}
}
}
/**
* try to check host in objects
* <p>
* This function tries to find all hosts in the objects and if
* only one host found and it is not actual host, change the
* host name to new one.
* <p>
*
* @return none
*/
function checkHost(callback) {
const objectData = objects.getStatus();
// only file master host controller needs to check/fix the host assignments from the instances
// for redis it is currently not possible to detect a single host system with a changed hostname for sure!
if (compactGroupController || !objectData.server) {
return callback && callback();
}
objects.getObjectView('system', 'host', {}, (_err, doc) => {
if (!_err && doc && doc.rows &&
doc.rows.length === 1 &&
doc.rows[0].value.common.name !== hostname) {
const oldHostname = doc.rows[0].value.common.name;
const oldId = doc.rows[0].value._id;
// find out all instances and rewrite it to actual hostname
objects.getObjectView('system', 'instance', {}, (err, doc) => {
if (err && err.message.startsWith('Cannot find ')) {
typeof callback === 'function' && callback();
} else if (!doc.rows || doc.rows.length === 0) {
logger.info(hostLogPrefix + ' no instances found');
// no instances found
typeof callback === 'function' && callback();
} else {
// reassign all instances
changeHost(doc.rows, oldHostname, hostname, () => {
logger.info(`${hostLogPrefix} Delete host ${oldId}`);
// delete host object
objects.delObject(oldId, () =>
// delete all hosts states
objects.getObjectView('system', 'state', {startkey: 'system.host.' + oldHostname + '.', endkey: 'system.host.' + oldHostname + '.\u9999', include_docs: true}, (_err, doc) =>
delObjects(doc && Array.isArray(doc.rows) ? doc.rows : null, () => callback && callback())));
});
}
});
} else if (typeof callback === 'function') {
callback();
}
});
}
/**
* Collects the dialog information, e.g. used by Admin "System Settings"
*
* @param {'extended'|'normal'|'no-city'|'none'} type - type of required information
* @returns {Promise<object>|void}
*/
async function collectDiagInfo(type) {
if (type !== 'extended' && type !== 'normal' && type !== 'no-city') {
return null;
} else {
let systemConfig;
let err;
try {
systemConfig = await objects.getObjectAsync('system.config');
} catch (e) {
err = e;
}
if (err || !systemConfig || !systemConfig.common) {
logger.warn(`System config object is corrupt, please run "iobroker setup first". Error: ${err.message}`);
systemConfig = systemConfig || {};
systemConfig.common = systemConfig.common || {};
}
let obj;
try {
obj = await objects.getObjectAsync('system.meta.uuid');
} catch {
// ignore obj is undefined
}
// create uuid
if (!obj) {
obj = {native: {uuid: 'not found'}};
}
let doc;
err = null;
try {
doc = await objects.getObjectViewAsync('system', 'host', {});
} catch (e) {
err = e;
}
// we need to show city and country at the beginning, so include it now and delete it later if not allowed.
const diag = {
uuid: obj.native.uuid,
language: systemConfig.common.language,
country: '',
city: '',
hosts: [],
node: process.version,
arch: os.arch(),
adapters: {},
statesType: config.states.type, // redis or file
objectsType: config.objects.type // redis or file
};
if (type === 'extended' || type === 'no-city') {
const cpus = os.cpus();
diag.country = systemConfig.common.country;
diag.model = cpus && cpus[0] && cpus[0].model ? cpus[0].model : 'unknown';
diag.cpus = cpus ? cpus.length : 1;
diag.mem = os.totalmem();
diag.ostype = os.type();
delete diag.city;
}
if (type === 'extended') {
diag.city = systemConfig.common.city;
} else if (type === 'normal') {
delete diag.city;
delete diag.country;
}
if (!err && doc && doc.rows.length) {
doc.rows.sort((a, b) => {
try {
return semver.lt((a && a.value && a.value.common) ? a.value.common.installedVersion : '0.0.0', (b && b.value && b.value.common) ? b.value.common.installedVersion : '0.0.0');
} catch {
logger.error(`${hostLogPrefix} Invalid versions: ${(a && a.value && a.value.common) ? a.value.common.installedVersion : '0.0.0'}[${(a && a.value && a.value.common) ? a.value.common.name : 'unknown'}] or ${(b && b.value && b.value.common) ? b.value.common.installedVersion : '0.0.0'}[${(b && b.value && b.value.common) ? b.value.common.name : 'unknown'}]`);
return 0;
}
});
// Read installed versions of all hosts
for (const row of doc.rows) {
diag.hosts.push({
version: row.value.common.installedVersion,
platform: row.value.common.platform,
type: row.value.native.os.platform
});
}
}
doc = null;
err = null;
try {
doc = await objects.getObjectViewAsync('system', 'adapter', {});
} catch (e) {
err = e;
}
let visFound = false;
if (!err && doc && doc.rows.length) {
// Read installed versions of all adapters
for (const row of doc.rows) {
diag.adapters[row.value.common.name] = {
version: row.value.common.version,
platform: row.value.common.platform
};
if (row.value.common.name === 'vis') {
visFound = true;
}
}
}
// read number of vis datapoints
if (visFound) {
const visUtils = require('./lib/vis/states');
try {
return new Promise(resolve => {
visUtils(objects, null, 0, null, (err, points) => {
let total = null;
const tasks = [];
if (points && points.length) {
for (const point of points) {
if (point.id === 'vis.0.datapoints.total') {
total = point.val;
}
tasks.push({
_id: point.id,
type: 'state',
native: {},
common: {
name: 'Datapoints count',
role: 'state',
type: 'number',
read: true,
write: false
},
state: {
val: point.val,
ack: true
}
});
}
}
if (total !== null) {
diag.vis = total;
}
extendObjects(tasks, () => resolve(diag));
});
});
} catch (e) {
logger.error(`${hostLogPrefix} cannot call visUtils: ${e}`);
return diag;