Spaces:
Paused
Paused
Update app.py
Browse files
app.py
CHANGED
|
@@ -349,6 +349,7 @@ async def get_or_create_user_session(user_id: str, ws: WebSocket) -> dict:
|
|
| 349 |
"last_bg_hash": "",
|
| 350 |
"active_ws": ws,
|
| 351 |
"created": time.time(),
|
|
|
|
| 352 |
}
|
| 353 |
_user_displays[user_id] = sess
|
| 354 |
print(f"[session] ✅ New user '{user_id}' → display {display}")
|
|
@@ -411,6 +412,7 @@ async def reset_user_computer(user_id: str) -> dict:
|
|
| 411 |
_user_displays[user_id]["xvfb_proc"] = new_proc
|
| 412 |
_user_displays[user_id]["last_bg_shot_ts"] = 0.0
|
| 413 |
_user_displays[user_id]["last_bg_hash"] = ""
|
|
|
|
| 414 |
|
| 415 |
print(f"[reset] ✅ Computer reset done for '{user_id}' on {display}")
|
| 416 |
return _user_displays.get(user_id, {})
|
|
@@ -492,7 +494,7 @@ def _capture_via_import_pipe(display: str):
|
|
| 492 |
try:
|
| 493 |
r = subprocess.run(
|
| 494 |
["import", "-window", "root", "-silent", "png:-"],
|
| 495 |
-
env=env,
|
| 496 |
)
|
| 497 |
if r.returncode != 0 or not r.stdout or len(r.stdout) < 500:
|
| 498 |
return None, 0, 0
|
|
@@ -548,7 +550,7 @@ def _capture_via_xwd(display: str):
|
|
| 548 |
try:
|
| 549 |
r = subprocess.run(
|
| 550 |
["xwd", "-root", "-display", display, "-silent"],
|
| 551 |
-
env=env,
|
| 552 |
)
|
| 553 |
if r.returncode != 0 or not r.stdout or len(r.stdout) < 500:
|
| 554 |
return None, 0, 0
|
|
@@ -575,7 +577,16 @@ def _capture_raw(display: str) -> tuple:
|
|
| 575 |
ImageMagick وغير python-xlib) — هذا يضمن أن فشل مصدر واحد (مثل
|
| 576 |
ImageMagick معطوب أو تعارض في مكتبة xlib) لا يعني فشل الالتقاط
|
| 577 |
بالكامل، لأن xwd تسلك مساراً مستقلاً تماماً وقد تنجح حين تفشل الأخريان.
|
| 578 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 579 |
"""
|
| 580 |
for method_name, method in (
|
| 581 |
("import-pipe", _capture_via_import_pipe),
|
|
@@ -588,7 +599,7 @@ def _capture_raw(display: str) -> tuple:
|
|
| 588 |
if img and not _is_black_screen(img):
|
| 589 |
if attempt > 0:
|
| 590 |
print(f"[cap:{display}] ✅ {method_name} succeeded on retry")
|
| 591 |
-
return img, w, h
|
| 592 |
except Exception as e:
|
| 593 |
print(f"[cap:{display}] {method_name}: {e}")
|
| 594 |
if attempt == 0:
|
|
@@ -602,7 +613,7 @@ def _capture_raw(display: str) -> tuple:
|
|
| 602 |
draw.rectangle([(0, 0), (sw, 55)], fill=(30, 40, 80))
|
| 603 |
draw.text((10, 10), f"⚠️ Screenshot failed — DISPLAY={display}", fill=(255, 120, 80))
|
| 604 |
draw.text((10, 32), "Methods tried: import-pipe, xlib-direct, xwd", fill=(130, 130, 150))
|
| 605 |
-
return img, sw or 1280, sh or 800
|
| 606 |
|
| 607 |
|
| 608 |
def _get_screen_size(display: str) -> tuple:
|
|
@@ -610,7 +621,7 @@ def _get_screen_size(display: str) -> tuple:
|
|
| 610 |
r = subprocess.run(
|
| 611 |
["xdotool", "getdisplaygeometry"],
|
| 612 |
env={**os.environ, "DISPLAY": display},
|
| 613 |
-
capture_output=True, text=True
|
| 614 |
)
|
| 615 |
parts = r.stdout.strip().split()
|
| 616 |
return int(parts[0]), int(parts[1])
|
|
@@ -623,7 +634,7 @@ def _get_mouse_pos(display: str) -> tuple:
|
|
| 623 |
r = subprocess.run(
|
| 624 |
["xdotool", "getmouselocation"],
|
| 625 |
env={**os.environ, "DISPLAY": display},
|
| 626 |
-
capture_output=True, text=True
|
| 627 |
)
|
| 628 |
mx = int(re.search(r"x:(\d+)", r.stdout).group(1))
|
| 629 |
my = int(re.search(r"y:(\d+)", r.stdout).group(1))
|
|
@@ -720,10 +731,11 @@ def capture_with_grid(display: str, scale: float = 0.85, quality: int = 72,
|
|
| 720 |
"""
|
| 721 |
from PIL import Image, ImageDraw
|
| 722 |
|
| 723 |
-
img, ow, oh = _capture_raw(display)
|
| 724 |
if img is None:
|
| 725 |
return {"data": "", "data_grid": "", "data_grid2": "", "data_grid3": "",
|
| 726 |
-
"width": 1280, "height": 800, "mouse_x": 0, "mouse_y": 0
|
|
|
|
| 727 |
|
| 728 |
mx, my = (force_mx, force_my) if force_mx is not None else _get_mouse_pos(display)
|
| 729 |
|
|
@@ -759,6 +771,7 @@ def capture_with_grid(display: str, scale: float = 0.85, quality: int = 72,
|
|
| 759 |
"data_grid2": data_grid2,
|
| 760 |
"data_grid3": data_grid2, # توافقية رجعية: نفس قيمة data_grid2 لأي كود قديم يقرأ data_grid3
|
| 761 |
"width": ow, "height": oh, "mouse_x": mx, "mouse_y": my,
|
|
|
|
| 762 |
}
|
| 763 |
|
| 764 |
|
|
@@ -1060,7 +1073,17 @@ async def handle_action(ws: WebSocket, msg: dict, sess: dict):
|
|
| 1060 |
|
| 1061 |
result = await _safe_capture(0.65, 72, force_mx, force_my)
|
| 1062 |
sess["last_bg_shot_ts"] = time.time()
|
| 1063 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1064 |
fh = _frame_hash(result["data"])
|
| 1065 |
if fh != sess.get("last_bg_hash", ""):
|
| 1066 |
sess["last_bg_hash"] = fh
|
|
@@ -1078,6 +1101,8 @@ async def handle_action(ws: WebSocket, msg: dict, sess: dict):
|
|
| 1078 |
"mouse_y": result["mouse_y"],
|
| 1079 |
"has_grid": True,
|
| 1080 |
})
|
|
|
|
|
|
|
| 1081 |
|
| 1082 |
if extra_shot_delay:
|
| 1083 |
await asyncio.sleep(extra_shot_delay)
|
|
@@ -1104,42 +1129,127 @@ async def handle_action(ws: WebSocket, msg: dict, sess: dict):
|
|
| 1104 |
# لقطة خلفية فاشلة لا يجب أن توقف الجلسة أو تظهر بصمت في اللوق فقط.
|
| 1105 |
print(f"[shot_bg:{display}] ⚠️ {e}")
|
| 1106 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1107 |
async def shot_explicit(label: str = ""):
|
| 1108 |
"""screenshot صريح — يُرسل دائماً بدون delta suppression، وله أولوية مطلقة
|
| 1109 |
على أي عملية shot_bg تلقائية جارية (لا ينتظر خلفها طويلاً).
|
| 1110 |
-
|
| 1111 |
-
|
| 1112 |
-
|
| 1113 |
-
ب
|
| 1114 |
-
|
| 1115 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1116 |
try:
|
| 1117 |
-
result = await _priority_capture(0.65, 75)
|
| 1118 |
-
if not result or not result["data"]:
|
| 1119 |
-
print(f"[shot_explicit:{display}] ⚠️ empty capture result")
|
| 1120 |
-
await send({"type": "error", "action": "screenshot", "msg": "capture returned empty"})
|
| 1121 |
-
return
|
| 1122 |
-
sess["last_bg_hash"] = _frame_hash(result["data"])
|
| 1123 |
await send({
|
| 1124 |
-
"type": "
|
| 1125 |
-
"
|
| 1126 |
-
"
|
| 1127 |
-
"data_grid2": result.get("data_grid2", ""),
|
| 1128 |
-
"data_grid3": result.get("data_grid3", ""),
|
| 1129 |
-
"ts": int(time.time() * 1000),
|
| 1130 |
-
"auto": False, "label": label,
|
| 1131 |
-
"screen_width": result["width"],
|
| 1132 |
-
"screen_height": result["height"],
|
| 1133 |
-
"mouse_x": result["mouse_x"],
|
| 1134 |
-
"mouse_y": result["mouse_y"],
|
| 1135 |
-
"has_grid": True,
|
| 1136 |
})
|
| 1137 |
-
except Exception
|
| 1138 |
-
|
| 1139 |
-
try:
|
| 1140 |
-
await send({"type": "error", "action": "screenshot", "msg": str(e)})
|
| 1141 |
-
except Exception:
|
| 1142 |
-
pass
|
| 1143 |
|
| 1144 |
# ── reset_computer: إعادة ضبط الكمبيوتر كأنه جديد ─────────
|
| 1145 |
if action == "reset_computer":
|
|
|
|
| 349 |
"last_bg_hash": "",
|
| 350 |
"active_ws": ws,
|
| 351 |
"created": time.time(),
|
| 352 |
+
"consecutive_capture_failures": 0, # عدّاد فشل الالتقاط المتتالي — يُصفَّر عند أي نجاح أو reset
|
| 353 |
}
|
| 354 |
_user_displays[user_id] = sess
|
| 355 |
print(f"[session] ✅ New user '{user_id}' → display {display}")
|
|
|
|
| 412 |
_user_displays[user_id]["xvfb_proc"] = new_proc
|
| 413 |
_user_displays[user_id]["last_bg_shot_ts"] = 0.0
|
| 414 |
_user_displays[user_id]["last_bg_hash"] = ""
|
| 415 |
+
_user_displays[user_id]["consecutive_capture_failures"] = 0
|
| 416 |
|
| 417 |
print(f"[reset] ✅ Computer reset done for '{user_id}' on {display}")
|
| 418 |
return _user_displays.get(user_id, {})
|
|
|
|
| 494 |
try:
|
| 495 |
r = subprocess.run(
|
| 496 |
["import", "-window", "root", "-silent", "png:-"],
|
| 497 |
+
env=env, capture_output=True
|
| 498 |
)
|
| 499 |
if r.returncode != 0 or not r.stdout or len(r.stdout) < 500:
|
| 500 |
return None, 0, 0
|
|
|
|
| 550 |
try:
|
| 551 |
r = subprocess.run(
|
| 552 |
["xwd", "-root", "-display", display, "-silent"],
|
| 553 |
+
env=env, capture_output=True
|
| 554 |
)
|
| 555 |
if r.returncode != 0 or not r.stdout or len(r.stdout) < 500:
|
| 556 |
return None, 0, 0
|
|
|
|
| 577 |
ImageMagick وغير python-xlib) — هذا يضمن أن فشل مصدر واحد (مثل
|
| 578 |
ImageMagick معطوب أو تعارض في مكتبة xlib) لا يعني فشل الالتقاط
|
| 579 |
بالكامل، لأن xwd تسلك مساراً مستقلاً تماماً وقد تنجح حين تفشل الأخريان.
|
| 580 |
+
لا يوجد أي حد زمني (timeout) على أي محاولة — كل محاولة تُترك تكتمل
|
| 581 |
+
فعلياً مهما طال الوقت؛ الحماية من التعليق الفعلي (X server ميت) تأتي
|
| 582 |
+
من طبقة أعلى (شمّاعة الفشل المتتالي + reset تلقائي) وليس من قطع
|
| 583 |
+
العملية في المنتصف.
|
| 584 |
+
|
| 585 |
+
── القيمة الرابعة المُعادة (is_placeholder) ──
|
| 586 |
+
True فقط عندما فشلت الطرق الثلاث فعلياً ورجعنا لصورة "Screenshot
|
| 587 |
+
failed" المرسومة يدوياً. الطبقة الأعلى (capture_with_grid) تستخدم
|
| 588 |
+
هذا العلم لتفعيل عدّاد الفشل المتتالي وتحفيز reset تلقائي كامل —
|
| 589 |
+
بدل الاعتماد على فحص محتوى النص داخل الصورة.
|
| 590 |
"""
|
| 591 |
for method_name, method in (
|
| 592 |
("import-pipe", _capture_via_import_pipe),
|
|
|
|
| 599 |
if img and not _is_black_screen(img):
|
| 600 |
if attempt > 0:
|
| 601 |
print(f"[cap:{display}] ✅ {method_name} succeeded on retry")
|
| 602 |
+
return img, w, h, False
|
| 603 |
except Exception as e:
|
| 604 |
print(f"[cap:{display}] {method_name}: {e}")
|
| 605 |
if attempt == 0:
|
|
|
|
| 613 |
draw.rectangle([(0, 0), (sw, 55)], fill=(30, 40, 80))
|
| 614 |
draw.text((10, 10), f"⚠️ Screenshot failed — DISPLAY={display}", fill=(255, 120, 80))
|
| 615 |
draw.text((10, 32), "Methods tried: import-pipe, xlib-direct, xwd", fill=(130, 130, 150))
|
| 616 |
+
return img, sw or 1280, sh or 800, True
|
| 617 |
|
| 618 |
|
| 619 |
def _get_screen_size(display: str) -> tuple:
|
|
|
|
| 621 |
r = subprocess.run(
|
| 622 |
["xdotool", "getdisplaygeometry"],
|
| 623 |
env={**os.environ, "DISPLAY": display},
|
| 624 |
+
capture_output=True, text=True
|
| 625 |
)
|
| 626 |
parts = r.stdout.strip().split()
|
| 627 |
return int(parts[0]), int(parts[1])
|
|
|
|
| 634 |
r = subprocess.run(
|
| 635 |
["xdotool", "getmouselocation"],
|
| 636 |
env={**os.environ, "DISPLAY": display},
|
| 637 |
+
capture_output=True, text=True
|
| 638 |
)
|
| 639 |
mx = int(re.search(r"x:(\d+)", r.stdout).group(1))
|
| 640 |
my = int(re.search(r"y:(\d+)", r.stdout).group(1))
|
|
|
|
| 731 |
"""
|
| 732 |
from PIL import Image, ImageDraw
|
| 733 |
|
| 734 |
+
img, ow, oh, is_placeholder = _capture_raw(display)
|
| 735 |
if img is None:
|
| 736 |
return {"data": "", "data_grid": "", "data_grid2": "", "data_grid3": "",
|
| 737 |
+
"width": 1280, "height": 800, "mouse_x": 0, "mouse_y": 0,
|
| 738 |
+
"is_placeholder": True}
|
| 739 |
|
| 740 |
mx, my = (force_mx, force_my) if force_mx is not None else _get_mouse_pos(display)
|
| 741 |
|
|
|
|
| 771 |
"data_grid2": data_grid2,
|
| 772 |
"data_grid3": data_grid2, # توافقية رجعية: نفس قيمة data_grid2 لأي كود قديم يقرأ data_grid3
|
| 773 |
"width": ow, "height": oh, "mouse_x": mx, "mouse_y": my,
|
| 774 |
+
"is_placeholder": is_placeholder, # True إذا فشلت الطرق الثلاث وهذه صورة "Screenshot failed"
|
| 775 |
}
|
| 776 |
|
| 777 |
|
|
|
|
| 1073 |
|
| 1074 |
result = await _safe_capture(0.65, 72, force_mx, force_my)
|
| 1075 |
sess["last_bg_shot_ts"] = time.time()
|
| 1076 |
+
# ── مساهمة في عدّاد الفشل المشترك مع shot_explicit ──
|
| 1077 |
+
# shot_bg تبقى صامتة عند الفشل (لا ترسل شيئاً — هذا سلوكها الأصلي
|
| 1078 |
+
# المقصود، فهي مجرد محاولة إضافية سريعة وليست خط الدفاع الموثوق)،
|
| 1079 |
+
# لكنها الآن تُحدّث نفس عدّاد الفشل المتتالي الذي يستخدمه
|
| 1080 |
+
# shot_explicit. هذا يعني: لو الـ display معطوب فعلاً، العدّاد
|
| 1081 |
+
# يبدأ بالارتفاع من أول لقطة تلقائية فاشلة، فتصل shot_explicit
|
| 1082 |
+
# (التي دائماً تُطلَب صراحة بعدها من العميل) للعتبة وتُفعّل الـ
|
| 1083 |
+
# reset التلقائي أسرع، بدل الانتظار حتى تتراكم فشلتان داخل
|
| 1084 |
+
# shot_explicit نفسها فقط.
|
| 1085 |
+
if result.get("data") and not result.get("is_placeholder"):
|
| 1086 |
+
sess["consecutive_capture_failures"] = 0
|
| 1087 |
fh = _frame_hash(result["data"])
|
| 1088 |
if fh != sess.get("last_bg_hash", ""):
|
| 1089 |
sess["last_bg_hash"] = fh
|
|
|
|
| 1101 |
"mouse_y": result["mouse_y"],
|
| 1102 |
"has_grid": True,
|
| 1103 |
})
|
| 1104 |
+
else:
|
| 1105 |
+
sess["consecutive_capture_failures"] = sess.get("consecutive_capture_failures", 0) + 1
|
| 1106 |
|
| 1107 |
if extra_shot_delay:
|
| 1108 |
await asyncio.sleep(extra_shot_delay)
|
|
|
|
| 1129 |
# لقطة خلفية فاشلة لا يجب أن توقف الجلسة أو تظهر بصمت في اللوق فقط.
|
| 1130 |
print(f"[shot_bg:{display}] ⚠️ {e}")
|
| 1131 |
|
| 1132 |
+
# ── عتبة الفشل المتتالي قبل تفعيل reset تلقائي كامل للـ display ──
|
| 1133 |
+
# فشلة واحدة عابرة (ضغط CPU لحظي، تزاحم مع shot_bg) لا تستدعي reset —
|
| 1134 |
+
# لكن فشلتان متتاليتان فعليتان (كلتاهما رجعتا is_placeholder=True) تعني
|
| 1135 |
+
# أن هناك مشكلة حقيقية في الـ display نفسه (Xvfb ميت/عالق)، وليس عطلاً
|
| 1136 |
+
# عابراً، فنعيد ضبط الكمبيوتر تلقائياً بدل الاستسلام وإرسال "error".
|
| 1137 |
+
_AUTO_RESET_FAILURE_THRESHOLD = 2
|
| 1138 |
+
# حد أقصى لعدد جولات (محاولة التقاط → عند الفشل المتكرر → reset → إعادة
|
| 1139 |
+
# محاولة) داخل نفس طلب screenshot الواحد، لضمان أن الحلقة تنتهي دائماً
|
| 1140 |
+
# ولو استمر الفشل حتى بعد عدة عمليات reset (بيئة معطوبة جذرياً)، بدل أن
|
| 1141 |
+
# تدور للأبد بصمت. هذا ليس "timeout" زمنياً — هو سقف على عدد المحاولات.
|
| 1142 |
+
_MAX_CAPTURE_ROUNDS = 6
|
| 1143 |
+
|
| 1144 |
async def shot_explicit(label: str = ""):
|
| 1145 |
"""screenshot صريح — يُرسل دائماً بدون delta suppression، وله أولوية مطلقة
|
| 1146 |
على أي عملية shot_bg تلقائية جارية (لا ينتظر خلفها طويلاً).
|
| 1147 |
+
|
| 1148 |
+
── إصلاح جذري v14: لا "error" نهائي بعد الآن — بل reset تلقائي كامل ──
|
| 1149 |
+
سابقاً: فشل التقاط واحد (نتيجة فارغة) كان يُرسل "error" فوراً للعميل،
|
| 1150 |
+
فيبقى العميل يعيد طلب screenshot بلا نهاية بينما السيرفر لا يفعل شيئاً
|
| 1151 |
+
مختلفاً في كل مرة (نفس الـ display المعطوب، بلا أي محاولة إصلاح فعلية).
|
| 1152 |
+
|
| 1153 |
+
الآن: فشل واحد → إعادة محاولة فورية (قد يكون عابراً). فشلان متتاليان
|
| 1154 |
+
فعليان (كلاهما is_placeholder=True) → reset تلقائي كامل لهذا المستخدم
|
| 1155 |
+
(نفس ما يفعله المستخدم يدوياً بزر "إعادة الضبط") ثم إعادة المحاولة
|
| 1156 |
+
تلقائياً على الـ display الجديد النظيف — كل هذا داخل نفس الطلب، بلا
|
| 1157 |
+
أي تدخل من العميل. رسالة "error" الصريحة لم تعد تُرسَل أبداً من هذا
|
| 1158 |
+
المسار؛ العميل يستقبل إما screenshot ناجحاً حقيقياً، أو (في أسوأ حالة
|
| 1159 |
+
نادرة جداً: بيئة معطوبة جذرياً حتى بعد عدة محاولات reset) رسالة
|
| 1160 |
+
"computer_reset" تخبره أن الجلسة بدأت من جديد ليطلب screenshot تالياً
|
| 1161 |
+
بنفسه — أبداً حالة صمت لا نهائية.
|
| 1162 |
+
"""
|
| 1163 |
+
for _round in range(_MAX_CAPTURE_ROUNDS):
|
| 1164 |
+
try:
|
| 1165 |
+
result = await _priority_capture(0.65, 75)
|
| 1166 |
+
got_real_frame = bool(result and result.get("data") and not result.get("is_placeholder"))
|
| 1167 |
+
|
| 1168 |
+
if got_real_frame:
|
| 1169 |
+
sess["consecutive_capture_failures"] = 0
|
| 1170 |
+
sess["last_bg_hash"] = _frame_hash(result["data"])
|
| 1171 |
+
await send({
|
| 1172 |
+
"type": "screenshot",
|
| 1173 |
+
"data": result["data"],
|
| 1174 |
+
"data_grid": result.get("data_grid", ""),
|
| 1175 |
+
"data_grid2": result.get("data_grid2", ""),
|
| 1176 |
+
"data_grid3": result.get("data_grid3", ""),
|
| 1177 |
+
"ts": int(time.time() * 1000),
|
| 1178 |
+
"auto": False, "label": label,
|
| 1179 |
+
"screen_width": result["width"],
|
| 1180 |
+
"screen_height": result["height"],
|
| 1181 |
+
"mouse_x": result["mouse_x"],
|
| 1182 |
+
"mouse_y": result["mouse_y"],
|
| 1183 |
+
"has_grid": True,
|
| 1184 |
+
})
|
| 1185 |
+
return
|
| 1186 |
+
|
| 1187 |
+
# ── فشل هذه الجولة (نتيجة فارغة تماماً، أو placeholder) ──
|
| 1188 |
+
sess["consecutive_capture_failures"] = sess.get("consecutive_capture_failures", 0) + 1
|
| 1189 |
+
fail_count = sess["consecutive_capture_failures"]
|
| 1190 |
+
print(f"[shot_explicit:{display}] ⚠️ capture failed (round {_round+1}/{_MAX_CAPTURE_ROUNDS}, "
|
| 1191 |
+
f"consecutive={fail_count})")
|
| 1192 |
+
|
| 1193 |
+
if fail_count >= _AUTO_RESET_FAILURE_THRESHOLD:
|
| 1194 |
+
# ── مشكلة حقيقية وليست عابرة — reset تلقائي كامل ──
|
| 1195 |
+
print(f"[shot_explicit:{display}] 🔄 auto-reset triggered after {fail_count} consecutive failures")
|
| 1196 |
+
await send({
|
| 1197 |
+
"type": "computer_reset",
|
| 1198 |
+
"msg": "🔄 إعادة ضبط تلقائية بعد فشل التقاط متكرر — الشاشة تبدأ من جديد نظيفة",
|
| 1199 |
+
"auto": True,
|
| 1200 |
+
})
|
| 1201 |
+
await reset_user_computer(sess["user_id"])
|
| 1202 |
+
# بعد الـ reset الكامل، أمهل Xvfb الجديد لحظة قصيرة ليستقر
|
| 1203 |
+
# قبل أول محاولة التقاط عليه (نفس المهلة المستخدمة في مسار
|
| 1204 |
+
# reset_computer اليدوي أعلاه لضمان اتساق السلوك).
|
| 1205 |
+
await asyncio.sleep(2.0)
|
| 1206 |
+
# الجولة التالية من هذه الحلقة نفسها ستعيد المحاولة على
|
| 1207 |
+
# الـ display الجديد النظيف تلقائياً — بلا حاجة لأي طلب
|
| 1208 |
+
# إضافي من العميل ولا لإرسال "error" له في الأثناء.
|
| 1209 |
+
continue
|
| 1210 |
+
|
| 1211 |
+
# فشل عابر فقط (أول فشلة) — أعد المحاولة فوراً بلا reset
|
| 1212 |
+
await asyncio.sleep(0.4)
|
| 1213 |
+
continue
|
| 1214 |
+
|
| 1215 |
+
except Exception as e:
|
| 1216 |
+
# استثناء غير متوقع تماماً (وليس مجرد نتيجة فارغة) — يُعامَل
|
| 1217 |
+
# بنفس م��طق الفشل أعلاه بدل الاستسلام الفوري، لأن أسباباً
|
| 1218 |
+
# كهذه (اتصال X عالق، انهيار عملية) هي بالضبط ما يُصلحه reset.
|
| 1219 |
+
sess["consecutive_capture_failures"] = sess.get("consecutive_capture_failures", 0) + 1
|
| 1220 |
+
fail_count = sess["consecutive_capture_failures"]
|
| 1221 |
+
print(f"[shot_explicit:{display}] ❌ EXCEPTION (round {_round+1}/{_MAX_CAPTURE_ROUNDS}, "
|
| 1222 |
+
f"consecutive={fail_count}): {e}")
|
| 1223 |
+
if fail_count >= _AUTO_RESET_FAILURE_THRESHOLD:
|
| 1224 |
+
try:
|
| 1225 |
+
await send({
|
| 1226 |
+
"type": "computer_reset",
|
| 1227 |
+
"msg": "🔄 إعادة ضبط تلقائية بعد خطأ متكرر في الالتقاط — الشاشة تبدأ من جديد نظيفة",
|
| 1228 |
+
"auto": True,
|
| 1229 |
+
})
|
| 1230 |
+
except Exception:
|
| 1231 |
+
pass
|
| 1232 |
+
try:
|
| 1233 |
+
await reset_user_computer(sess["user_id"])
|
| 1234 |
+
await asyncio.sleep(2.0)
|
| 1235 |
+
except Exception as reset_ex:
|
| 1236 |
+
print(f"[shot_explicit:{display}] ❌ reset itself failed: {reset_ex}")
|
| 1237 |
+
continue
|
| 1238 |
+
|
| 1239 |
+
# ── وصلنا لحد _MAX_CAPTURE_ROUNDS جولة دون أي نجاح حقيقي، حتى بعد
|
| 1240 |
+
# محاولات reset متعددة — بيئة معطوبة جذرياً (نادر جداً). بدل الصمت
|
| 1241 |
+
# التام، نُعلم العميل صراحة أن الجلسة أُعيد ضبطها ليطلب screenshot
|
| 1242 |
+
# تالياً بنفسه بدل انتظار frame لن يصل أبداً في هذا الطلب تحديداً.
|
| 1243 |
+
print(f"[shot_explicit:{display}] ❌ giving up after {_MAX_CAPTURE_ROUNDS} rounds — "
|
| 1244 |
+
f"environment appears broken even after reset attempts")
|
| 1245 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1246 |
await send({
|
| 1247 |
+
"type": "computer_reset",
|
| 1248 |
+
"msg": "⚠️ استمرت مشكلة الالتقاط رغم إعادة الضبط — الرجاء طلب لقطة شاشة جديدة",
|
| 1249 |
+
"auto": True,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1250 |
})
|
| 1251 |
+
except Exception:
|
| 1252 |
+
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1253 |
|
| 1254 |
# ── reset_computer: إعادة ضبط الكمبيوتر كأنه جديد ─────────
|
| 1255 |
if action == "reset_computer":
|