@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.
Files changed (38) hide show
  1. package/build/cjs/index.js +3 -3
  2. package/build/cjs/lib/common/aliasProcessing.js +6 -0
  3. package/build/cjs/lib/common/aliasProcessing.js.map +2 -2
  4. package/build/cjs/lib/common/interview.js +2 -0
  5. package/build/cjs/lib/common/interview.js.map +1 -1
  6. package/build/cjs/lib/common/logger.d.ts +34 -1
  7. package/build/cjs/lib/common/logger.js +37 -37
  8. package/build/cjs/lib/common/logger.js.map +2 -2
  9. package/build/cjs/lib/common/maybeCallback.js +4 -0
  10. package/build/cjs/lib/common/maybeCallback.js.map +1 -1
  11. package/build/cjs/lib/common/password.d.ts +9 -0
  12. package/build/cjs/lib/common/password.js +8 -6
  13. package/build/cjs/lib/common/password.js.map +2 -2
  14. package/build/cjs/lib/common/session.js +10 -5
  15. package/build/cjs/lib/common/session.js.map +2 -2
  16. package/build/cjs/lib/common/tools.d.ts +164 -59
  17. package/build/cjs/lib/common/tools.js +497 -476
  18. package/build/cjs/lib/common/tools.js.map +3 -3
  19. package/build/esm/lib/common/aliasProcessing.d.ts.map +1 -1
  20. package/build/esm/lib/common/aliasProcessing.js +5 -0
  21. package/build/esm/lib/common/aliasProcessing.js.map +1 -1
  22. package/build/esm/lib/common/logger.d.ts +34 -1
  23. package/build/esm/lib/common/logger.d.ts.map +1 -1
  24. package/build/esm/lib/common/logger.js +45 -49
  25. package/build/esm/lib/common/logger.js.map +1 -1
  26. package/build/esm/lib/common/password.d.ts +9 -0
  27. package/build/esm/lib/common/password.d.ts.map +1 -1
  28. package/build/esm/lib/common/password.js +5 -0
  29. package/build/esm/lib/common/password.js.map +1 -1
  30. package/build/esm/lib/common/session.d.ts.map +1 -1
  31. package/build/esm/lib/common/session.js +6 -6
  32. package/build/esm/lib/common/session.js.map +1 -1
  33. package/build/esm/lib/common/tools.d.ts +164 -59
  34. package/build/esm/lib/common/tools.d.ts.map +1 -1
  35. package/build/esm/lib/common/tools.js +535 -586
  36. package/build/esm/lib/common/tools.js.map +1 -1
  37. package/build/tsconfig.build.tsbuildinfo +1 -1
  38. 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 forge from 'node-forge';
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 { password } from '../../lib/common/password.js';
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 which started to support UI upgrade */
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
- // unicode character classes, we do that by OR-ing the character classes and negating the result.
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-to-date, throws error on invalid version strings
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
- // TODO: this is only here for backward compatibility, if MULTIHOST password was still setup with old decryption
174
- export function decryptPhrase(password, data, callback) {
175
- const decipher = crypto.createDecipher('aes192', password);
176
- try {
177
- let decrypted = '';
178
- decipher.on('readable', () => {
179
- const data = decipher.read();
180
- if (data) {
181
- decrypted += data.toString('utf8');
182
- }
183
- });
184
- decipher.on('error', error => {
185
- console.error(`Cannot decode secret: ${error.message}`);
186
- callback(null);
187
- });
188
- decipher.on('end', () => callback(decrypted));
189
- decipher.write(data, 'hex');
190
- decipher.end();
191
- }
192
- catch (e) {
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 && state.val) {
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?.toString()}`);
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 = 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 && mac === '') {
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
- mac = entry.mac;
457
+ givenMac = entry.mac;
425
458
  break;
426
459
  }
427
460
  }
428
461
  }
429
- if (mac) {
462
+ if (givenMac) {
430
463
  break;
431
464
  }
432
465
  }
433
466
  }
434
- if (!_isDocker && mac === '') {
467
+ if (!_isDocker && !givenMac) {
435
468
  const mac = await getMac();
436
469
  return uuid(mac);
437
470
  }
438
- if (!_isDocker && mac) {
471
+ if (!_isDocker && givenMac) {
439
472
  const md5sum = crypto.createHash('md5');
440
- md5sum.update(mac);
441
- mac = md5sum.digest('hex');
442
- u = `${mac.substring(0, 8)}-${mac.substring(8, 12)}-${mac.substring(12, 16)}-${mac.substring(16, 20)}-${mac.substring(20)}`;
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
- * @param callback
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, callback) {
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
- res.data.pipe(fs.createWriteStream(tmpFile)).on('close', () => {
597
- console.log(`downloaded ${tmpFile}`);
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
- else {
607
- try {
608
- if (fs.existsSync(urlOrPath)) {
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
- catch (err) {
623
- console.log(`File "${urlOrPath}" could no be read: ${err.message}`);
624
- process.exit(EXIT_CODES.FILE_NOT_FOUND);
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
- // Return content of the JSON file. Download it or read directly
629
- export async function getJson(urlOrPath, agent, callback) {
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
- agent = agent || '';
635
- let sources = {};
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 && typeof urlOrPath === 'object') {
638
- if (callback) {
639
- callback(urlOrPath);
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
- else if (!urlOrPath) {
643
- console.log('Empty url!');
644
- if (callback) {
645
- callback(null);
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
- else {
649
- if (urlOrPath.substring(0, 'http://'.length) === 'http://' ||
650
- urlOrPath.substring(0, 'https://'.length) === 'https://') {
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
- else {
672
- if (fs.existsSync(urlOrPath)) {
673
- try {
674
- sources = fs.readJSONSync(urlOrPath);
675
- }
676
- catch (e) {
677
- console.log(`Cannot parse json file from ${urlOrPath}. Error: ${e.message}`);
678
- if (callback) {
679
- callback(null, urlOrPath);
680
- }
681
- return;
682
- }
683
- if (callback) {
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 = 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
- else if (!urlOrPath) {
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, callback) {
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
- exec(cliCommand, { timeout: 2000, windowsHide: true }, (error, stdout, _stderr) => {
909
- let version;
910
- if (error) {
911
- // command failed
912
- if (typeof callback === 'function') {
913
- callback(error);
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, callback) {
926
- getJson(sources[name].meta, '', ioPack => {
927
- const packUrl = sources[name].meta.replace('io-package.json', 'package.json');
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
- if (sources._helper) {
930
- sources._helper.failCounter.push(name);
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
- if (callback) {
933
- callback(sources, name);
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
- else {
937
- setImmediate(() => {
938
- getJson(packUrl, '', pack => {
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
- function _getRepositoryFile(sources, path, callback) {
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
- sources._helper = { failCounter: [] };
1023
- sources._helper.timeout = setTimeout(() => {
1024
- if (sources._helper) {
1025
- delete sources._helper;
1026
- for (const __name of Object.keys(sources)) {
1027
- if (sources[__name].processed !== undefined) {
1028
- delete sources[__name].processed;
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
- if (callback) {
1032
- callback(new Error(`Timeout by read all package.json (${count}) seconds`), sources);
962
+ else {
963
+ packSource.version = 'npm error';
1033
964
  }
1034
- callback = undefined;
1035
965
  }
1036
- }, count * 1000);
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
- async function _checkRepositoryFileHash(urlOrPath, additionalInfo, callback) {
1097
- // read hash of file
1098
- if (urlOrPath.startsWith('http://') || urlOrPath.startsWith('https://')) {
1099
- urlOrPath = urlOrPath.replace(/\.json$/, '-hash.json');
1100
- let json = null;
1101
- try {
1102
- const res = await axios.get(urlOrPath, { headers: { 'Accept-Encoding': 'gzip', timeout: 10000 } });
1103
- if (!res.data || res.status !== 200) {
1104
- throw new Error(`Invalid response, body: ${res.data}, status code: ${res.status}`);
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
- json = res.data;
1107
- }
1108
- catch (e) {
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
- else {
1119
- // Either we have no sources cached or the hash changed
1120
- // => force download of new sources
1121
- console.log('hash changed or no sources cached => force download of new sources');
1122
- callback(null, null, json.hash);
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
- else {
1126
- // Could not download new sources, use the old ones
1127
- console.log('failed to download new sources, use cached sources');
1128
- callback(null, additionalInfo.sources, '');
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
- else {
1132
- // it is a file and file has not hash
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(urlOrPath, additionalInfo, callback) {
1023
+ export async function getRepositoryFile() {
1144
1024
  let sources = {};
1145
- let _path = '';
1146
- if (typeof additionalInfo === 'function') {
1147
- // @ts-expect-error: fix all fun calls then remove
1148
- callback = additionalInfo;
1149
- additionalInfo = {};
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
- else if (!urlOrPath) {
1163
- try {
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
- else {
1197
- let agent = '';
1198
- if (additionalInfo) {
1199
- // Add some information to user-agent, like chrome, IE and Firefox do
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 _hash;
1241
- let data;
1062
+ let hashResponse;
1063
+ let data = null;
1242
1064
  if (url.startsWith('http://') || url.startsWith('https://')) {
1243
1065
  try {
1244
- _hash = await axios({ url: url.replace(/\.json$/, '-hash.json'), timeout: 10_000 });
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 && _hash?.data && _hash.data.hash === 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
- data = await axios({
1081
+ const response = await axios({
1257
1082
  url,
1258
1083
  timeout: 10_000,
1259
1084
  headers: { 'User-Agent': agent },
1260
1085
  });
1261
- data = data.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: _hash?.data ? hash !== _hash.data.hash : true,
1284
- hash: _hash?.data ? _hash.data.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 not read adapters from js-controller/node_module/adapter and check first in parent directory
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 npm
1194
+ * Read version of system NPM
1370
1195
  *
1371
- * @param callback return result
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(callback) {
1379
- const { exec } = require('node:child_process');
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
- // @ts-expect-error TODO
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
- try {
1392
- let timeout = setTimeout(() => {
1393
- timeout = null;
1394
- if (callback) {
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 free space
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
- * Following info will be collected:
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] && systemRepos.native.repositories[repo].json)
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 getSystemNpmVersionAsync();
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, that have ack = true
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 && cb(Math.max(0, t - start - ms));
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 convert state values by read and write of aliases. Function is synchronous.
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
- if (!allEnums) {
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
- if (!error) {
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, that default value has correct type
2467
- if (obj.common.def !== undefined && obj.common.def !== null) {
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 && typeof obj.common.read !== 'boolean') {
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 && typeof obj.common.write !== 'boolean') {
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 !== null && // we allow null for deletion TODO: implement https://github.com/ioBroker/ioBroker.js-controller/issues/1735
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
- if (!instances.includes(adapters[i])) {
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
- * On error, the promise rejects with the exit code or signal, as well as stdout and stderr.
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 cpExecAsync(command, { ...defaultOptions, ...execOptions });
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 && systemLicenses.native && systemLicenses.native.password && systemLicenses.native.login) {
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, is a valid dataDir
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 ids
3252
+ * Get all ioBroker process IDs
3408
3253
  *
3409
- * @returns process ids or empty array if no process running
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