from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.triggers.cron import CronTrigger _flask_app = None scheduler = BackgroundScheduler( job_defaults={"coalesce": True, "max_instances": 1}, timezone="UTC", ) def init_scheduler(flask_app): global _flask_app _flask_app = flask_app if not scheduler.running: scheduler.start() # Nettoyage des runs bloqués à "running" (app redémarrée pendant un backup) scheduler.add_job( func=_cleanup_stuck_runs, trigger="interval", hours=1, id="cleanup_stuck_runs", replace_existing=True, ) def _cleanup_stuck_runs(): from datetime import timedelta with _flask_app.app_context(): from db import db, Run cutoff = __import__("datetime").datetime.utcnow() - timedelta(hours=6) stuck = Run.query.filter( Run.status == "running", Run.started_at < cutoff, ).all() for run in stuck: run.status = "error" run.log_text = (run.log_text or "") + "\n[timeout] Run marqué en erreur par le nettoyage automatique." run.finished_at = __import__("datetime").datetime.utcnow() if stuck: db.session.commit() def _execute_job(job_id): with _flask_app.app_context(): from jobs.ynh_backup import execute_job execute_job(job_id) def schedule_job(job): import logging job_key = f"job_{job.id}" try: trigger = CronTrigger.from_crontab(job.cron_expr) except Exception: logging.warning(f"Job #{job.id} « {job.name} » : expression cron invalide « {job.cron_expr} » — job non planifié.") return if scheduler.get_job(job_key): scheduler.reschedule_job(job_key, trigger=trigger) else: scheduler.add_job( func=_execute_job, trigger=trigger, id=job_key, kwargs={"job_id": job.id}, replace_existing=True, ) def remove_job(job_id): job_key = f"job_{job_id}" if scheduler.get_job(job_key): scheduler.remove_job(job_key) def get_next_run(job_id): job_key = f"job_{job_id}" apsjob = scheduler.get_job(job_key) if apsjob and apsjob.next_run_time: return apsjob.next_run_time return None