THEZYZSTUDIO commited on
Commit
b1f94a7
ยท
verified ยท
1 Parent(s): 825664d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +234 -245
app.py CHANGED
@@ -74,6 +74,17 @@ def _detect_browser() -> str:
74
  BROWSER = _detect_browser()
75
  print(f"๐ŸŒ Browser: {BROWSER}")
76
 
 
 
 
 
 
 
 
 
 
 
 
77
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
78
  # โ”€โ”€ Firefox Profile (per-user, isolated, crash-recovery DISABLED) โ”€
79
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
@@ -125,6 +136,38 @@ user_pref("datareporting.policy.dataSubmissionEnabled", false);
125
  user_pref("app.normandy.enabled", false);
126
  user_pref("app.update.enabled", false);
127
  user_pref("app.update.auto", false);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  """
129
 
130
  def _ensure_firefox_profile(user_id: str, display: str) -> str:
@@ -192,8 +235,12 @@ def _start_xvfb(display: str) -> "subprocess.Popen | None":
192
  pass
193
 
194
  try:
 
 
 
 
195
  proc = subprocess.Popen(
196
- ["Xvfb", display, "-screen", "0", "1920x1080x24",
197
  "-nolisten", "tcp", "-ac"],
198
  stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
199
  )
@@ -434,170 +481,90 @@ def _load_capture_image(path: str):
434
  return None, 0, 0
435
 
436
 
437
- def _capture_via_x11_stack(display: str):
438
- """ุงู„ุทุฑูŠู‚ุฉ ุงู„ุฃูˆู„ู‰: scrot โ†’ ImageMagick import โ†’ ffmpeg x11grab โ†’ python-xlib."""
 
 
 
 
439
  from PIL import Image
440
  env = {**os.environ, "DISPLAY": display}
441
- ts = int(time.time() * 1000)
442
- disp_safe = display.replace(":", "")
443
- tmp_base = f"/tmp/zss_{disp_safe}_{ts}"
444
- created_files = []
445
-
446
- def _cleanup_files():
447
- for p in created_files:
448
- try:
449
- if p and os.path.exists(p):
450
- os.unlink(p)
451
- except Exception:
452
- pass
453
-
454
  try:
455
- p1 = f"{tmp_base}_scrot.png"
456
- created_files.append(p1)
457
- try:
458
- subprocess.run(["scrot", "-q", "95", p1], env=env, timeout=8, capture_output=True)
459
- img, w, h = _load_capture_image(p1)
460
- if img and not _is_black_screen(img):
461
- return img, w, h
462
- except Exception as e:
463
- print(f"[cap:{display}] scrot: {e}")
464
-
465
- p2 = f"{tmp_base}_im.png"
466
- created_files.append(p2)
467
- try:
468
- subprocess.run(["import", "-window", "root", "-silent", p2], env=env, timeout=10, capture_output=True)
469
- img, w, h = _load_capture_image(p2)
470
- if img and not _is_black_screen(img):
471
- return img, w, h
472
- except Exception as e:
473
- print(f"[cap:{display}] import: {e}")
474
-
475
- p3 = f"{tmp_base}_ff.png"
476
- created_files.append(p3)
477
- try:
478
- sw, sh = _get_screen_size(display)
479
- subprocess.run([
480
- "ffmpeg", "-y", "-f", "x11grab",
481
- "-video_size", f"{sw}x{sh}",
482
- "-i", display,
483
- "-vframes", "1", "-q:v", "1", p3
484
- ], env=env, timeout=12, capture_output=True)
485
- img, w, h = _load_capture_image(p3)
486
- if img and not _is_black_screen(img):
487
- return img, w, h
488
- except Exception as e:
489
- print(f"[cap:{display}] ffmpeg: {e}")
490
-
491
- try:
492
- from Xlib import display as Xdisp, X
493
- xd = Xdisp.Display(display)
494
- root = xd.screen().root
495
- geom = root.get_geometry()
496
- w, h = geom.width, geom.height
497
- raw = root.get_image(0, 0, w, h, X.ZPixmap, 0xFFFFFFFF)
498
- data = raw.data
499
- pixels = bytearray(len(data) // 4 * 3)
500
- for i in range(0, len(data) - 3, 4):
501
- b, g, r_c = data[i], data[i+1], data[i+2]
502
- j = (i // 4) * 3
503
- pixels[j], pixels[j+1], pixels[j+2] = r_c, g, b
504
- img = Image.frombytes("RGB", (w, h), bytes(pixels))
505
- if not _is_black_screen(img):
506
- return img, w, h
507
- except Exception as e:
508
- print(f"[cap:{display}] xlib: {e}")
509
-
510
  return None, 0, 0
511
- finally:
512
- _cleanup_files()
513
-
514
-
515
- def _capture_via_xwd(display: str):
516
- """ุงู„ุทุฑูŠู‚ุฉ ุงู„ุซุงู†ูŠุฉ: xwd ุซู… convert ุฅู„ู‰ PNG."""
517
- env = {**os.environ, "DISPLAY": display}
518
- ts = int(time.time() * 1000)
519
- disp_safe = display.replace(":", "")
520
- xwd_path = f"/tmp/zss_{disp_safe}_{ts}.xwd"
521
- png_path = f"/tmp/zss_{disp_safe}_{ts}.png"
522
- try:
523
- with open(xwd_path, "wb") as f:
524
- subprocess.run(["xwd", "-root", "-silent"], env=env, stdout=f, stderr=subprocess.PIPE, timeout=12)
525
- subprocess.run(["convert", xwd_path, png_path], env=env, capture_output=True, timeout=15)
526
- img, w, h = _load_capture_image(png_path)
527
- if img and not _is_black_screen(img):
528
- return img, w, h
529
- except FileNotFoundError as e:
530
- print(f"[cap:{display}] xwd/convert not found: {e}")
531
  except Exception as e:
532
- print(f"[cap:{display}] xwd: {e}")
533
- finally:
534
- for p in (xwd_path, png_path):
535
- try:
536
- if os.path.exists(p):
537
- os.unlink(p)
538
- except Exception:
539
- pass
540
- return None, 0, 0
541
 
542
 
543
- def _capture_via_pyscreenshot(display: str):
544
- """ุงู„ุทุฑูŠู‚ุฉ ุงู„ุซุงู„ุซุฉ: pyscreenshot backend ู…ู† ุฏุงุฎู„ Python."""
545
- ts = int(time.time() * 1000)
546
- disp_safe = display.replace(":", "")
547
- png_path = f"/tmp/zss_{disp_safe}_{ts}_pyshot.png"
548
- env = {**os.environ, "DISPLAY": display}
 
549
  try:
550
- code = """import sys
551
- from pyscreenshot import grab
552
- out = sys.argv[1]
553
- img = grab()
554
- img.save(out)
555
- """
556
- subprocess.run([sys.executable, "-c", code, png_path], env=env, capture_output=True, timeout=18)
557
- img, w, h = _load_capture_image(png_path)
558
- if img and not _is_black_screen(img):
559
- return img, w, h
560
- except FileNotFoundError as e:
561
- print(f"[cap:{display}] pyscreenshot not available: {e}")
562
  except Exception as e:
563
- print(f"[cap:{display}] pyscreenshot: {e}")
564
- finally:
565
- try:
566
- if os.path.exists(png_path):
567
- os.unlink(png_path)
568
- except Exception:
569
- pass
570
- return None, 0, 0
571
 
572
 
573
  def _capture_raw(display: str) -> tuple:
574
- """ูŠู„ุชู‚ุท ุงู„ุดุงุดุฉ ู…ู† display ู…ุญุฏุฏ ุนุจุฑ 3 ุทุฑู‚ ู…ุณุชู‚ู„ุฉ."""
575
- for attempt in range(3):
576
- if attempt > 0:
577
- time.sleep(0.8)
578
- print(f"[cap:{display}] retry attempt {attempt+1}")
579
- for method_name, method in (
580
- ("x11-stack", _capture_via_x11_stack),
581
- ("xwd", _capture_via_xwd),
582
- ("pyscreenshot", _capture_via_pyscreenshot),
583
- ):
 
 
584
  try:
585
  img, w, h = method(display)
586
  if img and not _is_black_screen(img):
587
- print(f"[cap:{display}] โœ… {method_name} succeeded on attempt {attempt+1}")
 
588
  return img, w, h
589
  except Exception as e:
590
  print(f"[cap:{display}] {method_name}: {e}")
 
 
591
 
592
  print(f"[cap:{display}] โš ๏ธ All methods failed โ€” placeholder")
593
  from PIL import Image as PILImg, ImageDraw
594
  sw, sh = _get_screen_size(display)
595
- img = PILImg.new("RGB", (sw or 1920, sh or 1080), (15, 20, 40))
596
  draw = ImageDraw.Draw(img)
597
  draw.rectangle([(0, 0), (sw, 55)], fill=(30, 40, 80))
598
  draw.text((10, 10), f"โš ๏ธ Screenshot failed โ€” DISPLAY={display}", fill=(255, 120, 80))
599
- draw.text((10, 32), "Methods: x11-stack, xwd+convert, pyscreenshot (3 attempts)", fill=(130, 130, 150))
600
- return img, sw or 1920, sh or 1080
 
601
 
602
  def _get_screen_size(display: str) -> tuple:
603
  try:
@@ -609,7 +576,7 @@ def _get_screen_size(display: str) -> tuple:
609
  parts = r.stdout.strip().split()
610
  return int(parts[0]), int(parts[1])
611
  except Exception:
612
- return 1920, 1080
613
 
614
 
615
  def _get_mouse_pos(display: str) -> tuple:
@@ -637,15 +604,15 @@ def _draw_mouse_marker(draw, msx, msy, color=(255, 50, 50, 240)):
637
 
638
  def _render_grid_variant(base_img, ow, oh, sw, sh, mx, my, display,
639
  grid_step: int, line_color: tuple, label_every: int,
640
- major_only_labels: bool = True) -> "Image.Image":
641
  """
