# services/schedulerservice.py (Versione Headless) import os import json import locale import subprocess from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.triggers.cron import CronTrigger from apscheduler.triggers.interval import IntervalTrigger class SchedulerService: """ Servizio di backend per la schedulazione e l'esecuzione di task. È completamente "headless" e non ha un'interfaccia grafica. La sua configurazione è gestita tramite un file JSON esterno. """ def __init__(self, core_api, config): self.core = core_api self.config_file = config.get('config_file') self.scheduler = BackgroundScheduler(timezone="Europe/Rome") self.python_executable = self.core.get_python_executable() def start(self): """Avvia il servizio e carica i job schedulati.""" self.log("Avvio servizio...") if not self.config_file: self.log("Nessun 'config_file' specificato per lo scheduler. Il servizio non eseguirà task.", 'warning') return self._load_jobs_from_file() self.scheduler.start() self.log("Servizio avviato e operativo.") def stop(self): """Ferma lo scheduler in modo pulito.""" self.log("Arresto servizio...") if self.scheduler.running: self.scheduler.shutdown(wait=False) self.log("Servizio fermato.") def log(self, message, level='info'): """Metodo di logging standard per il servizio.""" # Aggiunge un prefisso per identificare l'origine del log self.core.log(f"[SCHEDULER] {message}", level) def _load_jobs_from_file(self): """Carica e imposta i job dal file di configurazione JSON.""" self.scheduler.remove_all_jobs() jobs_data = self.get_jobs_config() for job_info in jobs_data: trigger = self._create_trigger(job_info) if trigger: self.scheduler.add_job( self._run_task, args=[job_info['script_path']], trigger=trigger, id=job_info['id'] ) self.log(f"Job '{job_info['id']}' caricato.") def _create_trigger(self, job_info): """Crea un oggetto trigger di APScheduler basato sulla configurazione.""" trigger_type = job_info.get('trigger') if trigger_type == 'cron': return CronTrigger(hour=job_info.get('hour'), minute=job_info.get('minute'), day_of_week=job_info.get('day_of_week'), timezone="Europe/Rome") elif trigger_type == 'interval': return IntervalTrigger(seconds=job_info.get('seconds'), timezone="Europe/Rome") self.log(f"Trigger non valido '{trigger_type}' per il job '{job_info.get('id')}'.", 'warning') return None def _run_task(self, script_path): """Esegue un singolo script/batch in un processo separato.""" self.log(f"Avvio task: {os.path.basename(script_path)}") command = [] if script_path.lower().endswith('.py'): command = [self.python_executable, script_path] elif script_path.lower().endswith(('.bat', '.sh')): command = [script_path] else: self.log(f"ERRORE: Tipo di file non supportato: {os.path.basename(script_path)}", 'error') return try: system_encoding = locale.getpreferredencoding(False) result = subprocess.run(command, check=True, capture_output=True, text=True, encoding=system_encoding, errors='replace', shell=True) self.log(f"Task '{os.path.basename(script_path)}' completato.") if result.stdout: self.log(f" > Output:\n{result.stdout.strip()}") except subprocess.CalledProcessError as e: self.log(f"ERRORE task '{os.path.basename(script_path)}'.", 'error') if e.stderr: self.log(f" > Errore:\n{e.stderr.strip()}", 'error') except Exception as e: self.log(f"ERRORE imprevisto task '{os.path.basename(script_path)}': {e}", 'error') def get_jobs_config(self): """Legge e restituisce la configurazione dei job dal file JSON.""" try: # Espande il percorso per renderlo assoluto se necessario config_path = self.config_file if not os.path.isabs(config_path): config_path = os.path.join(self.core.base_dir, config_path) with open(config_path, 'r', encoding='utf-8') as f: return json.load(f) except (FileNotFoundError, json.JSONDecodeError): return [] except Exception as e: self.log(f"Impossibile leggere il file di configurazione dei job: {e}", 'error') return [] # Nota: il metodo save_jobs_config non è più necessario in modalità headless, # perché la configurazione viene gestita manualmente modificando il file JSON.