diff --git a/AppImage/components/lxc-terminal-modal.tsx b/AppImage/components/lxc-terminal-modal.tsx index fa718f17..cc5d33e2 100644 --- a/AppImage/components/lxc-terminal-modal.tsx +++ b/AppImage/components/lxc-terminal-modal.tsx @@ -24,9 +24,9 @@ interface LxcTerminalModalProps { vmName: string } -function getWebSocketUrl(): string { +function getWebSocketUrl(vmid: number): string { if (typeof window === "undefined") { - return "ws://localhost:8008/ws/terminal" + return `ws://localhost:8008/ws/terminal/lxc/${vmid}` } const { protocol, hostname, port } = window.location @@ -34,9 +34,9 @@ function getWebSocketUrl(): string { const wsProtocol = protocol === "https:" ? "wss:" : "ws:" if (isStandardPort) { - return `${wsProtocol}//${hostname}/ws/terminal` + return `${wsProtocol}//${hostname}/ws/terminal/lxc/${vmid}` } else { - return `${wsProtocol}//${hostname}:${API_PORT}/ws/terminal` + return `${wsProtocol}//${hostname}:${API_PORT}/ws/terminal/lxc/${vmid}` } } @@ -98,15 +98,11 @@ export function LxcTerminalModal({ // Small delay to ensure Dialog content is rendered const initTimeout = setTimeout(() => { - if (!terminalContainerRef.current) { - console.log("[v0] Terminal container not ready") - return - } + if (!terminalContainerRef.current) return initTerminal() }, 100) const initTerminal = async () => { - console.log("[v0] Initializing LXC terminal for vmid:", vmid) const [TerminalClass, FitAddonClass] = await Promise.all([ import("xterm").then((mod) => mod.Terminal), import("xterm-addon-fit").then((mod) => mod.FitAddon), @@ -160,14 +156,12 @@ export function LxcTerminalModal({ termRef.current = term fitAddonRef.current = fitAddon - // Connect WebSocket - const wsUrl = getWebSocketUrl() - console.log("[v0] LXC Terminal connecting to:", wsUrl) + // Connect WebSocket - direct connection to LXC container + const wsUrl = getWebSocketUrl(vmid) const ws = new WebSocket(wsUrl) wsRef.current = ws ws.onopen = () => { - console.log("[v0] LXC Terminal WebSocket connected") setConnectionStatus("online") // Start heartbeat ping @@ -181,22 +175,13 @@ export function LxcTerminalModal({ } }, 25000) - // Sync terminal size first + // Sync terminal size fitAddon.fit() ws.send(JSON.stringify({ type: "resize", cols: term.cols, rows: term.rows, })) - - // Auto-execute pct enter command after terminal is ready - // Wait for shell prompt to appear before sending command - setTimeout(() => { - if (ws.readyState === WebSocket.OPEN) { - // Send the command as plain text like user would type - ws.send(`pct enter ${vmid}\r`) - } - }, 800) } ws.onmessage = (event) => { @@ -207,14 +192,12 @@ export function LxcTerminalModal({ term.write(event.data) } - ws.onerror = (error) => { - console.log("[v0] LXC Terminal WebSocket error:", error) + ws.onerror = () => { setConnectionStatus("offline") term.writeln("\r\n\x1b[31m[ERROR] WebSocket connection error\x1b[0m") } - ws.onclose = (event) => { - console.log("[v0] LXC Terminal WebSocket closed:", event.code, event.reason) + ws.onclose = () => { setConnectionStatus("offline") if (pingIntervalRef.current) { clearInterval(pingIntervalRef.current) @@ -397,13 +380,13 @@ export function LxcTerminalModal({ {/* Mobile/Tablet control buttons */} {showMobileControls && ( -
-
+
+
@@ -411,7 +394,7 @@ export function LxcTerminalModal({ variant="outline" size="sm" onClick={sendTab} - className="h-9 px-3 bg-zinc-800 border-zinc-700 text-zinc-300" + className="h-8 px-2 text-xs bg-zinc-800 border-zinc-700 text-zinc-300" > TAB @@ -419,7 +402,7 @@ export function LxcTerminalModal({ variant="outline" size="sm" onClick={sendArrowUp} - className="h-9 w-9 p-0 bg-zinc-800 border-zinc-700" + className="h-8 w-8 p-0 bg-zinc-800 border-zinc-700" > @@ -427,7 +410,7 @@ export function LxcTerminalModal({ variant="outline" size="sm" onClick={sendArrowDown} - className="h-9 w-9 p-0 bg-zinc-800 border-zinc-700" + className="h-8 w-8 p-0 bg-zinc-800 border-zinc-700" > @@ -435,7 +418,7 @@ export function LxcTerminalModal({ variant="outline" size="sm" onClick={sendArrowLeft} - className="h-9 w-9 p-0 bg-zinc-800 border-zinc-700" + className="h-8 w-8 p-0 bg-zinc-800 border-zinc-700" > @@ -443,7 +426,7 @@ export function LxcTerminalModal({ variant="outline" size="sm" onClick={sendArrowRight} - className="h-9 w-9 p-0 bg-zinc-800 border-zinc-700" + className="h-8 w-8 p-0 bg-zinc-800 border-zinc-700" > @@ -451,7 +434,7 @@ export function LxcTerminalModal({ variant="outline" size="sm" onClick={sendEnter} - className="h-9 px-3 bg-blue-600/20 border-blue-600/50 text-blue-400 hover:bg-blue-600/30" + className="h-8 px-2 text-xs bg-blue-600/20 border-blue-600/50 text-blue-400 hover:bg-blue-600/30" > Enter @@ -460,7 +443,7 @@ export function LxcTerminalModal({ variant="outline" size="sm" onClick={handleCtrlPress} - className={`h-9 px-3 ${ctrlPressed + className={`h-8 px-2 text-xs ${ctrlPressed ? "bg-yellow-600/30 border-yellow-600/50 text-yellow-400" : "bg-zinc-800 border-zinc-700 text-zinc-300"}`} > diff --git a/AppImage/scripts/flask_terminal_routes.py b/AppImage/scripts/flask_terminal_routes.py index bc5f5a2c..56500e62 100644 --- a/AppImage/scripts/flask_terminal_routes.py +++ b/AppImage/scripts/flask_terminal_routes.py @@ -250,6 +250,123 @@ 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 + shell_process = subprocess.Popen( + ['/usr/bin/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"""