master
John Lauer Update 40 file(s) 817d1d2 2mo ago
"""launch.py — cold-start KiCad (the project manager) from nothing.

Fills the gap found in issue #19: `kicad_open_editors` only INVENTORIES editors
inside already-running KiCad instances, and every other open_* verb launches a
specific editor. This is the verb whose one job is "KiCad is closed; open it."

Windows-first (Popen on kicad.exe with a no-activate startupinfo so the window
opens in the BACKGROUND per the bridge's focus rules); macOS falls back to
`open -a KiCad`.
"""
import os
import subprocess
import sys
import time
from pathlib import Path

IS_WINDOWS = sys.platform == "win32"


def _kicad_windows() -> list:
    try:
        from handlers.kicad_ui import _find_all_kicad_windows
        return _find_all_kicad_windows()
    except Exception:
        return []


def handle_launch(kicad_info: dict, args: dict) -> dict:
    """Cold-start KiCad's project manager (idempotent).

    Args (all optional):
      project:     path to a .kicad_pro to open with it
      waitSeconds: how long to wait for a KiCad window (default 15)
    """
    existing = _kicad_windows()
    if existing:
        return {
            "success": True,
            "alreadyRunning": True,
            "windows": [w.get("title") for w in existing][:10],
            "output": f"KiCad is already running ({len(existing)} window(s)) — nothing launched.",
            "_hint": ("Idempotent no-op: KiCad was already up. Use kicad_open_editors to inventory "
                      "editors, or the open_* verbs to open a specific one."),
        }

    project = (args.get("project") or "").strip()
    if project and not Path(project).exists():
        return {"success": False, "error": f"Project not found: {project}",
                "_hint": "Absolute Windows path required; %VAR% expansion is not performed."}

    if not IS_WINDOWS:
        try:
            cmd = ["open", "-a", "KiCad"] + ([project] if project else [])
            _p = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
            try:
                from handlers.win_focus import register_owned_pid
                register_owned_pid(_p.pid)
            except Exception:
                pass
            return {"success": True, "launched": True,
                    "output": "KiCad launch requested via `open -a KiCad`."}
        except Exception as e:
            return {"success": False, "error": f"Failed to launch KiCad: {e}"}

    exe = kicad_info.get("kicad_exe")
    if not exe or not Path(exe).exists():
        return {
            "success": False,
            "error": "kicad.exe not found",
            "_hint": ("No KiCad installation detected. kicad_readiness reports install state; "
                      "kicad_upgrade installs KiCad silently (per-user, no UAC)."),
        }

    _si = None
    try:
        from handlers import win_focus
        _si = win_focus.background_startupinfo()
    except Exception:
        pass
    # CAPTURE KICAD'S OWN DIAGNOSTICS INSTEAD OF DISCARDING THEM.
    #
    # This used to be stdout=DEVNULL, stderr=DEVNULL. KiCad writes no log file:
    # everything it has to say about itself goes to stderr via wxLogError /
    # wxLogTrace, and a Windows GUI process has no console, so DEVNULL meant the
    # ONLY channel KiCad has for reporting its own failures was being thrown away
    # by the very thing driving it. A failed board load could then only be found
    # by screenshotting a modal dialog and reading the pixels.
    #
    # KICAD_ENABLE_WXTRACE=1 is required in a RELEASE build or wx logging is off
    # entirely (common/single_top.cpp:202, kicad/kicad.cpp:579). Errors and
    # warnings need no WXTRACE mask; masks only add the trace firehose.
    _log_path = None
    _log_fh = None
    try:
        from handlers.kicad_log import new_log_path, trace_env
        _log_path = new_log_path("launch")
        _log_fh = open(_log_path, "ab", buffering=0)
        _env = trace_env(args.get("traceMasks"))
    except Exception:  # never let logging break launching
        _log_fh, _env = None, None
    try:
        _p = subprocess.Popen([exe] + ([project] if project else []),
                              stdout=(_log_fh or subprocess.DEVNULL),
                              stderr=(_log_fh or subprocess.DEVNULL),
                              env=_env,
                              startupinfo=_si)
        try:
            from handlers.win_focus import register_owned_pid
            register_owned_pid(_p.pid)
        except Exception:
            pass
    except Exception as e:
        return {"success": False, "error": f"Failed to launch KiCad: {e}"}

    # The parent's copy of the handle is not needed once the child owns it; the
    # child keeps writing after we close ours.
    try:
        if _log_fh:
            _log_fh.close()
    except Exception:
        pass

    wait_s = float(args.get("waitSeconds") or 15)
    deadline = time.monotonic() + max(3.0, wait_s)
    windows: list = []
    while time.monotonic() < deadline:
        windows = _kicad_windows()
        if windows:
            break
        time.sleep(0.5)

    if windows:
        return {
            "success": True,
            "launched": True,
            "windows": [w.get("title") for w in windows][:10],
            "output": f"KiCad launched — {len(windows)} window(s) up.",
            "logPath": str(_log_path) if _log_path else None,
            "_hint": ("Launched in the BACKGROUND (no focus steal). First run on a fresh install "
                      "may show a setup wizard — kicad_window_info self-heals benign dialogs; "
                      "kicad_screenshot_all shows every window if something looks stuck."),
            "_next": ["kicad_window_info", "kicad_open_editors", "kicad_open_board"],
        }
    return {
        "success": False,
        "logPath": str(_log_path) if _log_path else None,
        "error": f"kicad.exe started but no KiCad window appeared within {int(wait_s)}s",
        "_hint": ("Cold start on a slow box (or software-OpenGL) can exceed the wait — retry "
                  "kicad_window_info in a few seconds before treating this as a failure. "
                  "A modal (crash-recovery / first-run) can also hold the window back: "
                  "kicad_screenshot_all will show it."),
        "statusVerb": "kicad_window_info",
    }