mirror of
https://github.com/hansjone/netx.git
synced 2026-10-15 04:53:20 +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
|
|
@ -545,18 +545,26 @@ def _connect_direct(
|
|||
|
||||
|
||||
def _read_channel(conn: ConnectHandler, wait: float = 0.5, max_loops: int = 40) -> str:
|
||||
if wait > 0:
|
||||
time.sleep(wait)
|
||||
"""Collect device output.
|
||||
|
||||
``wait`` is the longest we park for the *first* bytes; once output has arrived the read
|
||||
returns as soon as the channel is quiet again. (It used to sleep the whole ``wait`` and
|
||||
then poll at 50-150ms, which added ~0.1-0.4s of dead time to every login-prompt step.)
|
||||
"""
|
||||
quiet = 0.04 if wait <= 0.15 else 0.12
|
||||
first_deadline = time.monotonic() + max(0.0, wait)
|
||||
got_at: float | None = None
|
||||
chunks: list[str] = []
|
||||
for _ in range(max_loops):
|
||||
reads = 0
|
||||
while reads < max_loops:
|
||||
try:
|
||||
part = conn.read_channel()
|
||||
except Exception:
|
||||
break
|
||||
if part is None or part is False:
|
||||
break
|
||||
part = ""
|
||||
# MagicMock truthy-but-empty: stop when unit tests leave read_channel unconfigured.
|
||||
if not isinstance(part, (str, bytes, bytearray)):
|
||||
elif not isinstance(part, (str, bytes, bytearray)):
|
||||
text = str(part)
|
||||
# unittest.mock default str looks like "<MagicMock name=...>"
|
||||
if text.startswith("<MagicMock") or text.startswith("<Mock"):
|
||||
|
|
@ -564,10 +572,18 @@ def _read_channel(conn: ConnectHandler, wait: float = 0.5, max_loops: int = 40)
|
|||
part = text
|
||||
elif isinstance(part, (bytes, bytearray)):
|
||||
part = bytes(part).decode("utf-8", errors="replace")
|
||||
if not part:
|
||||
now = time.monotonic()
|
||||
if part:
|
||||
chunks.append(part)
|
||||
reads += 1
|
||||
got_at = now
|
||||
continue
|
||||
if got_at is None:
|
||||
if now >= first_deadline:
|
||||
break
|
||||
elif now - got_at >= quiet:
|
||||
break
|
||||
chunks.append(part)
|
||||
time.sleep(0.05 if wait <= 0.15 else 0.15)
|
||||
time.sleep(0.01)
|
||||
return "".join(chunks)
|
||||
|
||||
|
||||
|
|
@ -801,11 +817,11 @@ def _interactive_target_auth(
|
|||
return
|
||||
else:
|
||||
empty_after_pass = 0
|
||||
time.sleep(0.12)
|
||||
time.sleep(0.02)
|
||||
continue
|
||||
|
||||
if not buf.strip():
|
||||
time.sleep(0.2)
|
||||
time.sleep(0.02)
|
||||
continue
|
||||
# Huawei stelnet often reprints the hop ``[sysname]`` after host-key trust and
|
||||
# *before* ``Enter password:``. Do not treat that as target login success.
|
||||
|
|
@ -826,7 +842,7 @@ def _interactive_target_auth(
|
|||
if denied:
|
||||
raise paramiko.AuthenticationException(f"target_auth_rejected: {denied}")
|
||||
return
|
||||
time.sleep(0.15)
|
||||
time.sleep(0.02)
|
||||
denied = _target_auth_failure_snippet(acc, sent_pass=sent_pass)
|
||||
if denied:
|
||||
raise paramiko.AuthenticationException(f"target_auth_rejected: {denied}")
|
||||
|
|
@ -879,7 +895,7 @@ def _huawei_enter_system_view(
|
|||
for _ in range(8):
|
||||
part = _read_channel(conn, wait=0.2, max_loops=10)
|
||||
if not part:
|
||||
time.sleep(0.15)
|
||||
time.sleep(0.02)
|
||||
continue
|
||||
acc += part
|
||||
if emit_raw:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue