The 60 s SIGTERM grace was hardcoded, unlike every other vendor knob
(GNS3_SHM_SIZE, GNS3_DEVICES, GNS3_MASK_UDEV, ...) which rides the
environment line. Parse GNS3_STOP_TIMEOUT=<seconds> (default 60,
clamped to 1-600, invalid values keep the default) and use it in
VendorDockerVM._terminate_container().
DockerVM.stop() terminated containers with an immediate SIGKILL — fine
for init.sh-based containers whose state is persisted beforehand, but a
systemd NOS (Cisco XRd, SR Linux) needs a graceful shutdown and treats
the abrupt kill as an unclean shutdown (exit 137 on every stop).
Extract the final termination into _terminate_container() and override
it in VendorDockerVM: POST /containers/{id}/stop?t=60 sends SIGTERM and
waits for systemd to stop services; Docker itself SIGKILLs the
container once the grace period expires, so no fallback is needed.
Docker's 304 (already stopped) is swallowed.
The reconnect-blank-screen bug: when sr_cli exited (quit / idle timeout /
crash) the while-true wrapper restarted it mid-session with no client
attached, so its startup CPR probe (\e[6n) went unanswered and the TUI
degraded/blocked. On reconnect lazy_started=True skipped recreation, so the
client saw a blank screen.
Fix: drop the while-true wrapper. Now when the CLI exits, the exec pty
closes (EOF), the broadcast task ends, and the next client connection
detects the dead upstream via _upstream_alive() and recreates the exec —
with a terminal attached, so CPR is answered. A live exec is reused
(just a Ctrl-L redraw).
_LazyExecTelnetServer is extracted from a closure to module level so the
reconnect/recreate logic is unit-testable. Add 9 tests covering
_upstream_alive states and the recreate-on-death / reuse-if-live /
close-half-dead-writer / no-while-true behaviors.
Full Docker suite (120) passes.
25 tests covering:
- Docker.create_node factory: selects VendorDockerVM iff console_type ==
docker_exec, DockerVM otherwise (including telnet/ssh/vnc/http/none/spice)
- GNS3_* env parsing: SKIP_INIT, INTERFACE_NAMES, CONSOLE_CMD (single and
multiline), defaults
- create(): init.sh skipped under GNS3_SKIP_INIT, prepended otherwise;
GNS3_MAX_ETHERNET follows the interface rename; /etc/network mount dropped
under SKIP_INIT (and host skeleton dir removed) but kept without it
- _add_ubridge_connection: move_to_ns targets the renamed interface
(mgmt0) or falls back to eth{N}
- start(): docker_exec console dispatch + SKIP_INIT volume bridge + permission
fix; without SKIP_INIT the vendor passes are skipped
- _fix_permissions: skips dead/missing containers (no restart), targets
/gns3volumes bind-mount paths
- _setup_skip_init_volumes: runs the docker exec bridge script
- _cleanup_console_resources: closes the exec pty writer
Full Docker suite (111) and compute suite (395) pass — the four hook
extractions in DockerVM introduce no regressions.