mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-11-06 14:30:13 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
efca9dcfa8 | ||
|
|
bf7d2175df | ||
|
|
1c1d27a9a6 | ||
|
|
eb56974dc9 | ||
|
|
be010ea36e |
No files matched your search
@@ -93,6 +93,7 @@ export const enTerminalMessages: Messages = {
|
||||
'terminal.statusbar.reconnect.tooltip': 'Reconnect this session',
|
||||
'terminal.serverStats.cpu': 'CPU Usage',
|
||||
'terminal.serverStats.cpuCores': 'CPU Core Usage',
|
||||
'terminal.serverStats.gpu': 'GPU Usage',
|
||||
'terminal.serverStats.memory': 'Memory Usage',
|
||||
'terminal.serverStats.memoryDetails': 'Memory Details',
|
||||
'terminal.serverStats.memUsed': 'Used',
|
||||
|
||||
@@ -93,6 +93,7 @@ export const esTerminalMessages: Messages = {
|
||||
'terminal.statusbar.reconnect.tooltip': 'Reconectar esta sesión',
|
||||
'terminal.serverStats.cpu': 'Uso de CPU',
|
||||
'terminal.serverStats.cpuCores': 'Uso de núcleos de CPU',
|
||||
'terminal.serverStats.gpu': 'Uso de GPU',
|
||||
'terminal.serverStats.memory': 'Uso de memoria',
|
||||
'terminal.serverStats.memoryDetails': 'Detalles de memoria',
|
||||
'terminal.serverStats.memUsed': 'Usada',
|
||||
|
||||
@@ -114,6 +114,7 @@ export const ruTerminalMessages: Messages = {
|
||||
'terminal.statusbar.reconnect.tooltip': 'Переподключить эту сессию',
|
||||
'terminal.serverStats.cpu': 'Использование CPU',
|
||||
'terminal.serverStats.cpuCores': 'Использование ядер CPU',
|
||||
'terminal.serverStats.gpu': 'Использование GPU',
|
||||
'terminal.serverStats.memory': 'Использование памяти',
|
||||
'terminal.serverStats.memoryDetails': 'Сведения о памяти',
|
||||
'terminal.serverStats.memUsed': 'Использовано',
|
||||
|
||||
@@ -503,6 +503,7 @@ export const zhCNVaultMessages: Messages = {
|
||||
'terminal.statusbar.reconnect.tooltip': '重新连接当前会话',
|
||||
'terminal.serverStats.cpu': 'CPU 使用率',
|
||||
'terminal.serverStats.cpuCores': 'CPU 核心使用率',
|
||||
'terminal.serverStats.gpu': 'GPU 使用率',
|
||||
'terminal.serverStats.memory': '内存使用',
|
||||
'terminal.serverStats.memoryDetails': '内存详情',
|
||||
'terminal.serverStats.memUsed': '已用',
|
||||
|
||||
@@ -503,6 +503,7 @@ export const zhTWVaultMessages: Messages = {
|
||||
'terminal.statusbar.reconnect.tooltip': '重新連線目前工作階段',
|
||||
'terminal.serverStats.cpu': 'CPU 使用率',
|
||||
'terminal.serverStats.cpuCores': 'CPU 核心使用率',
|
||||
'terminal.serverStats.gpu': 'GPU 使用率',
|
||||
'terminal.serverStats.memory': '記憶體使用',
|
||||
'terminal.serverStats.memoryDetails': '記憶體詳情',
|
||||
'terminal.serverStats.memUsed': '已用',
|
||||
|
||||
@@ -33,6 +33,10 @@ export interface ServerStats {
|
||||
cpu: number | null; // CPU usage percentage (0-100)
|
||||
cpuCores: number | null; // Number of CPU cores
|
||||
cpuPerCore: number[]; // Per-core CPU usage array
|
||||
gpu: number | null; // NVIDIA GPU utilization percentage (0-100), null when no GPU data
|
||||
gpuName: string | null; // First GPU name (tooltip)
|
||||
gpuMemUsed: number | null; // Summed VRAM used in MB
|
||||
gpuMemTotal: number | null; // Summed VRAM total in MB
|
||||
memTotal: number | null; // Total memory in MB
|
||||
memUsed: number | null; // Used memory in MB (excluding buffers/cache)
|
||||
memFree: number | null; // Free memory in MB
|
||||
@@ -107,6 +111,10 @@ function createEmptyServerStats(): ServerStats {
|
||||
cpu: null,
|
||||
cpuCores: null,
|
||||
cpuPerCore: [],
|
||||
gpu: null,
|
||||
gpuName: null,
|
||||
gpuMemUsed: null,
|
||||
gpuMemTotal: null,
|
||||
memTotal: null,
|
||||
memUsed: null,
|
||||
memFree: null,
|
||||
@@ -200,6 +208,10 @@ function normalizeServerStats(stats: Partial<ServerStats>): ServerStats {
|
||||
cpu: stats.cpu ?? null,
|
||||
cpuCores: stats.cpuCores ?? null,
|
||||
cpuPerCore: stats.cpuPerCore || [],
|
||||
gpu: stats.gpu ?? null,
|
||||
gpuName: stats.gpuName ?? null,
|
||||
gpuMemUsed: stats.gpuMemUsed ?? null,
|
||||
gpuMemTotal: stats.gpuMemTotal ?? null,
|
||||
memTotal: stats.memTotal ?? null,
|
||||
memUsed: stats.memUsed ?? null,
|
||||
memFree: stats.memFree ?? null,
|
||||
|
||||
@@ -17,3 +17,15 @@ test("server stats stay subscribed while the terminal is in the background", ()
|
||||
assert.doesNotMatch(source, /usePaneVisible/);
|
||||
assert.doesNotMatch(source, /isVisible,/);
|
||||
});
|
||||
|
||||
test("GPU chip only renders when the remote reports GPU utilization", () => {
|
||||
const source = readFileSync(new URL("./TerminalServerStats.tsx", import.meta.url), "utf8");
|
||||
|
||||
// Conditional render — hosts without nvidia-smi must not show an empty GPU chip.
|
||||
assert.match(source, /\{serverStats\.gpu !== null && \(/);
|
||||
// Sits directly after the CPU chip, before the memory chip.
|
||||
assert.match(
|
||||
source,
|
||||
/terminal\.serverStats\.gpu[\s\S]{0,2000}terminal\.serverStats\.memory/,
|
||||
);
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Activity, ArrowDownToLine, ArrowUpFromLine, Cpu, HardDrive, MemoryStick } from 'lucide-react';
|
||||
import { Activity, ArrowDownToLine, ArrowUpFromLine, Cpu, Gpu, HardDrive, MemoryStick } from 'lucide-react';
|
||||
|
||||
import { useI18n } from '../../application/i18n/I18nProvider';
|
||||
import { cn } from '../../lib/utils';
|
||||
@@ -122,6 +122,29 @@ export const TerminalServerStats: React.FC<TerminalServerStatsProps> = ({
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
{/* GPU — only rendered when the remote reports NVIDIA GPU data (Linux nvidia-smi) */}
|
||||
{serverStats.gpu !== null && (
|
||||
<div
|
||||
className="flex items-center gap-0.5 min-w-0 shrink"
|
||||
aria-label={t("terminal.serverStats.gpu")}
|
||||
title={
|
||||
[
|
||||
serverStats.gpuName,
|
||||
serverStats.gpuMemUsed !== null && serverStats.gpuMemTotal !== null && serverStats.gpuMemTotal > 0
|
||||
? `VRAM ${(serverStats.gpuMemUsed / 1024).toFixed(1)}/${(serverStats.gpuMemTotal / 1024).toFixed(1)}G`
|
||||
: null,
|
||||
].filter(Boolean).join(' · ') || undefined
|
||||
}
|
||||
>
|
||||
<Gpu size={10} className="flex-shrink-0" />
|
||||
<span className="truncate">
|
||||
{serverStats.gpu}%
|
||||
{serverStats.gpuMemUsed !== null && serverStats.gpuMemTotal !== null && serverStats.gpuMemTotal > 0
|
||||
? ` (${(serverStats.gpuMemUsed / 1024).toFixed(1)}/${(serverStats.gpuMemTotal / 1024).toFixed(1)}G)`
|
||||
: ''}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{/* Memory with HoverCard for htop-style bar and top processes */}
|
||||
<HoverCard openDelay={200} closeDelay={100}>
|
||||
<HoverCardTrigger asChild>
|
||||
|
||||
@@ -635,13 +635,7 @@ function createSessionOpsApi(ctx) {
|
||||
_rc_cwd=$(readlink "/proc/$1/cwd" 2>/dev/null)
|
||||
if [ -n "$_rc_cwd" ]; then printf '%s\\n' "$_rc_cwd"; return 0; fi
|
||||
if command -v lsof >/dev/null 2>&1; then
|
||||
# An unknown-type record can put a readlink error in its name field.
|
||||
# Only a confirmed directory name is usable as an upload destination.
|
||||
_rc_cwd=$(LC_ALL=C lsof -a -p "$1" -d cwd -Fnt 2>/dev/null | awk '
|
||||
/^f/ { is_dir=0 }
|
||||
/^t/ { is_dir=($0 == "tDIR" || $0 == "tVDIR") }
|
||||
/^n/ && is_dir { print substr($0, 2); exit }
|
||||
')
|
||||
_rc_cwd=$(LC_ALL=C lsof -a -p "$1" -d cwd -Fn 2>/dev/null | sed -n 's/^n//p' | head -n1)
|
||||
if [ -n "$_rc_cwd" ]; then printf 'NETCATTY_LSOF_CWD=%s\\n' "$_rc_cwd"; return 0; fi
|
||||
fi
|
||||
return 1
|
||||
@@ -1220,8 +1214,17 @@ function createSessionOpsApi(ctx) {
|
||||
`kernel=$(uname -r 2>/dev/null || echo "")`,
|
||||
`uptime=$(awk '{printf "%.0f",$1}' /proc/uptime 2>/dev/null || echo "")`,
|
||||
`loadavg=$(awk '{print $1" "$2" "$3}' /proc/loadavg 2>/dev/null || echo "")`,
|
||||
// GPU: NVIDIA GPUs expose utilization + VRAM through the driver-bundled
|
||||
// nvidia-smi. Best-effort — empty on hosts without the tool, so the UI
|
||||
// simply hides the GPU chip. Utilization is averaged across GPUs and
|
||||
// VRAM is summed independently; incomplete memory totals remain unknown.
|
||||
// The first GPU's name is kept for the tooltip.
|
||||
// Bound GPU queries independently so a stuck driver cannot stop CPU/memory
|
||||
// polling. Hosts without timeout also omit this optional metric.
|
||||
// macOS is skipped: powermetrics needs root, which stats must not.
|
||||
`gpustat=$(gpucsv=$(timeout -s KILL 2 nvidia-smi --query-gpu=utilization.gpu,memory.used,memory.total,name --format=csv,noheader,nounits 2>/dev/null) && printf '%s\\n' "$gpucsv" | awk -F', *' 'NF >= 4 {g++; if($1 ~ /^[0-9]+$/) {n++; u+=$1} if($2 ~ /^[0-9]+$/) {mu+=$2; uc++} if($3 ~ /^[0-9]+$/) {mt+=$3; tc++} if(name=="") name=$4} END{if(n>0) printf "%.0f %s %s %s", u/n, (uc==g ? sprintf("%.0f",mu) : "N/A"), (tc==g ? sprintf("%.0f",mt) : "N/A"), name}' 2>/dev/null || echo "")`,
|
||||
// Output all stats (using CPURAW and PERCORERAW instead of CPU and PERCORE)
|
||||
`echo "CPURAW:$cpuraw|CORES:$cores|PERCORERAW:$percoreraw|MEMINFO:$meminfo|PROCS:$procs|NET:$net|HOST:$hostname_value|OS:$osname|KERNEL:$kernel|UPTIME:$uptime|LOAD:$loadavg"`
|
||||
`echo "CPURAW:$cpuraw|CORES:$cores|PERCORERAW:$percoreraw|MEMINFO:$meminfo|PROCS:$procs|NET:$net|GPU:$gpustat|HOST:$hostname_value|OS:$osname|KERNEL:$kernel|UPTIME:$uptime|LOAD:$loadavg"`
|
||||
].join('; ');
|
||||
|
||||
// Client-side run bound shared by the stats and disk probes; the remote
|
||||
@@ -1339,7 +1342,10 @@ function createSessionOpsApi(ctx) {
|
||||
let kernelRelease = "";
|
||||
let uptimeSeconds = null;
|
||||
let loadAverage = [];
|
||||
|
||||
let gpu = null;
|
||||
let gpuName = null;
|
||||
let gpuMemUsed = null;
|
||||
let gpuMemTotal = null;
|
||||
for (const part of parts) {
|
||||
if (part.startsWith('CPU:')) {
|
||||
// macOS: command reports normalized CPU% directly (no delta needed)
|
||||
@@ -1466,6 +1472,28 @@ function createSessionOpsApi(ctx) {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (part.startsWith('GPU:')) {
|
||||
// Linux: "<avgUtil> <sumMemUsed> <sumMemTotal> <first GPU name>"
|
||||
const gpuStr = part.substring(4).trim();
|
||||
if (gpuStr && gpuStr !== '') {
|
||||
const gpuFields = gpuStr.split(/\s+/);
|
||||
const util = parseInt(gpuFields[0], 10);
|
||||
if (!isNaN(util)) {
|
||||
gpu = Math.min(100, Math.max(0, util));
|
||||
if (gpuFields.length >= 2) {
|
||||
const memUsed = parseInt(gpuFields[1], 10);
|
||||
if (!isNaN(memUsed)) gpuMemUsed = memUsed;
|
||||
}
|
||||
if (gpuFields.length >= 3) {
|
||||
const memTotal = parseInt(gpuFields[2], 10);
|
||||
if (!isNaN(memTotal)) gpuMemTotal = memTotal;
|
||||
}
|
||||
if (gpuFields.length >= 4) {
|
||||
const name = gpuFields.slice(3).join(' ').trim();
|
||||
if (name) gpuName = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (part.startsWith('HOST:')) {
|
||||
hostname = part.substring(5).trim();
|
||||
} else if (part.startsWith('OS:')) {
|
||||
@@ -1614,6 +1642,10 @@ function createSessionOpsApi(ctx) {
|
||||
cpu, // CPU usage percentage (0-100)
|
||||
cpuCores, // Number of CPU cores
|
||||
cpuPerCore, // Per-core CPU usage array
|
||||
gpu, // NVIDIA GPU utilization percentage (0-100), null when unavailable
|
||||
gpuName, // First GPU name (tooltip), null when unavailable
|
||||
gpuMemUsed, // Summed VRAM used in MB, null when unavailable
|
||||
gpuMemTotal, // Summed VRAM total in MB, null when unavailable
|
||||
memTotal, // Total memory in MB
|
||||
memUsed, // Used memory in MB (excluding buffers/cache)
|
||||
memFree, // Free memory in MB
|
||||
|
||||
@@ -29,7 +29,7 @@ function makeApi(session, siblingSessions = [], overrides = {}) {
|
||||
sessions,
|
||||
setTimeout: overrides.setTimeout || setTimeout,
|
||||
clearTimeout: overrides.clearTimeout || clearTimeout,
|
||||
quoteShellArg: overrides.quoteShellArg || quoteShellArg,
|
||||
quoteShellArg,
|
||||
log: () => {},
|
||||
});
|
||||
}
|
||||
@@ -123,47 +123,6 @@ test("session cwd probe decodes the marked lsof pathname", async () => {
|
||||
assert.deepEqual(result, { success: true, cwd: "/srv/中文" });
|
||||
});
|
||||
|
||||
test("lsof cwd fallback accepts only directory records, not diagnostic names (#3237)", async () => {
|
||||
let script;
|
||||
const api = makeApi({
|
||||
shellPid: "4242",
|
||||
connRef: { count: 1 },
|
||||
stream: {},
|
||||
conn: {
|
||||
exec(_command, callback) {
|
||||
callback(null, makePwdStream("/srv/app", "4242"));
|
||||
},
|
||||
},
|
||||
}, [], {
|
||||
quoteShellArg(value) {
|
||||
if (!script && value.includes("read_shell_cwd()")) script = value;
|
||||
return quoteShellArg(value);
|
||||
},
|
||||
});
|
||||
await api.getSessionPwd(null, { sessionId: "session-1" });
|
||||
const helper = script.match(/ read_shell_cwd\(\) \{[\s\S]*?\n \}/)?.[0];
|
||||
assert.ok(helper, "exercise the actual remote cwd helper");
|
||||
const { spawnSync } = require("node:child_process");
|
||||
for (const [output, expected] of [
|
||||
["p4242\nfcwd\ntunknown\nn/proc/4242/cwd (readlink: Permission denied)\n", null],
|
||||
["p4242\nfcwd\ntunknown\nn/proc/4242/cwd (readlink: No such file or directory)\n", null],
|
||||
["p4242\nfcwd\nn/proc/4242/cwd\n", null],
|
||||
["p4242\nfcwd\ntDIR\nn/srv/app\n", "/srv/app"],
|
||||
["p4242\nfcwd\ntVDIR\nn/usr/home/alice\n", "/usr/home/alice"],
|
||||
["p4242\nfcwd\ntDIR\nn/tmp/literal (readlink: Permission denied)\n", "/tmp/literal (readlink: Permission denied)"],
|
||||
["p4242\nfcwd\ntDIR\nn/tmp/\\xe4\\xb8\\xad\\xe6\\x96\\x87\n", "/tmp/\\xe4\\xb8\\xad\\xe6\\x96\\x87"],
|
||||
]) {
|
||||
const result = spawnSync("sh", ["-c", `
|
||||
readlink() { return 1; }
|
||||
lsof() { printf '%s' "$LSOF_TEST_OUTPUT"; }
|
||||
${helper}
|
||||
read_shell_cwd 4242
|
||||
`], { encoding: "utf8", env: { ...process.env, LSOF_TEST_OUTPUT: output } });
|
||||
assert.equal(result.status, expected === null ? 1 : 0, output);
|
||||
assert.equal(result.stdout, expected === null ? "" : `NETCATTY_LSOF_CWD=${expected}\n`, output);
|
||||
}
|
||||
});
|
||||
|
||||
test("session cwd probe closes a remote command that exceeds its timeout", async () => {
|
||||
const stream = new EventEmitter();
|
||||
stream.stderr = new EventEmitter();
|
||||
|
||||
@@ -2,6 +2,9 @@ const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const { spawnSync } = require("node:child_process");
|
||||
const { EventEmitter } = require("node:events");
|
||||
const { mkdtempSync, writeFileSync, rmSync } = require("node:fs");
|
||||
const { join } = require("node:path");
|
||||
const { getTempFilePath } = require("../tempDirBridge.cjs");
|
||||
|
||||
const { createSessionOpsApi } = require("./sessionOps.cjs");
|
||||
|
||||
@@ -619,6 +622,105 @@ test("getServerStats does not touch the companion path for a normal SSH session"
|
||||
assert.equal(result.success, true);
|
||||
});
|
||||
|
||||
test("getServerStats reports null GPU fields when the host has no nvidia-smi data", async () => {
|
||||
const sessions = new Map();
|
||||
sessions.set("sid", { type: "ssh", conn: fakeConn(LINUX_STATS) });
|
||||
|
||||
const api = makeSessionOps(sessions);
|
||||
const result = await api.getServerStats({ sender: {} }, { sessionId: "sid" });
|
||||
|
||||
assert.equal(result.success, true);
|
||||
assert.equal(result.stats.gpu, null);
|
||||
assert.equal(result.stats.gpuName, null);
|
||||
assert.equal(result.stats.gpuMemUsed, null);
|
||||
assert.equal(result.stats.gpuMemTotal, null);
|
||||
});
|
||||
|
||||
test("getServerStats parses NVIDIA GPU utilization and VRAM from the stats line", async () => {
|
||||
// Mirrors the remote line: nvidia-smi CSV is space-squeezed to
|
||||
// "<avgUtil> <sumMemUsed> <sumMemTotal> <first GPU name>".
|
||||
const sessions = new Map();
|
||||
sessions.set("sid", {
|
||||
type: "ssh",
|
||||
conn: fakeConn(`${LINUX_STATS}|GPU:73 2048 24576 NVIDIA GeForce RTX 4090`),
|
||||
});
|
||||
|
||||
const api = makeSessionOps(sessions);
|
||||
const result = await api.getServerStats({ sender: {} }, { sessionId: "sid" });
|
||||
|
||||
assert.equal(result.success, true);
|
||||
assert.equal(result.stats.gpu, 73);
|
||||
assert.equal(result.stats.gpuName, "NVIDIA GeForce RTX 4090");
|
||||
assert.equal(result.stats.gpuMemUsed, 2048);
|
||||
assert.equal(result.stats.gpuMemTotal, 24576);
|
||||
});
|
||||
|
||||
test("getServerStats tolerates a malformed GPU section", async () => {
|
||||
const sessions = new Map();
|
||||
sessions.set("sid", {
|
||||
type: "ssh",
|
||||
conn: fakeConn(`${LINUX_STATS}|GPU:[Not Supported]`),
|
||||
});
|
||||
|
||||
const api = makeSessionOps(sessions);
|
||||
const result = await api.getServerStats({ sender: {} }, { sessionId: "sid" });
|
||||
|
||||
assert.equal(result.success, true);
|
||||
assert.equal(result.stats.gpu, null);
|
||||
assert.equal(result.stats.gpuName, null);
|
||||
});
|
||||
|
||||
for (const scenario of [
|
||||
{ name: "multiple GPUs", script: "printf '%s\\n' '73, 2048, 24576, NVIDIA RTX 4090' '71, 1024, 24576, NVIDIA RTX 4090'", gpu: 72, used: 3072, total: 49152 },
|
||||
{ name: "idle GPU", script: "printf '%s\\n' '0, 0, 24576, NVIDIA RTX 4090'", gpu: 0, used: 0, total: 24576 },
|
||||
{ name: "mixed utilization availability", script: "printf '%s\\n' '73, 2048, 24576, NVIDIA RTX 4090' '[N/A], 1024, 8192, NVIDIA A100 MIG'", gpu: 73, used: 3072, total: 32768 },
|
||||
{ name: "unavailable memory usage", script: "printf '%s\\n' '73, [N/A], 24576, NVIDIA RTX 4090'", gpu: 73, total: 24576 },
|
||||
{ name: "partial memory totals", script: "printf '%s\\n' '73, 2048, 24576, NVIDIA RTX 4090' '71, 1024, [N/A], NVIDIA RTX 4090'", gpu: 72, used: 3072 },
|
||||
{ name: "unsupported utilization", script: "printf '%s\\n' '[N/A], 2048, 24576, NVIDIA RTX 4090'", gpu: null },
|
||||
{ name: "failed query with partial output", script: "printf '%s\\n' '73, 2048, 24576, NVIDIA RTX 4090'; exit 1", gpu: null },
|
||||
{ name: "missing query tool", script: "exit 127", gpu: null },
|
||||
{ name: "missing timeout tool", script: "exec sleep 20", gpu: null, noTimeout: true },
|
||||
{ name: "blocked GPU query", script: "trap '' TERM; exec sleep 20", gpu: null },
|
||||
]) {
|
||||
test(`real stats shell preserves other metrics with ${scenario.name}`, async (t) => {
|
||||
if (spawnSync("timeout", ["-s", "KILL", "2", "true"]).status !== 0) {
|
||||
t.skip("GNU/BusyBox-compatible timeout is unavailable on this test host");
|
||||
return;
|
||||
}
|
||||
const dir = mkdtempSync(getTempFilePath("gpu-stats-"));
|
||||
t.after(() => rmSync(dir, { recursive: true, force: true }));
|
||||
writeFileSync(join(dir, "nvidia-smi"), `#!/bin/sh\n${scenario.script}\n`, { mode: 0o755 });
|
||||
const sessions = new Map([["sid", {
|
||||
type: "ssh",
|
||||
conn: {
|
||||
exec(command, cb) {
|
||||
const execution = spawnSync("sh", ["-c", [
|
||||
"uname() { printf '%s\\n' Linux; }",
|
||||
"nproc() { printf '%s\\n' 4; }",
|
||||
"top() { return 1; }",
|
||||
"df() { return 1; }",
|
||||
scenario.noTimeout
|
||||
? unwrapExecShC(command).replace("timeout -s KILL 2 nvidia-smi", "nc_missing_timeout -s KILL 2 nvidia-smi")
|
||||
: unwrapExecShC(command),
|
||||
].join("\n")], {
|
||||
encoding: "utf8",
|
||||
env: { ...process.env, PATH: `${dir}:${process.env.PATH}` },
|
||||
timeout: 7000,
|
||||
});
|
||||
assert.equal(execution.status, 0, execution.stderr);
|
||||
cb(null, fakeStream(execution.stdout));
|
||||
},
|
||||
},
|
||||
}]]);
|
||||
const result = await makeSessionOps(sessions).getServerStats({ sender: {} }, { sessionId: "sid" });
|
||||
assert.equal(result.success, true);
|
||||
assert.equal(result.stats.cpuCores, 4);
|
||||
assert.equal(result.stats.gpu, scenario.gpu);
|
||||
assert.equal(result.stats.gpuMemUsed, scenario.used ?? null);
|
||||
assert.equal(result.stats.gpuMemTotal, scenario.total ?? null);
|
||||
});
|
||||
}
|
||||
|
||||
test("getServerStats measures latency by pinging the stats connection", async () => {
|
||||
const sessions = new Map();
|
||||
const session = {
|
||||
|
||||
+4
@@ -281,6 +281,10 @@ declare global {
|
||||
cpu: number | null; // CPU usage percentage (0-100)
|
||||
cpuCores: number | null; // Number of CPU cores
|
||||
cpuPerCore: number[]; // Per-core CPU usage array
|
||||
gpu?: number | null; // NVIDIA utilization; absent on unsupported transports
|
||||
gpuName?: string | null; // First GPU name
|
||||
gpuMemUsed?: number | null; // Summed VRAM used in MB
|
||||
gpuMemTotal?: number | null; // Summed VRAM total in MB
|
||||
memTotal: number | null; // Total memory in MB
|
||||
memUsed: number | null; // Used memory in MB (excluding buffers/cache)
|
||||
memFree: number | null; // Free memory in MB
|
||||
|
||||
Reference in new issue
Block a user