Bjo53 commited on
Commit
fdcb061
·
verified ·
1 Parent(s): b05ee9d

Fix: restore original pty speed + keep sessions alive

Browse files
Files changed (1) hide show
  1. app.py +99 -107
app.py CHANGED
@@ -17,11 +17,11 @@ app = FastAPI()
17
  app.mount("/static", StaticFiles(directory="static"), name="static")
18
 
19
  PERSISTENT_DIR = Path("/persistent")
20
- SESSION_DIR = PERSISTENT_DIR / "sessions"
21
  PKGS_FILE = PERSISTENT_DIR / "installed_packages.txt"
22
- SESSION_STATE = PERSISTENT_DIR / "session_state.json"
23
 
24
- SESSION_DIR.mkdir(parents=True, exist_ok=True)
 
25
 
26
  @app.get("/")
27
  async def index():
@@ -33,20 +33,11 @@ async def get_pass():
33
 
34
  @app.get("/api/sessions")
35
  async def list_sessions():
36
- sessions = []
37
- for f in SESSION_DIR.glob("*.json"):
38
- try:
39
- data = json.loads(f.read_text())
40
- sid = data.get("id", f.stem)
41
- alive = subprocess.run(
42
- ["screen", "-ls", f"term_{sid}"],
43
- capture_output=True
44
- ).returncode == 0
45
- data["alive"] = alive
46
- sessions.append(data)
47
- except:
48
- pass
49
- return JSONResponse(sessions)
50
 
51
  @app.websocket("/ws")
52
  async def terminal(websocket: WebSocket):
@@ -54,6 +45,8 @@ async def terminal(websocket: WebSocket):
54
 
55
  session_id = None
56
  read_task = None
 
 
57
 
58
  try:
59
  while True:
@@ -62,113 +55,112 @@ async def terminal(websocket: WebSocket):
62
 
63
  if msg.get("type") == "attach":
64
  session_id = msg.get("session_id")
65
- if not session_id or not (SESSION_DIR / f"{session_id}.json").exists():
66
- session_id = str(uuid.uuid4())[:8]
67
- (SESSION_DIR / f"{session_id}.json").write_text(json.dumps({
68
- "id": session_id,
69
- "created": str(subprocess.getoutput("date -Iseconds")),
70
- }))
71
 
72
- await websocket.send_text(json.dumps({
73
- "type": "session_info",
74
- "session_id": session_id
75
- }))
76
-
77
- alive = subprocess.run(
78
- ["screen", "-ls", f"term_{session_id}"],
79
- capture_output=True
80
- ).returncode == 0
81
-
82
- if not alive:
83
- subprocess.Popen(
84
- ["screen", "-dmS", f"term_{session_id}", "-L", "-Logfile",
85
- f"/persistent/sessions/{session_id}.log", "/bin/bash", "--login"],
86
- stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
87
- )
88
- await asyncio.sleep(0.3)
89
-
90
- state_file = PERSISTENT_DIR / "session_state.json"
91
- if state_file.exists():
92
  try:
93
- state = json.loads(state_file.read_text())
94
- cwd = state.get("cwd", "/persistent")
95
- subprocess.run(
96
- ["screen", "-S", f"term_{session_id}", "-X", "stuff",
97
- f"cd {cwd}\nclear\n"],
98
- capture_output=True, timeout=3
99
- )
100
  except:
101
  pass
102
-
103
- subprocess.run(
104
- ["screen", "-S", f"term_{session_id}", "-X", "stuff", " \n"],
105
- capture_output=True, timeout=3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  )
 
 
 
 
 
 
 
 
 
 
 
 
107
 
108
  if read_task:
109
  read_task.cancel()
110
- read_task = asyncio.create_task(read_screen(websocket, session_id))
111
- continue
112
-
113
- if msg.get("type") == "input" and session_id:
114
- data = msg["data"]
115
- subprocess.run(
116
- ["screen", "-S", f"term_{session_id}", "-X", "stuff", data],
117
- capture_output=True, timeout=5
118
- )
119
 
120
- elif msg.get("type") == "resize" and session_id:
121
- cols = msg.get("cols", 200)
122
- rows = msg.get("rows", 50)
123
- subprocess.run(
124
- ["screen", "-S", f"term_{session_id}", "-X", "width", "cols", str(cols)],
125
- capture_output=True, timeout=3
126
- )
127
- subprocess.run(
128
- ["screen", "-S", f"term_{session_id}", "-X", "width", "lines", str(rows)],
129
- capture_output=True, timeout=3
130
- )
131
 
132
- elif msg.get("type") == "key" and session_id:
133
- key = msg.get("key", "")
134
- subprocess.run(
135
- ["screen", "-S", f"term_{session_id}", "-X", "stuff", key],
136
- capture_output=True, timeout=3
137
- )
 
 
 
138
 
139
  except (WebSocketDisconnect, Exception):
140
  pass
141
  finally:
142
  if read_task:
143
  read_task.cancel()
 
 
144
 
145
- async def read_screen(websocket: WebSocket, session_id: str):
146
- last_content = ""
147
  try:
148
  while True:
149
- await asyncio.sleep(0.05)
150
- try:
151
- result = subprocess.run(
152
- ["screen", "-S", f"term_{session_id}", "-X", "hardcopy", "-h", "/tmp/screen_out.txt"],
153
- capture_output=True, timeout=3
154
- )
155
- if result.returncode == 0:
156
- try:
157
- content = Path("/tmp/screen_out.txt").read_text(errors="ignore")
158
- if content != last_content:
159
- if len(content) > len(last_content):
160
- new_data = content[len(last_content):]
161
- else:
162
- new_data = content
163
- last_content = content
164
- if new_data:
165
- await websocket.send_text(json.dumps({
166
- "type": "output",
167
- "data": new_data
168
- }))
169
- except:
170
- pass
171
- except:
172
- break
173
- except asyncio.CancelledError:
174
  pass
 
