utils/gatelock/scripts/locker_child.py
Krzysztof kuhy Rudnicki 2d341dd41e
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
gatelock 0.13.0: locks stay clickable together
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
2026-11-11 12:49:40 +01:00

102 lines
3.1 KiB
Python
Executable File

#!/usr/bin/env python3
# Copyright (c) 2026 Krzysztof Rudnicki
# pylint: disable=missing-function-docstring
"""One stand-in locker for ``verify_all_lockers.py``: the real startup sequence.
Mirrors what leetcode_guard / screen_locker / diet_guard / book_guard /
focus_guard do: ``Arbiter`` -> ``publish`` -> ``wait_for_turn`` (windowless) ->
``acquire_holder`` -> ``LockWindow`` -> ``setup`` -> ``grab_input`` -> mainloop,
with the real app name and rank. Its only UI is one button; clicking it ends
the lock, like a solve does. Everything is reported on stdout, one line each:
``ARMED``, ``BTN x y`` (button centre, root coordinates), ``GRAB 0|1``,
``CLICKED``.
Usage: ``locker_child.py <app> <rank> <queue_poll_s>``
"""
from __future__ import annotations
import sys
import tkinter as tk
from gatelock import (
Arbiter,
GateRoot,
LockConfig,
LockWindow,
wait_for_turn,
)
REPORT_MS = 250
class _Child:
"""The hooks object, as each real locker is."""
def __init__(self, app: str, rank: int, poll: float) -> None:
self.app = app
self.button: tk.Button | None = None
config = LockConfig(mode="hard", app_name=app, rank=rank)
arbiter = Arbiter(
app,
rank,
grab=config.resolved_grab(),
disable_vt=False,
)
arbiter.publish()
wait_for_turn(arbiter, poll=poll)
arbiter.acquire_holder()
self.root = GateRoot(className=app)
self.lock = LockWindow(self.root, config, self, arbiter=arbiter)
self.lock.setup()
self.say("ARMED")
self.lock.grab_input()
self.root.after(REPORT_MS, self.report)
def say(self, text: str) -> None:
sys.stdout.write(f"{text}\n")
sys.stdout.flush()
def report(self) -> None:
self.say(f"GRAB {int(self.lock.recovery.holds_grab())}")
button = self.button
if button is not None and button.winfo_ismapped():
x = button.winfo_rootx() + button.winfo_width() // 2
y = button.winfo_rooty() + button.winfo_height() // 2
self.say(f"BTN {x} {y}")
self.root.after(REPORT_MS, self.report)
def clicked(self) -> None:
self.say("CLICKED")
self.lock.close()
# -- LockWindowHooks ---------------------------------------------------
def build_surface(self, parent: tk.Misc, surface: object) -> None:
del surface
self.button = tk.Button(parent, text=self.app, command=self.clicked)
self.button.place(relx=0.5, rely=0.5, width=240, height=90, anchor="center")
def teardown_surface(self, surface: object) -> None:
del surface
self.button = None
def on_focus_ready(self, surface: object) -> None:
del surface
def on_callback_error(self) -> None:
self.say("ERROR callback")
def on_close(self) -> None:
return
def main(argv: list[str]) -> int:
"""Arm one locker and run its mainloop."""
child = _Child(argv[0], int(argv[1]), float(argv[2]))
child.lock.run()
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))