* fix: audit hardening — input validation, error handling, and robustness - immich_api: guard person["id"] with .get() + early return on missing field - immich_api: include page number in pagination exception log - immich_api: validate faces response is a list before indexing - executor: wrap Image.open() in try/except for non-image HTTP responses - executor: strip leading 'v' from Frigate version before parsing (v0.16.0 was misread) - config: wrap FRIGATE_SCORE_CEILING float() parse in try/except with warning - config: warn when both DATA_DIR and legacy CWD config files exist simultaneously - scheduler: wrap PID file write in try/except so /tmp failures don't crash startup - scheduler: clamp sleep to 60s max to bound recovery time after NTP clock jumps - frigate_api: log unexpected non-list type in get_frigate_person_files at DEBUG * fix: LIMIT env var crash and symlink guard on person output dir - jobs: wrap int(LIMIT) parse in try/except — bad value (e.g. "30.5", "all") now logs a warning and falls back to the default instead of crashing - executor: check for symlink before shutil.rmtree on person_dir — prevents following a symlink out of OUTPUT_DIR on a shared volume * chore: bump version to 0.5.3
64 lines
2.0 KiB
Python
64 lines
2.0 KiB
Python
#!/usr/bin/env python3
|
|
import logging
|
|
import os
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
try:
|
|
from croniter import croniter
|
|
except ImportError:
|
|
print("croniter not installed. Run: uv add croniter")
|
|
sys.exit(1)
|
|
|
|
# Imported at module level so models loaded during the first run stay
|
|
# resident in memory across all subsequent scheduled runs.
|
|
from winnow.cli import main
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _check_models() -> None:
|
|
insightface_home = os.environ.get("INSIGHTFACE_HOME", "/models/.insightface")
|
|
buffalo = Path(insightface_home) / "models" / "buffalo_l"
|
|
if not buffalo.exists():
|
|
print(" InsightFace Buffalo_L not found — will download on first run", flush=True)
|
|
|
|
|
|
def _run_scheduler() -> None:
|
|
schedule = os.environ.get("CRON_SCHEDULE")
|
|
if not schedule:
|
|
print("Error: CRON_SCHEDULE environment variable is required.", flush=True)
|
|
sys.exit(1)
|
|
|
|
try:
|
|
Path("/tmp/winnow.pid").write_text(str(os.getpid()))
|
|
except OSError as e:
|
|
print(f"Warning: could not write PID file: {e}", flush=True)
|
|
|
|
now = time.time()
|
|
cron = croniter(schedule, now)
|
|
next_run = cron.get_next(float)
|
|
print(f"Next run: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(next_run))}", flush=True)
|
|
|
|
while True:
|
|
now = time.time()
|
|
if now >= next_run:
|
|
print(f"\n[{time.strftime('%Y-%m-%d %H:%M:%S')}] Starting winnow run...", flush=True)
|
|
_check_models()
|
|
try:
|
|
main()
|
|
print("winnow run complete", flush=True)
|
|
except KeyboardInterrupt:
|
|
raise
|
|
except Exception as e:
|
|
logger.error("winnow run failed: %s", e, exc_info=True)
|
|
print(f"winnow run failed: {e}", flush=True)
|
|
next_run = cron.get_next(float)
|
|
print(f"Next run: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(next_run))}", flush=True)
|
|
time.sleep(min(60, max(1, next_run - time.time())))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
_run_scheduler()
|