Spaces:
Sleeping
Sleeping
File size: 5,775 Bytes
6124680 c07dd5a 6124680 c07dd5a 6124680 b51b393 afb5f98 6124680 afb5f98 6124680 afb5f98 fdcb061 afb5f98 fdcb061 afb5f98 6124680 7b11cb5 afb5f98 fdcb061 afb5f98 6124680 c07dd5a afb5f98 fdcb061 6124680 afb5f98 6124680 afb5f98 c07dd5a fdcb061 afb5f98 fdcb061 afb5f98 fdcb061 afb5f98 fdcb061 afb5f98 fdcb061 afb5f98 fdcb061 afb5f98 fdcb061 6124680 afb5f98 fdcb061 6124680 fdcb061 6124680 fdcb061 6124680 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 | import os
import pty
import asyncio
import struct
import fcntl
import termios
import json
import subprocess
import uuid
from pathlib import Path
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")
PERSISTENT_DIR = Path("/persistent")
PKGS_FILE = PERSISTENT_DIR / "installed_packages.txt"
PERSISTENT_DIR.mkdir(parents=True, exist_ok=True)
# In-memory session storage - ptys stay alive even after WebSocket disconnects
sessions = {}
@app.get("/")
async def index():
return FileResponse("static/index.html")
@app.get("/api/pass")
async def get_pass():
return {"pass": os.environ.get("TERMINAL_PASS", "")}
@app.get("/api/sessions")
async def list_sessions():
alive = []
for sid, s in sessions.items():
if s.get("proc") and s["proc"].returncode is None:
alive.append({"id": sid, "alive": True})
return JSONResponse(alive)
@app.websocket("/ws")
async def terminal(websocket: WebSocket):
await websocket.accept()
session_id = None
read_task = None
master_fd = None
proc = None
try:
while True:
message = await websocket.receive_text()
msg = json.loads(message)
if msg.get("type") == "attach":
session_id = msg.get("session_id")
# Reuse existing session if alive
if session_id and session_id in sessions:
s = sessions[session_id]
if s.get("proc") and s["proc"].returncode is None:
master_fd = s["master_fd"]
proc = s["proc"]
await websocket.send_text(json.dumps({
"type": "session_info",
"session_id": session_id
}))
# Send current terminal state
try:
output = s.get("last_output", "")
if output:
await websocket.send_text(json.dumps({
"type": "output",
"data": output[-4000:]
}))
except:
pass
if read_task:
read_task.cancel()
read_task = asyncio.create_task(read_pty(websocket, master_fd, session_id))
continue
# Create new session
session_id = session_id or str(uuid.uuid4())[:8]
master_fd, slave_fd = pty.openpty()
env = {
**os.environ,
"TERM": "xterm-256color",
"HOME": "/persistent",
"USER": "ubuntu",
"SHELL": "/bin/bash",
}
proc = await asyncio.create_subprocess_exec(
"/bin/bash", "--login",
stdin=slave_fd, stdout=slave_fd, stderr=slave_fd,
env=env
)
os.close(slave_fd)
sessions[session_id] = {
"master_fd": master_fd,
"proc": proc,
"last_output": ""
}
await websocket.send_text(json.dumps({
"type": "session_info",
"session_id": session_id
}))
if read_task:
read_task.cancel()
read_task = asyncio.create_task(read_pty(websocket, master_fd, session_id))
elif msg.get("type") == "input":
if master_fd is not None:
try:
os.write(master_fd, msg["data"].encode("utf-8"))
except OSError:
pass
elif msg.get("type") == "resize":
if master_fd is not None:
try:
fcntl.ioctl(
master_fd, termios.TIOCSWINSZ,
struct.pack("HHHH", msg.get("rows", 24), msg.get("cols", 80), 0, 0)
)
except OSError:
pass
except (WebSocketDisconnect, Exception):
pass
finally:
if read_task:
read_task.cancel()
# DON'T kill the process - keep it alive for reconnection
# Just clean up the WebSocket reference
async def read_pty(websocket: WebSocket, master_fd: int, session_id: str):
loop = asyncio.get_event_loop()
try:
while True:
data = await loop.run_in_executor(None, lambda: os.read(master_fd, 4096))
if data:
try:
text = data.decode("utf-8")
except UnicodeDecodeError:
text = data.decode("utf-8", errors="ignore")
# Cache last output for reconnect
if session_id in sessions:
sessions[session_id]["last_output"] = sessions[session_id].get("last_output", "") + text
# Keep only last 8000 chars
if len(sessions[session_id]["last_output"]) > 8000:
sessions[session_id]["last_output"] = sessions[session_id]["last_output"][-4000:]
try:
await websocket.send_text(json.dumps({"type": "output", "data": text}))
except:
break
except (OSError, asyncio.CancelledError):
pass
|