THEZYZSTUDIO commited on
Commit
bc79214
ยท
verified ยท
1 Parent(s): d4ace36

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +176 -24
app.py CHANGED
@@ -12,6 +12,7 @@ THE Z AI โ€” Computer Mode Server v7 โ€” SMART CONTROL
12
 
13
  import asyncio
14
  import base64
 
15
  import io
16
  import json
17
  import os
@@ -25,23 +26,159 @@ from fastapi.middleware.cors import CORSMiddleware
25
  from fastapi.responses import JSONResponse
26
  import uvicorn
27
 
28
- DISPLAY = os.environ.get("DISPLAY", ":1")
29
- os.environ["DISPLAY"] = DISPLAY
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
 
31
  # โ”€โ”€โ”€ ุชุดุฎูŠุต ุฃุฏูˆุงุช ุงู„ู€ capture ุนู†ุฏ ุงู„ุจุฏุงูŠุฉ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
32
  def _check_capture_tools():
33
- tools = ["scrot", "import", "xwd", "convert", "ffmpeg", "xdotool"]
34
  print("โ”€โ”€โ”€ Capture Tools Check โ”€โ”€โ”€")
35
  for t in tools:
36
  r = subprocess.run(["which", t], capture_output=True, text=True)
37
  status = "โœ…" if r.returncode == 0 else "โŒ"
38
  print(f" {status} {t}: {r.stdout.strip() or 'not found'}")
39
  try:
40
- r2 = subprocess.run(["xdpyinfo", "-display", DISPLAY],
41
  capture_output=True, text=True, timeout=3)
42
- print(f" {'โœ…' if r2.returncode==0 else 'โŒ'} DISPLAY={DISPLAY}: {'active' if r2.returncode==0 else 'not active'}")
43
  except Exception as ex:
44
- print(f" โŒ DISPLAY={DISPLAY}: {ex}")
45
  print("โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€")
46
 
47
  try:
@@ -184,9 +321,9 @@ def _is_empty_result(stdout: str) -> bool:
184
  # โ”€โ”€โ”€ Screen Capture โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
185
 
186
  def _get_screen_size() -> tuple[int, int]:
187
- """ูŠูุนูŠุฏ ุญุฌู… ุงู„ุดุงุดุฉ ุงู„ุญู‚ูŠู‚ูŠ"""
188
  r = subprocess.run(["xdotool", "getdisplaygeometry"],
189
- env={**os.environ, "DISPLAY": DISPLAY},
190
  capture_output=True, text=True, timeout=5)
191
  try:
192
  parts = r.stdout.strip().split()
@@ -196,9 +333,9 @@ def _get_screen_size() -> tuple[int, int]:
196
 
197
 
198
  def _get_mouse_pos() -> tuple[int, int]:
199
- """ูŠูุนูŠุฏ ู…ูˆู‚ุน ุงู„ู…ุงูˆุณ ุงู„ุญุงู„ูŠ"""
200
  r = subprocess.run(["xdotool", "getmouselocation"],
201
- env={**os.environ, "DISPLAY": DISPLAY},
202
  capture_output=True, text=True, timeout=5)
203
  try:
204
  mx = int(re.search(r"x:(\d+)", r.stdout).group(1))
@@ -220,7 +357,8 @@ def capture_screen_raw() -> tuple[object, int, int]:
220
  5. python-xlib / Xlib ู…ุจุงุดุฑุฉ
