THEZYZSTUDIO commited on
Commit
537a87e
·
verified ·
1 Parent(s): 9490d6d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +150 -40
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, timeout=6, capture_output=True
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, timeout=6, capture_output=True
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, timeout=5
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, timeout=5
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
- if result["data"]:
 
 
 
 
 
 
 
 
 
 
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
- ── إصلاح جذري: try/except شامل + إرسال رسالة خطأ صريحة للعميل ──
1111
- المشكلة القديمة: هذه الدالة تُشغَّل عبر asyncio.create_task بدون أي
1112
- معالجة استثناء محيطة بها. أي خطأ غير متوقع كان يُسقط الـ task بالكامل
1113
- بصمت تام لا رسالة تصل للعميل ولا حتى سطر في اللوق، فيبقى العميل
1114
- ينتظر frame لن يصل أبداً. الآن أي فشل يُسجَّل ويُرسَل كرسالة "error"
1115
- صريحة، والعميل (index.html) يتعامل معها ويعيد المحاولة فوراً."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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": "screenshot",
1125
- "data": result["data"],
1126
- "data_grid": result.get("data_grid", ""),
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 as e:
1138
- print(f"[shot_explicit:{display}] ❌ EXCEPTION: {e}")
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":