vdi-cloud/terminal/kiosk-shell/tests/verify_entry_surface.py

135 lines
5.3 KiB
Python

#!/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)