mirror of
https://github.com/hansjone/netx.git
synced 2026-10-15 03:40:51 +08:00
Speed up WebCRT and record audited commands from the device echo.
Performance: event-driven stdout pump (no polling thread per socket, no 200ms coalescing stall), keystrokes written immediately, blocking SSH reads, bounded deque queue with batch drain, incremental UTF-8 decode, buffered session log, non-blocking session teardown, shorter login/settle waits, xterm WebGL renderer and idle-time direct writes. Zero-latency fake device: echo p50 11ms -> 2.3ms, command RTT 219ms -> 20ms, bulk output 2.1 -> 8.3 MB/s. Audit: track the device's own input row server-side (webcrt_cmd_audit) and resolve each Enter from the echoed line feed, so Tab, backspace, cursor-key edits, history recall, paste, Ctrl-C and pagers record what actually ran. Password lines are redacted; pending Enters are flushed on close. Hop: stop closing the hop session when the target prints the same prompt as the hop (system-view); recognise the Huawei "closed by the remote host" message. Generic NEs that show a network prompt now get the network key map. Adds bench_webcrt.py, fake_ssh_device.py and e2e_webcrt_input_scenarios.py. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
0d894efe5e
commit
45de8144a3
14 changed files with 2060 additions and 241 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import { Button, Input } from "@heroui/react";
|
||||
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react";
|
||||
import { FitAddon } from "@xterm/addon-fit";
|
||||
import { WebglAddon } from "@xterm/addon-webgl";
|
||||
import { Terminal } from "@xterm/xterm";
|
||||
import "@xterm/xterm/css/xterm.css";
|
||||
import { useI18n } from "../../../i18n";
|
||||
|
|
@ -250,13 +251,20 @@ function keyEventToStdin(e: KeyboardEvent): string | null {
|
|||
return null;
|
||||
}
|
||||
|
||||
function decodeBytes(buf: ArrayBuffer, _encoding: string): string {
|
||||
// Server normalizes device encodings (e.g. GBK) to UTF-8 on the wire.
|
||||
try {
|
||||
return new TextDecoder("utf-8").decode(buf);
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
/**
|
||||
* Server normalizes device encodings (e.g. GBK) to UTF-8 on the wire. Frames are cut at
|
||||
* arbitrary byte boundaries, so decode as a stream (a split multi-byte char must not
|
||||
* turn into U+FFFD).
|
||||
*/
|
||||
function createStdoutDecoder(): (buf: ArrayBuffer) => string {
|
||||
const dec = new TextDecoder("utf-8");
|
||||
return (buf) => {
|
||||
try {
|
||||
return dec.decode(buf, { stream: true });
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const PREF_KEY = "netx.webcrt.termPrefs";
|
||||
|
|
@ -540,6 +548,28 @@ export const WebTerminal = forwardRef<WebTerminalHandle, Props>(function WebTerm
|
|||
const fit = new FitAddon();
|
||||
term.loadAddon(fit);
|
||||
term.open(host);
|
||||
// GPU renderer: the default DOM renderer is the main cost when a device dumps
|
||||
// thousands of lines. Fall back silently (headless/old GPU/context loss).
|
||||
let webgl: WebglAddon | null = null;
|
||||
try {
|
||||
webgl = new WebglAddon();
|
||||
webgl.onContextLoss(() => {
|
||||
try {
|
||||
webgl?.dispose();
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
webgl = null;
|
||||
});
|
||||
term.loadAddon(webgl);
|
||||
} catch {
|
||||
try {
|
||||
webgl?.dispose();
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
webgl = null;
|
||||
}
|
||||
termRef.current = term;
|
||||
fitRef.current = fit;
|
||||
|
||||
|
|
@ -594,27 +624,31 @@ export const WebTerminal = forwardRef<WebTerminalHandle, Props>(function WebTerm
|
|||
|
||||
const isActiveSocket = () => !cancelled && !!ws && wsRef.current === ws;
|
||||
|
||||
// Coalesce high-rate stdout into one paint frame to cut xterm write churn.
|
||||
// Echo goes straight to xterm when the terminal is idle (no frame of added latency);
|
||||
// only bursts are coalesced into one paint frame to cut xterm write churn.
|
||||
let writeBuf = "";
|
||||
let writeRaf = 0;
|
||||
let lastWriteAt = 0;
|
||||
const decodeStdout = createStdoutDecoder();
|
||||
const flushWriteBuf = () => {
|
||||
writeRaf = 0;
|
||||
if (writeRaf) {
|
||||
cancelAnimationFrame(writeRaf);
|
||||
writeRaf = 0;
|
||||
}
|
||||
const chunk = writeBuf;
|
||||
writeBuf = "";
|
||||
if (!chunk || cancelled) return;
|
||||
lastWriteAt = performance.now();
|
||||
term.write(applyKeywordHighlight(chunk, keywordHighlightRef.current));
|
||||
};
|
||||
const writeStdout = (raw: string) => {
|
||||
if (!raw || !isActiveSocket()) return;
|
||||
lastStdoutAtRef.current = performance.now();
|
||||
const now = performance.now();
|
||||
lastStdoutAtRef.current = now;
|
||||
// Always forward to parent (log buffer / frozen failure transcript); recording flag only gates download.
|
||||
onStdoutRef.current?.(raw);
|
||||
writeBuf += raw;
|
||||
if (writeBuf.length >= 16384) {
|
||||
if (writeRaf) {
|
||||
cancelAnimationFrame(writeRaf);
|
||||
writeRaf = 0;
|
||||
}
|
||||
if (writeBuf.length >= 16384 || (!writeRaf && now - lastWriteAt >= 16)) {
|
||||
flushWriteBuf();
|
||||
return;
|
||||
}
|
||||
|
|
@ -641,13 +675,13 @@ export const WebTerminal = forwardRef<WebTerminalHandle, Props>(function WebTerm
|
|||
socket.onmessage = (ev) => {
|
||||
if (!isActiveSocket()) return;
|
||||
if (ev.data instanceof ArrayBuffer) {
|
||||
writeStdout(decodeBytes(ev.data, encodingRef.current));
|
||||
writeStdout(decodeStdout(ev.data));
|
||||
return;
|
||||
}
|
||||
if (typeof Blob !== "undefined" && ev.data instanceof Blob) {
|
||||
void ev.data.arrayBuffer().then((buf) => {
|
||||
if (!isActiveSocket()) return;
|
||||
writeStdout(decodeBytes(buf, encodingRef.current));
|
||||
writeStdout(decodeStdout(buf));
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
|
@ -902,6 +936,12 @@ export const WebTerminal = forwardRef<WebTerminalHandle, Props>(function WebTerm
|
|||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
try {
|
||||
webgl?.dispose();
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
webgl = null;
|
||||
if (writeRaf) {
|
||||
cancelAnimationFrame(writeRaf);
|
||||
writeRaf = 0;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue