mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-10 21:27:13 +01:00
Both previous escapes were unreachable, for the same underlying reason stated two different ways. A Tk key binding cannot fire, because once the workout app takes its seat grab X delivers every keystroke to that app alone. And "run touch from another terminal" cannot be done, because the app covers the screen with an override-redirect fullscreen window -- there is no other terminal to reach. An escape has to be handled INSIDE the grab, by the app itself, or it is not an escape. So --demo-escape arms a key handler in the runner: Ctrl+Shift+Q ungrabs the seat and closes the window. The grab, the override-redirect window and the handoff are all unchanged, so the demo exercises the real locking mechanism rather than a softened copy of it, and the hatch is the one deliberate way out. The handler ungrabs before closing. Destroying a window that still holds the seat can leave the pointer and keyboard grabbed with nothing left to release them, which needs exactly the reboot this hatch exists to prevent. Not Escape, which the Dart side already binds for in-app navigation; shadowing it would change the behaviour under test. Verified in both directions against the real debug bundle, each under its own Xvfb display: with the flag, the app exits on Ctrl+Shift+Q while holding the grab and the lock takes the screen back; without it, the identical keystroke does nothing and the app keeps the screen. A test asserts the production path never passes the flag. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DfJC1Wu1SEgJ6X49k5Jqtg
146 lines
5.0 KiB
Python
146 lines
5.0 KiB
Python
"""Try the workout-app handoff by hand, with one deliberate way out.
|
|
|
|
The lock itself is the real one: the demo builds the same lock window and the
|
|
workout app takes the same exclusive seat grab it takes in production, so what
|
|
is under test is the actual locking mechanism, not a softened copy.
|
|
|
|
Getting the escape right took two wrong tries, both worth recording because
|
|
they are the obvious ideas:
|
|
|
|
1. A Tk key binding cannot work. Once the workout app takes its seat grab
|
|
(``gdk_seat_grab`` with ``GDK_SEAT_CAPABILITY_ALL``) X delivers every
|
|
keystroke to that app and to nothing else, so no binding in this process
|
|
can fire -- no matter how healthy its event loop is.
|
|
2. "Escape from another terminal" cannot work either. The app covers the
|
|
screen with an override-redirect fullscreen window, so there is no other
|
|
terminal to reach and no way to raise one.
|
|
|
|
An escape therefore has to be handled INSIDE the app, by the app, or it cannot
|
|
be reached at all. ``--demo-escape`` (passed here, never by the production
|
|
supervisor) arms a key handler in the runner that drops the grab and quits:
|
|
|
|
Ctrl+Shift+Q
|
|
|
|
Verified in both directions: with the flag the real app exits on that
|
|
keystroke while holding the grab; without it the same keystroke does nothing.
|
|
|
|
VT switching is left enabled here as a second line of defence, so Ctrl+Alt+F2
|
|
also still works -- but the hatch above is the one this demo is testing.
|
|
|
|
One thing a green run does NOT prove: under the demo's local grab gatelock's
|
|
watchdog early-returns and never re-takes the grab, so the 1000ms steal-back
|
|
that makes ``_recovery.stop()`` load-bearing in production is not exercised.
|
|
|
|
python3 scripts/demo_workout_handoff.py
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from pathlib import Path
|
|
import sys
|
|
import tkinter as tk
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
|
|
from gatelock import LockConfig, LockWindow
|
|
|
|
from screen_locker._workout_app import ProcessHooks, workout_app_binary
|
|
from screen_locker._workout_handoff import lock_grab_handoff
|
|
from screen_locker._workout_session import WorkoutSession
|
|
|
|
|
|
class _DemoHooks:
|
|
"""A minimal lock body: a status line and the two buttons under test."""
|
|
|
|
def __init__(self) -> None:
|
|
self.status: tk.Label | None = None
|
|
self.lock: LockWindow | None = None
|
|
self.session: WorkoutSession | None = None
|
|
|
|
def build_surface(self, parent: tk.Misc, _surface: object) -> None:
|
|
tk.Label(
|
|
parent,
|
|
text="DEMO LOCK — local grab, VT switching still enabled",
|
|
font=("TkDefaultFont", 18, "bold"),
|
|
bg="#1B1D21",
|
|
fg="#E6E6E6",
|
|
).pack(pady=(120, 8))
|
|
self.status = tk.Label(
|
|
parent,
|
|
text=(
|
|
"Press “Start workout”. While the app holds the screen, "
|
|
"press Ctrl+Shift+Q to escape."
|
|
),
|
|
font=("TkDefaultFont", 12),
|
|
bg="#1B1D21",
|
|
fg="#9AA0A6",
|
|
)
|
|
self.status.pack(pady=8)
|
|
tk.Button(
|
|
parent,
|
|
text="Start workout",
|
|
font=("TkDefaultFont", 14, "bold"),
|
|
bg="#3B82F6",
|
|
fg="#FFFFFF",
|
|
padx=24,
|
|
pady=12,
|
|
command=self._start,
|
|
).pack(pady=16)
|
|
tk.Button(
|
|
parent,
|
|
text="✕ Close demo (or press Escape)",
|
|
bg="#EF4444",
|
|
fg="#FFFFFF",
|
|
command=self._close,
|
|
).pack(pady=4)
|
|
|
|
def _start(self) -> None:
|
|
if self.lock is None or self.status is None: # pragma: no cover
|
|
return
|
|
self.session = WorkoutSession(
|
|
lock_grab_handoff(self.lock),
|
|
after=self.lock.root.after,
|
|
on_status=self._set_status,
|
|
# The only escape that can be RECEIVED once the app grabs the seat.
|
|
hooks=ProcessHooks(demo_escape=True),
|
|
)
|
|
self.session.start()
|
|
|
|
def _set_status(self, text: str) -> None:
|
|
if self.status is not None: # pragma: no branch
|
|
self.status.config(text=text)
|
|
|
|
def _close(self) -> None:
|
|
if self.lock is not None: # pragma: no cover
|
|
self.lock.close()
|
|
|
|
def teardown_surface(self, _surface: object) -> None: ...
|
|
|
|
def on_focus_ready(self, _surface: object) -> None: ...
|
|
|
|
def on_close(self) -> None: ...
|
|
|
|
|
|
def main() -> int:
|
|
"""Run the demo lock. Returns non-zero when the app is not built."""
|
|
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
|
|
if workout_app_binary() is None:
|
|
return 1
|
|
root = tk.Tk()
|
|
root.title("Workout handoff demo")
|
|
# Local grab + VT switching enabled: every escape hatch stays open.
|
|
config = LockConfig(mode="hard", grab="local", disable_vt=False)
|
|
hooks = _DemoHooks()
|
|
lock = LockWindow(root, config, hooks)
|
|
hooks.lock = lock
|
|
lock.setup()
|
|
root.bind("<Escape>", lambda _event: lock.close())
|
|
lock.grab_input()
|
|
lock.run()
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|