ultravisor-beacon 0.0.7 → 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.7",
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": {
@@ -29,8 +29,14 @@ const libCapabilityAdapter = require('./Ultravisor-Beacon-CapabilityAdapter.cjs'
29
29
 
30
30
  class UltravisorBeaconCapabilityManager
31
31
  {
32
- constructor()
32
+ constructor(pLog)
33
33
  {
34
+ this.log = pLog || {
35
+ info: (...pArgs) => { console.log(...pArgs); },
36
+ warn: (...pArgs) => { console.warn(...pArgs); },
37
+ error: (...pArgs) => { console.error(...pArgs); }
38
+ };
39
+
34
40
  // Map of Capability name -> descriptor
35
41
  this._Capabilities = {};
36
42
  }
@@ -45,13 +51,13 @@ class UltravisorBeaconCapabilityManager
45
51
  {
46
52
  if (!pDescriptor || !pDescriptor.Capability)
47
53
  {
48
- console.error('[CapabilityManager] Descriptor must have a Capability name.');
54
+ this.log.error('[CapabilityManager] Descriptor must have a Capability name.');
49
55
  return false;
50
56
  }
51
57
 
52
58
  if (!pDescriptor.actions || Object.keys(pDescriptor.actions).length === 0)
53
59
  {
54
- console.warn(`[CapabilityManager] Capability "${pDescriptor.Capability}" has no actions.`);
60
+ this.log.warn(`[CapabilityManager] Capability "${pDescriptor.Capability}" has no actions.`);
55
61
  }
56
62
 
57
63
  this._Capabilities[pDescriptor.Capability] = pDescriptor;
@@ -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
@@ -45,6 +49,15 @@ class UltravisorBeaconClient
45
49
  Tags: {}
46
50
  }, pConfig || {});
47
51
 
52
+ // Logger: use provided Fable log or fall back to console
53
+ this.log = this._Config.Log || {
54
+ trace: (...pArgs) => { console.log(...pArgs); },
55
+ debug: (...pArgs) => { console.log(...pArgs); },
56
+ info: (...pArgs) => { console.log(...pArgs); },
57
+ warn: (...pArgs) => { console.warn(...pArgs); },
58
+ error: (...pArgs) => { console.error(...pArgs); }
59
+ };
60
+
48
61
  this._BeaconID = null;
49
62
  this._PollInterval = null;
50
63
  this._HeartbeatInterval = null;
@@ -52,13 +65,17 @@ class UltravisorBeaconClient
52
65
  this._ActiveWorkItems = 0;
53
66
  this._SessionCookie = null;
54
67
  this._Authenticating = false;
68
+ this._ReconnectPending = false;
69
+ this._ReconnectAttempts = 0;
70
+ this._MaxReconnectDelayMs = 300000;
55
71
 
56
72
  // WebSocket transport state — determined at runtime, not config
57
73
  this._WebSocket = null;
58
74
  this._UseWebSocket = false;
59
75
 
60
76
  this._Executor = new libBeaconExecutor({
61
- StagingPath: this._Config.StagingPath
77
+ StagingPath: this._Config.StagingPath,
78
+ Log: this.log
62
79
  });
63
80
 
64
81
  // Load capability providers
@@ -84,7 +101,7 @@ class UltravisorBeaconClient
84
101
  }
85
102
 
86
103
  let tmpCount = this._Executor.providerRegistry.loadProviders(tmpProviders);
87
- console.log(`[Beacon] Loaded ${tmpCount} capability provider(s).`);
104
+ this.log.info(`[Beacon] Loaded ${tmpCount} capability provider(s).`);
88
105
  }
89
106
 
90
107
  // ================================================================
@@ -96,31 +113,31 @@ class UltravisorBeaconClient
96
113
  */
97
114
  start(fCallback)