221
  """
222
  from PIL import Image
223
- env = {**os.environ, "DISPLAY": DISPLAY}
 
224
  tmp = f"/tmp/zs_{int(time.time()*1000)}"
225
  _tmp_files_to_clean = [] # ูƒู„ ุงู„ู…ู„ูุงุช ุงู„ู…ุคู‚ุชุฉ ุงู„ุชูŠ ุฃูู†ุดุฆุช ููŠ ู‡ุฐู‡ ุงู„ู…ุญุงูˆู„ุฉ
226
 
@@ -314,7 +452,7 @@ def capture_screen_raw() -> tuple[object, int, int]:
314
  "ffmpeg", "-y",
315
  "-f", "x11grab",
316
  "-video_size", f"{sw}x{sh}",
317
- "-i", DISPLAY,
318
  "-vframes", "1",
319
  "-q:v", "2",
320
  p
@@ -331,7 +469,7 @@ def capture_screen_raw() -> tuple[object, int, int]:
331
  try:
332
  from Xlib import display as Xdisplay
333
  from Xlib.ext.xtest import fake_input
334
- xdisp = Xdisplay.Display(DISPLAY)
335
  root = xdisp.screen().root
336
  geom = root.get_geometry()
337
  w, h = geom.width, geom.height
@@ -359,7 +497,7 @@ def capture_screen_raw() -> tuple[object, int, int]:
359
  img = Image.new("RGB", (sw or 1280, sh or 720), (20, 20, 30))
360
  draw = ImageDraw.Draw(img)
361
  draw.rectangle([(0, 0), (sw, 40)], fill=(30, 30, 50))
362
- draw.text((10, 10), f"โš ๏ธ Screenshot failed โ€” DISPLAY={DISPLAY}", fill=(255, 100, 100))
363
  draw.text((10, 50), "Methods tried: scrot, import, xwd, ffmpeg, xlib", fill=(150, 150, 150))
364
  return img, sw or 1280, sh or 720
365
  except:
@@ -511,7 +649,7 @@ def _sanitize_pkill_self_match(cmd: str) -> str:
511
 
512
  def run_raw_command(cmd: str, timeout: int = 60) -> dict:
513
  cmd = _sanitize_pkill_self_match(cmd)
514
- env = {**os.environ, "DISPLAY": DISPLAY,
515
  "PYTHONIOENCODING": "utf-8", "LANG": "en_US.UTF-8"}
516
  try:
517
  result = subprocess.run(cmd, shell=True, capture_output=True,
@@ -591,9 +729,10 @@ def run_command_smart(cmd: str, timeout: int = 60) -> dict:
591
 
592
  async def xdo(args: list, timeout=10) -> dict:
593
  """ูŠู†ูู‘ุฐ xdotool ููŠ thread ู…ู†ูุตู„ ุญุชู‰ ู„ุง ูŠูุฌู…ู‘ุฏ ุญู„ู‚ุฉ asyncio ุฃุซู†ุงุก ุงู„ุงู†ุชุธุงุฑ."""
 
594
  def _run():
595
  r = subprocess.run(["xdotool"] + args,
596
- env={**os.environ, "DISPLAY": DISPLAY},
597
  timeout=timeout, capture_output=True, text=True)
598
  return {"rc": r.returncode, "out": r.stdout, "err": r.stderr}
599
  return await asyncio.to_thread(_run)
@@ -610,11 +749,12 @@ async def type_text_smart(text: str) -> dict:
610
  if has_arabic:
611
  # ุทุฑูŠู‚ุฉ Clipboard ู„ุถู…ุงู† ูƒุชุงุจุฉ ุงู„ุนุฑุจูŠุฉ ุจุดูƒู„ ุตุญูŠุญ
612
  try:
 
613
  def _xclip_write():
614
  proc = subprocess.Popen(
615
  ["xclip", "-selection", "clipboard"],
616
  stdin=subprocess.PIPE,
617
- env={**os.environ, "DISPLAY": DISPLAY}
618
  )
619
  proc.communicate(text.encode("utf-8"))
620
  await asyncio.to_thread(_xclip_write)
@@ -641,7 +781,7 @@ def open_browser_smart(url: str = "") -> str:
641
  # ุฌุฑุจ ุฃูˆู„ุงู‹: BROWSER ุงู„ุนุงุฏูŠ
642
  cmd = f"{browser_cmd} --new-window '{url}' &"
643
  proc = subprocess.Popen(cmd, shell=True,
644
- env={**os.environ, "DISPLAY": DISPLAY},
645
  stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
646
  return f"{browser_cmd} โ†’ {url}"
647
 
@@ -919,11 +1059,12 @@ async def handle_action(ws: WebSocket, msg: dict):
919
  elif action == "clipboard_write":
920
  text = data.get("text", "")
921
  try:
 
922
  def _xclip_write2():
923
  proc = subprocess.Popen(
924
  ["xclip", "-selection", "clipboard"],
925
  stdin=subprocess.PIPE,
926
- env={**os.environ, "DISPLAY": DISPLAY}
927
  )
928
  proc.communicate(text.encode("utf-8"))
929
  await asyncio.to_thread(_xclip_write2)
@@ -960,7 +1101,7 @@ async def handle_action(ws: WebSocket, msg: dict):
960
  fixed_cmd = app_cmd.replace("firefox-esr", BROWSER)
961
 
962
  subprocess.Popen(fixed_cmd, shell=True,
963
- env={**os.environ, "DISPLAY": DISPLAY},
964
  stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
965
  await send({"type": "ack", "action": "open_app", "cmd": fixed_cmd})
966
 
@@ -1016,11 +1157,12 @@ async def handle_action(ws: WebSocket, msg: dict):
1016
  elif action == "paste":
1017
  text = data.get("text", "")
1018
  if text:
 
1019
  def _xclip_paste():
1020
  proc = subprocess.Popen(
1021
  ["xclip", "-selection", "clipboard"],
1022
  stdin=subprocess.PIPE,
1023
- env={**os.environ, "DISPLAY": DISPLAY}
1024
  )
1025
  proc.communicate(text.encode("utf-8"))
1026
  await asyncio.to_thread(_xclip_paste)
@@ -1043,13 +1185,21 @@ async def handle_action(ws: WebSocket, msg: dict):
1043
  async def websocket_endpoint(ws: WebSocket):
1044
  await ws.accept()
1045
  active_connections.append(ws)
1046
- w, h = _get_screen_size()
 
 
 
 
 
 
 
1047
  await ws.send_text(json.dumps({
1048
  "type": "connected",
1049
  "screen_width": w,
1050
  "screen_height": h,
1051
  "browser": BROWSER,
1052
- "msg": f"Z Computer Mode v7 โ€” Smart Control | Browser: {BROWSER} | Screen: {w}x{h}",
 
1053
  "commands": [
1054
  "screenshot", "terminal {cmd}", "mouse_move {x,y}", "mouse_click {x,y,button}",
1055
  "keyboard_type {text}", "keyboard_hotkey {keys}", "scroll {x,y,clicks}",
@@ -1057,7 +1207,7 @@ async def websocket_endpoint(ws: WebSocket):
1057
  "Simple cmds: 'open firefox', 'mouse go x y', 'click x y', 'type TEXT', 'key enter'"
1058
  ]
1059
  }, ensure_ascii=False))
1060
- # ุฃุฑุณู„ screenshot ุฃูˆู„ูŠุฉ ู…ุน grid
1061
  result = await asyncio.to_thread(capture_screen_with_grid, scale=0.65, quality=72)
1062
  if result["data"]:
1063
  await ws.send_text(json.dumps({
@@ -1082,6 +1232,8 @@ async def websocket_endpoint(ws: WebSocket):
1082
  finally:
1083
  if ws in active_connections:
1084
  active_connections.remove(ws)
 
 
1085
 
1086
 
1087
  # โ”€โ”€โ”€ REST โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
 
12
 
13
  import asyncio
14
  import base64
15
+ import contextvars
16
  import io
17
  import json
18
  import os
 
26
  from fastapi.responses import JSONResponse
27
  import uvicorn
28
 
29
+ # โ”€โ”€โ”€ DISPLAY ุงู„ุฃุณุงุณูŠ (ุงุญุชูŠุงุทูŠ ูู‚ุท โ€” ู„ุง ูŠูุณุชุฎุฏู… ูุนู„ูŠุงู‹ ุจุนุฏ ุชูุนูŠู„ ุงู„ุนุฒู„ ุงู„ูƒุงู…ู„) โ”€โ”€
30
+ _BASE_DISPLAY = os.environ.get("DISPLAY", ":1")
31
+ os.environ["DISPLAY"] = _BASE_DISPLAY
32
+
33
+ # โ”€โ”€โ”€ ุนุฒู„ ูƒุงู…ู„ ุจูŠู† ุงู„ู…ุณุชุฎุฏู…ูŠู†: ูƒู„ ุงุชุตุงู„ WebSocket ูŠุญุตู„ ุนู„ู‰ Xvfb + Firefox ุฎุงุตูŠู† ุจู‡ โ”€โ”€
34
+ # ูŠูุฎุฒูŽู‘ู† DISPLAY ุงู„ุญุงู„ูŠ ููŠ contextvar ุจุญูŠุซ ูƒู„ ุงู„ุฏูˆุงู„ (xdo, capture_screen_with_grid, ...)
35
+ # ุงู„ุชูŠ ุชูุณุชุฏุนู‰ ู…ู† ุฏุงุฎู„ ู…ุนุงู„ุฌุฉ ุฑุณุงุฆู„ ู‡ุฐุง ุงู„ุงุชุตุงู„ ุชู‚ุฑุฃ ุชู„ู‚ุงุฆูŠุงู‹ display ุงู„ุฎุงุต ุจุฌู„ุณุชู‡ุŒ
36
+ # ุฏูˆู† ุงู„ุญุงุฌุฉ ู„ุชู…ุฑูŠุฑ ุงู„ู…ุนุงู…ู„ ูŠุฏูˆูŠุงู‹ ุนุจุฑ ูƒู„ ุณู„ุณู„ุฉ ุงู„ุงุณุชุฏุนุงุกุงุช.
37
+ _current_display: contextvars.ContextVar[str] = contextvars.ContextVar(
38
+ "_current_display", default=_BASE_DISPLAY
39
+ )
40
+
41
+ def DISPLAY_OF() -> str:
42
+ """ูŠูุนูŠุฏ DISPLAY ุงู„ุฎุงุต ุจุงู„ุฌู„ุณุฉ ุงู„ุญุงู„ูŠุฉ (ุงู„ุงุชุตุงู„ ุงู„ุฐูŠ ุชุฌุฑูŠ ู…ุนุงู„ุฌุชู‡ ุงู„ุขู†)."""
43
+ return _current_display.get()
44
+
45
+ # ู†ุทุงู‚ ุฃุฑู‚ุงู… ุงู„ุดุงุดุงุช ุงู„ุงูุชุฑุงุถูŠุฉ ุงู„ุฏูŠู†ุงู…ูŠูƒูŠุฉ ุงู„ู…ุฎุตุตุฉ ู„ูƒู„ ุฌู„ุณุฉ โ€” ุชุจุฏุฃ ู…ู† 100 ู„ุชุฌู†ุจ ุงู„ุชุนุงุฑุถ
46
+ # ู…ุน ุฃูŠ DISPLAY ุฃุณุงุณูŠ ุซุงุจุช (:0, :1, :99) ู‚ุฏ ูŠูƒูˆู† ู…ุณุชุฎุฏูŽู…ุงู‹ ุฎุงุฑุฌูŠุงู‹.
47
+ _next_display_num = 100
48
+ _display_lock = asyncio.Lock()
49
+ # session_id (id(ws)) โ†’ {"display": ":N", "xvfb_proc": Popen, "firefox_proc": Popen, "num": N}
50
+ _active_sessions: dict[int, dict] = {}
51
+ _freed_display_nums: list[int] = [] # ุฃุฑู‚ุงู… ุดุงุดุงุช ุชุญุฑุฑุช ูŠู…ูƒู† ุฅุนุงุฏุฉ ุงุณุชุฎุฏุงู…ู‡ุง
52
+
53
+ async def _allocate_display_num() -> int:
54
+ """ูŠุญุฌุฒ ุฑู‚ู… ุดุงุดุฉ ูุฑูŠุฏ ู„ุฌู„ุณุฉ ุฌุฏูŠุฏุฉ โ€” ูŠุนูŠุฏ ุงุณุชุฎุฏุงู… ุฃุฑู‚ุงู… ู…ุญุฑูŽู‘ุฑุฉ ู…ู† ุฌู„ุณุงุช ู…ู†ุชู‡ูŠุฉ ุฃูˆู„ุงู‹."""
55
+ global _next_display_num
56
+ async with _display_lock:
57
+ if _freed_display_nums:
58
+ return _freed_display_nums.pop()
59
+ n = _next_display_num
60
+ _next_display_num += 1
61
+ return n
62
+
63
+ async def _start_session_display(num: int) -> tuple[str, "subprocess.Popen | None"]:
64
+ """ูŠูุดุบู‘ู„ Xvfb ุฌุฏูŠุฏุงู‹ ุชู…ุงู…ุงู‹ ุนู„ู‰ ุงู„ุฑู‚ู… ุงู„ู…ุญุฏุฏ ูˆูŠูุนูŠุฏ (display_string, process)."""
65
+ disp = f":{num}"
66
+
67
+ def _spawn():
68
+ try:
69
+ return subprocess.Popen(
70
+ ["Xvfb", disp, "-screen", "0", "1920x1080x24", "-nolisten", "tcp"],
71
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
72
+ )
73
+ except FileNotFoundError:
74
+ return None # Xvfb ุบูŠุฑ ู…ุซุจุช ููŠ ู‡ุฐู‡ ุงู„ุจูŠุฆุฉ
75
+
76
+ proc = await asyncio.to_thread(_spawn)
77
+ if proc is None:
78
+ # Xvfb ุบูŠุฑ ู…ุชุงุญ โ€” ุฑุฌูˆุน ุขู…ู† ู„ู€ DISPLAY ุงู„ุฃุณุงุณูŠ (ุจู„ุง ุนุฒู„ุŒ ู„ูƒู† ุจู„ุง ุงู†ู‡ูŠุงุฑ)
79
+ print("[session] โš ๏ธ Xvfb binary not found โ€” falling back to shared base display (no isolation)")
80
+ return _BASE_DISPLAY, None
81
+ # ุงู†ุชุธุงุฑ ู‚ุตูŠุฑ ุญุชู‰ ูŠุตุจุญ X server ุฌุงู‡ุฒุงู‹ ูุนู„ูŠุงู‹ ู‚ุจู„ ุงุณุชุฎุฏุงู…ู‡
82
+ def _check_ready():
83
+ try:
84
+ return subprocess.run(
85
+ ["xdpyinfo", "-display", disp], capture_output=True, timeout=2
86
+ ).returncode
87
+ except FileNotFoundError:
88
+ return None # xdpyinfo ุบูŠุฑ ู…ุซุจุช โ€” ู„ุง ูŠู…ูƒู† ุงู„ุชุญู‚ู‚ุŒ ุณู†ู†ุชุธุฑ ู…ู‡ู„ุฉ ุซุงุจุชุฉ ูู‚ุท
89
+ for _ in range(30):
90
+ await asyncio.sleep(0.1)
91
+ rc = await asyncio.to_thread(_check_ready)
92
+ if rc is None:
93
+ await asyncio.sleep(0.5) # ู…ู‡ู„ุฉ ุงุญุชูŠุงุทูŠุฉ ุซุงุจุชุฉ ุจุฏู„ ุงู„ุชุญู‚ู‚ ุงู„ุฏู‚ูŠู‚
94
+ break
95
+ if rc == 0:
96
+ break
97
+ return disp, proc
98
+
99
+ async def _start_session_browser(disp: str) -> "subprocess.Popen | None":
100
+ """ูŠูุดุบู‘ู„ ู…ุชุตูุญ Firefox ุฎุงุต ุจู‡ุฐู‡ ุงู„ุฌู„ุณุฉ ุนู„ู‰ ุงู„ุดุงุดุฉ ุงู„ุงูุชุฑุงุถูŠุฉ ุงู„ุฎุงุตุฉ ุจู‡ุง."""
101
+ env = {**os.environ, "DISPLAY": disp}
102
+
103
+ def _spawn():
104
+ try:
105
+ return subprocess.Popen(
106
+ [BROWSER, "--new-window", "--no-remote", "about:blank"],
107
+ env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
108
+ )
109
+ except Exception as e:
110
+ print(f"[session] โš ๏ธ failed to launch browser on {disp}: {e}")
111
+ return None
112
+ proc = await asyncio.to_thread(_spawn)
113
+ return proc
114
+
115
+ async def create_session_for(ws: "WebSocket") -> str:
116
+ """
117
+ ูŠูู†ุดุฆ ุจูŠุฆุฉ ูƒู…ุจูŠูˆุชุฑ ุงูุชุฑุงุถูŠ ู…ุนุฒูˆู„ุฉ ุชู…ุงู…ุงู‹ (Xvfb + ู…ุชุตูุญ ุฎุงุตูŠู†) ู„ู‡ุฐุง ุงู„ุงุชุตุงู„ุŒ
118
+ ูˆูŠูุนูŠุฏ ุณู„ุณู„ุฉ DISPLAY ุงู„ุฎุงุตุฉ ุจู‡ุง (ู…ุซู„ ':103'). ูŠู๏ฟฝ๏ฟฝุชุฏุนู‰ ุนู†ุฏ ูุชุญ ุฃูŠ ุงุชุตุงู„ WebSocket ุฌุฏูŠุฏ.
119
+ """
120
+ num = await _allocate_display_num()
121
+ disp, xvfb_proc = await _start_session_display(num)
122
+ if xvfb_proc is None:
123
+ # ุฑุฌุนู†ุง ู„ู€ DISPLAY ุงู„ุฃุณุงุณูŠ ุงู„ู…ุดุชุฑูƒ (Xvfb ุบูŠุฑ ู…ุชุงุญ) โ€” ู„ุง ุญุงุฌุฉ ู„ู…ุชุตูุญ ุฌุฏูŠุฏ ู…ู†ูุตู„ุŒ
124
+ # ุงู„ู…ุชุตูุญ ุงู„ุฃุณุงุณูŠ (ุฅู† ูˆูุฌุฏ ุฎุงุฑุฌูŠุงู‹) ูŠูƒููŠุŒ ูˆู„ุง ุฏุงุนูŠ ู„ุชุชุจุน ุฌู„ุณุฉ ูุนู„ูŠุฉ ู„ู‡ุฐุง ุงู„ุงุชุตุงู„.
125
+ _freed_display_nums.append(num)
126
+ return disp
127
+ firefox_proc = await _start_session_browser(disp)
128
+ _active_sessions[id(ws)] = {
129
+ "display": disp, "num": num,
130
+ "xvfb_proc": xvfb_proc, "firefox_proc": firefox_proc,
131
+ }
132
+ print(f"[session] โœ… created isolated session display={disp} for ws={id(ws)} (total active: {len(_active_sessions)})")
133
+ return disp
134
+
135
+ async def destroy_session_for(ws: "WebSocket"):
136
+ """ูŠูุบู„ู‚ ูˆูŠูุญุฑู‘ุฑ ุจูŠุฆุฉ ุงู„ูƒู…ุจูŠูˆุชุฑ ุงู„ุงูุชุฑุงุถูŠ ุงู„ุฎุงุตุฉ ุจู‡ุฐุง ุงู„ุงุชุตุงู„ ุจุงู„ูƒุงู…ู„ ุนู†ุฏ ุงู†ู‚ุทุงุนู‡."""
137
+ sess = _active_sessions.pop(id(ws), None)
138
+ if not sess:
139
+ return
140
+
141
+ def _kill_all():
142
+ for key in ("firefox_proc", "xvfb_proc"):
143
+ p = sess.get(key)
144
+ if p:
145
+ try:
146
+ p.terminate()
147
+ try:
148
+ p.wait(timeout=3)
149
+ except Exception:
150
+ p.kill()
151
+ except Exception:
152
+ pass
153
+ # ุชุฃูƒูŠุฏ ุฅุถุงููŠ: ู‚ุชู„ ุฃูŠ ุนู…ู„ูŠุฉ ู…ุชุจู‚ูŠุฉ ู…ุฑุชุจุทุฉ ุจู‡ุฐุง DISPLAY ุจุงู„ุชุญุฏูŠุฏ ูู‚ุท
154
+ disp = sess.get("display", "")
155
+ if disp:
156
+ try:
157
+ subprocess.run(
158
+ ["pkill", "-9", "-f", f"DISPLAY={disp}"],
159
+ capture_output=True, timeout=3
160
+ )
161
+ except Exception:
162
+ pass
163
+
164
+ await asyncio.to_thread(_kill_all)
165
+ _freed_display_nums.append(sess["num"])
166
+ print(f"[session] ๐Ÿ—‘๏ธ destroyed session display={sess['display']} for ws={id(ws)} (remaining active: {len(_active_sessions)})")
167
 
168
  # โ”€โ”€โ”€ ุชุดุฎูŠุต ุฃุฏูˆุงุช ุงู„ู€ capture ุนู†ุฏ ุงู„ุจุฏุงูŠุฉ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
169
  def _check_capture_tools():
170
+ tools = ["scrot", "import", "xwd", "convert", "ffmpeg", "xdotool", "Xvfb", "xdpyinfo"]
171
  print("โ”€โ”€โ”€ Capture Tools Check โ”€โ”€โ”€")
172
  for t in tools:
173
  r = subprocess.run(["which", t], capture_output=True, text=True)
174
  status = "โœ…" if r.returncode == 0 else "โŒ"
175
  print(f" {status} {t}: {r.stdout.strip() or 'not found'}")
176
  try:
177
+ r2 = subprocess.run(["xdpyinfo", "-display", _BASE_DISPLAY],
178
  capture_output=True, text=True, timeout=3)
179
+ print(f" {'โœ…' if r2.returncode==0 else 'โŒ'} DISPLAY={_BASE_DISPLAY}: {'active' if r2.returncode==0 else 'not active'}")
180
  except Exception as ex:
181
+ print(f" โŒ DISPLAY={_BASE_DISPLAY}: {ex}")
182
  print("โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€")
183
 
184
  try:
 
321
  # โ”€โ”€โ”€ Screen Capture โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
322
 
323
  def _get_screen_size() -> tuple[int, int]:
324
+ """ูŠูุนูŠุฏ ุญุฌู… ุงู„ุดุงุดุฉ ุงู„ุญู‚ูŠู‚ูŠ (ู„ุดุงุดุฉ ุงู„ุฌู„ุณุฉ ุงู„ุญุงู„ูŠุฉ)"""
325
  r = subprocess.run(["xdotool", "getdisplaygeometry"],
326
+ env={**os.environ, "DISPLAY": DISPLAY_OF()},
327
  capture_output=True, text=True, timeout=5)
328
  try:
329
  parts = r.stdout.strip().split()
 
333
 
334
 
335
  def _get_mouse_pos() -> tuple[int, int]:
336
+ """ูŠูุนูŠุฏ ู…ูˆู‚ุน ุงู„ู…ุงูˆุณ ุงู„ุญุงู„ูŠ (ู„ุดุงุดุฉ ุงู„ุฌู„ุณุฉ ุงู„ุญุงู„ูŠุฉ)"""
337
  r = subprocess.run(["xdotool", "getmouselocation"],
338
+ env={**os.environ, "DISPLAY": DISPLAY_OF()},
339
  capture_output=True, text=True, timeout=5)
340
  try:
341
  mx = int(re.search(r"x:(\d+)", r.stdout).group(1))
 
357
  5. python-xlib / Xlib ู…ุจุงุดุฑุฉ
358
  """
