mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-10 20:27:13 +01:00
The demo promised three escapes and delivered none: the only way out was a hard reboot. Two independent causes, and only one of them was demo-only. launch_workout_app blocks until the app exits. Called straight from a Tk button that froze the event loop, so the Escape binding and the close button could not fire -- measured: zero handlers ran during the block, and the events were never delivered, not merely delivered late. WorkoutSession drives the same phases through `after` instead, on the main thread, so grab_release and grab_input stay where Tk requires them and the loop keeps pumping (199 ticks during a run that previously managed none). The second cause survives a healthy event loop. The Flutter app takes its own gdk_seat_grab with GDK_SEAT_CAPABILITY_ALL and never releases it, so while it is up X delivers the keyboard to it and no binding of ours can fire. Correct for production, fatal for a demo, and no flag turns it off -- so the demo's escape is a file the app cannot intercept: touch ~/.cache/stop-workout-demo. The previous commit's rationale for the blocking was wrong and is corrected here rather than left to mislead. It claimed pumping Tk would let the recovery watchdog steal the screen mid-workout; in fact release() stops the watchdog first and stop() cancels its timers outright, so there were never any ticks to race with. The blocking was incidental, which is why removing it costs the production handoff nothing. Verified with the real debug bundle under Xvfb: with the app up and holding its seat grab, the event loop is alive, the escape file terminates it, and the lock takes the grab back. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DfJC1Wu1SEgJ6X49k5Jqtg
84 lines
3.4 KiB
Python
84 lines
3.4 KiB
Python
"""Handing the lock screen to the workout app, and taking it back.
|
|
|
|
The Tk locker cannot simply ``close()`` to let the app take over: ``close()``
|
|
also restores VT switching, which would hand the user Ctrl+Alt+F2 and an exit
|
|
from the very obligation the lock exists to enforce. Only the *grab* moves.
|
|
|
|
The recovery watchdog has to be stopped for the duration, and that is the part
|
|
that is easy to get wrong. It is built never to release -- it re-takes a grab
|
|
it finds missing, deliberately, so that pulling a cable is not an escape. Left
|
|
running it would treat the Flutter app exactly like an intruder and steal the
|
|
screen back within one verify tick, mid-workout. So the order is: stop the
|
|
watchdog, release the grab, run the app, re-take the grab, restart the
|
|
watchdog. The Tk window itself stays mapped underneath the whole time, which
|
|
is what keeps the screen covered if the app dies.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import logging
|
|
import tkinter as tk
|
|
from typing import TYPE_CHECKING
|
|
|
|
from screen_locker._workout_app import GrabHandoff, launch_workout_app
|
|
|
|
if TYPE_CHECKING:
|
|
from gatelock import LockWindow
|
|
|
|
__all__ = ["lock_grab_handoff", "start_workout"]
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
def lock_grab_handoff(lock: LockWindow) -> GrabHandoff:
|
|
"""Build the release/reacquire pair for a live lock window.
|
|
|
|
Neither direction touches VT switching or the arbiter claim: this is a
|
|
loan of the grab, not a teardown.
|
|
"""
|
|
|
|
def release() -> None:
|
|
# Stop first: a watchdog tick between the release and the app's own
|
|
# grab would take the screen straight back off it.
|
|
lock._recovery.stop()
|
|
with contextlib.suppress(tk.TclError):
|
|
lock.root.grab_release()
|
|
_logger.warning(
|
|
"Released the X grab to the workout app; the lock window stays "
|
|
"mapped and VT switching stays disabled."
|
|
)
|
|
|
|
def reacquire() -> None:
|
|
# Unconditional and idempotent: whatever happened to the app, this
|
|
# machine must not be left with nothing holding the screen.
|
|
lock.grab_input()
|
|
_logger.warning("Re-took the X grab from the workout app.")
|
|
|
|
return GrabHandoff(release=release, reacquire=reacquire)
|
|
|
|
|
|
def start_workout(lock: LockWindow) -> str:
|
|
"""Run the workout app as the lock surface; return a status for the UI.
|
|
|
|
Never raises: this is wired to a button on a screen the user cannot leave,
|
|
so a failure has to come back as a message rather than a traceback that
|
|
would leave the grab in an unknown state.
|
|
|
|
Blocks until the app exits, so it is only safe for a caller with no event
|
|
loop to starve. Behind a Tk button use :class:`WorkoutSession` instead:
|
|
blocking there freezes the loop, which makes every binding on the lock --
|
|
including the escapes -- inert until the app exits.
|
|
|
|
An earlier version of this docstring claimed the blocking was deliberate,
|
|
to stop the recovery watchdog ticking mid-workout. That was wrong:
|
|
``release`` stops the watchdog first, and ``stop()`` cancels its timers
|
|
outright, so there are no ticks to race with either way.
|
|
"""
|
|
result = launch_workout_app(lock_grab_handoff(lock))
|
|
if not result.launched:
|
|
_logger.error("Could not start the PC workout: %s", result.reason)
|
|
return f"Could not start the workout: {result.reason}"
|
|
_logger.warning("The workout app finished: %s", result.reason)
|
|
return "Workout app closed — checking whether it counted…"
|