@iobroker/js-controller-common-db 7.2.3 → 8.0.0-alpha.1-20260921-e5941ca8a
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/cjs/index.js +3 -3
- package/build/cjs/lib/common/aliasProcessing.js +6 -0
- package/build/cjs/lib/common/aliasProcessing.js.map +2 -2
- package/build/cjs/lib/common/interview.js +2 -0
- package/build/cjs/lib/common/interview.js.map +1 -1
- package/build/cjs/lib/common/logger.d.ts +34 -1
- package/build/cjs/lib/common/logger.js +37 -37
- package/build/cjs/lib/common/logger.js.map +2 -2
- package/build/cjs/lib/common/maybeCallback.js +4 -0
- package/build/cjs/lib/common/maybeCallback.js.map +1 -1
- package/build/cjs/lib/common/password.d.ts +9 -0
- package/build/cjs/lib/common/password.js +8 -6
- package/build/cjs/lib/common/password.js.map +2 -2
- package/build/cjs/lib/common/session.js +10 -5
- package/build/cjs/lib/common/session.js.map +2 -2
- package/build/cjs/lib/common/tools.d.ts +164 -59
- package/build/cjs/lib/common/tools.js +497 -476
- package/build/cjs/lib/common/tools.js.map +3 -3
- package/build/esm/lib/common/aliasProcessing.d.ts.map +1 -1
- package/build/esm/lib/common/aliasProcessing.js +5 -0
- package/build/esm/lib/common/aliasProcessing.js.map +1 -1
- package/build/esm/lib/common/logger.d.ts +34 -1
- package/build/esm/lib/common/logger.d.ts.map +1 -1
- package/build/esm/lib/common/logger.js +45 -49
- package/build/esm/lib/common/logger.js.map +1 -1
- package/build/esm/lib/common/password.d.ts +9 -0
- package/build/esm/lib/common/password.d.ts.map +1 -1
- package/build/esm/lib/common/password.js +5 -0
- package/build/esm/lib/common/password.js.map +1 -1
- package/build/esm/lib/common/session.d.ts.map +1 -1
- package/build/esm/lib/common/session.js +6 -6
- package/build/esm/lib/common/session.js.map +1 -1
- package/build/esm/lib/common/tools.d.ts +164 -59
- package/build/esm/lib/common/tools.d.ts.map +1 -1
- package/build/esm/lib/common/tools.js +535 -586
- package/build/esm/lib/common/tools.js.map +1 -1
- package/build/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +18 -18
|
@@ -1,30 +1,29 @@
|
|
|
1
|
-
import fs from 'fs-extra';
|
|
2
1
|
import path from 'node:path';
|
|
3
2
|
import semver from 'semver';
|
|
4
3
|
import os from 'node:os';
|
|
5
|
-
import
|
|
6
|
-
import deepClone from 'deep-clone';
|
|
7
|
-
import { exec as cpExecAsync } from 'promisify-child-process';
|
|
4
|
+
import crypto from 'node:crypto';
|
|
8
5
|
import { createInterface } from 'node:readline';
|
|
9
6
|
import { PassThrough } from 'node:stream';
|
|
10
|
-
import { detectPackageManager, packageManagers } from '@alcalzone/pak';
|
|
11
|
-
import { EXIT_CODES } from '../../lib/common/exitCodes.js';
|
|
12
7
|
import zlib from 'node:zlib';
|
|
13
|
-
import {
|
|
14
|
-
import jwt from 'jsonwebtoken';
|
|
15
|
-
import axios from 'axios';
|
|
16
|
-
import crypto from 'node:crypto';
|
|
17
|
-
import { exec } from 'node:child_process';
|
|
8
|
+
import { exec, execFile } from 'node:child_process';
|
|
18
9
|
import { URLSearchParams } from 'node:url';
|
|
19
10
|
import events from 'node:events';
|
|
20
|
-
import { maybeCallbackWithError } from '../../lib/common/maybeCallback.js';
|
|
21
|
-
// @ts-expect-error has no types
|
|
22
|
-
import extend from 'node.extend';
|
|
23
11
|
import { setDefaultResultOrder } from 'node:dns';
|
|
24
|
-
import { applyAliasAutoScaling, applyAliasConvenienceConversion, applyAliasTransformer, } from '../../lib/common/aliasProcessing.js';
|
|
25
12
|
import * as url from 'node:url';
|
|
26
13
|
import { createRequire } from 'node:module';
|
|
14
|
+
import forge from 'node-forge';
|
|
15
|
+
import fs from 'fs-extra';
|
|
16
|
+
import deepClone from 'deep-clone';
|
|
17
|
+
import { detectPackageManager, packageManagers, } from '@alcalzone/pak';
|
|
18
|
+
import jwt from 'jsonwebtoken';
|
|
19
|
+
import axios from 'axios';
|
|
20
|
+
// @ts-expect-error has no types
|
|
21
|
+
import extend from 'node.extend';
|
|
22
|
+
import { EXIT_CODES } from '../../lib/common/exitCodes.js';
|
|
23
|
+
import { password } from '../../lib/common/password.js';
|
|
24
|
+
import { applyAliasAutoScaling, applyAliasConvenienceConversion, applyAliasTransformer, } from '../../lib/common/aliasProcessing.js';
|
|
27
25
|
import { DEFAULT_OBJECTS_WARN_LIMIT } from '../../lib/common/constants.js';
|
|
26
|
+
const HASH_TEXT = 'hash';
|
|
28
27
|
// eslint-disable-next-line unicorn/prefer-module
|
|
29
28
|
const thisDir = url.fileURLToPath(new URL('.', import.meta.url || `file://${__filename}`));
|
|
30
29
|
// eslint-disable-next-line unicorn/prefer-module
|
|
@@ -47,12 +46,12 @@ const OFFICIAL_DOCKER_FILE = '/opt/scripts/.docker_config/.thisisdocker';
|
|
|
47
46
|
const DOCKER_INFO_URL = 'https://hub.docker.com/v2/namespaces/iobroker/repositories/iobroker/tags?page_size=1';
|
|
48
47
|
/** Time the image approx. needs to be built and published to DockerHub */
|
|
49
48
|
const DOCKER_HUB_BUILD_TIME_MS = 6 * 60 * 60 * 1_000;
|
|
50
|
-
/** Version of official Docker image
|
|
49
|
+
/** Version of the official Docker image that started to support UI upgrade */
|
|
51
50
|
const DOCKER_VERSION_UI_UPGRADE = '8.1.0';
|
|
52
51
|
let lastCalculationOfIps;
|
|
53
52
|
let ownIpArr = [];
|
|
54
53
|
// Here we define all characters that are forbidden in IDs. Since we want to allow multiple
|
|
55
|
-
//
|
|
54
|
+
// Unicode character classes, we do that by OR-ing the character classes and negating the result.
|
|
56
55
|
// Also, we can easily whitelist characters this way.
|
|
57
56
|
//
|
|
58
57
|
// We allow:
|
|
@@ -161,7 +160,7 @@ export function checkNonEditable(oldObject, newObject) {
|
|
|
161
160
|
return true;
|
|
162
161
|
}
|
|
163
162
|
/**
|
|
164
|
-
* Checks if a version is up
|
|
163
|
+
* Checks if a version is up to date, throws error on invalid version strings
|
|
165
164
|
*
|
|
166
165
|
* @param repoVersion version in repository
|
|
167
166
|
* @param installedVersion the current installed version
|
|
@@ -170,29 +169,64 @@ export function upToDate(repoVersion, installedVersion) {
|
|
|
170
169
|
// Check if the installed version is at least the repo version
|
|
171
170
|
return semver.gte(installedVersion, repoVersion);
|
|
172
171
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
console.error(`Cannot decode secret: ${e.message}`);
|
|
194
|
-
callback(null);
|
|
172
|
+
/**
|
|
173
|
+
* Derive the key and IV from a password exactly as the legacy `crypto.createCipher`/`createDecipher`
|
|
174
|
+
* functions did: OpenSSL's `EVP_BytesToKey` with MD5, a single iteration and no salt. Needed to stay
|
|
175
|
+
* compatible with data that was encrypted before `createDecipheriv` was used.
|
|
176
|
+
*
|
|
177
|
+
* @param password The password to derive the key material from (interpreted as UTF-8, like legacy Node)
|
|
178
|
+
* @param keyLen Required key length in bytes
|
|
179
|
+
* @param ivLen Required IV length in bytes
|
|
180
|
+
* @returns The derived key and IV
|
|
181
|
+
*/
|
|
182
|
+
function evpBytesToKeyMD5(password, keyLen, ivLen) {
|
|
183
|
+
const passwordBuffer = Buffer.from(password, 'utf8');
|
|
184
|
+
let material = Buffer.alloc(0);
|
|
185
|
+
let block = Buffer.alloc(0);
|
|
186
|
+
while (material.length < keyLen + ivLen) {
|
|
187
|
+
block = crypto
|
|
188
|
+
.createHash('md5')
|
|
189
|
+
.update(Buffer.concat([block, passwordBuffer]))
|
|
190
|
+
.digest();
|
|
191
|
+
material = Buffer.concat([material, block]);
|
|
195
192
|
}
|
|
193
|
+
return {
|
|
194
|
+
key: material.subarray(0, keyLen),
|
|
195
|
+
iv: material.subarray(keyLen, keyLen + ivLen),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
// TODO: this is only here for backward compatibility, if MULTIHOST password was still setup with old decryption
|
|
199
|
+
/**
|
|
200
|
+
* Decrypt a phrase that was encrypted with the legacy aes192 algorithm
|
|
201
|
+
*
|
|
202
|
+
* @param password The password used to derive the decryption key
|
|
203
|
+
* @param data The encrypted data to decrypt
|
|
204
|
+
*/
|
|
205
|
+
export function decryptPhrase(password, data) {
|
|
206
|
+
const { key, iv } = evpBytesToKeyMD5(password, 24, 16);
|
|
207
|
+
const decipher = crypto.createDecipheriv('aes-192-cbc', key, iv);
|
|
208
|
+
return new Promise(resolve => {
|
|
209
|
+
try {
|
|
210
|
+
let decrypted = '';
|
|
211
|
+
decipher.on('readable', () => {
|
|
212
|
+
const data = decipher.read();
|
|
213
|
+
if (data) {
|
|
214
|
+
decrypted += data.toString('utf8');
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
decipher.on('error', error => {
|
|
218
|
+
console.error(`Cannot decode secret: ${error.message}`);
|
|
219
|
+
resolve(null);
|
|
220
|
+
});
|
|
221
|
+
decipher.on('end', () => resolve(decrypted));
|
|
222
|
+
decipher.write(data, 'hex');
|
|
223
|
+
decipher.end();
|
|
224
|
+
}
|
|
225
|
+
catch (e) {
|
|
226
|
+
console.error(`Cannot decode secret: ${e.message}`);
|
|
227
|
+
resolve(null);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
196
230
|
}
|
|
197
231
|
/**
|
|
198
232
|
* Checks if multiple host objects exists, without using object views
|
|
@@ -201,10 +235,10 @@ export function decryptPhrase(password, data, callback) {
|
|
|
201
235
|
* @returns true if only one host object exists
|
|
202
236
|
*/
|
|
203
237
|
export async function isSingleHost(objects) {
|
|
204
|
-
const res = await objects.getObjectList({
|
|
238
|
+
const res = (await objects.getObjectList({
|
|
205
239
|
startkey: 'system.host.',
|
|
206
240
|
endkey: 'system.host.\u9999',
|
|
207
|
-
});
|
|
241
|
+
}));
|
|
208
242
|
const hostObjs = res.rows.filter(obj => obj.value && obj.value.type === 'host');
|
|
209
243
|
return hostObjs.length <= 1; // on setup no host object is there yet
|
|
210
244
|
}
|
|
@@ -226,7 +260,7 @@ export async function isHostRunning(objects, states) {
|
|
|
226
260
|
res.rows = res.rows.filter(obj => obj.value?.type === 'host');
|
|
227
261
|
for (const hostObj of res.rows) {
|
|
228
262
|
const state = await states.getState(`${hostObj.id}.alive`);
|
|
229
|
-
if (state
|
|
263
|
+
if (state?.val) {
|
|
230
264
|
return true;
|
|
231
265
|
}
|
|
232
266
|
}
|
|
@@ -272,10 +306,10 @@ export function findIPs() {
|
|
|
272
306
|
return ownIpArr;
|
|
273
307
|
}
|
|
274
308
|
/**
|
|
275
|
-
* Find the correct path based on given path and url
|
|
309
|
+
* Find the correct path based on the given path and url
|
|
276
310
|
*
|
|
277
|
-
* @param path
|
|
278
|
-
* @param url
|
|
311
|
+
* @param path The base path the url may be relative to
|
|
312
|
+
* @param url The url or relative/absolute path to resolve
|
|
279
313
|
*/
|
|
280
314
|
function findPath(path, url) {
|
|
281
315
|
if (!url) {
|
|
@@ -293,7 +327,7 @@ function findPath(path, url) {
|
|
|
293
327
|
return `${thisDir}/../${path}${url}`;
|
|
294
328
|
}
|
|
295
329
|
/**
|
|
296
|
-
* Get MAC address of this host
|
|
330
|
+
* Get the MAC address of this host
|
|
297
331
|
*/
|
|
298
332
|
async function getMac() {
|
|
299
333
|
const macRegex = /(?:[a-z0-9]{2}[:-]){5}[a-z0-9]{2}/gi;
|
|
@@ -304,7 +338,7 @@ async function getMac() {
|
|
|
304
338
|
throw new Error(stderr);
|
|
305
339
|
}
|
|
306
340
|
if (typeof stdout !== 'string') {
|
|
307
|
-
throw new Error(`Unexpected stdout: ${stdout
|
|
341
|
+
throw new Error(`Unexpected stdout: ${typeof stdout}`);
|
|
308
342
|
}
|
|
309
343
|
let macAddress;
|
|
310
344
|
let match;
|
|
@@ -406,40 +440,39 @@ export function isDocker() {
|
|
|
406
440
|
* @param givenMac the given MAC address
|
|
407
441
|
*/
|
|
408
442
|
async function uuid(givenMac) {
|
|
409
|
-
givenMac
|
|
443
|
+
givenMac ??= '';
|
|
410
444
|
const _isDocker = isDocker();
|
|
411
445
|
// return constant UUID for all CI environments to keep the statistics clean
|
|
412
446
|
if (require('ci-info').isCI) {
|
|
413
447
|
return '55travis-pipe-line-cior-githubaction';
|
|
414
448
|
}
|
|
415
|
-
let mac = givenMac !== null ? givenMac || '' : null;
|
|
416
449
|
let u;
|
|
417
|
-
if (!_isDocker &&
|
|
450
|
+
if (!_isDocker && !givenMac) {
|
|
418
451
|
const ifaces = os.networkInterfaces();
|
|
419
452
|
// Find first not empty MAC
|
|
420
453
|
for (const iface of Object.values(ifaces)) {
|
|
421
454
|
if (iface) {
|
|
422
455
|
for (const entry of iface) {
|
|
423
456
|
if (entry.mac !== '00:00:00:00:00:00') {
|
|
424
|
-
|
|
457
|
+
givenMac = entry.mac;
|
|
425
458
|
break;
|
|
426
459
|
}
|
|
427
460
|
}
|
|
428
461
|
}
|
|
429
|
-
if (
|
|
462
|
+
if (givenMac) {
|
|
430
463
|
break;
|
|
431
464
|
}
|
|
432
465
|
}
|
|
433
466
|
}
|
|
434
|
-
if (!_isDocker &&
|
|
467
|
+
if (!_isDocker && !givenMac) {
|
|
435
468
|
const mac = await getMac();
|
|
436
469
|
return uuid(mac);
|
|
437
470
|
}
|
|
438
|
-
if (!_isDocker &&
|
|
471
|
+
if (!_isDocker && givenMac) {
|
|
439
472
|
const md5sum = crypto.createHash('md5');
|
|
440
|
-
md5sum.update(
|
|
441
|
-
|
|
442
|
-
u = `${
|
|
473
|
+
md5sum.update(givenMac);
|
|
474
|
+
givenMac = md5sum.digest('hex');
|
|
475
|
+
u = `${givenMac.substring(0, 8)}-${givenMac.substring(8, 12)}-${givenMac.substring(12, 16)}-${givenMac.substring(16, 20)}-${givenMac.substring(20)}`;
|
|
443
476
|
}
|
|
444
477
|
else {
|
|
445
478
|
// Returns a RFC4122 compliant v4 UUID https://gist.github.com/LeverOne/1308368 (DO WTF YOU WANT TO PUBLIC LICENSE)
|
|
@@ -549,7 +582,7 @@ export async function createUuid(objects) {
|
|
|
549
582
|
return;
|
|
550
583
|
}
|
|
551
584
|
// Read vis license
|
|
552
|
-
const licObj = objects.getObject('system.adapter.vis.0');
|
|
585
|
+
const licObj = await objects.getObject('system.adapter.vis.0');
|
|
553
586
|
if (!licObj || !licObj.native || !licObj.native.license) {
|
|
554
587
|
return updateUuid('', objects);
|
|
555
588
|
}
|
|
@@ -573,13 +606,13 @@ export async function createUuid(objects) {
|
|
|
573
606
|
console.warn(`Provide following information in email: ${data.email}, invoice: ${data.invoice}`);
|
|
574
607
|
}
|
|
575
608
|
/**
|
|
576
|
-
* Download file to tmp or return file name directly
|
|
609
|
+
* Download the file to tmp or return the file name directly
|
|
577
610
|
*
|
|
578
|
-
* @param urlOrPath
|
|
579
|
-
* @param fileName
|
|
580
|
-
* @
|
|
611
|
+
* @param urlOrPath URL to download from, or a local file path
|
|
612
|
+
* @param fileName Target file name used when the source is downloaded to the tmp directory
|
|
613
|
+
* @returns The path to the local file
|
|
581
614
|
*/
|
|
582
|
-
export async function getFile(urlOrPath, fileName
|
|
615
|
+
export async function getFile(urlOrPath, fileName) {
|
|
583
616
|
// If object was read
|
|
584
617
|
if (urlOrPath.substring(0, 'http://'.length) === 'http://' ||
|
|
585
618
|
urlOrPath.substring(0, 'https://'.length) === 'https://') {
|
|
@@ -593,150 +626,112 @@ export async function getFile(urlOrPath, fileName, callback) {
|
|
|
593
626
|
'Accept-Encoding': 'gzip',
|
|
594
627
|
},
|
|
595
628
|
});
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
callback && callback(tmpFile);
|
|
629
|
+
await new Promise((resolve, reject) => {
|
|
630
|
+
res.data.pipe(fs.createWriteStream(tmpFile)).on('close', resolve).on('error', reject);
|
|
599
631
|
});
|
|
632
|
+
console.log(`downloaded ${tmpFile}`);
|
|
600
633
|
}
|
|
601
634
|
catch (e) {
|
|
602
635
|
console.log(`Cannot download "${tmpFile}": ${e.message}`);
|
|
603
|
-
callback && callback(tmpFile);
|
|
604
636
|
}
|
|
637
|
+
return tmpFile;
|
|
605
638
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
callback && callback(urlOrPath);
|
|
610
|
-
}
|
|
611
|
-
else if (fs.existsSync(`${thisDir}/../${urlOrPath}`)) {
|
|
612
|
-
callback && callback(`${thisDir}/../${urlOrPath}`);
|
|
613
|
-
}
|
|
614
|
-
else if (fs.existsSync(`${thisDir}/../tmp/${urlOrPath}`)) {
|
|
615
|
-
callback && callback(`${thisDir}/../tmp/${urlOrPath}`);
|
|
616
|
-
}
|
|
617
|
-
else {
|
|
618
|
-
console.log(`File not found: ${urlOrPath}`);
|
|
619
|
-
process.exit(EXIT_CODES.FILE_NOT_FOUND);
|
|
620
|
-
}
|
|
639
|
+
try {
|
|
640
|
+
if (fs.existsSync(urlOrPath)) {
|
|
641
|
+
return urlOrPath;
|
|
621
642
|
}
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
643
|
+
if (fs.existsSync(`${thisDir}/../${urlOrPath}`)) {
|
|
644
|
+
return `${thisDir}/../${urlOrPath}`;
|
|
645
|
+
}
|
|
646
|
+
if (fs.existsSync(`${thisDir}/../tmp/${urlOrPath}`)) {
|
|
647
|
+
return `${thisDir}/../tmp/${urlOrPath}`;
|
|
625
648
|
}
|
|
649
|
+
console.log(`File not found: ${urlOrPath}`);
|
|
650
|
+
process.exit(EXIT_CODES.FILE_NOT_FOUND);
|
|
626
651
|
}
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
if (typeof agent === 'function') {
|
|
631
|
-
callback = agent;
|
|
632
|
-
agent = '';
|
|
652
|
+
catch (err) {
|
|
653
|
+
console.log(`File "${urlOrPath}" could no be read: ${err.message}`);
|
|
654
|
+
process.exit(EXIT_CODES.FILE_NOT_FOUND);
|
|
633
655
|
}
|
|
634
|
-
|
|
635
|
-
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Return the content of a JSON file, downloading it from a URL or reading it directly from disk
|
|
659
|
+
*
|
|
660
|
+
* @param urlOrPath URL to download the JSON from, or a local file path
|
|
661
|
+
* @param agent User agent string used for the download request
|
|
662
|
+
*/
|
|
663
|
+
export async function getJson(urlOrPath, agent) {
|
|
664
|
+
agent ||= '';
|
|
665
|
+
let result = {};
|
|
636
666
|
// If object was read
|
|
637
|
-
if (urlOrPath
|
|
638
|
-
|
|
639
|
-
|
|
667
|
+
if (!urlOrPath) {
|
|
668
|
+
console.log('Empty url!');
|
|
669
|
+
return null;
|
|
670
|
+
}
|
|
671
|
+
if (urlOrPath.substring(0, 'http://'.length) === 'http://' ||
|
|
672
|
+
urlOrPath.substring(0, 'https://'.length) === 'https://') {
|
|
673
|
+
try {
|
|
674
|
+
const res = await axios.get(urlOrPath, {
|
|
675
|
+
headers: { 'Accept-Encoding': 'gzip', timeout: 10000, 'User-Agent': agent },
|
|
676
|
+
});
|
|
677
|
+
if (res.status !== 200 || !res.data) {
|
|
678
|
+
throw new Error(`Invalid response, body: ${res.data}, status code: ${res.status}`);
|
|
679
|
+
}
|
|
680
|
+
result = res.data;
|
|
681
|
+
return result;
|
|
682
|
+
}
|
|
683
|
+
catch (e) {
|
|
684
|
+
console.warn(`Cannot download json from ${urlOrPath}. Error: ${e.message}`);
|
|
685
|
+
return null;
|
|
640
686
|
}
|
|
641
687
|
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
688
|
+
if (fs.existsSync(urlOrPath)) {
|
|
689
|
+
try {
|
|
690
|
+
result = fs.readJSONSync(urlOrPath);
|
|
691
|
+
}
|
|
692
|
+
catch (e) {
|
|
693
|
+
console.log(`Cannot parse json file from ${urlOrPath}. Error: ${e.message}`);
|
|
694
|
+
return null;
|
|
646
695
|
}
|
|
696
|
+
return result;
|
|
647
697
|
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
try {
|
|
652
|
-
const res = await axios.get(urlOrPath, {
|
|
653
|
-
headers: { 'Accept-Encoding': 'gzip', timeout: 10000, 'User-Agent': agent },
|
|
654
|
-
});
|
|
655
|
-
if (res.status !== 200 || !res.data) {
|
|
656
|
-
throw new Error(`Invalid response, body: ${res.data}, status code: ${res.status}`);
|
|
657
|
-
}
|
|
658
|
-
sources = res.data;
|
|
659
|
-
if (callback) {
|
|
660
|
-
callback(sources, urlOrPath);
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
catch (e) {
|
|
664
|
-
console.warn(`Cannot download json from ${urlOrPath}. Error: ${e.message}`);
|
|
665
|
-
if (callback) {
|
|
666
|
-
callback(null, urlOrPath);
|
|
667
|
-
}
|
|
668
|
-
return;
|
|
669
|
-
}
|
|
698
|
+
if (fs.existsSync(`${thisDir}/../${urlOrPath}`)) {
|
|
699
|
+
try {
|
|
700
|
+
result = fs.readJSONSync(`${thisDir}/../${urlOrPath}`);
|
|
670
701
|
}
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
callback(sources, urlOrPath);
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
else if (fs.existsSync(`${thisDir}/../${urlOrPath}`)) {
|
|
688
|
-
try {
|
|
689
|
-
sources = fs.readJSONSync(`${thisDir}/../${urlOrPath}`);
|
|
690
|
-
}
|
|
691
|
-
catch (e) {
|
|
692
|
-
console.log(`Cannot parse json file from ${thisDir}/../${urlOrPath}. Error: ${e.message}`);
|
|
693
|
-
if (callback) {
|
|
694
|
-
callback(null, urlOrPath);
|
|
695
|
-
}
|
|
696
|
-
return;
|
|
697
|
-
}
|
|
698
|
-
if (callback) {
|
|
699
|
-
callback(sources, urlOrPath);
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
else if (fs.existsSync(`${thisDir}/../tmp/${urlOrPath}`)) {
|
|
703
|
-
try {
|
|
704
|
-
sources = fs.readJSONSync(`${thisDir}/../tmp/${urlOrPath}`);
|
|
705
|
-
}
|
|
706
|
-
catch (e) {
|
|
707
|
-
console.log(`Cannot parse json file from ${thisDir}/../tmp/${urlOrPath}. Error: ${e.message}`);
|
|
708
|
-
if (callback) {
|
|
709
|
-
callback(null, urlOrPath);
|
|
710
|
-
}
|
|
711
|
-
return;
|
|
712
|
-
}
|
|
713
|
-
if (callback) {
|
|
714
|
-
callback(sources, urlOrPath);
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
else {
|
|
718
|
-
if (callback) {
|
|
719
|
-
callback(null, urlOrPath);
|
|
720
|
-
}
|
|
721
|
-
}
|
|
702
|
+
catch (e) {
|
|
703
|
+
console.log(`Cannot parse json file from ${thisDir}/../${urlOrPath}. Error: ${e.message}`);
|
|
704
|
+
return null;
|
|
705
|
+
}
|
|
706
|
+
return result;
|
|
707
|
+
}
|
|
708
|
+
if (fs.existsSync(`${thisDir}/../tmp/${urlOrPath}`)) {
|
|
709
|
+
try {
|
|
710
|
+
result = fs.readJSONSync(`${thisDir}/../tmp/${urlOrPath}`);
|
|
711
|
+
}
|
|
712
|
+
catch (e) {
|
|
713
|
+
console.log(`Cannot parse json file from ${thisDir}/../tmp/${urlOrPath}. Error: ${e.message}`);
|
|
714
|
+
return null;
|
|
722
715
|
}
|
|
716
|
+
return result;
|
|
723
717
|
}
|
|
718
|
+
return null;
|
|
724
719
|
}
|
|
725
720
|
/**
|
|
726
|
-
* Return content of the json file. Download it or read directly
|
|
721
|
+
* Return the content of the json file. Download it or read directly
|
|
727
722
|
*
|
|
728
723
|
* @param urlOrPath URL where the json file could be found
|
|
729
724
|
* @param agent optional agent identifier like "Windows Chrome 12.56"
|
|
730
725
|
* @returns json object
|
|
731
726
|
*/
|
|
732
727
|
export async function getJsonAsync(urlOrPath, agent) {
|
|
733
|
-
agent
|
|
728
|
+
agent ||= '';
|
|
734
729
|
let sources = {};
|
|
735
730
|
// If object was read
|
|
736
731
|
if (urlOrPath && typeof urlOrPath === 'object') {
|
|
737
732
|
return urlOrPath;
|
|
738
733
|
}
|
|
739
|
-
|
|
734
|
+
if (!urlOrPath) {
|
|
740
735
|
console.log('Empty url!');
|
|
741
736
|
return null;
|
|
742
737
|
}
|
|
@@ -789,7 +784,7 @@ export async function getJsonAsync(urlOrPath, agent) {
|
|
|
789
784
|
}
|
|
790
785
|
}
|
|
791
786
|
/**
|
|
792
|
-
* Scans directory for adapters and adds information to list
|
|
787
|
+
* Scans the directory for adapters and adds information to list
|
|
793
788
|
*
|
|
794
789
|
* @param dirName name of the directory to scan
|
|
795
790
|
* @param list prefilled list to extend adapters too
|
|
@@ -899,334 +894,161 @@ export function getInstalledInfo(hostRunningVersion) {
|
|
|
899
894
|
* Reads an adapter's npm version
|
|
900
895
|
*
|
|
901
896
|
* @param adapter The adapter to read the npm version from. Null for the root ioBroker packet
|
|
902
|
-
* @param callback
|
|
903
897
|
*/
|
|
904
|
-
function getNpmVersion(adapter
|
|
898
|
+
async function getNpmVersion(adapter) {
|
|
905
899
|
adapter = adapter ? `${appName}.${adapter}` : appName;
|
|
906
900
|
adapter = adapter.toLowerCase();
|
|
907
901
|
const cliCommand = `npm view ${adapter}@latest version`;
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
return;
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
else if (stdout) {
|
|
918
|
-
version = semver.valid(stdout.trim());
|
|
919
|
-
}
|
|
920
|
-
if (typeof callback === 'function') {
|
|
921
|
-
callback(null, version);
|
|
922
|
-
}
|
|
923
|
-
});
|
|
902
|
+
const result = await execAsync(cliCommand, { timeout: 2000, windowsHide: true });
|
|
903
|
+
let version;
|
|
904
|
+
if (result?.stdout) {
|
|
905
|
+
version = semver.valid(result.stdout.trim());
|
|
906
|
+
}
|
|
907
|
+
return version || '';
|
|
924
908
|
}
|
|
925
|
-
function getIoPack(sources, name,
|
|
926
|
-
|
|
927
|
-
|
|
909
|
+
async function getIoPack(sources, name, failCounter) {
|
|
910
|
+
let packSource = sources[name];
|
|
911
|
+
try {
|
|
912
|
+
const ioPack = await getJson(packSource.meta);
|
|
913
|
+
const packUrl = packSource.meta.replace('io-package.json', 'package.json');
|
|
928
914
|
if (!ioPack) {
|
|
929
|
-
|
|
930
|
-
|
|
915
|
+
failCounter.push(name);
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
const pack = await getJson(packUrl);
|
|
919
|
+
const version = packSource.version;
|
|
920
|
+
const type = packSource.type;
|
|
921
|
+
// If installed from git or something else,
|
|
922
|
+
// js-controller is exception, because can be installed from npm and from git
|
|
923
|
+
if (packSource.url && name !== 'js-controller') {
|
|
924
|
+
if (ioPack?.common) {
|
|
925
|
+
sources[name] = extend(true, packSource, ioPack.common);
|
|
926
|
+
packSource = sources[name];
|
|
927
|
+
// overwrite type of adapter from repository
|
|
928
|
+
if (type) {
|
|
929
|
+
packSource.type = type;
|
|
930
|
+
}
|
|
931
|
+
if (pack?.licenses?.length) {
|
|
932
|
+
packSource.license ||= pack.licenses[0].type;
|
|
933
|
+
packSource.licenseUrl ||= pack.licenses[0].url;
|
|
934
|
+
}
|
|
931
935
|
}
|
|
932
|
-
|
|
933
|
-
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
if (ioPack?.common) {
|
|
939
|
+
sources[name] = extend(true, packSource, ioPack.common);
|
|
940
|
+
packSource = sources[name];
|
|
941
|
+
if (pack?.licenses?.length) {
|
|
942
|
+
packSource.license ||= pack.licenses[0].type;
|
|
943
|
+
packSource.licenseUrl ||= pack.licenses[0].url;
|
|
934
944
|
}
|
|
935
945
|
}
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
const version = sources[name].version;
|
|
940
|
-
const type = sources[name].type;
|
|
941
|
-
// If installed from git or something else,
|
|
942
|
-
// js-controller is exception, because can be installed from npm and from git
|
|
943
|
-
if (sources[name].url && name !== 'js-controller') {
|
|
944
|
-
if (ioPack && ioPack.common) {
|
|
945
|
-
sources[name] = extend(true, sources[name], ioPack.common);
|
|
946
|
-
// overwrite type of adapter from repository
|
|
947
|
-
if (type) {
|
|
948
|
-
sources[name].type = type;
|
|
949
|
-
}
|
|
950
|
-
if (pack && pack.licenses && pack.licenses.length) {
|
|
951
|
-
if (!sources[name].license) {
|
|
952
|
-
sources[name].license = pack.licenses[0].type;
|
|
953
|
-
}
|
|
954
|
-
if (!sources[name].licenseUrl) {
|
|
955
|
-
sources[name].licenseUrl = pack.licenses[0].url;
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
if (callback) {
|
|
960
|
-
callback(sources, name);
|
|
961
|
-
}
|
|
962
|
-
}
|
|
963
|
-
else {
|
|
964
|
-
if (ioPack && ioPack.common) {
|
|
965
|
-
sources[name] = extend(true, sources[name], ioPack.common);
|
|
966
|
-
if (pack && pack.licenses && pack.licenses.length) {
|
|
967
|
-
if (!sources[name].license) {
|
|
968
|
-
sources[name].license = pack.licenses[0].type;
|
|
969
|
-
}
|
|
970
|
-
if (!sources[name].licenseUrl) {
|
|
971
|
-
sources[name].licenseUrl = pack.licenses[0].url;
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
// overwrite type of adapter from repository
|
|
976
|
-
if (type) {
|
|
977
|
-
sources[name].type = type;
|
|
978
|
-
}
|
|
979
|
-
if (version) {
|
|
980
|
-
sources[name].version = version;
|
|
981
|
-
if (callback) {
|
|
982
|
-
callback(sources, name);
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
else {
|
|
986
|
-
if (sources[name].meta.substring(0, 'http://'.length) === 'http://' ||
|
|
987
|
-
sources[name].meta.substring(0, 'https://'.length) === 'https://') {
|
|
988
|
-
//installed from npm
|
|
989
|
-
getNpmVersion(name, (_err, version) => {
|
|
990
|
-
if (version) {
|
|
991
|
-
sources[name].version = version;
|
|
992
|
-
}
|
|
993
|
-
else {
|
|
994
|
-
sources[name].version = 'npm error';
|
|
995
|
-
}
|
|
996
|
-
if (callback) {
|
|
997
|
-
callback(sources, name);
|
|
998
|
-
}
|
|
999
|
-
});
|
|
1000
|
-
}
|
|
1001
|
-
else {
|
|
1002
|
-
if (callback) {
|
|
1003
|
-
callback(sources, name);
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
});
|
|
1009
|
-
});
|
|
946
|
+
// overwrite type of adapter from repository
|
|
947
|
+
if (type) {
|
|
948
|
+
packSource.type = type;
|
|
1010
949
|
}
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
if (!sources._helper) {
|
|
1015
|
-
let count = 0;
|
|
1016
|
-
for (const _name in sources) {
|
|
1017
|
-
if (!Object.prototype.hasOwnProperty.call(sources, _name)) {
|
|
1018
|
-
continue;
|
|
1019
|
-
}
|
|
1020
|
-
count++;
|
|
950
|
+
if (version) {
|
|
951
|
+
packSource.version = version;
|
|
952
|
+
return;
|
|
1021
953
|
}
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
}
|
|
954
|
+
if (packSource.meta.substring(0, 'http://'.length) === 'http://' ||
|
|
955
|
+
packSource.meta.substring(0, 'https://'.length) === 'https://') {
|
|
956
|
+
// installed from npm
|
|
957
|
+
try {
|
|
958
|
+
const version = await getNpmVersion(name);
|
|
959
|
+
if (version) {
|
|
960
|
+
packSource.version = version;
|
|
1030
961
|
}
|
|
1031
|
-
|
|
1032
|
-
|
|
962
|
+
else {
|
|
963
|
+
packSource.version = 'npm error';
|
|
1033
964
|
}
|
|
1034
|
-
callback = undefined;
|
|
1035
965
|
}
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
for (const name of Object.keys(sources)) {
|
|
1039
|
-
if (sources[name].processed || name === '_helper') {
|
|
1040
|
-
continue;
|
|
1041
|
-
}
|
|
1042
|
-
sources[name].processed = true;
|
|
1043
|
-
if (sources[name].url) {
|
|
1044
|
-
sources[name].url = findPath(path, sources[name].url);
|
|
1045
|
-
}
|
|
1046
|
-
if (sources[name].meta) {
|
|
1047
|
-
sources[name].meta = findPath(path, sources[name].meta);
|
|
1048
|
-
}
|
|
1049
|
-
if (sources[name].icon) {
|
|
1050
|
-
sources[name].icon = findPath(path, sources[name].icon);
|
|
1051
|
-
}
|
|
1052
|
-
if (!sources[name].name && sources[name].meta) {
|
|
1053
|
-
getIoPack(sources, name, _ignore => {
|
|
1054
|
-
if (sources._helper) {
|
|
1055
|
-
if (sources._helper.failCounter.length > 10) {
|
|
1056
|
-
clearTimeout(sources._helper.timeout);
|
|
1057
|
-
delete sources._helper;
|
|
1058
|
-
for (const _name of Object.keys(sources)) {
|
|
1059
|
-
if (sources[_name].processed !== undefined) {
|
|
1060
|
-
delete sources[_name].processed;
|
|
1061
|
-
}
|
|
1062
|
-
}
|
|
1063
|
-
if (callback) {
|
|
1064
|
-
callback(new Error('Looks like there is no internet.'), sources);
|
|
1065
|
-
}
|
|
1066
|
-
callback = undefined;
|
|
1067
|
-
}
|
|
1068
|
-
else {
|
|
1069
|
-
// process next
|
|
1070
|
-
setImmediate(() => _getRepositoryFile(sources, path, callback));
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
});
|
|
1074
|
-
return;
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
// all packages are processed
|
|
1078
|
-
if (sources._helper) {
|
|
1079
|
-
let err;
|
|
1080
|
-
if (sources._helper.failCounter.length) {
|
|
1081
|
-
err = new Error(`Following packages cannot be read: ${sources._helper.failCounter.join(', ')}`);
|
|
1082
|
-
}
|
|
1083
|
-
clearTimeout(sources._helper.timeout);
|
|
1084
|
-
delete sources._helper;
|
|
1085
|
-
for (const __name of Object.keys(sources)) {
|
|
1086
|
-
if (sources[__name].processed !== undefined) {
|
|
1087
|
-
delete sources[__name].processed;
|
|
966
|
+
catch {
|
|
967
|
+
packSource.version = 'npm error';
|
|
1088
968
|
}
|
|
1089
969
|
}
|
|
1090
|
-
if (callback) {
|
|
1091
|
-
callback(err, sources);
|
|
1092
|
-
}
|
|
1093
|
-
callback = undefined;
|
|
1094
970
|
}
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
971
|
+
catch (e) {
|
|
972
|
+
console.error(`Cannot prepare repository entry for ${name}: ${e.message}`);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
async function _getRepositoryFile(sources, path = '') {
|
|
976
|
+
const names = Object.keys(sources).filter(name => name !== '_helper');
|
|
977
|
+
const failCounter = [];
|
|
978
|
+
let aborted = false;
|
|
979
|
+
// Hard time limit: one second per repository entry. Raced against the processing so a hanging
|
|
980
|
+
// network request cannot block forever.
|
|
981
|
+
let timeout;
|
|
982
|
+
const timeoutPromise = new Promise(resolve => {
|
|
983
|
+
timeout = setTimeout(() => {
|
|
984
|
+
aborted = true;
|
|
985
|
+
resolve(new Error(`Timeout by read all package.json (${names.length}) seconds`));
|
|
986
|
+
}, names.length * 10_000);
|
|
987
|
+
});
|
|
988
|
+
const processPromise = (async () => {
|
|
989
|
+
for (const name of names) {
|
|
990
|
+
if (aborted) {
|
|
991
|
+
// the timeout already won the race; this result is discarded
|
|
992
|
+
return null;
|
|
1105
993
|
}
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
console.warn(`Cannot download json from ${urlOrPath}. Error: ${e.message}`);
|
|
1110
|
-
}
|
|
1111
|
-
if (json && json.hash) {
|
|
1112
|
-
// The hash download was successful
|
|
1113
|
-
if (additionalInfo && additionalInfo.sources && json.hash === additionalInfo.hash) {
|
|
1114
|
-
// The hash is the same as for the cached sources
|
|
1115
|
-
console.log('hash unchanged, use cached sources');
|
|
1116
|
-
callback(null, additionalInfo.sources, json.hash);
|
|
994
|
+
const source = sources[name];
|
|
995
|
+
if (source.url) {
|
|
996
|
+
source.url = findPath(path, source.url);
|
|
1117
997
|
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
998
|
+
if (source.meta) {
|
|
999
|
+
source.meta = findPath(path, source.meta);
|
|
1000
|
+
}
|
|
1001
|
+
if (source.icon) {
|
|
1002
|
+
source.icon = findPath(path, source.icon);
|
|
1003
|
+
}
|
|
1004
|
+
if (!source.name && source.meta) {
|
|
1005
|
+
await getIoPack(sources, name, failCounter);
|
|
1006
|
+
if (failCounter.length > 10) {
|
|
1007
|
+
return new Error('Looks like there is no internet.');
|
|
1008
|
+
}
|
|
1123
1009
|
}
|
|
1124
1010
|
}
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
}
|
|
1011
|
+
return failCounter.length ? new Error(`Following packages cannot be read: ${failCounter.join(', ')}`) : null;
|
|
1012
|
+
})();
|
|
1013
|
+
try {
|
|
1014
|
+
return await Promise.race([processPromise, timeoutPromise]);
|
|
1130
1015
|
}
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
callback(null, null, 0);
|
|
1016
|
+
finally {
|
|
1017
|
+
clearTimeout(timeout);
|
|
1134
1018
|
}
|
|
1135
1019
|
}
|
|
1136
1020
|
/**
|
|
1137
1021
|
* Get a list of all adapters and controller in some repository file or in /conf/source-dist.json
|
|
1138
|
-
*
|
|
1139
|
-
* @param urlOrPath URL starting with http:// or https:// or local file link
|
|
1140
|
-
* @param additionalInfo destination object
|
|
1141
|
-
* @param callback function (err, sources, actualHash) { }
|
|
1142
1022
|
*/
|
|
1143
|
-
export function getRepositoryFile(
|
|
1023
|
+
export async function getRepositoryFile() {
|
|
1144
1024
|
let sources = {};
|
|
1145
|
-
let
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
}
|
|
1151
|
-
additionalInfo = additionalInfo || {};
|
|
1152
|
-
if (urlOrPath) {
|
|
1153
|
-
const parts = urlOrPath.split('/');
|
|
1154
|
-
_path = `${parts.splice(0, parts.length - 1).join('/')}/`;
|
|
1155
|
-
}
|
|
1156
|
-
// If an object was read
|
|
1157
|
-
if (urlOrPath && typeof urlOrPath === 'object') {
|
|
1158
|
-
if (typeof callback === 'function') {
|
|
1159
|
-
callback(null, urlOrPath);
|
|
1025
|
+
let controllerDir;
|
|
1026
|
+
try {
|
|
1027
|
+
controllerDir = getControllerDir();
|
|
1028
|
+
if (controllerDir) {
|
|
1029
|
+
sources = fs.readJSONSync(path.join(controllerDir, getDefaultDataDir(), 'sources.json'));
|
|
1160
1030
|
}
|
|
1161
1031
|
}
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
const controllerDir = getControllerDir();
|
|
1165
|
-
if (controllerDir) {
|
|
1166
|
-
sources = fs.readJSONSync(path.join(controllerDir, getDefaultDataDir(), 'sources.json'));
|
|
1167
|
-
}
|
|
1168
|
-
}
|
|
1169
|
-
catch {
|
|
1170
|
-
sources = {};
|
|
1171
|
-
}
|
|
1172
|
-
try {
|
|
1173
|
-
const controllerDir = getControllerDir();
|
|
1174
|
-
if (controllerDir) {
|
|
1175
|
-
const sourcesDist = fs.readJSONSync(path.join(controllerDir, 'conf', 'sources-dist.json'));
|
|
1176
|
-
sources = extend(true, sourcesDist, sources);
|
|
1177
|
-
}
|
|
1178
|
-
}
|
|
1179
|
-
catch {
|
|
1180
|
-
// continue regardless of error
|
|
1181
|
-
}
|
|
1182
|
-
for (const s of Object.keys(sources)) {
|
|
1183
|
-
if (additionalInfo[s] && additionalInfo[s].published) {
|
|
1184
|
-
sources[s].published = additionalInfo[s].published;
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
_getRepositoryFile(sources, _path, err => {
|
|
1188
|
-
if (err) {
|
|
1189
|
-
console.error(`[${new Date().toString()}] ${err.message}`);
|
|
1190
|
-
}
|
|
1191
|
-
if (typeof callback === 'function') {
|
|
1192
|
-
callback(err, sources);
|
|
1193
|
-
}
|
|
1194
|
-
});
|
|
1032
|
+
catch {
|
|
1033
|
+
sources = {};
|
|
1195
1034
|
}
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
agent = `${additionalInfo.name}, RND: ${additionalInfo.randomID || randomID}, Node:${additionalInfo.node}, V:${additionalInfo.controller}`;
|
|
1035
|
+
try {
|
|
1036
|
+
if (controllerDir) {
|
|
1037
|
+
const sourcesDist = fs.readJSONSync(path.join(controllerDir, 'conf', 'sources-dist.json'));
|
|
1038
|
+
sources = extend(true, sourcesDist, sources);
|
|
1201
1039
|
}
|
|
1202
|
-
// load hash of file first to not load the whole 1MB of sources
|
|
1203
|
-
_checkRepositoryFileHash(urlOrPath, additionalInfo, (err, sources, actualSourcesHash) => {
|
|
1204
|
-
if (!err && sources) {
|
|
1205
|
-
// Source file was not changed
|
|
1206
|
-
typeof callback === 'function' && callback(err, sources, actualSourcesHash);
|
|
1207
|
-
}
|
|
1208
|
-
else {
|
|
1209
|
-
getJson(urlOrPath, agent, sources => {
|
|
1210
|
-
if (sources) {
|
|
1211
|
-
for (const s of Object.keys(sources)) {
|
|
1212
|
-
if (additionalInfo[s] && additionalInfo[s].published) {
|
|
1213
|
-
sources[s].published = additionalInfo[s].published;
|
|
1214
|
-
}
|
|
1215
|
-
}
|
|
1216
|
-
setImmediate(() => _getRepositoryFile(sources, _path, err => {
|
|
1217
|
-
err && console.error(`[${new Date().toString()}] ${err.message}`);
|
|
1218
|
-
typeof callback === 'function' && callback(err, sources, actualSourcesHash);
|
|
1219
|
-
}));
|
|
1220
|
-
}
|
|
1221
|
-
else {
|
|
1222
|
-
// return cached sources, because no sources found
|
|
1223
|
-
console.log(`failed to download new sources, ${additionalInfo.sources ? 'use cached sources' : 'no cached sources available'}`);
|
|
1224
|
-
return maybeCallbackWithError(callback, `Cannot read "${urlOrPath}"`, additionalInfo.sources, '');
|
|
1225
|
-
}
|
|
1226
|
-
});
|
|
1227
|
-
}
|
|
1228
|
-
});
|
|
1229
1040
|
}
|
|
1041
|
+
catch {
|
|
1042
|
+
// continue regardless of error
|
|
1043
|
+
}
|
|
1044
|
+
const error = await _getRepositoryFile(sources);
|
|
1045
|
+
if (error) {
|
|
1046
|
+
console.error(`[${new Date().toString()}] ${error.message}`);
|
|
1047
|
+
}
|
|
1048
|
+
if (!error || Object.keys(sources).length > 0) {
|
|
1049
|
+
return sources;
|
|
1050
|
+
}
|
|
1051
|
+
throw error;
|
|
1230
1052
|
}
|
|
1231
1053
|
/**
|
|
1232
1054
|
* Read on repository
|
|
@@ -1237,28 +1059,31 @@ export function getRepositoryFile(urlOrPath, additionalInfo, callback) {
|
|
|
1237
1059
|
* @param _actualRepo Actual repository JSON content
|
|
1238
1060
|
*/
|
|
1239
1061
|
export async function getRepositoryFileAsync(url, hash, force, _actualRepo) {
|
|
1240
|
-
let
|
|
1241
|
-
let data;
|
|
1062
|
+
let hashResponse;
|
|
1063
|
+
let data = null;
|
|
1242
1064
|
if (url.startsWith('http://') || url.startsWith('https://')) {
|
|
1243
1065
|
try {
|
|
1244
|
-
|
|
1066
|
+
hashResponse = await axios({
|
|
1067
|
+
url: url.replace(/\.json$/, `-${HASH_TEXT}.json`),
|
|
1068
|
+
timeout: 10_000,
|
|
1069
|
+
});
|
|
1245
1070
|
}
|
|
1246
1071
|
catch {
|
|
1247
1072
|
// ignore missing hash file
|
|
1248
1073
|
}
|
|
1249
1074
|
// If we have the actual repo and the hash matches, return it
|
|
1250
|
-
if (_actualRepo && !force && hash &&
|
|
1075
|
+
if (_actualRepo && !force && hash && hashResponse?.data?.hash === hash) {
|
|
1251
1076
|
data = _actualRepo;
|
|
1252
1077
|
}
|
|
1253
1078
|
else {
|
|
1254
1079
|
const agent = `${appName}, RND: ${randomID}, Node:${process.version}, V:${require('@iobroker/js-controller-common/package.json').version}`;
|
|
1255
1080
|
try {
|
|
1256
|
-
|
|
1081
|
+
const response = await axios({
|
|
1257
1082
|
url,
|
|
1258
1083
|
timeout: 10_000,
|
|
1259
1084
|
headers: { 'User-Agent': agent },
|
|
1260
1085
|
});
|
|
1261
|
-
data =
|
|
1086
|
+
data = response.data;
|
|
1262
1087
|
}
|
|
1263
1088
|
catch (e) {
|
|
1264
1089
|
throw new Error(`Cannot download repository file from "${url}": ${e.message}`);
|
|
@@ -1280,8 +1105,8 @@ export async function getRepositoryFileAsync(url, hash, force, _actualRepo) {
|
|
|
1280
1105
|
}
|
|
1281
1106
|
return {
|
|
1282
1107
|
json: data,
|
|
1283
|
-
changed:
|
|
1284
|
-
hash:
|
|
1108
|
+
changed: hashResponse?.data ? hash !== hashResponse.data.hash : true,
|
|
1109
|
+
hash: hashResponse?.data ? hashResponse.data.hash : '',
|
|
1285
1110
|
};
|
|
1286
1111
|
}
|
|
1287
1112
|
/**
|
|
@@ -1323,7 +1148,7 @@ export function getAdapterDir(adapter) {
|
|
|
1323
1148
|
const possibilities = [`${appName.toLowerCase()}.${adapter}/package.json`, `${appName}.${adapter}/package.json`];
|
|
1324
1149
|
let adapterPath;
|
|
1325
1150
|
for (const possibility of possibilities) {
|
|
1326
|
-
// special case to
|
|
1151
|
+
// special case to didn't read adapters from js-controller/node_module/adapter and check first in parent directory
|
|
1327
1152
|
if (fs.existsSync(path.join(getControllerDir(), '..', possibility))) {
|
|
1328
1153
|
adapterPath = path.join(getControllerDir(), '..', possibility);
|
|
1329
1154
|
}
|
|
@@ -1366,59 +1191,28 @@ export function getHostName() {
|
|
|
1366
1191
|
}
|
|
1367
1192
|
}
|
|
1368
1193
|
/**
|
|
1369
|
-
* Read version of system
|
|
1194
|
+
* Read version of system NPM
|
|
1370
1195
|
*
|
|
1371
|
-
* @
|
|
1372
|
-
* <pre><code>
|
|
1373
|
-
* function (err, version) {
|
|
1374
|
-
* adapter.log.debug('NPM version is: ' + version);
|
|
1375
|
-
* }
|
|
1376
|
-
* </code></pre>
|
|
1196
|
+
* @returns NPM version
|
|
1377
1197
|
*/
|
|
1378
|
-
function getSystemNpmVersion(
|
|
1379
|
-
|
|
1380
|
-
// remove local node_modules\.bin dir from a path
|
|
1198
|
+
async function getSystemNpmVersion() {
|
|
1199
|
+
// remove local node_modules\.bin dir from a path,
|
|
1381
1200
|
// or we potentially get a wrong npm version
|
|
1382
1201
|
const newEnv = Object.assign({}, process.env);
|
|
1383
|
-
|
|
1384
|
-
newEnv.PATH = (newEnv.PATH || newEnv.Path || newEnv.path)
|
|
1202
|
+
newEnv.PATH = (newEnv.PATH || newEnv.Path || newEnv.path || '')
|
|
1385
1203
|
.split(path.delimiter)
|
|
1386
1204
|
.filter(dir => {
|
|
1387
1205
|
dir = dir.toLowerCase();
|
|
1388
1206
|
return !dir.includes('iobroker') || !dir.includes(path.join('node_modules', '.bin'));
|
|
1389
1207
|
})
|
|
1390
1208
|
.join(path.delimiter);
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
callback(new Error('timeout'));
|
|
1396
|
-
callback = undefined;
|
|
1397
|
-
}
|
|
1398
|
-
}, 10_000);
|
|
1399
|
-
exec('npm -v', { encoding: 'utf8', env: newEnv, windowsHide: true }, (error, stdout) => {
|
|
1400
|
-
//, stderr) {
|
|
1401
|
-
if (timeout) {
|
|
1402
|
-
clearTimeout(timeout);
|
|
1403
|
-
timeout = null;
|
|
1404
|
-
}
|
|
1405
|
-
if (stdout) {
|
|
1406
|
-
stdout = semver.valid(stdout.trim());
|
|
1407
|
-
}
|
|
1408
|
-
if (callback) {
|
|
1409
|
-
callback(error, stdout);
|
|
1410
|
-
callback = undefined;
|
|
1411
|
-
}
|
|
1412
|
-
});
|
|
1413
|
-
}
|
|
1414
|
-
catch (e) {
|
|
1415
|
-
if (callback) {
|
|
1416
|
-
callback(e);
|
|
1417
|
-
callback = undefined;
|
|
1418
|
-
}
|
|
1209
|
+
const result = await execAsync('npm -v', { env: newEnv, timeout: 10_000 });
|
|
1210
|
+
let version;
|
|
1211
|
+
if (result.stdout) {
|
|
1212
|
+
version = semver.valid(result.stdout.trim());
|
|
1419
1213
|
}
|
|
1214
|
+
return version || null;
|
|
1420
1215
|
}
|
|
1421
|
-
const getSystemNpmVersionAsync = promisify(getSystemNpmVersion);
|
|
1422
1216
|
/**
|
|
1423
1217
|
*
|
|
1424
1218
|
* Figure out which package manager is in charge, but with a fallback to npm.
|
|
@@ -1535,7 +1329,7 @@ export async function rebuildNodeModules(options = {}) {
|
|
|
1535
1329
|
return pak.rebuild(options.module ? [options.module] : undefined);
|
|
1536
1330
|
}
|
|
1537
1331
|
/**
|
|
1538
|
-
* Read disk
|
|
1332
|
+
* Read the disk-free space
|
|
1539
1333
|
*/
|
|
1540
1334
|
export async function getDiskInfo() {
|
|
1541
1335
|
const platform = process.platform;
|
|
@@ -1598,7 +1392,7 @@ export async function getDiskInfo() {
|
|
|
1598
1392
|
/**
|
|
1599
1393
|
* Returns information about a certificate
|
|
1600
1394
|
*
|
|
1601
|
-
* @param cert
|
|
1395
|
+
* @param cert The certificate as a PEM string, or a path to the certificate file
|
|
1602
1396
|
* @returns certificate information object
|
|
1603
1397
|
*/
|
|
1604
1398
|
export function getCertificateInfo(cert) {
|
|
@@ -1735,7 +1529,7 @@ function makeid(length) {
|
|
|
1735
1529
|
/**
|
|
1736
1530
|
* Collects information about host and available adapters
|
|
1737
1531
|
*
|
|
1738
|
-
*
|
|
1532
|
+
* The following info will be collected:
|
|
1739
1533
|
* - available adapters
|
|
1740
1534
|
* - node.js --version
|
|
1741
1535
|
* - npm --version
|
|
@@ -1760,12 +1554,12 @@ export async function getHostInfo(objects) {
|
|
|
1760
1554
|
? systemConfig.common.activeRepo
|
|
1761
1555
|
: [systemConfig.common.activeRepo];
|
|
1762
1556
|
repos
|
|
1763
|
-
.filter(repo => systemRepos.native.repositories[repo]
|
|
1557
|
+
.filter(repo => systemRepos.native.repositories[repo]?.json)
|
|
1764
1558
|
.forEach(repo => Object.assign(allRepos, systemRepos.native.repositories[repo].json));
|
|
1765
1559
|
}
|
|
1766
1560
|
if (!npmVersion) {
|
|
1767
1561
|
try {
|
|
1768
|
-
npmVersion = await
|
|
1562
|
+
npmVersion = (await getSystemNpmVersion()) || '';
|
|
1769
1563
|
}
|
|
1770
1564
|
catch (e) {
|
|
1771
1565
|
console.error(`Cannot get NPM version: ${e.message}`);
|
|
@@ -1786,7 +1580,7 @@ export async function getHostInfo(objects) {
|
|
|
1786
1580
|
time: dateObj.getTime(),
|
|
1787
1581
|
timeOffset: dateObj.getTimezoneOffset(),
|
|
1788
1582
|
NPM: npmVersion,
|
|
1789
|
-
'adapters count': Object.keys(allRepos).length,
|
|
1583
|
+
'adapters count': Object.keys(allRepos).filter(id => id !== '_repoInfo').length,
|
|
1790
1584
|
};
|
|
1791
1585
|
if (data.Platform === 'win32') {
|
|
1792
1586
|
data.Platform = 'Windows';
|
|
@@ -1817,8 +1611,8 @@ export function getControllerDir() {
|
|
|
1817
1611
|
const possibilities = ['iobroker.js-controller', 'ioBroker.js-controller'];
|
|
1818
1612
|
for (const pkg of possibilities) {
|
|
1819
1613
|
try {
|
|
1820
|
-
// package.json is guaranteed to be in the module root folder
|
|
1821
|
-
// so once that is resolved, take the dirname and we're done
|
|
1614
|
+
// package.json is guaranteed to be in the module root folder,
|
|
1615
|
+
// so once that is resolved, take the dirname, and we're done
|
|
1822
1616
|
const possiblePath = require.resolve(`${pkg}/package.json`);
|
|
1823
1617
|
if (fs.existsSync(possiblePath)) {
|
|
1824
1618
|
return path.dirname(possiblePath);
|
|
@@ -1865,7 +1659,7 @@ export function isDevServerInstallation() {
|
|
|
1865
1659
|
}
|
|
1866
1660
|
/**
|
|
1867
1661
|
* All paths are returned always relative to /node_modules/' + appName + '.js-controller
|
|
1868
|
-
* the result has always "/" as last symbol
|
|
1662
|
+
* the result has always "/" as the last symbol
|
|
1869
1663
|
*/
|
|
1870
1664
|
export function getDefaultDataDir() {
|
|
1871
1665
|
// Allow overriding the data directory with an environment variable
|
|
@@ -2045,6 +1839,14 @@ async function _setQualityForStates(states, keys, quality) {
|
|
|
2045
1839
|
}
|
|
2046
1840
|
}
|
|
2047
1841
|
}
|
|
1842
|
+
/**
|
|
1843
|
+
* Set the given quality code on all states belonging to an instance
|
|
1844
|
+
*
|
|
1845
|
+
* @param objects The objects database client
|
|
1846
|
+
* @param states The states database client
|
|
1847
|
+
* @param namespace The instance namespace whose states should be updated (e.g. "admin.0")
|
|
1848
|
+
* @param q The quality code to assign to each state
|
|
1849
|
+
*/
|
|
2048
1850
|
export function setQualityForInstance(objects, states, namespace, q) {
|
|
2049
1851
|
return new Promise((resolve, reject) => {
|
|
2050
1852
|
objects.getObjectView('system', 'state', {
|
|
@@ -2069,7 +1871,7 @@ export function setQualityForInstance(objects, states, namespace, q) {
|
|
|
2069
1871
|
}
|
|
2070
1872
|
// read all values for IDs
|
|
2071
1873
|
states.getStates(keys, async (_err, values) => {
|
|
2072
|
-
// Get only states
|
|
1874
|
+
// Get only states that have ack = true
|
|
2073
1875
|
keys = keys.filter((_id, i) => values[i] && values[i].ack);
|
|
2074
1876
|
// update quality code of the states to new one
|
|
2075
1877
|
await _setQualityForStates(states, keys, q);
|
|
@@ -2097,8 +1899,7 @@ export function pattern2RegEx(pattern) {
|
|
|
2097
1899
|
/**
|
|
2098
1900
|
* Checks if a pattern is valid
|
|
2099
1901
|
*
|
|
2100
|
-
* @param pattern
|
|
2101
|
-
* @pattern pattern to check for validity
|
|
1902
|
+
* @param pattern The pattern to check for validity
|
|
2102
1903
|
*/
|
|
2103
1904
|
export function isValidPattern(pattern) {
|
|
2104
1905
|
pattern = pattern.replace(/\*/g, '');
|
|
@@ -2179,7 +1980,7 @@ function decryptLegacy(key, value) {
|
|
|
2179
1980
|
*/
|
|
2180
1981
|
export function encrypt(key, value) {
|
|
2181
1982
|
if (!/^[0-9a-f]{48}$/.test(key)) {
|
|
2182
|
-
// key length is not matching for AES-192-CBC or key is no valid hex - fallback to old encryption
|
|
1983
|
+
// key length is not matching for AES-192-CBC, or key is no valid hex - fallback to old encryption
|
|
2183
1984
|
return encryptLegacy(key, value);
|
|
2184
1985
|
}
|
|
2185
1986
|
const iv = crypto.randomBytes(16);
|
|
@@ -2248,7 +2049,7 @@ export function measureEventLoopLag(ms, cb) {
|
|
|
2248
2049
|
// we use Math.max to handle a case where timers are running efficiently
|
|
2249
2050
|
// and our callback executes earlier than `ms` due to how timers are
|
|
2250
2051
|
// implemented. this is ok. it means we're healthy.
|
|
2251
|
-
cb
|
|
2052
|
+
cb?.(Math.max(0, t - start - ms));
|
|
2252
2053
|
start = t;
|
|
2253
2054
|
timeout = setTimeout(check, ms);
|
|
2254
2055
|
timeout.unref();
|
|
@@ -2259,10 +2060,10 @@ export function measureEventLoopLag(ms, cb) {
|
|
|
2259
2060
|
}
|
|
2260
2061
|
}
|
|
2261
2062
|
/**
|
|
2262
|
-
* This function
|
|
2063
|
+
* This function converts state values by read and write of aliases. Function is synchronous.
|
|
2263
2064
|
* On errors, null is returned instead
|
|
2264
2065
|
*
|
|
2265
|
-
* @param options
|
|
2066
|
+
* @param options Source and target common/id definitions, the state to convert and a logger
|
|
2266
2067
|
*/
|
|
2267
2068
|
export function formatAliasValue(options) {
|
|
2268
2069
|
const { sourceCommon, sourceId, targetCommon, targetId, state, logger } = options;
|
|
@@ -2325,9 +2126,7 @@ export function formatAliasValue(options) {
|
|
|
2325
2126
|
* @returns Promise All objects are tried to be updated - reject will happen as soon as one fails with the error of the first fail
|
|
2326
2127
|
*/
|
|
2327
2128
|
export async function removeIdFromAllEnums(objects, id, allEnums) {
|
|
2328
|
-
|
|
2329
|
-
allEnums = await getAllEnums(objects);
|
|
2330
|
-
}
|
|
2129
|
+
allEnums ||= await getAllEnums(objects);
|
|
2331
2130
|
let error = null;
|
|
2332
2131
|
for (const [enumId, enumObj] of Object.entries(allEnums)) {
|
|
2333
2132
|
const idx = enumObj.common.members ? enumObj.common.members.indexOf(id) : -1;
|
|
@@ -2340,9 +2139,7 @@ export async function removeIdFromAllEnums(objects, id, allEnums) {
|
|
|
2340
2139
|
allEnums[enumId] = enumObj;
|
|
2341
2140
|
}
|
|
2342
2141
|
catch (err) {
|
|
2343
|
-
|
|
2344
|
-
error = err;
|
|
2345
|
-
}
|
|
2142
|
+
error ||= err;
|
|
2346
2143
|
}
|
|
2347
2144
|
}
|
|
2348
2145
|
}
|
|
@@ -2351,7 +2148,7 @@ export async function removeIdFromAllEnums(objects, id, allEnums) {
|
|
|
2351
2148
|
}
|
|
2352
2149
|
}
|
|
2353
2150
|
/**
|
|
2354
|
-
* Parses dependencies to standardized object of form
|
|
2151
|
+
* Parses dependencies to a standardized object of form
|
|
2355
2152
|
*
|
|
2356
2153
|
* @param dependencies dependencies array or single dependency
|
|
2357
2154
|
* @returns parsedDeps parsed dependencies
|
|
@@ -2388,7 +2185,7 @@ export function parseDependencies(dependencies) {
|
|
|
2388
2185
|
*
|
|
2389
2186
|
* @param obj an object which will be validated
|
|
2390
2187
|
* @param extend (optional) if true checks allow more optional cases for extendObject calls
|
|
2391
|
-
* @throws Error if a property has the wrong type or `obj.type` is non-existing
|
|
2188
|
+
* @throws {Error} if a property has the wrong type or `obj.type` is non-existing
|
|
2392
2189
|
*/
|
|
2393
2190
|
export function validateGeneralObjectProperties(obj, extend) {
|
|
2394
2191
|
if (!obj || (obj.type === undefined && !extend)) {
|
|
@@ -2463,12 +2260,13 @@ export function validateGeneralObjectProperties(obj, extend) {
|
|
|
2463
2260
|
throw new Error(`obj.common.min (${obj.common.min}) needs to be less than or equal to obj.common.max (${obj.common.max})`);
|
|
2464
2261
|
}
|
|
2465
2262
|
}
|
|
2466
|
-
// ensure
|
|
2467
|
-
if (obj.common.def
|
|
2263
|
+
// ensure that default value has correct type
|
|
2264
|
+
if (obj.common.def != null) {
|
|
2468
2265
|
// else do what strictObjectChecks does for val
|
|
2469
2266
|
if (!((obj.common.type === 'mixed' && typeof obj.common.def !== 'object') ||
|
|
2470
2267
|
(obj.common.type !== 'object' && obj.common.type === typeof obj.common.def) ||
|
|
2471
2268
|
(obj.common.type === 'array' && typeof obj.common.def === 'string') ||
|
|
2269
|
+
// @ts-expect-error deprecated
|
|
2472
2270
|
(obj.common.type === 'json' && typeof obj.common.def === 'string') ||
|
|
2473
2271
|
(obj.common.type === 'object' && typeof obj.common.def === 'string'))) {
|
|
2474
2272
|
// types can be 'number', 'string', 'boolean', 'array', 'object', 'mixed', 'json';
|
|
@@ -2485,10 +2283,12 @@ export function validateGeneralObjectProperties(obj, extend) {
|
|
|
2485
2283
|
}
|
|
2486
2284
|
}
|
|
2487
2285
|
}
|
|
2488
|
-
if (obj.common.read !== undefined &&
|
|
2286
|
+
if (obj.common.read !== undefined &&
|
|
2287
|
+
typeof obj.common.read !== 'boolean') {
|
|
2489
2288
|
throw new Error(`obj.common.read has an invalid type! Expected "boolean", received "${typeof obj.common.read}"`);
|
|
2490
2289
|
}
|
|
2491
|
-
if (obj.common.write !== undefined &&
|
|
2290
|
+
if (obj.common.write !== undefined &&
|
|
2291
|
+
typeof obj.common.write !== 'boolean') {
|
|
2492
2292
|
throw new Error(`obj.common.write has an invalid type! Expected "boolean", received "${typeof obj.common.write}"`);
|
|
2493
2293
|
}
|
|
2494
2294
|
if (obj.common.role !== undefined && typeof obj.common.role !== 'string') {
|
|
@@ -2497,15 +2297,11 @@ export function validateGeneralObjectProperties(obj, extend) {
|
|
|
2497
2297
|
if (obj.common.desc !== undefined && typeof obj.common.desc !== 'string' && typeof obj.common.desc !== 'object') {
|
|
2498
2298
|
throw new Error(`obj.common.desc has an invalid type! Expected "string" or "object", received "${typeof obj.common.desc}"`);
|
|
2499
2299
|
}
|
|
2500
|
-
if (obj.type === 'state' &&
|
|
2501
|
-
obj.common.custom !== undefined &&
|
|
2502
|
-
obj.common.custom !== null &&
|
|
2503
|
-
!isObject(obj.common.custom)) {
|
|
2300
|
+
if (obj.type === 'state' && obj.common.custom != null && !isObject(obj.common.custom)) {
|
|
2504
2301
|
throw new Error(`obj.common.custom has an invalid type! Expected "object", received "${typeof obj.common.custom}"`);
|
|
2505
2302
|
}
|
|
2506
2303
|
// common.states needs to be a real object or an array
|
|
2507
|
-
if (obj.common.states
|
|
2508
|
-
obj.common.states !== undefined &&
|
|
2304
|
+
if (obj.common.states != null && // we allow null for deletion TODO: implement https://github.com/ioBroker/ioBroker.js-controller/issues/1735
|
|
2509
2305
|
!isObject(obj.common.states) &&
|
|
2510
2306
|
!Array.isArray(obj.common.states)) {
|
|
2511
2307
|
throw new Error(`obj.common.states has an invalid type! Expected "object", received "${typeof obj.common.states}"`);
|
|
@@ -2532,10 +2328,8 @@ export async function getAllInstances(adapters, objects) {
|
|
|
2532
2328
|
}
|
|
2533
2329
|
}
|
|
2534
2330
|
}
|
|
2535
|
-
else {
|
|
2536
|
-
|
|
2537
|
-
instances.push(adapters[i]);
|
|
2538
|
-
}
|
|
2331
|
+
else if (!instances.includes(adapters[i])) {
|
|
2332
|
+
instances.push(adapters[i]);
|
|
2539
2333
|
}
|
|
2540
2334
|
}
|
|
2541
2335
|
return instances;
|
|
@@ -2589,7 +2383,7 @@ export async function getInstances(adapter, objects, withObjects) {
|
|
|
2589
2383
|
}
|
|
2590
2384
|
/**
|
|
2591
2385
|
* Executes a command asynchronously. On success, the promise resolves with stdout and stderr.
|
|
2592
|
-
*
|
|
2386
|
+
* In error, the promise rejects with the exit code or signal, as well as stdout and stderr.
|
|
2593
2387
|
*
|
|
2594
2388
|
* @param command The command to execute
|
|
2595
2389
|
* @param execOptions The options for child_process.exec
|
|
@@ -2602,7 +2396,59 @@ export function execAsync(command, execOptions) {
|
|
|
2602
2396
|
// And we want to capture stdout/stderr
|
|
2603
2397
|
encoding: 'utf8',
|
|
2604
2398
|
};
|
|
2605
|
-
return
|
|
2399
|
+
return new Promise((resolve, reject) => {
|
|
2400
|
+
exec(command, { ...defaultOptions, ...execOptions }, (error, stdout, stderr) => {
|
|
2401
|
+
if (error) {
|
|
2402
|
+
reject(stderr ? new Error(stderr.toString()) : error);
|
|
2403
|
+
}
|
|
2404
|
+
else {
|
|
2405
|
+
resolve({
|
|
2406
|
+
stderr: stderr?.toString(),
|
|
2407
|
+
stdout: stdout?.toString(),
|
|
2408
|
+
});
|
|
2409
|
+
}
|
|
2410
|
+
});
|
|
2411
|
+
});
|
|
2412
|
+
}
|
|
2413
|
+
/**
|
|
2414
|
+
* Executes a command asynchronously. On success, the promise resolves with stdout and stderr.
|
|
2415
|
+
*
|
|
2416
|
+
* On error it rejects with the error of `child_process.execFile`, whose `code` (the exit code, or `ENOENT` when
|
|
2417
|
+
* the command does not exist), `killed` and `signal` say what happened - a command killed by the `timeout`
|
|
2418
|
+
* option has to stay distinguishable from one that exited non-zero. Only the message is replaced by the output
|
|
2419
|
+
* on stderr when there is any, because that usually says more than "Command failed".
|
|
2420
|
+
*
|
|
2421
|
+
* @param file The command to execute
|
|
2422
|
+
* @param args The arguments to pass to the command
|
|
2423
|
+
* @param execOptions The options for child_process.execFile
|
|
2424
|
+
* @returns child process promise
|
|
2425
|
+
*/
|
|
2426
|
+
export function execFileAsync(file, args, execOptions) {
|
|
2427
|
+
const defaultOptions = {
|
|
2428
|
+
// we do not want to show the node.js window on Windows
|
|
2429
|
+
windowsHide: true,
|
|
2430
|
+
// And we want to capture stdout/stderr
|
|
2431
|
+
encoding: 'utf8',
|
|
2432
|
+
};
|
|
2433
|
+
return new Promise((resolve, reject) => {
|
|
2434
|
+
execFile(file, [...args], { ...defaultOptions, ...execOptions }, (error, stdout, stderr) => {
|
|
2435
|
+
if (error) {
|
|
2436
|
+
// the error of execFile is an Error carrying `code`, `killed` and `signal` - what the caller
|
|
2437
|
+
// needs to tell a missing command and a timeout apart from a plain non-zero exit
|
|
2438
|
+
const failure = error;
|
|
2439
|
+
if (stderr) {
|
|
2440
|
+
failure.message = stderr.toString();
|
|
2441
|
+
}
|
|
2442
|
+
reject(failure);
|
|
2443
|
+
}
|
|
2444
|
+
else {
|
|
2445
|
+
resolve({
|
|
2446
|
+
stderr: stderr?.toString(),
|
|
2447
|
+
stdout: stdout?.toString(),
|
|
2448
|
+
});
|
|
2449
|
+
}
|
|
2450
|
+
});
|
|
2451
|
+
});
|
|
2606
2452
|
}
|
|
2607
2453
|
/**
|
|
2608
2454
|
* Takes input from one stream and writes it to another as soon as a complete line was read.
|
|
@@ -2684,12 +2530,12 @@ export async function resolveAdapterMainFile(adapter) {
|
|
|
2684
2530
|
/**
|
|
2685
2531
|
* Returns the default nodeArgs required to execute the main file, e.g., transpile hooks for TypeScript
|
|
2686
2532
|
*
|
|
2687
|
-
* @param mainFile
|
|
2533
|
+
* @param mainFile Path to the adapter main file that will be executed
|
|
2688
2534
|
* @returns default node args for cli
|
|
2689
2535
|
*/
|
|
2690
2536
|
export function getDefaultNodeArgs(mainFile) {
|
|
2691
2537
|
const ret = [];
|
|
2692
|
-
// Support executing TypeScript files directly
|
|
2538
|
+
// Support for executing TypeScript files directly
|
|
2693
2539
|
if (mainFile.endsWith('.ts')) {
|
|
2694
2540
|
ret.push('-r', '@alcalzone/esbuild-register');
|
|
2695
2541
|
}
|
|
@@ -2789,7 +2635,7 @@ export function removePreservedProperties(preserve, oldObj, newObj) {
|
|
|
2789
2635
|
// array, only rm selected subattributes instead of whole attribute
|
|
2790
2636
|
for (const rmProp of preserve[prop]) {
|
|
2791
2637
|
if (oldObj[prop][rmProp] !== undefined && newObj[prop][rmProp] !== undefined) {
|
|
2792
|
-
// only delete if conflicting
|
|
2638
|
+
// only delete it if conflicting
|
|
2793
2639
|
delete newObj[prop][rmProp];
|
|
2794
2640
|
}
|
|
2795
2641
|
}
|
|
@@ -3005,6 +2851,11 @@ export function getInstanceIndicatorObjects(namespace, adapterCommon) {
|
|
|
3005
2851
|
},
|
|
3006
2852
|
];
|
|
3007
2853
|
}
|
|
2854
|
+
/**
|
|
2855
|
+
* Normalize a logger-like object into a complete logger, filling missing log level methods with no-ops
|
|
2856
|
+
*
|
|
2857
|
+
* @param log A logger or partial logger object; if falsy, a no-op logger is created
|
|
2858
|
+
*/
|
|
3008
2859
|
export function getLogger(log) {
|
|
3009
2860
|
if (!log) {
|
|
3010
2861
|
log = {
|
|
@@ -3044,7 +2895,7 @@ export async function setExecutableCapabilities(execPath, capabilities, modeEffe
|
|
|
3044
2895
|
if (os.platform() !== 'linux') {
|
|
3045
2896
|
return;
|
|
3046
2897
|
}
|
|
3047
|
-
// if Docker and Admin Capabilities should be set check if we are allowed to do that
|
|
2898
|
+
// if Docker and Admin Capabilities should be set, check if we are allowed to do that
|
|
3048
2899
|
if (isDocker() && capabilities.includes('cap_net_admin')) {
|
|
3049
2900
|
try {
|
|
3050
2901
|
const systemCaps = fs.readFileSync(`/proc/${process.pid}/status`, 'utf-8');
|
|
@@ -3132,12 +2983,12 @@ async function _readLicenses(login, password) {
|
|
|
3132
2983
|
export async function updateLicenses(objects, login, password) {
|
|
3133
2984
|
// if login and password provided in the message, just try to read without saving it in system.licenses
|
|
3134
2985
|
if (login && password) {
|
|
3135
|
-
return _readLicenses(login, password);
|
|
2986
|
+
return await _readLicenses(login, password);
|
|
3136
2987
|
}
|
|
3137
2988
|
// get actual object
|
|
3138
2989
|
const systemLicenses = await objects.getObjectAsync('system.licenses');
|
|
3139
2990
|
// If password and login exist
|
|
3140
|
-
if (systemLicenses
|
|
2991
|
+
if (systemLicenses?.native?.password && systemLicenses.native.login) {
|
|
3141
2992
|
try {
|
|
3142
2993
|
// get the secret to decode the password
|
|
3143
2994
|
const systemConfig = await objects.getObjectAsync('system.config');
|
|
@@ -3172,10 +3023,7 @@ export async function updateLicenses(objects, login, password) {
|
|
|
3172
3023
|
// if password is invalid
|
|
3173
3024
|
if (err.message.includes('Authentication required') || err.message.includes('Cannot decode password:')) {
|
|
3174
3025
|
// clear existing licenses if exist
|
|
3175
|
-
if (systemLicenses
|
|
3176
|
-
systemLicenses.native &&
|
|
3177
|
-
systemLicenses.native.licenses &&
|
|
3178
|
-
systemLicenses.native.licenses.length) {
|
|
3026
|
+
if (systemLicenses?.native?.licenses?.length) {
|
|
3179
3027
|
systemLicenses.native.licenses = [];
|
|
3180
3028
|
systemLicenses.native.readTime = new Date().toISOString();
|
|
3181
3029
|
await objects.setObjectAsync('system.licenses', systemLicenses);
|
|
@@ -3186,10 +3034,7 @@ export async function updateLicenses(objects, login, password) {
|
|
|
3186
3034
|
}
|
|
3187
3035
|
else {
|
|
3188
3036
|
// if password or login are empty => clear existing licenses if exist
|
|
3189
|
-
if (systemLicenses
|
|
3190
|
-
systemLicenses.native &&
|
|
3191
|
-
systemLicenses.native.licenses &&
|
|
3192
|
-
systemLicenses.native.licenses.length) {
|
|
3037
|
+
if (systemLicenses?.native?.licenses?.length) {
|
|
3193
3038
|
systemLicenses.native.licenses = [];
|
|
3194
3039
|
systemLicenses.native.readTime = new Date().toISOString();
|
|
3195
3040
|
await objects.setObjectAsync('system.licenses', systemLicenses);
|
|
@@ -3234,7 +3079,7 @@ export function compressFileGZip(inputFilename, outputFilename, options = {}) {
|
|
|
3234
3079
|
});
|
|
3235
3080
|
}
|
|
3236
3081
|
/**
|
|
3237
|
-
* Validate if the dir
|
|
3082
|
+
* Validate if the dir is a valid dataDir
|
|
3238
3083
|
* Data dirs in node_modules are not allowed, note that dataDirs are relative to js-controller dir or absolute
|
|
3239
3084
|
*
|
|
3240
3085
|
* @param dataDir dataDir to check
|
|
@@ -3252,7 +3097,7 @@ export function validateDataDir(dataDir) {
|
|
|
3252
3097
|
};
|
|
3253
3098
|
}
|
|
3254
3099
|
/**
|
|
3255
|
-
* If an array is passed it will be stringified, else the parameter is returned
|
|
3100
|
+
* If an array is passed, it will be stringified, else the parameter is returned
|
|
3256
3101
|
*
|
|
3257
3102
|
* @param maybeArr parameter which will be stringified if it is an array
|
|
3258
3103
|
*/
|
|
@@ -3282,7 +3127,7 @@ function getDNSResolutionOrder() {
|
|
|
3282
3127
|
return dnsOrder;
|
|
3283
3128
|
}
|
|
3284
3129
|
/**
|
|
3285
|
-
* Checks if given ip address is matching ipv4 or ipv6 localhost
|
|
3130
|
+
* Checks if the given ip address is matching ipv4 or ipv6 localhost
|
|
3286
3131
|
*
|
|
3287
3132
|
* @param ip ipv4 or ipv6 address
|
|
3288
3133
|
*/
|
|
@@ -3291,7 +3136,7 @@ export function isLocalAddress(ip) {
|
|
|
3291
3136
|
return localAddresses.includes(ip);
|
|
3292
3137
|
}
|
|
3293
3138
|
/**
|
|
3294
|
-
* Checks if given ip address is matching ipv4 or ipv6 "listen all" address
|
|
3139
|
+
* Checks if the given ip address is matching ipv4 or ipv6 "listen all" address
|
|
3295
3140
|
*
|
|
3296
3141
|
* @param ip ipv4 or ipv6 address
|
|
3297
3142
|
*/
|
|
@@ -3306,7 +3151,7 @@ export function getLocalAddress() {
|
|
|
3306
3151
|
return dnsOrder === 'ipv4first' ? '127.0.0.1' : '::1';
|
|
3307
3152
|
}
|
|
3308
3153
|
/**
|
|
3309
|
-
* Get the ip to listen to all addresses according to configured DNS resolution strategy
|
|
3154
|
+
* Get the ip to listen to all addresses according to a configured DNS resolution strategy
|
|
3310
3155
|
*/
|
|
3311
3156
|
export function getListenAllAddress() {
|
|
3312
3157
|
const dnsOrder = getDNSResolutionOrder();
|
|
@@ -3320,7 +3165,7 @@ export function ensureDNSOrder() {
|
|
|
3320
3165
|
setDefaultResultOrder(dnsOrder);
|
|
3321
3166
|
}
|
|
3322
3167
|
/**
|
|
3323
|
-
* Determine if ioBroker is installed as systemd service
|
|
3168
|
+
* Determine if ioBroker is installed as a systemd service
|
|
3324
3169
|
*/
|
|
3325
3170
|
export async function isIoBrokerInstalledAsSystemd() {
|
|
3326
3171
|
try {
|
|
@@ -3404,9 +3249,9 @@ export function getPidsFileName() {
|
|
|
3404
3249
|
return path.join(getControllerDir(), 'pids.txt');
|
|
3405
3250
|
}
|
|
3406
3251
|
/**
|
|
3407
|
-
* Get all ioBroker process
|
|
3252
|
+
* Get all ioBroker process IDs
|
|
3408
3253
|
*
|
|
3409
|
-
* @returns process
|
|
3254
|
+
* @returns process IDs or empty array if no process running
|
|
3410
3255
|
*/
|
|
3411
3256
|
export async function getPids() {
|
|
3412
3257
|
let pids = [];
|
|
@@ -3422,7 +3267,7 @@ export async function getPids() {
|
|
|
3422
3267
|
return pids;
|
|
3423
3268
|
}
|
|
3424
3269
|
/**
|
|
3425
|
-
* Check if given string is a valid loglevel
|
|
3270
|
+
* Check if a given string is a valid loglevel
|
|
3426
3271
|
*
|
|
3427
3272
|
* @param level level to validate
|
|
3428
3273
|
*/
|
|
@@ -3438,5 +3283,109 @@ export async function getControllerPid() {
|
|
|
3438
3283
|
const pids = await getPids();
|
|
3439
3284
|
return pids.pop();
|
|
3440
3285
|
}
|
|
3286
|
+
/**
|
|
3287
|
+
* Check if a process with the given id currently exists
|
|
3288
|
+
*
|
|
3289
|
+
* @param pid process id to check
|
|
3290
|
+
* @returns true if a process with this id is running
|
|
3291
|
+
*/
|
|
3292
|
+
export function isProcessRunning(pid) {
|
|
3293
|
+
// 0 and negative values address process groups instead of a single process and would not
|
|
3294
|
+
// throw below, so a corrupt pids file must not slip through here
|
|
3295
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
3296
|
+
return false;
|
|
3297
|
+
}
|
|
3298
|
+
try {
|
|
3299
|
+
// Signal 0 sends nothing, it only performs the existence and permission check
|
|
3300
|
+
process.kill(pid, 0);
|
|
3301
|
+
return true;
|
|
3302
|
+
}
|
|
3303
|
+
catch (e) {
|
|
3304
|
+
// EPERM means the process does exist, it just belongs to another user
|
|
3305
|
+
return e.code === 'EPERM';
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
/**
|
|
3309
|
+
* Check if a command line belongs to a js-controller process
|
|
3310
|
+
*
|
|
3311
|
+
* Only the started script counts, not a mere mention of the controller directory, which also shows
|
|
3312
|
+
* up in the command line of the CLI or of tools working on the installation.
|
|
3313
|
+
*
|
|
3314
|
+
* @param commandLine the full command line of the process
|
|
3315
|
+
* @returns true if the command line looks like the controller
|
|
3316
|
+
*/
|
|
3317
|
+
function isControllerCommandLine(commandLine) {
|
|
3318
|
+
const command = commandLine.toLowerCase();
|
|
3319
|
+
return (
|
|
3320
|
+
// On POSIX the title the running controller gives itself replaces the command line
|
|
3321
|
+
command.startsWith(`${appNameLowerCase}.js-controller`) ||
|
|
3322
|
+
// "node <path>/controller.js", as started by the CLI, the service or the installer
|
|
3323
|
+
/(^|[\s"'/\\])controller\.js(["'\s]|$)/.test(command) ||
|
|
3324
|
+
// The entry point started directly
|
|
3325
|
+
/[/\\](iobroker\.js-controller|controller)[/\\]build[/\\](esm|cjs)[/\\]main\.js/.test(command));
|
|
3326
|
+
}
|
|
3327
|
+
/**
|
|
3328
|
+
* Check if a pid is proven to belong to a program which is not the controller
|
|
3329
|
+
*
|
|
3330
|
+
* After a reboot the operating system may hand a recorded pid to an unrelated program, which
|
|
3331
|
+
* Windows in particular does readily, so a live pid alone does not prove the controller is
|
|
3332
|
+
* running. Being a Node.js process proves nothing either - Node-RED, zigbee2mqtt or this very CLI
|
|
3333
|
+
* are Node.js processes too - so the command line has to identify the controller. This only
|
|
3334
|
+
* reports a foreign program when it could actually be identified as one: whenever the process
|
|
3335
|
+
* cannot be inspected, the answer is "no", because acting on a wrong guess would start a second
|
|
3336
|
+
* controller and both would then fail with EADDRINUSE.
|
|
3337
|
+
*
|
|
3338
|
+
* @param pid process id to inspect
|
|
3339
|
+
* @returns true only if the process was identified as a different program
|
|
3340
|
+
*/
|
|
3341
|
+
export async function isForeignProcess(pid) {
|
|
3342
|
+
// Only a plain positive integer may become part of the PowerShell command below
|
|
3343
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
3344
|
+
return false;
|
|
3345
|
+
}
|
|
3346
|
+
// The stale pid may even have been handed to the process asking right now
|
|
3347
|
+
if (pid === process.pid) {
|
|
3348
|
+
return true;
|
|
3349
|
+
}
|
|
3350
|
+
try {
|
|
3351
|
+
if (os.platform() === 'win32') {
|
|
3352
|
+
// tasklist only knows the image name, which is node.exe for every Node.js program
|
|
3353
|
+
const { stdout } = await execFileAsync('powershell.exe', [
|
|
3354
|
+
'-NoProfile',
|
|
3355
|
+
'-NonInteractive',
|
|
3356
|
+
'-Command',
|
|
3357
|
+
`[Console]::OutputEncoding = [Text.Encoding]::UTF8; Get-CimInstance Win32_Process -Filter 'ProcessId=${pid}' | Select-Object Name, CommandLine | ConvertTo-Json -Compress`,
|
|
3358
|
+
], { timeout: 10_000 });
|
|
3359
|
+
// No output means the process is gone
|
|
3360
|
+
const output = (stdout || '').trim();
|
|
3361
|
+
if (!output) {
|
|
3362
|
+
return false;
|
|
3363
|
+
}
|
|
3364
|
+
const { Name, CommandLine } = JSON.parse(output);
|
|
3365
|
+
if (!Name) {
|
|
3366
|
+
return false;
|
|
3367
|
+
}
|
|
3368
|
+
if (!Name.toLowerCase().startsWith('node')) {
|
|
3369
|
+
return true;
|
|
3370
|
+
}
|
|
3371
|
+
// Without elevation the command line of another user's process is not readable
|
|
3372
|
+
return CommandLine ? !isControllerCommandLine(CommandLine) : false;
|
|
3373
|
+
}
|
|
3374
|
+
// Unlike comm, args is not truncated and shows the title the controller gives itself
|
|
3375
|
+
const { stdout } = await execFileAsync('ps', ['-p', String(pid), '-o', 'args='], { timeout: 2000 });
|
|
3376
|
+
const commandLine = (stdout || '').trim();
|
|
3377
|
+
if (!commandLine) {
|
|
3378
|
+
return false;
|
|
3379
|
+
}
|
|
3380
|
+
return !isControllerCommandLine(commandLine);
|
|
3381
|
+
}
|
|
3382
|
+
catch (e) {
|
|
3383
|
+
// Could not inspect the process - assume it is the controller. Say so, because otherwise a machine
|
|
3384
|
+
// where the lookup cannot run at all - no PowerShell, no WMI service, a timeout - keeps answering
|
|
3385
|
+
// "Controller is already running with pid ..." without a hint that nothing was ever checked.
|
|
3386
|
+
console.warn(`Cannot inspect process ${pid}, assuming it belongs to the controller: ${e.message}`);
|
|
3387
|
+
return false;
|
|
3388
|
+
}
|
|
3389
|
+
}
|
|
3441
3390
|
export * from '../../lib/common/maybeCallback.js';
|
|
3442
3391
|
//# sourceMappingURL=tools.js.map
|