docs: terminal geometry, WS size forwarding, GNS3_CONSOLE_RESIZE

docker-exec-console.md gains a "Terminal geometry and size forwarding"
section: the WS binary control-frame protocol ({"cols","rows"} -> NAWS /
asyncssh resize), the tall 511x10000 default and when it is applied or
restored, the exec-creation race handling, the GNS3_CONSOLE_RESIZE=0
knob for paging CLIs, and the SR Linux flicker root cause with the
measured numbers (rows-driven ~2.4x output inflation on CPR-answering
clients; width-independent; sr_cli's scroll-append re-rendering is
inherent and identical outside GNS3).

vendor-nos-xrd.md: the appliance recipe now includes
GNS3_CONSOLE_RESIZE=0 with the rationale (shared exec + XR pager vs
browser resizes). New troubleshooting entry for the flicker symptom.
This commit is contained in:
YueGuobin 2026-08-20 14:08:49 +08:00
parent 65e8eb9e28
commit 8d8b2c9692
No known key found for this signature in database
2 changed files with 116 additions and 16 deletions

View File

@ -31,17 +31,20 @@ they let a vendor NOS run as a first-class GNS3 Docker router node.
> schema changes, so existing Docker nodes (FRR, ipterm, …) are unaffected.
> `console_type: "docker_exec"` is added to the `ConsoleType` enum.
## The three environment knobs
## Environment knobs
All three are read from the node's `environment` field. Entries prefixed with
All are read from the node's `environment` field. Entries prefixed with
`GNS3_` are **not** forwarded into the container (existing GNS3 behaviour), so
they stay host-side configuration.
they stay host-side configuration. The console-relevant ones (the full vendor
set, incl. `GNS3_SHM_SIZE` / `GNS3_DEVICES` / `GNS3_MASK_UDEV` /
`GNS3_STOP_TIMEOUT`, is documented in [vendor-nos-xrd.md](./vendor-nos-xrd.md)):
| Variable | Purpose |
|----------|---------|
| `GNS3_SKIP_INIT=1` | Do **not** prepend `/gns3/init.sh` to the entrypoint. Vendor NOS images must run their own entrypoint (e.g. SR Linux's `sr_linux`); GNS3's init script (busybox bootstrap, `ifup`, eth wait) interferes with them. |
| `GNS3_INTERFACE_NAMES=mgmt0,e1-1,e1-2,e1-3` | Rename the injected interfaces in adapter order instead of the default `eth{N}`. SR Linux expects `mgmt0` + `e1-N`; without this it does not recognise its datapath. |
| `GNS3_CONSOLE_CMD=/opt/srlinux/bin/sr_cli` | Command run by the `docker_exec` console inside the container. |
| `GNS3_CONSOLE_RESIZE=0` | Ignore client-driven console resizes (WS terminal-size control frames / telnet NAWS) and keep the tall no-paging PTY geometry. Set for CLIs that page on the PTY window size (IOS-XR) — see [Terminal geometry](#terminal-geometry-and-size-forwarding). |
## Architecture: `VendorDockerVM` subclass
@ -87,18 +90,21 @@ Setting `console_type: "docker_exec"` makes the node's primary console port run
```mermaid
graph LR
A[Web UI xterm.js] -->|console WS| B[GNS3 Compute telnet server]
B -->|binary pty stream| C[Docker exec API]
C -->|Tty:true pty| D[sr_cli / vendor CLI]
A -.->|NAWS size| B
B -.->|POST exec/.../resize| C
A[Web UI xterm.js] -->|console WS: text frames| B[Controller forward]
B -->|WS: text + binary| C[GNS3 Compute telnet server]
C -->|binary pty stream| D[Docker exec API]
D -->|Tty:true pty| E[sr_cli / vendor CLI]
A -.->|binary control frame {"cols","rows"}| B
B -.-> C
C -.->|POST exec/.../resize| D
```
The console uses GNS3's **existing shared/broadcast telnet model**: a single
exec instance (one CLI session) is broadcast to every console client, exactly
like the primary console shares one PID 1. There is deliberately **no
per-client session isolation** — this matches how every other GNS3 console
behaves.
behaves. The PTY geometry is likewise shared: the last client resize wins
(see [Terminal geometry](#terminal-geometry-and-size-forwarding)).
### Implementation
@ -159,8 +165,11 @@ reconnect logic is unit-tested.
non-multiplexed bidirectional pty byte stream — no frame demux needed.
5. **NAWS → exec resize.** The telnet server runs with `naws=True`; the
`window_size_changed_callback` calls `POST exec/{eid}/resize?h=&w=` so the
TUI lays out for the xterm.js window size.
`window_size_changed_callback` (`_on_naws`, gated by
`GNS3_CONSOLE_RESIZE`) calls `POST exec/{eid}/resize?h=&w=` so the TUI
lays out for the client's window size. The internal `_resize_exec` path
(creation-time default, restore-on-idle) is not gated. See
[Terminal geometry](#terminal-geometry-and-size-forwarding).
6. **Binary passthrough + redraw.** `binary=True` so TUI escape sequences reach
xterm.js intact; `echo=False` (the pty echoes). On every client (re)connect
@ -168,9 +177,66 @@ reconnect logic is unit-tested.
screen for a previous client redraws for the new one (otherwise a
reconnect shows a blank screen until the next output).
### Terminal geometry and size forwarding
The exec PTY geometry is a shared resource with three consumers that want
different things:
- **Browser clients (xterm.js)** need the PTY to match their real window, or
TUI CLIs misrender and over-render (below).
- **Non-NAWS clients** (netmiko, bare telnet — no terminal-size negotiation)
need the PTY *tall*: CLIs that page on the PTY window size (the IOS-XR
pager ignores `terminal length 0`) park at `--More--` on a 24-row PTY.
- **Concurrent sessions share one exec** — one browser resize changes what
every attached client sees.
Resolution:
1. **Tall default.** The exec is created at 511×10000 (width 511 matches
netmiko's `terminal width 511` convention). Non-NAWS clients get no paging
and no hard wrapping.
2. **WS terminal-size forwarding.** Console WebSocket clients may send
**binary control frames** — UTF-8 JSON `{"cols": N, "rows": N}`
alongside text frames carrying terminal data (xterm.js's AttachAddon only
sends text, so binary is an unambiguous side channel; valid ranges are
cols 25000, rows 2100000, anything else is silently ignored). The
controller forwards binary frames (previously only text was forwarded —
and a binary frame would have crashed the old `receive_text` loop), and
the compute side turns them into a telnet NAWS subnegotiation for
telnet-based consoles (docker_exec included) or an asyncssh
`change_terminal_size` for SSH consoles
(`base_node.py` `start_websocket_console`).
3. **Races.** A size frame that arrives before/during the exec creation is
remembered and applied right after creation — it is **not** overwritten by
the tall default. When the **last** client disconnects the exec goes back
to 511×10000, so a later non-NAWS client attaching to the still-live exec
doesn't inherit a browser geometry and hit PTY-window paging.
4. **`GNS3_CONSOLE_RESIZE=0`** makes the console ignore client resizes
entirely (the tall default is then permanent). Set it for paging CLIs
where a browser resize would break concurrent netmiko sessions on the
shared exec — XRd, which is line-oriented and doesn't need browser
resizing at all.
**Why the browser must send its size — the SR Linux flicker.** `sr_cli` is a
prompt_toolkit TUI that anchors its layout with cursor-position requests
(CPR), which xterm.js answers. On a 10000-row PTY canvas the CPR-anchored
model conflicts with the winsize model, and every incremental render re-emits
the accumulated output: measured with a CPR-answering client, one `info`
command produces **~145 KB instead of ~60 KB** (~7× duplicated lines either
way — the CLI re-renders its output region as a scroll-append stream; that
part is inherent to `sr_cli` and identical outside GNS3, verified via manual
`docker exec`). The inflation is driven by **rows** (24/32 → normal, 10000 →
pathological, at any width) and is invisible without CPR answers — which is
why plain-telnet probes and real xterm.js sessions behaved so differently.
In the Web UI the excess renders as frequent full-screen clear/redraw — the
"flicker". With the browser's real size forwarded (rows ≈ 30), output volume
and rendering return to normal.
**File**: `gns3server/compute/base_node.py` — the console WebSocket guard now
allows `docker_exec` (alongside `telnet`/`ssh`), since the WS bridge connects to
the console TCP port exactly as it does for telnet.
the console TCP port exactly as it does for telnet. The same WS handler also
intercepts binary control frames and propagates client terminal sizes (see
[Terminal geometry](#terminal-geometry-and-size-forwarding)).
### Why earlier approaches failed (context)
@ -418,17 +484,35 @@ present on the host.
effect. If it does not, the image needs the bridge earlier (a
vendor-specific entrypoint wrapper, not covered by this prototype).
**11. Web console flickers (full-screen clear/redraw) on every command**
- The PTY is stuck at the tall 511×10000 default while a CPR-answering client
is attached — see
[Terminal geometry](#terminal-geometry-and-size-forwarding). Check that the
Web UI actually sends the binary size control frames on connect/resize
(F12 → the console WS should show outgoing binary frames), and that the
server is new enough to forward them (the controller used to forward text
frames only). A client that never negotiates/forwards size (old Web UI,
bare telnet without NAWS) cannot trigger the fix — but also never answers
CPR, so it doesn't flicker either.
- The much milder per-keystroke/5 s cursor toggles (`\e[?25l…\e[?25h`) from
the TUI are normal and not this bug.
## Limitations
1. **Shared session (broadcast).** All console clients share one CLI session
and can see each other's input — identical to GNS3's existing primary
console model. There is no per-client independent session.
console model. There is no per-client independent session. The PTY
geometry is shared too (last resize wins): two browsers of different sizes
disagree harmlessly, but a browser on a *paging* CLI needs
`GNS3_CONSOLE_RESIZE=0` to stop resizing on behalf of concurrent netmiko
sessions (see [Terminal geometry](#terminal-geometry-and-size-forwarding)).
2. **`reset_console` not wired.** The console-reset action only handles
`telnet`/`ssh`; it is a no-op for `docker_exec` (non-blocking; reconnect
works fine).
3. **Prototype knobs.** `GNS3_SKIP_INIT` / `GNS3_INTERFACE_NAMES` /
`GNS3_CONSOLE_CMD` are environment-driven; they are not yet first-class node
schema fields and are not declared in the appliance (`gns3a`) schema.
`GNS3_CONSOLE_CMD` / `GNS3_CONSOLE_RESIZE` are environment-driven; they are
not yet first-class node schema fields and are not declared in the
appliance (`gns3a`) schema.
4. **Rootful-Docker assumption** (`UsernsMode: host`, set for all GNS3
Docker nodes) so the container-side chown acts on the host files' real
uid/gid (see the volume-persistence section).
@ -448,7 +532,11 @@ present on the host.
`_get_container_ifname`, `_cleanup_console_resources`).
- `gns3server/compute/docker/__init__.py``Docker._select_node_class` /
`create_node` factory.
- `gns3server/compute/base_node.py` — console WebSocket guard.
- `gns3server/compute/base_node.py` — console WebSocket guard; binary
terminal-size control frames → NAWS / asyncssh resize
(`start_websocket_console`).
- `gns3server/api/routes/controller/nodes.py` — console WS forwarding
(text and binary frames).
- `gns3server/schemas/common.py``ConsoleType.docker_exec`.
- containerlab `nodes/srl/srl.go` — reference for SR Linux launch command and
interface naming.
@ -457,6 +545,7 @@ present on the host.
| Version | Date | Changes |
|---------|------|---------|
| 1.6 | 2026-08-20 | Terminal geometry and size forwarding: WS binary control frames `{"cols","rows"}` → NAWS / asyncssh resize (controller now forwards binary frames; compute intercepts them); tall 511×10000 default kept for non-NAWS clients, applied post-creation and restored on last disconnect (client size racing exec creation wins over the default); new `GNS3_CONSOLE_RESIZE=0` knob for paging CLIs (XRd) where a browser resize would break concurrent netmiko sessions on the shared exec; documented the SR Linux flicker root cause (tall rows × CPR-answering client → ~2.4× re-emitted output; rows-driven, width-independent). |
| 1.5 | 2026-08-13 | Add appliance (`gns3a`) packaging section: 35-adapter full-chassis design, the three server-side schema fixes (DockerConsoleType, ApplianceV1_6.custom_adapters, extra_volumes passthrough), and the symbol-theme caveat (any `:/symbols/` symbol is rewritten to the category default at load). |
| 1.4 | 2026-08-13 | Reconnect fix: drop the while-true wrapper (it restarted the CLI with no client to answer CPR → blank screen on reconnect); the exec is now recreated on connect when the upstream has died. `_LazyExecTelnetServer` extracted to module level and unit-tested. |
| 1.3 | 2026-08-12 | Document runtime ownership safety (root processes, self-healing daemons, ACL evidence for SR Linux), the boot-ordering caveat (bridge after boot → verify save/stop/start closed loop), and troubleshooting #10. |

View File

@ -100,6 +100,7 @@ Note: "journal corrupted" messages with varying machine-IDs come from the
```
GNS3_SKIP_INIT=1
GNS3_CONSOLE_CMD=/pkg/bin/xr_cli.sh
GNS3_CONSOLE_RESIZE=0
GNS3_MASK_UDEV=1
GNS3_SHM_SIZE=1024
GNS3_DEVICES=/dev/fuse
@ -109,6 +110,15 @@ XR_MGMT_INTERFACES=linux:eth0,xr_name=Mg0/RP0/CPU0/0,chksum,snoop_v4,snoop_v6
XR_INTERFACES=linux:eth1,xr_name=Gi0/0/0/0;linux:eth2,xr_name=Gi0/0/0/1;...
```
`GNS3_CONSOLE_RESIZE=0` (console geometry lock): the XR pager pages on the
PTY window size and ignores `terminal length 0`, so the exec PTY must stay at
the tall no-paging default for **every** client. The docker_exec console is a
single shared exec — one browser's terminal-size resize (WS control frames →
NAWS) would change the geometry concurrent netmiko/copilot sessions see and
bring `--More--` back. XRd's CLI is line-oriented, so browsers lose nothing
by not resizing. See
[docker-exec-console.md](./docker-exec-console.md#terminal-geometry-and-size-forwarding).
XRd-specific gotchas (image-side, not GNS3):
- Management interface xr_name is **`Mg0/RP0/CPU0/0`** (short prefix, `CPU0`
@ -201,6 +211,7 @@ sequenceDiagram
| Version | Date | Changes |
|---------|------|---------|
| 1.5 | 2026-08-20 | Appliance env gains `GNS3_CONSOLE_RESIZE=0`: client-driven console resizes are ignored so the shared exec PTY stays at the tall no-paging geometry for concurrent netmiko/copilot sessions (browsers included). |
| 1.4 | 2026-08-15 | Code-review hardening: stop-query HTTP timeout scales with `GNS3_STOP_TIMEOUT` (values >300 s no longer abort); overlapping mask/config bind targets deduplicated (Docker "Duplicate mount point"); `ExtraConfig.target` validated at save time and directory forms rejected; host-readiness check no longer aborts on one unreadable `/proc/sys` key; base env parser strips trailing commas; vendor env knobs re-parsed on create (PUT environment takes effect); graceful stop limited to explicit user stop (delete/update/close keep the immediate kill); extra_configs under a persisted volume warns. |
| 1.3 | 2026-08-14 | Persistence corrected: XR's live data layer is `/xr-storage` (the image's symlink farm is materialized into real directories at bootstrap; `/xr-storage-shadow` is a pristine spare) — the appliance persists both. Vendor containers now stop gracefully (SIGTERM + `GNS3_STOP_TIMEOUT` grace, default 60 s) instead of being SIGKILLed on the spot. |
| 1.2 | 2026-08-14 | Link UDP self-loop root-caused to a port-allocation race and fixed — see [link-udp-self-loop](../bugs/link-udp-self-loop.md) (now marked Fixed). |