@danielng23/dsh-studio-host-system-metrics 0.1.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.
- package/README.md +34 -0
- package/lib/index.js +697 -0
- package/lib/invariant.js +13 -0
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +356 -0
- package/lib/typert.remote-client.d.ts +32 -0
- package/lib/typert.remote-client.js +241 -0
- package/lib/types/index.d.ts +65 -0
- package/lib/types/index.js +533 -0
- package/lib/types/invariant.d.ts +9 -0
- package/lib/types/invariant.js +11 -0
- package/lib/types/types.d.ts +148 -0
- package/lib/types/types.js +2 -0
- package/package.json +64 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,697 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { open, readdir, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
5
|
+
import { cpus, freemem, loadavg, networkInterfaces, totalmem, uptime } from "node:os";
|
|
6
|
+
//#region lib/types/index.js
|
|
7
|
+
/** System-monitor Host Remote serving `node:os` resource snapshots to the browser. */
|
|
8
|
+
var __runInitializers = function(thisArg, initializers, value) {
|
|
9
|
+
var useValue = arguments.length > 2;
|
|
10
|
+
for (var i = 0; i < initializers.length; i++) value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
11
|
+
return useValue ? value : void 0;
|
|
12
|
+
};
|
|
13
|
+
var __esDecorate = function(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
14
|
+
function accept(f) {
|
|
15
|
+
if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected");
|
|
16
|
+
return f;
|
|
17
|
+
}
|
|
18
|
+
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
19
|
+
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
20
|
+
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
21
|
+
var _, done = false;
|
|
22
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
23
|
+
var context = {};
|
|
24
|
+
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
25
|
+
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
26
|
+
context.addInitializer = function(f) {
|
|
27
|
+
if (done) throw new TypeError("Cannot add initializers after decoration has completed");
|
|
28
|
+
extraInitializers.push(accept(f || null));
|
|
29
|
+
};
|
|
30
|
+
var result = (0, decorators[i])(kind === "accessor" ? {
|
|
31
|
+
get: descriptor.get,
|
|
32
|
+
set: descriptor.set
|
|
33
|
+
} : descriptor[key], context);
|
|
34
|
+
if (kind === "accessor") {
|
|
35
|
+
if (result === void 0) continue;
|
|
36
|
+
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
37
|
+
if (_ = accept(result.get)) descriptor.get = _;
|
|
38
|
+
if (_ = accept(result.set)) descriptor.set = _;
|
|
39
|
+
if (_ = accept(result.init)) initializers.unshift(_);
|
|
40
|
+
} else if (_ = accept(result)) if (kind === "field") initializers.unshift(_);
|
|
41
|
+
else descriptor[key] = _;
|
|
42
|
+
}
|
|
43
|
+
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
44
|
+
done = true;
|
|
45
|
+
};
|
|
46
|
+
const execFile$1 = promisify(execFile);
|
|
47
|
+
/** Best-effort shell read: empty string on any failure. */
|
|
48
|
+
async function run(cmd, args) {
|
|
49
|
+
try {
|
|
50
|
+
const { stdout } = await execFile$1(cmd, args, {
|
|
51
|
+
timeout: 5e3,
|
|
52
|
+
encoding: "utf8"
|
|
53
|
+
});
|
|
54
|
+
return stdout;
|
|
55
|
+
} catch {
|
|
56
|
+
return "";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** CPU busy ratio since boot across all logical cores, 0..1. */
|
|
60
|
+
function busyRatio() {
|
|
61
|
+
let busy = 0;
|
|
62
|
+
let total = 0;
|
|
63
|
+
for (const core of cpus()) {
|
|
64
|
+
const coreBusy = core.times.user + core.times.nice + core.times.sys + core.times.irq;
|
|
65
|
+
busy += coreBusy;
|
|
66
|
+
total += coreBusy + core.times.idle;
|
|
67
|
+
}
|
|
68
|
+
return total === 0 ? 0 : busy / total;
|
|
69
|
+
}
|
|
70
|
+
/** Raw cumulative tick counts per logical core (client computes usage deltas). */
|
|
71
|
+
function coresTimes() {
|
|
72
|
+
return cpus().map((core) => ({ ...core.times }));
|
|
73
|
+
}
|
|
74
|
+
/** Swap totals in bytes; zeros when the platform exposes none. */
|
|
75
|
+
async function swapUsage() {
|
|
76
|
+
if (process.platform === "darwin") {
|
|
77
|
+
const out = await run("sysctl", ["-n", "vm.swapusage"]);
|
|
78
|
+
const match = /total = ([\d.]+)M\s+used = ([\d.]+)M/.exec(out);
|
|
79
|
+
if (match !== null) return {
|
|
80
|
+
totalBytes: Math.round(Number(match[1]) * 1048576),
|
|
81
|
+
usedBytes: Math.round(Number(match[2]) * 1048576)
|
|
82
|
+
};
|
|
83
|
+
} else if (process.platform === "linux") {
|
|
84
|
+
const out = await run("sh", ["-c", "grep -E \"^(SwapTotal|SwapFree):\" /proc/meminfo"]);
|
|
85
|
+
const totalKb = Number(/(?:^|\n)SwapTotal:\s+(\d+) kB/.exec(out)?.[1] ?? 0);
|
|
86
|
+
const freeKb = Number(/(?:^|\n)SwapFree:\s+(\d+) kB/.exec(out)?.[1] ?? 0);
|
|
87
|
+
return {
|
|
88
|
+
totalBytes: totalKb * 1024,
|
|
89
|
+
usedBytes: (totalKb - freeKb) * 1024
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
totalBytes: 0,
|
|
94
|
+
usedBytes: 0
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/** CPU thermal reading when the platform exposes one without privileges, else null. */
|
|
98
|
+
async function thermalLevel() {
|
|
99
|
+
if (process.platform === "darwin") {
|
|
100
|
+
const value = Number((await run("sysctl", ["-n", "machdep.xcpm.cpu_therm_level"])).trim());
|
|
101
|
+
return Number.isFinite(value) ? value : null;
|
|
102
|
+
}
|
|
103
|
+
if (process.platform === "linux") {
|
|
104
|
+
const value = Number((await run("sh", ["-c", "cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null"])).trim());
|
|
105
|
+
return Number.isFinite(value) ? Math.round(value / 1e3) : null;
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
/** Power state when readable: 'CHARGE' | 'AC' | 'BATTERY', else null. */
|
|
110
|
+
async function powerState() {
|
|
111
|
+
if (process.platform === "darwin") {
|
|
112
|
+
const out = await run("pmset", ["-g", "batt"]);
|
|
113
|
+
if (out.includes("charging")) return "CHARGE";
|
|
114
|
+
if (out.includes("AC Power")) return "AC";
|
|
115
|
+
if (out.includes("Battery Power")) return "BATTERY";
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
if (process.platform === "linux") {
|
|
119
|
+
const status = (await run("sh", ["-c", "cat /sys/class/power_supply/*/status 2>/dev/null | head -1"])).trim();
|
|
120
|
+
if (status === "Charging" || status === "Full") return "CHARGE";
|
|
121
|
+
if (status === "Discharging") return "BATTERY";
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
/** Static hardware identity, read once and cached. */
|
|
127
|
+
let hardwareCache;
|
|
128
|
+
async function hardwareInfo() {
|
|
129
|
+
if (hardwareCache !== void 0) return hardwareCache;
|
|
130
|
+
let manufacturer = "Unknown";
|
|
131
|
+
let model = "Unknown";
|
|
132
|
+
let chassis = "Unknown";
|
|
133
|
+
if (process.platform === "darwin") {
|
|
134
|
+
manufacturer = "Apple Inc.";
|
|
135
|
+
chassis = "Laptop";
|
|
136
|
+
model = (await run("sysctl", ["-n", "hw.model"])).trim() || "Unknown";
|
|
137
|
+
} else if (process.platform === "linux") {
|
|
138
|
+
manufacturer = (await run("sh", ["-c", "cat /sys/class/dmi/id/sys_vendor 2>/dev/null"])).trim() || "Unknown";
|
|
139
|
+
model = (await run("sh", ["-c", "cat /sys/class/dmi/id/product_name 2>/dev/null"])).trim() || "Unknown";
|
|
140
|
+
const chassisType = (await run("sh", ["-c", "cat /sys/class/dmi/id/chassis_type 2>/dev/null"])).trim();
|
|
141
|
+
chassis = chassisType === "10" || chassisType === "9" || chassisType === "8" ? "Notebook" : chassisType === "3" ? "Desktop" : chassisType === "" ? "Unknown" : `Type ${chassisType}`;
|
|
142
|
+
}
|
|
143
|
+
hardwareCache = {
|
|
144
|
+
manufacturer,
|
|
145
|
+
model,
|
|
146
|
+
chassis
|
|
147
|
+
};
|
|
148
|
+
return hardwareCache;
|
|
149
|
+
}
|
|
150
|
+
/** Top processes by CPU usage, descending. */
|
|
151
|
+
async function topProcesses(limit = 10) {
|
|
152
|
+
const out = await run("ps", process.platform === "darwin" ? [
|
|
153
|
+
"-axo",
|
|
154
|
+
"pid=,comm=,%cpu=,%mem=",
|
|
155
|
+
"-r"
|
|
156
|
+
] : [
|
|
157
|
+
"-axo",
|
|
158
|
+
"pid=,comm=,%cpu=,%mem=",
|
|
159
|
+
"--sort=-%cpu"
|
|
160
|
+
]);
|
|
161
|
+
const rows = [];
|
|
162
|
+
for (const line of out.split("\n")) {
|
|
163
|
+
const match = /^\s*(\d+)\s+(.*?)\s+([\d.]+)\s+([\d.]+)\s*$/.exec(line);
|
|
164
|
+
if (match === null) continue;
|
|
165
|
+
rows.push({
|
|
166
|
+
pid: Number(match[1]),
|
|
167
|
+
name: match[2] ?? "",
|
|
168
|
+
cpuPct: Number(match[3]),
|
|
169
|
+
memPct: Number(match[4])
|
|
170
|
+
});
|
|
171
|
+
if (rows.length >= limit) break;
|
|
172
|
+
}
|
|
173
|
+
return rows;
|
|
174
|
+
}
|
|
175
|
+
/** Total active process count. */
|
|
176
|
+
async function taskCount() {
|
|
177
|
+
const trimmed = (await run("ps", ["-axo", "pid="])).trim();
|
|
178
|
+
return trimmed === "" ? 0 : trimmed.split("\n").length;
|
|
179
|
+
}
|
|
180
|
+
/** The active interface: the first non-internal one with an IPv4 address. */
|
|
181
|
+
function activeInterface() {
|
|
182
|
+
const interfaces = networkInterfaces();
|
|
183
|
+
for (const [name, addresses] of Object.entries(interfaces)) for (const address of addresses ?? []) if (!address.internal && address.family === "IPv4") return {
|
|
184
|
+
name,
|
|
185
|
+
ip: address.address
|
|
186
|
+
};
|
|
187
|
+
return {
|
|
188
|
+
name: "—",
|
|
189
|
+
ip: null
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
/** Cumulative rx/tx bytes on the active interface (netstat/proc counters). */
|
|
193
|
+
async function interfaceBytes(name) {
|
|
194
|
+
if (process.platform === "darwin") {
|
|
195
|
+
const lines = (await run("netstat", ["-ib"])).split("\n");
|
|
196
|
+
const header = lines.find((line) => line.includes("Ibytes"));
|
|
197
|
+
const indexOfIbytes = header?.indexOf("Ibytes") ?? -1;
|
|
198
|
+
const indexOfObytes = header?.indexOf("Obytes") ?? -1;
|
|
199
|
+
for (const line of lines) {
|
|
200
|
+
if (new RegExp(`^${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s`).exec(line) === null) continue;
|
|
201
|
+
const rx = Number(line.slice(indexOfIbytes).split(/\s+/)[0] ?? "0");
|
|
202
|
+
const tx = Number(line.slice(indexOfObytes).split(/\s+/)[0] ?? "0");
|
|
203
|
+
if (Number.isFinite(rx) && Number.isFinite(tx)) return {
|
|
204
|
+
rxBytes: rx,
|
|
205
|
+
txBytes: tx
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
rxBytes: 0,
|
|
210
|
+
txBytes: 0
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
if (process.platform === "linux") {
|
|
214
|
+
const out = await run("sh", ["-c", `grep "${name}:" /proc/net/dev`]);
|
|
215
|
+
const match = /:\s*(\d+)\s+\d+\s+\d+\s+\d+\s+\d+\s+\d+\s+\d+\s+\d+\s+(\d+)/.exec(out);
|
|
216
|
+
if (match !== null) return {
|
|
217
|
+
rxBytes: Number(match[1]),
|
|
218
|
+
txBytes: Number(match[2])
|
|
219
|
+
};
|
|
220
|
+
return {
|
|
221
|
+
rxBytes: 0,
|
|
222
|
+
txBytes: 0
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
rxBytes: 0,
|
|
227
|
+
txBytes: 0
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
/** Round-trip ping to 8.8.8.8 in ms, or null when unreachable. */
|
|
231
|
+
async function pingMs() {
|
|
232
|
+
const out = await run("ping", process.platform === "darwin" ? [
|
|
233
|
+
"-c",
|
|
234
|
+
"1",
|
|
235
|
+
"-t",
|
|
236
|
+
"2",
|
|
237
|
+
"8.8.8.8"
|
|
238
|
+
] : [
|
|
239
|
+
"-c",
|
|
240
|
+
"1",
|
|
241
|
+
"-W",
|
|
242
|
+
"2",
|
|
243
|
+
"8.8.8.8"
|
|
244
|
+
]);
|
|
245
|
+
const match = /time=([\d.]+)\s*ms/.exec(out);
|
|
246
|
+
return match === null ? null : Number(match[1]);
|
|
247
|
+
}
|
|
248
|
+
/** Storage usage of the process cwd's mount. */
|
|
249
|
+
async function storageInfo() {
|
|
250
|
+
const path = process.cwd();
|
|
251
|
+
if (process.platform === "darwin" || process.platform === "linux") {
|
|
252
|
+
const lines = (await run("df", ["-k", path])).trim().split("\n").slice(1);
|
|
253
|
+
const match = /^[\S]+\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)%/.exec(lines[0] ?? "");
|
|
254
|
+
if (match !== null) return {
|
|
255
|
+
path,
|
|
256
|
+
totalBytes: Number(match[1]) * 1024,
|
|
257
|
+
usedBytes: Number(match[2]) * 1024,
|
|
258
|
+
usedPct: Number(match[4])
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
path,
|
|
263
|
+
totalBytes: 0,
|
|
264
|
+
usedBytes: 0,
|
|
265
|
+
usedPct: 0
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Remote-only service exposing host resource snapshots to the browser. Every
|
|
270
|
+
* snapshot is projected directly from `node:os` at call time; no cache exists
|
|
271
|
+
* to synchronize.
|
|
272
|
+
* @typert service systemMetrics
|
|
273
|
+
*/
|
|
274
|
+
let SystemMetricsService = (() => {
|
|
275
|
+
let _classSuper = TypertRemoteService;
|
|
276
|
+
let _instanceExtraInitializers = [];
|
|
277
|
+
let _snapshot_decorators;
|
|
278
|
+
let _overview_decorators;
|
|
279
|
+
let _listDirectory_decorators;
|
|
280
|
+
let _readFile_decorators;
|
|
281
|
+
let _writeFile_decorators;
|
|
282
|
+
let _runCommand_decorators;
|
|
283
|
+
return class SystemMetricsService extends _classSuper {
|
|
284
|
+
static {
|
|
285
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
286
|
+
_snapshot_decorators = [Remote("snapshot")];
|
|
287
|
+
_overview_decorators = [Remote("overview")];
|
|
288
|
+
_listDirectory_decorators = [Remote("listDirectory")];
|
|
289
|
+
_readFile_decorators = [Remote("readFile")];
|
|
290
|
+
_writeFile_decorators = [Remote("writeFile")];
|
|
291
|
+
_runCommand_decorators = [Remote("runCommand")];
|
|
292
|
+
__esDecorate(this, null, _snapshot_decorators, {
|
|
293
|
+
kind: "method",
|
|
294
|
+
name: "snapshot",
|
|
295
|
+
static: false,
|
|
296
|
+
private: false,
|
|
297
|
+
access: {
|
|
298
|
+
has: (obj) => "snapshot" in obj,
|
|
299
|
+
get: (obj) => obj.snapshot
|
|
300
|
+
},
|
|
301
|
+
metadata: _metadata
|
|
302
|
+
}, null, _instanceExtraInitializers);
|
|
303
|
+
__esDecorate(this, null, _overview_decorators, {
|
|
304
|
+
kind: "method",
|
|
305
|
+
name: "overview",
|
|
306
|
+
static: false,
|
|
307
|
+
private: false,
|
|
308
|
+
access: {
|
|
309
|
+
has: (obj) => "overview" in obj,
|
|
310
|
+
get: (obj) => obj.overview
|
|
311
|
+
},
|
|
312
|
+
metadata: _metadata
|
|
313
|
+
}, null, _instanceExtraInitializers);
|
|
314
|
+
__esDecorate(this, null, _listDirectory_decorators, {
|
|
315
|
+
kind: "method",
|
|
316
|
+
name: "listDirectory",
|
|
317
|
+
static: false,
|
|
318
|
+
private: false,
|
|
319
|
+
access: {
|
|
320
|
+
has: (obj) => "listDirectory" in obj,
|
|
321
|
+
get: (obj) => obj.listDirectory
|
|
322
|
+
},
|
|
323
|
+
metadata: _metadata
|
|
324
|
+
}, null, _instanceExtraInitializers);
|
|
325
|
+
__esDecorate(this, null, _readFile_decorators, {
|
|
326
|
+
kind: "method",
|
|
327
|
+
name: "readFile",
|
|
328
|
+
static: false,
|
|
329
|
+
private: false,
|
|
330
|
+
access: {
|
|
331
|
+
has: (obj) => "readFile" in obj,
|
|
332
|
+
get: (obj) => obj.readFile
|
|
333
|
+
},
|
|
334
|
+
metadata: _metadata
|
|
335
|
+
}, null, _instanceExtraInitializers);
|
|
336
|
+
__esDecorate(this, null, _writeFile_decorators, {
|
|
337
|
+
kind: "method",
|
|
338
|
+
name: "writeFile",
|
|
339
|
+
static: false,
|
|
340
|
+
private: false,
|
|
341
|
+
access: {
|
|
342
|
+
has: (obj) => "writeFile" in obj,
|
|
343
|
+
get: (obj) => obj.writeFile
|
|
344
|
+
},
|
|
345
|
+
metadata: _metadata
|
|
346
|
+
}, null, _instanceExtraInitializers);
|
|
347
|
+
__esDecorate(this, null, _runCommand_decorators, {
|
|
348
|
+
kind: "method",
|
|
349
|
+
name: "runCommand",
|
|
350
|
+
static: false,
|
|
351
|
+
private: false,
|
|
352
|
+
access: {
|
|
353
|
+
has: (obj) => "runCommand" in obj,
|
|
354
|
+
get: (obj) => obj.runCommand
|
|
355
|
+
},
|
|
356
|
+
metadata: _metadata
|
|
357
|
+
}, null, _instanceExtraInitializers);
|
|
358
|
+
if (_metadata) Object.defineProperty(this, Symbol.metadata, {
|
|
359
|
+
enumerable: true,
|
|
360
|
+
configurable: true,
|
|
361
|
+
writable: true,
|
|
362
|
+
value: _metadata
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
constructor(ctx) {
|
|
366
|
+
super(ctx, "systemMetrics");
|
|
367
|
+
__runInitializers(this, _instanceExtraInitializers);
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Read the current host resource state.
|
|
371
|
+
* @returns load averages, since-boot CPU busy ratio, memory, and uptime.
|
|
372
|
+
*/
|
|
373
|
+
snapshot() {
|
|
374
|
+
return {
|
|
375
|
+
loadavg: loadavg(),
|
|
376
|
+
cpuBusyRatio: busyRatio(),
|
|
377
|
+
totalMemoryBytes: totalmem(),
|
|
378
|
+
freeMemoryBytes: freemem(),
|
|
379
|
+
uptimeSeconds: uptime(),
|
|
380
|
+
timestamp: Date.now()
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Read the rich system overview for the left system panel: per-core CPU
|
|
385
|
+
* tick counts (the client computes usage deltas between polls), memory and
|
|
386
|
+
* swap, thermal/power/hardware (best-effort), and top processes.
|
|
387
|
+
* @returns the overview.
|
|
388
|
+
*/
|
|
389
|
+
async overview() {
|
|
390
|
+
const memoryTotal = totalmem();
|
|
391
|
+
const memoryFree = freemem();
|
|
392
|
+
const active = activeInterface();
|
|
393
|
+
const [swap, thermal, power, hardware, processes, tasks, bytes, ping, storage] = await Promise.all([
|
|
394
|
+
swapUsage(),
|
|
395
|
+
thermalLevel(),
|
|
396
|
+
powerState(),
|
|
397
|
+
hardwareInfo(),
|
|
398
|
+
topProcesses(),
|
|
399
|
+
taskCount(),
|
|
400
|
+
interfaceBytes(active.name),
|
|
401
|
+
pingMs(),
|
|
402
|
+
storageInfo()
|
|
403
|
+
]);
|
|
404
|
+
const network = {
|
|
405
|
+
interfaceName: active.name,
|
|
406
|
+
state: active.ip === null ? "IPv4 OFFLINE" : "IPv4 ONLINE",
|
|
407
|
+
ip: active.ip,
|
|
408
|
+
pingMs: ping,
|
|
409
|
+
rxBytes: bytes.rxBytes,
|
|
410
|
+
txBytes: bytes.txBytes
|
|
411
|
+
};
|
|
412
|
+
return {
|
|
413
|
+
timestamp: Date.now(),
|
|
414
|
+
platform: process.platform,
|
|
415
|
+
uptimeSeconds: uptime(),
|
|
416
|
+
loadavg: loadavg(),
|
|
417
|
+
memory: {
|
|
418
|
+
totalBytes: memoryTotal,
|
|
419
|
+
usedBytes: memoryTotal - memoryFree
|
|
420
|
+
},
|
|
421
|
+
swap,
|
|
422
|
+
cores: coresTimes(),
|
|
423
|
+
thermalLevel: thermal,
|
|
424
|
+
powerState: power,
|
|
425
|
+
hardware,
|
|
426
|
+
tasks,
|
|
427
|
+
processes,
|
|
428
|
+
network,
|
|
429
|
+
storage
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* List one directory for the filesystem browser.
|
|
434
|
+
* @param path - absolute directory to list.
|
|
435
|
+
* @returns the listing (or an error string when unreadable).
|
|
436
|
+
*/
|
|
437
|
+
async listDirectory(path) {
|
|
438
|
+
try {
|
|
439
|
+
return {
|
|
440
|
+
path,
|
|
441
|
+
entries: (await readdir(path, { withFileTypes: true })).map((dirent) => ({
|
|
442
|
+
name: dirent.name,
|
|
443
|
+
isDirectory: dirent.isDirectory()
|
|
444
|
+
})).sort((left, right) => Number(right.isDirectory) - Number(left.isDirectory) || left.name.localeCompare(right.name)),
|
|
445
|
+
error: null
|
|
446
|
+
};
|
|
447
|
+
} catch (error) {
|
|
448
|
+
return {
|
|
449
|
+
path,
|
|
450
|
+
entries: [],
|
|
451
|
+
error: error instanceof Error ? error.message : String(error)
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Read one file for the bottom-right preview pane / editor. Text payloads
|
|
457
|
+
* are capped at 4 MiB, images at 4 MiB, videos at 12 MiB; oversized files
|
|
458
|
+
* are truncated and flagged rather than refused. Kind is decided by
|
|
459
|
+
* extension with a UTF-8 sniff fallback for unknown extensions.
|
|
460
|
+
* @param path - absolute file path.
|
|
461
|
+
* @returns the preview payload (or an error string when unreadable).
|
|
462
|
+
*/
|
|
463
|
+
async readFile(path) {
|
|
464
|
+
try {
|
|
465
|
+
const info = await stat(path);
|
|
466
|
+
if (info.isDirectory()) return {
|
|
467
|
+
path,
|
|
468
|
+
kind: "unsupported",
|
|
469
|
+
mime: "",
|
|
470
|
+
sizeBytes: 0,
|
|
471
|
+
truncated: false,
|
|
472
|
+
text: null,
|
|
473
|
+
dataUrl: null,
|
|
474
|
+
error: "is a directory"
|
|
475
|
+
};
|
|
476
|
+
const byExtension = previewKindOf(path.slice(path.lastIndexOf(".") + 1).toLowerCase());
|
|
477
|
+
if (byExtension !== null && byExtension.kind !== "text") {
|
|
478
|
+
const cap = byExtension.kind === "image" ? PREVIEW_IMAGE_CAP : PREVIEW_VIDEO_CAP;
|
|
479
|
+
const buffer = await readFirst(path, Math.min(info.size, cap));
|
|
480
|
+
return {
|
|
481
|
+
path,
|
|
482
|
+
kind: byExtension.kind,
|
|
483
|
+
mime: byExtension.mime,
|
|
484
|
+
sizeBytes: info.size,
|
|
485
|
+
truncated: info.size > cap,
|
|
486
|
+
text: null,
|
|
487
|
+
dataUrl: `data:${byExtension.mime};base64,${buffer.toString("base64")}`,
|
|
488
|
+
error: null
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
const sample = await readFirst(path, Math.min(info.size, 8192));
|
|
492
|
+
const kind = byExtension ?? sniffTextKind(sample);
|
|
493
|
+
if (kind === null) return {
|
|
494
|
+
path,
|
|
495
|
+
kind: "unsupported",
|
|
496
|
+
mime: "application/octet-stream",
|
|
497
|
+
sizeBytes: info.size,
|
|
498
|
+
truncated: info.size > PREVIEW_TEXT_CAP,
|
|
499
|
+
text: null,
|
|
500
|
+
dataUrl: null,
|
|
501
|
+
error: null
|
|
502
|
+
};
|
|
503
|
+
const buffer = info.size > PREVIEW_TEXT_CAP ? await readFirst(path, PREVIEW_TEXT_CAP) : sample;
|
|
504
|
+
return {
|
|
505
|
+
path,
|
|
506
|
+
kind: "text",
|
|
507
|
+
mime: kind.mime,
|
|
508
|
+
sizeBytes: info.size,
|
|
509
|
+
truncated: info.size > PREVIEW_TEXT_CAP,
|
|
510
|
+
text: buffer.toString("utf8"),
|
|
511
|
+
dataUrl: null,
|
|
512
|
+
error: null
|
|
513
|
+
};
|
|
514
|
+
} catch (error) {
|
|
515
|
+
return {
|
|
516
|
+
path,
|
|
517
|
+
kind: "unsupported",
|
|
518
|
+
mime: "",
|
|
519
|
+
sizeBytes: 0,
|
|
520
|
+
truncated: false,
|
|
521
|
+
text: null,
|
|
522
|
+
dataUrl: null,
|
|
523
|
+
error: error instanceof Error ? error.message : String(error)
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Write one text file from the bottom-right editor. Creates or replaces the
|
|
529
|
+
* file at `path` with the given UTF-8 content; the parent directory must
|
|
530
|
+
* exist. Trust surface matches `readFile`: the GUI already reads arbitrary
|
|
531
|
+
* paths the host process can reach, so writing carries the same parity.
|
|
532
|
+
* @param path - absolute file path.
|
|
533
|
+
* @param content - full text content to persist.
|
|
534
|
+
* @returns the write result (or an error string when unwritable).
|
|
535
|
+
*/
|
|
536
|
+
async writeFile(path, content) {
|
|
537
|
+
try {
|
|
538
|
+
await writeFile(path, content, "utf8");
|
|
539
|
+
return {
|
|
540
|
+
path,
|
|
541
|
+
sizeBytes: Buffer.byteLength(content, "utf8"),
|
|
542
|
+
error: null
|
|
543
|
+
};
|
|
544
|
+
} catch (error) {
|
|
545
|
+
return {
|
|
546
|
+
path,
|
|
547
|
+
sizeBytes: 0,
|
|
548
|
+
error: error instanceof Error ? error.message : String(error)
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Run one shell command for the bottom-right terminal panel. Executes
|
|
554
|
+
* `command` through `sh -c` with a 30s timeout and a 4 MiB output cap.
|
|
555
|
+
* Non-zero exits return the captured output with the real exit code; a
|
|
556
|
+
* command that cannot start (or times out) returns exitCode null.
|
|
557
|
+
* Trust surface matches `writeFile`: the GUI already reads and writes
|
|
558
|
+
* arbitrary paths the host process can reach, so executing commands the
|
|
559
|
+
* operator types carries the same parity.
|
|
560
|
+
* @param command - the shell command line to execute.
|
|
561
|
+
* @returns captured stdout/stderr and the exit code.
|
|
562
|
+
*/
|
|
563
|
+
async runCommand(command) {
|
|
564
|
+
try {
|
|
565
|
+
const { stdout, stderr } = await execFile$1("sh", ["-c", command], {
|
|
566
|
+
timeout: 3e4,
|
|
567
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
568
|
+
encoding: "utf8"
|
|
569
|
+
});
|
|
570
|
+
return {
|
|
571
|
+
stdout,
|
|
572
|
+
stderr,
|
|
573
|
+
exitCode: 0
|
|
574
|
+
};
|
|
575
|
+
} catch (error) {
|
|
576
|
+
const err = error;
|
|
577
|
+
return {
|
|
578
|
+
stdout: typeof err.stdout === "string" ? err.stdout : "",
|
|
579
|
+
stderr: typeof err.stderr === "string" ? err.stderr : error instanceof Error ? error.message : String(error),
|
|
580
|
+
exitCode: typeof err.code === "number" ? err.code : null
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
})();
|
|
586
|
+
/** Read at most `length` bytes from the file start. */
|
|
587
|
+
async function readFirst(path, length) {
|
|
588
|
+
const handle = await open(path, "r");
|
|
589
|
+
try {
|
|
590
|
+
const buffer = Buffer.alloc(length);
|
|
591
|
+
const { bytesRead } = await handle.read(buffer, 0, length, 0);
|
|
592
|
+
return bytesRead === length ? buffer : buffer.subarray(0, bytesRead);
|
|
593
|
+
} finally {
|
|
594
|
+
await handle.close();
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
/** Preview payload caps (bytes). */
|
|
598
|
+
const PREVIEW_TEXT_CAP = 4 * 1024 * 1024;
|
|
599
|
+
const PREVIEW_IMAGE_CAP = 4 * 1024 * 1024;
|
|
600
|
+
const PREVIEW_VIDEO_CAP = 12 * 1024 * 1024;
|
|
601
|
+
/** Image MIME by extension. */
|
|
602
|
+
const IMAGE_MIME = {
|
|
603
|
+
png: "image/png",
|
|
604
|
+
jpg: "image/jpeg",
|
|
605
|
+
jpeg: "image/jpeg",
|
|
606
|
+
gif: "image/gif",
|
|
607
|
+
webp: "image/webp",
|
|
608
|
+
svg: "image/svg+xml",
|
|
609
|
+
bmp: "image/bmp",
|
|
610
|
+
ico: "image/x-icon",
|
|
611
|
+
avif: "image/avif"
|
|
612
|
+
};
|
|
613
|
+
/** Video MIME by extension. */
|
|
614
|
+
const VIDEO_MIME = {
|
|
615
|
+
mp4: "video/mp4",
|
|
616
|
+
m4v: "video/mp4",
|
|
617
|
+
webm: "video/webm",
|
|
618
|
+
ogv: "video/ogg",
|
|
619
|
+
ogg: "video/ogg",
|
|
620
|
+
mov: "video/quicktime"
|
|
621
|
+
};
|
|
622
|
+
/** Extensions treated as plain text. */
|
|
623
|
+
const TEXT_EXTENSIONS = new Set([
|
|
624
|
+
"txt",
|
|
625
|
+
"text",
|
|
626
|
+
"md",
|
|
627
|
+
"markdown",
|
|
628
|
+
"json",
|
|
629
|
+
"yaml",
|
|
630
|
+
"yml",
|
|
631
|
+
"toml",
|
|
632
|
+
"ini",
|
|
633
|
+
"cfg",
|
|
634
|
+
"conf",
|
|
635
|
+
"log",
|
|
636
|
+
"csv",
|
|
637
|
+
"ts",
|
|
638
|
+
"tsx",
|
|
639
|
+
"js",
|
|
640
|
+
"jsx",
|
|
641
|
+
"mjs",
|
|
642
|
+
"cjs",
|
|
643
|
+
"css",
|
|
644
|
+
"scss",
|
|
645
|
+
"html",
|
|
646
|
+
"htm",
|
|
647
|
+
"xml",
|
|
648
|
+
"sh",
|
|
649
|
+
"bash",
|
|
650
|
+
"zsh",
|
|
651
|
+
"py",
|
|
652
|
+
"rb",
|
|
653
|
+
"go",
|
|
654
|
+
"rs",
|
|
655
|
+
"java",
|
|
656
|
+
"c",
|
|
657
|
+
"h",
|
|
658
|
+
"cpp",
|
|
659
|
+
"hpp",
|
|
660
|
+
"cs",
|
|
661
|
+
"php",
|
|
662
|
+
"sql",
|
|
663
|
+
"env",
|
|
664
|
+
"lock"
|
|
665
|
+
]);
|
|
666
|
+
/** Decide the preview kind from the file extension (null = sniff). */
|
|
667
|
+
function previewKindOf(extension) {
|
|
668
|
+
const imageMime = IMAGE_MIME[extension];
|
|
669
|
+
if (imageMime !== void 0) return {
|
|
670
|
+
kind: "image",
|
|
671
|
+
mime: imageMime
|
|
672
|
+
};
|
|
673
|
+
const videoMime = VIDEO_MIME[extension];
|
|
674
|
+
if (videoMime !== void 0) return {
|
|
675
|
+
kind: "video",
|
|
676
|
+
mime: videoMime
|
|
677
|
+
};
|
|
678
|
+
if (TEXT_EXTENSIONS.has(extension)) return {
|
|
679
|
+
kind: "text",
|
|
680
|
+
mime: "text/plain"
|
|
681
|
+
};
|
|
682
|
+
return null;
|
|
683
|
+
}
|
|
684
|
+
/** UTF-8 sniff: no NUL bytes and few control bytes → likely plain text. */
|
|
685
|
+
function sniffTextKind(sample) {
|
|
686
|
+
let control = 0;
|
|
687
|
+
for (const byte of sample) {
|
|
688
|
+
if (byte === 0) return null;
|
|
689
|
+
if (byte < 32 && byte !== 9 && byte !== 10 && byte !== 13) control += 1;
|
|
690
|
+
}
|
|
691
|
+
return sample.length > 0 && control / sample.length < .05 ? {
|
|
692
|
+
kind: "text",
|
|
693
|
+
mime: "text/plain"
|
|
694
|
+
} : null;
|
|
695
|
+
}
|
|
696
|
+
//#endregion
|
|
697
|
+
export { SystemMetricsService, SystemMetricsService as default };
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region lib/types/invariant.js
|
|
2
|
+
/** Package-owned invariant companion. @module @danielng23/dsh-host-system-metrics/invariant */
|
|
3
|
+
const PACKAGE_NAME = "@danielng23/dsh-host-system-metrics";
|
|
4
|
+
/** Cordis companion plugin name. */
|
|
5
|
+
const name = "host-system-metrics-invariant";
|
|
6
|
+
/** Service required before the companion can reserve package ownership. */
|
|
7
|
+
const inject = ["invariants"];
|
|
8
|
+
/** No runtime invariant: every snapshot is projected directly from `node:os` at call time. */
|
|
9
|
+
const install = () => {};
|
|
10
|
+
/** Register this package's invariant companion. */
|
|
11
|
+
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
12
|
+
//#endregion
|
|
13
|
+
export { apply, inject, name };
|