Spaces:
Paused
Paused
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,40 +1,48 @@
|
|
| 1 |
"""
|
| 2 |
-
THE Z AI — Computer Mode Server
|
| 3 |
-
====================================================
|
| 4 |
-
ال
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
"""
|
| 23 |
|
| 24 |
import asyncio
|
| 25 |
import base64
|
| 26 |
-
import contextvars
|
| 27 |
import hashlib
|
| 28 |
import io
|
| 29 |
import json
|
| 30 |
import os
|
| 31 |
import re
|
| 32 |
import subprocess
|
| 33 |
-
import tempfile
|
| 34 |
import time
|
| 35 |
import urllib.parse
|
| 36 |
-
import uuid
|
| 37 |
-
from pathlib import Path
|
| 38 |
|
| 39 |
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
| 40 |
from fastapi.middleware.cors import CORSMiddleware
|
|
@@ -52,15 +60,7 @@ os.environ["DISPLAY"] = SHARED_DISPLAY
|
|
| 52 |
# semaphore للتحكم في عدد عمليات terminal المتوازية
|
| 53 |
_terminal_sem = asyncio.Semaphore(4)
|
| 54 |
|
| 55 |
-
#
|
| 56 |
-
_tmp_lock = asyncio.Lock()
|
| 57 |
-
|
| 58 |
-
# ── contextvar للـ profile الخاص بالجلسة ──
|
| 59 |
-
_current_profile: contextvars.ContextVar[str] = contextvars.ContextVar(
|
| 60 |
-
"_current_profile", default=""
|
| 61 |
-
)
|
| 62 |
-
|
| 63 |
-
# ── تخزين الجلسات: session_id → {profile_dir, last_screenshot_ts, last_frame_hash} ──
|
| 64 |
_active_sessions: dict[int, dict] = {}
|
| 65 |
_sessions_lock = asyncio.Lock()
|
| 66 |
|
|
@@ -138,30 +138,25 @@ print(f"🌐 Browser: {BROWSER}")
|
|
| 138 |
|
| 139 |
async def create_session(ws: WebSocket) -> dict:
|
| 140 |
"""
|
| 141 |
-
ينشئ
|
| 142 |
-
|
| 143 |
-
لمنع التداخل بين جلسات متعددة على نفس DISPLAY.
|
| 144 |
"""
|
| 145 |
sid = id(ws)
|
| 146 |
-
profile_dir = f"/tmp/zpc_{sid}"
|
| 147 |
-
os.makedirs(profile_dir, exist_ok=True)
|
| 148 |
-
|
| 149 |
sess = {
|
| 150 |
"id": sid,
|
| 151 |
-
"
|
| 152 |
-
"
|
| 153 |
-
"last_frame_hash": "",
|
| 154 |
"browser_proc": None,
|
| 155 |
"created": time.time(),
|
| 156 |
}
|
| 157 |
async with _sessions_lock:
|
| 158 |
_active_sessions[sid] = sess
|
| 159 |
-
print(f"[session] ✅ Created session {sid}
|
| 160 |
return sess
|
| 161 |
|
| 162 |
|
| 163 |
async def destroy_session(ws: WebSocket):
|
| 164 |
-
"""يح
|
| 165 |
sid = id(ws)
|
| 166 |
async with _sessions_lock:
|
| 167 |
sess = _active_sessions.pop(sid, None)
|
|
@@ -169,7 +164,6 @@ async def destroy_session(ws: WebSocket):
|
|
| 169 |
return
|
| 170 |
|
| 171 |
def _cleanup():
|
| 172 |
-
# أغلق المتصفح إذا كان مفتوحاً
|
| 173 |
bp = sess.get("browser_proc")
|
| 174 |
if bp:
|
| 175 |
try:
|
|
@@ -180,27 +174,15 @@ async def destroy_session(ws: WebSocket):
|
|
| 180 |
bp.kill()
|
| 181 |
except Exception:
|
| 182 |
pass
|
| 183 |
-
# احذف profile
|
| 184 |
-
import shutil
|
| 185 |
-
try:
|
| 186 |
-
shutil.rmtree(sess["profile"], ignore_errors=True)
|
| 187 |
-
except Exception:
|
| 188 |
-
pass
|
| 189 |
|
| 190 |
await asyncio.to_thread(_cleanup)
|
| 191 |
print(f"[session] 🗑️ Destroyed session {sid}")
|
| 192 |
|
| 193 |
|
| 194 |
-
def get_browser_cmd(url: str = ""
|
| 195 |
-
"""يبني أمر فتح المتصفح
|
| 196 |
-
cmd = [BROWSER]
|
| 197 |
-
if
|
| 198 |
-
cmd += ["--profile", profile]
|
| 199 |
-
cmd += ["--no-remote"]
|
| 200 |
-
if url:
|
| 201 |
-
cmd += [url]
|
| 202 |
-
else:
|
| 203 |
-
cmd += ["about:blank"]
|
| 204 |
return cmd
|
| 205 |
|
| 206 |
|
|
@@ -224,9 +206,8 @@ def _is_black_screen(img) -> bool:
|
|
| 224 |
|
| 225 |
def _capture_raw() -> tuple:
|
| 226 |
"""
|
| 227 |
-
يلتقط الشاشة بـ 4 طرق متسلسلة.
|
| 228 |
يُعيد (PIL.Image | None, width, height).
|
| 229 |
-
ملاحظة: كل ملفات /tmp تُحذف دائماً (finally block).
|
| 230 |
"""
|
| 231 |
from PIL import Image
|
| 232 |
env = {**os.environ, "DISPLAY": SHARED_DISPLAY}
|
|
@@ -257,93 +238,96 @@ def _capture_raw() -> tuple:
|
|
| 257 |
except Exception:
|
| 258 |
pass
|
| 259 |
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
|
|
|
| 264 |
try:
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 277 |
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
except Exception as e:
|
| 332 |
-
print(f"[cap] xlib: {e}")
|
| 333 |
-
|
| 334 |
-
# ── Fallback: placeholder ────────────────────────────
|
| 335 |
-
print("[cap] ⚠️ All methods failed or black — returning placeholder")
|
| 336 |
-
from PIL import Image as PILImg, ImageDraw
|
| 337 |
-
sw, sh = _get_screen_size()
|
| 338 |
-
img = PILImg.new("RGB", (sw or 1920, sh or 1080), (15, 15, 25))
|
| 339 |
-
draw = ImageDraw.Draw(img)
|
| 340 |
-
draw.rectangle([(0, 0), (sw, 50)], fill=(30, 30, 60))
|
| 341 |
-
draw.text((10, 15), f"⚠️ Screenshot failed — DISPLAY={SHARED_DISPLAY}", fill=(255, 100, 100))
|
| 342 |
-
draw.text((10, 35), f"Methods tried: scrot, import, ffmpeg, xlib", fill=(120, 120, 120))
|
| 343 |
-
return img, sw or 1920, sh or 1080
|
| 344 |
|
| 345 |
-
|
| 346 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 347 |
|
| 348 |
|
| 349 |
def _get_screen_size() -> tuple:
|
|
@@ -663,29 +647,33 @@ async def handle_action(ws: WebSocket, msg: dict, sess: dict):
|
|
| 663 |
except Exception:
|
| 664 |
pass
|
| 665 |
|
| 666 |
-
async def
|
| 667 |
-
|
| 668 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 669 |
await asyncio.sleep(delay)
|
| 670 |
|
| 671 |
-
# Rate limit: لا أكثر من لقطة كل 0.3 ثانية
|
| 672 |
now = time.time()
|
| 673 |
-
if now - sess.get("
|
| 674 |
-
await asyncio.sleep(0.3 - (now - sess["
|
| 675 |
|
| 676 |
result = await asyncio.to_thread(
|
| 677 |
capture_with_grid, 0.65, 72, force_mx, force_my
|
| 678 |
)
|
| 679 |
-
sess["
|
| 680 |
|
| 681 |
if not result["data"]:
|
| 682 |
return
|
| 683 |
|
| 684 |
# Delta suppression: لا ترسل إذا الصورة لم تتغير
|
| 685 |
fh = _frame_hash(result["data"])
|
| 686 |
-
if fh == sess.get("
|
| 687 |
-
return
|
| 688 |
-
sess["
|
| 689 |
|
| 690 |
await send({
|
| 691 |
"type": "screenshot",
|
|
@@ -700,14 +688,23 @@ async def handle_action(ws: WebSocket, msg: dict, sess: dict):
|
|
| 700 |
"has_grid": True,
|
| 701 |
})
|
| 702 |
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
|
|
|
|
|
|
|
|
|
| 706 |
result = await asyncio.to_thread(capture_with_grid, 0.65, 75)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 707 |
await send({
|
| 708 |
"type": "screenshot",
|
| 709 |
"data": result["data"],
|
| 710 |
"ts": int(time.time() * 1000),
|
|
|
|
|
|
|
| 711 |
"screen_width": result["width"],
|
| 712 |
"screen_height": result["height"],
|
| 713 |
"mouse_x": result["mouse_x"],
|
|
@@ -715,6 +712,10 @@ async def handle_action(ws: WebSocket, msg: dict, sess: dict):
|
|
| 715 |
"has_grid": True,
|
| 716 |
})
|
| 717 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 718 |
# ── terminal ─────────────────────────────────────────────
|
| 719 |
elif action == "terminal":
|
| 720 |
cmd = data.get("cmd", "")
|
|
@@ -729,14 +730,15 @@ async def handle_action(ws: WebSocket, msg: dict, sess: dict):
|
|
| 729 |
"stderr": res.get("stderr", ""),
|
| 730 |
"returncode": res["returncode"],
|
| 731 |
})
|
| 732 |
-
|
|
|
|
| 733 |
|
| 734 |
# ── mouse_move ───────────────────────────────────────────
|
| 735 |
elif action == "mouse_move":
|
| 736 |
x, y = int(data.get("x", 0)), int(data.get("y", 0))
|
| 737 |
await xdo(["mousemove", "--sync", str(x), str(y)])
|
| 738 |
await send({"type": "ack", "action": "mouse_move", "x": x, "y": y})
|
| 739 |
-
asyncio.create_task(
|
| 740 |
|
| 741 |
# ── mouse_click ──────────────────────────────────────────
|
| 742 |
elif action == "mouse_click":
|
|
@@ -751,7 +753,7 @@ async def handle_action(ws: WebSocket, msg: dict, sess: dict):
|
|
| 751 |
await xdo(["click", btn])
|
| 752 |
btn_name = {"1": "left", "2": "middle", "3": "right"}.get(btn, "left")
|
| 753 |
await send({"type": "ack", "action": "mouse_click", "x": x, "y": y, "button": btn_name})
|
| 754 |
-
asyncio.create_task(
|
| 755 |
|
| 756 |
# ── mouse_drag ───────────────────────────────────────────
|
| 757 |
elif action == "mouse_drag":
|
|
@@ -764,7 +766,7 @@ async def handle_action(ws: WebSocket, msg: dict, sess: dict):
|
|
| 764 |
await asyncio.sleep(0.1)
|
| 765 |
await xdo(["mouseup", "1"])
|
| 766 |
await send({"type": "ack", "action": "mouse_drag"})
|
| 767 |
-
asyncio.create_task(
|
| 768 |
|
| 769 |
# ── keyboard_type ────────────────────────────────────────
|
| 770 |
elif action == "keyboard_type":
|
|
@@ -772,7 +774,7 @@ async def handle_action(ws: WebSocket, msg: dict, sess: dict):
|
|
| 772 |
if text:
|
| 773 |
res = await type_smart(text)
|
| 774 |
await send({"type": "ack", "action": "keyboard_type", "method": res["method"]})
|
| 775 |
-
asyncio.create_task(
|
| 776 |
|
| 777 |
# ── keyboard_hotkey ──────────────────────────────────────
|
| 778 |
elif action == "keyboard_hotkey":
|
|
@@ -780,7 +782,7 @@ async def handle_action(ws: WebSocket, msg: dict, sess: dict):
|
|
| 780 |
if keys:
|
| 781 |
await xdo(["key", "--clearmodifiers", "+".join(keys)])
|
| 782 |
await send({"type": "ack", "action": "keyboard_hotkey", "keys": keys})
|
| 783 |
-
asyncio.create_task(
|
| 784 |
|
| 785 |
# ── keyboard_press ───────────────────────────────────────
|
| 786 |
elif action == "keyboard_press":
|
|
@@ -788,7 +790,7 @@ async def handle_action(ws: WebSocket, msg: dict, sess: dict):
|
|
| 788 |
if key:
|
| 789 |
await xdo(["key", "--clearmodifiers", key])
|
| 790 |
await send({"type": "ack", "action": "keyboard_press"})
|
| 791 |
-
asyncio.create_task(
|
| 792 |
|
| 793 |
# ── scroll ───────────────────────────────────────────────
|
| 794 |
elif action == "scroll":
|
|
@@ -800,7 +802,7 @@ async def handle_action(ws: WebSocket, msg: dict, sess: dict):
|
|
| 800 |
await xdo(["click", btn])
|
| 801 |
await asyncio.sleep(0.025)
|
| 802 |
await send({"type": "ack", "action": "scroll", "clicks": clicks})
|
| 803 |
-
asyncio.create_task(
|
| 804 |
|
| 805 |
# ── clipboard_write ──────────────────────────────────────
|
| 806 |
elif action == "clipboard_write":
|
|
@@ -829,7 +831,7 @@ async def handle_action(ws: WebSocket, msg: dict, sess: dict):
|
|
| 829 |
await asyncio.sleep(0.1)
|
| 830 |
await xdo(["key", "--clearmodifiers", "ctrl+v"])
|
| 831 |
await send({"type": "ack", "action": "paste"})
|
| 832 |
-
asyncio.create_task(
|
| 833 |
|
| 834 |
# ── open_app ─────────────────────────────────────────────
|
| 835 |
elif action == "open_app":
|
|
@@ -838,15 +840,14 @@ async def handle_action(ws: WebSocket, msg: dict, sess: dict):
|
|
| 838 |
await send({"type": "ack", "action": "open_app"})
|
| 839 |
return
|
| 840 |
|
| 841 |
-
#
|
| 842 |
-
profile = sess.get("profile", "")
|
| 843 |
-
if profile and any(b in cmd for b in ("firefox", "chromium")):
|
| 844 |
-
if "--profile" not in cmd:
|
| 845 |
-
cmd = cmd.replace(BROWSER, f"{BROWSER} --profile {profile}", 1)
|
| 846 |
-
|
| 847 |
-
# نظّف lock files
|
| 848 |
if "firefox" in cmd.lower():
|
| 849 |
-
await run_cmd(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 850 |
|
| 851 |
env = {**os.environ, "DISPLAY": SHARED_DISPLAY}
|
| 852 |
proc = subprocess.Popen(
|
|
@@ -857,22 +858,34 @@ async def handle_action(ws: WebSocket, msg: dict, sess: dict):
|
|
| 857 |
sess["browser_proc"] = proc
|
| 858 |
|
| 859 |
await send({"type": "ack", "action": "open_app", "cmd": cmd})
|
| 860 |
-
|
| 861 |
-
|
| 862 |
-
|
|
|
|
|
|
|
|
|
|
| 863 |
|
| 864 |
# ── open_browser ─────────────────────────────────────────
|
| 865 |
elif action == "open_browser":
|
| 866 |
url = _safe_search(data.get("url", "") or "about:blank")
|
| 867 |
-
|
| 868 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 869 |
env = {**os.environ, "DISPLAY": SHARED_DISPLAY}
|
| 870 |
-
await run_cmd("rm -f ~/.mozilla/firefox/*/lock ~/.mozilla/firefox/*/.parentlock 2>/dev/null", 3)
|
| 871 |
proc = subprocess.Popen(cmd, env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
| 872 |
sess["browser_proc"] = proc
|
| 873 |
await send({"type": "ack", "action": "open_browser", "url": url})
|
| 874 |
-
|
| 875 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 876 |
|
| 877 |
# ── open_tab ─────────────────────────────────────────────
|
| 878 |
elif action == "open_tab":
|
|
@@ -885,25 +898,25 @@ async def handle_action(ws: WebSocket, msg: dict, sess: dict):
|
|
| 885 |
await asyncio.sleep(0.2)
|
| 886 |
await xdo(["key", "--clearmodifiers", "Return"])
|
| 887 |
await send({"type": "ack", "action": "open_tab", "url": url})
|
| 888 |
-
asyncio.create_task(
|
| 889 |
|
| 890 |
# ── close_tab ────────────────────────────────────────────
|
| 891 |
elif action == "close_tab":
|
| 892 |
await xdo(["key", "--clearmodifiers", "ctrl+w"])
|
| 893 |
await send({"type": "ack", "action": "close_tab"})
|
| 894 |
-
asyncio.create_task(
|
| 895 |
|
| 896 |
# ── browser_back ─────────────────────────────────────────
|
| 897 |
elif action == "browser_back":
|
| 898 |
await xdo(["key", "--clearmodifiers", "alt+Left"])
|
| 899 |
await send({"type": "ack", "action": "browser_back"})
|
| 900 |
-
asyncio.create_task(
|
| 901 |
|
| 902 |
# ── browser_forward ───────────���──────────────────────────
|
| 903 |
elif action == "browser_forward":
|
| 904 |
await xdo(["key", "--clearmodifiers", "alt+Right"])
|
| 905 |
await send({"type": "ack", "action": "browser_forward"})
|
| 906 |
-
asyncio.create_task(
|
| 907 |
|
| 908 |
# ── browser_search ───────────────────────────────────────
|
| 909 |
elif action == "browser_search":
|
|
@@ -914,7 +927,7 @@ async def handle_action(ws: WebSocket, msg: dict, sess: dict):
|
|
| 914 |
await asyncio.sleep(0.15)
|
| 915 |
await xdo(["key", "--clearmodifiers", "Return"])
|
| 916 |
await send({"type": "ack", "action": "browser_search"})
|
| 917 |
-
asyncio.create_task(
|
| 918 |
|
| 919 |
# ── screen_info ──────────────────────────────────────────
|
| 920 |
elif action == "screen_info":
|
|
@@ -963,10 +976,10 @@ async def websocket_endpoint(ws: WebSocket):
|
|
| 963 |
"browser": BROWSER,
|
| 964 |
"display": SHARED_DISPLAY,
|
| 965 |
"session_id": id(ws),
|
| 966 |
-
"msg": f"Z Computer Mode
|
| 967 |
}, ensure_ascii=False))
|
| 968 |
|
| 969 |
-
# لقطة شاشة أولية
|
| 970 |
result = await asyncio.to_thread(capture_with_grid, 0.65, 72)
|
| 971 |
if result["data"]:
|
| 972 |
await ws.send_text(json.dumps({
|
|
@@ -980,7 +993,7 @@ async def websocket_endpoint(ws: WebSocket):
|
|
| 980 |
"mouse_y": result["mouse_y"],
|
| 981 |
"has_grid": True,
|
| 982 |
}, ensure_ascii=False))
|
| 983 |
-
sess["
|
| 984 |
|
| 985 |
except Exception as e:
|
| 986 |
print(f"[ws] init error: {e}")
|
|
@@ -1052,15 +1065,10 @@ async def _cleanup_tmp():
|
|
| 1052 |
while True:
|
| 1053 |
await asyncio.sleep(300)
|
| 1054 |
try:
|
| 1055 |
-
|
| 1056 |
["find", "/tmp", "-name", "zss_*", "-mmin", "+10", "-delete"],
|
| 1057 |
capture_output=True, timeout=10
|
| 1058 |
)
|
| 1059 |
-
r2 = subprocess.run(
|
| 1060 |
-
["find", "/tmp", "-name", "zpc_*", "-mmin", "+60",
|
| 1061 |
-
"-not", "-newer", "/tmp", "-delete"],
|
| 1062 |
-
capture_output=True, timeout=10
|
| 1063 |
-
)
|
| 1064 |
except Exception as e:
|
| 1065 |
print(f"[cleanup] {e}")
|
| 1066 |
|
|
|
|
| 1 |
"""
|
| 2 |
+
THE Z AI — Computer Mode Server v10 — SCREENSHOT RACE-CONDITION FIXED
|
| 3 |
+
=======================================================================
|
| 4 |
+
المشاكل التي تسبب "لم تصل لقطة شاشة" — وجدناها وأصلحناها:
|
| 5 |
+
|
| 6 |
+
🐛 BUG 1 — Delta Suppression يكتم Screenshot المطلوبة صراحةً:
|
| 7 |
+
في v9، دالة shot() كانت تتحقق من hash الصورة السابقة وتُلغي الإرسال إذا
|
| 8 |
+
"لم تتغير الصورة" — لكن الـ HTML يطلب screenshot بشكل صريح ويستظهر وصولها
|
| 9 |
+
بمقارنة _pcLastFrameTs. إذا الشاشة لم تتغير (مثلاً بعد keyboard_hotkey)،
|
| 10 |
+
السيرفر يُلغي الإرسال → الـ HTML ينتظر إلى ما لا نهاية → "لم تصل لقطة شاشة".
|
| 11 |
+
الإصلاح: Delta suppression تنطبق على auto_shot فقط (الخلفية)، وليس على
|
| 12 |
+
طلبات screenshot الصريحة (action == "screenshot").
|
| 13 |
+
|
| 14 |
+
🐛 BUG 2 — auto_shot كـ background task يتأخر أو يُلغى:
|
| 15 |
+
asyncio.create_task(shot_bg(...)) كانت تُنشئ task في الخلفية قد يتأخر تنفيذها
|
| 16 |
+
لأن asyncio يُجدولها لاحقاً — الـ HTML يطلب screenshot ثم ينتظر 12 ثانية
|
| 17 |
+
لكن الـ auto_shot قد لم يُرسل بعد بسبب تزاحم الـ tasks.
|
| 18 |
+
الإصلاح: الأوامر الصريحة (screenshot action) تُجيب فوراً بـ await، لا create_task.
|
| 19 |
+
فقط auto_shot الخلفي (بعد mouse_move مثلاً) يبقى كـ create_task.
|
| 20 |
+
|
| 21 |
+
🐛 BUG 3 — Rate Limit 0.3s يمنع الرد الفوري:
|
| 22 |
+
الـ HTML يطلب screenshot ثم ينتظر — لكن السيرفر كان ينتظر 0.3 ثانية أولاً
|
| 23 |
+
بسبب rate limiting. مع تكدس الطلبات، التأخر يتراكم → timeout → "لم تصل".
|
| 24 |
+
الإصلاح: Rate limit تنطبق على auto_shot الخلفي فقط، لا على الطلبات الصريحة.
|
| 25 |
+
|
| 26 |
+
✅ إصلاحات إضافية من v9 (تبقى):
|
| 27 |
+
- حذف --profile (كان يسبب "Firefox profile cannot be loaded")
|
| 28 |
+
- Xvfb واحد ثابت مشترك
|
| 29 |
+
- screenshot retry x3 إذا الصورة سوداء
|
| 30 |
+
- Heartbeat ping/pong كل 20 ثانية
|
| 31 |
+
- terminal semaphore(4)
|
| 32 |
+
- SafeSearch على كل URL
|
| 33 |
+
- تنظيف /tmp كل 5 دقائق
|
| 34 |
"""
|
| 35 |
|
| 36 |
import asyncio
|
| 37 |
import base64
|
|
|
|
| 38 |
import hashlib
|
| 39 |
import io
|
| 40 |
import json
|
| 41 |
import os
|
| 42 |
import re
|
| 43 |
import subprocess
|
|
|
|
| 44 |
import time
|
| 45 |
import urllib.parse
|
|
|
|
|
|
|
| 46 |
|
| 47 |
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
| 48 |
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
| 60 |
# semaphore للتحكم في عدد عمليات terminal المتوازية
|
| 61 |
_terminal_sem = asyncio.Semaphore(4)
|
| 62 |
|
| 63 |
+
# ── تخزين الجلسات: session_id → {last_bg_shot_ts, last_bg_hash, browser_proc} ──
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
_active_sessions: dict[int, dict] = {}
|
| 65 |
_sessions_lock = asyncio.Lock()
|
| 66 |
|
|
|
|
| 138 |
|
| 139 |
async def create_session(ws: WebSocket) -> dict:
|
| 140 |
"""
|
| 141 |
+
ينشئ session بسيطة لهذا المستخدم.
|
| 142 |
+
لا profile منفصل — Firefox يعمل بـ --no-remote فقط للعزل.
|
|
|
|
| 143 |
"""
|
| 144 |
sid = id(ws)
|
|
|
|
|
|
|
|
|
|
| 145 |
sess = {
|
| 146 |
"id": sid,
|
| 147 |
+
"last_bg_shot_ts": 0.0,
|
| 148 |
+
"last_bg_hash": "",
|
|
|
|
| 149 |
"browser_proc": None,
|
| 150 |
"created": time.time(),
|
| 151 |
}
|
| 152 |
async with _sessions_lock:
|
| 153 |
_active_sessions[sid] = sess
|
| 154 |
+
print(f"[session] ✅ Created session {sid}")
|
| 155 |
return sess
|
| 156 |
|
| 157 |
|
| 158 |
async def destroy_session(ws: WebSocket):
|
| 159 |
+
"""يحرر موارد الجلسة عند انقطاع الاتصال."""
|
| 160 |
sid = id(ws)
|
| 161 |
async with _sessions_lock:
|
| 162 |
sess = _active_sessions.pop(sid, None)
|
|
|
|
| 164 |
return
|
| 165 |
|
| 166 |
def _cleanup():
|
|
|
|
| 167 |
bp = sess.get("browser_proc")
|
| 168 |
if bp:
|
| 169 |
try:
|
|
|
|
| 174 |
bp.kill()
|
| 175 |
except Exception:
|
| 176 |
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
|
| 178 |
await asyncio.to_thread(_cleanup)
|
| 179 |
print(f"[session] 🗑️ Destroyed session {sid}")
|
| 180 |
|
| 181 |
|
| 182 |
+
def get_browser_cmd(url: str = "") -> list:
|
| 183 |
+
"""يبني أمر فتح المتصفح بدون profile — --no-remote يمنع التداخل."""
|
| 184 |
+
cmd = [BROWSER, "--no-remote"]
|
| 185 |
+
cmd.append(url if url else "about:blank")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
return cmd
|
| 187 |
|
| 188 |
|
|
|
|
| 206 |
|
| 207 |
def _capture_raw() -> tuple:
|
| 208 |
"""
|
| 209 |
+
يلتقط الشاشة بـ 4 طرق متسلسلة مع retry إذا الصورة سوداء.
|
| 210 |
يُعيد (PIL.Image | None, width, height).
|
|
|
|
| 211 |
"""
|
| 212 |
from PIL import Image
|
| 213 |
env = {**os.environ, "DISPLAY": SHARED_DISPLAY}
|
|
|
|
| 238 |
except Exception:
|
| 239 |
pass
|
| 240 |
|
| 241 |
+
# نحاول 3 مرات إذا الصورة سوداء (مثلاً: Firefox لا يزال يتحمّل)
|
| 242 |
+
for attempt in range(3):
|
| 243 |
+
if attempt > 0:
|
| 244 |
+
time.sleep(0.8)
|
| 245 |
+
print(f"[cap] retry attempt {attempt+1} (black screen)")
|
| 246 |
try:
|
| 247 |
+
# ── Method 1: scrot ──────────────────────────────
|
| 248 |
+
p1 = f"{tmp_base}_a{attempt}_scrot.png"
|
| 249 |
+
created_files.append(p1)
|
| 250 |
+
try:
|
| 251 |
+
r = subprocess.run(
|
| 252 |
+
["scrot", "-q", "95", p1],
|
| 253 |
+
env=env, timeout=8, capture_output=True
|
| 254 |
+
)
|
| 255 |
+
img, w, h = _try_load(p1)
|
| 256 |
+
if img and not _is_black_screen(img):
|
| 257 |
+
print(f"[cap] ✅ scrot (attempt {attempt+1})")
|
| 258 |
+
return img, w, h
|
| 259 |
+
if img:
|
| 260 |
+
print(f"[cap] scrot: black screen (attempt {attempt+1})")
|
| 261 |
+
except Exception as e:
|
| 262 |
+
print(f"[cap] scrot: {e}")
|
| 263 |
|
| 264 |
+
# ── Method 2: ImageMagick import ─────────────────
|
| 265 |
+
p2 = f"{tmp_base}_a{attempt}_im.png"
|
| 266 |
+
created_files.append(p2)
|
| 267 |
+
try:
|
| 268 |
+
subprocess.run(
|
| 269 |
+
["import", "-window", "root", "-silent", p2],
|
| 270 |
+
env=env, timeout=10, capture_output=True
|
| 271 |
+
)
|
| 272 |
+
img, w, h = _try_load(p2)
|
| 273 |
+
if img and not _is_black_screen(img):
|
| 274 |
+
print(f"[cap] ✅ import (attempt {attempt+1})")
|
| 275 |
+
return img, w, h
|
| 276 |
+
except Exception as e:
|
| 277 |
+
print(f"[cap] import: {e}")
|
| 278 |
|
| 279 |
+
# ── Method 3: ffmpeg x11grab ──────────────────────
|
| 280 |
+
p3 = f"{tmp_base}_a{attempt}_ff.png"
|
| 281 |
+
created_files.append(p3)
|
| 282 |
+
try:
|
| 283 |
+
sw, sh = _get_screen_size()
|
| 284 |
+
subprocess.run([
|
| 285 |
+
"ffmpeg", "-y", "-f", "x11grab",
|
| 286 |
+
"-video_size", f"{sw}x{sh}",
|
| 287 |
+
"-i", SHARED_DISPLAY,
|
| 288 |
+
"-vframes", "1", "-q:v", "1", p3
|
| 289 |
+
], env=env, timeout=12, capture_output=True)
|
| 290 |
+
img, w, h = _try_load(p3)
|
| 291 |
+
if img and not _is_black_screen(img):
|
| 292 |
+
print(f"[cap] ✅ ffmpeg (attempt {attempt+1})")
|
| 293 |
+
return img, w, h
|
| 294 |
+
except Exception as e:
|
| 295 |
+
print(f"[cap] ffmpeg: {e}")
|
| 296 |
|
| 297 |
+
# ── Method 4: python-xlib ─────────────────────────
|
| 298 |
+
try:
|
| 299 |
+
from Xlib import display as Xdisp, X
|
| 300 |
+
xd = Xdisp.Display(SHARED_DISPLAY)
|
| 301 |
+
root = xd.screen().root
|
| 302 |
+
geom = root.get_geometry()
|
| 303 |
+
w, h = geom.width, geom.height
|
| 304 |
+
raw = root.get_image(0, 0, w, h, X.ZPixmap, 0xFFFFFFFF)
|
| 305 |
+
data = raw.data
|
| 306 |
+
pixels = bytearray(len(data) // 4 * 3)
|
| 307 |
+
for i in range(0, len(data) - 3, 4):
|
| 308 |
+
b, g, r_c = data[i], data[i+1], data[i+2]
|
| 309 |
+
j = (i // 4) * 3
|
| 310 |
+
pixels[j], pixels[j+1], pixels[j+2] = r_c, g, b
|
| 311 |
+
img = Image.frombytes("RGB", (w, h), bytes(pixels))
|
| 312 |
+
if not _is_black_screen(img):
|
| 313 |
+
print(f"[cap] ✅ xlib (attempt {attempt+1})")
|
| 314 |
+
return img, w, h
|
| 315 |
+
except Exception as e:
|
| 316 |
+
print(f"[cap] xlib: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 317 |
|
| 318 |
+
finally:
|
| 319 |
+
_cleanup_files()
|
| 320 |
+
|
| 321 |
+
# إذا فشلت كل المحاولات — placeholder واضح
|
| 322 |
+
print("[cap] ⚠️ All methods failed or black — placeholder")
|
| 323 |
+
from PIL import Image as PILImg, ImageDraw
|
| 324 |
+
sw, sh = _get_screen_size()
|
| 325 |
+
img = PILImg.new("RGB", (sw or 1920, sh or 1080), (15, 20, 40))
|
| 326 |
+
draw = ImageDraw.Draw(img)
|
| 327 |
+
draw.rectangle([(0, 0), (sw, 55)], fill=(30, 40, 80))
|
| 328 |
+
draw.text((10, 10), f"⚠️ Screenshot failed — DISPLAY={SHARED_DISPLAY}", fill=(255, 120, 80))
|
| 329 |
+
draw.text((10, 32), "Methods tried: scrot, import, ffmpeg, xlib (3 attempts each)", fill=(130, 130, 150))
|
| 330 |
+
return img, sw or 1920, sh or 1080
|
| 331 |
|
| 332 |
|
| 333 |
def _get_screen_size() -> tuple:
|
|
|
|
| 647 |
except Exception:
|
| 648 |
pass
|
| 649 |
|
| 650 |
+
async def shot_bg(label: str = "", delay: float = 0.5,
|
| 651 |
+
force_mx: int | None = None, force_my: int | None = None):
|
| 652 |
+
"""
|
| 653 |
+
auto_shot في الخلفية — فقط للمعاينة البصرية (بعد mouse/keyboard/scroll).
|
| 654 |
+
يطبّق: rate limit + delta suppression.
|
| 655 |
+
لا يُرسل إذا الصورة لم تتغير — المستخدم يرى التحديث عند الطلب الصريح.
|
| 656 |
+
"""
|
| 657 |
await asyncio.sleep(delay)
|
| 658 |
|
| 659 |
+
# Rate limit: لا أكثر من لقطة كل 0.3 ثانية لنفس الجلسة
|
| 660 |
now = time.time()
|
| 661 |
+
if now - sess.get("last_bg_shot_ts", 0) < 0.3:
|
| 662 |
+
await asyncio.sleep(0.3 - (now - sess["last_bg_shot_ts"]))
|
| 663 |
|
| 664 |
result = await asyncio.to_thread(
|
| 665 |
capture_with_grid, 0.65, 72, force_mx, force_my
|
| 666 |
)
|
| 667 |
+
sess["last_bg_shot_ts"] = time.time()
|
| 668 |
|
| 669 |
if not result["data"]:
|
| 670 |
return
|
| 671 |
|
| 672 |
# Delta suppression: لا ترسل إذا الصورة لم تتغير
|
| 673 |
fh = _frame_hash(result["data"])
|
| 674 |
+
if fh == sess.get("last_bg_hash", ""):
|
| 675 |
+
return # لم تتغير — لا ترسل
|
| 676 |
+
sess["last_bg_hash"] = fh
|
| 677 |
|
| 678 |
await send({
|
| 679 |
"type": "screenshot",
|
|
|
|
| 688 |
"has_grid": True,
|
| 689 |
})
|
| 690 |
|
| 691 |
+
async def shot_explicit(label: str = ""):
|
| 692 |
+
"""
|
| 693 |
+
screenshot صريح — يُرسل دائماً بدون delta suppression ولا rate limit.
|
| 694 |
+
يُستخدم عند action == "screenshot" أو بعد terminal/open_app.
|
| 695 |
+
الـ HTML ينتظر هذا الرد بمقارنة _pcLastFrameTs — يجب أن يصل دائماً.
|
| 696 |
+
"""
|
| 697 |
result = await asyncio.to_thread(capture_with_grid, 0.65, 75)
|
| 698 |
+
if not result["data"]:
|
| 699 |
+
return
|
| 700 |
+
# حدّث hash الخلفي أيضاً لمنع إرسال نفس الصورة مرتين في auto_shot
|
| 701 |
+
sess["last_bg_hash"] = _frame_hash(result["data"])
|
| 702 |
await send({
|
| 703 |
"type": "screenshot",
|
| 704 |
"data": result["data"],
|
| 705 |
"ts": int(time.time() * 1000),
|
| 706 |
+
"auto": False,
|
| 707 |
+
"label": label,
|
| 708 |
"screen_width": result["width"],
|
| 709 |
"screen_height": result["height"],
|
| 710 |
"mouse_x": result["mouse_x"],
|
|
|
|
| 712 |
"has_grid": True,
|
| 713 |
})
|
| 714 |
|
| 715 |
+
# ── screenshot (صريح من الـ HTML — يُرسل دائماً بدون تأخير) ──────
|
| 716 |
+
if action == "screenshot":
|
| 717 |
+
await shot_explicit("explicit screenshot")
|
| 718 |
+
|
| 719 |
# ── terminal ─────────────────────────────────────────────
|
| 720 |
elif action == "terminal":
|
| 721 |
cmd = data.get("cmd", "")
|
|
|
|
| 730 |
"stderr": res.get("stderr", ""),
|
| 731 |
"returncode": res["returncode"],
|
| 732 |
})
|
| 733 |
+
# screenshot صريح بعد terminal — الـ HTML ينتظره
|
| 734 |
+
asyncio.create_task(shot_explicit(f"after terminal"))
|
| 735 |
|
| 736 |
# ── mouse_move ───────────────────────────────────────────
|
| 737 |
elif action == "mouse_move":
|
| 738 |
x, y = int(data.get("x", 0)), int(data.get("y", 0))
|
| 739 |
await xdo(["mousemove", "--sync", str(x), str(y)])
|
| 740 |
await send({"type": "ack", "action": "mouse_move", "x": x, "y": y})
|
| 741 |
+
asyncio.create_task(shot_bg(f"move ({x},{y})", delay=0.2, force_mx=x, force_my=y))
|
| 742 |
|
| 743 |
# ── mouse_click ──────────────────────────────────────────
|
| 744 |
elif action == "mouse_click":
|
|
|
|
| 753 |
await xdo(["click", btn])
|
| 754 |
btn_name = {"1": "left", "2": "middle", "3": "right"}.get(btn, "left")
|
| 755 |
await send({"type": "ack", "action": "mouse_click", "x": x, "y": y, "button": btn_name})
|
| 756 |
+
asyncio.create_task(shot_bg(f"click {btn_name} ({x},{y})", delay=0.5, force_mx=x, force_my=y))
|
| 757 |
|
| 758 |
# ── mouse_drag ───────────────────────────────────────────
|
| 759 |
elif action == "mouse_drag":
|
|
|
|
| 766 |
await asyncio.sleep(0.1)
|
| 767 |
await xdo(["mouseup", "1"])
|
| 768 |
await send({"type": "ack", "action": "mouse_drag"})
|
| 769 |
+
asyncio.create_task(shot_bg("after drag", delay=0.4, force_mx=x2, force_my=y2))
|
| 770 |
|
| 771 |
# ── keyboard_type ────────────────────────────────────────
|
| 772 |
elif action == "keyboard_type":
|
|
|
|
| 774 |
if text:
|
| 775 |
res = await type_smart(text)
|
| 776 |
await send({"type": "ack", "action": "keyboard_type", "method": res["method"]})
|
| 777 |
+
asyncio.create_task(shot_bg("after type", delay=0.5))
|
| 778 |
|
| 779 |
# ── keyboard_hotkey ──────────────────────────────────────
|
| 780 |
elif action == "keyboard_hotkey":
|
|
|
|
| 782 |
if keys:
|
| 783 |
await xdo(["key", "--clearmodifiers", "+".join(keys)])
|
| 784 |
await send({"type": "ack", "action": "keyboard_hotkey", "keys": keys})
|
| 785 |
+
asyncio.create_task(shot_bg("after hotkey", delay=0.4))
|
| 786 |
|
| 787 |
# ── keyboard_press ───────────────────────────────────────
|
| 788 |
elif action == "keyboard_press":
|
|
|
|
| 790 |
if key:
|
| 791 |
await xdo(["key", "--clearmodifiers", key])
|
| 792 |
await send({"type": "ack", "action": "keyboard_press"})
|
| 793 |
+
asyncio.create_task(shot_bg("after key", delay=0.4))
|
| 794 |
|
| 795 |
# ── scroll ───────────────────────────────────────────────
|
| 796 |
elif action == "scroll":
|
|
|
|
| 802 |
await xdo(["click", btn])
|
| 803 |
await asyncio.sleep(0.025)
|
| 804 |
await send({"type": "ack", "action": "scroll", "clicks": clicks})
|
| 805 |
+
asyncio.create_task(shot_bg("after scroll", delay=0.4))
|
| 806 |
|
| 807 |
# ── clipboard_write ──────────────────────────────────────
|
| 808 |
elif action == "clipboard_write":
|
|
|
|
| 831 |
await asyncio.sleep(0.1)
|
| 832 |
await xdo(["key", "--clearmodifiers", "ctrl+v"])
|
| 833 |
await send({"type": "ack", "action": "paste"})
|
| 834 |
+
asyncio.create_task(shot_bg("after paste", delay=0.5))
|
| 835 |
|
| 836 |
# ── open_app ─────────────────────────────────────────────
|
| 837 |
elif action == "open_app":
|
|
|
|
| 840 |
await send({"type": "ack", "action": "open_app"})
|
| 841 |
return
|
| 842 |
|
| 843 |
+
# نظّف lock files قبل فتح Firefox (يمنع "already running" dialog)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 844 |
if "firefox" in cmd.lower():
|
| 845 |
+
await run_cmd(
|
| 846 |
+
"pkill -f '[f]irefox' 2>/dev/null; sleep 0.5; "
|
| 847 |
+
"rm -f ~/.mozilla/firefox/*/lock ~/.mozilla/firefox/*/.parentlock 2>/dev/null; "
|
| 848 |
+
"echo CLEANED",
|
| 849 |
+
timeout=8
|
| 850 |
+
)
|
| 851 |
|
| 852 |
env = {**os.environ, "DISPLAY": SHARED_DISPLAY}
|
| 853 |
proc = subprocess.Popen(
|
|
|
|
| 858 |
sess["browser_proc"] = proc
|
| 859 |
|
| 860 |
await send({"type": "ack", "action": "open_app", "cmd": cmd})
|
| 861 |
+
async def _open_app_shots():
|
| 862 |
+
await asyncio.sleep(3.0)
|
| 863 |
+
await shot_explicit("app opening 3s")
|
| 864 |
+
await asyncio.sleep(5.0)
|
| 865 |
+
await shot_explicit("app loaded 8s")
|
| 866 |
+
asyncio.create_task(_open_app_shots())
|
| 867 |
|
| 868 |
# ── open_browser ─────────────────────────────────────────
|
| 869 |
elif action == "open_browser":
|
| 870 |
url = _safe_search(data.get("url", "") or "about:blank")
|
| 871 |
+
# نظّف أي Firefox قديم أولاً
|
| 872 |
+
await run_cmd(
|
| 873 |
+
"pkill -f '[f]irefox' 2>/dev/null; sleep 0.5; "
|
| 874 |
+
"rm -f ~/.mozilla/firefox/*/lock ~/.mozilla/firefox/*/.parentlock 2>/dev/null; "
|
| 875 |
+
"echo CLEANED",
|
| 876 |
+
timeout=8
|
| 877 |
+
)
|
| 878 |
+
cmd = get_browser_cmd(url)
|
| 879 |
env = {**os.environ, "DISPLAY": SHARED_DISPLAY}
|
|
|
|
| 880 |
proc = subprocess.Popen(cmd, env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
| 881 |
sess["browser_proc"] = proc
|
| 882 |
await send({"type": "ack", "action": "open_browser", "url": url})
|
| 883 |
+
async def _open_browser_shots():
|
| 884 |
+
await asyncio.sleep(3.0)
|
| 885 |
+
await shot_explicit("browser opening 3s")
|
| 886 |
+
await asyncio.sleep(6.0)
|
| 887 |
+
await shot_explicit("browser loaded 9s")
|
| 888 |
+
asyncio.create_task(_open_browser_shots())
|
| 889 |
|
| 890 |
# ── open_tab ─────────────────────────────────────────────
|
| 891 |
elif action == "open_tab":
|
|
|
|
| 898 |
await asyncio.sleep(0.2)
|
| 899 |
await xdo(["key", "--clearmodifiers", "Return"])
|
| 900 |
await send({"type": "ack", "action": "open_tab", "url": url})
|
| 901 |
+
asyncio.create_task(shot_bg("after open_tab", delay=1.5))
|
| 902 |
|
| 903 |
# ── close_tab ────────────────────────────────────────────
|
| 904 |
elif action == "close_tab":
|
| 905 |
await xdo(["key", "--clearmodifiers", "ctrl+w"])
|
| 906 |
await send({"type": "ack", "action": "close_tab"})
|
| 907 |
+
asyncio.create_task(shot_bg("after close_tab", delay=0.5))
|
| 908 |
|
| 909 |
# ── browser_back ─────────────────────────────────────────
|
| 910 |
elif action == "browser_back":
|
| 911 |
await xdo(["key", "--clearmodifiers", "alt+Left"])
|
| 912 |
await send({"type": "ack", "action": "browser_back"})
|
| 913 |
+
asyncio.create_task(shot_bg("after back", delay=0.8))
|
| 914 |
|
| 915 |
# ── browser_forward ───────────���──────────────────────────
|
| 916 |
elif action == "browser_forward":
|
| 917 |
await xdo(["key", "--clearmodifiers", "alt+Right"])
|
| 918 |
await send({"type": "ack", "action": "browser_forward"})
|
| 919 |
+
asyncio.create_task(shot_bg("after forward", delay=0.8))
|
| 920 |
|
| 921 |
# ── browser_search ───────────────────────────────────────
|
| 922 |
elif action == "browser_search":
|
|
|
|
| 927 |
await asyncio.sleep(0.15)
|
| 928 |
await xdo(["key", "--clearmodifiers", "Return"])
|
| 929 |
await send({"type": "ack", "action": "browser_search"})
|
| 930 |
+
asyncio.create_task(shot_bg("after search", delay=1.5))
|
| 931 |
|
| 932 |
# ── screen_info ──────────────────────────────────────────
|
| 933 |
elif action == "screen_info":
|
|
|
|
| 976 |
"browser": BROWSER,
|
| 977 |
"display": SHARED_DISPLAY,
|
| 978 |
"session_id": id(ws),
|
| 979 |
+
"msg": f"Z Computer Mode v10 | Browser: {BROWSER} | Screen: {w}x{h}",
|
| 980 |
}, ensure_ascii=False))
|
| 981 |
|
| 982 |
+
# لقطة شاشة أولية — دائماً تُرسل بغض النظر عن أي شيء
|
| 983 |
result = await asyncio.to_thread(capture_with_grid, 0.65, 72)
|
| 984 |
if result["data"]:
|
| 985 |
await ws.send_text(json.dumps({
|
|
|
|
| 993 |
"mouse_y": result["mouse_y"],
|
| 994 |
"has_grid": True,
|
| 995 |
}, ensure_ascii=False))
|
| 996 |
+
sess["last_bg_hash"] = _frame_hash(result["data"])
|
| 997 |
|
| 998 |
except Exception as e:
|
| 999 |
print(f"[ws] init error: {e}")
|
|
|
|
| 1065 |
while True:
|
| 1066 |
await asyncio.sleep(300)
|
| 1067 |
try:
|
| 1068 |
+
subprocess.run(
|
| 1069 |
["find", "/tmp", "-name", "zss_*", "-mmin", "+10", "-delete"],
|
| 1070 |
capture_output=True, timeout=10
|
| 1071 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1072 |
except Exception as e:
|
| 1073 |
print(f"[cleanup] {e}")
|
| 1074 |
|