thumbnail_data_url swaps the process-wide os.environ["XAUTHORITY"] around ImageGrab.grab; a multiplexed gateway thumbnails several profiles from worker threads at once, so one grab could run with another profile's cookie (Xlib auth failure or the wrong screen) and the restore left the wrong value behind. A module lock serialises swap+grab+restore. (cherry picked from commit be789c85fdbae869a31d63f30a3434f03d350a50)
47 lines
1.8 KiB
Python
47 lines
1.8 KiB
Python
"""Thumbnail of a bot's screen: one JPEG grab of the profile's Xvnc display.
|
|
|
|
Feeds the Screen hero in Hermes Desktop (the big preview at the top of a bot's pane). Read-only:
|
|
it never touches the lease, so a human in control is not disturbed and the bot is not blocked.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import io
|
|
import os
|
|
import threading
|
|
from typing import Optional
|
|
|
|
from tools.bot_desktop import runtime
|
|
|
|
THUMB_MAX = (960, 600)
|
|
_grab_lock = threading.Lock()
|
|
|
|
|
|
def thumbnail_data_url(max_size: tuple[int, int] = THUMB_MAX, quality: int = 72) -> Optional[str]:
|
|
"""``data:image/jpeg;base64,...`` of the running screen, or ``None`` when no screen is up."""
|
|
env = runtime.published_env()
|
|
display = env.get("DISPLAY")
|
|
if not display or runtime._launcher_pid() is None:
|
|
return None
|
|
from PIL import ImageGrab # Pillow is a hard dependency; import lazily to keep status calls cheap
|
|
|
|
# Xlib reads XAUTHORITY from the process env; the launcher publishes a per-profile cookie file.
|
|
# The swap is process-wide, so two profiles grabbed on worker threads at once serialise here or
|
|
# one would grab with the other's cookie and restore the wrong value.
|
|
with _grab_lock:
|
|
previous = os.environ.get("XAUTHORITY")
|
|
if env.get("XAUTHORITY"):
|
|
os.environ["XAUTHORITY"] = env["XAUTHORITY"]
|
|
try:
|
|
image = ImageGrab.grab(xdisplay=display)
|
|
finally:
|
|
if previous is None:
|
|
os.environ.pop("XAUTHORITY", None)
|
|
else:
|
|
os.environ["XAUTHORITY"] = previous
|
|
image.thumbnail(max_size)
|
|
buf = io.BytesIO()
|
|
image.convert("RGB").save(buf, "JPEG", quality=quality, optimize=True)
|
|
return "data:image/jpeg;base64," + base64.b64encode(buf.getvalue()).decode("ascii")
|