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 +1 -1
- package/source/Ultravisor-Beacon-CapabilityManager.cjs +9 -3
- package/source/Ultravisor-Beacon-Client.cjs +157 -48
- package/source/Ultravisor-Beacon-Executor.cjs +8 -3
- package/source/Ultravisor-Beacon-ProviderRegistry.cjs +14 -8
- package/source/Ultravisor-Beacon-Service.cjs +16 -1
- package/test/Ultravisor-Beacon-Service_tests.js +136 -0
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
136
|
+
this.log.error(`[Beacon] Authentication failed: ${pAuthError.message}`);
|
|
120
137
|
return fCallback(pAuthError);
|
|
121
138
|
}
|
|
122
139
|
|
|
123
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
255
|
+
this.log.warn(`[Beacon] Deregistration warning: ${pError.message}`);
|
|
239
256
|
}
|
|
240
|
-
|
|
257
|
+
this.log.info(`[Beacon] Stopped.`);
|
|
241
258
|
if (fCallback) return fCallback(null);
|
|
242
259
|
});
|
|
243
260
|
}
|
|
244
261
|
else
|
|
245
262
|
{
|
|
246
|
-
|
|
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
|
-
|
|
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
|
-
|
|
361
|
+
this.log.info(`[Beacon] Reconnecting — re-authenticating...`);
|
|
345
362
|
|
|
346
363
|
this._authenticate((pAuthError) =>
|
|
347
364
|
{
|
|
348
365
|
if (pAuthError)
|
|
349
366
|
{
|
|
350
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
591
|
+
this.log.warn(`[Beacon] Work item [${pWorkItem.WorkItemHash}] completed with exit code ${tmpOutputs.ExitCode}`);
|
|
513
592
|
}
|
|
514
593
|
else
|
|
515
594
|
{
|
|
516
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1061
|
+
this.log.info(`[Beacon] Reconnecting — attempt ${this._ReconnectAttempts}...`);
|
|
955
1062
|
|
|
956
1063
|
this._authenticate((pAuthError) =>
|
|
957
1064
|
{
|
|
958
1065
|
if (pAuthError)
|
|
959
1066
|
{
|
|
960
|
-
|
|
1067
|
+
this.log.error(`[Beacon] Re-authentication failed: ${pAuthError.message}`);
|
|
961
1068
|
this._Authenticating = false;
|
|
962
|
-
|
|
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
|
-
|
|
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
|
-
|
|
981
|
-
|
|
1087
|
+
this.log.error(`[Beacon] HTTP fallback failed: ${pHTTPError.message}`);
|
|
1088
|
+
this._scheduleReconnect();
|
|
982
1089
|
return;
|
|
983
1090
|
}
|
|
984
|
-
|
|
1091
|
+
this._ReconnectAttempts = 0;
|
|
1092
|
+
this.log.info(`[Beacon] Reconnected via HTTP polling as ${pHTTPBeacon.BeaconID}`);
|
|
985
1093
|
});
|
|
986
1094
|
return;
|
|
987
1095
|
}
|
|
988
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
);
|