Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 49 additions & 2 deletions hermes_cli/cron.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,21 @@
Cron subcommand for hermes CLI.

Handles standalone cron management commands like list, create, edit,
pause/resume/run/remove, status, and tick.
pause/resume/run/remove, status, tick, and daemon.
"""

import json
import logging
import re
import signal
import sys
import threading
import time
from pathlib import Path
from typing import Iterable, List, Optional

logger = logging.getLogger(__name__)

PROJECT_ROOT = Path(__file__).parent.parent.resolve()
sys.path.insert(0, str(PROJECT_ROOT))

Expand Down Expand Up @@ -456,6 +463,43 @@ def _job_action(action: str, job_id: str, success_verb: str) -> int:
return 0


def cron_daemon(args):
"""Run the cron scheduler as a standalone daemon (independent of gateway)."""
interval = getattr(args, 'interval', 60)
stop_event = threading.Event()

def _signal_handler(signum, frame):
logger.info("Shutdown signal received, stopping cron daemon...")
stop_event.set()

# Only set up signal handlers in the main thread
try:
signal.signal(signal.SIGINT, _signal_handler)
signal.signal(signal.SIGTERM, _signal_handler)
except ValueError:
# Not in main thread (e.g., when imported and called programmatically)
# The caller is responsible for handling shutdown
pass

logger.info(f"Cron daemon started (interval={interval}s). Press Ctrl+C to stop.")
print(f"Cron daemon started (interval={interval}s). Press Ctrl+C to stop.")

tick_count = 0
while not stop_event.is_set():
try:
from cron.scheduler import tick
tick(verbose=True, sync=True)
except Exception as e:
logger.error(f"Cron tick error: {e}")

tick_count += 1
stop_event.wait(timeout=interval)

logger.info("Cron daemon stopped")
print("Cron daemon stopped")
return 0


def cron_command(args):
"""Handle cron subcommands."""
subcmd = getattr(args, 'cron_command', None)
Expand All @@ -477,6 +521,9 @@ def cron_command(args):
cron_runs(getattr(args, "job_id", None), getattr(args, "limit", 20))
return 0

if subcmd == "daemon":
return cron_daemon(args)

if subcmd in {"create", "add"}:
return cron_create(args)

Expand All @@ -496,5 +543,5 @@ def cron_command(args):
return _job_action("remove", args.job_id, "Removed")

print(f"Unknown cron command: {subcmd}")
print("Usage: hermes cron [list|create|edit|pause|resume|run|remove|status|runs|tick]")
print("Usage: hermes cron [list|create|edit|pause|resume|run|remove|status|runs|tick|daemon]")
sys.exit(1)
10 changes: 10 additions & 0 deletions hermes_cli/subcommands/cron.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,5 +165,15 @@ def build_cron_parser(subparsers, *, cmd_cron: Callable) -> None:
# cron tick (mostly for debugging)
cron_tick = cron_subparsers.add_parser("tick", help="Run due jobs once and exit")
add_accept_hooks_flag(cron_tick)

# cron daemon (standalone cron scheduler)
cron_daemon_parser = cron_subparsers.add_parser(
"daemon", help="Run cron scheduler as standalone daemon (independent of gateway)"
)
cron_daemon_parser.add_argument(
"--interval", type=int, default=60, help="Tick interval in seconds (default: 60)"
)
add_accept_hooks_flag(cron_daemon_parser)

add_accept_hooks_flag(cron_parser)
cron_parser.set_defaults(func=cmd_cron)
Loading