import strawberry from typing import List, Optional import os import sys import subprocess import signal from pathlib import Path from api.crontab_manager import CrontabManager from common import PID_FILE def get_manager(): tabfile = os.getenv("TABFILE") return CrontabManager(tabfile=tabfile) def is_wecker_ringing() -> bool: """Check if the wecker process is currently running via the PID file.""" if not os.path.exists(PID_FILE): return False try: pid: int with open(PID_FILE) as f: pid = int(f.read().strip()) os.kill(pid, 0) return True except (ValueError, ProcessLookupError, PermissionError, OSError): return False def _project_root() -> Path: return Path(__file__).parent.parent.absolute() def _default_command() -> str: """Return the default shell command to run wecker.py from crontab.""" project_root = _project_root() python_exec = sys.executable return f"cd {project_root} && {python_exec} wecker.py >> wecker.log 2>&1" def _start_wecker_process() -> subprocess.Popen: """Start wecker.py without invoking a shell.""" project_root = _project_root() return subprocess.Popen( [sys.executable, str(project_root / "wecker.py")], cwd=project_root, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True, ) @strawberry.type class Alarm: id: str cron_expression: str command: str is_enabled: bool @strawberry.type class Query: @strawberry.field def is_ringing(self) -> bool: return is_wecker_ringing() @strawberry.field def get_alarms(self) -> List[Alarm]: manager = get_manager() return [Alarm(**a) for a in manager.get_alarms()] @strawberry.field def get_alarm(self, id: str) -> Optional[Alarm]: manager = get_manager() alarms = manager.get_alarms() for a in alarms: if a["id"] == id: return Alarm(**a) return None @strawberry.type class Mutation: @strawberry.field def set_alarm( self, cron_expression: str, is_enabled: bool = True, id: Optional[str] = None, ) -> Alarm: command = _default_command() manager = get_manager() new_id = manager.set_alarm( alarm_id=id, cron_expression=cron_expression, command=command, is_enabled=is_enabled, ) return Alarm( id=new_id, cron_expression=cron_expression, command=command, is_enabled=is_enabled, ) @strawberry.field def delete_alarm(self, id: str) -> bool: manager = get_manager() # Verify it exists alarms = manager.get_alarms() exists = any(a["id"] == id for a in alarms) if exists: manager.delete_alarm(id) return True return False @strawberry.field def start_ringing(self) -> bool: """Start the wecker alarm if it is not already ringing. Returns True if started, False if already ringing.""" if is_wecker_ringing(): return False _start_wecker_process() return True @strawberry.field def stop_ringing(self) -> bool: """Stop the wecker alarm if it is currently ringing. Returns True if stopped, False if not ringing.""" if not is_wecker_ringing(): return False try: with open(PID_FILE) as f: pid = int(f.read().strip()) os.kill(pid, signal.SIGTERM) if os.path.exists(PID_FILE): os.remove(PID_FILE) return True except (ValueError, ProcessLookupError, PermissionError, OSError): # PID file is stale or process already dead if os.path.exists(PID_FILE): os.remove(PID_FILE) return False schema = strawberry.Schema(query=Query, mutation=Mutation)