mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-11 15:54:11 +01:00
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
file length / file-length (push) Waiting to run
gatelock pre-commit / pre-commit (push) Waiting to run
gatelock tests / test (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
palette drift / palette (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
shared gate tests / gate unit tests (push) Waiting to run
A keyring prompt (gcr-prompter) that grabbed input before the leetcode lock armed left the lock visible but unclickable: every click went to the prompt hidden under it. Each failed grab_set_global also froze Tk ~1 s, so the lock barely repainted. - Raise known credential prompts above the lock after 2 s of a blocked grab; games and other grabbers stay covered. - Probe the grab on a side Xlib connection first so a blocked grab never stalls the Tk loop (worst gap 1.05 s -> 0.05 s). - Log the Tk grab error and stop blaming our own claim. - 1.5 s startup grace in wait_for_turn, and a weaker armed lock steps back (windowless, re-arms later) when a stronger one is live, so only one lock is ever on screen. - Real-X checks: verify_prompt_yield, verify_grab_responsive, verify_all_lockers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WzxdWZbpxQZj41WpbQPuxB
114 lines
3.7 KiB
Python
Executable File
114 lines
3.7 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# Copyright (c) 2026 Krzysztof Rudnicki
|
|
# pylint: disable=consider-using-with,import-error
|
|
r"""Prove the lock's Tk loop stays responsive while a foreign client holds the grab.
|
|
|
|
Tk's ``grab -global`` blocks the whole event loop for ~1.0 s on every FAILED
|
|
attempt, and the lock retries every 200 ms plus once a tick, so under a held
|
|
grab (2026-11-11: the keyring prompt) the loop was starved: no repaint, no
|
|
clicks, rows and buttons without text.
|
|
|
|
``prompt_grab_helper.py`` holds the grab (class ``steam_game``: not a prompt,
|
|
so no yielding muddies the measurement). A real ``mainloop`` runs for 5 s with
|
|
a 50 ms ``after`` heartbeat; the worst gap between beats must stay under
|
|
300 ms. After the helper lets go the lock must still take the grab.
|
|
|
|
PYTHONPATH=. xvfb-run -a -s "-screen 0 1600x1200x24" \
|
|
python3 scripts/verify_grab_responsive.py
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from pathlib import Path
|
|
import select
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from unittest.mock import MagicMock
|
|
|
|
from gatelock import GateRoot, LockConfig, LockWindow
|
|
from gatelock._config import GrabPolicy
|
|
|
|
HELPER = Path(__file__).with_name("prompt_grab_helper.py")
|
|
BEAT_MS, RUN_S, MAX_GAP_S, WAIT_S = 50, 5.0, 0.3, 8.0
|
|
|
|
|
|
def _say(message: str) -> None:
|
|
sys.stdout.write(f"{message}\n")
|
|
|
|
|
|
def _line(proc: subprocess.Popen[str]) -> str:
|
|
out = proc.stdout
|
|
ready = select.select([out], [], [], WAIT_S)[0] if out else []
|
|
return out.readline().strip() if ready and out else ""
|
|
|
|
|
|
def _worst_gap(root: GateRoot) -> float:
|
|
"""Run the real mainloop for RUN_S; return the largest heartbeat gap."""
|
|
last = [time.monotonic()]
|
|
worst = [0.0]
|
|
|
|
def beat() -> None:
|
|
now = time.monotonic()
|
|
worst[0] = max(worst[0], now - last[0])
|
|
last[0] = now
|
|
root.after(BEAT_MS, beat)
|
|
|
|
root.after(BEAT_MS, beat)
|
|
root.after(int(RUN_S * 1000), root.quit)
|
|
root.mainloop()
|
|
return worst[0]
|
|
|
|
|
|
def main() -> int:
|
|
"""Hold a foreign grab, measure the loop, then release; 0 when all held."""
|
|
if os.environ.get("DISPLAY", "").split(".")[0] in {"", ":0"}:
|
|
_say("refusing to run: DISPLAY must be a private X server, never :0")
|
|
return 2
|
|
helper = subprocess.Popen(
|
|
[sys.executable, str(HELPER), "steam_game"],
|
|
stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE,
|
|
text=True,
|
|
)
|
|
failed = 0
|
|
if _line(helper) != "READY":
|
|
_say("FAIL helper did not take the grab")
|
|
return 1
|
|
root = GateRoot()
|
|
config = LockConfig(mode="hard", grab=GrabPolicy(disable_vt=False, retry_ms=200))
|
|
lock = LockWindow(root, config, MagicMock())
|
|
try:
|
|
lock.setup()
|
|
lock.grab_input()
|
|
gap = _worst_gap(root)
|
|
ok = gap < MAX_GAP_S
|
|
_say(f"{'PASS' if ok else 'FAIL'} worst loop gap {gap:.3f}s (< {MAX_GAP_S}s)")
|
|
failed += not ok
|
|
grab_held = lock.recovery.holds_grab()
|
|
_say(f"{'FAIL' if grab_held else 'PASS'} helper still holds the grab")
|
|
failed += grab_held
|
|
if helper.stdin:
|
|
helper.stdin.write("release\n")
|
|
helper.stdin.flush()
|
|
_line(helper)
|
|
deadline = time.monotonic() + WAIT_S
|
|
while time.monotonic() < deadline and not lock.recovery.holds_grab():
|
|
root.update()
|
|
time.sleep(0.05)
|
|
got = lock.recovery.holds_grab()
|
|
_say(f"{'PASS' if got else 'FAIL'} lock took the grab once it was free")
|
|
failed += not got
|
|
finally:
|
|
lock.close()
|
|
if helper.stdin:
|
|
helper.stdin.close()
|
|
helper.wait(timeout=WAIT_S)
|
|
_say("OK" if not failed else f"{failed} FAILED")
|
|
return 1 if failed else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|