Project Files
src / systemUtilities.ts
import * as os from "os";
import * as child_process from "child_process";
import * as net from "net";
import { sanitizeCommand, validateExecutionParams } from "./securityEnhanced";
import { EXEC_DEFAULT_TIMEOUT_MS } from "./constants";
import { resolveCwd, normalizeLineEndings } from "./utils";
export interface ProcessInfo {
pid: number;
name: string;
cpuUsage: number;
memoryUsage: number;
status: string;
}
export interface SystemInfo {
platform: NodeJS.Platform;
arch: os.Arch;
totalMemory: number;
freeMemory: number;
usedMemory: number;
memoryPercent: number;
cpus: {
model: string;
speed: number;
usage: number;
}[];
uptime: number;
loadAvg: {
load1: number;
load5: number;
load15: number;
};
hostname: string;
tempDir: string;
homeDir: string;
cpuCount: number;
}
export interface NetworkInfo {
interfaces: {
name: string;
address: string;
family: "IPv4" | "IPv6";
internal: boolean;
mask: string;
}[];
}
export interface DiskInfo {
device: string;
mountpoint: string;
size: number;
used: number;
free: number;
usePercent: number;
}
export interface CheckNetworkResult {
success: boolean;
host: string;
resolved: boolean;
port?: number;
tcpConnectMs?: number;
dnsMs?: number;
error?: string;
}
export interface ProcessKillResult {
success: boolean;
pid: number;
signal?: string;
error?: string;
}
// --- CPU usage tracking ---
let lastCpuTimes: os.CpuInfo[] = [];
let lastCpuTime = 0;
function getCpuUsagePercentages(): number[] {
const now = Date.now();
const intervals = (now - lastCpuTime) / 1000;
lastCpuTime = now;
const currentTimes = os.cpus().map(cpu => {
const t = cpu.times;
return {
user: t.user,
nice: t.nice,
sys: t.sys,
idle: t.idle,
irq: t.irq || 0,
};
});
const result: number[] = [];
for (let i = 0; i < currentTimes.length; i++) {
const prev = lastCpuTimes[i] || { user: 0, nice: 0, sys: 0, idle: 0, irq: 0 };
const curr = currentTimes[i];
const prevTotal = prev.user + prev.nice + prev.sys + prev.idle + prev.irq;
const currTotal = curr.user + curr.nice + curr.sys + curr.idle + curr.irq;
const totalDiff = currTotal - prevTotal;
if (totalDiff === 0) {
result.push(0);
} else {
const busyDiff = (curr.user - prev.user) + (curr.sys - prev.sys) + (curr.irq - prev.irq);
result.push(Math.round((busyDiff / totalDiff) * 10000) / 100);
}
}
lastCpuTimes = currentTimes;
return result;
}
// --- System info ---
export function getSystemInfo(): SystemInfo {
const cpus = os.cpus();
const cpuUsage = getCpuUsagePercentages();
const totalMem = os.totalmem();
const freeMem = os.freemem();
const usedMem = totalMem - freeMem;
const loadAvg = os.loadavg();
return {
platform: process.platform,
arch: os.arch(),
totalMemory: totalMem,
freeMemory: freeMem,
usedMemory: usedMem,
memoryPercent: Math.round((usedMem / totalMem) * 10000) / 100,
cpus: cpus.map((cpu, idx) => ({
model: cpu.model,
speed: cpu.speed,
usage: cpuUsage[idx] || 0,
})),
uptime: os.uptime(),
loadAvg: { load1: loadAvg[0], load5: loadAvg[1], load15: loadAvg[2] },
hostname: os.hostname(),
tempDir: os.tmpdir(),
homeDir: os.homedir(),
cpuCount: cpus.length,
};
}
// --- Process info ---
export function listProcesses(): ProcessInfo[] {
const platform = process.platform;
let cmd: string;
let args: string[];
if (platform === "win32") {
cmd = "wmic";
args = ["process", "get", "ProcessId,Name,CPUUsage,WorkingSetSize,Status"];
} else {
cmd = "ps";
args = ["-A", "-o", "pid,comm,%cpu,rss,state"];
}
try {
const output = child_process.execFileSync(cmd, args, {
timeout: 5000,
encoding: "utf-8",
});
return parseProcesses(output, platform);
} catch {
return [];
}
}
function parseProcesses(output: string, platform: NodeJS.Platform): ProcessInfo[] {
const lines = output.split("\n").filter((l) => l.trim().length > 0);
const processes: ProcessInfo[] = [];
if (platform === "win32") {
for (const line of lines.slice(1)) { // skip header
const parts = line.split("\t").map((p) => p.trim());
if (parts.length < 4) continue;
const pid = parseInt(parts[0], 10);
if (isNaN(pid)) continue;
processes.push({
pid,
name: parts[1] || "Unknown",
cpuUsage: parseFloat(parts[2]) || 0,
memoryUsage: parseInt(parts[3], 10) || 0,
status: parts[4] || "Unknown",
});
}
} else {
for (const line of lines) {
const parts = line.trim().split(/\s+/);
if (parts.length < 5) continue;
const pid = parseInt(parts[0], 10);
if (isNaN(pid)) continue;
processes.push({
pid,
name: parts[1] || "Unknown",
cpuUsage: parseFloat(parts[2]) || 0,
memoryUsage: parseInt(parts[3], 10) || 0,
status: parts[4] || "Unknown",
});
}
}
return processes;
}
// --- Kill process ---
export function killProcess(pid: number, signal?: string): ProcessKillResult {
if (!Number.isInteger(pid) || pid <= 0) {
return { success: false, pid, error: "Invalid PID" };
}
try {
const platform = process.platform;
if (platform === "win32") {
child_process.execSync(`taskkill /PID ${pid} /F`, { stdio: "pipe", timeout: 5000 });
} else {
const sig = signal || "SIGTERM";
process.kill(pid, sig as NodeJS.Signals);
}
return { success: true, pid, signal };
} catch (err: any) {
return { success: false, pid, error: err.message };
}
}
// --- Network info ---
export function getNetworkInfo(): NetworkInfo {
const interfaces = os.networkInterfaces();
const result: NetworkInfo["interfaces"] = [];
for (const [name, ifaces] of Object.entries(interfaces)) {
if (!ifaces) continue;
for (const iface of ifaces) {
result.push({
name,
address: iface.address,
family: iface.family as "IPv4" | "IPv6",
internal: iface.internal,
mask: iface.netmask || "",
});
}
}
return { interfaces: result };
}
// --- Disk info ---
export function getDiskInfo(): DiskInfo[] {
if (process.platform === "win32") {
return getWindowsDiskInfo();
}
return getUnixDiskInfo();
}
function getWindowsDiskInfo(): DiskInfo[] {
try {
const output = child_process.execFileSync(
"wmic",
["logicaldisk", "get", "deviceid,freespace,size"],
{ encoding: "utf-8", timeout: 5000 },
);
const lines = output.split("\n").filter((l) => l.trim().length > 0);
const disks: DiskInfo[] = [];
for (const line of lines.slice(1)) {
const parts = line.split(/\s+/).filter(Boolean);
if (parts.length < 3) continue;
disks.push({
device: parts[0],
mountpoint: parts[0] + "\\",
size: parseInt(parts[2], 10) || 0,
used: (parseInt(parts[2], 10) || 0) - (parseInt(parts[1], 10) || 0),
free: parseInt(parts[1], 10) || 0,
usePercent: 0,
});
}
return disks;
} catch {
return [];
}
}
function getUnixDiskInfo(): DiskInfo[] {
try {
const output = child_process.execFileSync("df", ["-h"], { encoding: "utf-8", timeout: 5000 });
const lines = output.split("\n").filter((l) => l.trim().length > 0);
const disks: DiskInfo[] = [];
for (const line of lines.slice(1)) {
const parts = line.split(/\s+/);
if (parts.length < 6) continue;
disks.push({
device: parts[0],
mountpoint: parts[5],
size: parseDiskSize(parts[1]),
used: parseDiskSize(parts[2]),
free: parseDiskSize(parts[3]),
usePercent: parseFloat(parts[4].replace("%", "")) || 0,
});
}
return disks;
} catch {
return [];
}
}
function parseDiskSize(sizeStr: string): number {
const unitMap: Record<string, number> = { K: 1024, M: 1024 ** 2, G: 1024 ** 3, T: 1024 ** 4, P: 1024 ** 5 };
const match = sizeStr.match(/^([0-9.]+)([KMGTP])$/i);
if (!match) return 0;
return Math.round(parseFloat(match[1]) * unitMap[match[2].toUpperCase()]);
}
// --- Check network (real TCP connectivity check) ---
export async function checkNetwork(
host: string,
port: number = 80,
timeoutMs: number = 5000,
): Promise<CheckNetworkResult> {
if (!host || typeof host !== "string") {
return { success: false, host: "", resolved: false, error: "Invalid host" };
}
try {
const dns = require("dns");
const startTime = Date.now();
return await new Promise<CheckNetworkResult>((resolve) => {
dns.lookup(host, (err, address, family) => {
const dnsMs = Date.now() - startTime;
if (err) {
resolve({ success: false, host, resolved: false, dnsMs, error: err.message });
return;
}
// Perform actual TCP connection check
const socket = new net.Socket();
const connectStart = Date.now();
socket.setTimeout(timeoutMs);
socket.on("connect", () => {
const tcpMs = Date.now() - connectStart;
socket.destroy();
resolve({
success: true,
host,
resolved: true,
port,
tcpConnectMs: tcpMs,
dnsMs,
});
});
socket.on("timeout", () => {
socket.destroy();
resolve({
success: false,
host,
resolved: true,
port,
dnsMs,
tcpConnectMs: Date.now() - connectStart,
error: `Connection to ${host}:${port} timed out (${timeoutMs}ms)`,
});
});
socket.on("error", (e: NodeJS.ErrnoException) => {
socket.destroy();
resolve({
success: false,
host,
resolved: true,
port,
dnsMs,
tcpConnectMs: Date.now() - connectStart,
error: `TCP connection failed: ${e.message}`,
});
});
socket.connect(port, address);
});
});
} catch (err: any) {
return { success: false, host, resolved: false, error: err.message };
}
}