ultravisor-beacon-capability 1.0.0

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.
Files changed (38) hide show
  1. package/README.md +106 -0
  2. package/docs/.nojekyll +0 -0
  3. package/docs/README.md +103 -0
  4. package/docs/_brand.json +18 -0
  5. package/docs/_cover.md +13 -0
  6. package/docs/_sidebar.md +31 -0
  7. package/docs/_topbar.md +5 -0
  8. package/docs/_version.json +7 -0
  9. package/docs/api/README.md +44 -0
  10. package/docs/api/action-convention.md +148 -0
  11. package/docs/api/add-action.md +68 -0
  12. package/docs/api/beacon-capability.md +89 -0
  13. package/docs/api/build-action-map.md +88 -0
  14. package/docs/api/connect.md +81 -0
  15. package/docs/api/disconnect.md +50 -0
  16. package/docs/api/is-connected.md +33 -0
  17. package/docs/api/lifecycle-hooks.md +115 -0
  18. package/docs/architecture.md +237 -0
  19. package/docs/css/docuserve.css +327 -0
  20. package/docs/examples/README.md +58 -0
  21. package/docs/examples/certificate-expiry-monitor.md +212 -0
  22. package/docs/examples/docker-container-management.md +265 -0
  23. package/docs/examples/log-archive-and-upload.md +214 -0
  24. package/docs/examples/log-file-cleanup.md +199 -0
  25. package/docs/examples/mysql-maintenance.md +253 -0
  26. package/docs/examples/postgresql-aggregation.md +247 -0
  27. package/docs/examples/rest-api-health-check.md +213 -0
  28. package/docs/examples/rest-endpoint-sync.md +240 -0
  29. package/docs/examples/server-metrics-collection.md +199 -0
  30. package/docs/examples/shell-commands.md +176 -0
  31. package/docs/index.html +39 -0
  32. package/docs/quickstart.md +199 -0
  33. package/docs/retold-catalog.json +85 -0
  34. package/docs/retold-keyword-index.json +10642 -0
  35. package/package.json +33 -0
  36. package/source/Ultravisor-Beacon-Capability-ActionMap.cjs +132 -0
  37. package/source/Ultravisor-Beacon-Capability.cjs +276 -0
  38. package/test/Ultravisor-Beacon-Capability_tests.js +744 -0
