121 lines
5.8 KiB
Python
121 lines
5.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Controlled existing-stream POC; always restores the remote-video cursor toggle.
|
|
|
|
Only run with cursor-poc-host.ps1's disposable foreground window on Windows.
|
|
Shape selections here are scripted replay, NOT automatic host synchronization.
|
|
"""
|
|
import importlib.util
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import signal
|
|
import subprocess as S
|
|
import time
|
|
|
|
ROOT = Path(__file__).resolve().parent
|
|
os.environ.update(DISPLAY=":0", XAUTHORITY="/home/armbian/.Xauthority")
|
|
spec = importlib.util.spec_from_file_location("cursor_poc", ROOT / "cursor-poc-x11.py")
|
|
m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m)
|
|
OUT = ROOT / "cursor-poc-evidence-v3"
|
|
OUT.mkdir(exist_ok=False) # Never overwrite earlier evidence.
|
|
|
|
def xd(*args):
|
|
return S.check_output(["xdotool", *map(str,args)], text=True).strip()
|
|
|
|
pid = int(xd("getwindowfocus", "getwindowpid"))
|
|
window = int(xd("getwindowfocus"))
|
|
cmd = Path(f"/proc/{pid}/cmdline").read_bytes().replace(b"\0", b" ")
|
|
if b"/moonlight " not in cmd or b" stream 192.168.1.70 Desktop " not in cmd or b" --absolute-mouse " not in cmd:
|
|
raise RuntimeError("Unexpected foreground process")
|
|
assets = m.validate_assets(m.read_json(ROOT / "windows-shapes.json", 4*1024*1024))
|
|
cursor = m.LocalCursor(window)
|
|
if any(cursor.snapshot()["argb"]):
|
|
cursor.close(); raise RuntimeError("Original local cursor is not hidden")
|
|
hidden_remote = False
|
|
paused = False
|
|
records = []
|
|
def interrupted(signum, frame):
|
|
raise SystemExit(128+signum)
|
|
signal.signal(signal.SIGTERM, interrupted)
|
|
signal.signal(signal.SIGINT, interrupted)
|
|
|
|
def focus():
|
|
if int(xd("getwindowfocus", "getwindowpid")) != pid:
|
|
raise RuntimeError("Moonlight lost foreground; stop the POC")
|
|
|
|
def capture(name):
|
|
with (OUT / (name+".xwd")).open("wb") as f:
|
|
S.run(["xwd", "-root", "-silent"], stdout=f, check=True)
|
|
|
|
try:
|
|
cursor.cursors = {k: cursor.create(v) for k,v in assets.items()}
|
|
cursor.cursors["__hidden"] = cursor.create(dict(width=1,height=1,hotX=0,hotY=0,argb=[0]))
|
|
focus(); xd("mousemove", 320, 500); time.sleep(.8)
|
|
capture("01-remote-cursor")
|
|
focus(); xd("key", "ctrl+alt+shift+n"); hidden_remote=True
|
|
xd("mousemove",321,500); time.sleep(1.0)
|
|
capture("02-video-cursor-disabled")
|
|
for name,x in (("arrow",320),("ibeam",960),("sizewe",1600),("wait",2240),("__hidden",320)):
|
|
focus(); y=1250 if name=="__hidden" else 500
|
|
xd("mousemove", x,y); time.sleep(.25)
|
|
cursor.apply(cursor.cursors[name]); time.sleep(.05)
|
|
snap=cursor.snapshot()
|
|
(OUT/(name+".json")).write_text(json.dumps(snap))
|
|
if name=="__hidden":
|
|
assert not any(snap["argb"]), "Hidden cursor still has visible pixels"
|
|
else:
|
|
a=assets[name]
|
|
assert (snap["width"],snap["height"],snap["hotX"],snap["hotY"]) == (a["width"],a["height"],a["hotX"],a["hotY"])
|
|
assert any(snap["argb"]), "Visible cursor has no pixels"
|
|
assert (snap["x"],snap["y"])==(x,y)
|
|
records.append(dict(shape=name,x=x,y=y,visible=any(snap["argb"])))
|
|
time.sleep(.3) # Allow the host probe to observe the real shape/visibility.
|
|
focus(); xd("mousemove",320,500); cursor.apply(cursor.cursors["arrow"]); time.sleep(.4)
|
|
# Freeze only this client for < 0.5s; X11 cursor must move while its video/input
|
|
# event processing is stopped. SIGCONT is guaranteed before cursor restoration.
|
|
os.kill(pid,signal.SIGSTOP); paused=True
|
|
try:
|
|
first=cursor.snapshot()
|
|
xd("mousemove",500,650); time.sleep(.12)
|
|
second=cursor.snapshot()
|
|
assert (second["x"],second["y"])==(500,650)
|
|
assert first["argb"]==second["argb"]
|
|
records.append(dict(test="client-paused-local-motion",before=[first["x"],first["y"]],after=[second["x"],second["y"]],passed=True))
|
|
finally:
|
|
os.kill(pid,signal.SIGCONT); paused=False
|
|
time.sleep(.5)
|
|
# Click only within the disposable Windows form. Scripted shape replay is
|
|
# deliberately distinct from the not-yet-implemented live cursor protocol.
|
|
for i in range(32):
|
|
focus(); n=i%4; x=320+640*n; y=600+(i//4)*20
|
|
cursor.apply(cursor.cursors[("arrow","ibeam","sizewe","wait")[n]])
|
|
xd("mousemove",x,y); time.sleep(.06); xd("click",1); time.sleep(.06)
|
|
capture("03-local-mode-video-only")
|
|
# Verify the actual helper's lease failure path while video cursor is hidden.
|
|
cursor.apply(cursor.original)
|
|
lease=OUT/"lease.json"
|
|
lease.write_text(json.dumps(dict(version=1,shape="arrow",visible=True,expires=time.time()+.8)))
|
|
result=S.run(["python3",str(ROOT/"cursor-poc-x11.py"),"--window",str(window),"--pid",str(pid),
|
|
"--assets",str(ROOT/"windows-shapes.json"),"--state",str(lease),"--seconds","3"],
|
|
capture_output=True,text=True,timeout=5)
|
|
(OUT/"lease-expiry.log").write_text(result.stdout+result.stderr)
|
|
assert result.returncode==0 and "lease-ended" in result.stdout and "sdl-hidden-cursor-restored" in result.stdout
|
|
# Xorg can briefly have no current cursor image after another connection's
|
|
# final cursor resource is released. Trigger pointer recomputation before
|
|
# checking the *post-exit* image; do not mistake this for an alpha image.
|
|
xd("mousemove",2240,741); time.sleep(.1)
|
|
assert not any(cursor.snapshot()["argb"])
|
|
records.append(dict(test="helper-lease-expiry",passed=True))
|
|
finally:
|
|
if paused: os.kill(pid,signal.SIGCONT)
|
|
try:
|
|
cursor.close(sdl_pid=pid)
|
|
finally:
|
|
# Remote restoration must still run when local cleanup raises an error.
|
|
if hidden_remote:
|
|
xd("windowactivate","--sync",window); focus()
|
|
xd("key","ctrl+alt+shift+n"); time.sleep(.8)
|
|
capture("04-remote-cursor-restored")
|
|
(OUT/"results.json").write_text(json.dumps(records,indent=2))
|
|
print(json.dumps(dict(restored_remote_cursor=hidden_remote,records=records)),flush=True)
|