CORVO-AI commited on
Commit
e9856a9
·
verified ·
1 Parent(s): fbc7990

Update file-manager/app.py

Browse files
Files changed (1) hide show
  1. file-manager/app.py +114 -119
file-manager/app.py CHANGED
@@ -1,5 +1,5 @@
1
- import os, shutil, html, urllib.parse, subprocess, threading, queue, sys, time, json
2
- from flask import Flask, request, redirect, send_file, Response, stream_with_context
3
  from werkzeug.middleware.dispatcher import DispatcherMiddleware
4
  from werkzeug.serving import run_simple
5
 
@@ -13,58 +13,83 @@ def is_sensitive(name):
13
 
14
  def mask_secrets(text):
15
  out = []
16
- for line in text.splitlines():
17
  if any(h in line.upper() for h in SECRET_HINTS) and ("=" in line or ":" in line):
18
- out.append("*** line masked (possible secret) ***")
19
  else:
20
  out.append(line)
21
- return "\n".join(out)
22
 
23
  # =================================================================
24
- # PERSISTENT INTERACTIVE PYTHON REPL (supports input + streaming)
 
25
  # =================================================================
26
- class Repl:
27
  def __init__(self):
28
- self.p = subprocess.Popen(
29
- [sys.executable, "-i", "-u"], # -i interactive, -u unbuffered
30
- stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
31
- text=True, bufsize=1,
32
- )
33
- self.q = queue.Queue()
34
- threading.Thread(target=self._reader, daemon=True).start()
 
 
 
 
 
 
 
 
 
 
 
 
 
35
 
36
  def _reader(self):
37
- for line in self.p.stdout:
38
- self.q.put(line)
 
 
 
 
 
 
 
 
 
39
 
40
- def send(self, text):
41
- if self.p.poll() is None:
42
- self.p.stdin.write(text + "\n")
43
- self.p.stdin.flush()
 
44
 
45
- def drain(self, wait=0.3):
46
- # collect whatever the REPL has produced so far
47
- time.sleep(wait)
48
- chunks = []
49
- while not self.q.empty():
50
- chunks.append(self.q.get())
51
- return mask_secrets("".join(chunks))
52
 
53
  def alive(self):
54
- return self.p.poll() is None
 
 
 
 
55
 
56
  def kill(self):
57
  try:
58
- self.p.kill()
59
- except Exception:
60
  pass
61
 
62
- _repl = {"r": None}
63
- def get_repl():
64
- if _repl["r"] is None or not _repl["r"].alive():
65
- _repl["r"] = Repl()
66
- _repl["r"].drain(0.5) # swallow the startup banner
67
- return _repl["r"]
68
 
69
  PAGE = """<!doctype html><html><head><title>Box</title>
70
  <style>
@@ -74,112 +99,82 @@ table{{border-collapse:collapse;width:100%}} td,th{{padding:4px 8px;border-botto
74
  .bar{{background:#1a1a1a;padding:10px;margin-bottom:10px;border-radius:6px}}
75
  .tabs a{{margin-right:14px;font-weight:bold}}
76
  textarea{{width:100%;background:#000;color:#47E6C1;border:1px solid #333;padding:8px}}
77
- pre{{background:#000;color:#78D64B;padding:12px;border-radius:6px;overflow:auto;height:55vh;white-space:pre-wrap}}
78
- input[type=text]{{background:#000;color:#eee;border:1px solid #333;padding:8px}}
79
  button{{background:#F46821;color:#fff;border:0;padding:8px 14px;border-radius:4px;cursor:pointer}}
80
  </style></head><body>
81
- <div class="tabs bar"><a href="/fm/">📁 Files</a><a href="/fm/term">🖥️ Shell (stream)</a>
82
- <a href="/fm/py">🐍 Python REPL</a><a href="/reload">♻️ Reload</a><a href="/gpuinfo">🎮 GPU</a>
83
  <span style="color:#777">test mode · ephemeral</span></div>
84
  {body}</body></html>"""
85
 
