iobroker.js-controller
Version:
Updated by reinstall.js on 2018-06-11T15:19:56.688Z
1,149 lines (1,043 loc) • 50 kB
JavaScript
/**
* Setup
*
* Copyright 2013-2022 bluefox <dogafox@gmail.com>
*
* MIT License
*
*/
;
const COLOR_RED = '\x1b[31m';
const COLOR_YELLOW = '\x1b[33m';
const COLOR_RESET = '\x1b[0m';
const COLOR_GREEN = '\x1b[32m';
/** @class */
function Setup(options) {
const fs = require('fs-extra');
const path = require('path');
const { tools, EXIT_CODES } = require('@iobroker/js-controller-common');
const dbTools = require('@iobroker/js-controller-common-db').tools;
const Backup = require('./setupBackup');
const deepClone = require('deep-clone');
const pluginInfos = require('./pluginInfos');
options = options || {};
const processExit = options.processExit;
const dbConnect = options.dbConnect;
const params = options.params;
const cleanDatabase = options.cleanDatabase;
const resetDbConnect = options.resetDbConnect;
const restartController = options.restartController;
let objects;
let states;
function mkpathSync(rootpath, dirpath) {
// Remove filename
dirpath = dirpath.split('/');
dirpath.pop();
if (!dirpath.length) {
return;
}
for (const dir of dirpath) {
rootpath = path.join(rootpath, dir);
if (!fs.existsSync(rootpath)) {
if (dir !== '..') {
fs.mkdirSync(rootpath);
} else {
throw new Error(`Cannot create ${rootpath}${dirpath.join('/')}`);
}
}
}
}
async function informAboutPlugins(systemConfig, callback) {
let ioPackage;
let ioConfig;
const configFile = tools.getConfigFileName();
try {
ioPackage = JSON.parse(fs.readFileSync(path.join(__dirname, '../../io-package.json'), 'utf8'));
} catch {
console.error('Cannot read js-controller io-package.json. Ignore plugins defined there.');
}
try {
ioConfig = JSON.parse(fs.readFileSync(configFile, 'utf8'));
} catch {
console.error('Can not read js-controller config file. Ignore plugins defined there.');
}
const plugins = {};
if (ioPackage && ioPackage.common && ioPackage.common.plugins) {
for (const [plugin, pluginData] of Object.entries(ioPackage.common.plugins)) {
if (pluginData.enabled !== false) {
plugins[plugin] = pluginData;
}
}
}
if (ioConfig && ioConfig.plugins) {
for (const [plugin, pluginData] of Object.entries(ioConfig.plugins)) {
if (!plugins[plugin] && pluginData.enabled !== false) {
plugins[plugin] = pluginData;
}
}
}
let systemLang = 'en';
let systemDiag = 'extended';
if (systemConfig && systemConfig.common) {
systemDiag = systemConfig.common.diag || 'extended';
systemLang = systemConfig.common.language || 'en';
}
for (const plugin of Object.keys(plugins)) {
const pluginInfo = pluginInfos.PLUGIN_INFOS[plugin];
if (!pluginInfo) {
// We do not have relevant information to display
continue;
}
if (systemDiag === 'none' && pluginInfos.isReportingPlugin(plugin)) {
// Reporting plugins respect "diag" and do not send information if diag is disabled
continue;
}
let enabledState;
try {
enabledState = await states.getStateAsync(
`system.host.${tools.getHostName()}.plugins.${plugin}.enabled`
);
} catch {
// ignore
}
if (enabledState && enabledState.val !== undefined) {
// already configured, so do not output again
continue;
}
const infoHeadLine = pluginInfo.headline[systemLang] || pluginInfo.headline.en;
const infoText = pluginInfo.text[systemLang] || pluginInfo.text.en;
console.error(COLOR_RED);
console.error(infoHeadLine);
console.error(COLOR_YELLOW);
console.error(infoText);
console.error();
console.error(COLOR_RESET);
}
callback();
}
/**
* Called after io-package objects are created
*
* @param {Record<string, any>} systemConfig
* @param {() => void} callback
* @return {Promise<void>}
*/
async function setupReady(systemConfig, callback) {
if (!callback) {
console.log('database setup done. You can add adapters and start ' + tools.appName + ' now');
return processExit(EXIT_CODES.NO_ERROR);
}
// clean up invalid user group assignments (non-existing user in a group)
try {
await _cleanupInvalidGroupAssignments();
} catch (e) {
console.error(`Cannot clean up invalid user group assignments: ${e.message}`);
}
if (!objects.syncFileDirectory || !objects.dirExists) {
return void informAboutPlugins(systemConfig, callback);
}
// check meta.user
try {
const objExists = await objects.objectExists('meta.user');
if (objExists) {
// check if dir is missing
const dirExists = objects.dirExists('meta.user');
if (!dirExists) {
// create meta.user, so users see them as upload target
await objects.mkdirAsync('meta.user');
console.log('Successfully created "meta.user" directory');
}
}
} catch (err) {
console.warn(`Could not create directory "meta.user": ${err.message}`);
}
try {
const { numberSuccess, notifications } = objects.syncFileDirectory();
numberSuccess &&
console.log(
`${numberSuccess} file(s) successfully synchronized with ioBroker storage.
Please DO NOT copy files manually into ioBroker storage directories!`
);
if (notifications.length) {
console.log();
console.log('The following notifications happened during sync: ');
notifications.forEach(el => console.log(`- ${el}`));
console.log();
}
return void informAboutPlugins(systemConfig, callback);
} catch (err) {
console.error(`Error on file directory sync: ${err.message}`);
return void informAboutPlugins(systemConfig, callback);
}
}
async function dbSetup(iopkg, ignoreExisting, callback) {
if (typeof ignoreExisting === 'function') {
callback = ignoreExisting;
ignoreExisting = false;
}
if (iopkg.objects && iopkg.objects.length > 0) {
const obj = iopkg.objects.pop();
objects.getObject(obj._id, (err, _obj) => {
if (err || !_obj || obj._id.startsWith('_design/')) {
obj.from = `system.host.${tools.getHostName()}.cli`;
obj.ts = Date.now();
objects.setObject(obj._id, obj, () => {
console.log(`object ${obj._id} ${err || !_obj ? 'created' : 'updated'}`);
setTimeout(dbSetup, 25, iopkg, ignoreExisting, callback);
});
} else {
!ignoreExisting && console.log('object ' + obj._id + ' yet exists');
setTimeout(dbSetup, 25, iopkg, ignoreExisting, callback);
}
});
} else {
await tools.createUuid(objects);
// check if encrypt secret exists
objects.getObject('system.config', (err, obj) => {
let configFixed = false;
if (obj && obj.type !== 'config') {
obj.type = 'config';
obj.from = `system.host.${tools.getHostName()}.cli`;
obj.ts = Date.now();
configFixed = true;
}
if (obj && (!obj.native || !obj.native.secret)) {
require('crypto').randomBytes(24, (ex, buf) => {
obj.native = obj.native || {};
obj.native.secret = buf.toString('hex');
obj.from = `system.host.${tools.getHostName()}.cli`;
obj.ts = Date.now();
objects.setObject('system.config', obj, () => setupReady(obj, callback));
});
} else {
if (configFixed) {
objects.setObject('system.config', obj, () => setupReady(obj, callback));
} else {
setupReady(obj, callback);
}
}
});
}
}
/**
* Creates objects and does object related cleanup
*
* @param {() => void} callback
* @param {boolean} checkCertificateOnly
*/
function setupObjects(callback, checkCertificateOnly) {
dbConnect(params, async (_objects, _states) => {
objects = _objects;
states = _states;
const iopkg = fs.readJsonSync(`${__dirname}/../../io-package.json`);
await _maybeMigrateSets();
if (checkCertificateOnly) {
let certObj;
if (iopkg && iopkg.objects) {
for (let i = 0; i < iopkg.objects.length; i++) {
if (iopkg.objects[i] && iopkg.objects[i]._id === 'system.certificates') {
certObj = iopkg.objects[i];
break;
}
}
}
if (certObj) {
let obj;
try {
obj = await objects.getObjectAsync('system.certificates');
} catch {
// ignore
}
if (
obj &&
obj.native &&
obj.native.certificates &&
obj.native.certificates.defaultPublic !== undefined
) {
let cert = tools.getCertificateInfo(obj.native.certificates.defaultPublic);
if (cert) {
const dateCertStart = Date.parse(cert.validityNotBefore);
const dateCertEnd = Date.parse(cert.validityNotAfter);
// check, if certificate is invalid (too old, longer then 825 days or keylength too short)
if (
dateCertEnd <= Date.now() ||
cert.keyLength < 2048 ||
dateCertEnd - dateCertStart > 365 * 24 * 60 * 60 * 1000
) {
// generate new certificates
if (cert.certificateFilename) {
console.log(
`Existing file certificate (${cert.certificateFilename}) is invalid (too old, validity longer then 345 days or keylength too short). Please check it!`
);
} else {
console.log(
'Existing earlier generated certificate is invalid (too old, validity longer then 345 days or keylength too short). Generating new Certificate!'
);
cert = null;
}
}
}
if (!cert) {
const newCert = tools.generateDefaultCertificates();
obj.native.certificates.defaultPrivate = newCert.defaultPrivate;
obj.native.certificates.defaultPublic = newCert.defaultPublic;
try {
await objects.setObjectAsync(obj._id, obj);
console.log(`object ${obj._id} updated`);
} catch {
//ignore
}
dbSetup(iopkg, true, callback);
return;
}
}
dbSetup(iopkg, true, callback);
} else {
dbSetup(iopkg, true, callback);
}
} else {
dbSetup(iopkg, callback);
}
});
}
/**
* Asks the user if he wants to migrate objects if it makes sense and performs migration according to input
*
* @param {object} newConfig - updated config
* @param {object} oldConfig - previous config
* @param {import("readline").ReadLine} rl - readline object
* @param {function(number=):void} callback - callback function
*/
function migrateObjects(newConfig, oldConfig, rl, callback) {
// allow migration if one of the db types changed or host changed of redis
const oldStatesHasServer = dbTools.statesDbHasServer(oldConfig.states.type);
const oldObjectsHasServer = dbTools.statesDbHasServer(oldConfig.objects.type);
const newStatesHasServer = dbTools.statesDbHasServer(newConfig.states.type);
const newObjectsHasServer = dbTools.statesDbHasServer(newConfig.objects.type);
const oldStatesLocalServer = dbTools.isLocalStatesDbServer(oldConfig.states.type, oldConfig.states.host);
const oldObjectsLocalServer = dbTools.isLocalObjectsDbServer(oldConfig.objects.type, oldConfig.objects.host);
const newStatesLocalServer = dbTools.isLocalStatesDbServer(newConfig.states.type, newConfig.states.host);
const newObjectsLocalServer = dbTools.isLocalObjectsDbServer(newConfig.objects.type, newConfig.objects.host);
if (
oldConfig &&
(oldConfig.states.type !== newConfig.states.type ||
oldConfig.objects.type !== newConfig.objects.type ||
(!oldStatesHasServer && oldConfig.states.host !== newConfig.states.host) ||
(!oldObjectsHasServer && oldConfig.objects.host !== newConfig.objects.host))
) {
let fromMaster = oldStatesLocalServer || oldObjectsLocalServer;
let toMaster = newStatesLocalServer || newObjectsLocalServer;
if (!oldStatesHasServer && !oldObjectsHasServer) {
fromMaster = null; // Master can not be detected, check new
}
if (!newStatesHasServer && !newObjectsHasServer) {
toMaster = null; // new
}
let allowMigration;
if (fromMaster) {
if (!toMaster) {
const answer = rl.question(
`Please choose if this is a Master/single host (enter "m") or a Slave host (enter "S") you are about to edit. For Slave hosts the data migration will be skipped. [S/m]: `,
{
limit: /^[SsMm]?$/,
defaultInput: 'S'
}
);
allowMigration = !(answer === 'S' || answer === 's');
} else {
const answer = rl.question(
`This host appears to be a Master or a Single host system. Is this correct? [Y/n]: `,
{
limit: /^[YyNnJj]?$/,
defaultInput: 'Y'
}
);
allowMigration = answer === 'Y' || answer === 'y' || answer === 'J' || answer === 'j';
}
} else {
if (toMaster) {
const answer = rl.question(
`It appears that you want to convert this slave host into a Master or Single host system. Is this correct? [Y/n]: `,
{
limit: /^[YyNnJj]?$/,
defaultInput: 'Y'
}
);
allowMigration = answer === 'Y' || answer === 'y' || answer === 'J' || answer === 'j';
} else {
const answer = rl.question(
`This host appears to be an ioBroker SLAVE system. Migration will be skipped. Is this correct? [Y/n]: `,
{
limit: /^[YyNnJj]?$/,
defaultInput: 'Y'
}
);
allowMigration = !(answer === 'Y' || answer === 'y' || answer === 'J' || answer === 'j');
}
}
if (oldObjectsHasServer && !newObjectsHasServer) {
console.log(COLOR_YELLOW);
console.log(`Important: Using ${newConfig.objects.type} for the Objects database is only supported`);
console.log('with js-controller 2.0 or higher!');
console.log('When your system consists of multiple hosts please make sure to have');
console.log('js-controller 2.0 or higher installed on ALL hosts *before* continuing!');
if (allowMigration) {
console.log('');
console.log('');
console.log('Important #2: If you already did the migration on an other host');
console.log('please *do not* migrate again! This can destroy your system!');
console.log('');
console.log('');
console.log('Important #3: The process will migrate all files that were officially');
console.log('uploaded into the ioBroker system. If you have manually copied files into');
console.log('iobroker-data/files/... into own directories then these files will NOT be');
console.log('migrated! Make sure all files are in adapter directories inside the files');
console.log('directory!');
}
console.log(COLOR_RESET);
}
// FileDB -> JSONL migration is handled in the DB classes. Skip migration if both DBs are changed from File -> JsonL
if (
(oldConfig.states.type === newConfig.states.type ||
(oldConfig.states.type === 'file' && newConfig.states.type === 'jsonl')) &&
(oldConfig.objects.type === newConfig.objects.type ||
(oldConfig.objects.type === 'file' && newConfig.objects.type === 'jsonl'))
) {
console.log('Explicit migration from file to jsonl is not necessary, skipping...');
allowMigration = false;
}
let answer = 'N';
if (allowMigration) {
console.log();
answer = rl.question(
`Do you want to migrate objects and states from "${oldConfig.objects.type}/${oldConfig.states.type}" to "${newConfig.objects.type}/${newConfig.states.type}" [y/N]: `,
{
limit: /^[YyNnJj]?$/,
defaultInput: 'N'
}
);
if (
newConfig.objects.type !== oldConfig.objects.type &&
(answer === 'Y' || answer === 'y' || answer === 'J' || answer === 'j')
) {
console.log(COLOR_YELLOW);
answer = rl.question(
`Migrating the objects database will overwrite all objects! Are you sure that this is not a slave host and you want to migrate the data? [y/N]: `,
{
limit: /^[YyNnJj]?$/,
defaultInput: 'N'
}
);
console.log(COLOR_RESET);
}
}
if (answer === 'Y' || answer === 'y' || answer === 'J' || answer === 'j') {
console.log(`Connecting to previous DB "${oldConfig.objects.type}"...`);
dbConnect(params, async (objects, states, isOffline) => {
if (!isOffline) {
console.error(COLOR_RED);
console.error('Cannot migrate DB while js-controller is still running!');
console.error(
'Please stop ioBroker and try again. No settings have been changed.' + COLOR_RESET
);
return void callback(EXIT_CODES.CONTROLLER_RUNNING);
}
const backup = new Backup({
states,
objects,
cleanDatabase,
restartController,
processExit: callback
});
console.log('Creating backup ...');
console.log(`${COLOR_GREEN}This can take some time ... please be patient!${COLOR_RESET}`);
let filePath = await backup.createBackup('', true);
const origBackupPath = filePath;
filePath = filePath.replace('.tar.gz', '-migration.tar.gz');
try {
fs.renameSync(origBackupPath, filePath);
} catch {
filePath = origBackupPath;
console.log('[Not Critical Error] Could not rename Backup file');
}
console.log('Backup created: ' + filePath);
await resetDbConnect();
console.log(`updating conf/${tools.appName}.json`);
fs.writeFileSync(tools.getConfigFileName() + '.bak', JSON.stringify(oldConfig, null, 2));
fs.writeFileSync(tools.getConfigFileName(), JSON.stringify(newConfig, null, 2));
console.log('');
console.log(`Connecting to new DB "${newConfig.objects.type}" (can take up to 20s) ...`);
dbConnect(true, Object.assign(params, { timeout: 20000 }), (objects, states) => {
if (!states || !objects) {
console.error(COLOR_RED);
console.log(
'New Database could not be connected. Please check your settings. No settings have been changed.' +
COLOR_RESET
);
console.log('restoring conf/' + tools.appName + '.json');
fs.writeFileSync(tools.getConfigFileName(), JSON.stringify(oldConfig, null, 2));
fs.unlinkSync(tools.getConfigFileName() + '.bak');
return void callback(EXIT_CODES.MIGRATION_ERROR);
}
const backup = new Backup({
states,
objects,
cleanDatabase,
restartController,
processExit: callback,
dbMigration: true
});
console.log('Restore backup ...');
console.log(`${COLOR_GREEN}This can take some time ... please be patient!${COLOR_RESET}`);
backup.restoreBackup(filePath, false, true, async err => {
if (err) {
console.log(`Error happened during restore: ${err.message}`);
console.log();
console.log('restoring conf/' + tools.appName + '.json');
fs.writeFileSync(tools.getConfigFileName(), JSON.stringify(oldConfig, null, 2));
fs.unlinkSync(tools.getConfigFileName() + '.bak');
} else {
await _maybeMigrateSets();
console.log('Backup restored - Migration successful');
console.log(COLOR_YELLOW);
console.log('Important: If your system consists of multiple hosts please execute ');
console.log('"iobroker upload all" on the master AFTER all other hosts/slaves have ');
console.log('also been updated to this states/objects database configuration AND are');
console.log('running!' + COLOR_RESET);
}
callback(err ? EXIT_CODES.MIGRATION_ERROR : 0);
});
});
});
return;
} else if (!newObjectsHasServer) {
console.log('');
console.log('No Database migration was done.');
console.log(
`${COLOR_YELLOW}If this was done on your master host please execute "iobroker setup first" to newly initialize all objects.${COLOR_RESET}`
);
console.log('');
}
}
console.log(`updating conf/${tools.appName}.json`);
fs.writeFileSync(tools.getConfigFileName(), JSON.stringify(newConfig, null, 2));
callback();
}
this.setupCustom = callback => {
const rl = require('readline-sync');
let config;
let originalConfig;
// read actual configuration
try {
if (fs.existsSync(tools.getConfigFileName())) {
config = fs.readJSONSync(tools.getConfigFileName());
originalConfig = deepClone(config);
} else {
config = require(`../../conf/${tools.appName}-dist.json`);
}
} catch {
config = require(`../../conf/${tools.appName}-dist.json`);
}
const currentObjectsType = originalConfig.objects.type || 'jsonl';
const currentStatesType = originalConfig.states.type || 'jsonl';
console.log('Current configuration:');
console.log('- Objects database:');
console.log(` - Type: ${originalConfig.objects.type}`);
console.log(` - Host/Unix Socket: ${originalConfig.objects.host}`);
console.log(` - Port: ${originalConfig.objects.port}`);
if (Array.isArray(originalConfig.objects.host)) {
console.log(
` - Sentinel-Master-Name: ${
originalConfig.objects.sentinelName ? originalConfig.objects.sentinelName : 'mymaster'
}`
);
}
console.log('- States database:');
console.log(` - Type: ${originalConfig.states.type}`);
console.log(` - Host/Unix Socket: ${originalConfig.states.host}`);
console.log(` - Port: ${originalConfig.states.port}`);
if (Array.isArray(originalConfig.states.host)) {
console.log(
` - Sentinel-Master-Name: ${
originalConfig.states.sentinelName ? originalConfig.states.sentinelName : 'mymaster'
}`
);
}
if (
dbTools.objectsDbHasServer(originalConfig.objects.type) ||
dbTools.statesDbHasServer(originalConfig.states.type)
) {
console.log(`- Data Directory: ${tools.getDefaultDataDir()}`);
}
if (originalConfig && originalConfig.system && originalConfig.system.hostname) {
console.log(`- Host name: ${originalConfig.system.hostname}`);
}
console.log('');
let otype = rl.question(
`Type of objects DB [(j)sonl, (f)ile, (r)edis, ...], default [${currentObjectsType}]: `,
{
defaultInput: currentObjectsType
}
);
otype = otype.toLowerCase();
if (otype === 'r') {
otype = 'redis';
} else if (otype === 'f') {
otype = 'file';
} else if (otype === 'j') {
otype = 'jsonl';
}
let getDefaultObjectsPort;
try {
const path = require.resolve(`@iobroker/db-objects-${otype}`);
getDefaultObjectsPort = require(path).getDefaultPort;
} catch {
console.log(`${COLOR_RED}Unknown objects type: ${otype}${COLOR_RESET}`);
if (otype !== 'file' && otype !== 'redis') {
console.log(COLOR_YELLOW);
console.log(`Please check that the objects db type you entered is really correct!`);
console.log(`If yes please use "npm i @iobroker/db-objects-${otype}" to install it manually.`);
console.log(`You also need to make sure you stay up to date with this package in the future!`);
console.log(COLOR_RESET);
}
return void callback(EXIT_CODES.INVALID_ARGUMENTS);
}
if (otype === 'redis' && originalConfig.objects.type !== 'redis') {
console.log(COLOR_YELLOW);
console.log('When Objects and Files are stored in a Redis database please consider the following:');
console.log('1. All data will be stored in RAM, make sure to have enough free RAM available!');
console.log(
'2. Make sure to check Redis persistence options to make sure a Redis problem will not cause data loss!'
);
console.log('3. The Redis persistence files can get big, make sure not to use an SD card to store them.');
console.log(COLOR_RESET);
}
const defaultObjectsHost = otype === originalConfig.objects.type ? originalConfig.objects.host : '127.0.0.1';
let ohost = rl.question(
`Host / Unix Socket of objects DB(${otype}), default[${
Array.isArray(defaultObjectsHost) ? defaultObjectsHost.join(',') : defaultObjectsHost
}]: `,
{
defaultInput: Array.isArray(defaultObjectsHost) ? defaultObjectsHost.join(',') : defaultObjectsHost
}
);
ohost = ohost.toLowerCase();
const op = getDefaultObjectsPort(ohost);
const oSentinel = otype === 'redis' && ohost.includes(',');
if (oSentinel) {
ohost = ohost.split(',');
ohost.forEach((host, idx) => (ohost[idx] = host.trim()));
}
const defaultObjectsPort =
otype === originalConfig.objects.type && ohost === originalConfig.objects.host
? originalConfig.objects.port
: op;
const userObjPort = rl.question(
`Port of objects DB(${otype}), default[${
Array.isArray(defaultObjectsPort) ? defaultObjectsPort.join(',') : defaultObjectsPort
}]: `,
{
defaultInput: Array.isArray(defaultObjectsPort) ? defaultObjectsPort.join(',') : defaultObjectsPort,
limit: /^[0-9, ]+$/
}
);
let oport;
if (userObjPort.includes(',')) {
oport = userObjPort.split(',');
oport.forEach((port, idx) => {
oport[idx] = parseInt(port.trim(), 10);
if (isNaN(oport[idx])) {
console.log(`${COLOR_RED}Invalid objects port: ${oport[idx]}${COLOR_RESET}`);
return void callback(EXIT_CODES.INVALID_ARGUMENTS);
}
});
} else {
oport = parseInt(userObjPort, 10);
if (isNaN(oport)) {
console.log(`${COLOR_RED}Invalid objects port: ${oport}${COLOR_RESET}`);
return void callback(EXIT_CODES.INVALID_ARGUMENTS);
}
}
let oSentinelName = null;
if (oSentinel) {
const defaultSentinelName = originalConfig.objects.sentinelName
? originalConfig.objects.sentinelName
: 'mymaster';
oSentinelName = rl.question(`Objects Redis Sentinel Master Name [${defaultSentinelName}]: `, {
defaultInput: defaultSentinelName
});
}
let defaultStatesType = currentStatesType;
try {
require.resolve(`@iobroker/db-states-${otype}`);
defaultStatesType = otype; // if states db is also available with same type we use as default
} catch {
// ignore, unchanged
}
let stype = rl.question(
`Type of states DB [(j)sonl, (f)file, (r)edis, ...], default [${defaultStatesType}]: `,
{
defaultInput: defaultStatesType
}
);
stype = stype.toLowerCase();
if (stype === 'r') {
stype = 'redis';
} else if (stype === 'f') {
stype = 'file';
} else if (stype === 'j') {
stype = 'jsonl';
}
let getDefaultStatesPort;
try {
const path = require.resolve(`@iobroker/db-states-${stype}`);
getDefaultStatesPort = require(path).getDefaultPort;
} catch {
console.log(`${COLOR_RED}Unknown states type: ${stype}${COLOR_RESET}`);
if (stype !== 'file' && stype !== 'redis') {
console.log(COLOR_YELLOW);
console.log(`Please check that the states db type you entered is really correct!`);
console.log(`If yes please use "npm i @iobroker/db-states-${stype}" to install it manually.`);
console.log(`You also need to make sure you stay up to date with this package in the future!`);
console.log(COLOR_RESET);
}
return void callback(EXIT_CODES.INVALID_ARGUMENTS);
}
if (stype === 'redis' && originalConfig.states.type !== 'redis' && otype !== 'redis') {
console.log(COLOR_YELLOW);
console.log('When States are stored in a Redis database please make sure to configure Redis');
console.log('persistence to make sure a Redis problem will not cause data loss!');
console.log(COLOR_RESET);
}
let defaultStatesHost =
stype === originalConfig.states.type ? originalConfig.states.host : ohost || '127.0.0.1';
if (stype === otype) {
defaultStatesHost = ohost;
}
let shost = rl.question(
`Host / Unix Socket of states DB (${stype}), default[${
Array.isArray(defaultStatesHost) ? defaultStatesHost.join(',') : defaultStatesHost
}]: `,
{
defaultInput: Array.isArray(defaultStatesHost) ? defaultStatesHost.join(',') : defaultStatesHost
}
);
shost = shost.toLowerCase();
const sp = getDefaultStatesPort(shost);
const sSentinel = stype === 'redis' && shost.includes(',');
if (sSentinel) {
shost = shost.split(',');
shost.forEach((host, idx) => (shost[idx] = host.trim()));
}
let defaultStatesPort =
stype === originalConfig.states.type && shost === originalConfig.states.host
? originalConfig.states.port
: sp;
if (stype === otype && !dbTools.statesDbHasServer(stype) && shost === ohost) {
defaultStatesPort = oport;
}
const userStatePort = rl.question(
`Port of states DB (${stype}), default[${
Array.isArray(defaultStatesPort) ? defaultStatesPort.join(',') : defaultStatesPort
}]: `,
{
defaultInput: Array.isArray(defaultStatesPort) ? defaultStatesPort.join(',') : defaultStatesPort,
limit: /^[0-9, ]+$/
}
);
let sport;
if (userStatePort.includes(',')) {
sport = userStatePort.split(',');
sport.forEach((port, idx) => {
sport[idx] = parseInt(port.trim(), 10);
if (isNaN(sport[idx])) {
console.log(`${COLOR_RED}Invalid states port: ${sport[idx]}${COLOR_RESET}`);
return void callback(EXIT_CODES.INVALID_ARGUMENTS);
}
});
} else {
sport = parseInt(userStatePort, 10);
if (isNaN(sport)) {
console.log(`${COLOR_RED}Invalid states port: ${sport}${COLOR_RESET}`);
return void callback(EXIT_CODES.INVALID_ARGUMENTS);
}
}
let sSentinelName = null;
if (sSentinel) {
const defaultSentinelName = originalConfig.states.sentinelName
? originalConfig.states.sentinelName
: oSentinelName && oport === sport
? oSentinelName
: 'mymaster';
sSentinelName = rl.question(`States Redis Sentinel Master Name [${defaultSentinelName}]: `, {
defaultInput: defaultSentinelName
});
}
let dir;
let hname;
if (dbTools.isLocalStatesDbServer(stype, shost) || dbTools.isLocalObjectsDbServer(otype, ohost)) {
dir = rl.question(`Data directory (file), default[${tools.getDefaultDataDir()}]: `, {
defaultInput: tools.getDefaultDataDir()
});
hname = rl.question(
`Host name of this machine [${
originalConfig && originalConfig.system
? originalConfig.system.hostname || require('os').hostname()
: require('os').hostname()
}]: `,
{
defaultInput: (originalConfig && originalConfig.system && originalConfig.system.hostname) || ''
}
);
} else {
hname = rl.question(`Host name of this machine [${require('os').hostname()}]: `, {
defaultInput: ''
});
}
if (hname.match(/\s/)) {
console.log(`${COLOR_RED}Invalid host name: ${hname}${COLOR_RESET}`);
return void callback(EXIT_CODES.INVALID_ARGUMENTS);
}
config.system = config.system || {};
config.system.hostname = hname;
config.objects.host = ohost;
config.objects.type = otype;
config.objects.port = oport;
config.states.host = shost;
config.states.type = stype;
config.states.port = sport;
config.states.dataDir = undefined;
config.objects.dataDir = undefined;
if (dir) {
config.objects.dataDir = dir;
}
if (dir) {
config.states.dataDir = dir;
}
if (config.objects.type === 'redis' && oSentinel && oSentinelName && oSentinelName !== 'mymaster') {
config.objects.sentinelName = oSentinelName;
}
if (config.states.type === 'redis' && sSentinel && sSentinelName && sSentinelName !== 'mymaster') {
config.states.sentinelName = sSentinelName;
}
migrateObjects(config, originalConfig, rl, callback);
};
/**
* Checks if single host setup and if so migrates and activates Redis Sets Usage
* @return {Promise<void>}
* @private
*/
async function _maybeMigrateSets() {
try {
// if we have a single host system we need to ensure that existing objects are migrated to sets before doing anything else
if (await tools.isSingleHost(objects)) {
await objects.activateSets();
const noMigrated = await objects.migrateToSets();
if (noMigrated) {
console.log(`Successfully migrated ${noMigrated} objects to Redis Sets`);
}
}
} catch (e) {
console.warn(`Could not migrate objects to corresponding sets: ${e.message}`);
}
}
/**
* Removes non-existing users from groups
*
* @return {Promise<void>}
* @private
*/
async function _cleanupInvalidGroupAssignments() {
const usersView = await objects.getObjectViewAsync('system', 'user');
const groupView = await objects.getObjectViewAsync('system', 'group');
const existingUsers = usersView.rows.map(obj => obj.value._id);
for (const group of groupView.rows) {
// reference for readability
const groupMembers = group.value.common.members;
if (!Array.isArray(groupMembers)) {
// fix legacy objects
const obj = group.value;
obj.common.members = [];
await objects.setObjectAsync(obj._id, obj);
continue;
}
let changed = false;
for (let i = groupMembers.length - 1; i >= 0; i--) {
if (!existingUsers.includes(groupMembers[i])) {
// we have found a non-existing user, so remove it
changed = true;
console.log(`Removed non-existing user "${groupMembers[i]}" from group "${group.value._id}"`);
groupMembers.splice(i, 1);
}
}
if (changed) {
await objects.setObjectAsync(group.value._id, group.value);
}
}
}
this.setup = function (callback, ignoreIfExist, useRedis) {
let config;
let isCreated = false;
const platform = require('os').platform();
const otherInstallDirs = [];
// copy reinstall.js file into root
if (fs.existsSync(__dirname + '/../../../../node_modules/')) {
try {
if (fs.existsSync(__dirname + '/../../reinstall.js')) {
fs.writeFileSync(
__dirname + '/../../../../reinstall.js',
fs.readFileSync(__dirname + '/../../reinstall.js')
);
}
} catch (e) {
console.warn(`Cannot write file. Not critical: ${e.message}`);
}
}
// Delete files for other OS
if (platform.startsWith('win')) {
otherInstallDirs.push(__dirname + '/../../' + tools.appName);
otherInstallDirs.push(__dirname + '/../../' + tools.appName.substring(0, 3));
otherInstallDirs.push(__dirname + '/../../killall.sh');
otherInstallDirs.push(__dirname + '/../../reinstall.sh');
} else {
otherInstallDirs.push(__dirname + '/../../_service_' + tools.appName + '.bat');
otherInstallDirs.push(__dirname + '/../../' + tools.appName + '.bat');
otherInstallDirs.push(__dirname + '/../../' + tools.appName.substring(0, 3) + '.bat');
// copy scripts to root directory
if (fs.existsSync(__dirname + '/../../../../node_modules/')) {
const startFile = `#!/usr/bin/env node
require('${path.normalize(__dirname + '/..')}/setup').execute();`;
try {
if (fs.existsSync(__dirname + '/../../killall.sh')) {
fs.writeFileSync(
__dirname + '/../../../../killall.sh',
fs.readFileSync(__dirname + '/../../killall.sh'),
{ mode: 492 /* 0754 */ }
);
}
if (fs.existsSync(__dirname + '/../../reinstall.sh')) {
fs.writeFileSync(
__dirname + '/../../../../reinstall.sh',
fs.readFileSync(__dirname + '/../../reinstall.sh'),
{ mode: 492 /* 0754 */ }
);
}
if (!fs.existsSync(__dirname + '/../../../../' + tools.appName.substring(0, 3))) {
fs.writeFileSync(__dirname + '/../../../../' + tools.appName.substring(0, 3), startFile, {
mode: 492 /* 0754 */
});
}
if (!fs.existsSync(__dirname + '/../../../../' + tools.appName)) {
fs.writeFileSync(__dirname + '/../../../../' + tools.appName, startFile, {
mode: 492 /* 0754 */
});
}
} catch (e) {
console.warn(`Cannot write file. Not critical: ${e.message}`);
}
}
}
for (let t = 0; t < otherInstallDirs.length; t++) {
if (fs.existsSync(otherInstallDirs[t])) {
const stat = fs.statSync(otherInstallDirs[t]);
if (stat.isDirectory()) {
const files = fs.readdirSync(otherInstallDirs[t]);
for (let f = 0; f < files.length; f++) {
fs.unlinkSync(otherInstallDirs[t] + '/' + files[f]);
}
fs.rmdirSync(otherInstallDirs[t]);
} else {
try {
fs.unlinkSync(otherInstallDirs[t]);
} catch (e) {
console.warn(`Cannot delete file. Not critical: ${e.message}`);
}
}
}
}
// Create log and tmp directory
if (!fs.existsSync(__dirname + '/../../tmp')) {
fs.mkdirSync(__dirname + '/../../tmp');
}
const configFileName = tools.getConfigFileName();
// only change config if non existing - else setup custom has to be used
if (!fs.existsSync(configFileName)) {
isCreated = true;
if (fs.existsSync(__dirname + '/../../conf/' + tools.appName + '-dist.json')) {
config = require(`../../conf/${tools.appName}-dist.json`);
} else {
config = require(`../../conf/${tools.appName.toLowerCase()}-dist.json`);
}
console.log(`creating conf/${tools.appName}.json`);
config.objects.host = params.objects || '127.0.0.1';
config.states.host = params.states || '127.0.0.1';
if (useRedis) {
config.states.type = 'redis';
config.states.port = params.port || 6379;
config.objects.type = 'redis';
config.objects.port = params.port || 6379;
}
// this path is relative to js-controller
config.dataDir = tools.getDefaultDataDir();
const _path = path
.normalize(__dirname + '/../../../node_modules/' + tools.appName + '.js-controller')
.replace(/\\/g, '/');
if (fs.existsSync(_path)) {
if (_path.indexOf('/node_modules/') !== -1) {
mkpathSync(__dirname + '/../../', config.dataDir);
} else {
mkpathSync(__dirname + '../../', config.dataDir);
}
} else {
mkpathSync(__dirname + '/../', '../' + config.dataDir);
}
const dirName = path.dirname(configFileName);
if (!fs.existsSync(dirName)) {
mkpathSync('', dirName.replace(/\\/g, '/'));
}
// Create default data dir
fs.writeFileSync(configFileName, JSON.stringify(config, null, 2));
try {
// Create
if (
__dirname
.toLowerCase()
.replace(/\\/g, '/')
.indexOf('node_modules/' + tools.appName + '.js-controller') !== -1
) {
const parts = config.dataDir.split('/');
// Remove appName-data/
parts.pop();
parts.pop();
const path_ = parts.join('/');
if (!fs.existsSync(__dirname + '/../../' + path_ + '/log')) {
fs.mkdirSync(__dirname + '/../../' + path_ + '/log');
}
} else {
if (!fs.existsSync(__dirname + '/../../log')) {
fs.mkdirSync(__dirname + '/../../log');
}
}
} catch (err) {
console.log(`Non-critical error: ${err.message}`);
}
} else if (ignoreIfExist) {
// it is a setup first run and config exists yet
try {
config = fs.readJSONSync(configFileName);
if (!Object.prototype.hasOwnProperty.call(config, 'dataDir')) {
// Workaround: there was a bug with admin v5 which could remove the dataDir attribute -> fix this
// TODO: remove it as soon as all adapters are fixed which use systemConfig.dataDir
config.dataDir = tools.getDefaultDataDir();
fs.writeJSONSync(configFileName, config, { spaces: 2 });
}
} catch (err) {
console.warn(`Cannot check config file: ${err.message}`);
}
setupObjects(() => callback && callback(), true);
return;
}
setupObjects(() => callback && callback(isCreated));
};
}
module.exports = Setup;