diamond-in commited on
Commit
a84d02b
·
verified ·
1 Parent(s): 84fa162

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +26 -17
app.py CHANGED
@@ -7,30 +7,31 @@ import select
7
  import subprocess
8
  import fcntl
9
  import signal
 
 
10
 
11
  app = FastAPI()
12
 
13
- # Global Session State
14
- SESSION = {"fd": None, "process": None}
15
 
16
  class InputData(BaseModel):
17
  data: str
18
 
19
  @app.post("/start")
20
  def start_terminal():
21
- """Starts the root shell session"""
22
  # Kill existing
23
  if SESSION["process"]:
24
  try:
25
  os.kill(SESSION["process"].pid, signal.SIGTERM)
26
  except: pass
27
 
28
- # Create PTY
29
  master, slave = pty.openpty()
30
 
31
- # Start bash as root (sudo)
 
32
  p = subprocess.Popen(
33
- ["sudo", "bash"],
34
  stdin=slave,
35
  stdout=slave,
36
  stderr=slave,
@@ -39,41 +40,49 @@ def start_terminal():
39
  )
40
  os.close(slave)
41
 
42
- # Set non-blocking
43
  fl = fcntl.fcntl(master, fcntl.F_GETFL)
44
  fcntl.fcntl(master, fcntl.F_SETFL, fl | os.O_NONBLOCK)
45
 
46
  SESSION["fd"] = master
47
  SESSION["process"] = p
 
48
  return {"status": "started"}
49
 
50
- def stream_generator():
51
- """Yields data instantly as it appears in the terminal"""
52
  fd = SESSION["fd"]
53
  while True:
54
  if fd:
55
- # Wait for data (timeout 0.1s to prevent CPU 100%)
56
  r, _, _ = select.select([fd], [], [], 0.1)
57
  if fd in r:
58
  try:
59
- # Read chunk
60
- data = os.read(fd, 1024)
61
  if data:
 
62
  yield data
63
  except OSError:
64
  break
 
 
 
 
 
 
 
 
 
65
  else:
66
- # If no session, wait a bit and retry
67
- select.select([], [], [], 0.5)
68
 
69
  @app.get("/stream")
70
  def stream_output():
71
- """The magic endpoint: Keeps connection open and streams text"""
72
- return StreamingResponse(stream_generator(), media_type="text/plain")
73
 
74
  @app.post("/write")
75
  def write_input(payload: InputData):
76
- """Sends keystrokes to the terminal"""
77
  if SESSION["fd"]:
 
78
  os.write(SESSION["fd"], payload.data.encode())
79
  return {"status": "ok"}
 
7
  import subprocess
8
  import fcntl
9
  import signal
10
+ import asyncio
11
+ import time
12
 
13
  app = FastAPI()
14
 
15
+ # Global Session
16
+ SESSION = {"fd": None, "process": None, "last_activity": time.time()}
17
 
18
  class InputData(BaseModel):
19
  data: str
20
 
21
  @app.post("/start")
22
  def start_terminal():
 
23
  # Kill existing
24
  if SESSION["process"]:
25
  try:
26
  os.kill(SESSION["process"].pid, signal.SIGTERM)
27
  except: pass
28
 
 
29
  master, slave = pty.openpty()
30
 
31
+ # FIX 1: Add '-i' for Interactive mode (Fixes "no job control")
32
+ # FIX 2: Use setsid to detach correctly
33
  p = subprocess.Popen(
34
+ ["sudo", "bash", "-i"],
35
  stdin=slave,
36
  stdout=slave,
37
  stderr=slave,
 
40
  )
41
  os.close(slave)
42
 
43
+ # Non-blocking read
44
  fl = fcntl.fcntl(master, fcntl.F_GETFL)
45
  fcntl.fcntl(master, fcntl.F_SETFL, fl | os.O_NONBLOCK)
46
 
47
  SESSION["fd"] = master
48
  SESSION["process"] = p
49
+ SESSION["last_activity"] = time.time()
50
  return {"status": "started"}
51
 
52
+ async def stream_generator():
53
+ """Yields data. Sends a 'ping' if idle to prevent disconnects."""
54
  fd = SESSION["fd"]
55
  while True:
56
  if fd:
57
+ # Check for data
58
  r, _, _ = select.select([fd], [], [], 0.1)
59
  if fd in r:
60
  try:
61
+ data = os.read(fd, 4096)
 
62
  if data:
63
+ SESSION["last_activity"] = time.time()
64
  yield data
65
  except OSError:
66
  break
67
+ else:
68
+ # FIX 3: Heartbeat
69
+ # If no data for 10 seconds, yield a null byte to keep HTTP open
70
+ # The client will ignore this byte.
71
+ if time.time() - SESSION["last_activity"] > 10:
72
+ yield b'\x00' # Null byte (Heartbeat)
73
+ SESSION["last_activity"] = time.time()
74
+
75
+ await asyncio.sleep(0.01)
76
  else:
77
+ await asyncio.sleep(1)
 
78
 
79
  @app.get("/stream")
80
  def stream_output():
81
+ return StreamingResponse(stream_generator(), media_type="application/octet-stream")
 
82
 
83
  @app.post("/write")
84
  def write_input(payload: InputData):
 
85
  if SESSION["fd"]:
86
+ # Write data exactly as received
87
  os.write(SESSION["fd"], payload.data.encode())
88
  return {"status": "ok"}