"""YakPanel - Web Terminal API (WebSocket)""" import asyncio import os import sys from fastapi import APIRouter, WebSocket, WebSocketDisconnect router = APIRouter(prefix="/terminal", tags=["terminal"]) @router.websocket("/ws") async def terminal_websocket(websocket: WebSocket): """WebSocket terminal - spawns shell and streams I/O""" await websocket.accept() token = websocket.query_params.get("token") if token: from app.core.security import decode_token if not decode_token(token): await websocket.close(code=4001) return if sys.platform == "win32": proc = await asyncio.create_subprocess_shell( "cmd.exe", stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT, ) else: proc = await asyncio.create_subprocess_shell( "/bin/bash", stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT, env={**os.environ, "TERM": "xterm-256color"}, ) async def read_stdout(): try: while proc.returncode is None and proc.stdout: data = await proc.stdout.read(4096) if data: await websocket.send_text(data.decode("utf-8", errors="replace")) except (WebSocketDisconnect, ConnectionResetError): pass finally: try: proc.kill() except ProcessLookupError: pass async def read_websocket(): try: while True: msg = await websocket.receive() data = msg.get("text") or (msg.get("bytes") or b"").decode("utf-8", errors="replace") if data and proc.stdin and not proc.stdin.is_closing(): proc.stdin.write(data.encode("utf-8")) await proc.stdin.drain() except (WebSocketDisconnect, ConnectionResetError): pass finally: try: proc.kill() except ProcessLookupError: pass await asyncio.gather(read_stdout(), read_websocket())