Spaces:
Paused
Paused
Update app.py
Browse files
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
|
| 29 |
-
os.environ
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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",
|
| 41 |
capture_output=True, text=True, timeout=3)
|
| 42 |
-
print(f" {'โ
' if r2.returncode==0 else 'โ'} DISPLAY={
|
| 43 |
except Exception as ex:
|
| 44 |
-
print(f" โ DISPLAY={
|
| 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":
|
| 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":
|
| 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 |
-
|
|
|
|
| 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",
|
| 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(
|
| 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={
|
| 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":
|
| 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":
|
| 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":
|
| 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":
|
| 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":
|
| 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":
|
| 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":
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1047 |
await ws.send_text(json.dumps({
|
| 1048 |
"type": "connected",
|
| 1049 |
"screen_width": w,
|
| 1050 |
"screen_height": h,
|
| 1051 |
"browser": BROWSER,
|
| 1052 |
-
"
|
|
|
|
| 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 โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|