#!/usr/bin/env python3 """Opt-in live X11 entry regression check. Follow with verify_return_surface. DISPLAY=:0 python3 verify_entry_surface.py 0 Starts a real stream from card 0/1/2 and uses Ctrl+Alt+Shift+Q to return. Requires verify_return_surface.py in the same directory. """ import ctypes as C import json import subprocess import sys import time from verify_return_surface import x, display, root, attrs, find, child_at, Attributes class ClassHint(C.Structure): _fields_ = [("name", C.c_void_p), ("class_name", C.c_void_p)] x.XQueryTree.argtypes = [C.c_void_p, C.c_ulong, C.POINTER(C.c_ulong), C.POINTER(C.c_ulong), C.POINTER(C.POINTER(C.c_ulong)), C.POINTER(C.c_uint)] x.XGetClassHint.argtypes = [C.c_void_p, C.c_ulong, C.POINTER(ClassHint)] x.XFree.argtypes = [C.c_void_p] # A client may destroy a window between QueryTree and GetWindowAttributes. # Ignore that expected BadWindow race; all other errors fail the check. errors = [] class XError(C.Structure): _fields_ = [("type", C.c_int), ("display", C.c_void_p), ("resource", C.c_ulong), ("serial", C.c_ulong), ("error_code", C.c_ubyte), ("request_code", C.c_ubyte), ("minor_code", C.c_ubyte)] @C.CFUNCTYPE(C.c_int, C.c_void_p, C.POINTER(XError)) def on_error(_, error): if error.contents.error_code != 3: # BadWindow errors.append(error.contents.error_code) return 0 x.XSetErrorHandler.argtypes = [C.c_void_p] x.XSetErrorHandler(on_error) def windows(): returned_root, parent, count = C.c_ulong(), C.c_ulong(), C.c_uint() children = C.POINTER(C.c_ulong)() x.XQueryTree(display, root, C.byref(returned_root), C.byref(parent), C.byref(children), C.byref(count)) ids = [children[i] for i in range(count.value)] if children: x.XFree(children) result = {} for window in ids: a = Attributes() if not x.XGetWindowAttributes(display, window, C.byref(a)) or a.map_state != 2: continue hint = ClassHint() cls = "" if x.XGetClassHint(display, window, C.byref(hint)): if hint.class_name: cls = C.string_at(hint.class_name).decode("utf-8", "replace") x.XFree(hint.class_name) if hint.name: x.XFree(hint.name) result[window] = (a, cls) return result def main(): index = int(sys.argv[1]) if index not in (0, 1, 2): raise ValueError("Desktop index must be 0/1/2") geometry = attrs(root) width, height = geometry.width, geometry.height kiosk = find("--onlyvisible", "--name", "^VDI 云终端$")[0] if find("--onlyvisible", "--class", "com.moonlight_stream.Moonlight"): raise RuntimeError("Start this check from the idle desktop list") subprocess.run(["xdotool", "mousemove", str(width - 155), str(209 + 110 * index), "click", "1"], check=True) started = time.monotonic() samples, violations, states, first_geometry = 0, [], [], {} foreground_at = None points = [(20, 10), (width // 2, height // 2), (width - 20, height - 10)] ready = False while time.monotonic() - started < 35: elapsed = time.monotonic() - started mapped = windows() samples += 1 tops = [child_at(px, py) for px, py in points] classes = {w: cls for w, (_, cls) in mapped.items()} for window, (a, cls) in mapped.items(): if "moonlight" in cls.lower(): geometry = [a.x, a.y, a.width, a.height] first_geometry.setdefault(window, {"t": round(elapsed, 4), "class": cls, "geometry": geometry}) if cls != "com.moonlight_stream.Moonlight" or geometry != [0, 0, width, height]: violations.append({"t": elapsed, "window": window, "class": cls, "geometry": geometry}) if kiosk not in mapped: violations.append({"t": elapsed, "reason": "kiosk backing window unmapped"}) if not states or states[-1]["windows"] != tops: states.append({"t": round(elapsed, 4), "windows": tops, "classes": classes}) if all(classes.get(w) == "com.moonlight_stream.Moonlight" for w in tops): if foreground_at is None: foreground_at = elapsed if elapsed - foreground_at >= 2: ready = True break else: foreground_at = None time.sleep(0.004) with open("/tmp/entry-live-%s.xwd" % index, "wb") as screenshot: subprocess.run(["xwd", "-root", "-silent"], stdout=screenshot, check=True) subprocess.run(["xdotool", "key", "--clearmodifiers", "ctrl+alt+shift+q"], check=True) time.sleep(1) returned = all(child_at(px, py) == kiosk for px, py in points) result = {"desktop_index": index, "entry_samples": samples, "stream_foreground": ready, "returned_to_kiosk": returned, "first_mapped_moonlight_geometry": first_geometry, "stack_changes": states, "violations": violations, "x11_errors": errors, "ok": ready and returned and not violations and not errors} print(json.dumps(result, indent=2, ensure_ascii=False)) return 0 if result["ok"] else 1 try: sys.exit(main()) finally: x.XCloseDisplay(display)