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,500 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ultravisor Beacon Executor
|
|
3
|
+
*
|
|
4
|
+
* Routes work items to the appropriate capability provider via the
|
|
5
|
+
* ProviderRegistry. Replaces the former hard-coded switch statement
|
|
6
|
+
* with a pluggable, composable provider architecture.
|
|
7
|
+
*
|
|
8
|
+
* Supports file transfer for remote dispatch scenarios:
|
|
9
|
+
* - Pre-execute: downloads source files from a SourceURL
|
|
10
|
+
* - Post-execute: base64-encodes output files into Outputs
|
|
11
|
+
* - Affinity-scoped download caching for repeated operations on the same file
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const libFS = require('fs');
|
|
15
|
+
const libPath = require('path');
|
|
16
|
+
const libHTTP = require('http');
|
|
17
|
+
const libHTTPS = require('https');
|
|
18
|
+
|
|
19
|
+
const libBeaconProviderRegistry = require('./Ultravisor-Beacon-ProviderRegistry.cjs');
|
|
20
|
+
|
|
21
|
+
class UltravisorBeaconExecutor
|
|
22
|
+
{
|
|
23
|
+
constructor(pConfig)
|
|
24
|
+
{
|
|
25
|
+
this._Config = pConfig || {};
|
|
26
|
+
this._StagingPath = this._Config.StagingPath || process.cwd();
|
|
27
|
+
this._ProviderRegistry = new libBeaconProviderRegistry();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get the provider registry.
|
|
32
|
+
* Used by BeaconClient for capability list and provider lifecycle.
|
|
33
|
+
*/
|
|
34
|
+
get providerRegistry()
|
|
35
|
+
{
|
|
36
|
+
return this._ProviderRegistry;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Execute a work item by routing to the appropriate provider.
|
|
41
|
+
*
|
|
42
|
+
* If the work item's Settings include file transfer directives
|
|
43
|
+
* (SourceURL, OutputFilename), the executor handles downloading
|
|
44
|
+
* the source file before execution and collecting the output file
|
|
45
|
+
* after execution. This is transparent to providers.
|
|
46
|
+
*
|
|
47
|
+
* @param {object} pWorkItem - { WorkItemHash, Capability, Action, Settings, TimeoutMs }
|
|
48
|
+
* @param {function} fCallback - function(pError, pResult) where pResult = { Outputs, Log }
|
|
49
|
+
* @param {function} [fReportProgress] - Optional progress callback passed through to provider
|
|
50
|
+
*/
|
|
51
|
+
execute(pWorkItem, fCallback, fReportProgress)
|
|
52
|
+
{
|
|
53
|
+
let tmpCapability = pWorkItem.Capability || 'Shell';
|
|
54
|
+
let tmpAction = pWorkItem.Action || '';
|
|
55
|
+
|
|
56
|
+
let tmpResolved = this._ProviderRegistry.resolve(tmpCapability, tmpAction);
|
|
57
|
+
|
|
58
|
+
if (!tmpResolved)
|
|
59
|
+
{
|
|
60
|
+
return fCallback(null, {
|
|
61
|
+
Outputs: {
|
|
62
|
+
StdOut: `Unknown capability: ${tmpCapability}` +
|
|
63
|
+
(tmpAction ? `/${tmpAction}` : ''),
|
|
64
|
+
ExitCode: -1,
|
|
65
|
+
Result: ''
|
|
66
|
+
},
|
|
67
|
+
Log: [`Beacon Executor: no provider for [${tmpCapability}` +
|
|
68
|
+
(tmpAction ? `/${tmpAction}` : '') + `].`]
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let tmpContext = {
|
|
73
|
+
StagingPath: this._StagingPath
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
let tmpSettings = pWorkItem.Settings || {};
|
|
77
|
+
|
|
78
|
+
// Check if file transfer is needed
|
|
79
|
+
if (tmpSettings.SourceURL || tmpSettings.OutputFilename)
|
|
80
|
+
{
|
|
81
|
+
return this._executeWithFileTransfer(
|
|
82
|
+
pWorkItem, tmpResolved, tmpContext, fCallback, fReportProgress);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Standard execution — no file transfer
|
|
86
|
+
tmpResolved.provider.execute(
|
|
87
|
+
tmpResolved.action, pWorkItem, tmpContext, fCallback, fReportProgress);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ================================================================
|
|
91
|
+
// File Transfer Execution
|
|
92
|
+
// ================================================================
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Execute a work item with file transfer support.
|
|
96
|
+
*
|
|
97
|
+
* 1. Download source file (if SourceURL specified)
|
|
98
|
+
* 2. Substitute {SourcePath} and {OutputPath} in Command
|
|
99
|
+
* 3. Execute the provider
|
|
100
|
+
* 4. Collect output file (if OutputFilename specified)
|
|
101
|
+
* 5. Clean up work directory
|
|
102
|
+
*/
|
|
103
|
+
_executeWithFileTransfer(pWorkItem, pResolved, pContext, fCallback, fReportProgress)
|
|
104
|
+
{
|
|
105
|
+
let tmpSelf = this;
|
|
106
|
+
let tmpSettings = pWorkItem.Settings || {};
|
|
107
|
+
let tmpLog = [];
|
|
108
|
+
|
|
109
|
+
// Phase 1: Prepare — download source file and set up paths
|
|
110
|
+
this._prepareFileTransfer(pWorkItem, tmpLog,
|
|
111
|
+
(pPrepareError) =>
|
|
112
|
+
{
|
|
113
|
+
if (pPrepareError)
|
|
114
|
+
{
|
|
115
|
+
return fCallback(null, {
|
|
116
|
+
Outputs: {
|
|
117
|
+
StdOut: `File transfer preparation failed: ${pPrepareError.message}`,
|
|
118
|
+
ExitCode: -1,
|
|
119
|
+
Result: ''
|
|
120
|
+
},
|
|
121
|
+
Log: tmpLog.concat([`File transfer error: ${pPrepareError.message}`])
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Phase 2: Execute the provider
|
|
126
|
+
pResolved.provider.execute(
|
|
127
|
+
pResolved.action, pWorkItem, pContext,
|
|
128
|
+
(pExecError, pResult) =>
|
|
129
|
+
{
|
|
130
|
+
if (pExecError)
|
|
131
|
+
{
|
|
132
|
+
tmpSelf._cleanupWorkDir(pWorkItem.WorkItemHash);
|
|
133
|
+
return fCallback(pExecError);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Phase 3: Collect output files
|
|
137
|
+
tmpSelf._collectOutputFiles(pWorkItem, pResult, tmpLog,
|
|
138
|
+
(pCollectError, pFinalResult) =>
|
|
139
|
+
{
|
|
140
|
+
// Clean up work directory (keep affinity staging)
|
|
141
|
+
tmpSelf._cleanupWorkDir(pWorkItem.WorkItemHash);
|
|
142
|
+
|
|
143
|
+
if (pCollectError)
|
|
144
|
+
{
|
|
145
|
+
return fCallback(null, {
|
|
146
|
+
Outputs: Object.assign(pResult.Outputs || {},
|
|
147
|
+
{
|
|
148
|
+
StdOut: (pResult.Outputs ? pResult.Outputs.StdOut || '' : '') +
|
|
149
|
+
'\nOutput collection failed: ' + pCollectError.message,
|
|
150
|
+
ExitCode: -1
|
|
151
|
+
}),
|
|
152
|
+
Log: (pResult.Log || []).concat(tmpLog)
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Merge file transfer log into result
|
|
157
|
+
pFinalResult.Log = (pFinalResult.Log || []).concat(tmpLog);
|
|
158
|
+
return fCallback(null, pFinalResult);
|
|
159
|
+
});
|
|
160
|
+
}, fReportProgress);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Prepare for file transfer: download source file, substitute paths.
|
|
166
|
+
*
|
|
167
|
+
* @param {object} pWorkItem - The work item (Settings will be modified in-place)
|
|
168
|
+
* @param {Array} pLog - Log array to append messages to
|
|
169
|
+
* @param {function} fCallback - function(pError)
|
|
170
|
+
*/
|
|
171
|
+
_prepareFileTransfer(pWorkItem, pLog, fCallback)
|
|
172
|
+
{
|
|
173
|
+
let tmpSettings = pWorkItem.Settings || {};
|
|
174
|
+
let tmpCommand = tmpSettings.Command || '';
|
|
175
|
+
|
|
176
|
+
// Set up output path (always, even if no source download)
|
|
177
|
+
if (tmpSettings.OutputFilename)
|
|
178
|
+
{
|
|
179
|
+
let tmpWorkDir = this._getWorkDir(pWorkItem);
|
|
180
|
+
let tmpOutputPath = libPath.join(tmpWorkDir, tmpSettings.OutputFilename);
|
|
181
|
+
tmpCommand = tmpCommand.replace(/\{OutputPath\}/g, tmpOutputPath);
|
|
182
|
+
// Store resolved path for later collection
|
|
183
|
+
tmpSettings._ResolvedOutputPath = tmpOutputPath;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Download source file if URL specified
|
|
187
|
+
if (tmpSettings.SourceURL)
|
|
188
|
+
{
|
|
189
|
+
let tmpSourceFilename = tmpSettings.SourceFilename || 'source_file';
|
|
190
|
+
let tmpDownloadDir;
|
|
191
|
+
let tmpSourcePath;
|
|
192
|
+
|
|
193
|
+
// Use affinity-scoped directory if AffinityKey is present
|
|
194
|
+
if (pWorkItem.Settings.AffinityKey)
|
|
195
|
+
{
|
|
196
|
+
tmpDownloadDir = this._getAffinityDir(pWorkItem);
|
|
197
|
+
}
|
|
198
|
+
else
|
|
199
|
+
{
|
|
200
|
+
tmpDownloadDir = this._getWorkDir(pWorkItem);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
tmpSourcePath = libPath.join(tmpDownloadDir, tmpSourceFilename);
|
|
204
|
+
|
|
205
|
+
// Check if file already exists (affinity cache hit)
|
|
206
|
+
if (libFS.existsSync(tmpSourcePath))
|
|
207
|
+
{
|
|
208
|
+
pLog.push(`Source file cached (affinity): ${tmpSourceFilename}`);
|
|
209
|
+
tmpCommand = tmpCommand.replace(/\{SourcePath\}/g, tmpSourcePath);
|
|
210
|
+
tmpSettings.Command = tmpCommand;
|
|
211
|
+
tmpSettings._ResolvedSourcePath = tmpSourcePath;
|
|
212
|
+
return fCallback(null);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Download the file
|
|
216
|
+
pLog.push(`Downloading source: ${tmpSettings.SourceURL}`);
|
|
217
|
+
|
|
218
|
+
this._downloadFile(tmpSettings.SourceURL, tmpSourcePath,
|
|
219
|
+
(pDownloadError) =>
|
|
220
|
+
{
|
|
221
|
+
if (pDownloadError)
|
|
222
|
+
{
|
|
223
|
+
return fCallback(pDownloadError);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
pLog.push(`Downloaded: ${tmpSourceFilename} (${this._formatFileSize(tmpSourcePath)})`);
|
|
227
|
+
tmpCommand = tmpCommand.replace(/\{SourcePath\}/g, tmpSourcePath);
|
|
228
|
+
tmpSettings.Command = tmpCommand;
|
|
229
|
+
tmpSettings._ResolvedSourcePath = tmpSourcePath;
|
|
230
|
+
return fCallback(null);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
else
|
|
234
|
+
{
|
|
235
|
+
// No download needed — just update the command
|
|
236
|
+
tmpSettings.Command = tmpCommand;
|
|
237
|
+
return fCallback(null);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Collect output files after execution, base64-encoding if requested.
|
|
243
|
+
*
|
|
244
|
+
* @param {object} pWorkItem - The work item
|
|
245
|
+
* @param {object} pResult - The execution result { Outputs, Log }
|
|
246
|
+
* @param {Array} pLog - Log array to append messages to
|
|
247
|
+
* @param {function} fCallback - function(pError, pFinalResult)
|
|
248
|
+
*/
|
|
249
|
+
_collectOutputFiles(pWorkItem, pResult, pLog, fCallback)
|
|
250
|
+
{
|
|
251
|
+
let tmpSettings = pWorkItem.Settings || {};
|
|
252
|
+
|
|
253
|
+
if (!tmpSettings.OutputFilename || !tmpSettings.ReturnOutputAsBase64)
|
|
254
|
+
{
|
|
255
|
+
return fCallback(null, pResult);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
let tmpOutputPath = tmpSettings._ResolvedOutputPath;
|
|
259
|
+
|
|
260
|
+
if (!tmpOutputPath || !libFS.existsSync(tmpOutputPath))
|
|
261
|
+
{
|
|
262
|
+
pLog.push(`Output file not found: ${tmpSettings.OutputFilename}`);
|
|
263
|
+
return fCallback(new Error(`Output file not found: ${tmpSettings.OutputFilename}`));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
try
|
|
267
|
+
{
|
|
268
|
+
let tmpBuffer = libFS.readFileSync(tmpOutputPath);
|
|
269
|
+
let tmpBase64 = tmpBuffer.toString('base64');
|
|
270
|
+
|
|
271
|
+
pLog.push(`Output collected: ${tmpSettings.OutputFilename} (${this._formatFileSize(tmpOutputPath)})`);
|
|
272
|
+
|
|
273
|
+
// Merge into result
|
|
274
|
+
let tmpOutputs = pResult.Outputs || {};
|
|
275
|
+
tmpOutputs.OutputData = tmpBase64;
|
|
276
|
+
tmpOutputs.OutputFilename = tmpSettings.OutputFilename;
|
|
277
|
+
tmpOutputs.OutputSize = tmpBuffer.length;
|
|
278
|
+
|
|
279
|
+
return fCallback(null, {
|
|
280
|
+
Outputs: tmpOutputs,
|
|
281
|
+
Log: pResult.Log || []
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
catch (pReadError)
|
|
285
|
+
{
|
|
286
|
+
return fCallback(pReadError);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ================================================================
|
|
291
|
+
// File Download
|
|
292
|
+
// ================================================================
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Download a file from a URL to a local path.
|
|
296
|
+
* Streams to disk to handle large files.
|
|
297
|
+
*
|
|
298
|
+
* @param {string} pURL - The URL to download from
|
|
299
|
+
* @param {string} pOutputPath - Local file path to write to
|
|
300
|
+
* @param {function} fCallback - function(pError)
|
|
301
|
+
*/
|
|
302
|
+
_downloadFile(pURL, pOutputPath, fCallback)
|
|
303
|
+
{
|
|
304
|
+
let tmpLib = pURL.startsWith('https') ? libHTTPS : libHTTP;
|
|
305
|
+
|
|
306
|
+
// Ensure the directory exists
|
|
307
|
+
let tmpDir = libPath.dirname(pOutputPath);
|
|
308
|
+
if (!libFS.existsSync(tmpDir))
|
|
309
|
+
{
|
|
310
|
+
libFS.mkdirSync(tmpDir, { recursive: true });
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
let tmpFileStream = libFS.createWriteStream(pOutputPath);
|
|
314
|
+
let tmpCallbackFired = false;
|
|
315
|
+
|
|
316
|
+
let tmpComplete = (pError) =>
|
|
317
|
+
{
|
|
318
|
+
if (tmpCallbackFired)
|
|
319
|
+
{
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
tmpCallbackFired = true;
|
|
323
|
+
|
|
324
|
+
if (pError)
|
|
325
|
+
{
|
|
326
|
+
tmpFileStream.close();
|
|
327
|
+
// Clean up partial download
|
|
328
|
+
try { libFS.unlinkSync(pOutputPath); }
|
|
329
|
+
catch (pErr) { /* ignore */ }
|
|
330
|
+
return fCallback(pError);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return fCallback(null);
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
tmpLib.get(pURL, (pResponse) =>
|
|
337
|
+
{
|
|
338
|
+
// Handle redirects
|
|
339
|
+
if (pResponse.statusCode >= 300 && pResponse.statusCode < 400 && pResponse.headers.location)
|
|
340
|
+
{
|
|
341
|
+
tmpFileStream.close();
|
|
342
|
+
try { libFS.unlinkSync(pOutputPath); }
|
|
343
|
+
catch (pErr) { /* ignore */ }
|
|
344
|
+
return this._downloadFile(pResponse.headers.location, pOutputPath, fCallback);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (pResponse.statusCode !== 200)
|
|
348
|
+
{
|
|
349
|
+
return tmpComplete(new Error(`Download failed: HTTP ${pResponse.statusCode} for ${pURL}`));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
pResponse.pipe(tmpFileStream);
|
|
353
|
+
|
|
354
|
+
tmpFileStream.on('finish', () =>
|
|
355
|
+
{
|
|
356
|
+
tmpFileStream.close(() =>
|
|
357
|
+
{
|
|
358
|
+
tmpComplete(null);
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
pResponse.on('error', tmpComplete);
|
|
363
|
+
tmpFileStream.on('error', tmpComplete);
|
|
364
|
+
|
|
365
|
+
}).on('error', tmpComplete);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ================================================================
|
|
369
|
+
// Staging Directory Management
|
|
370
|
+
// ================================================================
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Get or create the affinity-scoped staging directory.
|
|
374
|
+
* Files here persist across work items with the same affinity key.
|
|
375
|
+
*
|
|
376
|
+
* @param {object} pWorkItem - Work item with Settings.AffinityKey
|
|
377
|
+
* @returns {string} Absolute path to the affinity directory
|
|
378
|
+
*/
|
|
379
|
+
_getAffinityDir(pWorkItem)
|
|
380
|
+
{
|
|
381
|
+
let tmpAffinityKey = (pWorkItem.Settings && pWorkItem.Settings.AffinityKey) || 'default';
|
|
382
|
+
// Sanitize the affinity key for use as a directory name
|
|
383
|
+
let tmpSafeKey = tmpAffinityKey.replace(/[^a-zA-Z0-9_-]/g, '_').substring(0, 64);
|
|
384
|
+
let tmpDir = libPath.join(this._StagingPath, `affinity-${tmpSafeKey}`);
|
|
385
|
+
|
|
386
|
+
if (!libFS.existsSync(tmpDir))
|
|
387
|
+
{
|
|
388
|
+
libFS.mkdirSync(tmpDir, { recursive: true });
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return tmpDir;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Get or create the work-item-scoped staging directory.
|
|
396
|
+
* Files here are cleaned up after the work item completes.
|
|
397
|
+
*
|
|
398
|
+
* @param {object} pWorkItem - Work item with WorkItemHash
|
|
399
|
+
* @returns {string} Absolute path to the work directory
|
|
400
|
+
*/
|
|
401
|
+
_getWorkDir(pWorkItem)
|
|
402
|
+
{
|
|
403
|
+
let tmpHash = pWorkItem.WorkItemHash || 'unknown';
|
|
404
|
+
let tmpDir = libPath.join(this._StagingPath, `work-${tmpHash}`);
|
|
405
|
+
|
|
406
|
+
if (!libFS.existsSync(tmpDir))
|
|
407
|
+
{
|
|
408
|
+
libFS.mkdirSync(tmpDir, { recursive: true });
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return tmpDir;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Clean up a work item's staging directory.
|
|
416
|
+
*
|
|
417
|
+
* @param {string} pWorkItemHash - The work item hash
|
|
418
|
+
*/
|
|
419
|
+
_cleanupWorkDir(pWorkItemHash)
|
|
420
|
+
{
|
|
421
|
+
let tmpDir = libPath.join(this._StagingPath, `work-${pWorkItemHash}`);
|
|
422
|
+
|
|
423
|
+
if (!libFS.existsSync(tmpDir))
|
|
424
|
+
{
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
try
|
|
429
|
+
{
|
|
430
|
+
libFS.rmSync(tmpDir, { recursive: true, force: true });
|
|
431
|
+
}
|
|
432
|
+
catch (pError)
|
|
433
|
+
{
|
|
434
|
+
// Best-effort cleanup
|
|
435
|
+
console.warn(`[Beacon Executor] Could not clean up work directory: ${pError.message}`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Clean up all affinity staging directories.
|
|
441
|
+
* Called during beacon shutdown.
|
|
442
|
+
*/
|
|
443
|
+
cleanupAffinityDirs()
|
|
444
|
+
{
|
|
445
|
+
try
|
|
446
|
+
{
|
|
447
|
+
let tmpEntries = libFS.readdirSync(this._StagingPath);
|
|
448
|
+
|
|
449
|
+
for (let i = 0; i < tmpEntries.length; i++)
|
|
450
|
+
{
|
|
451
|
+
if (tmpEntries[i].startsWith('affinity-'))
|
|
452
|
+
{
|
|
453
|
+
let tmpDir = libPath.join(this._StagingPath, tmpEntries[i]);
|
|
454
|
+
try
|
|
455
|
+
{
|
|
456
|
+
libFS.rmSync(tmpDir, { recursive: true, force: true });
|
|
457
|
+
}
|
|
458
|
+
catch (pError)
|
|
459
|
+
{
|
|
460
|
+
console.warn(`[Beacon Executor] Could not clean up affinity dir [${tmpEntries[i]}]: ${pError.message}`);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
catch (pError)
|
|
466
|
+
{
|
|
467
|
+
// Staging path doesn't exist or can't be read — fine
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// ================================================================
|
|
472
|
+
// Utilities
|
|
473
|
+
// ================================================================
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Format a file size for logging.
|
|
477
|
+
*
|
|
478
|
+
* @param {string} pFilePath - Path to the file
|
|
479
|
+
* @returns {string} Human-readable file size
|
|
480
|
+
*/
|
|
481
|
+
_formatFileSize(pFilePath)
|
|
482
|
+
{
|
|
483
|
+
try
|
|
484
|
+
{
|
|
485
|
+
let tmpStat = libFS.statSync(pFilePath);
|
|
486
|
+
let tmpSize = tmpStat.size;
|
|
487
|
+
|
|
488
|
+
if (tmpSize < 1024) return `${tmpSize} B`;
|
|
489
|
+
if (tmpSize < 1024 * 1024) return `${(tmpSize / 1024).toFixed(1)} KB`;
|
|
490
|
+
if (tmpSize < 1024 * 1024 * 1024) return `${(tmpSize / (1024 * 1024)).toFixed(1)} MB`;
|
|
491
|
+
return `${(tmpSize / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
492
|
+
}
|
|
493
|
+
catch (pError)
|
|
494
|
+
{
|
|
495
|
+
return 'unknown size';
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
module.exports = UltravisorBeaconExecutor;
|