ultravisor-beacon 0.0.8 → 0.0.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultravisor-beacon",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "description": "Ultravisor Beacon: lightweight beacon client and Fable service for remote task execution",
5
5
  "main": "source/Ultravisor-Beacon-Service.cjs",
6
6
  "scripts": {
@@ -33,8 +33,8 @@ class UltravisorBeaconCapabilityManager
33
33
  {
34
34
  this.log = pLog || {
35
35
  info: (...pArgs) => { console.log(...pArgs); },
36
- warn: (...pArgs) => { this.log.warn(...pArgs); },
37
- error: (...pArgs) => { this.log.error(...pArgs); }
36
+ warn: (...pArgs) => { console.warn(...pArgs); },
37
+ error: (...pArgs) => { console.error(...pArgs); }
38
38
  };
39
39
 
40
40
  // Map of Capability name -> descriptor
@@ -14,6 +14,10 @@
14
14
  */
15
15
 
16
16
  const libHTTP = require('http');
17
+ const libOS = require('os');
18
+ const libFS = require('fs');
19
+ const libPath = require('path');
20
+ const libCrypto = require('crypto');
17
21
 
18
22
  let libWebSocket;
19
23
  try
@@ -433,9 +437,71 @@ class UltravisorBeaconClient
433
437
  tmpBody.BindAddresses = this._Config.BindAddresses;
434
438
  }
435
439
 
440
+ // Host identity — used by the reachability matrix to detect beacons that
441
+ // live on the same physical machine. Caller can override; default is the
442
+ // node hostname (which inside a container is the container ID).
443
+ tmpBody.HostID = this._Config.HostID || libOS.hostname();
444
+
445
+ // Shared filesystem mounts — each entry tells the coordinator about a
446
+ // local filesystem tree this beacon advertises as accessible. When two
447
+ // beacons report the same MountID, the reachability matrix can pick the
448
+ // "shared-fs" strategy to skip an HTTP file transfer entirely.
449
+ //
450
+ // The MountID derivation includes stat.dev so two beacons that bind-mount
451
+ // the same host directory get the same ID, while two unrelated /media
452
+ // directories on different machines get different IDs.
453
+ tmpBody.SharedMounts = this._normalizeSharedMounts(this._Config.SharedMounts);
454
+
436
455
  this._httpRequest('POST', '/Beacon/Register', tmpBody, fCallback);
437
456
  }
438
457
 
458
+ _normalizeSharedMounts(pMounts)
459
+ {
460
+ if (!Array.isArray(pMounts) || pMounts.length === 0)
461
+ {
462
+ return [];
463
+ }
464
+ let tmpResult = [];
465
+ for (let i = 0; i < pMounts.length; i++)
466
+ {
467
+ let tmpEntry = pMounts[i];
468
+ if (!tmpEntry || !tmpEntry.Root)
469
+ {
470
+ continue;
471
+ }
472
+ let tmpRoot;
473
+ try
474
+ {
475
+ tmpRoot = libPath.resolve(tmpEntry.Root);
476
+ }
477
+ catch (pError)
478
+ {
479
+ continue;
480
+ }
481
+ let tmpMountID = tmpEntry.MountID;
482
+ if (!tmpMountID)
483
+ {
484
+ try
485
+ {
486
+ let tmpStat = libFS.statSync(tmpRoot);
487
+ tmpMountID = libCrypto.createHash('sha256')
488
+ .update(tmpStat.dev + ':' + tmpRoot)
489
+ .digest('hex').substring(0, 16);
490
+ }
491
+ catch (pError)
492
+ {
493
+ // Mount root does not exist on this beacon — skip it.
494
+ continue;
495
+ }
496
+ }
497
+ tmpResult.push({
498
+ MountID: tmpMountID,
499
+ Root: tmpRoot
500
+ });
501
+ }
502
+ return tmpResult;
503
+ }
504
+
439
505
  _deregister(fCallback)