642
  ูŠุฑุณู… ู†ุณุฎุฉ ุดุจูƒุฉ ุฅุญุฏุงุซูŠุงุช ูˆุงุญุฏุฉ ููˆู‚ ู†ุณุฎุฉ ู…ู† ุงู„ุตูˆุฑุฉ ุงู„ุฃุณุงุณูŠุฉ.
643
  grid_step โ†’ ุงู„ู…ุณุงูุฉ ุจุงู„ุจูƒุณู„ ุงู„ุญู‚ูŠู‚ูŠ ุจูŠู† ูƒู„ ุฎุท ุดุจูƒุฉ.
644
- line_color โ†’ ู„ูˆู† ุงู„ุฎุทูˆุท ูˆุงู„ุฃุฑู‚ุงู… (RGBA)ุŒ ู…ุซู„ ุฃุฎุถุฑ/ุณู…ุงูˆูŠ ุฃูˆ ุฃุญู…ุฑ.
645
- label_every โ†’ ูƒู„ ูƒู… ุจูƒุณู„ ุญู‚ูŠู‚ูŠ ุชููƒุชุจ ููŠู‡ ุชุณู…ูŠุฉ ุฅุญุฏุงุซูŠ (x,y) ูƒุงู…ู„ุฉ ุนู†ุฏ ุงู„ุชู‚ุงุทุนุงุช.
646
- major_only_labels โ†’ ุฅุฐุง TrueุŒ ุงู„ุฃุฑู‚ุงู… ุนู„ู‰ ุงู„ุญูˆุงู ุชุธู‡ุฑ ูู‚ุท ุนู†ุฏ ุฎุทูˆุท "ุฑุฆูŠุณูŠุฉ" (ู…ุถุงุนูุงุช label_every).
647
  """
648
- from PIL import Image, ImageDraw
649
  img = base_img.copy()
650
  draw = ImageDraw.Draw(img, "RGBA")
651
 
@@ -679,7 +646,6 @@ def _render_grid_variant(base_img, ow, oh, sw, sh, mx, my, display,
679
  y_majors.append((y_sc, y_r))
680
  y_sc += step_y; y_r += grid_step
681
 
682
- # ุชุณู…ูŠุฉ ุฅุญุฏุงุซูŠ (x,y) ูƒุงู…ู„ุฉ ุนู†ุฏ ูƒู„ ุชู‚ุงุทุน "ุฑุฆูŠุณูŠ" โ€” ุชุถู…ู† ุฑู‚ู…ุงู‹ ุญู‚ูŠู‚ูŠุงู‹ ู‚ุฑูŠุจุงู‹ ู…ู† ูƒู„ ู…ู†ุทู‚ุฉ
683
  for (xs, xr) in x_majors:
684
  for (ys, yr) in y_majors:
685
  label = f"{xr},{yr}"
@@ -700,26 +666,25 @@ def _render_grid_variant(base_img, ow, oh, sw, sh, mx, my, display,
700
  return final
701
 
702
 
703
- def capture_with_grid(display: str, scale: float = 0.65, quality: int = 72,
704
  force_mx: int | None = None,
705
  force_my: int | None = None,
706
  grid_step: int = 50) -> dict:
707
  """
