import type { UniDeskConfig } from "./config"; import { rootPath } from "./config"; import { runCommand } from "./command"; import type { RenderedCliResult } from "./output"; import { egressBenchmarkCompactResult, egressBenchmarkDryRun, egressBenchmarkStartScript, egressBenchmarkStatusScript, type EgressBenchmarkSpec } from "./egress-proxy-benchmark"; import { egressProxyTrafficCompactResult, egressProxyTrafficScript, type EgressProxyTrafficSpec } from "./egress-proxy-traffic"; import { readSub2ApiConfig } from "./platform-infra/config"; import { resolveTarget } from "./platform-infra/manifest"; import { runK3sBuildBenchmarkCommand } from "./platform-infra-k3s-build-benchmark"; type BenchmarkAction = "benchmark" | "benchmark-status" | "benchmark-logs"; type EgressProxyAction = BenchmarkAction | "traffic"; interface BenchmarkOptions { action: BenchmarkAction; targetId: string; profile: "no-mirror"; confirm: boolean; dryRun: boolean; samples: number; sampleTimeoutSeconds: number; timeoutSeconds: number; tailLines: number; } interface TrafficOptions { action: "traffic"; targetId: string; sampleSeconds: number; timeoutSeconds: number; limit: number; } type EgressProxyOptions = BenchmarkOptions | TrafficOptions; export async function runPlatformInfraEgressProxyCommand(_config: UniDeskConfig, args: string[]): Promise | RenderedCliResult> { if (args[0] === "host") { const { runPlatformInfraHostProxyCommand } = await import("./platform-infra-host-proxy"); return await runPlatformInfraHostProxyCommand(_config, args.slice(1)); } if (args[0] === "k3s-build-benchmark") return runK3sBuildBenchmarkCommand(args.slice(1)); const options = parseEgressProxyOptions(args); if (options.action === "traffic") { const spec = platformTrafficSpec(options); return proxyTraffic(spec, options); } const spec = platformBenchmarkSpec(options); if (options.action === "benchmark-status" || options.action === "benchmark-logs") return benchmarkStatus(spec, options); if (options.dryRun) { return renderBenchmark({ ok: true, command: "platform-infra egress-proxy benchmark", mode: "dry-run", mutation: false, plan: egressBenchmarkDryRun(spec), next: { confirm: `bun scripts/cli.ts platform-infra egress-proxy benchmark --target ${options.targetId} --profile ${options.profile} --confirm` }, }); } const result = runTrans(spec.route, egressBenchmarkStartScript(spec), options.timeoutSeconds); const parsed = parseJson(result.stdout); return renderBenchmark({ ok: result.exitCode === 0, command: "platform-infra egress-proxy benchmark", mode: "async-job", mutation: result.exitCode === 0, target: options.targetId, profile: options.profile, start: typeof parsed === "object" && parsed !== null ? parsed : { stdoutPreview: result.stdout.slice(0, 2000) }, result: egressBenchmarkCompactResult(result), }); } function proxyTraffic(spec: EgressProxyTrafficSpec, options: TrafficOptions): RenderedCliResult { const result = runTrans(spec.route, egressProxyTrafficScript(spec, options.sampleSeconds), options.timeoutSeconds); const parsed = parseJson(result.stdout); const data = typeof parsed === "object" && parsed !== null ? parsed as Record : { ok: false, reason: "traffic-output-unparseable", stdoutPreview: result.stdout.slice(0, 2000) }; return renderTraffic({ ...data, ok: result.exitCode === 0 && data.ok !== false, command: "platform-infra egress-proxy traffic", mode: "traffic", target: spec.targetId, namespace: spec.namespace, serviceName: spec.serviceName, servicePort: spec.port, sampleSecondsRequested: options.sampleSeconds, limit: options.limit, result: egressProxyTrafficCompactResult(result), }); } function benchmarkStatus(spec: EgressBenchmarkSpec, options: BenchmarkOptions): RenderedCliResult { const result = runTrans(spec.route, egressBenchmarkStatusScript(spec, options.tailLines), options.timeoutSeconds); const parsed = parseJson(result.stdout); const status = record(record(parsed).status); const state = text(status.state, "unknown"); return renderBenchmark({ ok: result.exitCode === 0 && (options.action === "benchmark-logs" || state !== "failed"), command: `platform-infra egress-proxy ${options.action}`, mode: "status", mutation: false, target: options.targetId, profile: options.profile, job: typeof parsed === "object" && parsed !== null ? parsed : { stdoutPreview: result.stdout.slice(0, 2000) }, result: egressBenchmarkCompactResult(result), }); } function platformBenchmarkSpec(options: BenchmarkOptions): EgressBenchmarkSpec { const sub2api = readSub2ApiConfig(); const target = resolveTarget(sub2api, options.targetId); const proxy = target.egressProxy; if (proxy === null || !proxy.enabled) throw new Error(`target ${target.id} has no enabled egressProxy`); return { scope: "platform-infra", targetId: target.id, route: target.route, namespace: target.namespace, serviceName: proxy.serviceName, port: proxy.listenPort, noProxy: proxy.noProxy, sourceType: proxy.sourceType, sourceRef: proxy.sourceRef, sourceConfigRef: proxy.sourceConfigRef, sourceFingerprint: proxy.sourceFingerprint, profile: options.profile, samples: options.samples, sampleTimeoutSeconds: options.sampleTimeoutSeconds, }; } function platformTrafficSpec(options: TrafficOptions): EgressProxyTrafficSpec { const sub2api = readSub2ApiConfig(); const target = resolveTarget(sub2api, options.targetId); const proxy = target.egressProxy; if (proxy === null || !proxy.enabled) throw new Error(`target ${target.id} has no enabled egressProxy`); return { scope: "platform-infra", targetId: target.id, route: target.route, namespace: target.namespace, deploymentName: proxy.deploymentName, serviceName: proxy.serviceName, port: proxy.listenPort, sourceType: proxy.sourceType, sourceRef: proxy.sourceRef, sourceConfigRef: proxy.sourceConfigRef, sourceFingerprint: proxy.sourceFingerprint, }; } function parseEgressProxyOptions(args: string[]): EgressProxyOptions { const actionRaw = args[0] ?? "benchmark"; if (!isEgressProxyAction(actionRaw)) { throw new Error("platform-infra egress-proxy usage: benchmark|benchmark-status|benchmark-logs|traffic|k3s-build-benchmark --target D601|D518 [--profile no-mirror|no-mirror-600m|real-deps-500m] [--sample-seconds N]"); } const action = actionRaw; const rest = args.slice(1); const targetId = option(rest, "--target") ?? "D601"; if (action === "traffic") { const sampleSeconds = positiveIntOption(rest, "--sample-seconds", 5, 45); return { action, targetId, sampleSeconds, timeoutSeconds: positiveIntOption(rest, "--timeout-seconds", Math.min(60, sampleSeconds + 20), 60), limit: positiveIntOption(rest, "--limit", 50, 200), }; } const profileRaw = option(rest, "--profile") ?? "no-mirror"; if (profileRaw !== "no-mirror") throw new Error("--profile currently supports no-mirror"); const confirm = rest.includes("--confirm"); const dryRun = rest.includes("--dry-run") || !confirm; if (confirm && rest.includes("--dry-run")) throw new Error("benchmark accepts only one of --confirm or --dry-run"); return { action, targetId, profile: profileRaw, confirm, dryRun: action === "benchmark" ? dryRun : false, samples: positiveIntOption(rest, "--samples", 5, 20), sampleTimeoutSeconds: positiveIntOption(rest, "--sample-timeout-seconds", 240, 900), timeoutSeconds: positiveIntOption(rest, "--timeout-seconds", 30, 60), tailLines: positiveIntOption(rest, "--tail", 40, 200), }; } function isEgressProxyAction(value: string): value is EgressProxyAction { return value === "benchmark" || value === "benchmark-status" || value === "benchmark-logs" || value === "traffic"; } function runTrans(route: string, script: string, timeoutSeconds: number) { return runCommand(["/root/.local/bin/trans", route, "sh", "--", script], rootPath(), { timeoutMs: timeoutSeconds * 1000 }); } function option(args: string[], name: string): string | null { const index = args.indexOf(name); if (index === -1) return null; const value = args[index + 1]; if (value === undefined || value.startsWith("--")) throw new Error(`${name} requires a value`); return value; } function positiveIntOption(args: string[], name: string, defaultValue: number, maxValue: number): number { const raw = option(args, name); if (raw === null) return defaultValue; const value = Number.parseInt(raw, 10); if (!Number.isInteger(value) || value < 1 || value > maxValue) throw new Error(`${name} must be an integer from 1 to ${maxValue}`); return value; } function parseJson(text: string): unknown { const trimmed = text.trim(); if (trimmed.length === 0) return null; try { return JSON.parse(trimmed); } catch { const start = trimmed.indexOf("{"); const end = trimmed.lastIndexOf("}"); if (start >= 0 && end > start) { try { return JSON.parse(trimmed.slice(start, end + 1)); } catch {} } } return null; } function renderBenchmark(result: Record): RenderedCliResult { const start = record(result.start); const job = record(result.job); const status = record(job.status); const plan = record(result.plan); const next = record(result.next); const rows = Array.isArray(status.rows) ? status.rows.map(record) : []; const target = text(result.target ?? plan.target); const profile = text(result.profile ?? plan.profile, "no-mirror"); const statusText = text(status.state, result.ok === false ? "failed" : "ok"); const logTail = typeof job.logTail === "string" ? job.logTail.trimEnd() : ""; const lines = [ `platform-infra egress-proxy ${result.mode === "dry-run" ? "benchmark dry-run" : "benchmark"}`, "", ...table(["TARGET", "PROFILE", "MODE", "STATUS"], [[target, profile, text(result.mode), statusText]]), "", rows.length === 0 ? "RESULTS\n-" : [ "RESULTS", ...table(["TEST", "SUCCESS", "SAMPLES", "P50", "P95", "FAILURES"], rows.map((row) => [ text(row.test), text(row.success), text(row.samples), text(row.p50Ms), text(row.p95Ms), JSON.stringify(row.failureFamilies ?? {}), ])), ].join("\n"), ...(logTail.length === 0 ? [] : ["", "LOG TAIL", logTail]), "", "NEXT", ` ${text(next.confirm, "")}`, ` ${text(start.statusCommand, `bun scripts/cli.ts platform-infra egress-proxy benchmark-status --target ${target} --profile ${profile}`)}`, ` ${text(start.logsCommand, `bun scripts/cli.ts platform-infra egress-proxy benchmark-logs --target ${target} --profile ${profile}`)}`, "", "Disclosure: default output is a bounded benchmark table; Secret/proxy source values are redacted.", ].filter((line) => line !== " "); return { ok: result.ok !== false, command: text(result.command, "platform-infra egress-proxy benchmark"), renderedText: lines.join("\n"), contentType: "text/plain" }; } function renderTraffic(result: Record): RenderedCliResult { const totals = record(result.totals); const source = record(result.source); const next = record(result.next); const clients = arrayRecords(result.clients).slice(0, Number(result.limit ?? 50)); const allClientCount = arrayRecords(result.clients).length; const ok = result.ok !== false; const target = text(result.target); const reason = text(result.reason, ok ? "ok" : "failed"); const rows = clients.map((client) => { const destinations = arrayRecords(client.topDestinations) .map((item) => `${text(item.destination)}(${text(item.samples)})`) .join(", "); return [ text(client.client), text(client.activeConnections, "0"), rate(client.totalBps), rate(client.uploadBps), rate(client.downloadBps), bytes(client.windowTotalBytes), bytes(client.activeTotalBytes), destinations || "-", ]; }); const lines = [ "PLATFORM-INFRA EGRESS-PROXY TRAFFIC", "", ...table(["TARGET", "STATUS", "POD", "DURATION", "SNAPSHOTS", "ACTIVE"], [[ target, ok ? "ok" : reason, text(result.pod), `${numberText(result.sampleSecondsActual, result.sampleSecondsRequested)}s`, text(result.snapshots, "0"), text(totals.activeConnections, "0"), ]]), "", "TOTALS", ...table(["SCOPE", "RATE", "WINDOW", "ACTIVE/CUMULATIVE"], [ ["per-client", rate(totals.clientTotalBps), bytes(totals.clientWindowTotalBytes), bytes(totals.clientActiveTotalBytes)], ["process", rate(totals.processWindowBps), bytes(totals.processWindowTotalBytes), bytes(totals.processTotalBytes)], ]), "", rows.length === 0 ? "CLIENTS\n-" : [ "CLIENTS", ...table(["CLIENT", "CONNS", "TOTAL/s", "UP/s", "DOWN/s", "WINDOW", "ACTIVE_TOTAL", "TOP_DESTINATIONS"], rows), ].join("\n"), allClientCount > clients.length ? `\nDisclosure: ${allClientCount - clients.length} more clients hidden; rerun with --limit ${Math.min(200, allClientCount)}.` : "", "", "SOURCE", ` target=${target} namespace=${text(result.namespace)} service=${text(result.serviceName)}:${text(result.servicePort)} controller=${text(result.controller)}`, ` sourceType=${text(source.sourceType)} sourceRef=${text(source.sourceRef)} sourceFingerprint=${text(source.sourceFingerprint)} valuesPrinted=false`, "", "NEXT", ` ${text(next.apply, "")}`, ` ${text(next.status, `bun scripts/cli.ts platform-infra sub2api status --target ${target} --full`)}`, ` bun scripts/cli.ts platform-infra egress-proxy traffic --target ${target} --sample-seconds 15`, "", "Disclosure: proxy API is read through pod loopback only. Per-client cumulative bytes are for the sample window; process cumulative is since proxy process start when reported by sing-box.", ].filter((line) => line !== " "); return { ok, command: "platform-infra egress-proxy traffic", renderedText: lines.join("\n"), contentType: "text/plain" }; } function table(headers: string[], rows: string[][]): string[] { const widths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => row[index]?.length ?? 0))); const render = (row: string[]) => row.map((cell, index) => cell.padEnd(widths[index] ?? cell.length)).join(" ").trimEnd(); return [render(headers), ...rows.map(render)]; } function record(value: unknown): Record { return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record : {}; } function arrayRecords(value: unknown): Array> { return Array.isArray(value) ? value.map(record) : []; } function text(value: unknown, fallback = "-"): string { if (value === undefined || value === null || value === "") return fallback; return String(value); } function numberText(value: unknown, fallback: unknown): string { const number = typeof value === "number" && Number.isFinite(value) ? value : Number(fallback); if (!Number.isFinite(number)) return text(value); return number.toFixed(number >= 10 ? 0 : 1).replace(/\.0$/u, ""); } function bytes(value: unknown): string { const number = Number(value); if (!Number.isFinite(number) || number <= 0) return "0 B"; const units = ["B", "KiB", "MiB", "GiB", "TiB"]; let scaled = number; let unit = 0; while (scaled >= 1024 && unit < units.length - 1) { scaled /= 1024; unit += 1; } return `${scaled >= 10 || unit === 0 ? scaled.toFixed(0) : scaled.toFixed(1)} ${units[unit]}`; } function rate(value: unknown): string { return `${bytes(value)}/s`; }