440
506
  {
441
507
  this._httpRequest('DELETE', `/Beacon/${this._BeaconID}`, null, fCallback);
@@ -51,7 +51,9 @@ class UltravisorBeaconService extends libFableServiceBase
51
51
  HeartbeatIntervalMs: 30000,
52
52
  StagingPath: '',
53
53
  Tags: {},
54
- Contexts: {}
54
+ Contexts: {},
55
+ HostID: '',
56
+ SharedMounts: []
55
57
  }, this.options || {});
56
58
 
57
59
  // Internal components
@@ -259,6 +261,18 @@ class UltravisorBeaconService extends libFableServiceBase
259
261
  Providers: []
260
262
  });
261
263
 
264
+ // Shared-fs identity (forwarded to the thin client which sends it to the
265
+ // coordinator at registration time so the reachability matrix can detect
266
+ // beacons that share a filesystem on the same host).
267
+ if (this.options.HostID)
268
+ {
269
+ tmpClientConfig.HostID = this.options.HostID;
270
+ }
271
+ if (Array.isArray(this.options.SharedMounts) && this.options.SharedMounts.length > 0)
272
+ {
273
+ tmpClientConfig.SharedMounts = this.options.SharedMounts;
274
+ }
275
+
262
276
  // Create thin client
263
277
  this._ThinClient = new libBeaconClient(tmpClientConfig);
264
278
 
@@ -10,12 +10,17 @@
10
10
  */
11
11
 
12
12
  const libAssert = require('assert');
13
+ const libFS = require('fs');
14
+ const libPath = require('path');
15
+ const libCrypto = require('crypto');
16
+ const libOS = require('os');
13
17
 
14
18
  const libCapabilityProvider = require('../source/Ultravisor-Beacon-CapabilityProvider.cjs');
15
19
  const libCapabilityAdapter = require('../source/Ultravisor-Beacon-CapabilityAdapter.cjs');
16
20
  const libCapabilityManager = require('../source/Ultravisor-Beacon-CapabilityManager.cjs');
17
21
  const libConnectivityHTTP = require('../source/Ultravisor-Beacon-ConnectivityHTTP.cjs');
18
22
  const libProviderRegistry = require('../source/Ultravisor-Beacon-ProviderRegistry.cjs');
23
+ const libBeaconClient = require('../source/Ultravisor-Beacon-Client.cjs');
19
24
 
20
25
  // We can require the service without Fable for standalone testing
21
26
  const libBeaconService = require('../source/Ultravisor-Beacon-Service.cjs');
@@ -604,5 +609,136 @@ suite
604
609
  );
605
610
  }
606
611
  );