708
- ูŠู„ุชู‚ุท ุงู„ุดุงุดุฉ ู…ู† display ู…ุญุฏุฏ ูˆูŠูู†ุชุฌ 4 ู†ุณุฎ ู…ู† ู†ูุณ ุงู„ู„ู‚ุทุฉ ุจุงู„ุถุจุท (ู†ูุณ ุงู„ู„ุญุธุฉ):
709
- - "data" โ†’ ุงู„ู†ุณุฎุฉ ุงู„ุนุงุฏูŠุฉ (ู†ุธูŠูุฉ ุชู…ุงู…ุงู‹) + ุนู„ุงู…ุฉ ุงู„ู…ุงูˆุณ ูู‚ุทุŒ ุจุฏูˆู† ุฃูŠ Grid.
710
- ู‡ุฐู‡ ูู‚ุท ุชูุนุฑุถ ู„ู„ู…ุณุชุฎุฏู… ููŠ ุงู„ูˆุงุฌู‡ุฉ ูƒู€ "ุงู„ุดุงุดุฉ ุงู„ุญู‚ูŠู‚ูŠุฉ".
711
- - "data_grid" โ†’ ุดุจูƒุฉ ุฅุญุฏุงุซูŠุงุช ุนุงุฏูŠุฉ (ูƒู„ 50px)ุŒ ุฃุฎุถุฑ/ุณู…ุงูˆูŠ โ€” ู„ู„ุฐูƒุงุก ุงู„ุงุตุทู†ุงุนูŠ.
712
- - "data_grid2" โ†’ ุดุจูƒุฉ ุฅุญุฏุงุซูŠุงุช ุฃุฏู‚ (ูƒู„ 25px)ุŒ ู†ูุณ ุงู„ุฃู„ูˆุงู† โ€” ู„ู„ุฐูƒุงุก ุงู„ุงุตุทู†ุงุนูŠ.
713
- - "data_grid3" โ†’ ุดุจูƒุฉ ุฅุญุฏุงุซูŠุงุช ูุงุฆู‚ุฉ ุงู„ุฏู‚ุฉ (ูƒู„ 10px ุชู‚ุฑูŠุจุงู‹)ุŒ ุจุงู„ู„ูˆู† ุงู„ุฃุญู…ุฑ โ€” ู„ู„ุฐูƒุงุก ุงู„ุงุตุทู†ุงุนูŠ.
714
- ุงู„ุซู„ุงุซ ู†ุณุฎ ุงู„ุฃุฎูŠุฑุฉ ูƒู„ู‡ุง ู…ู† ู†ูุณ ุงู„ู„ู‚ุทุฉ ุงู„ุฃุตู„ูŠุฉ ุจุงู„ุถุจุท (ู†ูุณ ุงู„ู„ุญุธุฉุŒ ู„ุง ุงู„ุชู‚ุงุท ู…ุชูƒุฑุฑ)
715
- ู„ุถู…ุงู† ุชุทุงุจู‚ู‡ุง ุชู…ุงู…ุงู‹ ูˆู„ุชูุงุฏูŠ ุฃูŠ ุชุฃุฎูŠุฑ ุฃูˆ ุงุฎุชู„ุงู ุจูŠู†ู‡ุง.
716
  """
717
  from PIL import Image, ImageDraw
718
 
719
  img, ow, oh = _capture_raw(display)
720
  if img is None:
721
  return {"data": "", "data_grid": "", "data_grid2": "", "data_grid3": "",
722
- "width": 1920, "height": 1080, "mouse_x": 0, "mouse_y": 0}
723
 
724
  mx, my = (force_mx, force_my) if force_mx is not None else _get_mouse_pos(display)
725
 
@@ -729,7 +694,6 @@ def capture_with_grid(display: str, scale: float = 0.65, quality: int = 72,
729
  msx = int(mx * sw / ow)
730
  msy = int(my * sh / oh)
731
 
732
- # โ”€โ”€ ุงู„ู†ุณุฎุฉ ุงู„ุนุงุฏูŠุฉ ุงู„ู†ุธูŠูุฉ (ุจุฏูˆู† ุฃูŠ ุดุจูƒุฉ) โ€” ูู‚ุท ุนู„ุงู…ุฉ ุงู„ู…ุงูˆุณ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
733
  clean_img = base_img.copy()
734
  draw_clean = ImageDraw.Draw(clean_img, "RGBA")
735
  _draw_mouse_marker(draw_clean, msx, msy)
@@ -738,32 +702,23 @@ def capture_with_grid(display: str, scale: float = 0.65, quality: int = 72,
738
  clean_final.save(buf_clean, format="JPEG", quality=quality, optimize=True)
739
  data_clean = base64.b64encode(buf_clean.getvalue()).decode()
740
 
741
- # โ”€โ”€ ู†ุณุฎุฉ ุดุจูƒุฉ ุนุงุฏูŠุฉ: ูƒู„ 50pxุŒ ุฃุฎุถุฑ/ุณู…ุงูˆูŠ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
742
  grid1 = _render_grid_variant(base_img, ow, oh, sw, sh, mx, my, display,
743
  grid_step=grid_step, line_color=(0, 255, 180, 235),
744
  label_every=100)
745
  buf1 = io.BytesIO(); grid1.save(buf1, format="JPEG", quality=quality, optimize=True)
746
  data_grid1 = base64.b64encode(buf1.getvalue()).decode()
747
 
748
- # โ”€โ”€ ู†ุณุฎุฉ ุดุจูƒุฉ ุฃุฏู‚: ูƒู„ 25pxุŒ ู†ูุณ ุงู„ุฃู„ูˆุงู† โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
749
  grid2 = _render_grid_variant(base_img, ow, oh, sw, sh, mx, my, display,
750
- grid_step=25, line_color=(0, 255, 180, 235),
751
- label_every=100)
752
- buf2 = io.BytesIO(); grid2.save(buf2, format="JPEG", quality=quality, optimize=True)
753
  data_grid2 = base64.b64encode(buf2.getvalue()).decode()
754
 
755
- # โ”€โ”€ ู†ุณุฎุฉ ุดุจูƒุฉ ูุงุฆู‚ุฉ ุงู„ุฏู‚ุฉ: ูƒู„ 10px ุชู‚ุฑูŠุจุงู‹ุŒ ุฃุญู…ุฑ ุจุงู„ูƒุงู…ู„ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
756
- grid3 = _render_grid_variant(base_img, ow, oh, sw, sh, mx, my, display,
757
- grid_step=10, line_color=(255, 60, 60, 235),
758
- label_every=50, major_only_labels=True)
759
- buf3 = io.BytesIO(); grid3.save(buf3, format="JPEG", quality=max(quality, 78), optimize=True)
760
- data_grid3 = base64.b64encode(buf3.getvalue()).decode()
761
-
762
  return {
763
- "data": data_clean, # ุงู„ู†ุณุฎุฉ ุงู„ุนุงุฏูŠุฉ ุงู„ู†ุธูŠูุฉ (ุชูุนุฑุถ ู„ู„ู…ุณุชุฎุฏู… ูู‚ุท)
764
- "data_grid": data_grid1, # ุดุจูƒุฉ ุนุงุฏูŠุฉ 50px (ุฃุฎุถุฑ/ุณู…ุงูˆูŠ)
765
- "data_grid2": data_grid2, # ุดุจูƒุฉ ุฃุฏู‚ 25px (ุฃุฎุถุฑ/ุณู…ุงูˆูŠ)
766
- "data_grid3": data_grid3, # ุดุจูƒุฉ ูุงุฆู‚ุฉ ุงู„ุฏู‚ุฉ 10px (ุฃุญู…ุฑ)
767
  "width": ow, "height": oh, "mouse_x": mx, "mouse_y": my,
768
  }
769
 
@@ -1058,75 +1013,94 @@ async def handle_action(ws: WebSocket, msg: dict, sess: dict):
1058
  ู„ุฃู† ุงู„ู…ุญุชูˆู‰ ู…ุชูˆู‚ุน ุฃู† ูŠูƒูˆู† ู…ุฎุชู„ูุงู‹ (ุตูุญุฉ ุชุญู…ู‘ู„ุช ุฃูƒุซุฑ) ุญุชู‰ ู„ูˆ ุชุดุงุจู‡ ุงู„ู€ hash
1059
  ุฌุฒุฆูŠุงู‹ ู…ุน ุฎู„ููŠุฉ ู…ุดุงุจู‡ุฉ.
1060
  """
1061
- await asyncio.sleep(delay)
1062
- now = time.time()
1063
- if now - sess.get("last_bg_shot_ts", 0) < 0.3:
1064
- await asyncio.sleep(0.3 - (now - sess["last_bg_shot_ts"]))
 
1065
 
1066
- result = await _safe_capture(0.65, 72, force_mx, force_my)
1067
- sess["last_bg_shot_ts"] = time.time()
1068
- if result["data"]:
1069
- fh = _frame_hash(result["data"])
1070
- if fh != sess.get("last_bg_hash", ""):
1071
- sess["last_bg_hash"] = fh
1072
- await send({
1073
- "type": "screenshot",
1074
- "data": result["data"],
1075
- "data_grid": result.get("data_grid", ""),
1076
- "data_grid2": result.get("data_grid2", ""),
1077
- "data_grid3": result.get("data_grid3", ""),
1078
- "ts": int(time.time() * 1000),
1079
- "auto": True, "label": label,
1080
- "screen_width": result["width"],
1081
- "screen_height": result["height"],
1082
- "mouse_x": result["mouse_x"],
1083
- "mouse_y": result["mouse_y"],
1084
- "has_grid": True,
1085
- })
1086
-
1087
- if extra_shot_delay:
1088
- await asyncio.sleep(extra_shot_delay)
1089
- result2 = await _safe_capture(0.65, 72, force_mx, force_my)
1090
  sess["last_bg_shot_ts"] = time.time()
