Bjo53 commited on
Commit
afb5f98
·
verified ·
1 Parent(s): c07dd5a

Update app.py for persistence

Browse files
Files changed (1) hide show
  1. app.py +135 -56
app.py CHANGED
@@ -6,14 +6,23 @@ import fcntl
6
  import termios
7
  import json
8
  import subprocess
 
 
9
 
10
  from fastapi import FastAPI, WebSocket, WebSocketDisconnect
11
- from fastapi.responses import FileResponse
12
  from fastapi.staticfiles import StaticFiles
13
 
14
  app = FastAPI()
15
  app.mount("/static", StaticFiles(directory="static"), name="static")
16
 
 
 
 
 
 
 
 
17
  @app.get("/")
18
  async def index():
19
  return FileResponse("static/index.html")
@@ -22,74 +31,144 @@ async def index():
22
  async def get_pass():
23
  return {"pass": os.environ.get("TERMINAL_PASS", "")}
24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  @app.websocket("/ws")
26
  async def terminal(websocket: WebSocket):
27
  await websocket.accept()
28
 
29
- master_fd, slave_fd = pty.openpty()
 
30
 
31
- proc = await asyncio.create_subprocess_exec(
32
- "/bin/bash",
33
- stdin=slave_fd,
34
- stdout=slave_fd,
35
- stderr=slave_fd,
36
- env={
37
- **os.environ,
38
- "TERM": "xterm-256color",
39
- "HOME": "/persistent",
40
- "USER": "ubuntu"
41
- }
42
- )
43
 
44
- os.close(slave_fd)
 
 
 
 
 
 
 
45
 
46
- loop = asyncio.get_event_loop()
 
 
 
47
 
48
- async def read_pty():
49
- while True:
50
- try:
51
- data = await loop.run_in_executor(
52
- None,
53
- lambda: os.read(master_fd, 4096)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  )
55
- if data:
56
- try:
57
- text = data.decode("utf-8")
58
- except UnicodeDecodeError:
59
- text = data.decode("utf-8", errors="ignore")
60
- await websocket.send_text(text)
61
- except OSError:
62
- break
63
 
64
- read_task = asyncio.create_task(read_pty())
 
 
 
 
65
 
 
 
66
  try:
67
  while True:
68
- message = await websocket.receive_text()
69
  try:
70
- msg = json.loads(message)
71
- if msg.get("type") == "input":
72
- os.write(master_fd, msg["data"].encode("utf-8"))
73
- elif msg.get("type") == "resize":
74
- cols = msg.get("cols", 80)
75
- rows = msg.get("rows", 24)
76
- fcntl.ioctl(
77
- master_fd,
78
- termios.TIOCSWINSZ,
79
- struct.pack("HHHH", rows, cols, 0, 0)
80
- )
81
- except (json.JSONDecodeError, OSError):
 
 
 
 
 
 
 
 
 
82
  break
83
- except (WebSocketDisconnect, Exception):
84
  pass
85
- finally:
86
- read_task.cancel()
87
- try:
88
- proc.terminate()
89
- await asyncio.wait_for(proc.wait(), timeout=2)
90
- except Exception:
91
- proc.kill()
92
- try:
93
- os.close(master_fd)
94
- except:
95
- pass
 
6
  import termios
7
  import json
8
  import subprocess
9
+ import uuid
10
+ from pathlib import Path
11
 
12
  from fastapi import FastAPI, WebSocket, WebSocketDisconnect
13
+ from fastapi.responses import FileResponse, JSONResponse
14
  from fastapi.staticfiles import StaticFiles
15
 
16
  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():
28
  return FileResponse("static/index.html")
 
31
  async def get_pass():
32
  return {"pass": os.environ.get("TERMINAL_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):
53
  await websocket.accept()
54
 
55
+ session_id = None
56
+ read_task = None
57
 
58
+ try:
59
+ while True:
60
+ message = await websocket.receive_text()
61
+ msg = json.loads(message)
 
 
 
 
 
 
 
 
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