From d2c73627360cad00e797a64e5007975c8e12ef35 Mon Sep 17 00:00:00 2001 From: MacRimi Date: Sat, 31 Jan 2026 18:10:55 +0100 Subject: [PATCH] Update terminal modal --- AppImage/components/lxc-terminal-modal.tsx | 75 ++++++++++--- AppImage/scripts/flask_terminal_routes.py | 118 --------------------- 2 files changed, 60 insertions(+), 133 deletions(-) diff --git a/AppImage/components/lxc-terminal-modal.tsx b/AppImage/components/lxc-terminal-modal.tsx index 469be12f..b68ae6d2 100644 --- a/AppImage/components/lxc-terminal-modal.tsx +++ b/AppImage/components/lxc-terminal-modal.tsx @@ -12,7 +12,6 @@ import { ArrowRight, CornerDownLeft, GripHorizontal, - X, } from "lucide-react" import "xterm/css/xterm.css" import { API_PORT } from "@/lib/api-config" @@ -24,9 +23,9 @@ interface LxcTerminalModalProps { vmName: string } -function getWebSocketUrl(vmid: number): string { +function getWebSocketUrl(): string { if (typeof window === "undefined") { - return `ws://localhost:8008/ws/terminal/lxc/${vmid}` + return "ws://localhost:8008/ws/terminal" } const { protocol, hostname, port } = window.location @@ -34,9 +33,9 @@ function getWebSocketUrl(vmid: number): string { const wsProtocol = protocol === "https:" ? "wss:" : "ws:" if (isStandardPort) { - return `${wsProtocol}//${hostname}/ws/terminal/lxc/${vmid}` + return `${wsProtocol}//${hostname}/ws/terminal` } else { - return `${wsProtocol}//${hostname}:${API_PORT}/ws/terminal/lxc/${vmid}` + return `${wsProtocol}//${hostname}:${API_PORT}/ws/terminal` } } @@ -55,12 +54,21 @@ export function LxcTerminalModal({ const [connectionStatus, setConnectionStatus] = useState<"connecting" | "online" | "offline">("connecting") const [isMobile, setIsMobile] = useState(false) const [isTablet, setIsTablet] = useState(false) + const isInsideLxcRef = useRef(false) + const outputBufferRef = useRef("") const [modalHeight, setModalHeight] = useState(500) const [isResizing, setIsResizing] = useState(false) const resizeBarRef = useRef(null) const modalHeightRef = useRef(500) + const [showLoginModal, setShowLoginModal] = useState(false) + const [loginUsername, setLoginUsername] = useState("") + const [loginPassword, setLoginPassword] = useState("") + const [loginError, setLoginError] = useState("") + const [isLoggingIn, setIsLoggingIn] = useState(false) + const waitingForPasswordRef = useRef(false) + // Detect mobile/tablet useEffect(() => { const checkDevice = () => { @@ -89,6 +97,8 @@ export function LxcTerminalModal({ termRef.current = null } setConnectionStatus("connecting") + isInsideLxcRef.current = false + outputBufferRef.current = "" } }, [isOpen]) @@ -156,10 +166,14 @@ export function LxcTerminalModal({ termRef.current = term fitAddonRef.current = fitAddon - // Connect WebSocket - direct connection to LXC container - const wsUrl = getWebSocketUrl(vmid) + // Connect WebSocket to host terminal + const wsUrl = getWebSocketUrl() const ws = new WebSocket(wsUrl) wsRef.current = ws + + // Reset state for new connection + isInsideLxcRef.current = false + outputBufferRef.current = "" ws.onopen = () => { setConnectionStatus("online") @@ -182,14 +196,13 @@ export function LxcTerminalModal({ cols: term.cols, rows: term.rows, })) - } - - ws.onmessage = (event) => { - // Filter out pong responses - if (event.data === '{"type": "pong"}' || event.data === '{"type":"pong"}') { - return - } - term.write(event.data) + + // Auto-execute pct enter after connection is ready + setTimeout(() => { + if (ws.readyState === WebSocket.OPEN) { + ws.send(`pct enter ${vmid}\r`) + } + }, 300) } ws.onerror = () => { @@ -210,6 +223,38 @@ export function LxcTerminalModal({ ws.send(data) } }) + + ws.onmessage = (event) => { + // Filter out pong responses + if (event.data === '{"type": "pong"}' || event.data === '{"type":"pong"}') { + return + } + + // Buffer output until we detect we're inside the LXC + // pct enter always enters directly without login prompt when run as root + if (!isInsideLxcRef.current) { + outputBufferRef.current += event.data + + // Detect when we're inside the LXC container + // Look for shell prompt pattern after pct enter command + const afterPctEnter = outputBufferRef.current.split(`pct enter ${vmid}`).pop() || "" + if (afterPctEnter.includes("@") && (afterPctEnter.includes("$") || afterPctEnter.includes("#") || afterPctEnter.includes(":~") || afterPctEnter.includes(":/#"))) { + // Successfully inside LXC + isInsideLxcRef.current = true + + // Extract content after pct enter (skip command echo line) + const newlineIndex = afterPctEnter.indexOf('\n') + if (newlineIndex !== -1) { + const lxcContent = afterPctEnter.substring(newlineIndex + 1) + term.write(lxcContent) + } + return + } + } else { + // Already inside LXC, write directly + term.write(event.data) + } + } } return () => { diff --git a/AppImage/scripts/flask_terminal_routes.py b/AppImage/scripts/flask_terminal_routes.py index b416d0e7..bc5f5a2c 100644 --- a/AppImage/scripts/flask_terminal_routes.py +++ b/AppImage/scripts/flask_terminal_routes.py @@ -250,124 +250,6 @@ def terminal_websocket(ws): if session_id in active_sessions: del active_sessions[session_id] -@sock.route('/ws/terminal/lxc/') -def lxc_terminal_websocket(ws, vmid): - """WebSocket endpoint for direct LXC container terminal sessions""" - - # Create pseudo-terminal - master_fd, slave_fd = pty.openpty() - - # Start pct enter directly to connect to the LXC container - # pct is located in /usr/sbin on Proxmox VE - shell_process = subprocess.Popen( - ['/usr/sbin/pct', 'enter', str(vmid)], - stdin=slave_fd, - stdout=slave_fd, - stderr=slave_fd, - preexec_fn=os.setsid, - cwd='/', - env=dict(os.environ, TERM='xterm-256color') - ) - - session_id = id(ws) - active_sessions[session_id] = { - 'process': shell_process, - 'master_fd': master_fd, - 'vmid': vmid - } - - # Set non-blocking mode for master_fd - flags = fcntl.fcntl(master_fd, fcntl.F_GETFL) - fcntl.fcntl(master_fd, fcntl.F_SETFL, flags | os.O_NONBLOCK) - - # Set initial terminal size - set_winsize(master_fd, 30, 120) - - # Start thread to read PTY output and forward to WebSocket - output_thread = threading.Thread( - target=read_and_forward_output, - args=(master_fd, ws), - daemon=True - ) - output_thread.start() - - try: - while True: - data = ws.receive(timeout=None) - - if data is None: - break - - handled = False - - if isinstance(data, str): - try: - msg = json.loads(data) - except Exception: - msg = None - - if isinstance(msg, dict): - msg_type = msg.get('type') - - if msg_type == 'ping': - try: - ws.send(json.dumps({'type': 'pong'})) - except: - pass - handled = True - - elif msg_type == 'resize': - cols = int(msg.get('cols', 120)) - rows = int(msg.get('rows', 30)) - set_winsize(master_fd, rows, cols) - handled = True - - if handled: - continue - - if isinstance(data, str) and data.startswith('\x1b[8;'): - try: - parts = data[4:-1].split(';') - rows, cols = int(parts[0]), int(parts[1]) - set_winsize(master_fd, rows, cols) - continue - except Exception: - pass - - try: - os.write(master_fd, data.encode('utf-8')) - except OSError as e: - print(f"Error writing to LXC PTY: {e}") - break - - if shell_process.poll() is not None: - break - - except Exception as e: - print(f"LXC terminal session error: {e}") - finally: - try: - shell_process.terminate() - shell_process.wait(timeout=1) - except: - try: - shell_process.kill() - except: - pass - - try: - os.close(master_fd) - except: - pass - - try: - os.close(slave_fd) - except: - pass - - if session_id in active_sessions: - del active_sessions[session_id] - @sock.route('/ws/script/') def script_websocket(ws, session_id): """WebSocket endpoint for executing scripts with hybrid web mode"""