86
- # ---------------- INTERACTIVE PYTHON REPL PAGE ----------------
87
- @app.route("/py")
88
- def py_page():
89
  body = """
90
- <div class="bar"><b>Interactive Python</b> — persistent session. Try: <code>2+2</code>,
91
- then <code>name = input("your name? ")</code> and answer in the same box.</div>
92
- <pre id="out">Python REPL ready. Type code below and press Run (or Enter).\n</pre>
93
- <input type="text" id="line" style="width:80%" autofocus
94
- placeholder="python code or input to a running prompt">
95
- <button onclick="send()">Run</button>
96
- <button onclick="reset()">Restart REPL</button>
 
 
97
  <script>
98
  const out = document.getElementById('out');
99
  const line = document.getElementById('line');
100
  function append(t){ out.textContent += t; out.scrollTop = out.scrollHeight; }
 
101
  async function send(){
102
- const code = line.value;
103
- append('>>> ' + code + '\\n');
104
  line.value = '';
105
- const r = await fetch('/fm/py_send', {
106
- method:'POST', headers:{'Content-Type':'application/json'},
107
- body: JSON.stringify({code})
108
- });
109
- const data = await r.json();
110
- if (data.output) append(data.output);
 
 
111
  }
112
  async function reset(){
113
- await fetch('/fm/py_reset', {method:'POST'});
114
- out.textContent = 'REPL restarted.\\n';
 
 
 
 
 
 
 
 
 
115
  }
116
  line.addEventListener('keydown', e => { if(e.key==='Enter'){ e.preventDefault(); send(); }});
 
117
  </script>"""
118
  return Response(PAGE.format(body=body))
119
 
120
- @app.route("/py_send", methods=["POST"])
121
- def py_send():
122
  data = request.get_json(force=True)
123
- repl = get_repl()
124
- repl.send(data.get("code", ""))
125
- return Response(json.dumps({"output": repl.drain(0.4)}), mimetype="application/json")
126
-
127
- @app.route("/py_reset", methods=["POST"])
128
- def py_reset():
129
- if _repl["r"]:
130
- _repl["r"].kill()
131
- _repl["r"] = None
132
- get_repl()
133
  return Response(json.dumps({"ok": True}), mimetype="application/json")
134
 
135
- # ---------------- STREAMING ONE-SHOT SHELL ----------------
136
- @app.route("/term")
137
- def term_page():
138
- body = """
139
- <div class="bar"><b>Streaming shell</b> — output appears live, line by line.</div>
140
- <input type="text" id="cmd" style="width:80%" autofocus
141
- placeholder="e.g. python -c 'print(2+2)' , pip install rich , ls -la">
142
- <button onclick="run()">Run</button>
143
- <pre id="out"></pre>
144
- <script>
145
- const out = document.getElementById('out');
146
- async function run(){
147
- const cmd = document.getElementById('cmd').value;
148
- out.textContent = '$ ' + cmd + '\\n';
149
- const resp = await fetch('/fm/term_stream?cmd=' + encodeURIComponent(cmd));
150
- const reader = resp.body.getReader();
151
- const dec = new TextDecoder();
152
- while(true){
153
- const {value, done} = await reader.read();
154
- if(done) break;
155
- out.textContent += dec.decode(value);
156
- out.scrollTop = out.scrollHeight;
157
- }
158
- }
159
- document.getElementById('cmd').addEventListener('keydown',
160
- e => { if(e.key==='Enter'){ e.preventDefault(); run(); }});
161
- </script>"""
162
- return Response(PAGE.format(body=body))
163
 
164
- @app.route("/term_stream")
165
- def term_stream():
166
- cmd = request.args.get("cmd", "")
167
- @stream_with_context
168
- def gen():
169
- if not cmd.strip():
170
- yield "[empty command]\n"; return
171
- p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE,
172
- stderr=subprocess.STDOUT, text=True, bufsize=1)
173
- try:
174
- for line in p.stdout:
175
- yield mask_secrets(line)
176
- p.wait(timeout=300)
177
- yield f"\n[exit code {p.returncode}]\n"
178
- except Exception as e:
179
- yield f"\n[error: {e}]\n"
180
- return Response(gen(), mimetype="text/plain")
181
 
