Files
hermes-agent/tools/bot_desktop/thumbnail.py
teknium1 c1d996a84d fix(bot-screen): serialise the XAUTHORITY swap around thumbnail grabs
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)
2026-09-12 18:58:39 -07:00

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")