Open a shell in a running LXC container (ssh + pct enter) #2

Merged
Duckboot merged 3 commits from feat/lxc-shell into main 2026-11-02 20:44:00 +01:00
3 changed files with 26 additions and 0 deletions
Showing only changes of commit cccdaf7209 - Show all commits

No files matched your search

+4
View File
@@ -210,6 +210,10 @@ root's `authorized_keys` (verified: `ssh -o BatchMode=yes root@mainframe pct sta
- Exit 255 (ssh itself failed) -> error notify naming the guest and the code. Other - Exit 255 (ssh itself failed) -> error notify naming the guest and the code. Other
exit codes are the container shell's last status (`exit` after a failed command exit codes are the container shell's last status (`exit` after a failed command
gives 1) and are not errors. gives 1) and are not errors.
- Inside `suspend()`, right before ssh, the terminal gets a full reset (`ESC c`, what
`reset` sends) so the container shell starts on a clean screen without leftover TUI
terminal modes (user request). Done locally, not in the container: no dependency on
`reset` existing there, and `pct enter` stays unchanged.
- `OSError` from `subprocess.run` is caught *inside* the `suspend()` block: Textual - `OSError` from `subprocess.run` is caught *inside* the `suspend()` block: Textual
8.2.8's `suspend()` has no try/finally, so an exception escaping it leaves the app 8.2.8's `suspend()` has no try/finally, so an exception escaping it leaves the app
suspended. suspended.
+8
View File
@@ -1,6 +1,7 @@
"""Textual app for managing Proxmox guests through cv4pve-cli.""" """Textual app for managing Proxmox guests through cv4pve-cli."""
import subprocess import subprocess
import sys
from textual import work from textual import work
from textual.app import App, ComposeResult, SuspendNotSupported from textual.app import App, ComposeResult, SuspendNotSupported
@@ -358,6 +359,7 @@ class GuestApp(App):
try: try:
with self.suspend(): with self.suspend():
try: # inside: Textual's suspend() skips the resume if its body raises try: # inside: Textual's suspend() skips the resume if its body raises
_reset_terminal()
code = subprocess.run(cmd, check=False).returncode code = subprocess.run(cmd, check=False).returncode
except OSError as exc: except OSError as exc:
error = _message(exc) error = _message(exc)
@@ -426,6 +428,12 @@ class GuestApp(App):
self._start_job(guest, "edit", job) self._start_job(guest, "edit", job)
def _reset_terminal() -> None:
"""Full terminal reset (ESC c, what `reset` sends): clean screen, no leftover TUI modes."""
sys.stdout.write("\033c")
sys.stdout.flush()
def _scaled(value, unit: int): def _scaled(value, unit: int):
return "-" if value is None else round(value / unit) return "-" if value is None else round(value / unit)
+14
View File
@@ -907,3 +907,17 @@ async def test_table_reloads_after_shell(fake, monkeypatch):
await press_t(app, pilot) await press_t(app, pilot)
await settle(app, pilot) await settle(app, pilot)
assert fake.list_calls > before assert fake.list_calls > before
async def test_terminal_is_reset_inside_suspend_before_ssh(fake, monkeypatch):
# user request: a clean screen (and no leftover TUI terminal modes) in the container shell
fake.guests = [RUNNING_CT]
app = make_app()
events = patch_shell(monkeypatch, app)
resets = []
monkeypatch.setattr(
"cv4pve_tui.app._reset_terminal", lambda: resets.append(list(events))
)
async with app.run_test() as pilot:
await press_t(app, pilot)
assert resets == [[("enter", None)]] # inside suspend, before ssh ran