from __future__ import annotations from pathlib import Path import os import sys import uuid import logging import subprocess import platform from concurrent.futures import ThreadPoolExecutor, as_completed from threading import Lock from watchdog.observers import Observer from backend.services.watchdog_handler import FileCreatedHandler from config import Config # Job ID별 진행률 (스레드 안전) _progress: dict[str, int] = {} _progress_lock = Lock() def _set_progress(job_id: str, value: int) -> None: with _progress_lock: _progress[job_id] = max(0, min(100, int(value))) def get_progress(job_id: str) -> int: with _progress_lock: return int(_progress.get(job_id, 0)) def on_complete(job_id: str) -> None: _set_progress(job_id, 100) # ───────────────────────────────────────────────────────────── # IP 목록 저장 # ───────────────────────────────────────────────────────────── def save_ip_addresses(ips: str, folder: str | os.PathLike[str]) -> list[tuple[str, str]]: out_dir = Path(folder) out_dir.mkdir(parents=True, exist_ok=True) ip_files: list[tuple[str, str]] = [] for i, raw in enumerate((ips or "").splitlines()): ip = raw.strip() if not ip: continue file_path = out_dir / f"ip_{i}.txt" file_path.write_text(ip + "\n", encoding="utf-8") ip_files.append((ip, str(file_path))) return ip_files # ───────────────────────────────────────────────────────────── # 개별 IP 처리 # ───────────────────────────────────────────────────────────── def _build_command(script: str, ip_file: str, xml_file: str | None) -> list[str]: script_path = Path(Config.SCRIPT_FOLDER) / script if not script_path.exists(): raise FileNotFoundError(f"스크립트를 찾을 수 없습니다: {script_path}") if script_path.suffix == ".sh": # Windows에서 .sh 실행은 bash 필요 (Git Bash/WSL 등). 없으면 예외 처리. if platform.system() == "Windows": bash = shutil.which("bash") # type: ignore[name-defined] if not bash: raise RuntimeError("Windows에서 .sh 스크립트를 실행하려면 bash가 필요합니다.") cmd = [bash, str(script_path), ip_file] else: cmd = [str(script_path), ip_file] elif script_path.suffix == ".py": cmd = [sys.executable, str(script_path), ip_file] else: raise ValueError(f"지원되지 않는 스크립트 형식: {script_path.suffix}") if xml_file: cmd.append(xml_file) return cmd def process_ip(ip_file: str, script: str, xml_file: str | None = None) -> None: ip = Path(ip_file).read_text(encoding="utf-8").strip() cmd = _build_command(script, ip_file, xml_file) logging.info("🔧 실행 명령: %s", " ".join(cmd)) try: result = subprocess.run( cmd, capture_output=True, text=True, check=True, cwd=str(Path(Config.SCRIPT_FOLDER)), timeout=int(os.getenv("SCRIPT_TIMEOUT", "1800")), # 30분 기본 ) logging.info("[%s] ✅ stdout:\n%s", ip, result.stdout) if result.stderr: logging.warning("[%s] ⚠ stderr:\n%s", ip, result.stderr) except subprocess.CalledProcessError as e: logging.error("[%s] ❌ 스크립트 실행 오류(code=%s): %s", ip, e.returncode, e.stderr or e) except subprocess.TimeoutExpired: logging.error("[%s] ⏰ 스크립트 실행 타임아웃", ip) # ───────────────────────────────────────────────────────────── # 병렬 처리 진입점 # ───────────────────────────────────────────────────────────── def process_ips_concurrently(ip_files, job_id, observer: Observer, script: str, xml_file: str | None): total = len(ip_files) completed = 0 _set_progress(job_id, 0) try: with ThreadPoolExecutor(max_workers=Config.MAX_WORKERS) as pool: futures = {pool.submit(process_ip, ip_path, script, xml_file): ip for ip, ip_path in ip_files} for fut in as_completed(futures): ip = futures[fut] try: fut.result() except Exception as e: logging.error("%s 처리 중 오류 발생: %s", ip, e) finally: completed += 1 if total: _set_progress(job_id, int(completed * 100 / total)) finally: try: observer.stop() observer.join(timeout=5) except Exception: pass # ───────────────────────────────────────────────────────────── # 외부에서 한 번에 처리(동기) # ───────────────────────────────────────────────────────────── def handle_ip_processing(ip_text: str, script: str, xml_file: str | None = None) -> str: job_id = str(uuid.uuid4()) temp_dir = Path(Config.UPLOAD_FOLDER) / job_id ip_files = save_ip_addresses(ip_text, temp_dir) xml_path = str(Path(Config.XML_FOLDER) / xml_file) if xml_file else None handler = FileCreatedHandler(job_id, len(ip_files)) observer = Observer() observer.schedule(handler, Config.IDRAC_INFO_FOLDER, recursive=False) observer.start() process_ips_concurrently(ip_files, job_id, observer, script, xml_path) return job_id