testsAndMisc/python_pkg/morning_routine/_orchestrator.py
Krzysztof kuhy Rudnicki 4eac1a45fe Remove wake_alarm (extracted to kuhyx/wake-alarm) and fix the orchestrator
Full history preserved via git filter-repo; production
wake-alarm.service already cut over to the pip-installed standalone
package and verified clean.

Also fixes morning_routine._orchestrator, which has been silently
broken since screen-locker's extraction on 2026-05-28: it still
referenced python_pkg.screen_locker.screen_lock, causing
ModuleNotFoundError on every morning the workout-lock handoff ran.
Both module references now point at the pip-installed standalone
packages (wake_alarm._alarm, screen_locker.screen_lock); fixing this
required pip-installing screen_locker into system Python for the
first time too (see the separate screen-locker packaging-fix
commits), which it had never been.
2026-06-22 12:47:06 +02:00

102 lines
3.6 KiB
Python

"""Orchestrate the morning wake/workout flow as one sequential routine.
The wake alarm (``wake_alarm``, https://github.com/kuhyx/wake-alarm) and the
workout screen lock (``screen_locker``, https://github.com/kuhyx/screen-locker)
used to run as two independent ``graphical-session.target`` user services,
each opening its own fullscreen ``-topmost`` Tk window. On a wake morning they
could grab the screen at the same time, so the alarm could end up hidden
behind the workout lock (or vice versa).
This orchestrator makes them one coherent flow by running them as **sequential
subprocesses**: the alarm runs first and owns the fullscreen until it is
dismissed, then the workout lock runs. Only one fullscreen window is ever alive
at a time, so they can never collide. Each subprocess still self-gates (the
alarm only fires on alarm days when undismissed; the lock exits if a skip was
earned or the workout is already logged), so this is safe to run on every wake.
Both ``wake_alarm`` and ``screen_locker`` are pip-installed into system
Python's user site-packages (each repo's own install.sh does this), so
``python -m <module>`` resolves them with no extra ``PYTHONPATH``/``cwd``
plumbing here.
Usage:
python -m python_pkg.morning_routine._orchestrator --with-alarm # resume
python -m python_pkg.morning_routine._orchestrator # lock only
"""
from __future__ import annotations
import argparse
import logging
import subprocess
import sys
from python_pkg.shared.logging_setup import configure_logging
_logger = logging.getLogger(__name__)
# Modules invoked as ``python -m <module> --production``.
ALARM_MODULE: str = "wake_alarm._alarm"
WORKOUT_LOCK_MODULE: str = "screen_locker.screen_lock"
def _run_module(module: str) -> int:
"""Run *module* as a blocking ``python -m`` subprocess in production mode.
Args:
module: Dotted module path to execute with ``python -m``.
Returns:
The subprocess exit code, or ``1`` when the process could not start.
"""
cmd = [sys.executable, "-m", module, "--production"]
_logger.info("morning-routine: running %s", module)
try:
result = subprocess.run(cmd, check=False)
except OSError:
_logger.warning("Failed to run %s", module, exc_info=True)
return 1
return result.returncode
def _run_alarm() -> int:
"""Run the wake alarm and block until it is dismissed (or self-exits)."""
return _run_module(ALARM_MODULE)
def _run_workout_lock() -> int:
"""Run the workout screen lock after the alarm has been dealt with."""
return _run_module(WORKOUT_LOCK_MODULE)
def _parse_args(argv: list[str]) -> argparse.Namespace:
"""Parse CLI arguments for the orchestrator."""
parser = argparse.ArgumentParser(description="Unified morning routine.")
parser.add_argument(
"--with-alarm",
action="store_true",
help="Run the wake alarm before the workout lock (used on resume).",
)
parser.add_argument(
"--production",
action="store_true",
help="Production mode (kept for systemd/CLI symmetry).",
)
return parser.parse_args(argv)
def main() -> None:
"""Entry point: optionally run the alarm, then always run the workout lock."""
configure_logging()
args = _parse_args(sys.argv[1:])
# Alarm first so it owns the fullscreen and escalates until dismissed; only
# then hand off to the workout lock. Running them in this order in a single
# process guarantees they never fight for the screen.
if args.with_alarm:
_run_alarm()
_run_workout_lock()
if __name__ == "__main__":
main()