359
  from PIL import Image
360
+ disp = DISPLAY_OF()
361
+ env = {**os.environ, "DISPLAY": disp}
362
  tmp = f"/tmp/zs_{int(time.time()*1000)}"
363
  _tmp_files_to_clean = [] # ูƒู„ ุงู„ู…ู„ูุงุช ุงู„ู…ุคู‚ุชุฉ ุงู„ุชูŠ ุฃูู†ุดุฆุช ููŠ ู‡ุฐู‡ ุงู„ู…ุญุงูˆู„ุฉ
364
 
 
452
  "ffmpeg", "-y",
453
  "-f", "x11grab",
454
  "-video_size", f"{sw}x{sh}",
455
+ "-i", disp,
456
  "-vframes", "1",
457
  "-q:v", "2",
458
  p
 
469
  try:
470
  from Xlib import display as Xdisplay
471
  from Xlib.ext.xtest import fake_input
472
+ xdisp = Xdisplay.Display(disp)
473
  root = xdisp.screen().root
474
  geom = root.get_geometry()
475
  w, h = geom.width, geom.height
 
497
  img = Image.new("RGB", (sw or 1280, sh or 720), (20, 20, 30))
498
  draw = ImageDraw.Draw(img)
499
  draw.rectangle([(0, 0), (sw, 40)], fill=(30, 30, 50))
