diff --git a/python/utils/kicad_process.py b/python/utils/kicad_process.py index 201e52d..d0d6df6 100644 --- a/python/utils/kicad_process.py +++ b/python/utils/kicad_process.py @@ -3,22 +3,77 @@ KiCAD Process Management Utilities Detects if KiCAD is running and provides auto-launch functionality. """ -import os -import subprocess -import logging -import platform -import time -from pathlib import Path +import os +import subprocess +import logging +import platform +import time +import ctypes +from ctypes import wintypes +from pathlib import Path from typing import Optional, List logger = logging.getLogger(__name__) -class KiCADProcessManager: - """Manages KiCAD process detection and launching""" - - @staticmethod - def is_running() -> bool: +class KiCADProcessManager: + """Manages KiCAD process detection and launching""" + + @staticmethod + def _windows_list_processes() -> List[dict]: + """List running processes on Windows using Toolhelp API.""" + processes: List[dict] = [] + try: + TH32CS_SNAPPROCESS = 0x00000002 + try: + ulong_ptr = wintypes.ULONG_PTR # type: ignore[attr-defined] + except AttributeError: + ulong_ptr = ctypes.c_ulonglong if ctypes.sizeof(ctypes.c_void_p) == 8 else ctypes.c_ulong + + class PROCESSENTRY32W(ctypes.Structure): + _fields_ = [ + ("dwSize", wintypes.DWORD), + ("cntUsage", wintypes.DWORD), + ("th32ProcessID", wintypes.DWORD), + ("th32DefaultHeapID", ulong_ptr), + ("th32ModuleID", wintypes.DWORD), + ("cntThreads", wintypes.DWORD), + ("th32ParentProcessID", wintypes.DWORD), + ("pcPriClassBase", wintypes.LONG), + ("dwFlags", wintypes.DWORD), + ("szExeFile", wintypes.WCHAR * wintypes.MAX_PATH), + ] + + CreateToolhelp32Snapshot = ctypes.windll.kernel32.CreateToolhelp32Snapshot + Process32FirstW = ctypes.windll.kernel32.Process32FirstW + Process32NextW = ctypes.windll.kernel32.Process32NextW + CloseHandle = ctypes.windll.kernel32.CloseHandle + + snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) + if snapshot == wintypes.HANDLE(-1).value: + return processes + + entry = PROCESSENTRY32W() + entry.dwSize = ctypes.sizeof(PROCESSENTRY32W) + + if Process32FirstW(snapshot, ctypes.byref(entry)): + while True: + processes.append({ + "pid": str(entry.th32ProcessID), + "name": entry.szExeFile, + "command": entry.szExeFile + }) + if not Process32NextW(snapshot, ctypes.byref(entry)): + break + + CloseHandle(snapshot) + except Exception as e: + logger.error(f"Error listing Windows processes: {e}") + + return processes + + @staticmethod + def is_running() -> bool: """ Check if KiCAD is currently running @@ -68,13 +123,13 @@ class KiCADProcessManager: ) return result.returncode == 0 - elif system == "Windows": - result = subprocess.run( - ["tasklist", "/FI", "IMAGENAME eq pcbnew.exe"], - capture_output=True, - text=True - ) - return "pcbnew.exe" in result.stdout + elif system == "Windows": + processes = KiCADProcessManager._windows_list_processes() + for proc in processes: + name = (proc.get("name") or "").lower() + if name in ("pcbnew.exe", "kicad.exe"): + return True + return False else: logger.warning(f"Process detection not implemented for {system}") @@ -96,11 +151,14 @@ class KiCADProcessManager: # Try to find executable in PATH first for cmd in ["pcbnew", "kicad"]: - result = subprocess.run( - ["which", cmd] if system != "Windows" else ["where", cmd], - capture_output=True, - text=True - ) + result = subprocess.run( + ["which", cmd] if system != "Windows" else ["where", cmd], + capture_output=True, + text=True, + encoding="mbcs" if system == "Windows" else None, + errors="ignore" if system == "Windows" else None, + timeout=5 if system == "Windows" else None + ) if result.returncode == 0: path = result.stdout.strip().split("\n")[0] logger.info(f"Found KiCAD executable: {path}") @@ -235,29 +293,17 @@ class KiCADProcessManager: "command": " ".join(parts[10:]) }) - elif system == "Windows": - result = subprocess.run( - ["tasklist", "/V", "/FO", "CSV"], - capture_output=True, - text=True - ) - import csv - reader = csv.reader(result.stdout.split("\n")) - for row in reader: - if row and len(row) > 0: - if "pcbnew" in row[0].lower() or "kicad" in row[0].lower(): - processes.append({ - "pid": row[1] if len(row) > 1 else "unknown", - "name": row[0], - "command": row[0] - }) + elif system == "Windows": + for proc in KiCADProcessManager._windows_list_processes(): + name = (proc.get("name") or "").lower() + if "pcbnew" in name or "kicad" in name: + processes.append(proc) except Exception as e: logger.error(f"Error getting process info: {e}") return processes - def check_and_launch_kicad(project_path: Optional[Path] = None, auto_launch: bool = True) -> dict: """ Check if KiCAD is running and optionally launch it