testsAndMisc/python_pkg/morning_routine/_orchestrator.py

96 lines
3.3 KiB
Python
Raw Normal View History

"""Orchestrate the morning wake/workout flow as one sequential routine.
The wake alarm (``python_pkg.wake_alarm``) and the workout screen lock
(``python_pkg.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.
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 = "python_pkg.wake_alarm._alarm"
WORKOUT_LOCK_MODULE: str = "python_pkg.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()