AIencoder commited on
Commit
e80361d
·
verified ·
1 Parent(s): e1d65a6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +164 -51
app.py CHANGED
@@ -30,52 +30,109 @@ SCREEN_W, SCREEN_H = 800, 600
30
  SNIPPETS_FILE = "/tmp/axon_snippets.json"
31
 
32
  # ═══════════════════════════════════════
33
- # Virtual Display
34
  # ═══════════════════════════════════════
35
  class VirtualDisplay:
36
  def __init__(self):
37
  self.xvfb_proc = None
38
  self.display = DISPLAY_NUM
 
39
  self._start_xvfb()
40
 
41
  def _start_xvfb(self):
42
  try:
43
- subprocess.run(["pkill", "-f", f"Xvfb {self.display}"], capture_output=True, timeout=5)
44
- time.sleep(0.2)
45
- self.xvfb_proc = subprocess.Popen(
46
- ["Xvfb", self.display, "-screen", "0", f"{SCREEN_W}x{SCREEN_H}x24",
47
- "-ac", "-nolisten", "tcp"],
48
- stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
49
- time.sleep(0.5)
 
 
 
 
 
 
 
50
  if self.xvfb_proc.poll() is None:
51
- print(f"[Xvfb] Running on {self.display}")
52
  os.environ["DISPLAY"] = self.display
 
53
  else:
54
- self.xvfb_proc = None
 
 
 
 
 
 
 
 
 
 
 
55
  except Exception as e:
56
  print(f"[Xvfb] {e}"); self.xvfb_proc = None
57
 
58
  def capture(self):
 
59
  if not self.is_running: return None
60
- tmp = tempfile.NamedTemporaryFile(suffix=".png", delete=False); tmp.close()
61
- for cmd in [
62
- ["scrot", "--display", self.display, tmp.name],
63
- f"import -window root -display {self.display} {tmp.name}",
64
- ]:
65
- try:
66
- kw = {"shell": True} if isinstance(cmd, str) else {}
67
- r = subprocess.run(cmd, capture_output=True, timeout=3,
68
- env={**os.environ, "DISPLAY": self.display}, **kw)
69
- if r.returncode == 0 and os.path.exists(tmp.name) and os.path.getsize(tmp.name) > 100:
70
- return tmp.name
71
  except: pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  try: os.unlink(tmp.name)
73
  except: pass
74
  return None
75
 
76
  @property
77
- def is_running(self): return self.xvfb_proc is not None and self.xvfb_proc.poll() is None
 
 
78
  def cleanup(self):
 
 
 
79
  if self.xvfb_proc:
80
  self.xvfb_proc.terminate()
81
  try: self.xvfb_proc.wait(timeout=3)
@@ -84,8 +141,10 @@ class VirtualDisplay:
84
  vdisplay = VirtualDisplay()
85
 
86
  # ═══════════════════════════════════════
87
- # GUI Process Manager
88
  # ═══════════════════════════════════════
 
 
89
  class GUIProcessManager:
90
  def __init__(self):
91
  self.process = None
@@ -94,28 +153,63 @@ class GUIProcessManager:
94
  self.stop()
95
  tmp = "/tmp/_axon_gui_run.py"
96
  with open(tmp, "w") as f: f.write(code)
 
 
97
  env = os.environ.copy()
98
- env.update({"DISPLAY": DISPLAY_NUM, "SDL_VIDEODRIVER": "x11"})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  try:
100
- self.process = subprocess.Popen([sys.executable, tmp], stdout=subprocess.PIPE,
101
- stderr=subprocess.PIPE, env=env, preexec_fn=os.setsid)
102
- time.sleep(0.5)
 
 
 
 
 
 
103
  if self.process.poll() is not None:
104
- out, err = self.process.communicate(timeout=2)
105
- msg = (out.decode(errors="replace") + "\n" + err.decode(errors="replace")).strip()
106
- return f"[Exited immediately]\n{msg}" if msg else "[Exited]"
107
- return f"[GUI launched — PID {self.process.pid}] Switch to DISPLAY tab to view."
108
- except Exception as e: return f"[Error] {e}"
 
 
 
 
 
 
 
 
 
109
 
110
  def stop(self):
111
  if self.process and self.process.poll() is None:
112
  pid = self.process.pid
113
- try: os.killpg(os.getpgid(pid), signal.SIGTERM); self.process.wait(timeout=3)
 
 
114
  except:
115
  try: os.killpg(os.getpgid(pid), signal.SIGKILL)
116
  except: pass
117
- self.process = None; return f"[Stopped PID {pid}]"
118
- self.process = None; return "[No process]"
 
 
119
 
120
  @property
121
  def is_running(self): return self.process is not None and self.process.poll() is None
@@ -479,8 +573,12 @@ def ai_gen(system_prompt, code, max_tokens=300):
479
  # ═══════════════════════════════════════
480
  # GUI code detection
481
  # ═══════════════════════════════════════
482
- GUI_HINTS = ["pygame", "tkinter", "turtle", "pyglet", "arcade", "kivy",
483
- "display.set_mode", "mainloop()"]
 
 
 
 
484
  def is_gui_code(c): return any(h in c for h in GUI_HINTS)
485
 
486
  # ═══════════════════════════════════════
@@ -689,10 +787,13 @@ with gr.Blocks(title="Axon Pro") as demo:
689
  display_image = gr.Image(label="", type="filepath", interactive=False, height=420)
690
  with gr.Row():
691
  capture_btn = gr.Button("📸 CAPTURE", size="sm", elem_classes="tb")
692
- auto_capture = gr.Checkbox(label="Auto (2s)", value=False)
 
 
 
693
  gui_status = gr.Markdown(
694
  f"<small>Xvfb: {'● ON' if vdisplay.is_running else '○ OFF'} "
695
- f"| {SCREEN_W}x{SCREEN_H}</small>")
696
 
697
  status_bar = gr.Markdown(
698
  f"**AXON PRO v4.0** │ Python {sys.version.split()[0]} │ CPU │ "
@@ -702,7 +803,7 @@ with gr.Blocks(title="Axon Pro") as demo:
702
  # State
703
  diff_original = gr.State("")
704
  diff_modified = gr.State("")
705
- auto_timer = gr.Timer(2, active=False)
706
 
707
  # ═══════════════════════════════════════
708
  # HANDLERS
@@ -745,9 +846,12 @@ with gr.Blocks(title="Axon Pro") as demo:
745
  fs.save_file(code)
746
 
747
  if is_gui_code(code):
748
- # GUI app — launch as background process, capture display
749
  msg = gui_mgr.launch(code)
750
- return msg, vdisplay.capture(), f"<small>Xvfb: ON | {gui_mgr.get_status()}</small>"
 
 
 
751
 
752
  # Normal script — run with subprocess, output goes to OUTPUT tab
753
  tmp = "/tmp/_axon_run.py"
@@ -768,11 +872,12 @@ with gr.Blocks(title="Axon Pro") as demo:
768
  output = "[Timed out after 30s]"
769
  except Exception as e:
770
  output = f"[Error] {e}"
771
- return output, gr.update(), gr.update()
772
 
773
  def on_stop():
774
- msg = gui_mgr.stop(); terminal._append(msg)
775
- return terminal.get_log(), f"<small>Xvfb: {'● ON' if vdisplay.is_running else '○ OFF'}</small>"
 
776
 
777
  # --- Terminal ---
778
  def on_term_cmd(cmd):
@@ -851,12 +956,19 @@ with gr.Blocks(title="Axon Pro") as demo:
851
  return vdisplay.capture()
852
 
853
  def on_auto_tick():
854
- if gui_mgr.process and gui_mgr.process.poll() is None:
855
- return vdisplay.capture()
856
- return gr.update()
 
 
857
 
858
  def on_auto_toggle(checked):
859
- return gr.Timer(2, active=checked)
 
 
 
 
 
860
 
861
  # ═══════════════════════════════════════
862
  # WIRING
@@ -870,9 +982,9 @@ with gr.Blocks(title="Axon Pro") as demo:
870
  del_btn.click(on_delete, None, [file_list, editor, editor, structure_view])
871
 
872
  # Run
873
- run_btn.click(on_run, editor, [run_output, display_image, gui_status]
874
  ).then(lambda: gr.Tabs(selected="output-tab"), None, bottom_tabs)
875
- stop_btn.click(on_stop, None, [term_out, gui_status])
876
 
877
  # Terminal
878
  term_in.submit(on_term_cmd, term_in, [term_out, term_in])
@@ -914,6 +1026,7 @@ with gr.Blocks(title="Axon Pro") as demo:
914
  # Display
915
  capture_btn.click(on_capture, None, display_image)
916
  auto_capture.change(on_auto_toggle, auto_capture, auto_timer)
 
917
  auto_timer.tick(on_auto_tick, None, display_image)
918
 
919
 
 
30
  SNIPPETS_FILE = "/tmp/axon_snippets.json"
31
 
32
  # ═══════════════════════════════════════
33
+ # Virtual Display (Optimized for pygame/tkinter/turtle)
34
  # ═══════════════════════════════════════
35
  class VirtualDisplay:
36
  def __init__(self):
37
  self.xvfb_proc = None
38
  self.display = DISPLAY_NUM
39
+ self._last_capture = None
40
  self._start_xvfb()
41
 
42
  def _start_xvfb(self):
43
  try:
44
+ subprocess.run(["pkill", "-f", f"Xvfb {self.display}"],
45
+ capture_output=True, timeout=5)
46
+ time.sleep(0.3)
47
+ # Full-featured Xvfb: GLX for OpenGL, RENDER for anti-aliasing,
48
+ # 24-bit color, no access control
49
+ self.xvfb_proc = subprocess.Popen([
50
+ "Xvfb", self.display,
51
+ "-screen", "0", f"{SCREEN_W}x{SCREEN_H}x24",
52
+ "-ac", # No access control
53
+ "-nolisten", "tcp", # Security
54
+ "+extension", "GLX", # OpenGL support (pygame)
55
+ "+extension", "RENDER", # Anti-aliased rendering
56
+ ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
57
+ time.sleep(0.8)
58
  if self.xvfb_proc.poll() is None:
 
59
  os.environ["DISPLAY"] = self.display
60
+ print(f"[Xvfb] Running on {self.display} ({SCREEN_W}x{SCREEN_H})")
61
  else:
62
+ # Fallback: simpler Xvfb without extensions
63
+ self.xvfb_proc = subprocess.Popen([
64
+ "Xvfb", self.display,
65
+ "-screen", "0", f"{SCREEN_W}x{SCREEN_H}x24", "-ac",
66
+ ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
67
+ time.sleep(0.5)
68
+ if self.xvfb_proc.poll() is None:
69
+ os.environ["DISPLAY"] = self.display
70
+ print(f"[Xvfb] Running (fallback mode)")
71
+ else:
72
+ self.xvfb_proc = None
73
+ print("[Xvfb] Failed to start")
74
  except Exception as e:
75
  print(f"[Xvfb] {e}"); self.xvfb_proc = None
76
 
77
  def capture(self):
78
+ """Capture the virtual display. Returns filepath or None."""
79
  if not self.is_running: return None
80
+
81
+ # Clean up previous capture to avoid /tmp filling up
82
+ if self._last_capture:
83
+ try: os.unlink(self._last_capture)
 
 
 
 
 
 
 
84
  except: pass
85
+
86
+ tmp = tempfile.NamedTemporaryFile(suffix=".png", delete=False)
87
+ tmp.close()
88
+ cap_env = {**os.environ, "DISPLAY": self.display}
89
+
90
+ # Method 1: xwd + convert (most reliable for Xvfb)
91
+ try:
92
+ xwd = subprocess.run(
93
+ ["xwd", "-root", "-display", self.display, "-silent"],
94
+ capture_output=True, timeout=3, env=cap_env)
95
+ if xwd.returncode == 0 and xwd.stdout:
96
+ conv = subprocess.run(
97
+ ["convert", "xwd:-", tmp.name],
98
+ input=xwd.stdout, capture_output=True, timeout=3)
99
+ if conv.returncode == 0 and os.path.getsize(tmp.name) > 100:
100
+ self._last_capture = tmp.name
101
+ return tmp.name
102
+ except: pass
103
+
104
+ # Method 2: scrot
105
+ try:
106
+ r = subprocess.run(
107
+ ["scrot", tmp.name],
108
+ capture_output=True, timeout=3, env=cap_env)
109
+ if r.returncode == 0 and os.path.exists(tmp.name) and os.path.getsize(tmp.name) > 100:
110
+ self._last_capture = tmp.name
111
+ return tmp.name
112
+ except: pass
113
+
114
+ # Method 3: import (ImageMagick)
115
+ try:
116
+ r = subprocess.run(
117
+ f"import -window root -display {self.display} {tmp.name}",
118
+ shell=True, capture_output=True, timeout=3, env=cap_env)
119
+ if r.returncode == 0 and os.path.exists(tmp.name) and os.path.getsize(tmp.name) > 100:
120
+ self._last_capture = tmp.name
121
+ return tmp.name
122
+ except: pass
123
+
124
  try: os.unlink(tmp.name)
125
  except: pass
126
  return None
127
 
128
  @property
129
+ def is_running(self):
130
+ return self.xvfb_proc is not None and self.xvfb_proc.poll() is None
131
+
132
  def cleanup(self):
133
+ if self._last_capture:
134
+ try: os.unlink(self._last_capture)
135
+ except: pass
136
  if self.xvfb_proc:
137
  self.xvfb_proc.terminate()
138
  try: self.xvfb_proc.wait(timeout=3)
 
141
  vdisplay = VirtualDisplay()
142
 
143
  # ═══════════════════════════════════════
144
+ # GUI Process Manager (pygame/tkinter/turtle)
145
  # ═══════════════════════════════════════
146
+ GUI_LOG = "/tmp/_axon_gui.log"
147
+
148
  class GUIProcessManager:
149
  def __init__(self):
150
  self.process = None
 
153
  self.stop()
154
  tmp = "/tmp/_axon_gui_run.py"
155
  with open(tmp, "w") as f: f.write(code)
156
+
157
+ # Full environment for GUI frameworks
158
  env = os.environ.copy()
159
+ env.update({
160
+ "DISPLAY": DISPLAY_NUM,
161
+ # SDL / pygame
162
+ "SDL_VIDEODRIVER": "x11",
163
+ "SDL_AUDIODRIVER": "dummy", # No audio device in headless
164
+ "PYGAME_HIDE_SUPPORT_PROMPT": "1", # Suppress pygame welcome
165
+ # Tkinter / general X11
166
+ "XDG_RUNTIME_DIR": "/tmp",
167
+ "MESA_GL_VERSION_OVERRIDE": "3.3", # OpenGL compat
168
+ "LIBGL_ALWAYS_SOFTWARE": "1", # Software rendering (no GPU)
169
+ # Python
170
+ "PYTHONUNBUFFERED": "1",
171
+ "PYTHONPATH": "/tmp/axon_workspace:" + env.get("PYTHONPATH", ""),
172
+ })
173
+
174
  try:
175
+ # Log file instead of PIPE — PIPE deadlocks pygame's output
176
+ log_f = open(GUI_LOG, "w")
177
+ self.process = subprocess.Popen(
178
+ [sys.executable, "-u", tmp], # -u = unbuffered
179
+ stdout=log_f, stderr=log_f,
180
+ env=env, preexec_fn=os.setsid, cwd="/tmp/axon_workspace")
181
+
182
+ time.sleep(1.0) # Give pygame time to create the window
183
+
184
  if self.process.poll() is not None:
185
+ log_f.close()
186
+ try:
187
+ with open(GUI_LOG) as f: msg = f.read().strip()
188
+ except: msg = ""
189
+ return f"[Exited immediately]\n{msg}" if msg else "[Exited — no output]"
190
+
191
+ return f"[GUI launched — PID {self.process.pid}] Auto-refresh enabled."
192
+ except Exception as e:
193
+ return f"[Error] {e}"
194
+
195
+ def get_log(self):
196
+ try:
197
+ with open(GUI_LOG) as f: return f.read()[-2000:]
198
+ except: return ""
199
 
200
  def stop(self):
201
  if self.process and self.process.poll() is None:
202
  pid = self.process.pid
203
+ try:
204
+ os.killpg(os.getpgid(pid), signal.SIGTERM)
205
+ self.process.wait(timeout=3)
206
  except:
207
  try: os.killpg(os.getpgid(pid), signal.SIGKILL)
208
  except: pass
209
+ self.process = None
210
+ return f"[Stopped PID {pid}]"
211
+ self.process = None
212
+ return "[No process]"
213
 
214
  @property
215
  def is_running(self): return self.process is not None and self.process.poll() is None
 
573
  # ═══════════════════════════════════════
574
  # GUI code detection
575
  # ═══════════════════════════════════════
576
+ GUI_HINTS = [
577
+ "pygame", "tkinter", "turtle", "pyglet", "arcade", "kivy",
578
+ "display.set_mode", "mainloop()", "Tk()", "Canvas(",
579
+ "pygame.init", "pygame.display", "screen.fill",
580
+ "from turtle import", "import turtle",
581
+ ]
582
  def is_gui_code(c): return any(h in c for h in GUI_HINTS)
583
 
584
  # ═══════════════════════════════════════
 
787
  display_image = gr.Image(label="", type="filepath", interactive=False, height=420)
788
  with gr.Row():
789
  capture_btn = gr.Button("📸 CAPTURE", size="sm", elem_classes="tb")
790
+ auto_capture = gr.Checkbox(label="Auto Refresh", value=False)
791
+ refresh_speed = gr.Dropdown(
792
+ choices=["0.5s", "1s", "2s", "5s"], value="1s",
793
+ label="", container=False, scale=1, interactive=True)
794
  gui_status = gr.Markdown(
795
  f"<small>Xvfb: {'● ON' if vdisplay.is_running else '○ OFF'} "
796
+ f"| {SCREEN_W}x{SCREEN_H} | SDL: x11 + dummy audio</small>")
797
 
798
  status_bar = gr.Markdown(
799
  f"**AXON PRO v4.0** │ Python {sys.version.split()[0]} │ CPU │ "
 
803
  # State
804
  diff_original = gr.State("")
805
  diff_modified = gr.State("")
806
+ auto_timer = gr.Timer(1, active=False)
807
 
808
  # ═══════════════════════════════════════
809
  # HANDLERS
 
846
  fs.save_file(code)
847
 
848
  if is_gui_code(code):
849
+ # GUI app — launch, take first screenshot, auto-enable refresh
850
  msg = gui_mgr.launch(code)
851
+ time.sleep(0.5) # Extra time for first frame
852
+ ss = vdisplay.capture()
853
+ status = f"<small>Xvfb: ● ON | {gui_mgr.get_status()}</small>"
854
+ return msg, ss, status, gr.update(value=True) # Enable auto-refresh
855
 
856
  # Normal script — run with subprocess, output goes to OUTPUT tab
857
  tmp = "/tmp/_axon_run.py"
 
872
  output = "[Timed out after 30s]"
873
  except Exception as e:
874
  output = f"[Error] {e}"
875
+ return output, gr.update(), gr.update(), gr.update()
876
 
877
  def on_stop():
878
+ msg = gui_mgr.stop()
879
+ status = f"<small>Xvfb: {'● ON' if vdisplay.is_running else '○ OFF'} | ○ idle</small>"
880
+ return msg, status, gr.update(value=False) # Disable auto-refresh
881
 
882
  # --- Terminal ---
883
  def on_term_cmd(cmd):
 
956
  return vdisplay.capture()
957
 
958
  def on_auto_tick():
959
+ ss = vdisplay.capture()
960
+ # Auto-disable if GUI process died
961
+ if not gui_mgr.is_running:
962
+ return ss if ss else gr.update()
963
+ return ss if ss else gr.update()
964
 
965
  def on_auto_toggle(checked):
966
+ return gr.Timer(1, active=checked)
967
+
968
+ def on_speed_change(speed, auto_on):
969
+ speed_map = {"0.5s": 0.5, "1s": 1, "2s": 2, "5s": 5}
970
+ interval = speed_map.get(speed, 1)
971
+ return gr.Timer(interval, active=auto_on)
972
 
973
  # ═══════════════════════════════════════
974
  # WIRING
 
982
  del_btn.click(on_delete, None, [file_list, editor, editor, structure_view])
983
 
984
  # Run
985
+ run_btn.click(on_run, editor, [run_output, display_image, gui_status, auto_capture]
986
  ).then(lambda: gr.Tabs(selected="output-tab"), None, bottom_tabs)
987
+ stop_btn.click(on_stop, None, [run_output, gui_status, auto_capture])
988
 
989
  # Terminal
990
  term_in.submit(on_term_cmd, term_in, [term_out, term_in])
 
1026
  # Display
1027
  capture_btn.click(on_capture, None, display_image)
1028
  auto_capture.change(on_auto_toggle, auto_capture, auto_timer)
1029
+ refresh_speed.change(on_speed_change, [refresh_speed, auto_capture], auto_timer)
1030
  auto_timer.tick(on_auto_tick, None, display_image)
1031
 
1032