500
+ draw.text((10, 10), f"โš ๏ธ Screenshot failed โ€” DISPLAY={disp}", fill=(255, 100, 100))
501
  draw.text((10, 50), "Methods tried: scrot, import, xwd, ffmpeg, xlib", fill=(150, 150, 150))
502
  return img, sw or 1280, sh or 720
503
  except:
 
649
 
650
  def run_raw_command(cmd: str, timeout: int = 60) -> dict:
651
  cmd = _sanitize_pkill_self_match(cmd)
652
+ env = {**os.environ, "DISPLAY": DISPLAY_OF(),
653
  "PYTHONIOENCODING": "utf-8", "LANG": "en_US.UTF-8"}
654
  try:
655
  result = subprocess.run(cmd, shell=True, capture_output=True,
 
729
 
730
  async def xdo(args: list, timeout=10) -> dict:
731
  """ูŠู†ูู‘ุฐ xdotool ููŠ thread ู…ู†ูุตู„ ุญุชู‰ ู„ุง ูŠูุฌู…ู‘ุฏ ุญู„ู‚ุฉ asyncio ุฃุซู†ุงุก ุงู„ุงู†ุชุธุงุฑ."""
732
+ disp = DISPLAY_OF()
733
  def _run():
734
  r = subprocess.run(["xdotool"] + args,
735
+ env={**os.environ, "DISPLAY": disp},
736
  timeout=timeout, capture_output=True, text=True)
737
  return {"rc": r.returncode, "out": r.stdout, "err": r.stderr}
738
  return await asyncio.to_thread(_run)
 
749
  if has_arabic:
750
  # ุทุฑูŠู‚ุฉ Clipboard ู„ุถู…ุงู† ูƒุชุงุจุฉ ุงู„ุนุฑุจูŠุฉ ุจุดูƒู„ ุตุญูŠุญ
751
  try:
752
+ disp = DISPLAY_OF()
753
  def _xclip_write():
754
  proc = subprocess.Popen(
755
  ["xclip", "-selection", "clipboard"],
756
  stdin=subprocess.PIPE,
757
+ env={**os.environ, "DISPLAY": disp}
758
  )
759
  proc.communicate(text.encode("utf-8"))
760
  await asyncio.to_thread(_xclip_write)
 
781
  # ุฌุฑุจ ุฃูˆู„ุงู‹: BROWSER ุงู„ุนุงุฏูŠ
782
  cmd = f"{browser_cmd} --new-window '{url}' &"
783
  proc = subprocess.Popen(cmd, shell=True,
784
+ env={**os.environ, "DISPLAY": DISPLAY_OF()},
785
  stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
786
  return f"{browser_cmd} โ†’ {url}"
787
 
 
1059
  elif action == "clipboard_write":
1060
  text = data.get("text", "")
1061
  try:
1062
+ disp = DISPLAY_OF()
1063
  def _xclip_write2():
1064
  proc = subprocess.Popen(
1065
  ["xclip", "-selection", "clipboard"],
1066
  stdin=subprocess.PIPE,
1067
+ env={**os.environ, "DISPLAY": disp}
1068
  )
1069
  proc.communicate(text.encode("utf-8"))
1070
  await asyncio.to_thread(_xclip_write2)
 
1101
  fixed_cmd = app_cmd.replace("firefox-esr", BROWSER)
1102
 
1103
  subprocess.Popen(fixed_cmd, shell=True,
1104
+ env={**os.environ, "DISPLAY": DISPLAY_OF()},
1105
  stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
1106
  await send({"type": "ack", "action": "open_app", "cmd": fixed_cmd})
1107
 
 
1157
  elif action == "paste":
1158
  text = data.get("text", "")
1159
  if text:
1160
+ disp = DISPLAY_OF()
1161
  def _xclip_paste():
1162
  proc = subprocess.Popen(
1163
  ["xclip", "-selection", "clipboard"],
1164
  stdin=subprocess.PIPE,
1165
+ env={**os.environ, "DISPLAY": disp}
1166
  )
1167
  proc.communicate(text.encode("utf-8"))
1168
  await asyncio.to_thread(_xclip_paste)
 
1185
  async def websocket_endpoint(ws: WebSocket):
1186
  await ws.accept()
1187
  active_connections.append(ws)
1188
+
1189
+ # โ”€โ”€ ุนุฒู„ ูƒุงู…ู„: ุฅู†ุดุงุก Xvfb + ู…ุชุตูุญ ุฎุงุตูŠู† ุจู‡ุฐุง ุงู„ุงุชุตุงู„ ูู‚ุท โ”€โ”€
1190
+ # ูƒู„ ุงุชุตุงู„ WebSocket ูŠุญุตู„ ุนู„ู‰ ุจูŠุฆุฉ ูƒู…ุจูŠูˆุชุฑ ุงูุชุฑุงุถูŠ ู…ุณุชู‚ู„ุฉ ุชู…ุงู…ุงู‹ ุนู† ุฃูŠ ุงุชุตุงู„ ุขุฎุฑุŒ
1191
+ # ุจุญูŠุซ ู„ุง ูŠุฑู‰ ุฃูŠ ู…ุณุชุฎุฏู… ุฃูŠ ุฃุซุฑ ู…ู† ู†ุดุงุท ู…ุณุชุฎุฏู… ุขุฎุฑ ู…ู‡ู…ุง ูƒุงู†ูˆุง ู…ุชุตู„ูŠู† ููŠ ู†ูุณ ุงู„ู„ุญุธุฉ.
1192
+ session_display = await create_session_for(ws)
1193
+ _current_display.set(session_display) # ูŠูุฑุจุท ุจู€ task ู‡ุฐุง ุงู„ุงุชุตุงู„ โ€” ู„ุง ูŠุคุซุฑ ุนู„ู‰ ุงุชุตุงู„ุงุช ุฃุฎุฑู‰
1194
+
1195
+ w, h = await asyncio.to_thread(_get_screen_size)
1196
  await ws.send_text(json.dumps({
1197
  "type": "connected",
1198
  "screen_width": w,
1199
  "screen_height": h,
1200
  "browser": BROWSER,
1201
+ "display": session_display,
1202
+ "msg": f"Z Computer Mode v7 โ€” Smart Control | Browser: {BROWSER} | Screen: {w}x{h} | Isolated session: {session_display}",
1203
  "commands": [
1204
  "screenshot", "terminal {cmd}", "mouse_move {x,y}", "mouse_click {x,y,button}",
1205
  "keyboard_type {text}", "keyboard_hotkey {keys}", "scroll {x,y,clicks}",
 
1207
  "Simple cmds: 'open firefox', 'mouse go x y', 'click x y', 'type TEXT', 'key enter'"
1208
  ]
1209
  }, ensure_ascii=False))
1210
+ # ุฃุฑุณู„ screenshot ุฃูˆู„ูŠุฉ ู…ุน grid (ู„ุดุงุดุฉ ู‡ุฐู‡ ุงู„ุฌู„ุณุฉ ุงู„ู…ุนุฒูˆู„ุฉ ูู‚ุท)
1211
  result = await asyncio.to_thread(capture_screen_with_grid, scale=0.65, quality=72)
1212
  if result["data"]:
1213
  await ws.send_text(json.dumps({
 
1232
  finally:
1233
  if ws in active_connections:
1234
  active_connections.remove(ws)
1235
+ # โ”€โ”€ ุชุญุฑูŠุฑ ุจูŠุฆุฉ ู‡ุฐู‡ ุงู„ุฌู„ุณุฉ ุจุงู„ูƒุงู…ู„ (Xvfb + ุงู„ู…ุชุตูุญ) ููˆุฑุงู‹ ุนู†ุฏ ุงู†ู‚ุทุงุน ุงู„ุงุชุตุงู„ โ”€โ”€
1236
+ await destroy_session_for(ws)
1237
 
1238
 
1239
  # โ”€โ”€โ”€ REST โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€