17
  app.mount("/static", StaticFiles(directory="static"), name="static")
18
 
19
  PERSISTENT_DIR = Path("/persistent")
 
20
  PKGS_FILE = PERSISTENT_DIR / "installed_packages.txt"
21
+ PERSISTENT_DIR.mkdir(parents=True, exist_ok=True)
22
 
23
+ # In-memory session storage - ptys stay alive even after WebSocket disconnects
24
+ sessions = {}
25
 
26
  @app.get("/")
27
  async def index():
 
33
 
34
  @app.get("/api/sessions")
35
  async def list_sessions():
36
+ alive = []
37
+ for sid, s in sessions.items():
38
+ if s.get("proc") and s["proc"].returncode is None:
39
+ alive.append({"id": sid, "alive": True})
40
+ return JSONResponse(alive)
 
 
 
 
 
 
 
 
 
41
 
42
  @app.websocket("/ws")
43
  async def terminal(websocket: WebSocket):
 
45
 
46
  session_id = None
47
  read_task = None
48
+ master_fd = None
49
+ proc = None
50
 
51
  try:
52
  while True:
 
55
 
56
  if msg.get("type") == "attach":
57
  session_id = msg.get("session_id")
 
 
 
 
 
 
58
 
59
+ # Reuse existing session if alive
60
+ if session_id and session_id in sessions:
61
+ s = sessions[session_id]
62
+ if s.get("proc") and s["proc"].returncode is None:
63
+ master_fd = s["master_fd"]
64
+ proc = s["proc"]
65
+ await websocket.send_text(json.dumps({
66
+ "type": "session_info",
67
+ "session_id": session_id
68
+ }))
69
+ # Send current terminal state
 
 
 
 
 
 
 
 
 
70
  try:
71
+ output = s.get("last_output", "")
72
+ if output:
73
+ await websocket.send_text(json.dumps({
74
+ "type": "output",
75
+ "data": output[-4000:]
76
+ }))
 
77
  except:
78
  pass
79
+ if read_task:
80
+ read_task.cancel()
81
+ read_task = asyncio.create_task(read_pty(websocket, master_fd, session_id))
82
+ continue
83
+
84
+ # Create new session
85
+ session_id = session_id or str(uuid.uuid4())[:8]
86
+ master_fd, slave_fd = pty.openpty()
87
+
88
+ env = {
89
+ **os.environ,
90
+ "TERM": "xterm-256color",
91
+ "HOME": "/persistent",
92
+ "USER": "ubuntu",
93
+ "SHELL": "/bin/bash",
94
+ }
95
+
96
+ proc = await asyncio.create_subprocess_exec(
97
+ "/bin/bash", "--login",
98
+ stdin=slave_fd, stdout=slave_fd, stderr=slave_fd,
99
+ env=env
100
  )
101
+ os.close(slave_fd)
102
+
103
+ sessions[session_id] = {
104
+ "master_fd": master_fd,
105
+ "proc": proc,
106
+ "last_output": ""
107
+ }
108
+
109
+ await websocket.send_text(json.dumps({
110
+ "type": "session_info",
111
+ "session_id": session_id
112
+ }))
113
 
114
  if read_task:
115
  read_task.cancel()
116
+ read_task = asyncio.create_task(read_pty(websocket, master_fd, session_id))
 
 
 
 
 
 
 
 
117
 
118
+ elif msg.get("type") == "input":
119
+ if master_fd is not None:
120
+ try:
121
+ os.write(master_fd, msg["data"].encode("utf-8"))
122
+ except OSError:
123
+ pass
 
 
 
 
 
124
 
125
+ elif msg.get("type") == "resize":
126
+ if master_fd is not None:
127
+ try:
128
+ fcntl.ioctl(
129
+ master_fd, termios.TIOCSWINSZ,
130
+ struct.pack("HHHH", msg.get("rows", 24), msg.get("cols", 80), 0, 0)
131
+ )
132
+ except OSError:
133
+ pass
134
 
135
  except (WebSocketDisconnect, Exception):
136
  pass
137
  finally:
138
  if read_task:
139
  read_task.cancel()
140
+ # DON'T kill the process - keep it alive for reconnection
141
+ # Just clean up the WebSocket reference
142
 
143
+ async def read_pty(websocket: WebSocket, master_fd: int, session_id: str):
144
+ loop = asyncio.get_event_loop()
145
  try:
146
  while True:
147
+ data = await loop.run_in_executor(None, lambda: os.read(master_fd, 4096))
148
+ if data:
149
+ try:
150
+ text = data.decode("utf-8")
151
+ except UnicodeDecodeError:
152
+ text = data.decode("utf-8", errors="ignore")
153
+
154
+ # Cache last output for reconnect
155
+ if session_id in sessions:
156
+ sessions[session_id]["last_output"] = sessions[session_id].get("last_output", "") + text
157
+ # Keep only last 8000 chars
158
+ if len(sessions[session_id]["last_output"]) > 8000:
159
+ sessions[session_id]["last_output"] = sessions[session_id]["last_output"][-4000:]
160
+
161
+ try:
162
+ await websocket.send_text(json.dumps({"type": "output", "data": text}))
163
+ except:
164
+ break
165
+ except (OSError, asyncio.CancelledError):
 
 
 
 
 
 
166
  pass