Files
Bjorn/loki/jobs.py
infinition aac77a3e76 Add Loki and Sentinel utility classes for web API endpoints
- Implemented LokiUtils class with GET and POST endpoints for managing scripts, jobs, and payloads.
- Added SentinelUtils class with GET and POST endpoints for managing events, rules, devices, and notifications.
- Both classes include error handling and JSON response formatting.
2026-03-14 22:33:10 +01:00

163 lines
5.4 KiB
Python

"""
Loki job manager — tracks HIDScript execution jobs.
Each job runs in its own daemon thread.
"""
import uuid
import time
import logging
import traceback
from datetime import datetime
from threading import Thread, Event
from logger import Logger
logger = Logger(name="loki.jobs", level=logging.DEBUG)
class LokiJobManager:
"""Manages HIDScript job lifecycle."""
def __init__(self, engine):
self.engine = engine
self._jobs = {} # job_id → job dict
self._threads = {} # job_id → Thread
self._stops = {} # job_id → Event
def create_job(self, script_name: str, script_content: str) -> str:
"""Create and start a new job. Returns job_id (UUID)."""
job_id = str(uuid.uuid4())[:8]
now = datetime.now().isoformat()
job = {
"id": job_id,
"script_name": script_name,
"status": "pending",
"output": "",
"error": "",
"started_at": None,
"finished_at": None,
"created_at": now,
}
self._jobs[job_id] = job
stop = Event()
self._stops[job_id] = stop
# Persist to DB
try:
db = self.engine.shared_data.db
db.execute(
"INSERT INTO loki_jobs (id, script_name, status, created_at) VALUES (?, ?, ?, ?)",
(job_id, script_name, "pending", now)
)
except Exception as e:
logger.error("DB insert job error: %s", e)
# Start execution thread
t = Thread(
target=self._run_job,
args=(job_id, script_content, stop),
daemon=True,
name=f"loki-job-{job_id}",
)
self._threads[job_id] = t
t.start()
logger.info("Job %s created: %s", job_id, script_name)
return job_id
def cancel_job(self, job_id: str) -> bool:
"""Cancel a running job."""
stop = self._stops.get(job_id)
if stop:
stop.set()
job = self._jobs.get(job_id)
if job and job["status"] == "running":
job["status"] = "cancelled"
job["finished_at"] = datetime.now().isoformat()
self._update_db(job_id, "cancelled", job.get("output", ""), "Cancelled by user")
logger.info("Job %s cancelled", job_id)
return True
return False
def get_all_jobs(self) -> list:
"""Return list of all jobs (most recent first)."""
jobs = list(self._jobs.values())
jobs.sort(key=lambda j: j.get("created_at", ""), reverse=True)
return jobs
def get_job(self, job_id: str) -> dict:
"""Get a single job by ID."""
return self._jobs.get(job_id)
def clear_completed(self):
"""Remove finished/failed/cancelled jobs from memory."""
to_remove = [
jid for jid, j in self._jobs.items()
if j["status"] in ("succeeded", "failed", "cancelled")
]
for jid in to_remove:
self._jobs.pop(jid, None)
self._threads.pop(jid, None)
self._stops.pop(jid, None)
try:
self.engine.shared_data.db.execute(
"DELETE FROM loki_jobs WHERE status IN ('succeeded', 'failed', 'cancelled')"
)
except Exception as e:
logger.error("DB clear jobs error: %s", e)
@property
def running_count(self) -> int:
return sum(1 for j in self._jobs.values() if j["status"] == "running")
# ── Internal ───────────────────────────────────────────────
def _run_job(self, job_id: str, script_content: str, stop: Event):
"""Execute a HIDScript in this thread."""
job = self._jobs[job_id]
job["status"] = "running"
job["started_at"] = datetime.now().isoformat()
self._update_db(job_id, "running")
try:
from loki.hidscript import HIDScriptParser
parser = HIDScriptParser(self.engine.hid_controller)
output_lines = parser.execute(script_content, stop_event=stop, job_id=job_id)
if stop.is_set():
job["status"] = "cancelled"
else:
job["status"] = "succeeded"
job["output"] = "\n".join(output_lines)
except Exception as e:
job["status"] = "failed"
job["error"] = str(e)
job["output"] = traceback.format_exc()
logger.error("Job %s failed: %s", job_id, e)
finally:
job["finished_at"] = datetime.now().isoformat()
self._update_db(
job_id, job["status"],
job.get("output", ""),
job.get("error", ""),
)
logger.info("Job %s finished: %s", job_id, job["status"])
def _update_db(self, job_id: str, status: str, output: str = "", error: str = ""):
"""Persist job state to database."""
try:
db = self.engine.shared_data.db
db.execute(
"UPDATE loki_jobs SET status=?, output=?, error=?, "
"started_at=?, finished_at=? WHERE id=?",
(status, output, error,
self._jobs.get(job_id, {}).get("started_at"),
self._jobs.get(job_id, {}).get("finished_at"),
job_id)
)
except Exception as e:
logger.error("DB update job error: %s", e)