ultravisor-beacon 0.0.1
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 +30 -0
- package/source/Ultravisor-Beacon-CLI.cjs +143 -0
- package/source/Ultravisor-Beacon-CapabilityAdapter.cjs +116 -0
- package/source/Ultravisor-Beacon-CapabilityManager.cjs +132 -0
- package/source/Ultravisor-Beacon-CapabilityProvider.cjs +129 -0
- package/source/Ultravisor-Beacon-Client.cjs +568 -0
- package/source/Ultravisor-Beacon-ConnectivityHTTP.cjs +52 -0
- package/source/Ultravisor-Beacon-Executor.cjs +500 -0
- package/source/Ultravisor-Beacon-ProviderRegistry.cjs +330 -0
- package/source/Ultravisor-Beacon-Service.cjs +288 -0
- package/source/providers/Ultravisor-Beacon-Provider-FileSystem.cjs +331 -0
- package/source/providers/Ultravisor-Beacon-Provider-LLM.cjs +966 -0
- package/source/providers/Ultravisor-Beacon-Provider-Shell.cjs +95 -0
- package/test/Ultravisor-Beacon-Service_tests.js +608 -0
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ultravisor Beacon Client
|
|
3
|
+
*
|
|
4
|
+
* A lightweight worker node that connects to an Ultravisor server,
|
|
5
|
+
* registers its capabilities, polls for work, executes tasks locally,
|
|
6
|
+
* and reports results back to the orchestrator.
|
|
7
|
+
*
|
|
8
|
+
* Capabilities are provided by pluggable CapabilityProviders loaded
|
|
9
|
+
* via the ProviderRegistry. The beacon advertises the aggregate set
|
|
10
|
+
* of capabilities from all loaded providers.
|
|
11
|
+
*
|
|
12
|
+
* Communication is HTTP-based (transport-agnostic design means this
|
|
13
|
+
* can be swapped for WebSocket, MQTT, etc. in the future).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const libHTTP = require('http');
|
|
17
|
+
|
|
18
|
+
const libBeaconExecutor = require('./Ultravisor-Beacon-Executor.cjs');
|
|
19
|
+
|
|
20
|
+
class UltravisorBeaconClient
|
|
21
|
+
{
|
|
22
|
+
constructor(pConfig)
|
|
23
|
+
{
|
|
24
|
+
this._Config = Object.assign({
|
|
25
|
+
ServerURL: 'http://localhost:54321',
|
|
26
|
+
Name: 'beacon-worker',
|
|
27
|
+
Password: '',
|
|
28
|
+
Capabilities: ['Shell'],
|
|
29
|
+
MaxConcurrent: 1,
|
|
30
|
+
PollIntervalMs: 5000,
|
|
31
|
+
HeartbeatIntervalMs: 30000,
|
|
32
|
+
StagingPath: process.cwd(),
|
|
33
|
+
Tags: {}
|
|
34
|
+
}, pConfig || {});
|
|
35
|
+
|
|
36
|
+
this._BeaconID = null;
|
|
37
|
+
this._PollInterval = null;
|
|
38
|
+
this._HeartbeatInterval = null;
|
|
39
|
+
this._Running = false;
|
|
40
|
+
this._ActiveWorkItems = 0;
|
|
41
|
+
this._SessionCookie = null;
|
|
42
|
+
this._Authenticating = false;
|
|
43
|
+
|
|
44
|
+
this._Executor = new libBeaconExecutor({
|
|
45
|
+
StagingPath: this._Config.StagingPath
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Load capability providers
|
|
49
|
+
this._loadProviders();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ================================================================
|
|
53
|
+
// Provider Loading
|
|
54
|
+
// ================================================================
|
|
55
|
+
|
|
56
|
+
_loadProviders()
|
|
57
|
+
{
|
|
58
|
+
let tmpProviders = this._Config.Providers;
|
|
59
|
+
|
|
60
|
+
if (!tmpProviders)
|
|
61
|
+
{
|
|
62
|
+
// Backward compatibility: convert Capabilities array to Provider descriptors
|
|
63
|
+
let tmpCapabilities = this._Config.Capabilities || ['Shell'];
|
|
64
|
+
tmpProviders = tmpCapabilities.map(function (pCap)
|
|
65
|
+
{
|
|
66
|
+
return { Source: pCap, Config: {} };
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let tmpCount = this._Executor.providerRegistry.loadProviders(tmpProviders);
|
|
71
|
+
console.log(`[Beacon] Loaded ${tmpCount} capability provider(s).`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ================================================================
|
|
75
|
+
// Lifecycle
|
|
76
|
+
// ================================================================
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Start the Beacon client: initialize providers, register, then begin polling.
|
|
80
|
+
*/
|
|
81
|
+
start(fCallback)
|
|
82
|
+
{
|
|
83
|
+
console.log(`[Beacon] Starting "${this._Config.Name}"...`);
|
|
84
|
+
console.log(`[Beacon] Server: ${this._Config.ServerURL}`);
|
|
85
|
+
|
|
86
|
+
// Initialize all providers before registering
|
|
87
|
+
this._Executor.providerRegistry.initializeAll((pInitError) =>
|
|
88
|
+
{
|
|
89
|
+
if (pInitError)
|
|
90
|
+
{
|
|
91
|
+
console.error(`[Beacon] Provider initialization failed: ${pInitError.message}`);
|
|
92
|
+
return fCallback(pInitError);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let tmpCapabilities = this._Executor.providerRegistry.getCapabilities();
|
|
96
|
+
console.log(`[Beacon] Capabilities: ${tmpCapabilities.join(', ')}`);
|
|
97
|
+
|
|
98
|
+
// Authenticate before registering
|
|
99
|
+
this._authenticate((pAuthError) =>
|
|
100
|
+
{
|
|
101
|
+
if (pAuthError)
|
|
102
|
+
{
|
|
103
|
+
console.error(`[Beacon] Authentication failed: ${pAuthError.message}`);
|
|
104
|
+
return fCallback(pAuthError);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
console.log(`[Beacon] Authenticated successfully.`);
|
|
108
|
+
|
|
109
|
+
this._register((pError, pBeacon) =>
|
|
110
|
+
{
|
|
111
|
+
if (pError)
|
|
112
|
+
{
|
|
113
|
+
console.error(`[Beacon] Registration failed: ${pError.message}`);
|
|
114
|
+
return fCallback(pError);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
this._BeaconID = pBeacon.BeaconID;
|
|
118
|
+
this._Running = true;
|
|
119
|
+
|
|
120
|
+
console.log(`[Beacon] Registered as ${this._BeaconID}`);
|
|
121
|
+
|
|
122
|
+
// Start polling for work
|
|
123
|
+
this._PollInterval = setInterval(() =>
|
|
124
|
+
{
|
|
125
|
+
this._poll();
|
|
126
|
+
}, this._Config.PollIntervalMs);
|
|
127
|
+
|
|
128
|
+
// Start heartbeat
|
|
129
|
+
this._HeartbeatInterval = setInterval(() =>
|
|
130
|
+
{
|
|
131
|
+
this._heartbeat();
|
|
132
|
+
}, this._Config.HeartbeatIntervalMs);
|
|
133
|
+
|
|
134
|
+
// Do an immediate poll
|
|
135
|
+
this._poll();
|
|
136
|
+
|
|
137
|
+
return fCallback(null, pBeacon);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Stop the Beacon client: stop polling, shutdown providers, deregister.
|
|
145
|
+
*/
|
|
146
|
+
stop(fCallback)
|
|
147
|
+
{
|
|
148
|
+
console.log(`[Beacon] Stopping...`);
|
|
149
|
+
this._Running = false;
|
|
150
|
+
|
|
151
|
+
if (this._PollInterval)
|
|
152
|
+
{
|
|
153
|
+
clearInterval(this._PollInterval);
|
|
154
|
+
this._PollInterval = null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (this._HeartbeatInterval)
|
|
158
|
+
{
|
|
159
|
+
clearInterval(this._HeartbeatInterval);
|
|
160
|
+
this._HeartbeatInterval = null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Clean up affinity staging directories
|
|
164
|
+
this._Executor.cleanupAffinityDirs();
|
|
165
|
+
|
|
166
|
+
// Shutdown providers
|
|
167
|
+
this._Executor.providerRegistry.shutdownAll((pShutdownError) =>
|
|
168
|
+
{
|
|
169
|
+
if (pShutdownError)
|
|
170
|
+
{
|
|
171
|
+
console.warn(`[Beacon] Provider shutdown warning: ${pShutdownError.message}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (this._BeaconID)
|
|
175
|
+
{
|
|
176
|
+
this._deregister((pError) =>
|
|
177
|
+
{
|
|
178
|
+
if (pError)
|
|
179
|
+
{
|
|
180
|
+
console.warn(`[Beacon] Deregistration warning: ${pError.message}`);
|
|
181
|
+
}
|
|
182
|
+
console.log(`[Beacon] Stopped.`);
|
|
183
|
+
if (fCallback) return fCallback(null);
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
else
|
|
187
|
+
{
|
|
188
|
+
console.log(`[Beacon] Stopped.`);
|
|
189
|
+
if (fCallback) return fCallback(null);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ================================================================
|
|
195
|
+
// Authentication
|
|
196
|
+
// ================================================================
|
|
197
|
+
|
|
198
|
+
_authenticate(fCallback)
|
|
199
|
+
{
|
|
200
|
+
let tmpBody = {
|
|
201
|
+
UserName: this._Config.Name,
|
|
202
|
+
Password: this._Config.Password || ''
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
let tmpBodyString = JSON.stringify(tmpBody);
|
|
206
|
+
let tmpParsedURL = new URL(this._Config.ServerURL);
|
|
207
|
+
let tmpOptions = {
|
|
208
|
+
hostname: tmpParsedURL.hostname,
|
|
209
|
+
port: tmpParsedURL.port || 80,
|
|
210
|
+
path: '/1.0/Authenticate',
|
|
211
|
+
method: 'POST',
|
|
212
|
+
headers: {
|
|
213
|
+
'Content-Type': 'application/json',
|
|
214
|
+
'Content-Length': Buffer.byteLength(tmpBodyString)
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
let tmpReq = libHTTP.request(tmpOptions, (pResponse) =>
|
|
219
|
+
{
|
|
220
|
+
let tmpData = '';
|
|
221
|
+
pResponse.on('data', (pChunk) => { tmpData += pChunk; });
|
|
222
|
+
pResponse.on('end', () =>
|
|
223
|
+
{
|
|
224
|
+
if (pResponse.statusCode >= 400)
|
|
225
|
+
{
|
|
226
|
+
return fCallback(new Error(`Authentication failed with HTTP ${pResponse.statusCode}`));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Extract session cookie from Set-Cookie headers
|
|
230
|
+
let tmpSetCookieHeaders = pResponse.headers['set-cookie'];
|
|
231
|
+
if (tmpSetCookieHeaders && tmpSetCookieHeaders.length > 0)
|
|
232
|
+
{
|
|
233
|
+
// Take the name=value portion (before the first semicolon)
|
|
234
|
+
let tmpCookieParts = tmpSetCookieHeaders[0].split(';');
|
|
235
|
+
this._SessionCookie = tmpCookieParts[0].trim();
|
|
236
|
+
console.log(`[Beacon] Session cookie acquired.`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
try
|
|
240
|
+
{
|
|
241
|
+
let tmpParsed = JSON.parse(tmpData);
|
|
242
|
+
return fCallback(null, tmpParsed);
|
|
243
|
+
}
|
|
244
|
+
catch (pParseError)
|
|
245
|
+
{
|
|
246
|
+
return fCallback(new Error(`Invalid JSON in auth response: ${tmpData.substring(0, 200)}`));
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
tmpReq.on('error', (pError) =>
|
|
252
|
+
{
|
|
253
|
+
return fCallback(pError);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
tmpReq.write(tmpBodyString);
|
|
257
|
+
tmpReq.end();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ================================================================
|
|
261
|
+
// Reconnection
|
|
262
|
+
// ================================================================
|
|
263
|
+
|
|
264
|
+
_reconnect()
|
|
265
|
+
{
|
|
266
|
+
if (this._Authenticating)
|
|
267
|
+
{
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
this._Authenticating = true;
|
|
271
|
+
|
|
272
|
+
// Clear existing intervals
|
|
273
|
+
if (this._PollInterval)
|
|
274
|
+
{
|
|
275
|
+
clearInterval(this._PollInterval);
|
|
276
|
+
this._PollInterval = null;
|
|
277
|
+
}
|
|
278
|
+
if (this._HeartbeatInterval)
|
|
279
|
+
{
|
|
280
|
+
clearInterval(this._HeartbeatInterval);
|
|
281
|
+
this._HeartbeatInterval = null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
this._SessionCookie = null;
|
|
285
|
+
|
|
286
|
+
console.log(`[Beacon] Reconnecting — re-authenticating...`);
|
|
287
|
+
|
|
288
|
+
this._authenticate((pAuthError) =>
|
|
289
|
+
{
|
|
290
|
+
if (pAuthError)
|
|
291
|
+
{
|
|
292
|
+
console.error(`[Beacon] Re-authentication failed: ${pAuthError.message}`);
|
|
293
|
+
this._Authenticating = false;
|
|
294
|
+
setTimeout(() => { this._reconnect(); }, 10000);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
console.log(`[Beacon] Re-authenticated, re-registering...`);
|
|
299
|
+
|
|
300
|
+
this._register((pRegError, pBeacon) =>
|
|
301
|
+
{
|
|
302
|
+
if (pRegError)
|
|
303
|
+
{
|
|
304
|
+
console.error(`[Beacon] Re-registration failed: ${pRegError.message}`);
|
|
305
|
+
this._Authenticating = false;
|
|
306
|
+
setTimeout(() => { this._reconnect(); }, 10000);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
this._BeaconID = pBeacon.BeaconID;
|
|
311
|
+
this._Authenticating = false;
|
|
312
|
+
|
|
313
|
+
console.log(`[Beacon] Reconnected as ${this._BeaconID}`);
|
|
314
|
+
|
|
315
|
+
// Restart polling
|
|
316
|
+
this._PollInterval = setInterval(() =>
|
|
317
|
+
{
|
|
318
|
+
this._poll();
|
|
319
|
+
}, this._Config.PollIntervalMs);
|
|
320
|
+
|
|
321
|
+
// Restart heartbeat
|
|
322
|
+
this._HeartbeatInterval = setInterval(() =>
|
|
323
|
+
{
|
|
324
|
+
this._heartbeat();
|
|
325
|
+
}, this._Config.HeartbeatIntervalMs);
|
|
326
|
+
|
|
327
|
+
// Immediate poll
|
|
328
|
+
this._poll();
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ================================================================
|
|
334
|
+
// Registration
|
|
335
|
+
// ================================================================
|
|
336
|
+
|
|
337
|
+
_register(fCallback)
|
|
338
|
+
{
|
|
339
|
+
let tmpBody = {
|
|
340
|
+
Name: this._Config.Name,
|
|
341
|
+
Capabilities: this._Executor.providerRegistry.getCapabilities(),
|
|
342
|
+
MaxConcurrent: this._Config.MaxConcurrent,
|
|
343
|
+
Tags: this._Config.Tags
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
this._httpRequest('POST', '/Beacon/Register', tmpBody, fCallback);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
_deregister(fCallback)
|
|
350
|
+
{
|
|
351
|
+
this._httpRequest('DELETE', `/Beacon/${this._BeaconID}`, null, fCallback);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ================================================================
|
|
355
|
+
// Polling
|
|
356
|
+
// ================================================================
|
|
357
|
+
|
|
358
|
+
_poll()
|
|
359
|
+
{
|
|
360
|
+
if (!this._Running || !this._BeaconID)
|
|
361
|
+
{
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (this._ActiveWorkItems >= this._Config.MaxConcurrent)
|
|
366
|
+
{
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
this._httpRequest('POST', '/Beacon/Work/Poll', { BeaconID: this._BeaconID },
|
|
371
|
+
(pError, pResponse) =>
|
|
372
|
+
{
|
|
373
|
+
if (pError)
|
|
374
|
+
{
|
|
375
|
+
// Silent on poll errors — just retry next interval
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (!pResponse || !pResponse.WorkItem)
|
|
380
|
+
{
|
|
381
|
+
// No work available
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Execute the work item
|
|
386
|
+
this._executeWorkItem(pResponse.WorkItem);
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// ================================================================
|
|
391
|
+
// Work Execution
|
|
392
|
+
// ================================================================
|
|
393
|
+
|
|
394
|
+
_executeWorkItem(pWorkItem)
|
|
395
|
+
{
|
|
396
|
+
this._ActiveWorkItems++;
|
|
397
|
+
console.log(`[Beacon] Executing work item [${pWorkItem.WorkItemHash}] (${pWorkItem.Capability}/${pWorkItem.Action})`);
|
|
398
|
+
|
|
399
|
+
// Create a progress callback that sends updates to the server
|
|
400
|
+
let tmpWorkItemHash = pWorkItem.WorkItemHash;
|
|
401
|
+
let fReportProgress = (pProgressData) =>
|
|
402
|
+
{
|
|
403
|
+
this._reportProgress(tmpWorkItemHash, pProgressData);
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
this._Executor.execute(pWorkItem, (pError, pResult) =>
|
|
407
|
+
{
|
|
408
|
+
this._ActiveWorkItems--;
|
|
409
|
+
|
|
410
|
+
if (pError)
|
|
411
|
+
{
|
|
412
|
+
console.error(`[Beacon] Execution error for [${pWorkItem.WorkItemHash}]: ${pError.message}`);
|
|
413
|
+
this._reportError(pWorkItem.WorkItemHash, pError.message, []);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Check if the result indicates an error (non-zero exit code)
|
|
418
|
+
let tmpOutputs = pResult.Outputs || {};
|
|
419
|
+
if (tmpOutputs.ExitCode && tmpOutputs.ExitCode !== 0)
|
|
420
|
+
{
|
|
421
|
+
console.warn(`[Beacon] Work item [${pWorkItem.WorkItemHash}] completed with exit code ${tmpOutputs.ExitCode}`);
|
|
422
|
+
}
|
|
423
|
+
else
|
|
424
|
+
{
|
|
425
|
+
console.log(`[Beacon] Work item [${pWorkItem.WorkItemHash}] completed successfully.`);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
this._reportComplete(pWorkItem.WorkItemHash, tmpOutputs, pResult.Log || []);
|
|
429
|
+
}, fReportProgress);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// ================================================================
|
|
433
|
+
// Reporting
|
|
434
|
+
// ================================================================
|
|
435
|
+
|
|
436
|
+
_reportComplete(pWorkItemHash, pOutputs, pLog)
|
|
437
|
+
{
|
|
438
|
+
this._httpRequest('POST', `/Beacon/Work/${pWorkItemHash}/Complete`,
|
|
439
|
+
{ Outputs: pOutputs, Log: pLog },
|
|
440
|
+
(pError) =>
|
|
441
|
+
{
|
|
442
|
+
if (pError)
|
|
443
|
+
{
|
|
444
|
+
console.error(`[Beacon] Failed to report completion for [${pWorkItemHash}]: ${pError.message}`);
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
_reportError(pWorkItemHash, pErrorMessage, pLog)
|
|
450
|
+
{
|
|
451
|
+
this._httpRequest('POST', `/Beacon/Work/${pWorkItemHash}/Error`,
|
|
452
|
+
{ ErrorMessage: pErrorMessage, Log: pLog },
|
|
453
|
+
(pError) =>
|
|
454
|
+
{
|
|
455
|
+
if (pError)
|
|
456
|
+
{
|
|
457
|
+
console.error(`[Beacon] Failed to report error for [${pWorkItemHash}]: ${pError.message}`);
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
_reportProgress(pWorkItemHash, pProgressData)
|
|
463
|
+
{
|
|
464
|
+
if (!pProgressData || !this._Running)
|
|
465
|
+
{
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
this._httpRequest('POST', `/Beacon/Work/${pWorkItemHash}/Progress`,
|
|
470
|
+
pProgressData,
|
|
471
|
+
(pError) =>
|
|
472
|
+
{
|
|
473
|
+
if (pError)
|
|
474
|
+
{
|
|
475
|
+
// Fire-and-forget — log but don't affect execution
|
|
476
|
+
console.warn(`[Beacon] Failed to report progress for [${pWorkItemHash}]: ${pError.message}`);
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// ================================================================
|
|
482
|
+
// Heartbeat
|
|
483
|
+
// ================================================================
|
|
484
|
+
|
|
485
|
+
_heartbeat()
|
|
486
|
+
{
|
|
487
|
+
if (!this._Running || !this._BeaconID)
|
|
488
|
+
{
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
this._httpRequest('POST', `/Beacon/${this._BeaconID}/Heartbeat`, {},
|
|
493
|
+
(pError) =>
|
|
494
|
+
{
|
|
495
|
+
if (pError)
|
|
496
|
+
{
|
|
497
|
+
console.warn(`[Beacon] Heartbeat failed: ${pError.message}`);
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// ================================================================
|
|
503
|
+
// HTTP Transport
|
|
504
|
+
// ================================================================
|
|
505
|
+
|
|
506
|
+
_httpRequest(pMethod, pPath, pBody, fCallback)
|
|
507
|
+
{
|
|
508
|
+
let tmpParsedURL = new URL(this._Config.ServerURL);
|
|
509
|
+
let tmpOptions = {
|
|
510
|
+
hostname: tmpParsedURL.hostname,
|
|
511
|
+
port: tmpParsedURL.port || 80,
|
|
512
|
+
path: pPath,
|
|
513
|
+
method: pMethod,
|
|
514
|
+
headers: {
|
|
515
|
+
'Content-Type': 'application/json'
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
// Attach session cookie if available
|
|
520
|
+
if (this._SessionCookie)
|
|
521
|
+
{
|
|
522
|
+
tmpOptions.headers['Cookie'] = this._SessionCookie;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
let tmpReq = libHTTP.request(tmpOptions, (pResponse) =>
|
|
526
|
+
{
|
|
527
|
+
let tmpData = '';
|
|
528
|
+
pResponse.on('data', (pChunk) => { tmpData += pChunk; });
|
|
529
|
+
pResponse.on('end', () =>
|
|
530
|
+
{
|
|
531
|
+
// Detect 401 and trigger reconnection
|
|
532
|
+
if (pResponse.statusCode === 401)
|
|
533
|
+
{
|
|
534
|
+
this._reconnect();
|
|
535
|
+
return fCallback(new Error('Unauthorized — reconnecting'));
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
try
|
|
539
|
+
{
|
|
540
|
+
let tmpParsed = JSON.parse(tmpData);
|
|
541
|
+
if (pResponse.statusCode >= 400)
|
|
542
|
+
{
|
|
543
|
+
return fCallback(new Error(tmpParsed.Error || `HTTP ${pResponse.statusCode}`));
|
|
544
|
+
}
|
|
545
|
+
return fCallback(null, tmpParsed);
|
|
546
|
+
}
|
|
547
|
+
catch (pParseError)
|
|
548
|
+
{
|
|
549
|
+
return fCallback(new Error(`Invalid JSON response: ${tmpData.substring(0, 200)}`));
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
tmpReq.on('error', (pError) =>
|
|
555
|
+
{
|
|
556
|
+
return fCallback(pError);
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
if (pBody && (pMethod === 'POST' || pMethod === 'PUT'))
|
|
560
|
+
{
|
|
561
|
+
tmpReq.write(JSON.stringify(pBody));
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
tmpReq.end();
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
module.exports = UltravisorBeaconClient;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ultravisor Beacon Connectivity — HTTP Transport
|
|
3
|
+
*
|
|
4
|
+
* Thin configuration layer for HTTP-based beacon connectivity.
|
|
5
|
+
* The actual HTTP transport is handled by the thin client
|
|
6
|
+
* (Ultravisor-Beacon-Client.cjs). This class exists as the
|
|
7
|
+
* abstraction point for swapping in alternative transports
|
|
8
|
+
* (e.g. WebSocket) in the future.
|
|
9
|
+
*
|
|
10
|
+
* For the HTTP transport, the thin client's polling, heartbeat,
|
|
11
|
+
* authentication, and reconnection logic are used as-is.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
class UltravisorBeaconConnectivityHTTP
|
|
15
|
+
{
|
|
16
|
+
constructor(pOptions)
|
|
17
|
+
{
|
|
18
|
+
this._Options = Object.assign({
|
|
19
|
+
ServerURL: 'http://localhost:54321',
|
|
20
|
+
Password: '',
|
|
21
|
+
PollIntervalMs: 5000,
|
|
22
|
+
HeartbeatIntervalMs: 30000
|
|
23
|
+
}, pOptions || {});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Get the transport configuration subset needed by the thin client.
|
|
28
|
+
*
|
|
29
|
+
* @returns {object} Config suitable for BeaconClient constructor
|
|
30
|
+
*/
|
|
31
|
+
getTransportConfig()
|
|
32
|
+
{
|
|
33
|
+
return {
|
|
34
|
+
ServerURL: this._Options.ServerURL,
|
|
35
|
+
Password: this._Options.Password,
|
|
36
|
+
PollIntervalMs: this._Options.PollIntervalMs,
|
|
37
|
+
HeartbeatIntervalMs: this._Options.HeartbeatIntervalMs
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get the transport type identifier.
|
|
43
|
+
*
|
|
44
|
+
* @returns {string}
|
|
45
|
+
*/
|
|
46
|
+
getTransportType()
|
|
47
|
+
{
|
|
48
|
+
return 'HTTP';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = UltravisorBeaconConnectivityHTTP;
|