135 lines
5.3 KiB
Python
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)
|