Spaces:
Running
Running
| """ | |
| THE Z AI โ Computer Mode Server v11 โ PER-USER ISOLATED DISPLAYS | |
| ================================================================= | |
| ูู ู ุณุชุฎุฏู ูุฏูู: | |
| โ Display Xvfb ุฎุงุต (:100, :101, :102 ...) | |
| โ ู ุชุตูุญ Firefox ู ุนุฒูู ุชู ุงู ุงู ูุง ูุชุดุงุฑู ู ุน ุฃู ู ุณุชุฎุฏู ุขุฎุฑ | |
| โ ููุทุงุช ุดุงุดุฉ ู ุณุชููุฉ โ ู ุง ูุฑุงู ู ุณุชุฎุฏู A ูุง ูุฎุชูุท ุจู ุณุชุฎุฏู B | |
| โ ุชุงุฑูุฎ terminal ู ุณุชูู ููู ู ุณุชุฎุฏู | |
| โ ุนูุฏ ูู ุงุชุตุงู ุฌุฏูุฏ ููู ุณุชุฎุฏู : ููุณ ุงูู display (ูุง ููุนุงุฏ ุฅูุดุงุก Xvfb) | |
| โ ุนูุฏ ุทูุจ ุฑูุณุชุงุฑุช / ูู ุจููุชุฑ ุฌุฏูุฏ: Xvfb + Firefox ููุนุงุฏุงู ุชู ุงู ุงู | |
| ุงูููุฑุฉ: | |
| - ูู user_id (email) โฆ display ุฑูู ุซุงุจุช (ู ุซูุงู :100 ููุฃููุ :101 ููุซุงูู) | |
| - ุฅุฐุง ุงูู ุณุชุฎุฏู ูุชุตู ู ุฌุฏุฏุงูุ ููุนูุฏ ุงุณุชุฎุฏุงู ููุณ ุงูู display ุงูุฎุงุต ุจู | |
| - ุฅุฐุง ุทูุจ ุฑูุณุชุงุฑุชุ ููุชู ุงูู Xvfb ูุงูู ุชุตูุญ ูููุนูุฏ ุฅูุดุงุกูู ุง | |
| - ุฅุฐุง ุงูู ุณุชุฎุฏู ุฌุฏูุฏุ ูุฎุตุต ูู display ุฌุฏูุฏ ูููุดุบูู Xvfb ูู | |
| """ | |
| import asyncio | |
| import base64 | |
| import hashlib | |
| import io | |
| import json | |
| import os | |
| import re | |
| import shutil | |
| import subprocess | |
| import sys | |
| import time | |
| import urllib.parse | |
| import threading | |
| from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Query | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.responses import JSONResponse, HTMLResponse | |
| import uvicorn | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโ ุฅุนุฏุงุฏ Display Pool โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # ุจุฏุงูุฉ ูุทุงู ุงูู displays ุงูุงูุชุฑุงุถูุฉ (ูู ูู ุชุบููุฑู) | |
| DISPLAY_BASE = 100 | |
| DISPLAY_MAX = 200 # ุฃูุตู ุนุฏุฏ ู ุณุชุฎุฏู ูู ู ุชุฒุงู ููู | |
| _display_lock = threading.Lock() | |
| # user_id โ display_info | |
| # display_info = { | |
| # "display": ":101", | |
| # "xvfb_proc": subprocess.Popen | None, | |
| # "browser_proc": subprocess.Popen | None, | |
| # "last_bg_shot_ts": float, | |
| # "last_bg_hash": str, | |
| # "active_ws": WebSocket | None, # ุงูุงุชุตุงู ุงูุญุงูู ููุฐุง ุงูู ุณุชุฎุฏู | |
| # } | |
| _user_displays: dict[str, dict] = {} | |
| _display_numbers: set[int] = set() # ุฃุฑูุงู ุงูู displays ุงูู ุณุชุฎุฏู ุฉ | |
| # semaphore ููุชุญูู ูู ุนู ููุงุช terminal (global ููู ุงูุณูุฑูุฑ) | |
| _terminal_sem = asyncio.Semaphore(8) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโ ุงูุชุดุงู ุงูู ุชุตูุญ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def _detect_browser() -> str: | |
| for c in ["firefox", "firefox-esr", "chromium-browser", "chromium", "google-chrome"]: | |
| r = subprocess.run(["which", c], capture_output=True, text=True) | |
| if r.returncode == 0 and r.stdout.strip(): | |
| return c | |
| return "firefox" | |
| BROWSER = _detect_browser() | |
| print(f"๐ Browser: {BROWSER}") | |
| # โโ CLI flags ุซุงุจุชุฉ ุชูุถุงู ููู ุชุดุบูู Firefox โโ | |
| # ุชุณุฑูุน ุงูุฅููุงุน (ูุง GPU ููุฒูุงุฆู ุญูููู ูู Xvfb ููุง ุฏุงุนู ูู ุญุงููุฉ | |
| # ุงูุชุณุฑูุน ุงูู ุฑุฆู ุงูุฐู ููุดู ุฏุงุฆู ุงู ููุง ููุณุจูุจ ุชุฃุฎูุฑุงู ูู ุงูุฅููุงุน)ุ | |
| # ูุชู ูุน ุฃู nag screens ุฃู crash-reporter windows ู ู ุงูุธููุฑ. | |
| FIREFOX_CLI_FLAGS = ["--no-remote", "--new-instance"] | |
| FIREFOX_ENV_EXTRA = { | |
| # ูู ูุน ู ุญุงููุงุช ุชุณุฑูุน GPU ุบูุฑ ุงูู ุชููุฑุฉ ูู Xvfb ู ู ุฅุจุทุงุก ุงูุฅููุงุน | |
| "MOZ_DISABLE_GPU_SANDBOX": "1", | |
| "MOZ_ACCELERATED": "0", | |
| } | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโ Firefox Profile (per-user, isolated, crash-recovery DISABLED) โ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # ุงูู ุดููุฉ ุงูุฌุฐุฑูุฉ ุงูุชู ูุฐุง ุงููุณู ูุญูููุง: | |
| # ุนูุฏ ูุชู Firefox (ุญุชู ุจู pkill "ูุทูู")ุ ูุง ููุบูู ุจุดูู ูุธูู ุฏุงุฆู ุงู ุนูู | |
| # ุณูุฑูุฑ ุจู ูุงุฑุฏ ู ุญุฏูุฏุฉ/Xvfbุ ูุชูุณุฌููู ุงูุฌูุณุฉ ุนูู ุฃููุง "crashed" ุฏุงุฎู | |
| # sessionstore.jsonlz4 ุงูุฎุงุต ุจุงูุจุฑููุงูู. ูู ุงูู ุฑุฉ ุงูุชุงููุฉ ููุชุญ Firefox | |
| # ููุนุฑุถ ุดุงุดุฉ "Sorry. We're having trouble getting your pages back" ุจุฏู | |
| # ุงูุตูุญุฉ ุงูู ุทููุจุฉ โ ููุฐู ูู ุงูุดุงุดุฉ ุงูุณูุฏุงุก/ุงููุงุดูุฉ ุงูุชู ุชุตู ูููุทุฉ "ูุงุฌุญุฉ" | |
| # ุชูููุงู (ููุณุช ุณูุฏุงุก ูุนูุงู) ููููุง ูุง ุชุญุชูู ุนูู ุงูู ุญุชูู ุงูู ุทููุจ ุฃุจุฏุงู. | |
| # ุงูุญู ุงูุฌุฐุฑู: ุจุฑููุงูู ุฎุงุต ุจูู ู ุณุชุฎุฏู (isolated) ู ุน user.js ูุนุทูู ุชู ุงู ุงู: | |
| # - ุงุณุชุฑุฌุงุน ุงูุฌูุณุฉ ุจุนุฏ ุงูุชุญุทูู (session restore prompt) | |
| # - ุฃู ู ุญุงููุฉ ูุฅุนุงุฏุฉ ูุชุญ ุชุจููุจุงุช ุณุงุจูุฉ | |
| # ุจูุฐุงุ ูู ูุชุญ ููุงูุฑูููุณ ูุจุฏุฃ ุตูุญุฉ ูุธููุฉ ูุงุฑุบุฉ ุฏุงุฆู ุงูุ ุจุบุถ ุงููุธุฑ ุนู ููููุฉ | |
| # ุฅุบูุงูู ุณุงุจูุงู. | |
| FIREFOX_PROFILE_PREFS = """ | |
| // โโ ุชุนุทูู ุงุณุชุฑุฌุงุน ุงูุฌูุณุฉ ููุงุฆูุงู (ุณุจุจ ุดุงุดุฉ "Restore Session") โโ | |
| user_pref("browser.sessionstore.resume_from_crash", false); | |
| user_pref("browser.sessionstore.resume_session_once", false); | |
| user_pref("browser.sessionstore.max_resumed_crashes", -1); | |
| user_pref("browser.sessionstore.restore_on_demand", false); | |
| user_pref("browser.sessionstore.enabled", false); | |
| user_pref("browser.sessionstore.privacy_level", 2); | |
| user_pref("browser.startup.page", 0); | |
| user_pref("browser.startup.homepage_override.mstone", "ignore"); | |
| user_pref("toolkit.startup.max_resumed_crashes", -1); | |
| // ุชุนุทูู ูุงูุฐุฉ "Restore previous session" ูููุงูุฐ ุงูุฃุฒู ุงุช ุงูู ุฎุชููุฉ | |
| user_pref("browser.sessionstore.max_tabs_undo", 0); | |
| user_pref("browser.sessionstore.max_windows_undo", 0); | |
| user_pref("browser.tabs.crashReporting.sendReport", false); | |
| user_pref("browser.crashReports.unsubmittedCheck.autoSubmit2", false); | |
| user_pref("browser.crashReports.unsubmittedCheck.enabled", false); | |
| user_pref("toolkit.crashreporter.infoURL", ""); | |
| // ุชุนุทูู ุดุงุดุฉ "Restore" ููุฐูู ุฃู ุฅุดุนุงุฑ ุชุญุทูู | |
| user_pref("browser.sessionstore.resumeFromCrash", false); | |
| user_pref("dom.ipc.plugins.flash.subprocess.crashreporter.enabled", false); | |
| // ุฅูุบุงุก ุงุณุชุนุงุฏุฉ ุขุฎุฑ ุฌูุณุฉ ุชู ุงู ุงู + ุฏุงุฆู ุงู about:blank | |
| user_pref("browser.startup.homepage", "about:blank"); | |
| // ุชุนุทูู ุชุญุฏูุซุงุช ูุฑุณุงุฆู onboarding ุงูุชู ูุฏ ุชุจุทุฆ ุฃู ุชุญุฌุจ ุงูููุทุฉ | |
| user_pref("browser.shell.checkDefaultBrowser", false); | |
| user_pref("browser.aboutwelcome.enabled", false); | |
| user_pref("browser.startup.firstrunSkipsHomepage", true); | |
| user_pref("startup.homepage_welcome_url", ""); | |
| user_pref("startup.homepage_welcome_url.additional", ""); | |
| user_pref("browser.uitour.enabled", false); | |
| user_pref("browser.newtabpage.activity-stream.feeds.telemetry", false); | |
| user_pref("datareporting.policy.dataSubmissionEnabled", false); | |
| user_pref("app.normandy.enabled", false); | |
| user_pref("app.update.enabled", false); | |
| user_pref("app.update.auto", false); | |
| // โโ ุฅุตูุงุญุงุช ุชุฌู ุฏ ุงูุชุญู ูู ("Connecting to ... ุชุจูู ููุฃุจุฏ") โโ | |
| // ุงูุณุจุจ ุงูุบุงูุจ ุนูู ุณูุฑูุฑุงุช ู ุฌุงููุฉ/container: ู ุญุงููุฉ IPv6 ุฃููุงู (ุบูุฑ | |
| // ู ุฏุนูู ูุนููุงู ุนูู ุฃุบูุจ ู ุฒูุฏู Render/HuggingFace ุงูู ุฌุงููุฉ) ูุจู ุงูุฑุฌูุน | |
| // ูู IPv4 ุจุนุฏ timeout ุทููู ุฌุฏุงู (ูุฏ ูุตู 20-30 ุซุงููุฉ ููู ุทูุจ DNS)ุ | |
| // ููุฐุง ุจุงูุถุจุท ุงูุณุจุจ ุงูุฐู ูุธูุฑ ูุดุงุดุฉ "Connecting..." ู ุนููุฉ ูุง ุชุชูุฏู ุฃุจุฏุงู. | |
| user_pref("network.dns.disableIPv6", true); | |
| user_pref("network.http.fast-fallback-to-IPv4", true); | |
| // ุชูููู ู ููุฉ connect ุงูุฅุฌู ุงููุฉ ููู ุทูุจ ู ู 15 ุซุงููุฉ (ุงูุงูุชุฑุงุถู) ุฅูู 8 ุซูุงูู | |
| // โ ุฃูุถู ุฃู ููุดู ุงูุทูุจ ุจุณุฑุนุฉ ูููุฑุฑ ุงูุฐูุงุก ุงูุงุตุทูุงุนู ุจุฏููุงู ู ู ุฃู ูุจูู ุนุงููุงู ุทูููุงู. | |
| user_pref("network.http.connection-timeout", 8); | |
| user_pref("network.http.response.timeout", 12); | |
| // ุชุนุทูู predictive prefetching/speculative connect ุงูุชู ุชุณุชููู ุนุฑุถ ุญุฒู ุฉ ุนูู | |
| // ุณูุฑูุฑ ู ุญุฏูุฏ ุงูู ูุงุฑุฏ ุจุฏูู ูุงุฆุฏุฉ ุญููููุฉ ููุง. | |
| user_pref("network.dns.disablePrefetch", true); | |
| user_pref("network.prefetch-next", false); | |
| user_pref("network.predictor.enabled", false); | |
| user_pref("network.http.speculative-parallel-limit", 0); | |
| // ุชุนุทูู safebrowsing ุงูุฐู ูุณุชุฏุนู ุทูุจุงุช ุฎุงุฑุฌูุฉ ุฅุถุงููุฉ ุนูุฏ ูุชุญ ูู ุตูุญุฉ | |
| // (ููุจุทุฆ ุงูุชุญู ูู ุฃู ูุนููู ูู ุงูุทูุจ ูููุงุฆู Google ูุดู ุฃู ุงุณุชุบุฑู). | |
| user_pref("browser.safebrowsing.malware.enabled", false); | |
| user_pref("browser.safebrowsing.phishing.enabled", false); | |
| user_pref("browser.safebrowsing.downloads.enabled", false); | |
| user_pref("browser.safebrowsing.provider.google4.updateURL", ""); | |
| user_pref("browser.safebrowsing.provider.google.updateURL", ""); | |
| // ุชุนุทูู telemetry/captive-portal checks ุงูุชู ุชูุชุญ ุงุชุตุงูุงุช ุฎูููุฉ ุบูุฑ ุถุฑูุฑูุฉ ุนูุฏ ุงูุฅููุงุน | |
| user_pref("network.captive-portal-service.enabled", false); | |
| user_pref("network.connectivity-service.enabled", false); | |
| user_pref("toolkit.telemetry.server", ""); | |
| // ุชูููู ุนุฏุฏ ุงุชุตุงูุงุช HTTP ุงูู ุชุฒุงู ูุฉ ููู ุฏูู ูู โ ูููู ุงูุถุบุท ุนูู ุดุจูุฉ ู ุญุฏูุฏุฉ | |
| // ุงููุทุงู ููุณูุฑูุฑุงุช ุงูู ุฌุงููุฉ ููููู ุงุญุชู ุงู ุงูุชุนูู. | |
| user_pref("network.http.max-persistent-connections-per-server", 4); | |
| user_pref("network.http.max-connections", 48); | |
| """ | |
| def _ensure_firefox_profile(user_id: str, display: str) -> str: | |
| """ | |
| ูููุดุฆ (ุฃู ููุนูุฏ ุงุณุชุฎุฏุงู ) ุจุฑููุงูู Firefox ู ุฎุตุต ููุธูู ููุฐุง ุงูู ุณุชุฎุฏู ุ | |
| ู ุน ุฅุนุฏุงุฏุงุช ุชุนุทูู ุงุณุชุฑุฌุงุน ุงูุฌูุณุฉ (crash recovery) ููุงุฆูุงู. | |
| ููุนูุฏ ุงูู ุณุงุฑ ุงููุงู ู ููุจุฑููุงูู. | |
| """ | |
| safe_id = re.sub(r"[^a-zA-Z0-9_.-]", "_", user_id) or "anon" | |
| profile_dir = os.path.expanduser(f"~/.zai_ff_profiles/{safe_id}") | |
| try: | |
| os.makedirs(profile_dir, exist_ok=True) | |
| prefs_path = os.path.join(profile_dir, "user.js") | |
| with open(prefs_path, "w", encoding="utf-8") as f: | |
| f.write(FIREFOX_PROFILE_PREFS) | |
| except Exception as e: | |
| print(f"[profile] โ ๏ธ failed to prepare profile for {user_id}: {e}") | |
| return profile_dir | |
| def _wipe_firefox_session_data(profile_dir: str): | |
| """ | |
| ูู ุณุญ ู ููุงุช ุงูุฌูุณุฉ ุงููุงุณุฏุฉ (sessionstore) + ุฃู lock ู ุชุจููู. | |
| ูุฐุง ุถุฑูุฑู ูุฃู ู ุฌุฑุฏ ุชุนุทูู session restore ูู user.js ูุง ูู ุณุญ ู ููุงุช | |
| ุฌูุณุฉ ุณุงุจูุฉ ู ูุฌูุฏุฉ ุจุงููุนู ุนูู ุงููุฑุต ู ู ูุจู ุชูุนูู ูุฐู ุงูุฅุนุฏุงุฏุงุช. | |
| """ | |
| try: | |
| subprocess.run( | |
| f"rm -f '{profile_dir}'/lock '{profile_dir}'/.parentlock " | |
| f"'{profile_dir}'/sessionstore.jsonlz4 " | |
| f"'{profile_dir}'/sessionstore-backups/*.jsonlz4 " | |
| f"'{profile_dir}'/sessionCheckpoints.json 2>/dev/null", | |
| shell=True, timeout=5, capture_output=True | |
| ) | |
| except Exception: | |
| pass | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโ Xvfb Management (per-display) โโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def _next_free_display() -> int: | |
| """ูุฌุฏ ุฑูู display ุญุฑ ุบูุฑ ู ุณุชุฎุฏู .""" | |
| used = _display_numbers.copy() | |
| for n in range(DISPLAY_BASE, DISPLAY_MAX): | |
| if n not in used: | |
| return n | |
| # ุฅุฐุง ุงู ุชูุฃุช ุงููุงุฆู ุฉ โ ุฃุนุฏ ุงุณุชุฎุฏุงู ุฃูุฏู display ุบูุฑ ูุดุท | |
| return DISPLAY_BASE | |
| def _start_xvfb(display: str) -> "subprocess.Popen | None": | |
| """ููุดุบูู Xvfb ุนูู display ู ุญุฏุฏ ูููุนูุฏ ุงูู Popen ุฃู None.""" | |
| # ุชุญูู ุฃููุงู: ูู ุงูู display ูุนู ู ุจุงููุนูุ | |
| try: | |
| r = subprocess.run( | |
| ["xdpyinfo", "-display", display], | |
| capture_output=True, timeout=3 | |
| ) | |
| if r.returncode == 0: | |
| print(f"[xvfb] โ Display {display} already active") | |
| return None # ูุนู ู ุจุงููุนู ุจุฏูู Popen ูุดุบููู | |
| except Exception: | |
| pass | |
| try: | |
| # โโ ุฏูุฉ 1280x800 ุจุฏู 1920x1080 โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # ูุฐุง ูููู ุญุฌู ุจูุงูุงุช ูู ููุทุฉ ุดุงุดุฉ ุจูุณุจุฉ ~60%ุ ูุจุงูุชุงูู ูุณุฑูุน ูู | |
| # ู ุฑุงุญู ุงูุงูุชูุงุท ูุงูุชุฑู ูุฒ ุจุดูู ูุจูุฑ ุนูู ุณูุฑูุฑ ุจู ูุงุฑุฏ ู ุญุฏูุฏุฉ. | |
| # ููุณ ุงูุฏูุฉ ุงูู ุณุชุฎุฏู ุฉ ูู ุงูุชุทุจููุงุช ุงูู ุฑุฌุนูุฉ ูู computer-use. | |
| proc = subprocess.Popen( | |
| ["Xvfb", display, "-screen", "0", "1280x800x24", | |
| "-nolisten", "tcp", "-ac"], | |
| stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL | |
| ) | |
| # ุงูุชุธุฑ ุญุชู ูุตุจุญ ุฌุงูุฒุงู (max 6 ุซูุงูู) | |
| for _ in range(60): | |
| time.sleep(0.1) | |
| try: | |
| r = subprocess.run( | |
| ["xdpyinfo", "-display", display], | |
| capture_output=True, timeout=2 | |
| ) | |
| if r.returncode == 0: | |
| print(f"[xvfb] โ Xvfb started on {display}") | |
| # ุฅุตูุงุญ: Xvfb ุจุฏูู ุฃู ุฑุณู ูุงุญู ูููู framebuffer ูุงุฑุบุงู ุชู ุงู ุงู | |
| # (ูุง ููู ุฎูููุฉ ุฃุตูุงู)ุ ูุฃูู ููุทุฉ ุดุงุดุฉ ูุฏ ุชุจุฏู "ูุงุฑุบุฉ ุจุดูู | |
| # ู ุฑุจู" ุฑุบู ุฃููุง ุงูุชูุงุท ูุงุฌุญ ุชูููุงู ูุดุงุดุฉ ูุธููุฉ ูุนูุงู. ูุฑุณู | |
| # ูููุงู ู ุญุงูุฏุงู ุจุณูุทุงู ููุฑุงู ุนุจุฑ xsetroot ูุชูุถูุญ ุฃู ุงูุดุงุดุฉ | |
| # ุฌุงูุฒุฉ ููุงุฑุบุฉ ุนู ุฏุงู (ุณุทุญ ู ูุชุจ ูุธูู) ูุง ุฃููุง ุฎุทุฃ ูู ุงูุงูุชูุงุท. | |
| try: | |
| env_xs = {**os.environ, "DISPLAY": display} | |
| subprocess.run( | |
| ["xsetroot", "-solid", "#2b2b2b"], | |
| env=env_xs, timeout=2, capture_output=True | |
| ) | |
| except Exception: | |
| pass | |
| return proc | |
| except Exception: | |
| continue | |
| print(f"[xvfb] โ ๏ธ Xvfb may not be ready yet on {display}") | |
| return proc | |
| except FileNotFoundError: | |
| print(f"[xvfb] โ Xvfb binary not found") | |
| return None | |
| except Exception as e: | |
| print(f"[xvfb] โ Failed to start on {display}: {e}") | |
| return None | |
| def _kill_proc(proc: "subprocess.Popen | None"): | |
| """ููุชู process ุจุฃู ุงู.""" | |
| if not proc: | |
| return | |
| try: | |
| proc.terminate() | |
| proc.wait(timeout=3) | |
| except Exception: | |
| try: | |
| proc.kill() | |
| except Exception: | |
| pass | |
| def _kill_display_processes(display: str): | |
| """ููุชู ูู ุงูุนู ููุงุช ุงูู ุฑุชุจุทุฉ ุจู display ู ุนูู.""" | |
| env_d = {**os.environ, "DISPLAY": display} | |
| # ูุชู ุงูู ุชุตูุญ โ SIGKILL ู ุจุงุดุฑ (-9) ูุชูุงุฏู ุฃู ุญุงูุฉ ุฅุบูุงู ูุตูู ุชูุณุฌููู | |
| # ูุงุญูุงู ูุชุญุทูู (crash) ุฏุงุฎู Firefox ูุชููุชุฌ ุดุงุดุฉ "Restore Session" | |
| for b in ["firefox", "firefox-esr", "chromium", "chrome"]: | |
| try: | |
| subprocess.run( | |
| ["pkill", "-9", "-f", f"[{b[0]}]{b[1:]}.*{display}"], | |
| timeout=3, capture_output=True | |
| ) | |
| except Exception: | |
| pass | |
| # ูุชู Xvfb ุนูู ูุฐุง ุงูู display | |
| try: | |
| subprocess.run( | |
| ["pkill", "-f", f"[X]vfb {display}"], | |
| timeout=3, capture_output=True | |
| ) | |
| except Exception: | |
| pass | |
| time.sleep(0.5) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโ Session Management (per user_id) โโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| async def get_or_create_user_session(user_id: str, ws: WebSocket) -> dict: | |
| """ | |
| ููุนูุฏ session ุงูู ุณุชุฎุฏู (ุฃู ูููุดุฆ ูุงุญุฏุฉ ุฌุฏูุฏุฉ ุฅุฐุง ูู ุชูู ู ูุฌูุฏุฉ). | |
| ูู user_id โ display ุฎุงุต + Xvfb ุฎุงุต. | |
| """ | |
| with _display_lock: | |
| if user_id in _user_displays: | |
| sess = _user_displays[user_id] | |
| # ุญุฏูุซ ุงูู WebSocket ุงูุญุงูู | |
| sess["active_ws"] = ws | |
| print(f"[session] ๐ Reconnected user '{user_id}' on display {sess['display']}") | |
| return sess | |
| # ู ุณุชุฎุฏู ุฌุฏูุฏ โ ุฎุตุต ูู display | |
| disp_num = _next_free_display() | |
| _display_numbers.add(disp_num) | |
| display = f":{disp_num}" | |
| sess = { | |
| "user_id": user_id, | |
| "display": display, | |
| "xvfb_proc": None, # ุณููุดุบููู ูุงุญูุงู | |
| "browser_proc": None, | |
| "last_bg_shot_ts": 0.0, | |
| "last_bg_hash": "", | |
| "active_ws": ws, | |
| "created": time.time(), | |
| } | |
| _user_displays[user_id] = sess | |
| print(f"[session] โ New user '{user_id}' โ display {display}") | |
| return sess | |
| async def reset_user_computer(user_id: str) -> dict: | |
| """ | |
| ููุนูุฏ ุถุจุท ุงููู ุจููุชุฑ ุงูุงูุชุฑุงุถู ููู ุณุชุฎุฏู : | |
| ููุชู Xvfb ูุงูู ุชุตูุญ ูููุดุบูู Xvfb ุฌุฏูุฏุงู ูุธููุงู. | |
| """ | |
| with _display_lock: | |
| sess = _user_displays.get(user_id) | |
| if not sess: | |
| return {} | |
| display = sess["display"] | |
| print(f"[reset] ๐ Resetting computer for user '{user_id}' on {display}") | |
| # ูุชู ุงูู ุชุตูุญ โ SIGKILL ู ุจุงุดุฑ (ุจุฏู terminate ุงููุทูู) ูุฃู ุงูุฃุฎูุฑ ูุฏ | |
| # ูุชุฑู ูุงูุฑูููุณ ูู ุญุงูุฉ ูุตู-ู ุบููุฉ ุชูุณุฌููู ูุชุญุทูู (crash) ูู ุงูู ุฑุฉ | |
| # ุงููุงุฏู ุฉุ ููุฐุง ุจุงูุถุจุท ู ุง ูููุชุฌ ุดุงุดุฉ "Restore Session" ูุงุญูุงู. | |
| browser_proc = sess.get("browser_proc") | |
| if browser_proc: | |
| try: | |
| browser_proc.kill() | |
| browser_proc.wait(timeout=3) | |
| except Exception: | |
| pass | |
| sess["browser_proc"] = None | |
| # ูุชู Xvfb | |
| _kill_proc(sess.get("xvfb_proc")) | |
| sess["xvfb_proc"] = None | |
| # ุชูุธูู ุดุงู ู ููุฐุง ุงูู display | |
| _kill_display_processes(display) | |
| # โโ ุชูุธูู ุดุงู ู ูุจูุงูุงุช ุฌูุณุฉ Firefox ุงููุงุณุฏุฉ (ุงูุณุจุจ ุงูุฌุฐุฑู ูุดุงุดุฉ | |
| # "Restore Session") โ ููุณ ููุท lock files ุจู sessionstore ูุงู ูุงูุ | |
| # ูู ูู ู ู ุงูุจุฑููุงูู ุงูุงูุชุฑุงุถู ูุฃู ุจุฑููุงูู ู ุฎุตุต ููุฐุง ุงูู ุณุชุฎุฏู โโ | |
| try: | |
| subprocess.run( | |
| "rm -f ~/.mozilla/firefox/*/lock ~/.mozilla/firefox/*/.parentlock " | |
| "~/.mozilla/firefox/*/sessionstore.jsonlz4 " | |
| "~/.mozilla/firefox/*/sessionstore-backups/*.jsonlz4 " | |
| "~/.mozilla/firefox/*/sessionCheckpoints.json 2>/dev/null", | |
| shell=True, timeout=5, capture_output=True | |
| ) | |
| except Exception: | |
| pass | |
| safe_id = re.sub(r"[^a-zA-Z0-9_.-]", "_", user_id) or "anon" | |
| custom_profile = os.path.expanduser(f"~/.zai_ff_profiles/{safe_id}") | |
| _wipe_firefox_session_data(custom_profile) | |
| # ุชุดุบูู Xvfb ุฌุฏูุฏ (ุฎุงุฑุฌ ุงูู lock ูุฃู start_xvfb ูุณุชุบุฑู ููุชุงู) | |
| new_proc = await asyncio.to_thread(_start_xvfb, display) | |
| with _display_lock: | |
| if user_id in _user_displays: | |
| _user_displays[user_id]["xvfb_proc"] = new_proc | |
| _user_displays[user_id]["last_bg_shot_ts"] = 0.0 | |
| _user_displays[user_id]["last_bg_hash"] = "" | |
| print(f"[reset] โ Computer reset done for '{user_id}' on {display}") | |
| return _user_displays.get(user_id, {}) | |
| async def ensure_xvfb_for_session(sess: dict): | |
| """ูุชุฃูุฏ ุฃู Xvfb ูุนู ู ููุฐุง ุงูู session โ ููุดุบููู ุฅุฐุง ูู ููู ูุฐูู.""" | |
| display = sess["display"] | |
| # ุชุญูู ุฅุฐุง ูุงู ูุนู ู ุจุงููุนู | |
| try: | |
| r = subprocess.run( | |
| ["xdpyinfo", "-display", display], | |
| capture_output=True, timeout=3 | |
| ) | |
| if r.returncode == 0: | |
| return # ูุนู ู | |
| except Exception: | |
| pass | |
| # ุดุบููู | |
| proc = await asyncio.to_thread(_start_xvfb, display) | |
| with _display_lock: | |
| if sess["user_id"] in _user_displays: | |
| _user_displays[sess["user_id"]]["xvfb_proc"] = proc | |
| async def destroy_user_ws(user_id: str, ws: WebSocket): | |
| """ | |
| ููุฒูู ุงูู WebSocket ู ู ุงูู session ุนูุฏ ุงููุทุงุน ุงูุงุชุตุงู. | |
| ูุง ูุญุฐู ุงูู session ููุณูุง โ ุงูู ุณุชุฎุฏู ูุญุชูุธ ุจูู ุจููุชุฑู. | |
| """ | |
| with _display_lock: | |
| sess = _user_displays.get(user_id) | |
| if sess and sess.get("active_ws") is ws: | |
| sess["active_ws"] = None | |
| print(f"[session] ๐ด User '{user_id}' disconnected (session kept)") | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโ Screenshot Engine (per-display) โโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def _is_black_screen(img) -> bool: | |
| try: | |
| small = img.resize((100, 100)) | |
| pixels = list(small.getdata()) | |
| avg = sum(sum(p[:3]) for p in pixels) / (len(pixels) * 3 * 255) | |
| return avg < 0.04 | |
| except Exception: | |
| return False | |
| def _load_capture_image(path: str): | |
| from PIL import Image | |
| if not path or not os.path.exists(path): | |
| return None, 0, 0 | |
| if os.path.getsize(path) < 1024: | |
| return None, 0, 0 | |
| try: | |
| img = Image.open(path).convert("RGB") | |
| w, h = img.size | |
| if w < 100 or h < 100: | |
| return None, 0, 0 | |
| return img, w, h | |
| except Exception as ex: | |
| print(f"[cap] load error for {path}: {ex}") | |
| return None, 0, 0 | |
| def _capture_via_import_pipe(display: str): | |
| """ | |
| ุงูุทุฑููุฉ ุงูุฃุณุงุณูุฉ ูุงููุญูุฏุฉ: `import -window root` ู ุน ุงูุฅุฎุฑุงุฌ ู ุจุงุดุฑุฉ | |
| ุนุจุฑ stdout (pipe) โ ุจุฏูู ุฃู ู ูู ู ุคูุช ุนูู ุงููุฑุต. ุฃุณุฑุน ุจุดูู ู ูุญูุธ ู ู | |
| ุงููุชุงุจุฉ ูู ูู ุซู ุฅุนุงุฏุฉ ูุฑุงุกุชูุ ุฎุตูุตุงู ุนูู ุชุฎุฒูู ุดุจูู ุจุทูุก. | |
| """ | |
| from PIL import Image | |
| env = {**os.environ, "DISPLAY": display} | |
| try: | |
| r = subprocess.run( | |
| ["import", "-window", "root", "-silent", "png:-"], | |
| env=env, timeout=6, capture_output=True | |
| ) | |
| if r.returncode != 0 or not r.stdout or len(r.stdout) < 500: | |
| return None, 0, 0 | |
| img = Image.open(io.BytesIO(r.stdout)).convert("RGB") | |
| w, h = img.size | |
| if w < 100 or h < 100: | |
| return None, 0, 0 | |
| return img, w, h | |
| except FileNotFoundError: | |
| print(f"[cap:{display}] import (ImageMagick) not installed") | |
| return None, 0, 0 | |
| except Exception as e: | |
| print(f"[cap:{display}] import-pipe: {e}") | |
| return None, 0, 0 | |
| def _capture_via_xlib_direct(display: str): | |
| """ | |
| ุฎุท ุงูุฏูุงุน ุงูุซุงูู ูุงูุฃุฎูุฑ: ูุฑุงุกุฉ X11 framebuffer ู ุจุงุดุฑุฉ ุนุจุฑ | |
| python-xlibุ ุจุฏูู subprocess ุฅุทูุงูุงู (ุฃุณุฑุน ู ู ุฃู ุฃุฏุงุฉ ุฎุงุฑุฌูุฉุ | |
| ููุนู ู ุญุชู ูู ImageMagick ุบูุฑ ู ุซุจูุช ุนูู ุงูุณูุฑูุฑ). | |
| """ | |
| from PIL import Image | |
| try: | |
| from Xlib import display as Xdisp, X | |
| xd = Xdisp.Display(display) | |
| root = xd.screen().root | |
| geom = root.get_geometry() | |
| w, h = geom.width, geom.height | |
| raw = root.get_image(0, 0, w, h, X.ZPixmap, 0xFFFFFFFF) | |
| img = Image.frombuffer("RGB", (w, h), raw.data, "raw", "BGRX", 0, 1) | |
| xd.close() | |
| return img, w, h | |
| except Exception as e: | |
| print(f"[cap:{display}] xlib-direct: {e}") | |
| return None, 0, 0 | |
| def _capture_raw(display: str) -> tuple: | |
| """ | |
| ููุชูุท ุงูุดุงุดุฉ ุจุฃุณุฑุน ุทุฑููุฉ ู ูุซููุฉ: import-pipe ุฃููุงู (ุณุฑูุนุฉ ุฌุฏุงู)ุ | |
| ูุนูุฏ ูุดููุง ููุท xlib-direct ูุจุฏูู ูุงุญุฏ. ุจุญุฏ ุฃูุตู ู ุญุงููุชูู ุณุฑูุนุชูู | |
| ููู ุทุฑููุฉ (ูููุณ 9 ู ุญุงููุงุช ูุงูุณุงุจู) โ ูุฃู ููุง ุงูุทุฑููุชูู ุฅู ุง ุชูุฌุญุงู | |
| ููุฑุงู ุฃู ุชูุดูุงู ูุณุจุจ ุฏุงุฆู ุ ูุฅุนุงุฏุฉ ุงูู ุญุงููุฉ ุฃูุซุฑ ู ู ู ุฑุชูู ูุง ุชูุบููุฑ | |
| ุงููุชูุฌุฉุ ููุท ุชูุจุทุฆ ุงูุงุณุชุฌุงุจุฉ. | |
| """ | |
| for method_name, method in ( | |
| ("import-pipe", _capture_via_import_pipe), | |
| ("xlib-direct", _capture_via_xlib_direct), | |
| ): | |
| for attempt in range(2): | |
| try: | |
| img, w, h = method(display) | |
| if img and not _is_black_screen(img): | |
| if attempt > 0: | |
| print(f"[cap:{display}] โ {method_name} succeeded on retry") | |
| return img, w, h | |
| except Exception as e: | |
| print(f"[cap:{display}] {method_name}: {e}") | |
| if attempt == 0: | |
| time.sleep(0.3) | |
| print(f"[cap:{display}] โ ๏ธ All methods failed โ placeholder") | |
| from PIL import Image as PILImg, ImageDraw | |
| sw, sh = _get_screen_size(display) | |
| img = PILImg.new("RGB", (sw or 1280, sh or 800), (15, 20, 40)) | |
| draw = ImageDraw.Draw(img) | |
| draw.rectangle([(0, 0), (sw, 55)], fill=(30, 40, 80)) | |
| draw.text((10, 10), f"โ ๏ธ Screenshot failed โ DISPLAY={display}", fill=(255, 120, 80)) | |
| draw.text((10, 32), "Methods tried: import-pipe, xlib-direct", fill=(130, 130, 150)) | |
| return img, sw or 1280, sh or 800 | |
| def _get_screen_size(display: str) -> tuple: | |
| try: | |
| r = subprocess.run( | |
| ["xdotool", "getdisplaygeometry"], | |
| env={**os.environ, "DISPLAY": display}, | |
| capture_output=True, text=True, timeout=5 | |
| ) | |
| parts = r.stdout.strip().split() | |
| return int(parts[0]), int(parts[1]) | |
| except Exception: | |
| return 1280, 800 | |
| def _get_mouse_pos(display: str) -> tuple: | |
| try: | |
| r = subprocess.run( | |
| ["xdotool", "getmouselocation"], | |
| env={**os.environ, "DISPLAY": display}, | |
| capture_output=True, text=True, timeout=5 | |
| ) | |
| mx = int(re.search(r"x:(\d+)", r.stdout).group(1)) | |
| my = int(re.search(r"y:(\d+)", r.stdout).group(1)) | |
| return mx, my | |
| except Exception: | |
| return 0, 0 | |
| def _draw_mouse_marker(draw, msx, msy, color=(255, 50, 50, 240)): | |
| """ูุฑุณู ุนูุงู ุฉ ุงูู ุงูุณ (ุฏุงุฆุฑุฉ + ุฎุทูุท ุชูุงุทุน) ูู ููุทุฉ ู ุญุฏุฏุฉ ุนูู ุตูุฑุฉ.""" | |
| r = 11 | |
| line_color = (color[0], color[1], color[2], 200) | |
| draw.ellipse([(msx-r, msy-r), (msx+r, msy+r)], outline=color, width=2) | |
| draw.line([(msx-18, msy), (msx+18, msy)], fill=line_color, width=1) | |
| draw.line([(msx, msy-18), (msx, msy+18)], fill=line_color, width=1) | |
| def _render_grid_variant(base_img, ow, oh, sw, sh, mx, my, display, | |
| grid_step: int, line_color: tuple, label_every: int, | |
| major_only_labels: bool = True): | |
| """ | |
| ูุฑุณู ูุณุฎุฉ ุดุจูุฉ ุฅุญุฏุงุซูุงุช ูุงุญุฏุฉ ููู ูุณุฎุฉ ู ู ุงูุตูุฑุฉ ุงูุฃุณุงุณูุฉ. | |
| grid_step โ ุงูู ุณุงูุฉ ุจุงูุจูุณู ุงูุญูููู ุจูู ูู ุฎุท ุดุจูุฉ. | |
| line_color โ ููู ุงูุฎุทูุท ูุงูุฃุฑูุงู (RGBA). | |
| label_every โ ูู ูู ุจูุณู ุชููุชุจ ููู ุชุณู ูุฉ ุฅุญุฏุงุซู (x,y) ูุงู ูุฉ ุนูุฏ ุงูุชูุงุทุนุงุช. | |
| major_only_labels โ ุฅุฐุง Trueุ ุงูุฃุฑูุงู ุนูู ุงูุญูุงู ุชุธูุฑ ููุท ุนูุฏ ุฎุทูุท "ุฑุฆูุณูุฉ". | |
| """ | |
| from PIL import ImageDraw | |
| img = base_img.copy() | |
| draw = ImageDraw.Draw(img, "RGBA") | |
| step_x = max(1, int(grid_step * sw / ow)) | |
| step_y = max(1, int(grid_step * sh / oh)) | |
| minor_color = (line_color[0], line_color[1], line_color[2], 35) | |
| major_color = (line_color[0], line_color[1], line_color[2], 110) | |
| text_color = line_color | |
| x_sc, x_r = step_x, grid_step | |
| x_majors = [] | |
| while x_sc < sw: | |
| is_major = (x_r % label_every == 0) | |
| draw.line([(x_sc, 0), (x_sc, sh)], fill=(major_color if is_major else minor_color), width=1) | |
| if is_major or not major_only_labels: | |
| draw.rectangle([(x_sc+1, 2), (x_sc+34, 15)], fill=(0, 0, 0, 175)) | |
| draw.text((x_sc+2, 3), str(x_r), fill=text_color) | |
| if is_major: | |
| x_majors.append((x_sc, x_r)) | |
| x_sc += step_x; x_r += grid_step | |
| y_sc, y_r = step_y, grid_step | |
| y_majors = [] | |
| while y_sc < sh: | |
| is_major = (y_r % label_every == 0) | |
| draw.line([(0, y_sc), (sw, y_sc)], fill=(major_color if is_major else minor_color), width=1) | |
| if is_major or not major_only_labels: | |
| draw.rectangle([(2, y_sc+1), (38, y_sc+14)], fill=(0, 0, 0, 175)) | |
| draw.text((3, y_sc+2), str(y_r), fill=text_color) | |
| if is_major: | |
| y_majors.append((y_sc, y_r)) | |
| y_sc += step_y; y_r += grid_step | |
| for (xs, xr) in x_majors: | |
| for (ys, yr) in y_majors: | |
| label = f"{xr},{yr}" | |
| tw = 6 * len(label) + 4 | |
| draw.rectangle([(xs+2, ys+2), (xs+2+tw, ys+13)], fill=(0, 0, 0, 150)) | |
| draw.text((xs+4, ys+2), label, fill=text_color) | |
| msx = int(mx * sw / ow) | |
| msy = int(my * sh / oh) | |
| _draw_mouse_marker(draw, msx, msy, color=(line_color[0], line_color[1], line_color[2], 240)) | |
| final = img.convert("RGB") | |
| draw2 = ImageDraw.Draw(final) | |
| draw2.rectangle([(0, 0), (sw, 20)], fill=(0, 0, 0)) | |
| draw2.text((4, 3), f"SCREEN {ow}x{oh} | MOUSE:({mx},{my}) | GRID={grid_step}px | DSP:{display}", fill=(0, 220, 160)) | |
| draw2.rectangle([(0, sh-20), (sw, sh)], fill=(0, 0, 0)) | |
| draw2.text((4, sh-17), "CLICKCOORDS = numbers at every intersection (real screen pixels)", fill=(180, 180, 70)) | |
| return final | |
| def capture_with_grid(display: str, scale: float = 0.85, quality: int = 72, | |
| force_mx: int | None = None, | |
| force_my: int | None = None, | |
| grid_step: int = 50) -> dict: | |
| """ | |
| ููุชูุท ุงูุดุงุดุฉ ู ู display ู ุญุฏุฏ ููููุชุฌ 3 ูุณุฎ ู ู ููุณ ุงูููุทุฉ ุจุงูุถุจุท (ููุณ ุงููุญุธุฉ): | |
| - "data" โ ุงููุณุฎุฉ ุงูุนุงุฏูุฉ (ูุธููุฉ ุชู ุงู ุงู) + ุนูุงู ุฉ ุงูู ุงูุณ ููุทุ ุจุฏูู ุฃู Grid. | |
| - "data_grid" โ ุดุจูุฉ ุฅุญุฏุงุซูุงุช ุนุงุฏูุฉ (ูู 50px)ุ ุฃุฎุถุฑ/ุณู ุงูู โ ููุฐูุงุก ุงูุงุตุทูุงุนู. | |
| - "data_grid2" โ ุดุจูุฉ ุฅุญุฏุงุซูุงุช ุฃุฏู (ูู 20px)ุ ุฃุญู ุฑ โ ููููุฑุงุช ุงูุฏูููุฉ. | |
| โโ ุชุจุณูุท v13: ูุณุฎุชุงู ููุท (ุจุฏู 3) ูุชูููู ุนุฏุฏ ุนู ููุงุช resize/encode ููู | |
| ููุทุฉ ุจูุณุจุฉ ูุฑุงุจุฉ ุณ4ุฅูู ุณ2ุ ูุฏูุฉ ุงูู ูุงุณ ุงููุงุญุฏ 1280x800 ุงูุฌุฏูุฏุฉ ูู | |
| ุชุนุฏ ุชุญุชุงุฌ ูุซูุงุซ ุชุฏุฑุฌุงุช ุฏูุฉ ู ููุตูุฉ ูุชุญุฏูุฏ ุงูุฅุญุฏุงุซูุงุช. | |
| """ | |
| from PIL import Image, ImageDraw | |
| img, ow, oh = _capture_raw(display) | |
| if img is None: | |
| return {"data": "", "data_grid": "", "data_grid2": "", "data_grid3": "", | |
| "width": 1280, "height": 800, "mouse_x": 0, "mouse_y": 0} | |
| mx, my = (force_mx, force_my) if force_mx is not None else _get_mouse_pos(display) | |
| sw = int(ow * scale) | |
| sh = int(oh * scale) | |
| base_img = img.resize((sw, sh), Image.LANCZOS) | |
| msx = int(mx * sw / ow) | |
| msy = int(my * sh / oh) | |
| clean_img = base_img.copy() | |
| draw_clean = ImageDraw.Draw(clean_img, "RGBA") | |
| _draw_mouse_marker(draw_clean, msx, msy) | |
| clean_final = clean_img.convert("RGB") | |
| buf_clean = io.BytesIO() | |
| clean_final.save(buf_clean, format="JPEG", quality=quality, optimize=True) | |
| data_clean = base64.b64encode(buf_clean.getvalue()).decode() | |
| grid1 = _render_grid_variant(base_img, ow, oh, sw, sh, mx, my, display, | |
| grid_step=grid_step, line_color=(0, 255, 180, 235), | |
| label_every=100) | |
| buf1 = io.BytesIO(); grid1.save(buf1, format="JPEG", quality=quality, optimize=True) | |
| data_grid1 = base64.b64encode(buf1.getvalue()).decode() | |
| grid2 = _render_grid_variant(base_img, ow, oh, sw, sh, mx, my, display, | |
| grid_step=20, line_color=(255, 60, 60, 235), | |
| label_every=60, major_only_labels=True) | |
| buf2 = io.BytesIO(); grid2.save(buf2, format="JPEG", quality=max(quality, 78), optimize=True) | |
| data_grid2 = base64.b64encode(buf2.getvalue()).decode() | |
| return { | |
| "data": data_clean, | |
| "data_grid": data_grid1, | |
| "data_grid2": data_grid2, | |
| "data_grid3": data_grid2, # ุชูุงูููุฉ ุฑุฌุนูุฉ: ููุณ ููู ุฉ data_grid2 ูุฃู ููุฏ ูุฏูู ููุฑุฃ data_grid3 | |
| "width": ow, "height": oh, "mouse_x": mx, "mouse_y": my, | |
| } | |
| def _frame_hash(data: str) -> str: | |
| """ | |
| ุฅุตูุงุญ: ูุงู ููุญุณุจ ุนูู ุฃูู 2000 ุญุฑู ููุท ู ู ุจูุงูุงุช ุงูุตูุฑุฉุ ู ู ุง ูุนูู ุฃู ุชุบููุฑ | |
| ุจุตุฑู ููุน ุฎุงุฑุฌ ุงูุฌุฒุก ุงูู ู ุซููู ุถู ู ุชูู ุงูุฃุญุฑู ุงูุฃููู (ู ุซู ุชุญุฏูุฏ ูุต ุดุฑูุท ุงูุนููุงู | |
| ุจุนุฏ Ctrl+Lุ ุฃู ุฃู ุชุบููุฑ ุทููู/ูู ู ูุทูุฉ ูุง ุชุชูุงูู ู ุน ุจุฏุงูุฉ ุชุฑู ูุฒ base64) ูุง | |
| ูููุชุดู ุฃุจุฏุงูุ ูุชูุญุฌุจ ุงูุตูุฑุฉ ุงูุฌุฏูุฏุฉ ุนุจุฑ delta suppression ุฑุบู ุงุฎุชูุงููุง ูุนููุงู | |
| ุนู ุงูุณุงุจูุฉุ ููุง ุชุตู ุฃู ููุทุฉ ู ุญุฏููุซุฉ ููุนู ูู. ุงูุญู: ุญุณุงุจ MD5 ุนูู ูุงู ู ุงูุจูุงูุงุช | |
| ุจุฏู ุชูุทูุนูุง โ ูุฐุง ุณุฑูุน ุฌุฏุงู (ุฃูู ู ู ู ููู ุซุงููุฉ ุญุชู ูุตูุฑ ูุจูุฑุฉ) ููุง ุชูููุฉ ุฃุฏุงุก | |
| ุญููููุฉุ ููุถู ู ุงูุชุดุงู ุฃู ุชุบููุฑ ุจุตุฑู ุญูููู ูู ุฃู ู ูุงู ู ู ุงูุตูุฑุฉ. | |
| """ | |
| return hashlib.md5(data.encode()).hexdigest() | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโ SafeSearch โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| _BING_RE = re.compile(r"https?://(?:www\.)?bing\.com[^\s'\"]*") | |
| _DDG_RE = re.compile(r"https?://(?:www\.)?duckduckgo\.com[^\s'\"]*") | |
| _GOOG_RE = re.compile(r"https?://(?:www\.)?google\.[a-z.]+[^\s'\"]*") | |
| def _safe_search(text: str) -> str: | |
| def _bing(m): | |
| u = m.group(0) | |
| return re.sub(r"adlt=\w+", "adlt=strict", u) if "adlt=" in u else u + ("&" if "?" in u else "?") + "adlt=strict" | |
| def _ddg(m): | |
| u = m.group(0) | |
| return re.sub(r"kp=\d", "kp=1", u) if "kp=" in u else u + ("&" if "?" in u else "?") + "kp=1" | |
| def _goog(m): | |
| u = m.group(0) | |
| return re.sub(r"safe=\w+", "safe=strict", u) if "safe=" in u else u + ("&" if "?" in u else "?") + "safe=strict" | |
| return _GOOG_RE.sub(_goog, _DDG_RE.sub(_ddg, _BING_RE.sub(_bing, text))) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโ Terminal Execution (per-display) โโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| _PKILL_RE = re.compile(r"\b(pkill|killall)\s+(-9\s+)?-f\s+(['\"]?)([a-zA-Z0-9_./-]+)\3") | |
| def _sanitize_pkill(cmd: str) -> str: | |
| def _fix(m): | |
| tool, d9, _q, p = m.group(1), m.group(2) or "", m.group(3), m.group(4) | |
| sp = f"[{p[0]}]{p[1:]}" if len(p) > 1 else p | |
| return f"{tool} {d9}-f '{sp}'" | |
| return _PKILL_RE.sub(_fix, cmd) | |
| async def run_cmd(cmd: str, display: str, timeout: int = 60) -> dict: | |
| """ููููุฐ ุฃู ุฑ bash ู ุน DISPLAY ุฎุงุต ุจุงูู ุณุชุฎุฏู .""" | |
| cmd = _sanitize_pkill(_safe_search(cmd)) | |
| async with _terminal_sem: | |
| env = {**os.environ, "DISPLAY": display, | |
| "PYTHONIOENCODING": "utf-8", "LANG": "en_US.UTF-8"} | |
| def _exec(): | |
| try: | |
| r = subprocess.run( | |
| cmd, shell=True, capture_output=True, | |
| text=True, timeout=timeout, env=env, executable="/bin/bash" | |
| ) | |
| return {"stdout": r.stdout[-15000:], "stderr": r.stderr[-3000:], "returncode": r.returncode} | |
| except subprocess.TimeoutExpired: | |
| return {"stdout": "", "stderr": f"โฑ๏ธ Timeout {timeout}s", "returncode": -1} | |
| except Exception as e: | |
| return {"stdout": "", "stderr": str(e), "returncode": -1} | |
| return await asyncio.to_thread(_exec) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโ xdotool helpers (per-display) โโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| async def xdo(args: list, display: str, timeout: int = 10) -> dict: | |
| env = {**os.environ, "DISPLAY": display} | |
| def _run(): | |
| r = subprocess.run( | |
| ["xdotool"] + args, env=env, | |
| timeout=timeout, capture_output=True, text=True | |
| ) | |
| return {"rc": r.returncode, "out": r.stdout, "err": r.stderr} | |
| return await asyncio.to_thread(_run) | |
| async def type_smart(text: str, display: str) -> dict: | |
| """ูุชุงุจุฉ ูุต ุฐููุฉ ุจู display ู ุญุฏุฏ.""" | |
| has_arabic = bool(re.search(r'[\u0600-\u06FF]', text)) | |
| env = {**os.environ, "DISPLAY": display} | |
| if has_arabic: | |
| def _paste(): | |
| p = subprocess.Popen( | |
| ["xclip", "-selection", "clipboard"], | |
| stdin=subprocess.PIPE, env=env | |
| ) | |
| p.communicate(text.encode("utf-8")) | |
| await asyncio.to_thread(_paste) | |
| await asyncio.sleep(0.15) | |
| await xdo(["key", "--clearmodifiers", "ctrl+v"], display) | |
| return {"method": "clipboard+paste"} | |
| else: | |
| r = await xdo(["type", "--clearmodifiers", "--delay", "25", text], display) | |
| return {"method": "xdotool", "rc": r["rc"]} | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโ Search Sources โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def _search_sources(query: str) -> list: | |
| q = urllib.parse.quote_plus(query) | |
| return [ | |
| {"name": "DuckDuckGo Instant", | |
| "cmd": f"curl -s --max-time 15 'https://api.duckduckgo.com/?q={q}&format=json&no_html=1&skip_disambig=1' | python3 -c \"import sys,json;d=json.load(sys.stdin);a=d.get('AbstractText','');r=d.get('RelatedTopics',[]);print('ANS:',a or 'none');[print('-',x.get('Text','')[:200]) for x in r[:6] if isinstance(x,dict)]\""}, | |
| {"name": "Google News RSS", | |
| "cmd": f"curl -sL --max-time 15 'https://news.google.com/rss/search?q={q}&hl=ar&gl=AR&ceid=AR:ar' | python3 -c \"import sys,re;x=sys.stdin.read();t=re.findall(r'<title><!\\[CDATA\\[(.*?)\\]\\]></title>|<title>(.*?)</title>',x);[(print(str(i+1)+'. '+(a or b).strip()[:160])) for i,(a,b) in enumerate(t[1:8]) if (a or b).strip()]\""}, | |
| {"name": "Wikipedia EN", | |
| "cmd": f"curl -s --max-time 12 'https://en.wikipedia.org/api/rest_v1/page/summary/{q}' | python3 -c \"import sys,json;d=json.load(sys.stdin);print(d.get('title','')+'\\n'+d.get('extract','')[:1200])\""}, | |
| {"name": "DuckDuckGo HTML", | |
| "cmd": f"curl -sL --max-time 15 -H 'User-Agent: Mozilla/5.0' 'https://html.duckduckgo.com/html/?q={q}' | python3 -c \"import sys,re;h=sys.stdin.read();s=re.findall(r'class=.result__snippet[^>]*>(.*?)</a>',h,re.DOTALL);clean=lambda x:re.sub('<[^>]+>','',x).strip();[print(str(i+1)+'. '+clean(x)[:200]) for i,x in enumerate(s[:7])]\""}, | |
| {"name": "HackerNews", | |
| "cmd": f"curl -s --max-time 12 'https://hn.algolia.com/api/v1/search?query={q}&hitsPerPage=6&tags=story' | python3 -c \"import sys,json;d=json.load(sys.stdin);[print(str(i+1)+'. '+h.get('title','')+' Pts:'+str(h.get('points',0))) for i,h in enumerate(d.get('hits',[])[:5])]\""}, | |
| {"name": "Reddit", | |
| "cmd": f"curl -sL --max-time 12 -H 'Accept: application/json' 'https://www.reddit.com/search.json?q={q}&sort=new&limit=6' | python3 -c \"import sys,json;d=json.load(sys.stdin);[print(str(i+1)+'. '+p['data'].get('title','')[:160]) for i,p in enumerate(d.get('data',{{}}).get('children',[])[:5])]\""}, | |
| ] | |
| def _result_empty(s: str) -> bool: | |
| if not s or len(s.strip()) < 15: | |
| return True | |
| return "ans: none" in s.lower() | |
| async def run_cmd_smart(cmd: str, display: str, timeout: int = 60) -> dict: | |
| res = await run_cmd(cmd, display, timeout) | |
| if res["returncode"] == 0 and not _result_empty(res["stdout"]): | |
| return res | |
| is_search = "curl" in cmd and any( | |
| x in cmd for x in ["search", "duckduck", "bing", "google", "wikipedia", "reddit"] | |
| ) | |
| if not is_search: | |
| return res | |
| m = re.search(r"[?&]q=([^&'\"\s]+)", cmd) | |
| query = urllib.parse.unquote(m.group(1).replace("+", " ")).strip() if m else "" | |
| if len(query) < 3: | |
| return res | |
| sources = _search_sources(query) | |
| results = [] | |
| for src in sources: | |
| r2 = await run_cmd(src["cmd"], display, 20) | |
| if not _result_empty(r2["stdout"]): | |
| results.append(f"[{src['name']}]\n{r2['stdout']}") | |
| if len(results) >= 2: | |
| break | |
| if results: | |
| return {"stdout": "\n\n".join(results)[:15000], "stderr": "", "returncode": 0} | |
| return res | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโ FastAPI App โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| app = FastAPI(title="Z-Computer-Mode v11 โ Per-User Isolation") | |
| app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, | |
| allow_methods=["*"], allow_headers=["*"]) | |
| async def root(): | |
| n = len(_user_displays) | |
| users_info = "" | |
| with _display_lock: | |
| for uid, sess in _user_displays.items(): | |
| ws_status = "๐ข ู ุชุตู" if sess.get("active_ws") else "โซ ุบูุฑ ู ุชุตู" | |
| users_info += f"<tr><td>{uid[:30]}...</td><td>{sess['display']}</td><td>{ws_status}</td></tr>" | |
| return f"""<!DOCTYPE html><html><head><meta charset="utf-8"> | |
| <title>Z Computer Mode v11</title> | |
| <style>body{{background:#080808;color:#00d4aa;font-family:monospace;padding:40px}} | |
| h1{{color:#4a90d9}}.ok{{color:#00ff88}}.info{{color:#888;font-size:13px}} | |
| table{{border-collapse:collapse;width:100%}}td,th{{border:1px solid #333;padding:6px 10px;font-size:13px}} | |
| th{{background:#1a1a2e;color:#4a90d9}}</style> | |
| </head><body> | |
| <h1>๐ฅ๏ธ THE Z AI โ Computer Mode Server v11</h1> | |
| <p class="ok">โ Server RUNNING โ Per-User Isolated Displays</p> | |
| <p>๐ Browser: <b>{BROWSER}</b></p> | |
| <p>๐ฅ Active users: <b>{n}</b></p> | |
| <p>๐ Display range: <b>:{DISPLAY_BASE} โ :{DISPLAY_MAX}</b></p> | |
| <hr style="border-color:#333"> | |
| <h3>๐ค User Sessions</h3> | |
| <table><tr><th>User ID</th><th>Display</th><th>Status</th></tr> | |
| {users_info or '<tr><td colspan="3" style="text-align:center;color:#666">No active sessions</td></tr>'} | |
| </table> | |
| <hr style="border-color:#333"> | |
| <p class="info">Endpoints: /health ยท /ws?user_id=email (WebSocket)</p> | |
| </body></html>""" | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโ Action Handler (per-session) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| async def handle_action(ws: WebSocket, msg: dict, sess: dict): | |
| action = msg.get("action", "") | |
| data = msg.get("data", {}) | |
| display = sess["display"] | |
| async def send(obj): | |
| try: | |
| await ws.send_text(json.dumps(obj, ensure_ascii=False)) | |
| except Exception: | |
| pass | |
| # โโ ุฅุตูุงุญ ุฌุฐุฑู: ููู ุฎุงุต ุจูู session ูู ูุน ุชุฑุงูู /ุชุถุงุฑุจ ุนู ููุงุช capture ุงูู ุชุฒุงู ูุฉ โโ | |
| # ุงูู ุดููุฉ ุงูุฃุตููุฉ: action == "screenshot" ูุงู ููููููุฐ ุจู await ู ุจุงุดุฑ ุถู ู ุญููุฉ | |
| # while True ุงูุฑุฆูุณูุฉ ูู websocket_endpointุ ูุฅุฐุง ุชุฃุฎุฑุช capture_with_grid (ูุฏ ุชุตู | |
| # ูุฏูููุฉ ููุตู ูู ุฃุณูุฃ ุณููุงุฑูู ูุดู/ุชุฃุฎุฑ scrot+import+ffmpeg)ุ ุชุจูู ุงูุญููุฉ ูููุง | |
| # ู ุญุฌูุจุฉ ููุง ุชุณุชูุจู ุฃู ุฑุณุงูุฉ ุนู ูู ุฌุฏูุฏุฉ (ููุฑุฉุ ุทูุจ screenshot ุขุฎุฑุ ุฅูุฎ) ุญุชู ุชูุชูู. | |
| # ุงูุญู: ูู ุทูุจ screenshot ููุดุบููู ููุฑุงู ุนุจุฑ create_task (ูุง ูุญุฌุจ ุงูุญููุฉ ุฃุจุฏุงู). | |
| # | |
| # โโ ุฅุตูุงุญ ุซุงูู (ู ูู ุฌุฏุงู): ุทูุจ screenshot ุงูุตุฑูุญ ู ู ุงูุนู ูู ูู ุฃููููุฉ ู ุทููุฉ โโ | |
| # ุงูู ุดููุฉ ุงูู ูุชุดูุฉ ูุงุญูุงู: ูู ุงุณุชูุฎุฏู ููู ูุงุญุฏ ู ุดุชุฑู ุจูู shot_bg (ุชููุงุฆูุฉุ ุจุนุฏ | |
| # ูู ูุนู ูููุฑุฉ/ูุชุญ ุชุจููุจ) ู shot_explicit (ุตุฑูุญุฉุ ูุทูุจูุง ุงูุนู ูู ู ุจุงุดุฑุฉ ุจุนุฏ ูู | |
| # ุฎุทูุฉ ูุชุญุฏูุซ ุงูุตูุฑุฉ ุงูู ุนุฑูุถุฉ)ุ ูุฅู shot_explicit ูุฏ ุชูุชุธุฑ ุฎูู shot_bg ุทูููุงู | |
| # ุฅุฐุง ูุงูุช ุงูุฃุฎูุฑุฉ ูุฏ ุจุฏุฃุช ูุนูุงู ูุนููุช ุฏุงุฎู capture_with_grid (ู ุญุงููุงุช ูุงุดูุฉ | |
| # ู ุชุชุงููุฉ). ูุฐุง ูุฌุนู ุงูุนู ูู ูุฑู "ูู ุชุตู ููุทุฉ ุดุงุดุฉ" ุจุดูู ู ุชูุฑุฑ ุญุชู ุจุนุฏ ูุฌุงุญ | |
| # ุงูุฎุทูุฉ ุงููุนููุฉ (ู ุซู open_tab)ุ ูุฃู ุทูุจู ุงูุนุงุฌู ูุงู ูุตุทู ุฎูู ุนู ููุฉ ุชููุงุฆูุฉ ุจุทูุฆุฉ. | |
| # ุงูุญู: shot_explicit (ุงูุตุฑูุญุฉ ููุท) ุชุญุงูู ุงูุญุตูู ุนูู ุงูููู ููุชุฑุฉ ูุตูุฑุฉ ุฌุฏุงู | |
| # (1 ุซุงููุฉ)ุ ูุฅู ูู ุชูุฌุญ (ูุฃู shot_bg ุชุณุชุฎุฏู ู)ุ ุชูููููุฐ ุงูุชุตููุฑ ู ุจุงุดุฑุฉ ุจุฏูู ููู | |
| # ุจุฏู ุงูุงูุชุธุงุฑ โ ูุฃู ุงุณุชุฌุงุจุฉ ุงูุนู ูู ุงูููุฑูุฉ ุฃูู ู ู ุชูุงุฏู ุชุฒุงุญู CPU ุนุฑุถู ุจุณูุทุ | |
| # ููุฑุงุกุฉ ุงูุดุงุดุฉ (X11) ุนู ููุฉ ูุฑุงุกุฉ ููุท ูุง ุชูุณุจุจ ุฃู ุชูู ุจูุงูุงุช ุนูุฏ ุงูุชุฒุงุญู . | |
| if "_shot_lock" not in sess: | |
| sess["_shot_lock"] = asyncio.Lock() | |
| _shot_lock = sess["_shot_lock"] | |
| async def _safe_capture(scale, quality, force_mx=None, force_my=None): | |
| """ | |
| ุชูุณุชุฎุฏู ู ู shot_bg (ุงูุชููุงุฆูุฉ) โ ุชูุชุธุฑ ุงูููู ุซู ุชูุชุธุฑ ุงูุชูุงุท ุงูุดุงุดุฉ | |
| ุญุชู ููุชูู ูุนููุงูุ ุจุฏูู ุฃู ุณูู ุฒู ูู ููุทุนูุง. ูุฐุง ูุถู ู ุฃู ุงูุนู ููุฉ ูู | |
| ุชููุทุน ุฃุจุฏุงู ูู ู ูุชุตููุง ูุชูุฑุฌุน ูุฑุงุบุงูุ ุณุชูู ู ุญุชู ุชูุฌุญ (ุฃู ุชูุดู ูู | |
| ุงูู ุญุงููุงุช ุงูุฏุงุฎููุฉ ูู capture_with_grid ูุชูุฑุฌุน placeholder ุตุฑูุญ). | |
| """ | |
| async with _shot_lock: | |
| return await asyncio.to_thread( | |
| capture_with_grid, display, scale, quality, force_mx, force_my | |
| ) | |
| async def _priority_capture(scale, quality, force_mx=None, force_my=None): | |
| """ | |
| ุชูุณุชุฎุฏู ู ู shot_explicit (ุงูุทูุจ ุงูุตุฑูุญ ู ู ุงูุนู ูู) ููุท โ ุฃููููุฉ ูุตูู. | |
| ุชุญุงูู ุงูุญุตูู ุนูู ุงูููู ูู ุฏุฉ ูุตูุฑุฉ (1 ุซุงููุฉ) ููุทุ ุฅู ูู ุชูุฌุญ (ุงูููู | |
| ู ุญุฌูุฒ ู ู shot_bg ุชููุงุฆูุฉ ุจุทูุฆุฉ)ุ ุชูููููุฐ ุงูุชุตููุฑ ููุฑุงู ุจุฏูู ููู ุจุฏู | |
| ุงูุงูุชุธุงุฑ ุฎูู ุนู ููุฉ ุฃุฎุฑู โ ุงูุนู ูู ูุฌุจ ุฃู ูุญุตู ุนูู ุฑุฏ ุณุฑูุน ุฏุงุฆู ุงู. | |
| ุจุนุฏ ุงูุญุตูู ุนูู ุงูููู (ุฃู ุชุฌุงูุฒู)ุ ูุง ููุฌุฏ ุฃู ุณูู ุฒู ูู ุนูู ุนู ููุฉ | |
| ุงูุงูุชูุงุท ููุณูุง โ ุชูุชุธุฑ ุญุชู ุชูุชู ู ูุนููุงู ุจุฏู ุฃู ุชููุทุน ูู ุงูู ูุชุตู. | |
| """ | |
| try: | |
| await asyncio.wait_for(_shot_lock.acquire(), timeout=1.0) | |
| try: | |
| return await asyncio.to_thread( | |
| capture_with_grid, display, scale, quality, force_mx, force_my | |
| ) | |
| finally: | |
| _shot_lock.release() | |
| except asyncio.TimeoutError: | |
| # ูู ูุญุตู ุนูู ุงูููู ุจุณุฑุนุฉ ูุงููุฉ โ ููููุฐ ุงูุชุตููุฑ ู ุจุงุดุฑุฉ ุจุฏูู ููู | |
| # ุจุฏู ุงูุงูุชุธุงุฑ ุฎูู ุนู ููุฉ ุฃุฎุฑูุ ููู ุจุฏูู ุฃู ุณูู ุฒู ูู ุนูู ุงูุงูุชูุงุท | |
| return await asyncio.to_thread( | |
| capture_with_grid, display, scale, quality, force_mx, force_my | |
| ) | |
| async def shot_bg(label: str = "", delay: float = 0.5, | |
| force_mx: int | None = None, force_my: int | None = None, | |
| extra_shot_delay: float | None = None): | |
| """ | |
| auto_shot ูู ุงูุฎูููุฉ โ ู ุน rate limit + delta suppression. | |
| ุฅุฐุง ุชู ุชู ุฑูุฑ extra_shot_delayุ ุชูุฑุณู ููุทุฉ ุซุงููุฉ ุฅุถุงููุฉ ุจุนุฏ ุฐูู ุงูุชุฃุฎูุฑ | |
| ุงูุฅุถุงูู (ู ุญุณูุจุงู ู ู ููุช ุงูุชูุงุก ุงูููุทุฉ ุงูุฃููู) โ ูุฐุง ูุบุทู ุญุงูุฉ ุงูููุฑ ุนูู | |
| ุฑุงุจุท ูููู ูุตูุญุฉ ุฌุฏูุฏุฉ ูุฏ ูุง ุชูุชู ู ุชุญู ูููุง ุฎูุงู ุงูุชุฃุฎูุฑ ุงูุฃููู ุงููุตูุฑ | |
| (ู ุซูุงู ุตูุญุฉ ุจุทูุฆุฉ ุนูู ุณูุฑูุฑ ู ุญุฏูุฏ ุงูู ูุงุฑุฏ)ุ ุฏูู ุงูุญุงุฌุฉ ูู ุนุฑูุฉ ู ุณุจูุฉ ุจููุน | |
| ุงูุนูุตุฑ ุงูู ูููุฑ ุนููู. ุชุชุฌุงูู delta suppression ููุฐู ุงูููุทุฉ ุงูุซุงููุฉ ุชุญุฏูุฏุงู | |
| ูุฃู ุงูู ุญุชูู ู ุชููุน ุฃู ูููู ู ุฎุชููุงู (ุตูุญุฉ ุชุญู ููุช ุฃูุซุฑ) ุญุชู ูู ุชุดุงุจู ุงูู hash | |
| ุฌุฒุฆูุงู ู ุน ุฎูููุฉ ู ุดุงุจูุฉ. | |
| """ | |
| try: | |
| await asyncio.sleep(delay) | |
| now = time.time() | |
| if now - sess.get("last_bg_shot_ts", 0) < 0.3: | |
| await asyncio.sleep(0.3 - (now - sess["last_bg_shot_ts"])) | |
| result = await _safe_capture(0.65, 72, force_mx, force_my) | |
| sess["last_bg_shot_ts"] = time.time() | |
| if result["data"]: | |
| fh = _frame_hash(result["data"]) | |
| if fh != sess.get("last_bg_hash", ""): | |
| sess["last_bg_hash"] = fh | |
| await send({ | |
| "type": "screenshot", | |
| "data": result["data"], | |
| "data_grid": result.get("data_grid", ""), | |
| "data_grid2": result.get("data_grid2", ""), | |
| "data_grid3": result.get("data_grid3", ""), | |
| "ts": int(time.time() * 1000), | |
| "auto": True, "label": label, | |
| "screen_width": result["width"], | |
| "screen_height": result["height"], | |
| "mouse_x": result["mouse_x"], | |
| "mouse_y": result["mouse_y"], | |
| "has_grid": True, | |
| }) | |
| if extra_shot_delay: | |
| await asyncio.sleep(extra_shot_delay) | |
| result2 = await _safe_capture(0.65, 72, force_mx, force_my) | |
| sess["last_bg_shot_ts"] = time.time() | |
| if result2["data"]: | |
| fh2 = _frame_hash(result2["data"]) | |
| sess["last_bg_hash"] = fh2 | |
| await send({ | |
| "type": "screenshot", | |
| "data": result2["data"], | |
| "data_grid": result2.get("data_grid", ""), | |
| "data_grid2": result2.get("data_grid2", ""), | |
| "data_grid3": result2.get("data_grid3", ""), | |
| "ts": int(time.time() * 1000), | |
| "auto": True, "label": f"{label} (delayed)", | |
| "screen_width": result2["width"], | |
| "screen_height": result2["height"], | |
| "mouse_x": result2["mouse_x"], | |
| "mouse_y": result2["mouse_y"], | |
| "has_grid": True, | |
| }) | |
| except Exception as e: | |
| # ููุทุฉ ุฎูููุฉ ูุงุดูุฉ ูุง ูุฌุจ ุฃู ุชููู ุงูุฌูุณุฉ ุฃู ุชุธูุฑ ุจุตู ุช ูู ุงูููู ููุท. | |
| print(f"[shot_bg:{display}] โ ๏ธ {e}") | |
| async def shot_explicit(label: str = ""): | |
| """screenshot ุตุฑูุญ โ ููุฑุณู ุฏุงุฆู ุงู ุจุฏูู delta suppressionุ ููู ุฃููููุฉ ู ุทููุฉ | |
| ุนูู ุฃู ุนู ููุฉ shot_bg ุชููุงุฆูุฉ ุฌุงุฑูุฉ (ูุง ููุชุธุฑ ุฎูููุง ุทูููุงู). | |
| โโ ุฅุตูุงุญ ุฌุฐุฑู: try/except ุดุงู ู + ุฅุฑุณุงู ุฑุณุงูุฉ ุฎุทุฃ ุตุฑูุญุฉ ููุนู ูู โโ | |
| ุงูู ุดููุฉ ุงููุฏูู ุฉ: ูุฐู ุงูุฏุงูุฉ ุชูุดุบููู ุนุจุฑ asyncio.create_task ุจุฏูู ุฃู | |
| ู ุนุงูุฌุฉ ุงุณุชุซูุงุก ู ุญูุทุฉ ุจูุง. ุฃู ุฎุทุฃ ุบูุฑ ู ุชููุน ูุงู ููุณูุท ุงูู task ุจุงููุงู ู | |
| ุจุตู ุช ุชุงู โ ูุง ุฑุณุงูุฉ ุชุตู ููุนู ูู ููุง ุญุชู ุณุทุฑ ูู ุงููููุ ููุจูู ุงูุนู ูู | |
| ููุชุธุฑ frame ูู ูุตู ุฃุจุฏุงู. ุงูุขู ุฃู ูุดู ููุณุฌููู ูููุฑุณูู ูุฑุณุงูุฉ "error" | |
| ุตุฑูุญุฉุ ูุงูุนู ูู (index.html) ูุชุนุงู ู ู ุนูุง ููุนูุฏ ุงูู ุญุงููุฉ ููุฑุงู.""" | |
| try: | |
| result = await _priority_capture(0.65, 75) | |
| if not result or not result["data"]: | |
| print(f"[shot_explicit:{display}] โ ๏ธ empty capture result") | |
| await send({"type": "error", "action": "screenshot", "msg": "capture returned empty"}) | |
| return | |
| sess["last_bg_hash"] = _frame_hash(result["data"]) | |
| await send({ | |
| "type": "screenshot", | |
| "data": result["data"], | |
| "data_grid": result.get("data_grid", ""), | |
| "data_grid2": result.get("data_grid2", ""), | |
| "data_grid3": result.get("data_grid3", ""), | |
| "ts": int(time.time() * 1000), | |
| "auto": False, "label": label, | |
| "screen_width": result["width"], | |
| "screen_height": result["height"], | |
| "mouse_x": result["mouse_x"], | |
| "mouse_y": result["mouse_y"], | |
| "has_grid": True, | |
| }) | |
| except Exception as e: | |
| print(f"[shot_explicit:{display}] โ EXCEPTION: {e}") | |
| try: | |
| await send({"type": "error", "action": "screenshot", "msg": str(e)}) | |
| except Exception: | |
| pass | |
| # โโ reset_computer: ุฅุนุงุฏุฉ ุถุจุท ุงููู ุจููุชุฑ ูุฃูู ุฌุฏูุฏ โโโโโโโโโ | |
| if action == "reset_computer": | |
| user_id = sess["user_id"] | |
| await send({"type": "ack", "action": "reset_computer", "status": "resetting"}) | |
| await reset_user_computer(user_id) | |
| # ุฃุฑุณู ููุทุฉ ุดุงุดุฉ ุจุนุฏ ุงูุฑูุณุชุงุฑุช | |
| await asyncio.sleep(2.0) | |
| await shot_explicit("after reset") | |
| await send({"type": "computer_reset", "msg": "โ ุชู ุฅุนุงุฏุฉ ุถุจุท ุงููู ุจููุชุฑ โ ุงูุดุงุดุฉ ุฌุฏูุฏุฉ ุชู ุงู ุงู"}) | |
| return | |
| # โโ screenshot โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # ุฅุตูุงุญ: create_task ุจุฏู await ู ุจุงุดุฑ โ ูุง ูุญุฌุจ ุญููุฉ ุงุณุชูุจุงู ุงูุฑุณุงุฆู ุฃุจุฏุงูุ | |
| # ุญุชู ูู ุชุฃุฎุฑุช capture_with_grid ูุฃู ุณุจุจ (CPU ุถุนููุ X11 ุจุทูุกุ ุฅูุฎ). | |
| # ูุฐุง ูุถู ู ุฃู ุฃู ุฅุฌุฑุงุก ูุงุญู (ููุฑุฉุ ุทุจุงุนุฉุ ุทูุจ screenshot ุขุฎุฑ) ูุตู ูููุนุงููุฌ | |
| # ููุฑุงู ุฏูู ุงูุชุธุงุฑ ุงูุชู ุงู ูุฐู ุงูููุทุฉ ุฃููุงู. | |
| if action == "screenshot": | |
| # โโ ุทุจูุฉ ุญู ุงูุฉ ุฅุถุงููุฉ: ุญุชู ูู ุญุตู ุฎุทุฃ ุบูุฑ ู ุชููุน ุชู ุงู ุงู ุฏุงุฎู | |
| # shot_explicit ุชููุช ู ู ุงูู try/except ุงูุฏุงุฎูู (ู ุซู CancelledError | |
| # ุฃู ุฎุทุฃ ุนูุฏ ุฅูุดุงุก ุงูู task ููุณู)ุ ูุฐุง ุงูุบูุงู ุงูุฎุงุฑุฌู ูุถู ู ุชุณุฌููู | |
| # ูู ุงูููู ุจุฏู ุฃู ูุฎุชูู ุจุตู ุช ูู "unhandled task exception" ูู event | |
| # loop ุจุงูุซูู (ููุฐุง ูุงู ูู ุณููู ุงูุชุฑุงุถู ุจุตุงู ุช ูู ุจุนุถ ุฅุนุฏุงุฏุงุช uvicorn). | |
| _task = asyncio.create_task(shot_explicit("explicit screenshot")) | |
| def _on_shot_done(t: asyncio.Task): | |
| exc = t.exception() if not t.cancelled() else None | |
| if exc: | |
| print(f"[screenshot-task:{display}] โ unhandled exception: {exc}") | |
| _task.add_done_callback(_on_shot_done) | |
| # โโ terminal โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| elif action == "terminal": | |
| cmd = data.get("cmd", "") | |
| if not cmd: | |
| await send({"type": "terminal_result", "stdout": "", "stderr": "no cmd", "returncode": -1}) | |
| return | |
| res = await run_cmd_smart(cmd, display, int(data.get("timeout", 60))) | |
| await send({ | |
| "type": "terminal_result", | |
| "cmd": cmd, | |
| "stdout": res["stdout"], | |
| "stderr": res.get("stderr", ""), | |
| "returncode": res["returncode"], | |
| }) | |
| # โโ ุชุนุทูู ุงูุงูุชูุงุท ุงูุชููุงุฆู: ูุง ุชูุคุฎุฐ ููุทุฉ ุดุงุดุฉ ุฅูุง ุจุทูุจ ุตุฑูุญ โโ | |
| # โโ mouse_move โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| elif action == "mouse_move": | |
| x, y = int(data.get("x", 0)), int(data.get("y", 0)) | |
| await xdo(["mousemove", "--sync", str(x), str(y)], display) | |
| await send({"type": "ack", "action": "mouse_move", "x": x, "y": y}) | |
| # โโ ุชุนุทูู ุงูุงูุชูุงุท ุงูุชููุงุฆู โโ | |
| # โโ mouse_click โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| elif action == "mouse_click": | |
| x, y = int(data.get("x", 0)), int(data.get("y", 0)) | |
| btn = {"left": "1", "middle": "2", "right": "3"}.get(data.get("button", "left"), "1") | |
| double = data.get("double", False) | |
| await xdo(["mousemove", "--sync", str(x), str(y)], display) | |
| await asyncio.sleep(0.07) | |
| if double: | |
| await xdo(["click", "--repeat", "2", "--delay", "100", btn], display) | |
| else: | |
| await xdo(["click", btn], display) | |
| btn_name = {"1": "left", "2": "middle", "3": "right"}.get(btn, "left") | |
| await send({"type": "ack", "action": "mouse_click", "x": x, "y": y, "button": btn_name}) | |
| # โโ ุชุนุทูู ุงูุงูุชูุงุท ุงูุชููุงุฆู โโ | |
| # โโ mouse_drag โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| elif action == "mouse_drag": | |
| x1, y1 = int(data.get("x1", 0)), int(data.get("y1", 0)) | |
| x2, y2 = int(data.get("x2", 0)), int(data.get("y2", 0)) | |
| await xdo(["mousemove", str(x1), str(y1)], display) | |
| await xdo(["mousedown", "1"], display) | |
| await asyncio.sleep(0.1) | |
| await xdo(["mousemove", str(x2), str(y2)], display) | |
| await asyncio.sleep(0.1) | |
| await xdo(["mouseup", "1"], display) | |
| await send({"type": "ack", "action": "mouse_drag"}) | |
| # โโ ุชุนุทูู ุงูุงูุชูุงุท ุงูุชููุงุฆู โโ | |
| # โโ keyboard_type โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| elif action == "keyboard_type": | |
| text = _safe_search(data.get("text", "")) | |
| if text: | |
| res = await type_smart(text, display) | |
| await send({"type": "ack", "action": "keyboard_type", "method": res["method"]}) | |
| # โโ ุชุนุทูู ุงูุงูุชูุงุท ุงูุชููุงุฆู โโ | |
| # โโ keyboard_hotkey โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| elif action == "keyboard_hotkey": | |
| keys = data.get("keys", []) | |
| if keys: | |
| await xdo(["key", "--clearmodifiers", "+".join(keys)], display) | |
| await send({"type": "ack", "action": "keyboard_hotkey", "keys": keys}) | |
| # โโ ุชุนุทูู ุงูุงูุชูุงุท ุงูุชููุงุฆู โโ | |
| # โโ keyboard_press โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| elif action == "keyboard_press": | |
| key = data.get("key", "") | |
| if key: | |
| await xdo(["key", "--clearmodifiers", key], display) | |
| await send({"type": "ack", "action": "keyboard_press"}) | |
| # โโ ุชุนุทูู ุงูุงูุชูุงุท ุงูุชููุงุฆู โโ | |
| # โโ scroll โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| elif action == "scroll": | |
| x, y = int(data.get("x", 960)), int(data.get("y", 540)) | |
| clicks = max(-5, min(5, int(data.get("clicks", 3)))) | |
| btn = "4" if clicks > 0 else "5" | |
| await xdo(["mousemove", str(x), str(y)], display) | |
| for _ in range(abs(clicks)): | |
| await xdo(["click", btn], display) | |
| await asyncio.sleep(0.025) | |
| await send({"type": "ack", "action": "scroll", "clicks": clicks}) | |
| # โโ ุชุนุทูู ุงูุงูุชูุงุท ุงูุชููุงุฆู โโ | |
| # โโ clipboard_write โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| elif action == "clipboard_write": | |
| text = data.get("text", "") | |
| env = {**os.environ, "DISPLAY": display} | |
| def _clip(): | |
| p = subprocess.Popen(["xclip", "-selection", "clipboard"], stdin=subprocess.PIPE, env=env) | |
| p.communicate(text.encode("utf-8")) | |
| await asyncio.to_thread(_clip) | |
| await send({"type": "ack", "action": "clipboard_write", "length": len(text)}) | |
| # โโ clipboard_read โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| elif action == "clipboard_read": | |
| res = await run_cmd("xclip -selection clipboard -o", display, 5) | |
| await send({"type": "clipboard_content", "text": res["stdout"]}) | |
| # โโ paste โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| elif action == "paste": | |
| text = data.get("text", "") | |
| if text: | |
| env = {**os.environ, "DISPLAY": display} | |
| def _clip2(): | |
| p = subprocess.Popen(["xclip", "-selection", "clipboard"], stdin=subprocess.PIPE, env=env) | |
| p.communicate(text.encode("utf-8")) | |
| await asyncio.to_thread(_clip2) | |
| await asyncio.sleep(0.1) | |
| await xdo(["key", "--clearmodifiers", "ctrl+v"], display) | |
| await send({"type": "ack", "action": "paste"}) | |
| # โโ ุชุนุทูู ุงูุงูุชูุงุท ุงูุชููุงุฆู โโ | |
| # โโ open_app โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| elif action == "open_app": | |
| cmd = _safe_search(data.get("cmd", "")) | |
| if not cmd: | |
| await send({"type": "ack", "action": "open_app"}) | |
| return | |
| if "firefox" in cmd.lower(): | |
| profile_dir = _ensure_firefox_profile(sess["user_id"], display) | |
| # ูุชู ููุฑู ูููู (SIGKILL) ุจุฏู pkill ุงููุทูู: ูู ูุน ูุงูุฑูููุณ ู ู | |
| # "ุงูุชูุงุท" ุญุงูุฉ ูุตู-ู ูุชูุญุฉ ุชูุณุฌููู ูุงุญูุงู ูุชุญุทูู (crash) ูููุชุฌ | |
| # ุดุงุดุฉ "Restore Session" ูู ุงูู ุฑุฉ ุงููุงุฏู ุฉ. | |
| await run_cmd( | |
| "pkill -9 -f '[f]irefox' 2>/dev/null; sleep 0.6; echo CLEANED", | |
| display, timeout=8 | |
| ) | |
| _wipe_firefox_session_data(profile_dir) | |
| # ุญูู --profile ุฏุงุฎู ุฃู ุฑ ูุงูุฑูููุณ (ุฅู ูู ููู ู ุญูููุงู ู ุณุจูุงู) | |
| if "--profile" not in cmd and "-P " not in cmd: | |
| flags_str = " ".join(FIREFOX_CLI_FLAGS) | |
| cmd = cmd.replace("firefox", f"firefox --profile '{profile_dir}' {flags_str}", 1) | |
| env = {**os.environ, "DISPLAY": display, **(FIREFOX_ENV_EXTRA if "firefox" in cmd.lower() else {})} | |
| proc = subprocess.Popen(cmd, shell=True, env=env, | |
| stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) | |
| if any(b in cmd for b in ("firefox", "chromium", "chrome")): | |
| with _display_lock: | |
| if sess["user_id"] in _user_displays: | |
| _user_displays[sess["user_id"]]["browser_proc"] = proc | |
| await send({"type": "ack", "action": "open_app", "cmd": cmd}) | |
| # โโ ุชุนุทูู ุงูุงูุชูุงุท ุงูุชููุงุฆู ุจุนุฏ ูุชุญ ุงูุชุทุจูู โโ | |
| # โโ open_browser โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| elif action == "open_browser": | |
| url = _safe_search(data.get("url", "") or "about:blank") | |
| profile_dir = _ensure_firefox_profile(sess["user_id"], display) | |
| # ูุชู ููุฑู ูููู (SIGKILL) ุจุฏู pkill ุงููุทูู โ ูู ูุน ุชุณุฌูู ุญุงูุฉ | |
| # "ุชุญุทูู " ูู sessionstoreุ ููู ุงูุณุจุจ ุงูุฌุฐุฑู ูุดุงุดุฉ "Restore Session" | |
| # ุงูุชู ุชุญุฌุจ ุงูู ุญุชูู ุงููุนูู ูุชููุชูุท ูููุทุฉ ุดุงุดุฉ ุตุงูุญุฉ (ุบูุฑ ุณูุฏุงุก) | |
| # ููููุง ููุณุช ุงูุตูุญุฉ ุงูู ุทููุจุฉ. | |
| await run_cmd( | |
| "pkill -9 -f '[f]irefox' 2>/dev/null; sleep 0.6; echo CLEANED", | |
| display, timeout=8 | |
| ) | |
| _wipe_firefox_session_data(profile_dir) | |
| env = {**os.environ, "DISPLAY": display, **FIREFOX_ENV_EXTRA} | |
| proc = subprocess.Popen( | |
| [BROWSER, "--profile", profile_dir, *FIREFOX_CLI_FLAGS, url], env=env, | |
| stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL | |
| ) | |
| with _display_lock: | |
| if sess["user_id"] in _user_displays: | |
| _user_displays[sess["user_id"]]["browser_proc"] = proc | |
| await send({"type": "ack", "action": "open_browser", "url": url}) | |
| # โโ ุชุนุทูู ุงูุงูุชูุงุท ุงูุชููุงุฆู ุจุนุฏ ูุชุญ ุงูู ุชุตูุญ โโ | |
| # โโ open_tab โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| elif action == "open_tab": | |
| # ุฅุตูุงุญ: ุฒูุงุฏุฉ ุงูุชุฃุฎูุฑุงุช ุจูู ูู ุฎุทูุฉ ูุฑุนูุฉ ููุง (Ctrl+T โ Ctrl+L โ ูุชุงุจุฉ ุงูุฑุงุจุท) | |
| # ุงูู ุดููุฉ ุงูุฃุตููุฉ: ุงูุชุฃุฎูุฑ ุงููุตูุฑ (0.4s) ุจุนุฏ Ctrl+T ูู ููู ูุงููุงู ุฏุงุฆู ุงูุ ุฎุตูุตุงู | |
| # ุนูู ุณูุฑูุฑ ู ุญุฏูุฏ ุงูู ูุงุฑุฏ ูุฏ ูุณุชุบุฑู ูุงูุฑูููุณ ููู ููุชุงู ุฃุทูู ูุฅูุดุงุก ุงูุชุจููุจ ุงูุฌุฏูุฏ | |
| # ูุนููุงู ูุชุญููู ุงูุชุฑููุฒ (focus) ุฅููู. ุฅุฐุง ูุตู Ctrl+L ูุงููุชุงุจุฉ ูุจู ุงูุชู ุงู ุฐููุ | |
| # ูุฅููุง ูุฏ ุชุฐูุจ ููุชุจููุจ ุงููุฏูู ุจุงูุฎุทุฃุ ููุง ุชูุชูู ุงูุตูุญุฉ ูุนููุงู ุฑุบู ูุฌุงุญ ุงูุฃู ุฑ | |
| # ุธุงูุฑูุงู (ุจุฏูู ุฃู ุฑุณุงูุฉ ุฎุทุฃ)ุ ููุจูู ุงูู ุณุชุฎุฏู ูุฑู ููุณ ุงูุตูุญุฉ ุงููุฏูู ุฉ ููุฃุจุฏ. | |
| url = _safe_search(data.get("url", "") or "about:blank") | |
| await xdo(["key", "--clearmodifiers", "ctrl+t"], display) | |
| await asyncio.sleep(0.8) | |
| await xdo(["key", "--clearmodifiers", "ctrl+l"], display) | |
| await asyncio.sleep(0.3) | |
| await type_smart(url, display) | |
| await asyncio.sleep(0.3) | |
| await xdo(["key", "--clearmodifiers", "Return"], display) | |
| await send({"type": "ack", "action": "open_tab", "url": url}) | |
| # โโ ุชุนุทูู ุงูุงูุชูุงุท ุงูุชููุงุฆู โโ | |
| # โโ close_tab โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| elif action == "close_tab": | |
| await xdo(["key", "--clearmodifiers", "ctrl+w"], display) | |
| await send({"type": "ack", "action": "close_tab"}) | |
| # โโ ุชุนุทูู ุงูุงูุชูุงุท ุงูุชููุงุฆู โโ | |
| # โโ browser_back โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| elif action == "browser_back": | |
| await xdo(["key", "--clearmodifiers", "alt+Left"], display) | |
| await send({"type": "ack", "action": "browser_back"}) | |
| # โโ ุชุนุทูู ุงูุงูุชูุงุท ุงูุชููุงุฆู โโ | |
| # โโ browser_forward โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| elif action == "browser_forward": | |
| await xdo(["key", "--clearmodifiers", "alt+Right"], display) | |
| await send({"type": "ack", "action": "browser_forward"}) | |
| # โโ ุชุนุทูู ุงูุงูุชูุงุท ุงูุชููุงุฆู โโ | |
| # โโ browser_search โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| elif action == "browser_search": | |
| url = _safe_search(data.get("url", "") or data.get("query", "")) | |
| await xdo(["key", "--clearmodifiers", "ctrl+l"], display) | |
| await asyncio.sleep(0.2) | |
| await type_smart(url, display) | |
| await asyncio.sleep(0.15) | |
| await xdo(["key", "--clearmodifiers", "Return"], display) | |
| await send({"type": "ack", "action": "browser_search"}) | |
| # โโ ุชุนุทูู ุงูุงูุชูุงุท ุงูุชููุงุฆู โโ | |
| # โโ screen_info โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| elif action == "screen_info": | |
| w, h = await asyncio.to_thread(_get_screen_size, display) | |
| mx, my = await asyncio.to_thread(_get_mouse_pos, display) | |
| await send({ | |
| "type": "screen_info", | |
| "width": w, "height": h, | |
| "mouse_x": mx, "mouse_y": my, | |
| "browser": BROWSER, | |
| "display": display, | |
| }) | |
| # โโ unknown โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| else: | |
| await send({"type": "error", "msg": f"Unknown action: '{action}'"}) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโ WebSocket Endpoint โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| async def websocket_endpoint( | |
| ws: WebSocket, | |
| user_id: str = Query(default="anonymous") | |
| ): | |
| await ws.accept() | |
| # ุงูุญุตูู ุนูู session ุงูู ุณุชุฎุฏู ุฃู ุฅูุดุงุก ูุงุญุฏุฉ ุฌุฏูุฏุฉ | |
| sess = await get_or_create_user_session(user_id, ws) | |
| # ุชุฃูุฏ ุฃู Xvfb ูุนู ู ููุฐุง ุงูู ุณุชุฎุฏู | |
| await ensure_xvfb_for_session(sess) | |
| display = sess["display"] | |
| async def _heartbeat(): | |
| while True: | |
| await asyncio.sleep(20) | |
| try: | |
| await ws.send_text(json.dumps({"type": "ping", "ts": int(time.time()*1000)})) | |
| except Exception: | |
| break | |
| hb_task = asyncio.create_task(_heartbeat()) | |
| try: | |
| w, h = await asyncio.to_thread(_get_screen_size, display) | |
| await ws.send_text(json.dumps({ | |
| "type": "connected", | |
| "screen_width": w, "screen_height": h, | |
| "browser": BROWSER, | |
| "display": display, | |
| "user_id": user_id, | |
| "session_id": id(ws), | |
| "msg": f"Z Computer Mode v11 | User: {user_id} | Display: {display} | Browser: {BROWSER} | Screen: {w}x{h}", | |
| }, ensure_ascii=False)) | |
| # ููุทุฉ ุดุงุดุฉ ุฃูููุฉ | |
| result = await asyncio.to_thread(capture_with_grid, display, 0.65, 72) | |
| if result["data"]: | |
| await ws.send_text(json.dumps({ | |
| "type": "screenshot", | |
| "data": result["data"], | |
| "data_grid": result.get("data_grid", ""), | |
| "data_grid2": result.get("data_grid2", ""), | |
| "data_grid3": result.get("data_grid3", ""), | |
| "ts": int(time.time() * 1000), | |
| "label": "Initial screen", | |
| "screen_width": result["width"], | |
| "screen_height": result["height"], | |
| "mouse_x": result["mouse_x"], | |
| "mouse_y": result["mouse_y"], | |
| "has_grid": True, | |
| }, ensure_ascii=False)) | |
| sess["last_bg_hash"] = _frame_hash(result["data"]) | |
| except Exception as e: | |
| print(f"[ws:{user_id}] init error: {e}") | |
| try: | |
| while True: | |
| raw = await ws.receive_text() | |
| try: | |
| msg = json.loads(raw) | |
| if msg.get("type") == "pong": | |
| continue | |
| await handle_action(ws, msg, sess) | |
| except json.JSONDecodeError: | |
| pass | |
| except WebSocketDisconnect: | |
| pass | |
| except Exception as e: | |
| print(f"[ws:{user_id}] error: {e}") | |
| finally: | |
| hb_task.cancel() | |
| await destroy_user_ws(user_id, ws) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโ REST Endpoints โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| async def rest_screenshot(user_id: str = "anonymous"): | |
| with _display_lock: | |
| sess = _user_displays.get(user_id) | |
| display = sess["display"] if sess else f":{DISPLAY_BASE}" | |
| result = await asyncio.to_thread(capture_with_grid, display, 0.7, 75) | |
| return JSONResponse({ | |
| "image": result["data"], | |
| "image_grid": result.get("data_grid", ""), | |
| "image_grid2": result.get("data_grid2", ""), | |
| "image_grid3": result.get("data_grid3", ""), | |
| "ts": int(time.time() * 1000), | |
| "screen_width": result["width"], | |
| "screen_height": result["height"], | |
| "mouse_x": result["mouse_x"], | |
| "mouse_y": result["mouse_y"], | |
| "has_grid": True, | |
| "display": display, | |
| "user_id": user_id, | |
| }) | |
| async def rest_terminal(body: dict): | |
| user_id = body.get("user_id", "anonymous") | |
| with _display_lock: | |
| sess = _user_displays.get(user_id) | |
| display = sess["display"] if sess else f":{DISPLAY_BASE}" | |
| return JSONResponse(await run_cmd_smart(body.get("cmd", ""), display, body.get("timeout", 60))) | |
| async def health(): | |
| with _display_lock: | |
| n = len(_user_displays) | |
| users = [ | |
| {"user_id": uid, "display": s["display"], | |
| "connected": bool(s.get("active_ws"))} | |
| for uid, s in _user_displays.items() | |
| ] | |
| return { | |
| "status": "ok", | |
| "version": "v13-fast-capture-fixed-firefox", | |
| "browser": BROWSER, | |
| "active_users": n, | |
| "users": users, | |
| } | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโ Background Tasks โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| async def _cleanup_tmp(): | |
| """ููุธูู /tmp ูู 5 ุฏูุงุฆู. ู ูุงุญุธุฉ v13: ู ุญุฑู ุงูุงูุชูุงุท | |
| ุงูุฌุฏูุฏ (import-pipe/xlib-direct) ูุง ููุชุจ ุฃู ู ููุงุช zss_* ุนูู ุงูุฅุทูุงู (ููุดูุก | |
| ูู ุงูุฐุงูุฑุฉ ุนุจุฑ pipe)ุ ููุฐู ุงูุฏุงูุฉ ุฃุตุจุญุช ุบูุฑ ุถุฑูุฑูุฉ ูุนููุงู ูููุทุงุช | |
| ุงูุดุงุดุฉุ ููููุง ุชูุฑู ูุดุจูุฉ ุฃู ุงู ุฅุถุงููุฉ (ู ุซูุงู ูู ุฃุถูู ููุฏ ู ุณุชูุจูุงู | |
| ููุชุจ ู ููุงุช ู ุคูุชุฉ ุจููุณ ุงูุจุงุฏุฆุฉ).""" | |
| while True: | |
| await asyncio.sleep(300) | |
| try: | |
| subprocess.run( | |
| ["find", "/tmp", "-name", "zss_*", "-mmin", "+10", "-delete"], | |
| capture_output=True, timeout=10 | |
| ) | |
| except Exception as e: | |
| print(f"[cleanup] {e}") | |
| async def _cleanup_idle_sessions(): | |
| """ | |
| ููุฒูู sessions ุงูู ุณุชุฎุฏู ูู ุบูุฑ ุงููุดุทูู (ูุง ุงุชุตุงู ู ูุฐ ุฃูุซุฑ ู ู ุณุงุนุฉ) | |
| ูุชุญุฑูุฑ ุงูู displays ูุงูุฐุงูุฑุฉ. | |
| """ | |
| while True: | |
| await asyncio.sleep(1800) # ูู 30 ุฏูููุฉ | |
| now = time.time() | |
| to_remove = [] | |
| with _display_lock: | |
| for uid, sess in list(_user_displays.items()): | |
| if sess.get("active_ws"): | |
| continue # ูุง ุชุญุฐู sessions ุงููุดุทุฉ | |
| if now - sess.get("created", now) > 3600: # ุฃูุซุฑ ู ู ุณุงุนุฉ | |
| to_remove.append((uid, sess)) | |
| for uid, sess in to_remove: | |
| print(f"[cleanup] ๐๏ธ Removing idle session for '{uid}' on {sess['display']}") | |
| _kill_proc(sess.get("browser_proc")) | |
| _kill_proc(sess.get("xvfb_proc")) | |
| _kill_display_processes(sess["display"]) | |
| with _display_lock: | |
| _user_displays.pop(uid, None) | |
| try: | |
| _display_numbers.discard(int(sess["display"].lstrip(":"))) | |
| except Exception: | |
| pass | |
| async def startup(): | |
| asyncio.create_task(_cleanup_tmp()) | |
| asyncio.create_task(_cleanup_idle_sessions()) | |
| print("โ Z Computer Mode v11 ready โ Per-User Isolated Displays") | |
| print(f" Display range: :{DISPLAY_BASE} โ :{DISPLAY_MAX}") | |
| print(f" Connect: wss://your-space.hf.space/ws?user_id=EMAIL") | |
| if __name__ == "__main__": | |
| port = int(os.environ.get("PORT", 7860)) | |
| uvicorn.run("app:app", host="0.0.0.0", port=port, log_level="info") | |