98
115
  {
99
- console.log(`[Beacon] Starting "${this._Config.Name}"...`);
100
- console.log(`[Beacon] Server: ${this._Config.ServerURL}`);
116
+ this.log.info(`[Beacon] Starting "${this._Config.Name}"...`);
117
+ this.log.info(`[Beacon] Server: ${this._Config.ServerURL}`);
101
118
 
102
119
  // Initialize all providers before registering
103
120
  this._Executor.providerRegistry.initializeAll((pInitError) =>
104
121
  {
105
122
  if (pInitError)
106
123
  {
107
- console.error(`[Beacon] Provider initialization failed: ${pInitError.message}`);
124
+ this.log.error(`[Beacon] Provider initialization failed: ${pInitError.message}`);
108
125
  return fCallback(pInitError);
109
126
  }
110
127
 
111
128
  let tmpCapabilities = this._Executor.providerRegistry.getCapabilities();
112
- console.log(`[Beacon] Capabilities: ${tmpCapabilities.join(', ')}`);
129
+ this.log.info(`[Beacon] Capabilities: ${tmpCapabilities.join(', ')}`);
113
130
 
114
131
  // Authenticate before registering (both transports need a session)
115
132
  this._authenticate((pAuthError) =>
116
133
  {
117
134
  if (pAuthError)
118
135
  {
119
- console.error(`[Beacon] Authentication failed: ${pAuthError.message}`);
136
+ this.log.error(`[Beacon] Authentication failed: ${pAuthError.message}`);
120
137
  return fCallback(pAuthError);
121
138
  }
122
139
 
123
- console.log(`[Beacon] Authenticated successfully.`);
140
+ this.log.info(`[Beacon] Authenticated successfully.`);
124
141
 
125
142
  // Try WebSocket first — if ws library is available and the
126
143
  // server supports it, we get push-based work dispatch.
@@ -131,7 +148,7 @@ class UltravisorBeaconClient
131
148
  {
132
149
  if (pWSError)
133
150
  {
134
- console.log(`[Beacon] WebSocket unavailable (${pWSError.message}), using HTTP polling.`);
151
+ this.log.info(`[Beacon] WebSocket unavailable (${pWSError.message}), using HTTP polling.`);
135
152
  this._UseWebSocket = false;
136
153
  this._startHTTP(fCallback);
137
154
  return;
@@ -157,14 +174,14 @@ class UltravisorBeaconClient
157
174
  {
158
175
  if (pError)
159
176
  {
160
- console.error(`[Beacon] Registration failed: ${pError.message}`);
177
+ this.log.error(`[Beacon] Registration failed: ${pError.message}`);
161
178
  return fCallback(pError);
162
179
  }
163
180
 
164
181
  this._BeaconID = pBeacon.BeaconID;
165
182
  this._Running = true;
166
183
 
167
- console.log(`[Beacon] Registered as ${this._BeaconID}`);
184
+ this.log.info(`[Beacon] Registered as ${this._BeaconID}`);
168
185
 
169
186
  // Start polling for work
170
187
  this._PollInterval = setInterval(() =>
@@ -190,7 +207,7 @@ class UltravisorBeaconClient
190
207
  */
191
208
  stop(fCallback)
192
209
  {
193
- console.log(`[Beacon] Stopping...`);
210
+ this.log.info(`[Beacon] Stopping...`);
194
211
  this._Running = false;
195
212
 
196
213
  if (this._PollInterval)
@@ -226,7 +243,7 @@ class UltravisorBeaconClient
226
243
  {
227
244
  if (pShutdownError)
228
245
  {
229
- console.warn(`[Beacon] Provider shutdown warning: ${pShutdownError.message}`);
246
+ this.log.warn(`[Beacon] Provider shutdown warning: ${pShutdownError.message}`);
230
247
  }
231
248
 
232
249
  if (this._BeaconID)
@@ -235,15 +252,15 @@ class UltravisorBeaconClient
235
252
  {
236
253
  if (pError)
237
254
  {
238
- console.warn(`[Beacon] Deregistration warning: ${pError.message}`);
255
+ this.log.warn(`[Beacon] Deregistration warning: ${pError.message}`);
239
256
  }
240
- console.log(`[Beacon] Stopped.`);
257
+ this.log.info(`[Beacon] Stopped.`);
241
258
  if (fCallback) return fCallback(null);
242
259
  });
243
260
  }
244
261
  else
245
262
  {
246
- console.log(`[Beacon] Stopped.`);
263
+ this.log.info(`[Beacon] Stopped.`);
247
264
  if (fCallback) return fCallback(null);
248
265
  }
249
266
  });
@@ -291,7 +308,7 @@ class UltravisorBeaconClient
291
308
  // Take the name=value portion (before the first semicolon)
292
309
  let tmpCookieParts = tmpSetCookieHeaders[0].split(';');
293
310
  this._SessionCookie = tmpCookieParts[0].trim();
294
- console.log(`[Beacon] Session cookie acquired.`);
311
+ this.log.info(`[Beacon] Session cookie acquired.`);
295
312
  }
296
313
 
297
314
  try
@@ -341,25 +358,25 @@ class UltravisorBeaconClient
341
358
 
342
359
  this._SessionCookie = null;
343
360
 
344
- console.log(`[Beacon] Reconnecting — re-authenticating...`);
361
+ this.log.info(`[Beacon] Reconnecting — re-authenticating...`);
345
362
 
346
363
  this._authenticate((pAuthError) =>
347
364
  {
348
365
  if (pAuthError)
349
366
  {
350
- console.error(`[Beacon] Re-authentication failed: ${pAuthError.message}`);
367
+ this.log.error(`[Beacon] Re-authentication failed: ${pAuthError.message}`);
351
368
  this._Authenticating = false;
352
369
  setTimeout(() => { this._reconnect(); }, 10000);
353
370
  return;
354
371
  }
355
372
 
356
- console.log(`[Beacon] Re-authenticated, re-registering...`);
373
+ this.log.info(`[Beacon] Re-authenticated, re-registering...`);
357
374
 
358
375
  this._register((pRegError, pBeacon) =>
359
376
  {
360
377
  if (pRegError)
361
378
  {
362
- console.error(`[Beacon] Re-registration failed: ${pRegError.message}`);
379
+ this.log.error(`[Beacon] Re-registration failed: ${pRegError.message}`);
363
380
  this._Authenticating = false;
364
381
  setTimeout(() => { this._reconnect(); }, 10000);
365
382
  return;
@@ -368,7 +385,7 @@ class UltravisorBeaconClient
368
385
  this._BeaconID = pBeacon.BeaconID;
369
386
  this._Authenticating = false;
370
387
 
371
- console.log(`[Beacon] Reconnected as ${this._BeaconID}`);
388
+ this.log.info(`[Beacon] Reconnected as ${this._BeaconID}`);
372
389
 
373
390
  // Restart polling
374
391
  this._PollInterval = setInterval(() =>
@@ -420,9 +437,71 @@ class UltravisorBeaconClient
420
437
  tmpBody.BindAddresses = this._Config.BindAddresses;
421
438
  }
422
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
+
423
455
  this._httpRequest('POST', '/Beacon/Register', tmpBody, fCallback);
424
456
  }
425
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
+
426
505
  _deregister(fCallback)
427
506
  {
428
507
  this._httpRequest('DELETE', `/Beacon/${this._BeaconID}`, null, fCallback);
@@ -471,7 +550,7 @@ class UltravisorBeaconClient
471
550
  _executeWorkItem(pWorkItem)
472
551
  {
473
552
  this._ActiveWorkItems++;
474
- console.log(`[Beacon] Executing work item [${pWorkItem.WorkItemHash}] (${pWorkItem.Capability}/${pWorkItem.Action})`);
553
+ this.log.info(`[Beacon] Executing work item [${pWorkItem.WorkItemHash}] (${pWorkItem.Capability}/${pWorkItem.Action})`);
475
554
 
476
555
  // Create a progress callback that sends updates to the server
477
556
  let tmpWorkItemHash = pWorkItem.WorkItemHash;
@@ -493,7 +572,7 @@ class UltravisorBeaconClient
493
572
 
494
573
  if (pError)
495
574
  {
496
- console.error(`[Beacon] Execution error for [${pWorkItem.WorkItemHash}]: ${pError.message}`);
575
+ this.log.error(`[Beacon] Execution error for [${pWorkItem.WorkItemHash}]: ${pError.message}`);
497
576
  if (this._UseWebSocket)
498
577
  {
499
578
  this._wsReportError(pWorkItem.WorkItemHash, pError.message, []);
@@ -509,11 +588,11 @@ class UltravisorBeaconClient
509
588
  let tmpOutputs = pResult.Outputs || {};
510
589
  if (tmpOutputs.ExitCode && tmpOutputs.ExitCode !== 0)
511
590
  {
512
- console.warn(`[Beacon] Work item [${pWorkItem.WorkItemHash}] completed with exit code ${tmpOutputs.ExitCode}`);
591
+ this.log.warn(`[Beacon] Work item [${pWorkItem.WorkItemHash}] completed with exit code ${tmpOutputs.ExitCode}`);
513
592
  }
514
593
  else
515
594
  {
516
- console.log(`[Beacon] Work item [${pWorkItem.WorkItemHash}] completed successfully.`);
595
+ this.log.info(`[Beacon] Work item [${pWorkItem.WorkItemHash}] completed successfully.`);
517
596
  }
518
597
 
519
598
  // Upload output file if one was produced (Result is a local path)
@@ -529,7 +608,7 @@ class UltravisorBeaconClient
529
608
  try
530
609
  {
531
610
  let tmpBuffer = tmpFS.readFileSync(tmpResultPath);
532
- console.log(`[Beacon] Uploading result file ${tmpOutputFilename} (${tmpBuffer.length} bytes) for [${pWorkItem.WorkItemHash}]`);
611
+ this.log.info(`[Beacon] Uploading result file ${tmpOutputFilename} (${tmpBuffer.length} bytes) for [${pWorkItem.WorkItemHash}]`);
533
612
  this._wsSend({
534
613
  Action: 'WorkResultUpload',
535
614
  WorkItemHash: pWorkItem.WorkItemHash,
@@ -540,7 +619,7 @@ class UltravisorBeaconClient
540
619
  }
541
620
  catch (pUploadError)
542
621
  {
543
- console.error(`[Beacon] Failed to upload result file: ${pUploadError.message}`);
622
+ this.log.error(`[Beacon] Failed to upload result file: ${pUploadError.message}`);
544
623
  }
545
624
  }
546
625
  }
@@ -568,7 +647,7 @@ class UltravisorBeaconClient
568
647
  {
569
648
  if (pError)
570
649
  {
571
- console.error(`[Beacon] Failed to report completion for [${pWorkItemHash}]: ${pError.message}`);
650
+ this.log.error(`[Beacon] Failed to report completion for [${pWorkItemHash}]: ${pError.message}`);
572
651
  }
573
652
  });
574
653
  }
@@ -581,7 +660,7 @@ class UltravisorBeaconClient
581
660
  {
582
661
  if (pError)
583
662
  {
584
- console.error(`[Beacon] Failed to report error for [${pWorkItemHash}]: ${pError.message}`);
663
+ this.log.error(`[Beacon] Failed to report error for [${pWorkItemHash}]: ${pError.message}`);
585
664
  }
586
665
  });
587
666
  }
@@ -600,7 +679,7 @@ class UltravisorBeaconClient
600
679
  if (pError)
601
680
  {
602
681
  // Fire-and-forget — log but don't affect execution
603
- console.warn(`[Beacon] Failed to report progress for [${pWorkItemHash}]: ${pError.message}`);
682
+ this.log.warn(`[Beacon] Failed to report progress for [${pWorkItemHash}]: ${pError.message}`);
604
683
  }
605
684
  });
606
685
  }
@@ -621,7 +700,7 @@ class UltravisorBeaconClient
621
700
  {
622
701
  if (pError)
623
702
  {
624
- console.warn(`[Beacon] Heartbeat failed: ${pError.message}`);
703
+ this.log.warn(`[Beacon] Heartbeat failed: ${pError.message}`);
625
704
  }
626
705
  });
627
706
  }
@@ -730,7 +809,7 @@ class UltravisorBeaconClient
730
809
 
731
810
  this._WebSocket.on('open', () =>
732
811
  {
733
- console.log(`[Beacon] WebSocket connected to ${tmpWSURL}`);
812
+ this.log.info(`[Beacon] WebSocket connected to ${tmpWSURL}`);
734
813
 
735
814
  // Register over WebSocket
736
815
  let tmpWSRegPayload = {
@@ -779,7 +858,7 @@ class UltravisorBeaconClient
779
858
 
780
859
  this._WebSocket.on('error', (pError) =>
781
860
  {
782
- console.error(`[Beacon] WebSocket error: ${pError.message}`);
861
+ this.log.error(`[Beacon] WebSocket error: ${pError.message}`);
783
862
  if (!tmpCallbackFired)
784
863
  {
785
864
  tmpCallbackFired = true;
@@ -789,13 +868,13 @@ class UltravisorBeaconClient
789
868
 
790
869
  this._WebSocket.on('close', () =>
791
870
  {
792
- console.log(`[Beacon] WebSocket connection closed.`);
871
+ this.log.info(`[Beacon] WebSocket connection closed.`);
793
872
  this._WebSocket = null;
794
873
 
795
- if (this._Running && !this._Authenticating)
874
+ if (this._Running && !this._Authenticating && !this._ReconnectPending)
796
875
  {
797
876
  // Connection lost while running — attempt reconnect
798
- this._wsReconnect();
877
+ this._scheduleReconnect();
799
878
  }
800
879
  });
801
880
  }
@@ -821,7 +900,7 @@ class UltravisorBeaconClient
821
900
  if (tmpData.EventType === 'BeaconRegistered')
822
901
  {
823
902
  this._BeaconID = tmpData.BeaconID;
824
- console.log(`[Beacon] Registered via WebSocket as ${this._BeaconID}`);
903
+ this.log.info(`[Beacon] Registered via WebSocket as ${this._BeaconID}`);
825
904
  if (typeof fRegistrationCallback === 'function')
826
905
  {
827
906
  fRegistrationCallback({ BeaconID: this._BeaconID });
@@ -831,14 +910,14 @@ class UltravisorBeaconClient
831
910
  {
832
911
  if (this._ActiveWorkItems >= this._Config.MaxConcurrent)
833
912
  {
834
- console.log(`[Beacon] At max concurrent capacity, ignoring pushed work item.`);
913
+ this.log.info(`[Beacon] At max concurrent capacity, ignoring pushed work item.`);
835
914
  return;
836
915
  }
837
916
  this._executeWorkItem(tmpData.WorkItem);
838
917
  }
839
918
  else if (tmpData.EventType === 'Deregistered')
840
919
  {
841
- console.log(`[Beacon] Deregistered by server.`);
920
+ this.log.info(`[Beacon] Deregistered by server.`);
842
921
  this._BeaconID = null;
843
922
  }
844
923
  }
@@ -932,6 +1011,34 @@ class UltravisorBeaconClient
932
1011
  }
933
1012
  }
934
1013
 
1014
+ /**
1015
+ * Schedule a reconnection with exponential backoff.
1016
+ * Prevents multiple close events from triggering parallel reconnections.
1017
+ */
1018
+ _scheduleReconnect()
1019
+ {
1020
+ if (this._ReconnectPending || this._Authenticating)
1021
+ {
1022
+ return;
1023
+ }
1024
+
1025
+ this._ReconnectPending = true;
1026
+ this._ReconnectAttempts++;
1027
+
1028
+ // Exponential backoff: 10s, 20s, 40s, 80s, 160s, capped at 5 min
1029
+ let tmpDelay = Math.min(
1030
+ this._Config.ReconnectIntervalMs * Math.pow(2, this._ReconnectAttempts - 1),
1031
+ this._MaxReconnectDelayMs);
1032
+
1033
+ this.log.info(`[Beacon] Scheduling reconnection attempt ${this._ReconnectAttempts} in ${Math.round(tmpDelay / 1000)}s`);
1034
+
1035
+ setTimeout(() =>
1036
+ {
1037
+ this._ReconnectPending = false;
1038
+ this._wsReconnect();
1039
+ }, tmpDelay);
1040
+ }
1041
+
935
1042
  /**
936
1043
  * Reconnect the WebSocket after unexpected disconnection.
937
1044
  * Falls back to HTTP polling if WebSocket can't be re-established.
@@ -951,15 +1058,15 @@ class UltravisorBeaconClient
951
1058
  }
952
1059
 
953
1060
  this._SessionCookie = null;
954
- console.log(`[Beacon] WebSocket disconnected re-authenticating...`);
1061
+ this.log.info(`[Beacon] Reconnectingattempt ${this._ReconnectAttempts}...`);
955
1062
 
956
1063
  this._authenticate((pAuthError) =>
957
1064
  {
958
1065
  if (pAuthError)
959
1066
  {
960
- console.error(`[Beacon] Re-authentication failed: ${pAuthError.message}`);
1067
+ this.log.error(`[Beacon] Re-authentication failed: ${pAuthError.message}`);
961
1068
  this._Authenticating = false;
962
- setTimeout(() => { this._wsReconnect(); }, this._Config.ReconnectIntervalMs);
1069
+ this._scheduleReconnect();
963
1070
  return;
964
1071
  }
965
1072
 
@@ -971,21 +1078,23 @@ class UltravisorBeaconClient
971
1078
  if (pError)
972
1079
  {
973
1080
  // WebSocket failed — fall back to HTTP polling
974
- console.log(`[Beacon] WebSocket reconnection failed, falling back to HTTP polling.`);
1081
+ this.log.info(`[Beacon] WebSocket reconnection failed, falling back to HTTP polling.`);
975
1082
  this._UseWebSocket = false;
976
1083
  this._startHTTP((pHTTPError, pHTTPBeacon) =>
977
1084
  {
978
1085
  if (pHTTPError)
979
1086
  {
980
- console.error(`[Beacon] HTTP fallback failed: ${pHTTPError.message}`);
981
- setTimeout(() => { this._wsReconnect(); }, this._Config.ReconnectIntervalMs);
1087
+ this.log.error(`[Beacon] HTTP fallback failed: ${pHTTPError.message}`);
1088
+ this._scheduleReconnect();
982
1089
  return;
983
1090
  }
984
- console.log(`[Beacon] Reconnected via HTTP polling as ${pHTTPBeacon.BeaconID}`);
1091
+ this._ReconnectAttempts = 0;
1092
+ this.log.info(`[Beacon] Reconnected via HTTP polling as ${pHTTPBeacon.BeaconID}`);
985
1093
  });
986
1094
  return;
987
1095
  }
988
- console.log(`[Beacon] WebSocket reconnected as ${pBeacon.BeaconID}`);
1096
+ this._ReconnectAttempts = 0;
1097
+ this.log.info(`[Beacon] WebSocket reconnected as ${pBeacon.BeaconID}`);
989
1098
  });
990
1099
  });
991
1100
  }
@@ -24,7 +24,12 @@ class UltravisorBeaconExecutor
24
24
  {
25
25
  this._Config = pConfig || {};
26
26
  this._StagingPath = this._Config.StagingPath || process.cwd();
27
- this._ProviderRegistry = new libBeaconProviderRegistry();
27
+ this.log = this._Config.Log || {
28
+ info: (...pArgs) => { console.log(...pArgs); },
29
+ warn: (...pArgs) => { this.log.warn(...pArgs); },
30
+ error: (...pArgs) => { console.error(...pArgs); }
31
+ };
32
+ this._ProviderRegistry = new libBeaconProviderRegistry(this.log);
28
33
  }
29
34
 
30
35
  /**
@@ -432,7 +437,7 @@ class UltravisorBeaconExecutor
432
437
  catch (pError)
433
438
  {
434
439
  // Best-effort cleanup
435
- console.warn(`[Beacon Executor] Could not clean up work directory: ${pError.message}`);
440
+ this.log.warn(`[Beacon Executor] Could not clean up work directory: ${pError.message}`);
436
441
  }
437
442
  }
438
443
 
@@ -457,7 +462,7 @@ class UltravisorBeaconExecutor
457
462
  }
458
463
  catch (pError)
459
464
  {
460
- console.warn(`[Beacon Executor] Could not clean up affinity dir [${tmpEntries[i]}]: ${pError.message}`);
465
+ this.log.warn(`[Beacon Executor] Could not clean up affinity dir [${tmpEntries[i]}]: ${pError.message}`);
461
466
  }
462
467
  }
463
468
  }
@@ -15,8 +15,14 @@ const libPath = require('path');
15
15
 
16
16
  class UltravisorBeaconProviderRegistry
17
17
  {
18
- constructor()
18
+ constructor(pLog)
19
19
  {
20
+ this.log = pLog || {
21
+ info: (...pArgs) => { console.log(...pArgs); },
22
+ warn: (...pArgs) => { console.warn(...pArgs); },
23
+ error: (...pArgs) => { console.error(...pArgs); }
24
+ };
25
+
20
26
  // Map of 'Capability:Action' → { provider, actionDef }
21
27
  this._ActionHandlers = {};
22
28
 
@@ -40,7 +46,7 @@ class UltravisorBeaconProviderRegistry
40
46
  {
41
47
  if (!pProvider || !pProvider.Capability)
42
48
  {
43
- console.error('[ProviderRegistry] Provider must have a Capability.');
49
+ this.log.error('[ProviderRegistry] Provider must have a Capability.');
44
50
  return false;
45
51
  }
46
52
 
@@ -49,7 +55,7 @@ class UltravisorBeaconProviderRegistry
49
55
 
50
56
  if (tmpActionNames.length === 0)
51
57
  {
52
- console.warn(`[ProviderRegistry] Provider "${pProvider.Name}" declares no actions.`);
58
+ this.log.warn(`[ProviderRegistry] Provider "${pProvider.Name}" declares no actions.`);
53
59
  }
54
60
 
55
61
  // Index each action by composite key
@@ -83,7 +89,7 @@ class UltravisorBeaconProviderRegistry
83
89
 
84
90
  this._Providers[pProvider.Name] = pProvider;
85
91
 
86
- console.log(`[ProviderRegistry] Registered "${pProvider.Name}" → ` +
92
+ this.log.info(`[ProviderRegistry] Registered "${pProvider.Name}" → ` +
87
93
  `${pProvider.Capability} [${tmpActionNames.join(', ')}]`);
88
94
 
89
95
  return true;
@@ -190,7 +196,7 @@ class UltravisorBeaconProviderRegistry
190
196
 
191
197
  if (!tmpSource)
192
198
  {
193
- console.error('[ProviderRegistry] Provider descriptor must have a Source.');
199
+ this.log.error('[ProviderRegistry] Provider descriptor must have a Source.');
194
200
  return false;
195
201
  }
196
202
 
@@ -222,13 +228,13 @@ class UltravisorBeaconProviderRegistry
222
228
  }
223
229
  catch (pError)
224
230
  {
225
- console.error(`[ProviderRegistry] Failed to load provider from "${tmpSource}": ${pError.message}`);
231
+ this.log.error(`[ProviderRegistry] Failed to load provider from "${tmpSource}": ${pError.message}`);
226
232
  return false;
227
233
  }
228
234
 
229
235
  if (!tmpProviderModule)
230
236
  {
231
- console.error(`[ProviderRegistry] Could not load provider from: ${tmpSource}`);
237
+ this.log.error(`[ProviderRegistry] Could not load provider from: ${tmpSource}`);
232
238
  return false;
233
239
  }
234
240
 
@@ -255,7 +261,7 @@ class UltravisorBeaconProviderRegistry
255
261
  }
256
262
  else
257
263
  {
258
- console.error(`[ProviderRegistry] Invalid provider export from "${tmpSource}": ` +
264
+ this.log.error(`[ProviderRegistry] Invalid provider export from "${tmpSource}": ` +
259
265
  `must be a class, factory function, or object with execute().`);
260
266
  return false;
261
267
  }
@@ -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
@@ -254,10 +256,23 @@ class UltravisorBeaconService extends libFableServiceBase
254
256
  Contexts: this.options.Contexts || {},
255
257
  BindAddresses: this.options.BindAddresses || [],
256
258
  Operations: this._Operations.length > 0 ? this._Operations : undefined,
259
+ Log: this.log,
257
260
  // Pass empty Providers array — we'll register adapters directly
258
261
  Providers: []
259
262
  });
260
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
+
261
276
  // Create thin client
262
277
  this._ThinClient = new libBeaconClient(tmpClientConfig);
263
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
  );