1091
- if result2["data"]:
1092
- fh2 = _frame_hash(result2["data"])
1093
- sess["last_bg_hash"] = fh2
1094
- await send({
1095
- "type": "screenshot",
1096
- "data": result2["data"],
1097
- "data_grid": result2.get("data_grid", ""),
1098
- "data_grid2": result2.get("data_grid2", ""),
1099
- "data_grid3": result2.get("data_grid3", ""),
1100
- "ts": int(time.time() * 1000),
1101
- "auto": True, "label": f"{label} (delayed)",
1102
- "screen_width": result2["width"],
1103
- "screen_height": result2["height"],
1104
- "mouse_x": result2["mouse_x"],
1105
- "mouse_y": result2["mouse_y"],
1106
- "has_grid": True,
1107
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1108
 
1109
  async def shot_explicit(label: str = ""):
1110
  """screenshot ุตุฑูŠุญ โ€” ูŠูุฑุณู„ ุฏุงุฆู…ุงู‹ ุจุฏูˆู† delta suppressionุŒ ูˆู„ู‡ ุฃูˆู„ูˆูŠุฉ ู…ุทู„ู‚ุฉ
1111
- ุนู„ู‰ ุฃูŠ ุนู…ู„ูŠุฉ shot_bg ุชู„ู‚ุงุฆูŠุฉ ุฌุงุฑูŠุฉ (ู„ุง ูŠู†ุชุธุฑ ุฎู„ูู‡ุง ุทูˆูŠู„ุงู‹)."""
1112
- result = await _priority_capture(0.65, 75)
1113
- if not result["data"]:
1114
- return
1115
- sess["last_bg_hash"] = _frame_hash(result["data"])
1116
- await send({
1117
- "type": "screenshot",
1118
- "data": result["data"],
1119
- "data_grid": result.get("data_grid", ""),
1120
- "data_grid2": result.get("data_grid2", ""),
1121
- "data_grid3": result.get("data_grid3", ""),
1122
- "ts": int(time.time() * 1000),
1123
- "auto": False, "label": label,
1124
- "screen_width": result["width"],
1125
- "screen_height": result["height"],
1126
- "mouse_x": result["mouse_x"],
1127
- "mouse_y": result["mouse_y"],
1128
- "has_grid": True,
1129
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1130
 
1131
  # โ”€โ”€ reset_computer: ุฅุนุงุฏุฉ ุถุจุท ุงู„ูƒู…ุจูŠูˆุชุฑ ูƒุฃู†ู‡ ุฌุฏูŠุฏ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1132
  if action == "reset_computer":
@@ -1145,7 +1119,17 @@ async def handle_action(ws: WebSocket, msg: dict, sess: dict):
1145
  # ู‡ุฐุง ูŠุถู…ู† ุฃู† ุฃูŠ ุฅุฌุฑุงุก ู„ุงุญู‚ (ู†ู‚ุฑุฉุŒ ุทุจุงุนุฉุŒ ุทู„ุจ screenshot ุขุฎุฑ) ูŠุตู„ ูˆูŠูุนุงู„ูŽุฌ
1146
  # ููˆุฑุงู‹ ุฏูˆู† ุงู†ุชุธุงุฑ ุงูƒุชู…ุงู„ ู‡ุฐู‡ ุงู„ู„ู‚ุทุฉ ุฃูˆู„ุงู‹.
1147
  if action == "screenshot":
1148
- asyncio.create_task(shot_explicit("explicit screenshot"))
 
 
 
 
 
 
 
 
 
 
1149
 
1150
  # โ”€โ”€ terminal โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1151
  elif action == "terminal":
@@ -1281,8 +1265,9 @@ async def handle_action(ws: WebSocket, msg: dict, sess: dict):
1281
  _wipe_firefox_session_data(profile_dir)
1282
  # ุญู‚ู† --profile ุฏุงุฎู„ ุฃู…ุฑ ูุงูŠุฑููˆูƒุณ (ุฅู† ู„ู… ูŠูƒู† ู…ุญู‚ูˆู†ุงู‹ ู…ุณุจู‚ุงู‹)
1283
  if "--profile" not in cmd and "-P " not in cmd:
1284
- cmd = cmd.replace("firefox", f"firefox --profile '{profile_dir}' --no-remote", 1)
1285
- env = {**os.environ, "DISPLAY": display}
 
1286
  proc = subprocess.Popen(cmd, shell=True, env=env,
1287
  stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
1288
  if any(b in cmd for b in ("firefox", "chromium", "chrome")):
@@ -1305,9 +1290,9 @@ async def handle_action(ws: WebSocket, msg: dict, sess: dict):
1305
  display, timeout=8
1306
  )
1307
  _wipe_firefox_session_data(profile_dir)
1308
- env = {**os.environ, "DISPLAY": display}
1309
  proc = subprocess.Popen(
1310
- [BROWSER, "--profile", profile_dir, "--no-remote", url], env=env,
1311
  stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
1312
  )
1313
  with _display_lock:
@@ -1507,7 +1492,7 @@ async def health():
1507
  ]
1508
  return {
1509
  "status": "ok",
1510
- "version": "v11-per-user-isolation",
1511
  "browser": BROWSER,
1512
  "active_users": n,
1513
  "users": users,
@@ -1519,7 +1504,11 @@ async def health():
1519
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
1520
 
1521
  async def _cleanup_tmp():
1522
- """ูŠู†ุธู‘ู /tmp ูƒู„ 5 ุฏู‚ุงุฆู‚."""
 
 
 
 
1523
  while True:
1524
  await asyncio.sleep(300)
1525
  try:
 
74
  BROWSER = _detect_browser()
75
  print(f"๐ŸŒ Browser: {BROWSER}")
76
 
77
+ # โ”€โ”€ CLI flags ุซุงุจุชุฉ ุชูุถุงู ู„ูƒู„ ุชุดุบูŠู„ Firefox โ”€โ”€
78
+ # ุชุณุฑู‘ุน ุงู„ุฅู‚ู„ุงุน (ู„ุง GPU ููŠุฒูŠุงุฆูŠ ุญู‚ูŠู‚ูŠ ููŠ Xvfb ูู„ุง ุฏุงุนูŠ ู„ู…ุญุงูˆู„ุฉ
79
+ # ุงู„ุชุณุฑูŠุน ุงู„ู…ุฑุฆูŠ ุงู„ุฐูŠ ูŠูุดู„ ุฏุงุฆู…ุงู‹ ู‡ู†ุง ูˆูŠุณุจู‘ุจ ุชุฃุฎูŠุฑุงู‹ ููŠ ุงู„ุฅู‚ู„ุงุน)ุŒ
80
+ # ูˆุชู…ู†ุน ุฃูŠ nag screens ุฃูˆ crash-reporter windows ู…ู† ุงู„ุธู‡ูˆุฑ.
81
+ FIREFOX_CLI_FLAGS = ["--no-remote", "--new-instance"]
82
+ FIREFOX_ENV_EXTRA = {
83
+ # ูŠู…ู†ุน ู…ุญุงูˆู„ุงุช ุชุณุฑูŠุน GPU ุบูŠุฑ ุงู„ู…ุชูˆูุฑุฉ ููŠ Xvfb ู…ู† ุฅุจุทุงุก ุงู„ุฅู‚ู„ุงุน
84
+ "MOZ_DISABLE_GPU_SANDBOX": "1",
85
+ "MOZ_ACCELERATED": "0",
86
+ }
87
+
88
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
89
  # โ”€โ”€ Firefox Profile (per-user, isolated, crash-recovery DISABLED) โ”€
90
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
 
136
  user_pref("app.normandy.enabled", false);
137
  user_pref("app.update.enabled", false);
138
  user_pref("app.update.auto", false);
139
+ // โ”€โ”€ ุฅุตู„ุงุญุงุช ุชุฌู…ุฏ ุงู„ุชุญู…ูŠู„ ("Connecting to ... ุชุจู‚ู‰ ู„ู„ุฃุจุฏ") โ”€โ”€
140
+ // ุงู„ุณุจุจ ุงู„ุบุงู„ุจ ุนู„ู‰ ุณูŠุฑูุฑุงุช ู…ุฌุงู†ูŠุฉ/container: ู…ุญุงูˆู„ุฉ IPv6 ุฃูˆู„ุงู‹ (ุบูŠุฑ
141
+ // ู…ุฏุนูˆู… ูุนู„ูŠุงู‹ ุนู„ู‰ ุฃุบู„ุจ ู…ุฒูˆุฏูŠ Render/HuggingFace ุงู„ู…ุฌุงู†ูŠุฉ) ู‚ุจู„ ุงู„ุฑุฌูˆุน
142
+ // ู„ู€ IPv4 ุจุนุฏ timeout ุทูˆูŠู„ ุฌุฏุงู‹ (ู‚ุฏ ูŠุตู„ 20-30 ุซุงู†ูŠุฉ ู„ูƒู„ ุทู„ุจ DNS)ุŒ
143
+ // ูˆู‡ุฐุง ุจุงู„ุถุจุท ุงู„ุณุจุจ ุงู„ุฐูŠ ูŠุธู‡ุฑ ูƒุดุงุดุฉ "Connecting..." ู…ุนู„ู‚ุฉ ู„ุง ุชุชู‚ุฏู… ุฃุจุฏุงู‹.
144
+ user_pref("network.dns.disableIPv6", true);
145
+ user_pref("network.http.fast-fallback-to-IPv4", true);
146
+ // ุชู‚ู„ูŠู„ ู…ู‡ู„ุฉ connect ุงู„ุฅุฌู…ุงู„ูŠุฉ ู„ูƒู„ ุทู„ุจ ู…ู† 15 ุซุงู†ูŠุฉ (ุงู„ุงูุชุฑุงุถูŠ) ุฅู„ู‰ 8 ุซูˆุงู†ู
147
+ // โ€” ุฃูุถู„ ุฃู† ูŠูุดู„ ุงู„ุทู„ุจ ุจุณุฑุนุฉ ูˆูŠู‚ุฑุฑ ุงู„ุฐูƒุงุก ุงู„ุงุตุทู†ุงุนูŠ ุจุฏูŠู„ุงู‹ ู…ู† ุฃู† ูŠุจู‚ู‰ ุนุงู„ู‚ุงู‹ ุทูˆูŠู„ุงู‹.
148
+ user_pref("network.http.connection-timeout", 8);
149
+ user_pref("network.http.response.timeout", 12);
150
+ // ุชุนุทูŠู„ predictive prefetching/speculative connect ุงู„ุชูŠ ุชุณุชู‡ู„ูƒ ุนุฑุถ ุญุฒู…ุฉ ุนู„ู‰
151
+ // ุณูŠุฑูุฑ ู…ุญุฏูˆุฏ ุงู„ู…ูˆุงุฑุฏ ุจุฏูˆู† ูุงุฆุฏุฉ ุญู‚ูŠู‚ูŠุฉ ู‡ู†ุง.
152
+ user_pref("network.dns.disablePrefetch", true);
153
+ user_pref("network.prefetch-next", false);
154
+ user_pref("network.predictor.enabled", false);
155
+ user_pref("network.http.speculative-parallel-limit", 0);
156
+ // ุชุนุทูŠู„ safebrowsing ุงู„ุฐูŠ ูŠุณุชุฏุนูŠ ุทู„ุจุงุช ุฎุงุฑุฌูŠุฉ ุฅุถุงููŠุฉ ุนู†ุฏ ูุชุญ ูƒู„ ุตูุญุฉ
157
+ // (ูŠูุจุทุฆ ุงู„ุชุญู…ูŠู„ ุฃูˆ ูŠุนู„ู‚ู‡ ู„ูˆ ุงู„ุทู„ุจ ู„ู‚ูˆุงุฆู… Google ูุดู„ ุฃูˆ ุงุณุชุบุฑู‚).
158
+ user_pref("browser.safebrowsing.malware.enabled", false);
159
+ user_pref("browser.safebrowsing.phishing.enabled", false);
160
+ user_pref("browser.safebrowsing.downloads.enabled", false);
161
+ user_pref("browser.safebrowsing.provider.google4.updateURL", "");
162
+ user_pref("browser.safebrowsing.provider.google.updateURL", "");
163
+ // ุชุนุทูŠู„ telemetry/captive-portal checks ุงู„ุชูŠ ุชูุชุญ ุงุชุตุงู„ุงุช ุฎู„ููŠุฉ ุบูŠุฑ ุถุฑูˆุฑูŠุฉ ุนู†ุฏ ุงู„ุฅู‚ู„ุงุน
164
+ user_pref("network.captive-portal-service.enabled", false);
165
+ user_pref("network.connectivity-service.enabled", false);
166
+ user_pref("toolkit.telemetry.server", "");
167
+ // ุชู‚ู„ูŠู„ ุนุฏุฏ ุงุชุตุงู„ุงุช HTTP ุงู„ู…ุชุฒุงู…ู†ุฉ ู„ูƒู„ ุฏูˆู…ูŠู† โ€” ูŠู‚ู„ู„ ุงู„ุถุบุท ุนู„ู‰ ุดุจูƒุฉ ู…ุญุฏูˆุฏุฉ
168
+ // ุงู„ู†ุทุงู‚ ู„ู„ุณูŠุฑูุฑุงุช ุงู„ู…ุฌุงู†ูŠุฉ ูˆูŠู‚ู„ู„ ุงุญุชู…ุงู„ ุงู„ุชุนู„ู‚.
169
+ user_pref("network.http.max-persistent-connections-per-server", 4);
170
+ user_pref("network.http.max-connections", 48);
171
  """
172
 
173
  def _ensure_firefox_profile(user_id: str, display: str) -> str:
 
235
  pass
236
 
237
  try:
238
+ # โ”€โ”€ ุฏู‚ุฉ 1280x800 ุจุฏู„ 1920x1080 โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
239
+ # ู‡ุฐุง ูŠู‚ู„ู„ ุญุฌู… ุจูŠุงู†ุงุช ูƒู„ ู„ู‚ุทุฉ ุดุงุดุฉ ุจู†ุณุจุฉ ~60%ุŒ ูˆุจุงู„ุชุงู„ูŠ ูŠุณุฑู‘ุน ูƒู„
240
+ # ู…ุฑุงุญู„ ุงู„ุงู„ุชู‚ุงุท ูˆุงู„ุชุฑู…ูŠุฒ ุจุดูƒู„ ูƒุจูŠุฑ ุนู„ู‰ ุณูŠุฑูุฑ ุจู…ูˆุงุฑุฏ ู…ุญุฏูˆุฏุฉ.
241
+ # ู†ูุณ ุงู„ุฏู‚ุฉ ุงู„ู…ุณุชุฎุฏู…ุฉ ููŠ ุงู„ุชุทุจูŠู‚ุงุช ุงู„ู…ุฑุฌุนูŠุฉ ู„ู€ computer-use.
242
  proc = subprocess.Popen(
243
+ ["Xvfb", display, "-screen", "0", "1280x800x24",
244
  "-nolisten", "tcp", "-ac"],
245
  stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
246
  )
 
481
  return None, 0, 0
482
 
483
 
484
+ def _capture_via_import_pipe(display: str):
485
+ """
486
+ ุงู„ุทุฑูŠู‚ุฉ ุงู„ุฃุณุงุณูŠุฉ ูˆุงู„ูˆุญูŠุฏุฉ: `import -window root` ู…ุน ุงู„ุฅุฎุฑุงุฌ ู…ุจุงุดุฑุฉ
487
+ ุนุจุฑ stdout (pipe) โ€” ุจุฏูˆู† ุฃูŠ ู…ู„ู ู…ุคู‚ุช ุนู„ู‰ ุงู„ู‚ุฑุต. ุฃุณุฑุน ุจุดูƒู„ ู…ู„ุญูˆุธ ู…ู†
488
+ ุงู„ูƒุชุงุจุฉ ู„ู…ู„ู ุซู… ุฅุนุงุฏุฉ ู‚ุฑุงุกุชู‡ุŒ ุฎุตูˆุตุงู‹ ุนู„ู‰ ุชุฎุฒูŠู† ุดุจูƒูŠ ุจุทูŠุก.
489
+ """
490
  from PIL import Image
491
  env = {**os.environ, "DISPLAY": display}
 
 
 
 
 
 
 
 
 
 
 
 
 
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
499
+ img = Image.open(io.BytesIO(r.stdout)).convert("RGB")
500
+ w, h = img.size
501
+ if w < 100 or h < 100:
502
+ return None, 0, 0
503
+ return img, w, h
504
+ except FileNotFoundError:
505
+ print(f"[cap:{display}] import (ImageMagick) not installed")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
506
  return None, 0, 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
507
  except Exception as e:
508
+ print(f"[cap:{display}] import-pipe: {e}")
509
+ return None, 0, 0
 
 
 
 
 
 
 
510
 
511
 
512
+ def _capture_via_xlib_direct(display: str):
513
+ """
514
+ ุฎุท ุงู„ุฏูุงุน ุงู„ุซุงู†ูŠ ูˆุงู„ุฃุฎูŠุฑ: ู‚ุฑุงุกุฉ X11 framebuffer ู…ุจุงุดุฑุฉ ุนุจุฑ
515
+ python-xlibุŒ ุจุฏูˆู† subprocess ุฅุทู„ุงู‚ุงู‹ (ุฃุณุฑุน ู…ู† ุฃูŠ ุฃุฏุงุฉ ุฎุงุฑุฌูŠุฉุŒ
516
+ ูˆูŠุนู…ู„ ุญุชู‰ ู„ูˆ ImageMagick ุบูŠุฑ ู…ุซุจู‘ุช ุนู„ู‰ ุงู„ุณูŠุฑูุฑ).
517
+ """
518
+ from PIL import Image
519
  try:
520
+ from Xlib import display as Xdisp, X
521
+ xd = Xdisp.Display(display)
522
+ root = xd.screen().root
523
+ geom = root.get_geometry()
524
+ w, h = geom.width, geom.height
525
+ raw = root.get_image(0, 0, w, h, X.ZPixmap, 0xFFFFFFFF)
526
+ img = Image.frombuffer("RGB", (w, h), raw.data, "raw", "BGRX", 0, 1)
527
+ xd.close()
528
+ return img, w, h
 
 
 
529
  except Exception as e:
530
+ print(f"[cap:{display}] xlib-direct: {e}")
531
+ return None, 0, 0
 
 
 
 
 
 
532
 
533
 
534
  def _capture_raw(display: str) -> tuple:
535
+ """
536
+ ูŠู„ุชู‚ุท ุงู„ุดุงุดุฉ ุจุฃุณุฑุน ุทุฑูŠู‚ุฉ ู…ูˆุซูˆู‚ุฉ: import-pipe ุฃูˆู„ุงู‹ (ุณุฑูŠุนุฉ ุฌุฏุงู‹)ุŒ
537
+ ูˆุนู†ุฏ ูุดู„ู‡ุง ูู‚ุท xlib-direct ูƒุจุฏูŠู„ ูˆุงุญุฏ. ุจุญุฏ ุฃู‚ุตู‰ ู…ุญุงูˆู„ุชูŠู† ุณุฑูŠุนุชูŠู†
538
+ ู„ูƒู„ ุทุฑูŠู‚ุฉ (ูˆู„ูŠุณ 9 ู…ุญุงูˆู„ุงุช ูƒุงู„ุณุงุจู‚) โ€” ู„ุฃู† ูƒู„ุง ุงู„ุทุฑูŠู‚ุชูŠู† ุฅู…ุง ุชู†ุฌุญุงู†
539
+ ููˆุฑุงู‹ ุฃูˆ ุชูุดู„ุงู† ู„ุณุจุจ ุฏุงุฆู…ุŒ ูˆุฅุนุงุฏุฉ ุงู„ู…ุญุงูˆู„ุฉ ุฃูƒุซุฑ ู…ู† ู…ุฑุชูŠู† ู„ุง ุชูุบูŠู‘ุฑ
540
+ ุงู„ู†ุชูŠุฌุฉุŒ ูู‚ุท ุชูุจุทุฆ ุงู„ุงุณุชุฌุงุจุฉ.
541
+ """
542
+ for method_name, method in (
543
+ ("import-pipe", _capture_via_import_pipe),
544
+ ("xlib-direct", _capture_via_xlib_direct),
545
+ ):
546
+ for attempt in range(2):
547
  try:
548
  img, w, h = method(display)
549
  if img and not _is_black_screen(img):
550
+ if attempt > 0:
551
+ print(f"[cap:{display}] โœ… {method_name} succeeded on retry")
552
  return img, w, h
553
  except Exception as e:
554
  print(f"[cap:{display}] {method_name}: {e}")
555
+ if attempt == 0:
556
+ time.sleep(0.3)
557
 
558
  print(f"[cap:{display}] โš ๏ธ All methods failed โ€” placeholder")
559
  from PIL import Image as PILImg, ImageDraw
560
  sw, sh = _get_screen_size(display)
561
+ img = PILImg.new("RGB", (sw or 1280, sh or 800), (15, 20, 40))
562
  draw = ImageDraw.Draw(img)
563
  draw.rectangle([(0, 0), (sw, 55)], fill=(30, 40, 80))
564
  draw.text((10, 10), f"โš ๏ธ Screenshot failed โ€” DISPLAY={display}", fill=(255, 120, 80))
565
+ draw.text((10, 32), "Methods tried: import-pipe, xlib-direct", fill=(130, 130, 150))
566
+ return img, sw or 1280, sh or 800
567
+
568
 
569
  def _get_screen_size(display: str) -> tuple:
570
  try:
 
576
  parts = r.stdout.strip().split()
577
  return int(parts[0]), int(parts[1])
578
  except Exception:
579
+ return 1280, 800
580
 
581
 
582
  def _get_mouse_pos(display: str) -> tuple:
 
604
 
605
  def _render_grid_variant(base_img, ow, oh, sw, sh, mx, my, display,
606
  grid_step: int, line_color: tuple, label_every: int,
607
+ major_only_labels: bool = True):
608
  """
609
  ูŠุฑุณู… ู†ุณุฎุฉ ุดุจูƒุฉ ุฅุญุฏุงุซูŠุงุช ูˆุงุญุฏุฉ ููˆู‚ ู†ุณุฎุฉ ู…ู† ุงู„ุตูˆุฑุฉ ุงู„ุฃุณุงุณูŠุฉ.
610
  grid_step โ†’ ุงู„ู…ุณุงูุฉ ุจุงู„ุจูƒุณู„ ุงู„ุญู‚ูŠู‚ูŠ ุจูŠู† ูƒู„ ุฎุท ุดุจูƒุฉ.
611
+ line_color โ†’ ู„ูˆู† ุงู„ุฎุทูˆุท ูˆุงู„ุฃุฑู‚ุงู… (RGBA).
612
+ label_every โ†’ ูƒู„ ูƒู… ุจูƒุณู„ ุชููƒุชุจ ููŠู‡ ุชุณู…ูŠุฉ ุฅุญุฏุงุซูŠ (x,y) ูƒุงู…ู„ุฉ ุนู†ุฏ ุงู„ุชู‚ุงุทุนุงุช.
613
+ major_only_labels โ†’ ุฅุฐุง TrueุŒ ุงู„ุฃุฑู‚ุงู… ุนู„ู‰ ุงู„ุญูˆุงู ุชุธู‡ุฑ ูู‚ุท ุนู†ุฏ ุฎุทูˆุท "ุฑุฆูŠุณูŠุฉ".
614
  """
615
+ from PIL import ImageDraw
616
  img = base_img.copy()
617
  draw = ImageDraw.Draw(img, "RGBA")
618
 
 
646
  y_majors.append((y_sc, y_r))
647
  y_sc += step_y; y_r += grid_step
648
 
 
649
  for (xs, xr) in x_majors:
650
  for (ys, yr) in y_majors:
651
  label = f"{xr},{yr}"
 
666
  return final
667
 
668
 
669
+ def capture_with_grid(display: str, scale: float = 0.85, quality: int = 72,
670
  force_mx: int | None = None,
671
  force_my: int | None = None,
672
  grid_step: int = 50) -> dict:
673
  """
674
+ ูŠู„ุชู‚ุท ุงู„ุดุงุดุฉ ู…ู† display ู…ุญุฏุฏ ูˆูŠูู†ุชุฌ 3 ู†ุณุฎ ู…ู† ู†ูุณ ุงู„ู„ู‚ุทุฉ ุจุงู„ุถุจุท (ู†ูุณ ุงู„ู„ุญุธุฉ):
675
+ - "data" โ†’ ุงู„ู†ุณุฎุฉ ุงู„ุนุงุฏูŠุฉ (ู†ุธูŠูุฉ ุชู…ุงู…ุงู‹) + ุนู„ุงู…ุฉ ุงู„ู…ุงูˆุณ ูู‚ุทุŒ ุจุฏูˆู† ุฃูŠ Grid.
676
+ - "data_grid" โ†’ ุดุจูƒุฉ ุฅุญุฏุงุซูŠุงุช ุนุงุฏูŠุฉ (ูƒู„ 50px)ุŒ ุฃุฎุถุฑ/ุณู…ุงูˆูŠ โ€” ู„ู„ุฐูƒุงุก ุงู„ุงุตุทู†ุงุนูŠ.
677
+ - "data_grid2" โ†’ ุดุจูƒุฉ ุฅุญุฏุงุซูŠุงุช ุฃุฏู‚ (ูƒู„ 20px)ุŒ ุฃุญู…ุฑ โ€” ู„ู„ู†ู‚ุฑุงุช ุงู„ุฏู‚ูŠู‚ุฉ.
678
+ โ€”โ€” ุชุจุณูŠุท v13: ู†ุณุฎุชุงู† ูู‚ุท (ุจุฏู„ 3) ู„ุชู‚ู„ูŠู„ ุนุฏุฏ ุนู…ู„ูŠุงุช resize/encode ู„ูƒู„
679
+ ู„ู‚ุทุฉ ุจู†ุณุจุฉ ู‚ุฑุงุจุฉ ุณ4ุฅู„ู‰ ุณ2ุŒ ูˆุฏู‚ุฉ ุงู„ู…ู‚ุงุณ ุงู„ูˆุงุญุฏ 1280x800 ุงู„ุฌุฏูŠุฏุฉ ู„ู…
680
+ ุชุนุฏ ุชุญุชุงุฌ ู„ุซู„ุงุซ ุชุฏุฑุฌุงุช ุฏู‚ุฉ ู…ู†ูุตู„ุฉ ู„ุชุญุฏูŠุฏ ุงู„ุฅุญุฏุงุซูŠุงุช.
 
681
  """
682
  from PIL import Image, ImageDraw
683
 
684
  img, ow, oh = _capture_raw(display)
685
  if img is None:
686
  return {"data": "", "data_grid": "", "data_grid2": "", "data_grid3": "",
687
+ "width": 1280, "height": 800, "mouse_x": 0, "mouse_y": 0}
688
 
689
  mx, my = (force_mx, force_my) if force_mx is not None else _get_mouse_pos(display)
690
 
 
694
  msx = int(mx * sw / ow)
695
  msy = int(my * sh / oh)
696
 
 
697
  clean_img = base_img.copy()
698
  draw_clean = ImageDraw.Draw(clean_img, "RGBA")
699
  _draw_mouse_marker(draw_clean, msx, msy)
 
702
  clean_final.save(buf_clean, format="JPEG", quality=quality, optimize=True)
703
  data_clean = base64.b64encode(buf_clean.getvalue()).decode()
704
 
 
705
  grid1 = _render_grid_variant(base_img, ow, oh, sw, sh, mx, my, display,
706
  grid_step=grid_step, line_color=(0, 255, 180, 235),
707
  label_every=100)
708
  buf1 = io.BytesIO(); grid1.save(buf1, format="JPEG", quality=quality, optimize=True)
709
  data_grid1 = base64.b64encode(buf1.getvalue()).decode()
710
 
 
711
  grid2 = _render_grid_variant(base_img, ow, oh, sw, sh, mx, my, display,
712
+ grid_step=20, line_color=(255, 60, 60, 235),
713
+ label_every=60, major_only_labels=True)
714
+ buf2 = io.BytesIO(); grid2.save(buf2, format="JPEG", quality=max(quality, 78), optimize=True)
715
  data_grid2 = base64.b64encode(buf2.getvalue()).decode()
716
 
 
 
 
 
 
 
 
717
  return {
718
+ "data": data_clean,
719
+ "data_grid": data_grid1,
720
+ "data_grid2": data_grid2,
721
+ "data_grid3": data_grid2, # ุชูˆุงูู‚ูŠุฉ ุฑุฌุนูŠุฉ: ู†ูุณ ู‚ูŠู…ุฉ data_grid2 ู„ุฃูŠ ูƒูˆุฏ ู‚ุฏูŠู… ูŠู‚ุฑุฃ data_grid3
722
  "width": ow, "height": oh, "mouse_x": mx, "mouse_y": my,
723
  }
724
 
 
1013
  ู„ุฃู† ุงู„ู…ุญุชูˆู‰ ู…ุชูˆู‚ุน ุฃู† ูŠูƒูˆู† ู…ุฎุชู„ูุงู‹ (ุตูุญุฉ ุชุญู…ู‘ู„ุช ุฃูƒุซุฑ) ุญุชู‰ ู„ูˆ ุชุดุงุจู‡ ุงู„ู€ hash
1014
  ุฌุฒุฆูŠุงู‹ ู…ุน ุฎู„ููŠุฉ ู…ุดุงุจู‡ุฉ.
1015
  """
1016
+ try:
1017
+ await asyncio.sleep(delay)
1018
+ now = time.time()
1019
+ if now - sess.get("last_bg_shot_ts", 0) < 0.3:
1020
+ await asyncio.sleep(0.3 - (now - sess["last_bg_shot_ts"]))
1021
 
1022
+ result = await _safe_capture(0.65, 72, force_mx, force_my)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1023
  sess["last_bg_shot_ts"] = time.time()
1024
+ if result["data"]:
1025
+ fh = _frame_hash(result["data"])
1026
+ if fh != sess.get("last_bg_hash", ""):
1027
+ sess["last_bg_hash"] = fh
1028
+ await send({
1029
+ "type": "screenshot",
1030
+ "data": result["data"],
1031
+ "data_grid": result.get("data_grid", ""),
1032
+ "data_grid2": result.get("data_grid2", ""),
1033
+ "data_grid3": result.get("data_grid3", ""),
1034
+ "ts": int(time.time() * 1000),
1035
+ "auto": True, "label": label,
1036
+ "screen_width": result["width"],
1037
+ "screen_height": result["height"],
1038
+ "mouse_x": result["mouse_x"],
1039
+ "mouse_y": result["mouse_y"],
1040
+ "has_grid": True,
1041
+ })
1042
+
1043
+ if extra_shot_delay:
1044
+ await asyncio.sleep(extra_shot_delay)
1045
+ result2 = await _safe_capture(0.65, 72, force_mx, force_my)
1046
+ sess["last_bg_shot_ts"] = time.time()
1047
+ if result2["data"]:
1048
+ fh2 = _frame_hash(result2["data"])
1049
+ sess["last_bg_hash"] = fh2
1050
+ await send({
1051
+ "type": "screenshot",
1052
+ "data": result2["data"],
1053
+ "data_grid": result2.get("data_grid", ""),
1054
+ "data_grid2": result2.get("data_grid2", ""),
1055
+ "data_grid3": result2.get("data_grid3", ""),
1056
+ "ts": int(time.time() * 1000),
1057
+ "auto": True, "label": f"{label} (delayed)",
1058
+ "screen_width": result2["width"],
1059
+ "screen_height": result2["height"],
1060
+ "mouse_x": result2["mouse_x"],
1061
+ "mouse_y": result2["mouse_y"],
1062
+ "has_grid": True,
1063
+ })
1064
+ except Exception as e:
1065
+ # ู„ู‚ุทุฉ ุฎู„ููŠุฉ ูุงุดู„ุฉ ู„ุง ูŠุฌุจ ุฃู† ุชูˆู‚ู ุงู„ุฌู„ุณุฉ ุฃูˆ ุชุธู‡ุฑ ุจุตู…ุช ููŠ ุงู„ู„ูˆู‚ ูู‚ุท.
1066
+ print(f"[shot_bg:{display}] โš ๏ธ {e}")
1067
 
1068
  async def shot_explicit(label: str = ""):
1069
  """screenshot ุตุฑูŠุญ โ€” ูŠูุฑุณู„ ุฏุงุฆู…ุงู‹ ุจุฏูˆู† delta suppressionุŒ ูˆู„ู‡ ุฃูˆู„ูˆูŠุฉ ู…ุทู„ู‚ุฉ
1070
+ ุนู„ู‰ ุฃูŠ ุนู…ู„ูŠุฉ shot_bg ุชู„ู‚ุงุฆูŠุฉ ุฌุงุฑูŠุฉ (ู„ุง ูŠู†ุชุธุฑ ุฎู„ูู‡ุง ุทูˆูŠู„ุงู‹).
1071
+ โ”€โ”€ ุฅุตู„ุงุญ ุฌุฐุฑูŠ: try/except ุดุงู…ู„ + ุฅุฑุณุงู„ ุฑุณุงู„ุฉ ุฎุทุฃ ุตุฑูŠุญุฉ ู„ู„ุนู…ูŠู„ โ”€โ”€
1072
+ ุงู„ู…ุดูƒู„ุฉ ุงู„ู‚ุฏูŠู…ุฉ: ู‡ุฐู‡ ุงู„ุฏุงู„ุฉ ุชูุดุบูŽู‘ู„ ุนุจุฑ asyncio.create_task ุจุฏูˆู† ุฃูŠ
1073
+ ู…ุนุงู„ุฌุฉ ุงุณุชุซู†ุงุก ู…ุญูŠุทุฉ ุจู‡ุง. ุฃูŠ ุฎุทุฃ ุบูŠุฑ ู…ุชูˆู‚ุน ูƒุงู† ูŠูุณู‚ุท ุงู„ู€ task ุจุงู„ูƒุงู…ู„
1074
+ ุจุตู…ุช ุชุงู… โ€” ู„ุง ุฑุณุงู„ุฉ ุชุตู„ ู„ู„ุนู…ูŠู„ ูˆู„ุง ุญุชู‰ ุณุทุฑ ููŠ ุงู„ู„ูˆู‚ุŒ ููŠุจู‚ู‰ ุงู„ุนู…ูŠู„
1075
+ ูŠู†ุชุธุฑ frame ู„ู† ูŠุตู„ ุฃุจุฏุงู‹. ุงู„ุขู† ุฃูŠ ูุดู„ ูŠูุณุฌูŽู‘ู„ ูˆูŠูุฑุณูŽู„ ูƒุฑุณุงู„ุฉ "error"
1076
+ ุตุฑูŠุญุฉุŒ ูˆุงู„ุนู…ูŠู„ (index.html) ูŠุชุนุงู…ู„ ู…ุนู‡ุง ูˆูŠุนูŠุฏ ุงู„ู…ุญุงูˆู„ุฉ ููˆุฑุงู‹."""
1077
+ try:
1078
+ result = await _priority_capture(0.65, 75)
1079
+ if not result or not result["data"]:
1080
+ print(f"[shot_explicit:{display}] โš ๏ธ empty capture result")
1081
+ await send({"type": "error", "action": "screenshot", "msg": "capture returned empty"})
1082
+ return
1083
+ sess["last_bg_hash"] = _frame_hash(result["data"])
1084
+ await send({
1085
+ "type": "screenshot",
1086
+ "data": result["data"],
1087
+ "data_grid": result.get("data_grid", ""),
1088
+ "data_grid2": result.get("data_grid2", ""),
1089
+ "data_grid3": result.get("data_grid3", ""),
1090
+ "ts": int(time.time() * 1000),
1091
+ "auto": False, "label": label,
1092
+ "screen_width": result["width"],
1093
+ "screen_height": result["height"],
1094
+ "mouse_x": result["mouse_x"],
1095
+ "mouse_y": result["mouse_y"],
1096
+ "has_grid": True,
1097
+ })
1098
+ except Exception as e:
1099
+ print(f"[shot_explicit:{display}] โŒ EXCEPTION: {e}")
1100
+ try:
1101
+ await send({"type": "error", "action": "screenshot", "msg": str(e)})
1102
+ except Exception:
1103
+ pass
1104
 
1105
  # โ”€โ”€ reset_computer: ุฅุนุงุฏุฉ ุถุจุท ุงู„ูƒู…ุจูŠูˆุชุฑ ูƒุฃู†ู‡ ุฌุฏูŠุฏ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1106
  if action == "reset_computer":
 
1119
  # ู‡ุฐุง ูŠุถู…ู† ุฃู† ุฃูŠ ุฅุฌุฑุงุก ู„ุงุญู‚ (ู†ู‚ุฑุฉุŒ ุทุจุงุนุฉุŒ ุทู„ุจ screenshot ุขุฎุฑ) ูŠุตู„ ูˆูŠูุนุงู„ูŽุฌ
1120
  # ููˆุฑุงู‹ ุฏูˆู† ุงู†ุชุธุงุฑ ุงูƒุชู…ุงู„ ู‡ุฐู‡ ุงู„ู„ู‚ุทุฉ ุฃูˆู„ุงู‹.
1121
  if action == "screenshot":
1122
+ # โ”€โ”€ ุทุจู‚ุฉ ุญู…ุงูŠุฉ ุฅุถุงููŠุฉ: ุญุชู‰ ู„ูˆ ุญุตู„ ุฎุทุฃ ุบูŠุฑ ู…ุชูˆู‚ุน ุชู…ุงู…ุงู‹ ุฏุงุฎู„
1123
+ # shot_explicit ุชูู„ุช ู…ู† ุงู„ู€ try/except ุงู„ุฏุงุฎู„ูŠ (ู…ุซู„ CancelledError
1124
+ # ุฃูˆ ุฎุทุฃ ุนู†ุฏ ุฅู†ุดุงุก ุงู„ู€ task ู†ูุณู‡)ุŒ ู‡ุฐุง ุงู„ุบู„ุงู ุงู„ุฎุงุฑุฌูŠ ูŠุถู…ู† ุชุณุฌูŠู„ู‡
1125
+ # ููŠ ุงู„ู„ูˆู‚ ุจุฏู„ ุฃู† ูŠุฎุชููŠ ุจุตู…ุช ูƒู€ "unhandled task exception" ููŠ event
1126
+ # loop ุจุงูŠุซูˆู† (ูˆู‡ุฐุง ูƒุงู† ู„ู‡ ุณู„ูˆูƒ ุงูุชุฑุงุถูŠ ุจุตุงู…ุช ููŠ ุจุนุถ ุฅุนุฏุงุฏุงุช uvicorn).
1127
+ _task = asyncio.create_task(shot_explicit("explicit screenshot"))
1128
+ def _on_shot_done(t: asyncio.Task):
1129
+ exc = t.exception() if not t.cancelled() else None
1130
+ if exc:
1131
+ print(f"[screenshot-task:{display}] โŒ unhandled exception: {exc}")
1132
+ _task.add_done_callback(_on_shot_done)
1133
 
1134
  # โ”€โ”€ terminal โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1135
  elif action == "terminal":
 
1265
  _wipe_firefox_session_data(profile_dir)
1266
  # ุญู‚ู† --profile ุฏุงุฎู„ ุฃู…ุฑ ูุงูŠุฑููˆูƒุณ (ุฅู† ู„ู… ูŠูƒู† ู…ุญู‚ูˆู†ุงู‹ ู…ุณุจู‚ุงู‹)
1267
  if "--profile" not in cmd and "-P " not in cmd:
1268
+ flags_str = " ".join(FIREFOX_CLI_FLAGS)
1269
+ cmd = cmd.replace("firefox", f"firefox --profile '{profile_dir}' {flags_str}", 1)
1270
+ env = {**os.environ, "DISPLAY": display, **(FIREFOX_ENV_EXTRA if "firefox" in cmd.lower() else {})}
1271
  proc = subprocess.Popen(cmd, shell=True, env=env,
1272
  stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
1273
  if any(b in cmd for b in ("firefox", "chromium", "chrome")):
 
1290
  display, timeout=8
1291
  )
1292
  _wipe_firefox_session_data(profile_dir)
1293
+ env = {**os.environ, "DISPLAY": display, **FIREFOX_ENV_EXTRA}
1294
  proc = subprocess.Popen(
1295
+ [BROWSER, "--profile", profile_dir, *FIREFOX_CLI_FLAGS, url], env=env,
1296
  stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
1297
  )
1298
  with _display_lock:
 
1492
  ]
1493
  return {
1494
  "status": "ok",
1495
+ "version": "v13-fast-capture-fixed-firefox",
1496
  "browser": BROWSER,
1497
  "active_users": n,
1498
  "users": users,
 
1504
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
1505
 
1506
  async def _cleanup_tmp():
1507
+ """ูŠู†ุธู‘ู /tmp ูƒู„ 5 ุฏู‚ุงุฆู‚. ู…ู„ุงุญุธุฉ v13: ู…ุญุฑูƒ ุงู„ุงู„ุชู‚ุงุท
1508
+ ุงู„ุฌุฏูŠุฏ (import-pipe/xlib-direct) ู„ุง ูŠูƒุชุจ ุฃูŠ ู…ู„ูุงุช zss_* ุนู„ู‰ ุงู„ุฅุทู„ุงู‚ (ูƒู„ุดูŠุก
1509
+ ููŠ ุงู„ุฐุงูƒุฑุฉ ุนุจุฑ pipe)ุŒ ูู‡ุฐู‡ ุงู„ุฏุงู„ุฉ ุฃุตุจุญุช ุบูŠุฑ ุถุฑูˆุฑูŠุฉ ูุนู„ูŠุงู‹ ู„ู„ู‚ุทุงุช
1510
+ ุงู„ุดุงุดุฉุŒ ู„ูƒู†ู‡ุง ุชูุฑูƒ ูƒุดุจูƒุฉ ุฃู…ุงู† ุฅุถุงููŠุฉ (ู…ุซู„ุงู‹ ู„ูˆ ุฃุถูŠู ูƒูˆุฏ ู…ุณุชู‚ุจู„ุงู‹
1511
+ ูŠูƒุชุจ ู…ู„ูุงุช ู…ุคู‚ุชุฉ ุจู†ูุณ ุงู„ุจุงุฏุฆุฉ)."""
1512
  while True:
1513
  await asyncio.sleep(300)
1514
  try: