diff --git a/CHANGELOG.md b/CHANGELOG.md index f5a5421af..be7cfb247 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ * (@GermanBluefox) Added typing for `visIconSets` in `io-package.json`(for vis-2 SVG icon sets) * (@GermanBluefox) Added typing for `smartName` in the enum objects * (@GermanBluefox) Added typing for `supportsLoadingMessage` in the instance objects +* (@GermanBluefox) Added creation of JavaScript password to encrypt vendor scripts ## 7.0.7 (2025-04-17) - Lucy * (@foxriver76) fixed the edge-case problem on Windows (if adapter calls `readDir` on single file) diff --git a/README.md b/README.md index 1e71aa960..6aba75adc 100644 --- a/README.md +++ b/README.md @@ -21,13 +21,13 @@ The ioBroker.js-controller is the heart of any ioBroker installation. The contro - [License](#license) ## Compatibility -* js-controller 7.x (Lucy) works with Node.js 18.x, 20.x and probably 22.x -* js-controller 6.x (Kiera) works with Node.js 18.x, 20.x and probably 22.x -* js-controller 5.x works with Node.js 16.x, 18.x and probably 20.x -* js-controller 4.x works with Node.js 12.x, 14.x, 16.x (incl. up to NPM 8) and probably 18.x -* js-controller 3.x works with Node.js 10.x, 12.x, 14.x and probably 16.x (first tests look good, NPM 7 still has some issues, so NPM6 is best) -* js-controller 2.x works with Node.js 8.x, 10.x, 12.x and probably 14.x (untested) -* js-controller 1.x works with Node.js 4.x, 6.x, 8.x and probably 10.x (untested) +* js-controller 7.x (Lucy) works with Node.js 18.x, 20.x, 22.x and probably 24.x +* js-controller 6.x (Kiera) works with Node.js 18.x, 20.x, 22.x +* js-controller 5.x works with Node.js 16.x, 18.x, 20.x +* js-controller 4.x works with Node.js 12.x, 14.x, 16.x (incl. up to NPM 8), 18.x +* js-controller 3.x works with Node.js 10.x, 12.x, 14.x , 16.x (first tests look good, NPM 7 still has some issues, so NPM6 is best) +* js-controller 2.x works with Node.js 8.x, 10.x, 12.x, 14.x (untested) +* js-controller 1.x works with Node.js 4.x, 6.x, 8.x, 10.x (untested) Please try to stay current with your Node.js version because the support is limited in time. As of now (May 2024) all Node.js versions below 18.x are no longer supported by Node.js and considered EOL (End Of Life). To upgrade your Node.js version and ioBroker, please follow https://forum.iobroker.net/topic/44566/how-to-node-js-f%C3%BCr-iobroker-richtig-updaten-2021-edition! diff --git a/packages/cli/src/lib/cli/cliLogs.ts b/packages/cli/src/lib/cli/cliLogs.ts index b731fca24..d0625733e 100644 --- a/packages/cli/src/lib/cli/cliLogs.ts +++ b/packages/cli/src/lib/cli/cliLogs.ts @@ -43,7 +43,7 @@ export class CLILogs extends CLICommand { const config = fs.readJSONSync(require.resolve(getConfigFileName())); const logger = toolsLogger(config.log); - /** @ts-expect-error todo adjust logger type */ + // @ts-expect-error todo adjust logger type let fileName = logger.getFileName(); if (fileName) { let lines = fs.readFileSync(fileName).toString('utf-8').split('\n'); diff --git a/packages/cli/src/lib/setup.ts b/packages/cli/src/lib/setup.ts index 24da3cf8b..d9e52123c 100644 --- a/packages/cli/src/lib/setup.ts +++ b/packages/cli/src/lib/setup.ts @@ -2723,6 +2723,7 @@ async function processCommand( case 'vendor': { const password = args[0]; const file = args[1]; + const javascriptPassword = args[2]; if (!password) { console.warn( `Please specify the password to update the vendor information!\n${tools.appName.toLowerCase()} vendor `, @@ -2735,7 +2736,7 @@ async function processCommand( const vendor = new Vendor({ objects }); try { - await vendor.checkVendor(file, password); + await vendor.checkVendor(file, password, javascriptPassword); console.log(`Synchronised vendor information.`); return void callback(); } catch (err) { @@ -2907,10 +2908,12 @@ async function unsetup(params: Record, callback: ExitCodeCb): Promi if (obj?.common.licenseConfirmed || obj?.common.language || obj?.native?.secret) { obj.common.language = 'en'; // allow with parameter --keepsecret to not delete the secret - // This is very specific use case for vendors and must not be described in documentation + // This is a very specific use case for vendors and must not be described in documentation if (!params.keepsecret) { obj.common.licenseConfirmed = false; - obj.native && delete obj.native.secret; + if (obj.native) { + delete obj.native.secret; + } } obj.from = `system.host.${tools.getHostName()}.cli`; diff --git a/packages/cli/src/lib/setup/dbConnection.ts b/packages/cli/src/lib/setup/dbConnection.ts index 0b82ebe4d..8b93e6ea4 100644 --- a/packages/cli/src/lib/setup/dbConnection.ts +++ b/packages/cli/src/lib/setup/dbConnection.ts @@ -32,9 +32,9 @@ export function dbConnect(onlyCheck: boolean, params: Record, callb /** * Connects to the DB or tests the connection. * - * @param onlyCheck - * @param params - * @param callback + * @param onlyCheck if only connection check should be performed + * @param params options used by dbConnect + * @param callback called when connection is established or check is done */ export async function dbConnect( onlyCheck: boolean | Record | DbConnectCallback, @@ -134,17 +134,16 @@ export async function dbConnect( }); } else { console.log( - `No connection to objects ${config.objects.host}:${config.objects.port}[${config.objects.type}]`, + `No connection to objects ${Array.isArray(config.objects.host) ? config.objects.host.join(', ') : config.objects.host}:${Array.isArray(config.objects.port) ? config.objects.port.join(', ') : config.objects.port}[${config.objects.type}]`, ); if (onlyCheck) { - callback && - callback({ - objects: objects!, - states: states!, - isOffline: true, - objectsDBType: config.objects.type, - config, - }); + callback?.({ + objects: objects!, + states: states!, + isOffline: true, + objectsDBType: config.objects.type, + config, + }); callback = undefined; } else { return void exitApplicationSave(EXIT_CODES.NO_CONNECTION_TO_OBJ_DB); @@ -215,7 +214,7 @@ export async function dbConnect( objects = null; } console.log( - `No connection to states ${config.states.host}:${config.states.port}[${config.states.type}]`, + `No connection to states ${Array.isArray(config.states.host) ? config.states.host.join(', ') : config.states.host}:${Array.isArray(config.states.port) ? config.states.port.join(', ') : config.states.port}[${config.states.type}]`, ); if (onlyCheck) { callback && diff --git a/packages/cli/src/lib/setup/multihostClient.ts b/packages/cli/src/lib/setup/multihostClient.ts index 06c82c30d..1ce814bb0 100644 --- a/packages/cli/src/lib/setup/multihostClient.ts +++ b/packages/cli/src/lib/setup/multihostClient.ts @@ -1,19 +1,42 @@ +/** + * Multihost discovery client used by the CLI setup utilities. + * + * This module implements a lightweight UDP-based discovery protocol (multicast/broadcast) + * to find other ioBroker hosts on the local network. It supports an optional + * password-based handshake and returns the objects/states database configuration + * necessary for remote setup and connection. + */ + import dgram from 'node:dgram'; +import * as crypto from 'node:crypto'; + import { tools } from '@iobroker/js-controller-common'; -import crypto from 'node:crypto'; const PORT = 50005; const MULTICAST_ADDR = '239.255.255.250'; -interface ReceivedMessage { - cmd: string; +/** + * Message structure received from a multihost server during discovery or connect. + */ +export interface ReceivedMessage { + /** Command name, e.g. 'browse' */ + cmd: 'browse' | 'auth'; + /** Unique message identifier */ id: number; - result: string; + /** Result string returned by server: 'ok', 'not authenticated', etc. */ + // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents + result: 'ok' | 'not authenticated' | string; + /** Optional IP address of responder */ ip?: string; + /** Optional hostname of responder */ hostname?: string; + /** Informational text */ info?: string; + /** Whether responder is a slave */ slave?: boolean; + /** Authentication token (when required) */ auth?: string; + /** Salt used for password hashing during authentication */ salt?: string; /** The states config of ioBroker.json */ states?: ioBroker.StatesDatabaseOptions; @@ -23,13 +46,25 @@ interface ReceivedMessage { export type BrowseResultEntry = Partial; +/** + * MHClient implements browsing and connecting to multihost-enabled ioBroker hosts. + * + * Usage: + * - Create an instance of MHClient + * - Call `browse(timeout, isDebug)` to discover hosts + * - Call `connect(ip, password, callback)` to retrieve configs from a host + */ export class MHClient { + /** Incremental message id used for request/response correlation */ private id: number = 1; private timer: NodeJS.Timeout | null = null; private server: dgram.Socket | undefined; /** - * Stops the MH server + * Stops the MH server and clears any pending timers. + * + * Cleans up the UDP socket and associated timeout to ensure no resources + * are leaked after browsing or connect operations finish. */ private stopServer(): void { if (this.server) { @@ -48,11 +83,11 @@ export class MHClient { } /** - * Calculate the SHA + * Calculate SHA-256 hash from secret and salt. * - * @param secret the MH secret - * @param salt the MH salt - * @param callback + * @param secret - Multihost secret/password + * @param salt - Salt provided by server + * @param callback - Called with hex-encoded SHA-256 result */ private sha(secret: string, salt: string, callback: (sha: string) => void): void { // calculate sha256 @@ -70,13 +105,13 @@ export class MHClient { } /** - * Starts the MH server + * Starts a UDP server socket used for discovery and authentication. * - * @param isBroadcast if server should receive broadcast - * @param timeout timeout after which MH server will be closed - * @param onReady ready handler - * @param onMessage message handler, if return true here, server will be stopped - * @param onFinished finished handler + * @param isBroadcast - If true, enables broadcast mode on the socket. + * @param timeout - Time in ms after which the server will be automatically closed. + * @param onReady - Called once the socket is bound and ready to send. + * @param onMessage - Handler invoked for each parsed message. Return true to stop the server. + * @param onFinished - Called when the server stops or an error occurs. */ private startServer( isBroadcast: boolean, @@ -130,10 +165,13 @@ export class MHClient { } /** - * Start MH browsing for server + * Browse for multihost servers. * - * @param timeout timeout to stop browsing - * @param isDebug debug will also show local addresses + * Sends a multicast/broadcast "browse" request and collects responses until timeout. + * + * @param timeout - Milliseconds to wait for responses. + * @param isDebug - If true, include local addresses and log received messages. + * @returns Promise resolving to an array of discovered hosts (partial ReceivedMessage entries). */ browse(timeout: number, isDebug: boolean): Promise { const result: BrowseResultEntry[] = []; @@ -185,11 +223,13 @@ export class MHClient { } /** - * Connect to server + * Connect to a single multihost server and retrieve its objects/states configuration. + * + * Performs an optional password-based authentication handshake if the server requires it. * - * @param ip ip address of server - * @param password password for authentication - * @param callback + * @param ip - IP address of the server to connect to. + * @param password - Password to use for authentication (if required). Pass empty string to skip. + * @param callback - Callback called with (err, objectsConfig, statesConfig, address). */ connect( ip: string, @@ -214,6 +254,7 @@ export class MHClient { this.server!.send(text, 0, text.length, PORT, ip); }, (msg, rinfo) => { + // we expect only one answer if (msg.cmd === 'browse' && msg.id === this.id) { if (msg.result === 'ok') { if (callCb) { @@ -223,10 +264,8 @@ export class MHClient { } else if (!msg.states) { callback(new Error(`Invalid configuration received: ${JSON.stringify(msg)}`)); callCb = false; - } else { - if (typeof callback === 'function') { - callback(undefined, msg.objects, msg.states, rinfo.address); - } + } else if (typeof callback === 'function') { + callback(undefined, msg.objects, msg.states, rinfo.address); } } } else if (msg.result === 'not authenticated') { diff --git a/packages/cli/src/lib/setup/setupMultihost.ts b/packages/cli/src/lib/setup/setupMultihost.ts index 86e514ea6..9076d5baa 100644 --- a/packages/cli/src/lib/setup/setupMultihost.ts +++ b/packages/cli/src/lib/setup/setupMultihost.ts @@ -1,12 +1,13 @@ import fs from 'fs-extra'; import path from 'node:path'; import readline from 'node:readline'; +import readlineSync from 'readline-sync'; +import prompt from 'prompt'; + import { tools } from '@iobroker/js-controller-common'; import { isLocalObjectsDbServer, isLocalStatesDbServer } from '@iobroker/js-controller-common'; import type { Client as ObjectsRedisClient } from '@iobroker/db-objects-redis'; import { MHClient, type BrowseResultEntry } from './multihostClient.js'; -import readlineSync from 'readline-sync'; -import prompt from 'prompt'; interface MHParams { secure?: boolean; @@ -14,16 +15,28 @@ interface MHParams { debug?: boolean; } +/** Options for Multihost CLI */ export interface CLIMultihostOptions { + /** Redis client for objects DB */ objects: ObjectsRedisClient; + /** Additional parameters */ params?: MHParams; } +/** + * Multihost CLI class + * Handles multihost related commands + */ export class Multihost { private readonly configName: string; private params: MHParams; private objects: ObjectsRedisClient; + /** + * Constructor + * + * @param options options for the Multihost CLI + */ constructor(options: CLIMultihostOptions) { this.configName = tools.getConfigFileName(); this.params = options.params || {}; @@ -79,8 +92,7 @@ export class Multihost { async browse(): Promise { const mhClient = new MHClient(); try { - const res = await mhClient.browse(2_000, !!this.params.debug); - return res; + return await mhClient.browse(2_000, !!this.params.debug); } catch (e) { throw new Error(`Multihost discovery client: Cannot browse: ${e.message}`); } @@ -146,8 +158,12 @@ export class Multihost { config.multihostService.enabled && config.multihostService.persist ? 'enabled' : 'disabled' }`, ); - console.log(`Objects: ${config.objects.type} on ${config.objects.host}`); - console.log(`States: ${config.states.type} on ${config.states.host}`); + console.log( + `Objects: ${config.objects.type} on ${Array.isArray(config.objects.host) ? config.objects.host.join(', ') : config.objects.host}`, + ); + console.log( + `States: ${config.states.type} on ${Array.isArray(config.states.host) ? config.states.host.join(', ') : config.states.host}`, + ); } /** @@ -166,7 +182,7 @@ export class Multihost { config.multihostService.enabled = true; config.multihostService.password = ''; console.log( - 'Multihost discovery server activated on this host. If iobroker is currently not running please start befeore trying to discover this host.', + 'Multihost discovery server activated on this host. If iobroker is currently not running please start before trying to discover this host.', ); console.log( 'Important: Multihost discovery works with UDP packets. Make sure they are routed correctly in your network. If you use Docker you also need to configure this correctly.', @@ -219,17 +235,17 @@ export class Multihost { }; prompt.start(); - prompt.get(schema, (err, password) => { + prompt.get(schema, (_err, password) => { if (password?.password) { if (password.password !== password.passwordRepeat) { callback(new Error('Secret phrases are not equal!')); } else { - this.objects.getObject('system.config', (err, obj) => { + void this.objects.getObject('system.config', (_err, obj) => { config.multihostService.password = tools.encrypt( obj!.native.secret, password.password as string, ); - this.showMHState(config, changed); + void this.showMHState(config, changed); callback(); }); } @@ -238,11 +254,11 @@ export class Multihost { } }); } else { - this.showMHState(config, changed); + void this.showMHState(config, changed); callback(); } } else { - this.showMHState(config, changed); + void this.showMHState(config, changed); callback(); } } @@ -253,7 +269,7 @@ export class Multihost { status(): void { const config = this.getConfig(); config.multihostService = config.multihostService || { enabled: false, secure: true }; - this.showMHState(config, false); + void this.showMHState(config, false); } /** @@ -266,10 +282,10 @@ export class Multihost { /** * Connect to given MH server * - * @param mhClient mhclient used for connection - * @param ip ip address of server + * @param mhClient MultiHost Client used for connection + * @param ip IP address of server * @param pass password - * @param callback + * @param callback callback */ connectHelper(mhClient: MHClient, ip: string, pass: string, callback: (err?: Error) => void): void { mhClient.connect(ip, pass, async (err, oObjects, oStates, ipHost) => { @@ -293,16 +309,37 @@ export class Multihost { `IP Address of the remote host is ${tools.getLocalAddress()}. Connections from this host will not be accepted. Please change the configuration of this host to accept remote connections.`, ), ); - } else { - if (tools.isListenAllAddress(config.states.host)) { - // TODO: why we set the remote IP only when the local config allows full connectivity? + } else if ( + (!Array.isArray(config.states.host) || config.states.host.length === 1) && + (!Array.isArray(config.objects.host) || config.objects.host.length === 1) + ) { + const sHost = Array.isArray(config.states.host) ? config.states.host[0] : config.states.host; + // If server delivers 0.0.0.0 or ::, set to actual IP of the host + if (tools.isListenAllAddress(sHost)) { config.states.host = ipHost ?? ''; } - if (tools.isListenAllAddress(config.objects.host)) { - // TODO: why we set the remote IP only when the local config allows full connectivity? + const oHost = Array.isArray(config.objects.host) ? config.objects.host[0] : config.objects.host; + // If server delivers 0.0.0.0 or ::, set to actual IP of the host + if (tools.isListenAllAddress(oHost)) { config.objects.host = ipHost ?? ''; } + fs.writeFileSync(this.configName, JSON.stringify(config, null, 2)); + console.log('Config ok. Please restart ioBroker: "iobroker restart"'); + callback(); + } else { + // Find if any of the hosts is "listen all" or reachable + if (Array.isArray(config.states.host)) { + config.states.host = config.states.host.map((sHost: string) => + tools.isListenAllAddress(sHost) ? (ipHost ?? '') : sHost + ); + } + if (Array.isArray(config.objects.host)) { + config.objects.host = config.objects.host.map((oHost: string) => + tools.isListenAllAddress(oHost) ? (ipHost ?? '') : oHost + ); + } + fs.writeFileSync(this.configName, JSON.stringify(config, null, 2)); console.log('Config ok. Please restart ioBroker: "iobroker restart"'); callback(); @@ -318,7 +355,7 @@ export class Multihost { * * @param index index of host to connect to * @param pass password - * @param callback + * @param callback callback */ async connect( index: number | null, diff --git a/packages/cli/src/lib/setup/setupSetup.ts b/packages/cli/src/lib/setup/setupSetup.ts index d803a55bb..c2e3208cc 100644 --- a/packages/cli/src/lib/setup/setupSetup.ts +++ b/packages/cli/src/lib/setup/setupSetup.ts @@ -6,13 +6,13 @@ * MIT License * */ +import fs from 'fs-extra'; +import path from 'node:path'; import type { CleanDatabaseHandler, IoPackage, ProcessExitCallback, RestartController } from '@/lib/_Types.js'; import type { Client as StatesRedisClient } from '@iobroker/db-states-redis'; import type { Client as ObjectsRedisClient } from '@iobroker/db-objects-redis'; -import fs from 'fs-extra'; -import path from 'node:path'; import { EXIT_CODES, tools } from '@iobroker/js-controller-common'; import { statesDbHasServer, @@ -324,7 +324,7 @@ Please DO NOT copy files manually into ioBroker storage directories!`, if (!configObj.native?.secret) { const buf = crypto.randomBytes(24); - configObj.native = configObj.native || {}; + configObj.native ||= {}; configObj.native.secret = buf.toString('hex'); configObj.from = `system.host.${tools.getHostName()}.cli`; configObj.ts = Date.now(); @@ -360,10 +360,10 @@ Please DO NOT copy files manually into ioBroker storage directories!`, await this._maybeMigrateSets(); if (checkCertificateOnly) { - let certObj; + let certObj: ioBroker.Object | undefined; if (iopkg?.objects) { for (const obj of iopkg.objects) { - if (obj && obj._id === 'system.certificates') { + if (obj?._id === 'system.certificates') { certObj = obj; break; } @@ -371,7 +371,7 @@ Please DO NOT copy files manually into ioBroker storage directories!`, } if (certObj) { - let obj; + let obj: ioBroker.Object | null | undefined = null; try { obj = await this.objects.getObjectAsync('system.certificates'); } catch { @@ -685,27 +685,30 @@ Please DO NOT copy files manually into ioBroker storage directories!`, } async setupCustom(): Promise { - let config; - let originalConfig; + let config: ioBroker.IoBrokerJson; // read actual configuration try { if (fs.existsSync(tools.getConfigFileName())) { config = fs.readJsonSync(tools.getConfigFileName()); - originalConfig = deepClone(config); } else { config = fs.readJsonSync(path.join(CONTROLLER_DIR, 'conf', `${tools.appName.toLowerCase()}-dist.json`)); } } catch { config = fs.readJsonSync(path.join(CONTROLLER_DIR, 'conf', `${tools.appName.toLowerCase()}-dist.json`)); } + const originalConfig: ioBroker.IoBrokerJson = deepClone(config); 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}`); + console.log( + ` - Host/Unix Socket: ${Array.isArray(originalConfig.objects.host) ? originalConfig.objects.host.join(',') : originalConfig.objects.host}`, + ); + console.log( + ` - Port: ${Array.isArray(originalConfig.objects.port) ? originalConfig.objects.port.join(',') : originalConfig.objects.port}`, + ); if (Array.isArray(originalConfig.objects.host)) { console.log( ` - Sentinel-Master-Name: ${ @@ -717,8 +720,12 @@ Please DO NOT copy files manually into ioBroker storage directories!`, } console.log('- States database:'); console.log(` - Type: ${originalConfig.states.type}`); - console.log(` - Host/Unix Socket: ${originalConfig.states.host}`); - console.log(` - Port: ${originalConfig.states.port}`); + console.log( + ` - Host/Unix Socket: ${Array.isArray(originalConfig.states.host) ? originalConfig.states.host.join(',') : originalConfig.states.host}`, + ); + console.log( + ` - Port: ${Array.isArray(originalConfig.states.port) ? originalConfig.states.port.join(',') : originalConfig.states.port}`, + ); if (Array.isArray(originalConfig.states.host)) { console.log( ` - Sentinel-Master-Name: ${ @@ -733,45 +740,45 @@ Please DO NOT copy files manually into ioBroker storage directories!`, if (hasObjectsServer || hasStatesServer) { console.log(`- Data Directory: ${tools.getDefaultDataDir()}`); } - if (originalConfig && originalConfig.system && originalConfig.system.hostname) { + if (originalConfig?.system?.hostname) { console.log(`- Host name: ${originalConfig.system.hostname}`); } console.log(''); - let otype = rl.question( + let oType = rl.question( `Type of objects DB [(j)sonl, (f)ile, (r)edis, ...], default [${currentObjectsType}]: `, { defaultInput: currentObjectsType, }, ); - otype = otype.toLowerCase(); + oType = oType.toLowerCase(); - if (otype === 'r') { - otype = 'redis'; - } else if (otype === 'f') { - otype = 'file'; - } else if (otype === 'j') { - otype = 'jsonl'; + 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}`); + 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_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(`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 EXIT_CODES.INVALID_ARGUMENTS; } - if (otype === 'redis' && originalConfig.objects.type !== 'redis') { + 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!'); @@ -783,9 +790,9 @@ Please DO NOT copy files manually into ioBroker storage directories!`, } const defaultObjectsHost = - otype === originalConfig.objects.type ? originalConfig.objects.host : tools.getLocalAddress(); + oType === originalConfig.objects.type ? originalConfig.objects.host : tools.getLocalAddress(); let oHost: string | string[] = rl.question( - `Host / Unix Socket of objects DB(${otype}), default[${ + `Host / Unix Socket of objects DB(${oType}), default[${ Array.isArray(defaultObjectsHost) ? defaultObjectsHost.join(',') : defaultObjectsHost }]: `, { @@ -795,19 +802,19 @@ Please DO NOT copy files manually into ioBroker storage directories!`, oHost = oHost.toLowerCase(); const op = getDefaultObjectsPort(oHost); - const oSentinel = otype === 'redis' && oHost.includes(','); + const oSentinel = oType === 'redis' && oHost.includes(','); if (oSentinel) { oHost = oHost.split(',').map(host => host.trim()); } const defaultObjectsPort = - otype === originalConfig.objects.type && oHost === originalConfig.objects.host + oType === originalConfig.objects.type && oHost === originalConfig.objects.host ? originalConfig.objects.port : op; const userObjPort = rl.question( - `Port of objects DB(${otype}), default[${ + `Port of objects DB(${oType}), default[${ Array.isArray(defaultObjectsPort) ? defaultObjectsPort.join(',') : defaultObjectsPort }]: `, { @@ -838,7 +845,7 @@ Please DO NOT copy files manually into ioBroker storage directories!`, } } - config.objects = await performObjectsInterview({ dbType: otype, config: config.objects }); + config.objects = await performObjectsInterview({ dbType: oType, config: config.objects }); let oSentinelName = null; if (oSentinel) { @@ -852,46 +859,46 @@ Please DO NOT copy files manually into ioBroker storage directories!`, 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 + require.resolve(`@iobroker/db-states-${oType}`); + defaultStatesType = oType as 'jsonl' | 'file' | 'redis'; // if states db is also available with same type we use as default } catch { // ignore, unchanged } - let stype = rl.question( + let sType = rl.question( `Type of states DB [(j)sonl, (f)file, (r)edis, ...], default [${defaultStatesType}]: `, { defaultInput: defaultStatesType, }, ); - stype = stype.toLowerCase(); + sType = sType.toLowerCase(); - if (stype === 'r') { - stype = 'redis'; - } else if (stype === 'f') { - stype = 'file'; - } else if (stype === 'j') { - stype = 'jsonl'; + 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}`); + 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_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(`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 EXIT_CODES.INVALID_ARGUMENTS; } - if (stype === 'redis' && originalConfig.states.type !== 'redis' && otype !== 'redis') { + 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!'); @@ -899,12 +906,12 @@ Please DO NOT copy files manually into ioBroker storage directories!`, } let defaultStatesHost = - stype === originalConfig.states.type ? originalConfig.states.host : oHost || tools.getLocalAddress(); - if (stype === otype) { + sType === originalConfig.states.type ? originalConfig.states.host : oHost || tools.getLocalAddress(); + if (sType === oType) { defaultStatesHost = oHost; } let sHost: string | string[] = rl.question( - `Host / Unix Socket of states DB (${stype}), default[${ + `Host / Unix Socket of states DB (${sType}), default[${ Array.isArray(defaultStatesHost) ? defaultStatesHost.join(',') : defaultStatesHost }]: `, { @@ -914,24 +921,24 @@ Please DO NOT copy files manually into ioBroker storage directories!`, sHost = sHost.toLowerCase(); const sp = getDefaultStatesPort(sHost); - const sSentinel = stype === 'redis' && sHost.includes(','); + const sSentinel = sType === 'redis' && sHost.includes(','); if (sSentinel) { sHost = sHost.split(',').map(host => host.trim()); } let defaultStatesPort = - stype === originalConfig.states.type && sHost === originalConfig.states.host + sType === originalConfig.states.type && sHost === originalConfig.states.host ? originalConfig.states.port : sp; - const statesHasServer = await statesDbHasServer(stype); + const statesHasServer = await statesDbHasServer(sType); - if (stype === otype && !statesHasServer && sHost === oHost) { + if (sType === oType && !statesHasServer && sHost === oHost) { defaultStatesPort = oPort; } const userStatePort = rl.question( - `Port of states DB (${stype}), default[${ + `Port of states DB (${sType}), default[${ Array.isArray(defaultStatesPort) ? defaultStatesPort.join(',') : defaultStatesPort }]: `, { @@ -963,7 +970,7 @@ Please DO NOT copy files manually into ioBroker storage directories!`, } } - config.states = await performStatesInterview({ dbType: stype, config: config.states }); + config.states = await performStatesInterview({ dbType: sType, config: config.states }); let sSentinelName = null; if (sSentinel) { @@ -977,11 +984,11 @@ Please DO NOT copy files manually into ioBroker storage directories!`, }); } - let dir; - let hname; + let dir: string | undefined; + let hname: string; - const hasLocalObjectsServer = await isLocalObjectsDbServer(otype, oHost); - const hasLocalStatesServer = await isLocalStatesDbServer(stype, sHost); + const hasLocalObjectsServer = await isLocalObjectsDbServer(oType, oHost); + const hasLocalStatesServer = await isLocalStatesDbServer(sType, sHost); if (hasLocalStatesServer || hasLocalObjectsServer) { let validDataDir = false; @@ -1024,13 +1031,13 @@ Please DO NOT copy files manually into ioBroker storage directories!`, return EXIT_CODES.INVALID_ARGUMENTS; } - config.system = config.system || {}; + config.system ||= {} as ioBroker.IoBrokerJson['system']; config.system.hostname = hname; config.objects.host = oHost; - config.objects.type = otype; + config.objects.type = oType as 'jsonl' | 'file' | 'redis'; config.objects.port = oPort; config.states.host = sHost; - config.states.type = stype; + config.states.type = sType as 'jsonl' | 'file' | 'redis'; config.states.port = sPort; config.states.dataDir = undefined; config.objects.dataDir = undefined; @@ -1057,8 +1064,7 @@ Please DO NOT copy files manually into ioBroker storage directories!`, config.states.sentinelName = sSentinelName; } - const exitCode = await this.migrateObjects(config, originalConfig); - return exitCode; + return await this.migrateObjects(config, originalConfig); } /** @@ -1070,7 +1076,7 @@ Please DO NOT copy files manually into ioBroker storage directories!`, } try { - // if we have a single host system we need to ensure that existing objects are migrated to sets before doing anything else + // 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(this.objects)) { await this.objects.activateSets(); const noMigrated = await this.objects.migrateToSets(); @@ -1448,14 +1454,14 @@ Please DO NOT copy files manually into ioBroker storage directories!`, } /** - * Setup the installation with config file, host object, scripts etc + * Set up the installation with config file, host object, scripts etc * * @param options setup options */ setup(options: SetupCommandOptions): void { const { ignoreIfExist, useRedis, callback } = options; - let config; + let config: ioBroker.IoBrokerJson; let isCreated = false; const platform = os.platform(); const otherInstallDirs = []; @@ -1602,22 +1608,10 @@ require('${path.normalize(`${thisDir}/..`)}/setup').execute();`; } } 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, with v5.1 we can for sure remove this - config.dataDir = tools.getDefaultDataDir(); - fs.writeJSONSync(configFileName, config, { spaces: 2 }); - } - } catch (e) { - console.warn(`Cannot check config file: ${e.message}`); - } - - this.setupObjects(() => callback && callback(), true); + this.setupObjects(() => callback?.(), true); return; } - this.setupObjects(() => callback && callback(isCreated)); + this.setupObjects(() => callback?.(isCreated)); } } diff --git a/packages/cli/src/lib/setup/setupVendor.ts b/packages/cli/src/lib/setup/setupVendor.ts index 6b52556a0..5b9ed6cc6 100644 --- a/packages/cli/src/lib/setup/setupVendor.ts +++ b/packages/cli/src/lib/setup/setupVendor.ts @@ -3,11 +3,99 @@ import { tools } from '@iobroker/js-controller-common'; import fs from 'fs-extra'; import deepClone from 'deep-clone'; import { isDeepStrictEqual } from 'node:util'; +import type { InternalLogger } from '@iobroker/js-controller-common-db/tools'; +import { randomBytes } from 'node:crypto'; export interface CLIVendorOptions { objects: ObjectsRedisClient; } +interface iobVendorFile { + vendor?: { + id?: string; + name?: string; + /** Logo for login in admin */ + icon?: string; + /** Logo in the top left corner of admin */ + logo?: string; + admin?: { + menu?: { + // Settings for left menu + editable?: false; // Hide edit button in menu + 'tab-hosts'?: false; // Hide hosts item (See all https://github.com/ioBroker/ioBroker.admin/blob/master/src-rx/src/components/Drawer.js#L142) + 'tab-files'?: false; // Hide files item + 'tab-users'?: false; // Hide users item + 'tab-intro'?: false; // Hide intro item + 'tab-info'?: false; // Hide info item + 'tab-adapters'?: false; // Hide adapters item + 'tab-instances'?: false; // Hide instances item + 'tab-objects'?: false; // Hide objects item + 'tab-enums'?: false; // Hide enums item + 'tab-devices'?: false; // Hide devices item + 'tab-logs'?: false; // Hide logs item + 'tab-scenes'?: false; // Hide scenes item + 'tab-events'?: false; // Hide events item + 'tab-javascript'?: false; // Hide javascript item + 'tab-text2command-0'?: false; // Hide text2command-0 item + 'tab-echarts'?: false; // Hide echarts item + [tabName: string]: false | undefined; + }; + appBar?: { + discovery?: false; + systemSettings?: false; + toggleTheme?: false; + expertMode?: false; + hostSelector?: false; + }; + settings?: { + tabConfig?: false; // Main config tab + tabRepositories?: false; // Repositories tab + tabCertificates?: false; // Certificates tab + tabLetsEncrypt?: false; // Let's Encrypt tab + tabDefaultACL?: false; // Default ACL tab + tabStatistics?: false; // Statistics tab + + language?: false; + tempUnit?: false; + currency?: false; + dateFormat?: false; + isFloatComma?: false; + defaultHistory?: false; + activeRepo?: false; + expertMode?: false; + defaultLogLevel?: false; + }; + adapters?: { + gitHubInstall?: false; // hide button install from GitHub/npm + statistics?: false; // hide statistics on the right top + filterUpdates?: false; // hide button filter updates in adapter tab + allowAdapterRating?: false; // do not show and do not load adapter ratings + }; + login?: { + title?: string; + motto?: string; + link?: string; + }; + }; + /** Favicon for admin */ + ico?: string; + uuidPrefix?: string; + }; + model?: { + /** name for host */ + name?: string; + /** Icon for host */ + icon?: string; + /** Color for host */ + color?: string; + }; + uuid?: string; + iobroker?: Partial; + objects?: { + [id: string]: ioBroker.Object; + }; +} + const VENDOR_FILE = '/etc/iob-vendor.json'; export class Vendor { @@ -42,18 +130,25 @@ export class Vendor { * * @param file file path if not given, default path is used * @param password vendor password + * @param javascriptPassword vendor JavaScript password * @param logger */ - async checkVendor(file: string | undefined, password: string, logger?: any): Promise { - logger = logger || { + async checkVendor( + file: string | undefined, + password: string, + javascriptPassword: string | undefined, + logger?: InternalLogger, + ): Promise { + logger ||= { debug: (text: string) => console.log(text), info: (text: string) => console.log(text), error: (text: string) => console.error(text), warn: (text: string) => console.warn(text), + silly: (text: string) => console.log(text), }; - file = file || VENDOR_FILE; - let data: Record; + file ||= VENDOR_FILE; + let data: iobVendorFile | null = null; if (fs.existsSync(file)) { try { data = fs.readJSONSync(file); @@ -66,9 +161,8 @@ export class Vendor { throw new Error(`"${file}" does not exist`); } - if (data.uuid) { + if (data?.uuid) { const uuid = data.uuid; - data.uuid = null; // check UUID const obj = await this.objects.getObject('system.meta.uuid'); @@ -78,7 +172,7 @@ export class Vendor { logger.info(`Update "system.meta.uuid:native.uuid" = "${obj.native.uuid}"`); - obj.nonEdit = obj.nonEdit || {}; + obj.nonEdit ||= {}; obj.nonEdit.password = password; try { await this.objects.setObjectAsync('system.meta.uuid', obj); @@ -99,7 +193,7 @@ export class Vendor { ts: new Date().getTime(), from: `system.host.${tools.getHostName()}.tools`, native: { - uuid: uuid, + uuid, }, }); logger.info(`object system.meta.uuid created: ${uuid}`); @@ -110,45 +204,83 @@ export class Vendor { } // patch iobroker.json file - if (data.iobroker) { + if (data?.iobroker) { const settings = fs.readJSONSync(tools.getConfigFileName()); logger.info('Update iobroker.json file'); this.deepMerge(settings, data.iobroker); fs.writeFileSync(tools.getConfigFileName(), JSON.stringify(settings, null, 2)); } - if (data.vendor) { + if (data?.vendor) { const vendor = deepClone(data.vendor); - data._vendor = deepClone(vendor); - data.vendor = null; // store vendor try { - const obj = await this.objects.getObject('system.config'); - if (obj && obj.native) { - if (!isDeepStrictEqual(obj.native.vendor, vendor)) { - obj.native.vendor = vendor; - obj.nonEdit = obj.nonEdit || {}; - obj.nonEdit.password = password; - await this.objects.setObjectAsync(obj._id, obj); + const configObj = await this.objects.getObject('system.config'); + if (configObj?.native) { + let javascriptPasswordEncrypted: string | undefined; + if (javascriptPassword) { + if (!configObj.native?.secret) { + const buf = randomBytes(24); + configObj.native.secret = buf.toString('hex'); + } + javascriptPasswordEncrypted = tools.encrypt(configObj.native.secret, javascriptPassword); + } + + if ( + !isDeepStrictEqual(configObj.native.vendor, vendor) || + configObj.native.javascriptPassword !== javascriptPasswordEncrypted + ) { + configObj.native.vendor = vendor; + configObj.nonEdit ||= {}; + if (javascriptPassword) { + configObj.native.javascriptPassword = javascriptPasswordEncrypted; + configObj.nonEdit.native ||= {}; + configObj.nonEdit.native.javascriptPassword = javascriptPasswordEncrypted; + } + configObj.nonEdit.password = password; + await this.objects.setObjectAsync(configObj._id, configObj); logger.info('object system.config updated'); } } } catch (e) { logger.error(`Cannot update system.config: ${e.message}`); } + } else if (javascriptPassword) { + const configObj = await this.objects.getObject('system.config'); + + if (configObj?.native) { + if (!configObj.native?.secret) { + const buf = randomBytes(24); + configObj.native.secret = buf.toString('hex'); + } + const javascriptPasswordEncrypted = tools.encrypt(configObj.native.secret, javascriptPassword); + if (configObj.native.javascriptPassword !== javascriptPasswordEncrypted) { + configObj.native.javascriptPassword = javascriptPasswordEncrypted; + configObj.nonEdit ||= {}; + configObj.nonEdit.password = password; + configObj.nonEdit.native ||= {}; + configObj.nonEdit.native.javascriptPassword = javascriptPasswordEncrypted; + try { + await this.objects.setObjectAsync(configObj._id, configObj); + logger.info('object system.config updated'); + } catch (e) { + logger.error(`Cannot update system.config: ${e.message}`); + } + } + } } // update all existing objects according to vendor - if (data.objects) { + if (data?.objects) { for (let id of Object.keys(data.objects)) { if (!id.includes('*')) { const _newObj = data.objects[id]; const obj = await this.objects.getObject(id); if (obj) { - obj.nonEdit = obj.nonEdit || {}; + obj.nonEdit ||= {} as ioBroker.NonEditable; const originalObj = deepClone(obj); - _newObj.nonEdit = _newObj.nonEdit || {}; + _newObj.nonEdit ||= {} as ioBroker.NonEditable; _newObj.nonEdit.passHash = obj.nonEdit.passHash; // merge objects tools.copyAttributes(_newObj, obj); @@ -183,13 +315,13 @@ export class Vendor { { checked: true }, ); - if (arr && arr.rows && arr.rows.length) { + if (arr?.rows?.length) { for (const row of arr.rows) { const obj = row.value; if (obj) { - obj.nonEdit = obj.nonEdit || {}; + obj.nonEdit ||= {}; const originalObj = deepClone(obj); - _obj.nonEdit = _obj.nonEdit || {}; + _obj.nonEdit ||= {}; _obj.nonEdit.passHash = obj.nonEdit.passHash; // merge objects tools.copyAttributes(_obj, obj); @@ -213,12 +345,11 @@ export class Vendor { } // update host as last - if (data.model) { + if (data?.model) { const model = data.model; - data.model = null; const hostname = tools.getHostName(); const obj = await this.objects.getObject(`system.host.${hostname}`); - if (obj && obj.common) { + if (obj?.common) { if ( (model.name && model.name !== 'JS controller' && obj.common.title === 'JS controller') || (model.icon && !obj.common.icon) || @@ -234,15 +365,18 @@ export class Vendor { obj.common.color = model.color; } - obj.nonEdit = obj.nonEdit || {}; + obj.nonEdit ||= {}; obj.nonEdit.password = password; - obj.common.title && + if (obj.common.title) { logger.info(`Update "system.host.${hostname}:common.title" = "${obj.common.title}"`); - obj.common.icon && + } + if (obj.common.icon) { logger.info(`Update "system.host.${hostname}:common.icon" = "${!!obj.common.icon}"`); - obj.common.color && + } + if (obj.common.color) { logger.info(`Update "system.host.${hostname}:common.color" = "${obj.common.color}"`); + } try { await this.objects.setObjectAsync(obj._id, obj); @@ -255,9 +389,6 @@ export class Vendor { } // restart ioBroker - setTimeout(() => { - logger.warn('RESTART!'); - process.exit(-1); - }, 2_000); + return true; } } diff --git a/packages/common-db/src/lib/common/logger.ts b/packages/common-db/src/lib/common/logger.ts index f6e39e7dc..31f1a178a 100644 --- a/packages/common-db/src/lib/common/logger.ts +++ b/packages/common-db/src/lib/common/logger.ts @@ -172,7 +172,7 @@ export function logger( prefix = userOptions.prefix || ''; noStdout = userOptions.noStdout; const winstonFormats = []; - /** @ts-expect-error formatter arg wrongly documented */ + // @ts-expect-error formatter arg wrongly documented winstonFormats.push(winston.format.timestamp({ format: timestamp })); if (userOptions.json === undefined || userOptions.json) { winstonFormats.push(winston.format.json()); @@ -410,14 +410,14 @@ export function logger( const log = winston.createLogger(options); - /** @ts-expect-error why do we override/add method to foreign instance? TODO */ + // @ts-expect-error why do we override/add method to foreign instance? TODO log.getFileName = function () { - /** @ts-expect-error we use undocumented stuff here TODO */ + // @ts-expect-error we use undocumented stuff here TODO let transport = this.transports.find(t => (t.transport && t.transport.dirname) || t.dirname); if (transport) { - /** @ts-expect-error we use undocumented stuff here TODO */ + // @ts-expect-error we use undocumented stuff here TODO transport = transport.transport ? transport.transport : transport; - /** @ts-expect-error we use undocumented stuff here TODO */ + // @ts-expect-error we use undocumented stuff here TODO return `${transport.dirname}/${transport.filename.replace('%DATE%', getDate())}`; } return ''; @@ -435,36 +435,36 @@ export function logger( */ // @ts-expect-error why do we override/add method to foreign instance? TODO log.activateDateChecker = function (isEnabled, daysCount) { - /** @ts-expect-error we use undocumented stuff here TODO */ + // @ts-expect-error we use undocumented stuff here TODO if (!isEnabled && this._fileChecker) { - /** @ts-expect-error we use undocumented stuff here TODO */ + // @ts-expect-error we use undocumented stuff here TODO clearInterval(this._fileChecker); - /** @ts-expect-error we use undocumented stuff here TODO */ + // @ts-expect-error we use undocumented stuff here TODO } else if (isEnabled && !this._fileChecker) { if (!daysCount) { daysCount = 3; } // Check every hour - /** @ts-expect-error we use undocumented stuff here TODO */ + // @ts-expect-error we use undocumented stuff here TODO this._fileChecker = setInterval(() => { this.transports.forEach(transport => { if ( - /** @ts-expect-error we use undocumented stuff here TODO */ + // @ts-expect-error we use undocumented stuff here TODO transport.name !== 'dailyRotateFile' || - /** @ts-expect-error we use undocumented stuff here TODO */ + // @ts-expect-error we use undocumented stuff here TODO !transport.options || - /** @ts-expect-error we use undocumented stuff here TODO */ + // @ts-expect-error we use undocumented stuff here TODO transport.options.name !== tools.appName ) { return; } - /** @ts-expect-error we use undocumented stuff here TODO */ + // @ts-expect-error we use undocumented stuff here TODO if (transport && fs.existsSync(transport.dirname)) { let files; try { - /** @ts-expect-error we use undocumented stuff here TODO */ + // @ts-expect-error we use undocumented stuff here TODO files = fs.readdirSync(transport.dirname); } catch (e) { console.error(`host.${hostname} Cannot read log directory: ${e.message}`); @@ -485,20 +485,20 @@ export function logger( message: `host.${hostname} Delete log file ${files[i]}`, }); console.log(`host.${hostname} Delete log file ${files[i]}`); - /** @ts-expect-error we use undocumented stuff here TODO */ + // @ts-expect-error we use undocumented stuff here TODO fs.unlinkSync(`${transport.dirname}/${files[i]}`); } catch (e) { // there is a bug under windows, that file stays opened and cannot be deleted this.log({ level: os.platform().startsWith('win') ? 'info' : 'error', message: `host.${hostname} Cannot delete file "${path.normalize( - /** @ts-expect-error we use undocumented stuff here TODO */ + // @ts-expect-error we use undocumented stuff here TODO `${transport.dirname}/${files[i]}`, )}": ${e}`, }); console.log( `host.${hostname} Cannot delete file "${path.normalize( - /** @ts-expect-error we use undocumented stuff here TODO */ + // @ts-expect-error we use undocumented stuff here TODO `${transport.dirname}/${files[i]}`, )}": ${e.message}`, ); diff --git a/packages/common-db/src/lib/common/tools.ts b/packages/common-db/src/lib/common/tools.ts index 0ed455248..586243f28 100644 --- a/packages/common-db/src/lib/common/tools.ts +++ b/packages/common-db/src/lib/common/tools.ts @@ -386,6 +386,11 @@ function getAppName(): AppName { export const appNameLowerCase = 'iobroker'; export const appName = getAppName(); +/** + * Find all own IPs (ipv4 and ipv6) + * + * The result is cached for 10 seconds. + */ export function findIPs(): string[] { if (!lastCalculationOfIps || Date.now() - lastCalculationOfIps > 10000) { lastCalculationOfIps = Date.now(); @@ -403,6 +408,12 @@ export function findIPs(): string[] { return ownIpArr; } +/** + * Find the correct path based on given path and url + * + * @param path + * @param url + */ function findPath(path: string, url: string): string { if (!url) { return ''; @@ -780,7 +791,7 @@ export async function getFile(urlOrPath: string, fileName: string, callback: (fi } } -// Return content of the json file. Download it or read directly +// Return content of the JSON file. Download it or read directly export async function getJson( urlOrPath: string, agent: string, @@ -1467,9 +1478,13 @@ export function getRepositoryFile( } } +/** Result of getRepositoryFileAsync */ export interface RepositoryFile { + /** The repository JSON content */ json: ioBroker.RepositoryJson; + /** Whether the repository has changed compared to the provided hash */ changed: boolean; + /** The actual hash of the repository */ hash: string; } @@ -1497,6 +1512,7 @@ export async function getRepositoryFileAsync( // ignore missing hash file } + // If we have the actual repo and the hash matches, return it if (_actualRepo && !force && hash && _hash?.data && _hash.data.hash === hash) { data = _actualRepo; } else { @@ -1529,7 +1545,7 @@ export async function getRepositoryFileAsync( return { json: data, changed: _hash?.data ? hash !== _hash.data.hash : true, - hash: _hash && _hash.data ? _hash.data.hash : '', + hash: _hash?.data ? _hash.data.hash : '', }; } diff --git a/packages/controller/src/lib/restart.ts b/packages/controller/src/lib/restart.ts index 4e672a554..83b453f45 100644 --- a/packages/controller/src/lib/restart.ts +++ b/packages/controller/src/lib/restart.ts @@ -58,5 +58,5 @@ export default async function restart(callback?: () => void): Promise { // eslint-disable-next-line unicorn/prefer-module const modulePath = url.fileURLToPath(import.meta.url || `file://${__filename}`); if (process.argv[1] === modulePath) { - restart(); + void restart(); } diff --git a/packages/controller/src/main.ts b/packages/controller/src/main.ts index 182e25fca..53cfafe82 100644 --- a/packages/controller/src/main.ts +++ b/packages/controller/src/main.ts @@ -324,12 +324,12 @@ async function startMultihost(__config?: ioBroker.IoBrokerJson): Promise { if (fs.existsSync(VENDOR_BOOTSTRAP_FILE)) { logger?.info(`${hostLogPrefix} Detected vendor file: ${fs.existsSync(VENDOR_BOOTSTRAP_FILE)}`); + let restartRequired = false; try { - const startScript = fs.readJSONSync(VENDOR_BOOTSTRAP_FILE); + const startScript: { + password?: string; + javascriptPassword?: string; + } = fs.readJSONSync(VENDOR_BOOTSTRAP_FILE); if (startScript.password) { const { Vendor } = await import('@iobroker/js-controller-cli'); @@ -1775,7 +1779,12 @@ async function setMeta(): Promise { logger?.info(`${hostLogPrefix} Apply vendor file: ${VENDOR_FILE}`); try { - await vendor.checkVendor(VENDOR_FILE, startScript.password, logger); + restartRequired = await vendor.checkVendor( + VENDOR_FILE, + startScript.password, + startScript.javascriptPassword, + logger, + ); logger?.info(`${hostLogPrefix} Vendor information synchronised.`); try { if (fs.existsSync(VENDOR_BOOTSTRAP_FILE)) { @@ -1805,6 +1814,12 @@ async function setMeta(): Promise { logger?.error(`${hostLogPrefix} Cannot delete file ${VENDOR_BOOTSTRAP_FILE}: ${e.message}`); } } + if (restartRequired) { + // terminate ioBroker to restart the controller as UUID probably changed + logger.info(`${hostLogPrefix} Restart js-controller because vendor information updated`); + await wait(200); + restart(() => !isStopping && stop(false)); + } } } }, @@ -3221,7 +3236,7 @@ function initInstances(): void { } } else if (procs[id].process) { // stop instance if disabled - stopInstance(id, false); + void stopInstance(id, false); } } diff --git a/packages/controller/test/lib/testAdapter.ts b/packages/controller/test/lib/testAdapter.ts index b803a41b2..0b53fa815 100644 --- a/packages/controller/test/lib/testAdapter.ts +++ b/packages/controller/test/lib/testAdapter.ts @@ -62,11 +62,11 @@ export default function testAdapter(options: Record): void { ]; const context: TestContext = { - /** @ts-expect-error will be filled in time */ + // @ts-expect-error will be filled in time objects: null, - /** @ts-expect-error will be filled in time */ + // @ts-expect-error will be filled in time states: null, - /** @ts-expect-error will be filled in time */ + // @ts-expect-error will be filled in time adapter: null, onControllerStateChanged: null, onControllerObjectChanged: null, diff --git a/packages/db-base/src/lib/inMemFileDB.ts b/packages/db-base/src/lib/inMemFileDB.ts index 9ef08f571..a3ea97281 100644 --- a/packages/db-base/src/lib/inMemFileDB.ts +++ b/packages/db-base/src/lib/inMemFileDB.ts @@ -45,7 +45,7 @@ export interface ConnectionOptions { enhancedLogging?: boolean; backup?: BackupOptions; /** relative path to the data dir */ - dataDir: string; + dataDir?: string; } type ChangeFunction = (id: string, state: any) => void; diff --git a/packages/types-dev/config.d.ts b/packages/types-dev/config.d.ts index b4d631f98..b83a2bdb3 100644 --- a/packages/types-dev/config.d.ts +++ b/packages/types-dev/config.d.ts @@ -49,12 +49,12 @@ interface JsonlOptions { export interface DatabaseOptions { /** Possible values: 'file' - [port 9001], 'jsonl' - [port 9001], 'redis' - [port 6379 or 26379 for sentinel]. */ type: 'jsonl' | 'file' | 'redis'; - '// type': string; - host: string; - port: number; + sentinelName?: string; + host: string | string[]; + port: number | number[]; connectTimeout: number; writeFileInterval: number; - dataDir: string; + dataDir?: string; options: { auth_pass: string; retry_max_delay: number; diff --git a/schemas/iobroker.json b/schemas/iobroker.json index 9a742e30f..da77f2708 100644 --- a/schemas/iobroker.json +++ b/schemas/iobroker.json @@ -197,14 +197,34 @@ ], "description": "Possible values: 'file' - [port 9001], 'jsonl' - [port 9001], 'redis' - [port 6379 or 26379 for sentinel]." }, - "// type": { + "sentinelName": { "type": "string" }, "host": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] }, "port": { - "type": "number" + "anyOf": [ + { + "type": "number" + }, + { + "type": "array", + "items": { + "type": "number" + } + } + ] }, "connectTimeout": { "type": "number" @@ -277,10 +297,8 @@ } }, "required": [ - "// type", "backup", "connectTimeout", - "dataDir", "host", "jsonlOptions", "noFileCache", @@ -435,14 +453,34 @@ ], "description": "Possible values: 'file' - [port 9001], 'jsonl' - [port 9001], 'redis' - [port 6379 or 26379 for sentinel]." }, - "// type": { + "sentinelName": { "type": "string" }, "host": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] }, "port": { - "type": "number" + "anyOf": [ + { + "type": "number" + }, + { + "type": "array", + "items": { + "type": "number" + } + } + ] }, "connectTimeout": { "type": "number" @@ -516,10 +554,8 @@ } }, "required": [ - "// type", "backup", "connectTimeout", - "dataDir", "host", "jsonlOptions", "maxQueue", diff --git a/schemas/updateSchemas.ts b/schemas/updateSchemas.ts index b82075582..c642a6a62 100644 --- a/schemas/updateSchemas.ts +++ b/schemas/updateSchemas.ts @@ -60,4 +60,4 @@ async function getSpdxLicenseIds(): Promise { } updateIobJSON(); -updateLicenseArray(); +void updateLicenseArray();