mirror of
https://github.com/FunnyWolf/agentic-soc-platform.git
synced 2026-08-22 13:12:56 +02:00
220 lines
9.0 KiB
Python
220 lines
9.0 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
import importlib
|
|
import threading
|
|
import time
|
|
import uuid
|
|
from typing import Callable, Dict, Optional
|
|
|
|
from apscheduler.schedulers.background import BackgroundScheduler
|
|
|
|
from Lib.baseplaybook import BasePlaybook
|
|
from Lib.log import logger
|
|
from Lib.moduleengine import ModuleEngine
|
|
from Lib.playbookloader import PlaybookLoader
|
|
from Lib.threadmodulemanager import thread_module_manager
|
|
from Lib.xcache import Xcache
|
|
from PLUGINS.Redis.redis_stream_api import RedisStreamAPI
|
|
from PLUGINS.SIRP.analysis import run_case_analysis
|
|
from PLUGINS.SIRP.sirpapi import Playbook, Case
|
|
from PLUGINS.SIRP.sirpextramodel import PlaybookJobStatus, PlaybookModel
|
|
|
|
|
|
class MainMonitor(object):
|
|
@staticmethod
|
|
def on_playbook_task_finished(thread_id: str, task_obj: BasePlaybook, result: object,
|
|
exception: Optional[Exception], context: Optional[Dict[str, str]] = None):
|
|
context = context or {}
|
|
playbook_row_id = context.get("playbook_row_id")
|
|
if not isinstance(playbook_row_id, str) or playbook_row_id == "":
|
|
logger.error(f"[Thread {thread_id}] Missing playbook_row_id in callback context.")
|
|
return
|
|
|
|
try:
|
|
playbook_current = Playbook.get(playbook_row_id, lazy_load=True)
|
|
except Exception as e:
|
|
logger.error(f"[Thread {thread_id}] Failed to load playbook for fallback status update.")
|
|
logger.exception(e)
|
|
return
|
|
|
|
# Business code has already written a terminal state; do not overwrite.
|
|
if playbook_current.job_status != PlaybookJobStatus.RUNNING:
|
|
logger.info(
|
|
f"[Thread {thread_id}] Skip fallback status update, current status: {playbook_current.job_status}, "
|
|
f"row_id: {playbook_row_id}"
|
|
)
|
|
return
|
|
|
|
fallback_status = PlaybookJobStatus.SUCCESS if exception is None else PlaybookJobStatus.FAILED
|
|
if exception is None:
|
|
remark = f"source=thread_fallback; thread_id={thread_id}; message=Task finished without explicit business status update."
|
|
else:
|
|
remark = (
|
|
f"source=thread_fallback; thread_id={thread_id}; "
|
|
f"exception={type(exception).__name__}; message={exception}"
|
|
)
|
|
|
|
model_tmp = PlaybookModel(row_id=playbook_row_id)
|
|
model_tmp.job_status = fallback_status
|
|
model_tmp.remark = remark
|
|
Playbook.update(model_tmp)
|
|
|
|
logger.info(
|
|
f"[Thread {thread_id}] Applied fallback status update: {fallback_status}, row_id: {playbook_row_id}"
|
|
)
|
|
|
|
MainScheduler: BackgroundScheduler
|
|
|
|
def __init__(self):
|
|
self._background_threads: Dict[str, list] = {}
|
|
self.engine = ModuleEngine()
|
|
self.redis_stream_api = RedisStreamAPI()
|
|
self.MainScheduler = BackgroundScheduler(timezone='Asia/Shanghai')
|
|
|
|
@staticmethod
|
|
def run_task_in_loop(task_func: Callable, task_name: str, retry_interval: int = 5, exec_interval: int = None):
|
|
"""
|
|
Run a task function in an infinite loop with error handling
|
|
|
|
Args:
|
|
task_func: The function to run
|
|
task_name: Name of the task for logging
|
|
retry_interval: Seconds to wait between retries on error
|
|
exec_interval: Seconds to wait between executions (defaults to retry_interval if None)
|
|
"""
|
|
# If exec_interval is not specified, use retry_interval
|
|
if exec_interval is None:
|
|
exec_interval = retry_interval
|
|
|
|
while True:
|
|
try:
|
|
task_func()
|
|
# Wait for the specified execution interval before running again
|
|
time.sleep(exec_interval)
|
|
except Exception as e:
|
|
logger.error(f"Error in {task_name}")
|
|
logger.exception(e)
|
|
time.sleep(retry_interval)
|
|
|
|
def start_background_task(self, task_func: Callable, task_name: str, retry_interval: int = 5,
|
|
exec_interval: int = None, thread_count: int = 1):
|
|
"""
|
|
Start a background task in one or more separate threads
|
|
|
|
Args:
|
|
task_func: The function to run
|
|
task_name: Name of the task for logging
|
|
retry_interval: Seconds to wait between retries on error
|
|
exec_interval: Seconds to wait between executions (defaults to retry_interval if None)
|
|
thread_count: Number of threads to spawn for this task
|
|
"""
|
|
if task_name not in self._background_threads:
|
|
self._background_threads[task_name] = []
|
|
|
|
for i in range(thread_count):
|
|
suffix = f"#{i}" if thread_count > 1 else ""
|
|
thread_name = f"{task_name}{suffix}"
|
|
thread = threading.Thread(
|
|
target=self.run_task_in_loop,
|
|
args=(task_func, thread_name, retry_interval, exec_interval),
|
|
daemon=True,
|
|
name=thread_name,
|
|
)
|
|
self._background_threads[task_name].append(thread)
|
|
thread.start()
|
|
logger.info(f"Started background task: {thread_name}")
|
|
|
|
def start(self):
|
|
logger.info("Starting background services...")
|
|
logger.info("Load PlaybookLoader module config")
|
|
PlaybookLoader.load_all_playbook_config()
|
|
|
|
delay_time = 3
|
|
|
|
# Start background tasks
|
|
self.start_background_task(self.subscribe_pending_playbook, "subscribe_pending_playbook", delay_time)
|
|
self.start_background_task(self.subscribe_case_analysis_scheduler, "subscribe_case_analysis_scheduler", delay_time)
|
|
self.start_background_task(self.subscribe_case_analysis_queue, "subscribe_case_analysis_queue", delay_time, thread_count=3)
|
|
|
|
# engine
|
|
self.engine.start()
|
|
logger.info("Background services started.")
|
|
|
|
@staticmethod
|
|
def subscribe_pending_playbook():
|
|
models = Playbook.list_pending_playbooks()
|
|
|
|
for model in models:
|
|
module_config = Xcache.get_module_config_by_name(model.name)
|
|
model_tmp = PlaybookModel(row_id=model.row_id)
|
|
if module_config is None:
|
|
PlaybookLoader.load_all_playbook_config() # try again
|
|
module_config = Xcache.get_module_config_by_name(model.name)
|
|
if module_config is None:
|
|
logger.error(f"PlaybookLoader module config not found: {model.name}")
|
|
model_tmp.job_status = PlaybookJobStatus.FAILED
|
|
model_tmp.remark = f"PlaybookLoader module config not found: {model.name}"
|
|
Playbook.update(model_tmp)
|
|
continue
|
|
|
|
load_path = module_config.get("load_path")
|
|
|
|
try:
|
|
class_intent = importlib.import_module(load_path)
|
|
playbook_intent: BasePlaybook = class_intent.Playbook()
|
|
playbook_intent._playbook_model = model
|
|
except Exception as E:
|
|
logger.exception(E)
|
|
model_tmp.job_status = PlaybookJobStatus.FAILED
|
|
model_tmp.remark = str(E)
|
|
Playbook.update(model_tmp)
|
|
continue
|
|
|
|
job_id = str(uuid.uuid1())
|
|
model_tmp.job_status = PlaybookJobStatus.RUNNING
|
|
model_tmp.job_id = job_id
|
|
Playbook.update(model_tmp)
|
|
|
|
try:
|
|
thread_module_manager.start_task(
|
|
playbook_intent,
|
|
thread_id=job_id,
|
|
on_finished=MainMonitor.on_playbook_task_finished,
|
|
callback_context={
|
|
"playbook_row_id": model.row_id,
|
|
},
|
|
)
|
|
except Exception as e:
|
|
model_tmp.job_status = PlaybookJobStatus.FAILED
|
|
model_tmp.remark = f"Failed to create playbook job. exception={type(e).__name__}; message={e}"
|
|
Playbook.update(model_tmp)
|
|
continue
|
|
|
|
logger.info(f"Create playbook job success: {job_id}")
|
|
|
|
@staticmethod
|
|
def subscribe_case_analysis_scheduler():
|
|
# Periodically move due cases into the analysis queue.
|
|
# 周期性扫描到点案件,并将其送入分析队列。
|
|
promoted_row_ids = Case.promote_due_analysis_cases()
|
|
if promoted_row_ids:
|
|
logger.info(f"Queued {len(promoted_row_ids)} case(s) for scheduled analysis.")
|
|
|
|
def subscribe_case_analysis_queue(self):
|
|
# Consume one queue message and hand it to the analysis runner.
|
|
# 每次消费一条队列消息,并交给分析执行器处理。
|
|
message = self.redis_stream_api.read_stream_message_with_id(
|
|
stream_name=Case.ANALYSIS_STREAM_NAME,
|
|
consumer_name="case-analysis-worker",
|
|
)
|
|
queue_message_id = message.get("message_id")
|
|
payload = message.get("data", {})
|
|
case_row_id = payload.get("case_row_id")
|
|
trigger = payload.get("trigger", "unknown")
|
|
|
|
if not case_row_id:
|
|
logger.error("Case analysis queue message missing case_row_id.")
|
|
return
|
|
|
|
run_case_analysis(case_row_id, trigger, queue_message_id=queue_message_id)
|