Files
Sean P. KaneandGitHub eb2f6fba2a fix: startup tweaks (#41)
* fix: startup tweaks

* fix: Add a few safety checks
2026-01-18 23:57:14 -08:00

232 lines
9.0 KiB
Python

"""Robust MCP Bridge Workbench - Initialization.
SPDX-License-Identifier: MIT
Copyright (c) 2025 Sean P. Kane (GitHub: spkane)
This module is executed when FreeCAD starts up. It handles initialization
tasks for the Robust MCP Bridge workbench, including auto-start of the
MCP bridge if configured. Works in both GUI and headless modes.
Note: Status bar updates are handled by InitGui.py since Qt operations
must run on the main thread.
"""
from __future__ import annotations
# Import FreeCAD first so we can log early
import FreeCAD
FreeCAD.Console.PrintMessage("Robust MCP Bridge: Init.py loaded\n")
from typing import TYPE_CHECKING, Any # noqa: E402
if TYPE_CHECKING:
from freecad_mcp_bridge.bridge_utils import GuiWaiter
FreeCAD.Console.PrintMessage("Robust MCP Bridge: Init loaded\n")
# Global reference to GuiWaiter and auto-start timer to prevent garbage collection
# Type annotations use Any for timer since it could be QTimer from PySide2 or PySide6
_auto_start_timer: Any | None = None
_gui_waiter: GuiWaiter | None = None
def _auto_start_bridge() -> None:
"""Auto-start the MCP bridge if configured in preferences.
This function is called via a deferred timer (GUI mode) or directly
(headless mode) after FreeCAD finishes loading. It starts the bridge
without requiring the workbench to be selected.
Args:
None.
Returns:
None. Early returns if auto-start is disabled or bridge is already running.
Raises:
Exception: Any exception during bridge startup is caught, logged to
FreeCAD.Console.PrintError with full traceback, and suppressed.
Side Effects:
- Imports and checks auto-start preference from preferences module
- Creates and starts a FreecadMCPPlugin instance if not already running
- Registers the plugin with the workbench commands module
- Prints status messages to FreeCAD.Console
Example:
This function is typically called via QTimer or GuiWaiter callback::
QtCore.QTimer.singleShot(1000, _auto_start_bridge)
"""
try:
from preferences import get_auto_start
if not get_auto_start():
return
# Check if bridge is already running
from commands import _mcp_plugin
if _mcp_plugin is not None and _mcp_plugin.is_running:
return
FreeCAD.Console.PrintMessage(
"Auto-starting MCP Bridge (configured in preferences)...\n"
)
# Import and start the bridge directly
from freecad_mcp_bridge.server import FreecadMCPPlugin
from preferences import get_socket_port, get_xmlrpc_port
xmlrpc_port = get_xmlrpc_port()
socket_port = get_socket_port()
plugin = FreecadMCPPlugin(
host="localhost",
port=socket_port,
xmlrpc_port=xmlrpc_port,
enable_xmlrpc=True,
)
plugin.start()
# Register plugin with commands module for restart detection
from freecad_mcp_bridge.bridge_utils import register_mcp_plugin
register_mcp_plugin(plugin, xmlrpc_port, socket_port)
FreeCAD.Console.PrintMessage("\n")
FreeCAD.Console.PrintMessage("=" * 50 + "\n")
FreeCAD.Console.PrintMessage("MCP Bridge started!\n")
FreeCAD.Console.PrintMessage(f" - XML-RPC: localhost:{xmlrpc_port}\n")
FreeCAD.Console.PrintMessage(f" - Socket: localhost:{socket_port}\n")
FreeCAD.Console.PrintMessage("=" * 50 + "\n")
FreeCAD.Console.PrintMessage(
"\nYou can now connect your MCP client (Claude Code, etc.) to FreeCAD.\n"
)
except Exception as e:
FreeCAD.Console.PrintError(f"Failed to auto-start MCP Bridge: {e}\n")
import traceback
FreeCAD.Console.PrintError(f"Traceback: {traceback.format_exc()}\n")
# Schedule auto-start after FreeCAD finishes loading
# Strategy:
# - If FreeCAD.GuiUp is True: Qt event loop is running, use timer for deferred start
# - If FreeCAD.GuiUp is False but QApplication exists: FreeCAD GUI is initializing.
# Use GuiWaiter to wait for GuiUp to become True before starting.
# This ensures the bridge uses Qt timer (not background thread) for queue processing.
# - If no QApplication: True headless mode, start bridge directly
#
# IMPORTANT: We check for QApplication.instance() rather than just QtCore availability
# because FreeCAD bundles PySide even in headless mode (freecadcmd), but there's no
# Qt event loop running. Without a QApplication, Qt timers will never fire.
#
# CRITICAL: We must wait for FreeCAD.GuiUp to be True before starting the bridge
# in GUI mode. If we start when GuiUp is False, the bridge's _start_queue_processor()
# will see GuiUp=False and use a background thread. Later, code executed on that
# thread will try to do Qt operations, causing crashes (SIGABRT in QCocoaWindow).
try:
from preferences import get_auto_start
_auto_start_enabled = get_auto_start()
FreeCAD.Console.PrintMessage(
f"Robust MCP Bridge: Auto-start preference = {_auto_start_enabled}\n"
)
if _auto_start_enabled:
# Try to import Qt and check for running QApplication
import contextlib
QtCore = None
QtWidgets = None
_has_qapp = False
_is_true_headless = False
try:
from PySide2 import QtCore, QtWidgets # type: ignore[assignment, no-redef]
except ImportError:
with contextlib.suppress(ImportError):
from PySide6 import ( # type: ignore[assignment, no-redef]
QtCore,
QtWidgets,
)
# Detect GUI mode vs true headless mode
# - True headless (freecadcmd): QCoreApplication exists but NOT QApplication
# - GUI mode early startup: No app yet, or QApplication being initialized
# - GUI mode ready: FreeCAD.GuiUp is True
if QtWidgets is not None and QtCore is not None:
qapp = QtWidgets.QApplication.instance()
if qapp is not None:
_has_qapp = True
else:
# No QApplication - check if QCoreApplication exists
# If QCoreApplication exists but is NOT a QApplication, it's true headless
qcore_app = QtCore.QCoreApplication.instance()
if qcore_app is not None and not isinstance(
qcore_app, QtWidgets.QApplication
):
_is_true_headless = True
# If no app at all, assume early GUI startup (will use GuiWaiter)
FreeCAD.Console.PrintMessage(
f"Robust MCP Bridge: GuiUp={FreeCAD.GuiUp}, "
f"QtCore={'available' if QtCore else 'unavailable'}, "
f"QApp={'running' if _has_qapp else 'none'}, "
f"headless={_is_true_headless}\n"
)
if FreeCAD.GuiUp:
# GUI is already up - use timer for deferred start
FreeCAD.Console.PrintMessage(
"Robust MCP Bridge: GUI already up, scheduling deferred start...\n"
)
if QtCore is not None:
_auto_start_timer = QtCore.QTimer()
_auto_start_timer.setSingleShot(True)
_auto_start_timer.timeout.connect(_auto_start_bridge)
_auto_start_timer.start(1000)
else:
# GUI is up but Qt import failed - start directly
_auto_start_bridge()
elif _is_true_headless:
# True headless mode - QCoreApplication exists but not QApplication
# No Qt event loop for GUI, so start bridge directly with background thread
FreeCAD.Console.PrintMessage(
"Robust MCP Bridge: True headless mode (QCoreApplication only), "
"starting directly...\n"
)
_auto_start_bridge()
elif QtCore is not None:
# GUI not ready yet (either QApplication exists or no app yet)
# Use GuiWaiter to wait for GuiUp to become True before starting
# This ensures the bridge uses Qt timer (not background thread) for queue
FreeCAD.Console.PrintMessage(
"Robust MCP Bridge: GUI not ready, using GuiWaiter...\n"
)
from freecad_mcp_bridge.bridge_utils import GuiWaiter
_gui_waiter = GuiWaiter(
callback=_auto_start_bridge,
log_prefix="Robust MCP Bridge",
timeout_error_extra=(
"\nTo start the bridge manually, select the Robust MCP Bridge "
"workbench\nand click 'Start MCP Bridge'.\n\n"
),
)
_gui_waiter.start()
else:
# No Qt available at all - unusual state, start directly
FreeCAD.Console.PrintMessage(
"Robust MCP Bridge: No Qt available, starting directly...\n"
)
_auto_start_bridge()
except Exception as e:
FreeCAD.Console.PrintWarning(f"Could not set up auto-start: {e}\n")
import traceback
FreeCAD.Console.PrintWarning(f"Traceback: {traceback.format_exc()}\n")