@@ -0,0 +1,240 @@
1
+ # Example: REST Endpoint Sync
2
+
3
+ A capability that fetches data from one REST API and pushes it to another. Common use case: syncing records between services, pulling data from a vendor API into an internal system, or replicating configuration between environments.
4
+
5
+ ## Full Source
6
+
7
+ ```javascript
8
+ const libFable = require('fable');
9
+ const libBeaconCapability = require('ultravisor-beacon-capability');
10
+ const libHTTPS = require('https');
11
+ const libHTTP = require('http');
12
+ const libURL = require('url');
13
+
14
+ class RESTSync extends libBeaconCapability
15
+ {
16
+ constructor(pFable, pOptions, pServiceHash)
17
+ {
18
+ super(pFable, pOptions, pServiceHash);
19
+ this.serviceType = 'RESTSync';
20
+ this.capabilityName = 'RESTSync';
21
+ }
22
+
23
+ /**
24
+ * Internal helper: make an HTTP request and return parsed JSON.
25
+ */
26
+ _request(pURL, pMethod, pHeaders, pBody, fCallback)
27
+ {
28
+ let tmpParsed = libURL.parse(pURL);
29
+ let tmpLib = (tmpParsed.protocol === 'https:') ? libHTTPS : libHTTP;
30
+
31
+ let tmpHeaders = Object.assign({ 'Content-Type': 'application/json' }, pHeaders || {});
32
+
33
+ let tmpOptions = {
34
+ hostname: tmpParsed.hostname,
35
+ port: tmpParsed.port,
36
+ path: tmpParsed.path,
37
+ method: pMethod,
38
+ headers: tmpHeaders,
39
+ timeout: 30000
40
+ };
41
+
42
+ let tmpReq = tmpLib.request(tmpOptions, (pRes) =>
43
+ {
44
+ let tmpData = '';
45
+ pRes.on('data', (pChunk) => { tmpData += pChunk; });
46
+ pRes.on('end', () =>
47
+ {
48
+ let tmpParsedBody = null;
49
+ try { tmpParsedBody = JSON.parse(tmpData); }
50
+ catch (pParseError) { tmpParsedBody = tmpData; }
51
+
52
+ if (pRes.statusCode >= 400)
53
+ {
54
+ return fCallback(new Error(`HTTP ${pRes.statusCode}: ${tmpData.substring(0, 500)}`));
55
+ }
56
+ return fCallback(null, { StatusCode: pRes.statusCode, Body: tmpParsedBody });
57
+ });
58
+ });
59
+
60
+ tmpReq.on('error', (pError) => fCallback(pError));
61
+ tmpReq.on('timeout', () => { tmpReq.destroy(); fCallback(new Error('Request timed out')); });
62
+
63
+ if (pBody)
64
+ {
65
+ tmpReq.write(typeof pBody === 'string' ? pBody : JSON.stringify(pBody));
66
+ }
67
+ tmpReq.end();
68
+ }
69
+
70
+ // --- Action: FetchAndPost ---
71
+
72
+ get actionFetchAndPost_Description()
73
+ {
74
+ return 'Fetch records from a source API and POST each to a destination API';
75
+ }
76
+
77
+ get actionFetchAndPost_Schema()
78
+ {
79
+ return [
80
+ { Name: 'SourceURL', DataType: 'String', Required: true, Description: 'GET endpoint that returns an array of records' },
81
+ { Name: 'SourceHeaders', DataType: 'Object', Required: false },
82
+ { Name: 'DestinationURL', DataType: 'String', Required: true, Description: 'POST endpoint for each record' },
83
+ { Name: 'DestinationHeaders', DataType: 'Object', Required: false },
84
+ { Name: 'RecordsPath', DataType: 'String', Required: false, Description: 'Dot-path to the array in the source response (e.g. "data.items")' },
85
+ { Name: 'DryRun', DataType: 'Boolean', Required: false, Default: false }
86
+ ];
87
+ }
88
+
89
+ actionFetchAndPost(pSettings, pWorkItem, fCallback, fReportProgress)
90
+ {
91
+ fReportProgress({ Percent: 5, Message: 'Fetching from source...' });
92
+
93
+ this._request(pSettings.SourceURL, 'GET', pSettings.SourceHeaders, null, (pFetchError, pFetchResult) =>
94
+ {
95
+ if (pFetchError)
96
+ {
97
+ return fCallback(pFetchError);
98
+ }
99
+
100
+ // Extract records array
101
+ let tmpRecords = pFetchResult.Body;
102
+ if (pSettings.RecordsPath)
103
+ {
104
+ let tmpParts = pSettings.RecordsPath.split('.');
105
+ for (let i = 0; i < tmpParts.length; i++)
106
+ {
107
+ tmpRecords = tmpRecords ? tmpRecords[tmpParts[i]] : undefined;
108
+ }
109
+ }
110
+
111
+ if (!Array.isArray(tmpRecords))
112
+ {
113
+ return fCallback(new Error(`Source did not return an array at path "${pSettings.RecordsPath || '(root)'}"`));
114
+ }
115
+
116
+ fReportProgress({ Percent: 20, Message: `Fetched ${tmpRecords.length} records` });
117
+
118
+ if (pSettings.DryRun)
119
+ {
120
+ return fCallback(null, {
121
+ Outputs: { DryRun: true, RecordCount: tmpRecords.length, SampleRecord: tmpRecords[0] || null },
122
+ Log: [`DRY RUN: Would POST ${tmpRecords.length} records to ${pSettings.DestinationURL}`]
123
+ });
124
+ }
125
+
126
+ // POST each record sequentially
127
+ let tmpIndex = 0;
128
+ let tmpSuccessCount = 0;
129
+ let tmpErrors = [];
130
+
131
+ let fnPostNext = () =>
132
+ {
133
+ if (tmpIndex >= tmpRecords.length)
134
+ {
135
+ return fCallback(null, {
136
+ Outputs: {
137
+ TotalRecords: tmpRecords.length,
138
+ SuccessCount: tmpSuccessCount,
139
+ ErrorCount: tmpErrors.length,
140
+ Errors: tmpErrors.slice(0, 20)
141
+ },
142
+ Log: [`Synced ${tmpSuccessCount}/${tmpRecords.length} records to ${pSettings.DestinationURL}`]
143
+ });
144
+ }
145
+
146
+ let tmpRecord = tmpRecords[tmpIndex];
147
+ tmpIndex++;
148
+
149
+ this._request(pSettings.DestinationURL, 'POST', pSettings.DestinationHeaders, tmpRecord, (pPostError) =>
150
+ {
151
+ if (pPostError)
152
+ {
153
+ tmpErrors.push({ Index: tmpIndex - 1, Error: pPostError.message });
154
+ }
155
+ else
156
+ {
157
+ tmpSuccessCount++;
158
+ }
159
+
160
+ let tmpPercent = 20 + Math.round((tmpIndex / tmpRecords.length) * 75);
161
+ fReportProgress({ Percent: tmpPercent, Message: `Posted ${tmpIndex} / ${tmpRecords.length}` });
162
+
163
+ setImmediate(fnPostNext);
164
+ });
165
+ };
166
+
167
+ fnPostNext();
168
+ });
169
+ }
170
+
171
+ // --- Action: MirrorEndpoint ---
172
+
173
+ get actionMirrorEndpoint_Description()
174
+ {
175
+ return 'GET from source and PUT the entire response body to a destination';
176
+ }
177
+
178
+ get actionMirrorEndpoint_Schema()
179
+ {
180
+ return [
181
+ { Name: 'SourceURL', DataType: 'String', Required: true },
182
+ { Name: 'SourceHeaders', DataType: 'Object', Required: false },
183
+ { Name: 'DestinationURL', DataType: 'String', Required: true },
184
+ { Name: 'DestinationHeaders', DataType: 'Object', Required: false }
185
+ ];
186
+ }
187
+
188
+ actionMirrorEndpoint(pSettings, pWorkItem, fCallback)
189
+ {
190
+ this._request(pSettings.SourceURL, 'GET', pSettings.SourceHeaders, null, (pFetchError, pFetchResult) =>
191
+ {
192
+ if (pFetchError) return fCallback(pFetchError);
193
+
194
+ this._request(pSettings.DestinationURL, 'PUT', pSettings.DestinationHeaders, pFetchResult.Body, (pPutError, pPutResult) =>
195
+ {
196
+ if (pPutError) return fCallback(pPutError);
197
+ return fCallback(null, {
198
+ Outputs: {
199
+ SourceStatus: pFetchResult.StatusCode,
200
+ DestinationStatus: pPutResult.StatusCode
201
+ },
202
+ Log: [`Mirrored ${pSettings.SourceURL} -> ${pSettings.DestinationURL}`]
203
+ });
204
+ });
205
+ });
206
+ }
207
+ }
208
+
209
+ // --- Startup ---
210
+
211
+ let tmpFable = new libFable({ Product: 'RESTSync', ProductVersion: '1.0.0' });
212
+ tmpFable.addServiceType('RESTSync', RESTSync);
213
+ let tmpCap = tmpFable.instantiateServiceProvider('RESTSync');
214
+
215
+ tmpCap.connect(
216
+ {
217
+ ServerURL: process.env.ULTRAVISOR_URL || 'http://localhost:54321',
218
+ Name: 'rest-sync-worker'
219
+ },
220
+ (pError) =>
221
+ {
222
+ if (pError) throw pError;
223
+ console.log('REST sync beacon online');
224
+ });
225
+
226
+ process.on('SIGTERM', () => { tmpCap.disconnect(() => process.exit(0)); });
227
+ ```
228
+
229
+ ## Registered Task Types
230
+
231
+ - `beacon-restsync-fetchandpost`
232
+ - `beacon-restsync-mirrorendpoint`
233
+
234
+ ## Key Points
235
+
236
+ - **No external HTTP library** -- uses Node.js built-in modules
237
+ - **RecordsPath** supports dot-notation for extracting arrays from nested JSON (e.g. `data.results`)
238
+ - **Sequential POSTs** avoid overwhelming the destination API; use `MaxConcurrent` on the beacon config for parallelism at the work item level
239
+ - **DryRun** fetches from source but skips destination writes
240
+ - **Error collection** is capped at 20 entries to avoid massive outputs
@@ -0,0 +1,199 @@
1
+ # Example: Server Metrics Collection
2
+
3
+ A capability that collects CPU, memory, disk, and process metrics from the host machine. Schedule it to run every few minutes to build a lightweight time series of infrastructure health in Ultravisor.
4
+
5
+ ## Full Source
6
+
7
+ ```javascript
8
+ const libFable = require('fable');
9
+ const libBeaconCapability = require('ultravisor-beacon-capability');
10
+ const libChildProcess = require('child_process');
11
+ const libOS = require('os');
12
+
13
+ class ServerMetrics extends libBeaconCapability
14
+ {
15
+ constructor(pFable, pOptions, pServiceHash)
16
+ {
17
+ super(pFable, pOptions, pServiceHash);
18
+ this.serviceType = 'ServerMetrics';
19
+ this.capabilityName = 'ServerMetrics';
20
+ }
21
+
22
+ // --- Action: CollectAll ---
23
+
24
+ get actionCollectAll_Description()
25
+ {
26
+ return 'Collect CPU, memory, disk, and load average metrics from the host';
27
+ }
28
+
29
+ actionCollectAll(pSettings, pWorkItem, fCallback)
30
+ {
31
+ let tmpMetrics = {
32
+ Timestamp: new Date().toISOString(),
33
+ Hostname: libOS.hostname(),
34
+ Platform: libOS.platform(),
35
+ Arch: libOS.arch(),
36
+ Uptime: libOS.uptime(),
37
+ LoadAverage: libOS.loadavg(),
38
+ CPUs: libOS.cpus().length,
39
+ TotalMemoryMB: Math.round(libOS.totalmem() / (1024 * 1024)),
40
+ FreeMemoryMB: Math.round(libOS.freemem() / (1024 * 1024)),
41
+ MemoryUsagePercent: Math.round((1 - libOS.freemem() / libOS.totalmem()) * 100)
42
+ };
43
+
44
+ // Get disk usage
45
+ libChildProcess.exec("df -h / | tail -1 | awk '{print $2, $3, $4, $5}'", (pError, pStdOut) =>
46
+ {
47
+ if (!pError && pStdOut.trim())
48
+ {
49
+ let tmpParts = pStdOut.trim().split(/\s+/);
50
+ tmpMetrics.DiskTotal = tmpParts[0] || 'unknown';
51
+ tmpMetrics.DiskUsed = tmpParts[1] || 'unknown';
52
+ tmpMetrics.DiskAvailable = tmpParts[2] || 'unknown';
53
+ tmpMetrics.DiskUsagePercent = tmpParts[3] || 'unknown';
54
+ }
55
+
56
+ return fCallback(null, {
57
+ Outputs: tmpMetrics,
58
+ Log: [
59
+ `${tmpMetrics.Hostname}: CPU load ${tmpMetrics.LoadAverage[0].toFixed(2)}, ` +
60
+ `Memory ${tmpMetrics.MemoryUsagePercent}%, ` +
61
+ `Disk ${tmpMetrics.DiskUsagePercent || 'N/A'}`
62
+ ]
63
+ });
64
+ });
65
+ }
66
+
67
+ // --- Action: TopProcesses ---
68
+
69
+ get actionTopProcesses_Description()
70
+ {
71
+ return 'List the top processes by CPU or memory usage';
72
+ }
73
+
74
+ get actionTopProcesses_Schema()
75
+ {
76
+ return [
77
+ { Name: 'SortBy', DataType: 'String', Required: false, Default: 'cpu', Description: '"cpu" or "memory"' },
78
+ { Name: 'Count', DataType: 'Integer', Required: false, Default: 10 }
79
+ ];
80
+ }
81
+
82
+ actionTopProcesses(pSettings, pWorkItem, fCallback)
83
+ {
84
+ let tmpSortFlag = (pSettings.SortBy === 'memory') ? '-m' : '-r';
85
+ let tmpCount = parseInt(pSettings.Count, 10) || 10;
86
+
87
+ // macOS and Linux have different ps flags; use a portable approach
88
+ let tmpCmd = `ps aux --sort=${(pSettings.SortBy === 'memory') ? '-%mem' : '-%cpu'} | head -${tmpCount + 1}`;
89
+
90
+ libChildProcess.exec(tmpCmd, (pError, pStdOut) =>
91
+ {
92
+ if (pError)
93
+ {
94
+ // Fallback for macOS
95
+ let tmpFallback = `ps aux | sort -nrk ${(pSettings.SortBy === 'memory') ? '4' : '3'} | head -${tmpCount}`;
96
+ libChildProcess.exec(tmpFallback, (pFallbackError, pFallbackOut) =>
97
+ {
98
+ if (pFallbackError) return fCallback(pFallbackError);
99
+ return fCallback(null, {
100
+ Outputs: { Processes: pFallbackOut, SortedBy: pSettings.SortBy || 'cpu' },
101
+ Log: [`Top ${tmpCount} processes by ${pSettings.SortBy || 'cpu'}`]
102
+ });
103
+ });
104
+ return;
105
+ }
106
+
107
+ return fCallback(null, {
108
+ Outputs: { Processes: pStdOut, SortedBy: pSettings.SortBy || 'cpu' },
109
+ Log: [`Top ${tmpCount} processes by ${pSettings.SortBy || 'cpu'}`]
110
+ });
111
+ });
112
+ }
113
+
114
+ // --- Action: NetworkConnections ---
115
+
116
+ get actionNetworkConnections_Description()
117
+ {
118
+ return 'Count active network connections by state';
119
+ }
120
+
121
+ actionNetworkConnections(pSettings, pWorkItem, fCallback)
122
+ {
123
+ let tmpCmd = "netstat -an 2>/dev/null | awk '/tcp/ {print $6}' | sort | uniq -c | sort -rn";
124
+
125
+ libChildProcess.exec(tmpCmd, (pError, pStdOut) =>
126
+ {
127
+ if (pError)
128
+ {
129
+ // Fallback: try ss on Linux
130
+ let tmpFallback = "ss -tan | awk 'NR>1 {print $1}' | sort | uniq -c | sort -rn";
131
+ libChildProcess.exec(tmpFallback, (pFallbackError, pFallbackOut) =>
132
+ {
133
+ if (pFallbackError) return fCallback(pFallbackError);
134
+ return this._parseConnectionCounts(pFallbackOut, fCallback);
135
+ });
136
+ return;
137
+ }
138
+
139
+ this._parseConnectionCounts(pStdOut, fCallback);
140
+ });
141
+ }
142
+
143
+ _parseConnectionCounts(pOutput, fCallback)
144
+ {
145
+ let tmpStates = {};
146
+ let tmpTotal = 0;
147
+ let tmpLines = pOutput.trim().split('\n');
148
+
149
+ for (let i = 0; i < tmpLines.length; i++)
150
+ {
151
+ let tmpMatch = tmpLines[i].trim().match(/(\d+)\s+(.*)/);
152
+ if (tmpMatch)
153
+ {
154
+ let tmpCount = parseInt(tmpMatch[1], 10);
155
+ tmpStates[tmpMatch[2]] = tmpCount;
156
+ tmpTotal += tmpCount;
157
+ }
158
+ }
159
+
160
+ return fCallback(null, {
161
+ Outputs: { States: tmpStates, TotalConnections: tmpTotal },
162
+ Log: [`Network connections: ${tmpTotal} total`]
163
+ });
164
+ }
165
+ }
166
+
167
+ // --- Startup ---
168
+
169
+ let tmpFable = new libFable({ Product: 'ServerMetrics', ProductVersion: '1.0.0' });
170
+ tmpFable.addServiceType('ServerMetrics', ServerMetrics);
171
+ let tmpCap = tmpFable.instantiateServiceProvider('ServerMetrics');
172
+
173
+ tmpCap.connect(
174
+ {
175
+ ServerURL: process.env.ULTRAVISOR_URL || 'http://localhost:54321',
176
+ Name: `metrics-${libOS.hostname()}`
177
+ },
178
+ (pError) =>
179
+ {
180
+ if (pError) throw pError;
181
+ console.log('Server metrics beacon online');
182
+ });
183
+
184
+ process.on('SIGTERM', () => { tmpCap.disconnect(() => process.exit(0)); });
185
+ ```
186
+
187
+ ## Registered Task Types
188
+
189
+ - `beacon-servermetrics-collectall`
190
+ - `beacon-servermetrics-topprocesses`
191
+ - `beacon-servermetrics-networkconnections`
192
+
193
+ ## Key Points
194
+
195
+ - **CollectAll** uses the Node.js `os` module for cross-platform metrics, with `df` for disk info
196
+ - **TopProcesses** includes a macOS fallback when Linux-style `ps --sort` is unavailable
197
+ - **NetworkConnections** tries `netstat` first, then falls back to `ss`
198
+ - Schedule `CollectAll` every 5 minutes in Ultravisor to build a lightweight metrics history
199
+ - The beacon name includes the hostname, so multiple servers can register the same capability
@@ -0,0 +1,176 @@
1
+ # Example: Shell Commands
2
+
3
+ Wrap basic shell commands as beacon actions. This is the simplest possible capability -- each action executes a shell command and returns the output.
4
+
5
+ ## Full Source
6
+
7
+ ```javascript
8
+ const libFable = require('fable');
9
+ const libBeaconCapability = require('ultravisor-beacon-capability');
10
+ const libChildProcess = require('child_process');
11
+
12
+ class ShellCommands extends libBeaconCapability
13
+ {
14
+ constructor(pFable, pOptions, pServiceHash)
15
+ {
16
+ super(pFable, pOptions, pServiceHash);
17
+ this.serviceType = 'ShellCommands';
18
+ this.capabilityName = 'ShellCommands';
19
+ }
20
+
21
+ // --- Action: Ping ---
22
+
23
+ get actionPing_Description()
24
+ {
25
+ return 'Ping a host and return the result';
26
+ }
27
+
28
+ get actionPing_Schema()
29
+ {
30
+ return [
31
+ { Name: 'Host', DataType: 'String', Required: true },
32
+ { Name: 'Count', DataType: 'Integer', Required: false, Default: 4 }
33
+ ];
34
+ }
35
+
36
+ actionPing(pSettings, pWorkItem, fCallback)
37
+ {
38
+ let tmpCount = pSettings.Count || 4;
39
+ let tmpCmd = `ping -c ${tmpCount} ${pSettings.Host}`;
40
+
41
+ this.log.info(`Pinging ${pSettings.Host} (${tmpCount} packets)...`);
42
+
43
+ libChildProcess.exec(tmpCmd, { timeout: 30000 }, (pError, pStdOut, pStdErr) =>
44
+ {
45
+ if (pError)
46
+ {
47
+ return fCallback(null, {
48
+ Outputs: { Success: false, StdOut: pStdOut || '', StdErr: pStdErr || '', ExitCode: pError.code },
49
+ Log: [`Ping failed: ${pError.message}`]
50
+ });
51
+ }
52
+ return fCallback(null, {
53
+ Outputs: { Success: true, StdOut: pStdOut, ExitCode: 0 },
54
+ Log: [`Pinged ${pSettings.Host} successfully`]
55
+ });
56
+ });
57
+ }
58
+
59
+ // --- Action: Uptime ---
60
+
61
+ get actionUptime_Description()
62
+ {
63
+ return 'Return the system uptime';
64
+ }
65
+
66
+ actionUptime(pSettings, pWorkItem, fCallback)
67
+ {
68
+ libChildProcess.exec('uptime', (pError, pStdOut) =>
69
+ {
70
+ if (pError) return fCallback(pError);
71
+ return fCallback(null, {
72
+ Outputs: { Uptime: pStdOut.trim() },
73
+ Log: ['Retrieved system uptime']
74
+ });
75
+ });
76
+ }
77
+
78
+ // --- Action: Whoami ---
79
+
80
+ get actionWhoami_Description()
81
+ {
82
+ return 'Return the current user and hostname';
83
+ }
84
+
85
+ actionWhoami(pSettings, pWorkItem, fCallback)
86
+ {
87
+ libChildProcess.exec('whoami && hostname', (pError, pStdOut) =>
88
+ {
89
+ if (pError) return fCallback(pError);
90
+ let tmpLines = pStdOut.trim().split('\n');
91
+ return fCallback(null, {
92
+ Outputs: { User: tmpLines[0], Hostname: tmpLines[1] || '' },
93
+ Log: ['Retrieved user and hostname']
94
+ });
95
+ });
96
+ }
97
+
98
+ // --- Action: RunCommand ---
99
+
100
+ get actionRunCommand_Description()
101
+ {
102
+ return 'Execute an arbitrary shell command with a timeout';
103
+ }
104
+
105
+ get actionRunCommand_Schema()
106
+ {
107
+ return [
108
+ { Name: 'Command', DataType: 'String', Required: true },
109
+ { Name: 'TimeoutSeconds', DataType: 'Integer', Required: false, Default: 60 },
110
+ { Name: 'WorkingDirectory', DataType: 'String', Required: false }
111
+ ];
112
+ }
113
+
114
+ actionRunCommand(pSettings, pWorkItem, fCallback)
115
+ {
116
+ let tmpOptions = {
117
+ timeout: (pSettings.TimeoutSeconds || 60) * 1000,
118
+ maxBuffer: 10 * 1024 * 1024
119
+ };
120
+
121
+ if (pSettings.WorkingDirectory)
122
+ {
123
+ tmpOptions.cwd = pSettings.WorkingDirectory;
124
+ }
125
+
126
+ this.log.info(`Executing: ${pSettings.Command}`);
127
+
128
+ libChildProcess.exec(pSettings.Command, tmpOptions, (pError, pStdOut, pStdErr) =>
129
+ {
130
+ let tmpExitCode = pError ? (pError.code || 1) : 0;
131
+ return fCallback(null, {
132
+ Outputs: {
133
+ StdOut: pStdOut || '',
134
+ StdErr: pStdErr || '',
135
+ ExitCode: tmpExitCode,
136
+ Success: tmpExitCode === 0
137
+ },
138
+ Log: [`Command exited with code ${tmpExitCode}`]
139
+ });
140
+ });
141
+ }
142
+ }
143
+
144
+ // --- Startup ---
145
+
146
+ let tmpFable = new libFable({ Product: 'ShellCommands', ProductVersion: '1.0.0' });
147
+ tmpFable.addServiceType('ShellCommands', ShellCommands);
148
+ let tmpCap = tmpFable.instantiateServiceProvider('ShellCommands');
149
+
150
+ tmpCap.connect(
151
+ {
152
+ ServerURL: process.env.ULTRAVISOR_URL || 'http://localhost:54321',
153
+ Name: `shell-${require('os').hostname()}`,
154
+ MaxConcurrent: 4
155
+ },
156
+ (pError, pInfo) =>
157
+ {
158
+ if (pError) throw pError;
159
+ console.log(`Shell commands beacon online: ${pInfo.BeaconID}`);
160
+ });
161
+
162
+ process.on('SIGTERM', () => { tmpCap.disconnect(() => process.exit(0)); });
163
+ ```
164
+
165
+ ## Registered Task Types
166
+
167
+ - `beacon-shellcommands-ping`
168
+ - `beacon-shellcommands-uptime`
169
+ - `beacon-shellcommands-whoami`
170
+ - `beacon-shellcommands-runcommand`
171
+
172
+ ## Key Points
173
+
174
+ - `exec` timeout prevents long-running commands from hanging
175
+ - Failed commands return results with `Success: false` rather than calling `fCallback(pError)` -- this ensures the work item completes (with error data in Outputs) rather than being marked as a hard failure
176
+ - `maxBuffer` is raised to 10MB for commands that produce large output
@@ -0,0 +1,39 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
7
+ <meta name="description" content="Ultravisor Beacon Capability v0.0.1 Documentation — Convention-based base class for building Ultravisor beacon capabilities with minimal boilerplate">
8
+
9
+ <title>Ultravisor Beacon Capability v0.0.1 Documentation</title>
10
+
11
+ <!-- Application Stylesheet -->
12
+ <link href="css/docuserve.css" rel="stylesheet">
13
+ <!-- KaTeX stylesheet for LaTeX equation rendering -->
14
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.css">
15
+ <!-- PICT Dynamic View CSS Container -->
16
+ <style id="PICT-CSS"></style>
17
+
18
+ <!-- Load the PICT library from jsDelivr CDN -->
19
+ <script src="https://cdn.jsdelivr.net/npm/pict@1/dist/pict.min.js" type="text/javascript"></script>
20
+ <!-- Bootstrap the Application -->
21
+ <script type="text/javascript">
22
+ //<![CDATA[
23
+ Pict.safeOnDocumentReady(() => { Pict.safeLoadPictApplication(PictDocuserve, 2)});
24
+ //]]>
25
+ </script>
26
+ </head>
27
+ <body>
28
+ <!-- The root container for the Pict application -->
29
+ <div id="Docuserve-Application-Container"></div>
30
+
31
+ <!-- Mermaid diagram rendering -->
32
+ <script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
33
+ <script>mermaid.initialize({ startOnLoad: false, theme: 'default' });</script>
34
+ <!-- KaTeX for LaTeX equation rendering -->
35
+ <script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.js"></script>
36
+ <!-- Load the Docuserve PICT Application Bundle from jsDelivr CDN -->
37
+ <script src="https://cdn.jsdelivr.net/npm/pict-docuserve@0/dist/pict-docuserve.min.js" type="text/javascript"></script>
38
+ </body>
39
+ </html>