import os import json import struct import signal import select import threading def _pty_set_winsize(fd, rows, cols): import fcntl import termios winsz = struct.pack("HHHH", int(rows), int(cols), 0, 0) fcntl.ioctl(fd, termios.TIOCSWINSZ, winsz) def terminal_session(ws): """PTY + bash, обмен через WebSocket (как ttyd, но на том же порту что и Flask).""" import pty pid, master_fd = pty.fork() if pid == 0: os.environ.setdefault("TERM", "xterm-256color") os.environ.setdefault("SHELL", "/bin/bash") try: os.execvp("/bin/bash", ["/bin/bash"]) except OSError: os.execvp("/bin/sh", ["/bin/sh"]) os._exit(127) relay_done = threading.Event() ws_lock = threading.Lock() def relay_out(): try: while not relay_done.is_set(): r, _, _ = select.select([master_fd], [], [], 0.25) if master_fd not in r: continue try: chunk = os.read(master_fd, 65536) except OSError: break if not chunk: break try: with ws_lock: ws.send(chunk) except Exception: break finally: relay_done.set() t = threading.Thread(target=relay_out, daemon=True) t.start() try: while True: msg = ws.receive() if msg is None: break if isinstance(msg, str) and msg.startswith("{"): try: j = json.loads(msg) if j.get("type") == "resize": r, c = int(j.get("rows") or 24), int(j.get("cols") or 80) if r > 0 and c > 0: _pty_set_winsize(master_fd, r, c) continue except (json.JSONDecodeError, TypeError, ValueError, KeyError): pass if isinstance(msg, str): msg = msg.encode("utf-8", errors="replace") try: os.write(master_fd, msg) except OSError: break finally: relay_done.set() try: os.close(master_fd) except OSError: pass try: os.kill(pid, signal.SIGTERM) except ProcessLookupError: pass try: os.waitpid(pid, 0) except ChildProcessError: pass t.join(timeout=1.5)