182
- # ---------------- FILE MANAGER (unchanged core) ----------------
183
  @app.route("/")
184
  def browse():
185
  path = os.path.abspath(request.args.get("path", "/"))
@@ -302,4 +297,4 @@ def delete():
302
 
303
  if __name__ == "__main__":
304
  wrapped = DispatcherMiddleware(Flask("empty"), {"/fm": app})
305
- run_simple("0.0.0.0", 9001, wrapped, threaded=True) # threaded=True for streaming
 
1
+ import os, shutil, html, urllib.parse, subprocess, pty, select, termios, struct, fcntl, signal, threading, json
2
+ from flask import Flask, request, redirect, send_file, Response
3
  from werkzeug.middleware.dispatcher import DispatcherMiddleware
4
  from werkzeug.serving import run_simple
5
 
 
13
 
14
  def mask_secrets(text):
15
  out = []
16
+ for line in text.splitlines(keepends=True):
17
  if any(h in line.upper() for h in SECRET_HINTS) and ("=" in line or ":" in line):
18
+ out.append("*** line masked (possible secret) ***\n")
19
  else:
20
  out.append(line)
21
+ return "".join(out)
22
 
23
  # =================================================================
24
+ # PERSISTENT PTY SHELL (real terminal: proper buffering, prompts,
25
+ # interactive input, cd persistence, colors)
26
  # =================================================================
27
+ class PtyShell:
28
  def __init__(self):