612
+
613
+ // ============================================================
614
+ // Shared-FS Reachability — SharedMounts normalization
615
+ // ============================================================
616
+ suite
617
+ (
618
+ 'SharedMounts Normalization',
619
+ function ()
620
+ {
621
+ test
622
+ (
623
+ 'Should return empty array for missing/empty input',
624
+ function ()
625
+ {
626
+ let tmpClient = new libBeaconClient({ Name: 'mt-test', Capabilities: [] });
627
+ libAssert.deepStrictEqual(tmpClient._normalizeSharedMounts(undefined), []);
628
+ libAssert.deepStrictEqual(tmpClient._normalizeSharedMounts(null), []);
629
+ libAssert.deepStrictEqual(tmpClient._normalizeSharedMounts([]), []);
630
+ libAssert.deepStrictEqual(tmpClient._normalizeSharedMounts('not-an-array'), []);
631
+ }
632
+ );
633
+
634
+ test
635
+ (
636
+ 'Should auto-derive MountID from stat.dev + path',
637
+ function ()
638
+ {
639
+ let tmpClient = new libBeaconClient({ Name: 'mt-test', Capabilities: [] });
640
+ let tmpRoot = libOS.tmpdir();
641
+ let tmpResolved = libPath.resolve(tmpRoot);
642
+ let tmpExpected = libCrypto.createHash('sha256')
643
+ .update(libFS.statSync(tmpResolved).dev + ':' + tmpResolved)
644
+ .digest('hex').substring(0, 16);
645
+
646
+ let tmpResult = tmpClient._normalizeSharedMounts([{ Root: tmpRoot }]);
647
+ libAssert.strictEqual(tmpResult.length, 1);
648
+ libAssert.strictEqual(tmpResult[0].MountID, tmpExpected);
649
+ libAssert.strictEqual(tmpResult[0].Root, tmpResolved);
650
+ }
651
+ );
652
+
653
+ test
654
+ (
655
+ 'Should preserve explicit MountID without stat',
656
+ function ()
657
+ {
658
+ let tmpClient = new libBeaconClient({ Name: 'mt-test', Capabilities: [] });
659
+ let tmpResult = tmpClient._normalizeSharedMounts(
660
+ [{ MountID: 'manual-id-1234', Root: libOS.tmpdir() }]);
661
+ libAssert.strictEqual(tmpResult.length, 1);
662
+ libAssert.strictEqual(tmpResult[0].MountID, 'manual-id-1234');
663
+ }
664
+ );
665
+
666
+ test
667
+ (
668
+ 'Should skip entries without a Root',
669
+ function ()
670
+ {
671
+ let tmpClient = new libBeaconClient({ Name: 'mt-test', Capabilities: [] });
672
+ let tmpResult = tmpClient._normalizeSharedMounts(
673
+ [{ MountID: 'no-root' }, { Root: libOS.tmpdir() }, null, undefined]);
674
+ libAssert.strictEqual(tmpResult.length, 1);
675
+ libAssert.strictEqual(tmpResult[0].Root, libPath.resolve(libOS.tmpdir()));
676
+ }
677
+ );
678
+
679
+ test
680
+ (
681
+ 'Should skip entries whose Root does not exist on this beacon',
682
+ function ()
683
+ {
684
+ let tmpClient = new libBeaconClient({ Name: 'mt-test', Capabilities: [] });
685
+ let tmpResult = tmpClient._normalizeSharedMounts(
686
+ [{ Root: '/nonexistent-path-12345-xyz' }]);
687
+ libAssert.deepStrictEqual(tmpResult, []);
688
+ }
689
+ );
690
+
691
+ test
692
+ (
693
+ 'Two beacons that point at the same Root should derive identical MountIDs',
694
+ function ()
695
+ {
696
+ let tmpClientA = new libBeaconClient({ Name: 'mt-a', Capabilities: [] });
697
+ let tmpClientB = new libBeaconClient({ Name: 'mt-b', Capabilities: [] });
698
+ let tmpA = tmpClientA._normalizeSharedMounts([{ Root: libOS.tmpdir() }]);
699
+ let tmpB = tmpClientB._normalizeSharedMounts([{ Root: libOS.tmpdir() }]);
700
+ libAssert.strictEqual(tmpA[0].MountID, tmpB[0].MountID);
701
+ }
702
+ );
703
+ }
704
+ );
705
+
706
+ // ============================================================
707
+ // BeaconService — HostID and SharedMounts forwarding
708
+ // ============================================================
709
+ suite
710
+ (
711
+ 'Service forwards HostID and SharedMounts',
712
+ function ()
713
+ {
714
+ test
715
+ (
716
+ 'Service options should accept HostID and SharedMounts with sensible defaults',
717
+ function ()
718
+ {
719
+ let tmpService = new libBeaconService({ Name: 'fwd-test' });
720
+ // Defaults from Object.assign should land
721
+ libAssert.strictEqual(tmpService.options.HostID, '');
722
+ libAssert.deepStrictEqual(tmpService.options.SharedMounts, []);
723
+ }
724
+ );
725
+
726
+ test
727
+ (
728
+ 'Service options should preserve caller-supplied HostID and SharedMounts',
729
+ function ()
730
+ {
731
+ let tmpService = new libBeaconService({
732
+ Name: 'fwd-test',
733
+ HostID: 'custom-host-id',
734
+ SharedMounts: [{ MountID: 'abc123', Root: '/data' }]
735
+ });
736
+ libAssert.strictEqual(tmpService.options.HostID, 'custom-host-id');
737
+ libAssert.deepStrictEqual(tmpService.options.SharedMounts,
738
+ [{ MountID: 'abc123', Root: '/data' }]);
739
+ }
740
+ );
741
+ }
742
+ );
607
743
  }
608
744
  );