29
+ self.pid, self.fd = pty.fork()
30
+ if self.pid == 0:
31
+ # child: become an interactive bash inside the PTY
32
+ os.environ["TERM"] = "xterm-256color"
33
+ os.execvp("bash", ["bash", "-i"])
34
+ else:
35
+ # parent: set the fd non-blocking so reads never hang
36
+ flags = fcntl.fcntl(self.fd, fcntl.F_GETFL)
37
+ fcntl.fcntl(self.fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
38
+ self._set_size(40, 120)
39
+ self.buffer = ""
40
+ self.lock = threading.Lock()
41
+ threading.Thread(target=self._reader, daemon=True).start()
42
+
43
+ def _set_size(self, rows, cols):
44
+ try:
45
+ fcntl.ioctl(self.fd, termios.TIOCSWINSZ,
46
+ struct.pack("HHHH", rows, cols, 0, 0))
47
+ except Exception:
48
+ pass
49
 
50
  def _reader(self):
51
+ while True:
52
+ try:
53
+ r, _, _ = select.select([self.fd], [], [], 0.5)
54
+ if r:
55
+ data = os.read(self.fd, 65536)
56
+ if not data:
57
+ break
58
+ with self.lock:
59
+ self.buffer += data.decode(errors="replace")
60
+ except OSError:
61
+ break
62
 
63
+ def write(self, text):
64
+ try:
65
+ os.write(self.fd, text.encode())
66
+ except OSError:
67
+ pass
68
 
69
+ def read_new(self):
70
+ with self.lock:
71
+ data = self.buffer
72
+ self.buffer = ""
73
+ return mask_secrets(data)
 
 
74
 
75
  def alive(self):
76
+ try:
77
+ pid, _ = os.waitpid(self.pid, os.WNOHANG)
78
+ return pid == 0
79
+ except OSError:
80
+ return False
81
 
82
  def kill(self):
83
  try:
84
+ os.kill(self.pid, signal.SIGKILL)
85
+ except OSError:
86
  pass
87
 
88
+ _shell = {"s": None}
89
+ def get_shell():
90
+ if _shell["s"] is None or not _shell["s"].alive():
91
+ _shell["s"] = PtyShell()
92
+ return _shell["s"]
 
93
 
94
  PAGE = """<!doctype html><html><head><title>Box</title>
95
  <style>
 
99
  .bar{{background:#1a1a1a;padding:10px;margin-bottom:10px;border-radius:6px}}
100
  .tabs a{{margin-right:14px;font-weight:bold}}
101
  textarea{{width:100%;background:#000;color:#47E6C1;border:1px solid #333;padding:8px}}
102
+ pre{{background:#000;color:#e6e6e6;padding:12px;border-radius:6px;overflow:auto;height:60vh;white-space:pre-wrap}}
103
+ input[type=text]{{background:#000;color:#eee;border:1px solid #333;padding:8px;font-family:monospace}}
104
  button{{background:#F46821;color:#fff;border:0;padding:8px 14px;border-radius:4px;cursor:pointer}}
105
  </style></head><body>
106
+ <div class="tabs bar"><a href="/fm/">📁 Files</a><a href="/fm/term">🖥️ Terminal</a>
107
+ <a href="/reload">♻️ Reload</a><a href="/gpuinfo">🎮 GPU</a>
108
  <span style="color:#777">test mode · ephemeral</span></div>
109
  {body}</body></html>"""
110
 
111
+ # ---------------- REAL TERMINAL PAGE ----------------
112
+ @app.route("/term")
113
+ def term_page():
114
  body = """
115
+ <div class="bar"><b>Live PTY terminal</b> — persistent bash. Full logs, prompts,
116
+ and interactive input all work. Try: <code>python3</code> then <code>2+2</code>,
117
+ or <code>python3 -c "n=input('name? '); print('hi',n)"</code>.</div>
118
+ <pre id="out"></pre>
119
+ <input type="text" id="line" style="width:82%" autofocus
120
+ placeholder="type a command or input, then Enter">
121
+ <button onclick="send()">Send</button>
122
+ <button onclick="ctrlc()">Ctrl-C</button>
123
+ <button onclick="reset()">Restart</button>
124
  <script>
125
  const out = document.getElementById('out');
126
  const line = document.getElementById('line');
127
  function append(t){ out.textContent += t; out.scrollTop = out.scrollHeight; }
128
+
129
  async function send(){
130
+ const text = line.value;
 
131
  line.value = '';
132
+ await fetch('/fm/term_in', {method:'POST',
133
+ headers:{'Content-Type':'application/json'},
134
+ body: JSON.stringify({data: text + "\\n"})});
135
+ }
136
+ async function ctrlc(){
137
+ await fetch('/fm/term_in', {method:'POST',
138
+ headers:{'Content-Type':'application/json'},
139
+ body: JSON.stringify({data: "\\u0003"})}); // Ctrl-C
140
  }
141
  async function reset(){
142
+ await fetch('/fm/term_reset', {method:'POST'});
143
+ out.textContent = '';
144
+ }
145
+ // Poll for new output every 300ms — reliable through the proxy
146
+ async function poll(){
147
+ try{
148
+ const r = await fetch('/fm/term_out');
149
+ const d = await r.json();
150
+ if(d.data) append(d.data);
151
+ }catch(e){}
152
+ setTimeout(poll, 300);
153
  }
154
  line.addEventListener('keydown', e => { if(e.key==='Enter'){ e.preventDefault(); send(); }});
155
+ poll();
156
  </script>"""
157
  return Response(PAGE.format(body=body))
158
 
159
+ @app.route("/term_in", methods=["POST"])
160
+ def term_in():
161
  data = request.get_json(force=True)
162
+ get_shell().write(data.get("data", ""))
 
 
 
 
 
 
 
 
 
163
  return Response(json.dumps({"ok": True}), mimetype="application/json")
164
 
165
+ @app.route("/term_out")
166
+ def term_out():
167
+ return Response(json.dumps({"data": get_shell().read_new()}), mimetype="application/json")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
 
169
+ @app.route("/term_reset", methods=["POST"])
170
+ def term_reset():
171
+ if _shell["s"]:
172
+ _shell["s"].kill()
173
+ _shell["s"] = None
174
+ get_shell()
175
+ return Response(json.dumps({"ok": True}), mimetype="application/json")
 
 
 
 
 
 
 
 
 
 
176
 
177
+ # ---------------- FILE MANAGER ----------------
178
  @app.route("/")
179
  def browse():
180
  path = os.path.abspath(request.args.get("path", "/"))
 
297
 
298
  if __name__ == "__main__":
299
  wrapped = DispatcherMiddleware(Flask("empty"), {"/fm": app})
300
+ run_simple("0.0.0.0", 9001, wrapped, threaded=True) # threaded=True for concurrent poll+input