From 11bf1e777989c8ba93d1d7cdb40a77ec06de6467 Mon Sep 17 00:00:00 2001 From: Michael Huth Date: Tue, 21 Jul 2026 23:47:35 +0200 Subject: [PATCH 01/12] Add a simple MCP server for Igor Pro 9+ - based on the COM automation server interface - uses python - needs user interaction if agent runs into a compilation error - documentation as rst file --- Packages/MIES/MIES_ClaudeHelper.ipf | 43 + Packages/MIES_Include.ipf | 1 + Packages/doc/igor-pro-bridge.rst | 266 ++++ Packages/doc/index.rst | 1 + .../igor-pro-bridge-1.9.0.mcpb | Bin 0 -> 22218 bytes tools/igor-mcp-bridge/server.py | 1119 +++++++++++++++++ 6 files changed, 1430 insertions(+) create mode 100644 Packages/MIES/MIES_ClaudeHelper.ipf create mode 100644 Packages/doc/igor-pro-bridge.rst create mode 100644 tools/igor-mcp-bridge/igor-pro-bridge-1.9.0.mcpb create mode 100644 tools/igor-mcp-bridge/server.py diff --git a/Packages/MIES/MIES_ClaudeHelper.ipf b/Packages/MIES/MIES_ClaudeHelper.ipf new file mode 100644 index 0000000000..80b21466ff --- /dev/null +++ b/Packages/MIES/MIES_ClaudeHelper.ipf @@ -0,0 +1,43 @@ +#pragma TextEncoding = "UTF-8" +#pragma rtGlobals = 3 // Use modern global access method and strict wave access. +#pragma rtFunctionErrors = 1 + +#ifdef AUTOMATED_TESTING +#pragma ModuleName = MIES_CH +#endif // AUTOMATED_TESTING + +/// @file MIES_ClaudeHelper.ipf +/// @brief Helper functions for the Igor Pro Bridge MCP server (tools/igor-mcp-bridge) + +#ifdef IGOR_PRO_BRIDGE + +/// AfterCompiledHook() is a predefined Igor hook: Igor calls it after ALL procedure +/// windows have compiled successfully (confirmed from Igor Pro Folder/Igor Help +/// Files/Advanced Topics.ihf). It is declared static so it coexists with any other +/// file's own static AfterCompiledHook() (e.g. the one in MIES_Include.ipf used only +/// for the too-old-Igor warning panel) without colliding. +/// +/// It records a monotonically increasing counter in root:gClaudeHelperCompileCounter +/// each time it fires. This gives the Igor Pro Bridge bridge a compile confirmation +/// driven by Igor itself, rather than only inferred by polling FunctionInfo() for a +/// non-existing function -- which can read stale state before Igor's operation queue +/// (RELOAD CHANGED PROCS / COMPILEPROCEDURES) has actually drained. There is no +/// equivalent Igor hook for a *failed* compile, so this only helps confirm success, +/// not detect failure. + +static Function AfterCompiledHook() + + // Bare Variable/G (no initializer) is safe to call unconditionally: per Igor + // Reference.ihf, /G "overwrites any existing variable" but "the variable is + // initialized when it is created if you supply the initial value" -- i.e. the + // overwrite-to-a-value only happens when an initializer is given. Without one, + // this creates the global at 0 the first time and leaves an existing value + // alone on every call after that, so no NVAR_Exists guard is needed. + variable/G root:gClaudeHelperCompileCounter + NVAR gClaudeHelperCompileCounter = root:gClaudeHelperCompileCounter + + gClaudeHelperCompileCounter += 1 + + return 0 +End +#endif // IGOR_PRO_BRIDGE diff --git a/Packages/MIES_Include.ipf b/Packages/MIES_Include.ipf index ac90388999..25ca02a47e 100644 --- a/Packages/MIES_Include.ipf +++ b/Packages/MIES_Include.ipf @@ -196,6 +196,7 @@ End #include "MIES_Cache" #include "MIES_CheckInstallation" +#include "MIES_ClaudeHelper" #include "MIES_Configuration" #include "MIES_ConversionConstants" #include "MIES_Constants" diff --git a/Packages/doc/igor-pro-bridge.rst b/Packages/doc/igor-pro-bridge.rst new file mode 100644 index 0000000000..470e14f3af --- /dev/null +++ b/Packages/doc/igor-pro-bridge.rst @@ -0,0 +1,266 @@ +.. _igor_pro_bridge_doc: + +=============== +Igor Pro Bridge +=============== + +Description +----------- + +The Igor Pro Bridge lets an AI coding agent (Claude, via a local MCP server) control an +already-running Igor Pro instance directly: execute commands, read wave data, edit +``.ipf`` files on disk and recompile them, and inspect the live environment -- without a +human needing to click anything in Igor Pro for routine steps. + +The code lives in ``tools/igor-mcp-bridge/``: + +- ``server.py``: the MCP server implementation (Python, using ``pywin32`` for COM). +- ``igor-pro-bridge-*.mcpb``: packaged Claude Desktop Extension bundles built from + ``server.py`` via the `mcpb `__ CLI. + +The companion procedure file ``Packages/MIES/MIES_ClaudeHelper.ipf`` (included from +``MIES_Include.ipf``) provides an ``AfterCompiledHook`` used by the bridge to get a more +reliable compile-success signal; see :ref:`igor_pro_bridge_claude_helper` below. + +This is Windows-only tooling for MIES development, not something end users of MIES +interact with. + +Architecture +------------ + +Igor Pro can act as a COM *server* on Windows via its built-in ActiveX Automation +Server (``IgorPro.Application``), documented in ``Igor Pro Folder/Miscellaneous/Windows +Automation/Automation Server.ihf``. Igor Pro cannot act as a COM *client*. The bridge is +a Python COM *client* process that attaches to an already-running Igor Pro instance via +``win32com.client.GetActiveObject("IgorPro.Application")`` and issues commands through +``Execute2``. + +``Execute2`` does not raise a COM/Automation error just because the Igor-level command +failed -- the bridge checks the returned error code itself and raises a Python +``RuntimeError`` when appropriate. Data is retrieved by including ``fprintf 0, "..."`` +calls in the command string and reading the result back. + +Requirements +------------ + +- Igor Pro 9.00 or later, running on Windows. The Automation Server is already included + in Igor Pro 9; ``RELOAD CHANGED PROCS``, which ``reload_and_compile_procedures`` + depends on, was introduced in Igor Pro 9.00 and sets the actual minimum version. +- Igor Pro must already be running before a tool call is made; the bridge attaches to + the running instance, it does not launch Igor. +- **Both Igor Pro and the bridge's Python process must run elevated (as + Administrator)**. This is a hard Windows COM requirement documented verbatim in + Igor's own Automation Server reference and is not optional. Note that reopening + Claude Desktop normally does not preserve elevation from a previous launch -- it must + be relaunched via "Run as administrator" each time. +- Python, accessible as ``python`` on ``PATH``, with the ``mcp`` and ``pywin32`` + packages installed (``pip install mcp pywin32``, followed by + ``python -m pywin32_postinstall -install``). The packaged extension does not vendor + these. + +Installation +------------ + +The bridge is distributed as a Claude Desktop Extension (``.mcpb``), not via manual +``claude_desktop_config.json`` editing (which does not work reliably for local MCP +servers in current Claude Desktop builds). + +- Build: ``mcpb pack tools/igor-mcp-bridge tools/igor-mcp-bridge/igor-pro-bridge-X.Y.Z.mcpb`` +- Install: Claude Desktop -> Settings -> Extensions -> Advanced settings -> Extension + Developer -> Install Extension, then select the ``.mcpb`` file. +- After installing a new version, fully restart Claude Desktop (elevated) so the + updated server code is actually loaded -- newly added tools can otherwise lag behind + what's installed. + +Available tools +---------------- + +``execute_igor_command(command)`` + Runs a command string on Igor's command line via ``Execute2``. Include an + ``fprintf 0, "..."`` call to get data back. **Caution**: if ``command`` calls + user-defined procedure code and the Debugger is enabled, a breakpoint/runtime + error/abort/stale-reference pause will hang this call indefinitely -- there is no + scriptable way to resume or dismiss the Debugger window. Prefer + ``execute_igor_command_unattended`` whenever nobody is watching who could close that + popup by hand. + +``execute_igor_command_unattended(command)`` + Same as ``execute_igor_command``, but automatically disables Igor's Debugger before + running the command and restores it afterward, even if the command raises. This is + the default choice for any unattended/automated call. On failure, both this and + ``execute_igor_command`` include any partial ``results``/``history`` output captured, + since Igor typically keeps running after an unhandled runtime error rather than + stopping (see :ref:`igor_pro_bridge_runtime_errors`). + +``get_wave(wave_path)`` + Returns the data of an existing 1D Igor wave (numeric or text) as a list. Complex and + multi-dimensional waves are not supported. + +``check_bridge_health()`` + Diagnoses exactly why the bridge can't reach Igor Pro, distinguishing three separate + failure modes: this process not running elevated, no Igor Pro COM object registered + at all, and a registered-but-dead COM object (Igor crashed or was force-closed, + leaving a stale registration that reconnecting alone can't fix). Run this first + whenever something doesn't work. + +``check_compilation_state()`` + Reports whether Igor's procedure code is currently compiled or uncompiled, using the + same technique as ``IsProcGlobalCompiled()`` in + ``Packages/igortest/procedures/igortest-test-compilation.ipf``. + +``reload_and_compile_procedures()`` + Forces Igor to reload changed ``.ipf`` files from disk (``RELOAD CHANGED PROCS``) and + attempt a fresh compilation (``COMPILEPROCEDURES``), then reports the resulting + compiled state. Use this after editing a ``.ipf`` file directly on disk. Both + commands go through Igor's operation queue rather than running immediately (see + "Operation Queue" in ``Advanced Topics.ihf``), so this cross-checks two independent + signals before trusting a "compiled" result -- see + :ref:`igor_pro_bridge_claude_helper` and :ref:`igor_pro_bridge_compile_dialog`. + +``get_debugger_state()`` / ``set_debugger_enabled(enabled, ...)`` / ``restore_debugger_settings()`` + Read, change, and restore Igor's Debugger settings (``DebuggerOptions``). Use + ``get_debugger_state()`` to snapshot the current settings before a longer unattended + session, ``set_debugger_enabled(False)`` to disable the Debugger for the run, and + ``restore_debugger_settings()`` to put things back afterward. + +``get_environment_summary()`` + Summarizes the live instance: Igor version/build, the loaded experiment, loaded XOPs, + currently included procedure files (with a category breakdown), the contents of the + always-present "Procedure" window (which can carry experiment-specific + ``#include``/``#define`` directives not present in any on-disk ``.ipf`` file), the + top-level global data folder layout, and the current Debugger settings. + +.. _igor_pro_bridge_unattended: + +Unattended execution caveats +----------------------------- + +Two independent things can silently stall an automated Claude/Igor session. Neither +hangs the bridge's own COM calls directly -- both instead leave Igor showing a GUI +element that only a human can dismiss. + +Debugger pauses +~~~~~~~~~~~~~~~~ + +If the Debugger is enabled and something trips it (a breakpoint, a runtime error with +"Debug on Error", a user abort, or a stale NVAR/SVAR/WAVE reference), Igor pauses and +opens the Debugger window. There is no documented operation to programmatically +resume, step, or dismiss that pause -- ``Debugger``/``DebuggerOptions`` are the only two +documented operations, and neither has a "continue" mode. The specific COM call that +triggered the pause then blocks forever, since ``Execute2`` is synchronous. Other new +COM calls still get answered while paused (Igor's command line stays reentrant), but the +original call, and anything waiting on it, is stuck for good. + +Mitigation: ``execute_igor_command_unattended`` disables the Debugger for the duration +of each call automatically. For a longer session, bracket it with +``get_debugger_state()`` / ``set_debugger_enabled(False)`` at the start and +``restore_debugger_settings()`` at the end instead. + +.. _igor_pro_bridge_compile_dialog: + +Compile-error dialogs +~~~~~~~~~~~~~~~~~~~~~~ + +Separately, a failed ``COMPILEPROCEDURES`` can leave a compile-error dialog open. This +does not hang the bridge's COM calls (they keep returning normally), but it does block +Igor's operation queue from ever draining -- confirmed from ``Advanced Topics.ihf``, +"Operation Queue": "Igor services the operation queue when no procedures are running +and the command line is empty." A modal dialog means Igor is never idle, so +``RELOAD CHANGED PROCS``/``COMPILEPROCEDURES`` queued by a later call sit there without +ever actually running -- ``reload_and_compile_procedures`` will keep reporting +"not compiled" even after the underlying ``.ipf`` file is genuinely fixed, until a +person closes that dialog by hand. + +There is no documented way to detect or dismiss this dialog via COM. When +``reload_and_compile_procedures`` times out, its result includes +``"prompt_user_to_check_for_dialog": true``; whatever is driving the bridge (e.g. an AI +agent) should use this as an explicit instruction to ask the human operator to check for +and close a stuck dialog, rather than silently retrying or only logging advisory text -- +that distinction was confirmed in practice to be what actually keeps an +agent-driven/unattended workflow moving. + +.. _igor_pro_bridge_runtime_errors: + +Igor's runtime error model (why a failure doesn't mean execution stopped) +--------------------------------------------------------------------------- + +With the Debugger disabled, an unhandled runtime error does not stop execution: it sets +Igor's internal runtime-error flag (readable via ``GetRTError(0)``, without clearing +it) and execution continues completely normally -- every subsequent line runs, +including side effects, all the way to the end of the function, unless something +explicitly checks the flag (see "Runtime Error / Abort Handling Conventions" in +:doc:`developers` for the project's ``AbortOnRTE``/``try``/``catch`` +conventions). If nothing ever checks it, the flag persists until execution unwinds all +the way back to the top-level command boundary -- i.e. this bridge's ``Execute2`` +call -- which reports it as that call's own failure, carrying the *original* error code +and message. This boundary check also clears the flag afterward, so a failure here +never contaminates the next command. + +The flag is "sticky": if two *different* unhandled runtime errors occur in sequence +with nothing checking/clearing in between, only the *first* one is ever visible -- +matching Igor's own documented caveat that ``GetErrMessage`` can be "incomplete" when +multiple errors occur. + +Practical consequence: a nonzero error code from ``execute_igor_command``/ +``execute_igor_command_unattended`` means at least one problem occurred and reports it, +but does **not** mean execution stopped there, and does **not** mean it was the only +problem. + +.. _igor_pro_bridge_claude_helper: + +MIES_ClaudeHelper.ipf and the AfterCompiledHook +------------------------------------------------- + +``Packages/MIES/MIES_ClaudeHelper.ipf``, included from ``MIES_Include.ipf``, defines: + +.. code-block:: igorpro + + static Function AfterCompiledHook() + + Variable/G root:gClaudeHelperCompileCounter + NVAR gClaudeHelperCompileCounter = root:gClaudeHelperCompileCounter + + gClaudeHelperCompileCounter += 1 + + return 0 + End + +``AfterCompiledHook`` is a predefined Igor hook that Igor calls only after *all* +procedure windows have compiled successfully. Unlike polling ``FunctionInfo()`` for a +non-existing function (which can read stale state before Igor's operation queue has +actually drained -- see :ref:`igor_pro_bridge_compile_dialog`), this counter only ever +changes at the exact moment Igor itself confirms a successful compile, so it is a +race-free confirmation signal. ``reload_and_compile_procedures`` reads a baseline before +issuing ``RELOAD CHANGED PROCS``/``COMPILEPROCEDURES`` and treats any increase as +immediate, trustworthy success, falling back to the ``FunctionInfo``-based poll when the +counter is unavailable. There is no equivalent hook for a *failed* compile. + +The whole function body is gated behind ``#ifdef IGOR_PRO_BRIDGE`` so it compiles out +entirely for a normal end-user build. To activate it, add: + +.. code-block:: igorpro + + #define IGOR_PRO_BRIDGE + +to the experiment's special "Procedure" window specifically -- Igor always compiles the +Procedure window first, so only a ``#define`` placed there is reliably visible to every +other file's ``#ifdef`` checks; a ``#define`` in an ordinary ``.ipf`` file has no such +guarantee. + +``AfterCompiledHook`` is declared ``static`` so it coexists with any other file's own +static ``AfterCompiledHook`` (e.g. the one in ``MIES_Include.ipf`` used only for the +too-old-Igor warning panel) without colliding. + +Known limitations +------------------ + +- No scriptable way to resume a Debugger pause or to detect/dismiss a compile-error + dialog -- both require a human, as described above. +- ``get_wave`` supports 1D, real-valued waves only. +- The pywin32 dynamic-dispatch calling convention for ``Execute2``'s multiple ``[out]`` + parameters is assumed to follow the standard IDispatch convention (parameters come + back as a tuple appended to the return value); this matches observed behavior in + practice. +- This is a *local* MCP server (stdio transport): it only works from a Claude Desktop + session running on the same Windows machine as Igor Pro, not from a cloud/sandboxed + session. diff --git a/Packages/doc/index.rst b/Packages/doc/index.rst index 82cfeb940d..913324d1bc 100644 --- a/Packages/doc/index.rst +++ b/Packages/doc/index.rst @@ -7,6 +7,7 @@ Table of Contents user installation developers + igor-pro-bridge reportingbugs releasenotes grouplist diff --git a/tools/igor-mcp-bridge/igor-pro-bridge-1.9.0.mcpb b/tools/igor-mcp-bridge/igor-pro-bridge-1.9.0.mcpb new file mode 100644 index 0000000000000000000000000000000000000000..aa513c374093044a0a8767053ca03797d9b0193c GIT binary patch literal 22218 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 BgbM%w literal 0 HcmV?d00001 diff --git a/tools/igor-mcp-bridge/server.py b/tools/igor-mcp-bridge/server.py new file mode 100644 index 0000000000..f0152ffbb4 --- /dev/null +++ b/tools/igor-mcp-bridge/server.py @@ -0,0 +1,1119 @@ +""" +Igor Pro MCP bridge server +========================== + +Exposes a running Igor Pro instance to Claude (or any MCP client) as a set of MCP tools, +by acting as a COM Automation *client* that talks to Igor Pro's built-in ActiveX +Automation *Server* on Windows. + +All API details below were extracted directly from the local file: + Igor Pro Folder\\Miscellaneous\\Windows Automation\\Automation Server.ihf +(WaveMetrics' own reference for this interface) during this session. Confirmed facts: + +- ProgID: "IgorPro.Application". +- Connect to an ALREADY RUNNING Igor instance with GetActiveObject (this is the Python + equivalent of the documented VB pattern `GetObject(, "IgorPro.Application")`). Using + win32com.client.Dispatch() instead would *launch* a new Igor instance, which requires + extra care (the docs warn the client must then wait for Igor to finish initializing + before calling methods) -- GetActiveObject sidesteps that entirely by only attaching to + something already running and initialized. +- Execute(BSTR cmds): fire-and-forget, raises a COM error on failure, no structured + output. +- Execute2(int flags, int codePage, BSTR cmds, int* pIgorErrorCode, BSTR* errorMsg, + BSTR* history, BSTR* results): does NOT raise a COM/Automation error just because the + Igor command itself failed -- you must check pIgorErrorCode (0 == success) yourself. + `codePage` is ignored since Igor 7 (pass 0). `results` is how you get data back: put + `fprintf 0, "..."` calls inside `cmds` and read them from `results` afterwards (this is + literally WaveMetrics' own documented example: `fprintf 0, "%g", V_avg` then read + `results`). +- IApplication.DataFolder(nameOrPath) -> IDataFolder. IDataFolder.Wave(waveNameOrPath) -> + IWave. `waveNameOrPath` may be an absolute path (e.g. "root:myFolder:myWave"), so in + practice you can anchor on "root:" and pass a full absolute path straight into .Wave(). +- IWave.GetDimensions(IgorProDataType* pDataType, long* pNumRows, long* pNumColumns, + long* pNumLayers, long* pNumChunks). +- IgorProDataType enum values (confirmed from the .ihf, exact hex values): + ipDataTypeText = 0 + ipDataTypeComplex = 0x01 (combination flag, OR'd with another value) + ipDataTypeFloat = 0x02 + ipDataTypeDouble = 0x04 + ipDataTypeSignedByte = 0x08 + ipDataTypeSignedShort = 0x10 + ipDataTypeSignedLong = 0x20 + ipDataTypeUnsignedByte = 0x48 + ipDataTypeUnsignedShort = 0x50 + ipDataTypeUnsignedLong = 0x60 + i.e. dataType == 0 means text, anything else is some numeric flavor (real-valued + numeric flavors all supported by GetNumericWavePointValue below; complex waves -- + dataType & 0x01 -- are NOT handled by this file yet, see limitation note below). +- IWave.GetNumericWavePointValue(long index, double* pValue) -- single-point numeric + read, "supports real data only" (per the docs' own wording), works for any real + numeric subtype (float/double/int/etc.), 1D waves only. +- IWave.GetTextWavePointValue(long index, int codePage, BSTR* pValue) -- single-point + text read, 1D waves only, codePage ignored since Igor 7 (pass 0). + (The docs also document GetRawTextWaveData/GetNumericWaveDataAsDouble, which pull an + entire wave at once via a SAFEARRAY, but explicitly recommend the point-value methods + "for most uses" -- and the point-value methods sidestep SAFEARRAY marshaling questions + entirely, so this file uses those instead. Whole-wave SAFEARRAY access could be added + later as a faster path for large waves.) +- **CRITICAL SETUP REQUIREMENT, confirmed verbatim from the docs**: "The Windows + operating system requires that you run the client and server (Igor) as administrator." + I.e. BOTH this Python process AND Igor Pro itself must be started as Administrator on + Windows 10+, or the COM connection will fail. This is not optional and is easy to miss. + +ONE THING THIS FILE CANNOT VERIFY FROM here (no Windows/Igor available to actually run +this): the exact Python-side calling convention pywin32's dynamic dispatch uses for +methods with multiple [out] parameters. The general IDispatch convention -- and how +win32com.client's dynamic dispatch conventionally exposes it -- is: [out]-only +parameters (not [in,out]) are NOT passed by the caller; instead they come back bundled +as a tuple appended to the method's normal return value. That is the convention this +file assumes throughout (e.g. `errorCode, errorMsg, history, results = igor.Execute2(0, +0, cmd)`). This is standard, well-established pywin32 behavior (the same pattern used +for e.g. Excel's Automation methods), not a wild guess -- but it has not been run against +the real Igor Pro COM server in this session, so treat it as the one item to confirm on +first real use. If it doesn't unpack as expected, print(repr(result)) from a raw call to +see the actual shape pywin32 returned and adjust the unpacking. + +Setup +----- + pip install mcp pywin32 + +Registering with Claude Desktop +-------------------------------- +Add to claude_desktop_config.json under "mcpServers": + + "igor-pro": { + "command": "python", + "args": ["C:\\path\\to\\server.py"] + } + +Then restart Claude Desktop. Remember: both Claude Desktop's Python process AND Igor Pro +itself need to be running elevated (as Administrator) for the COM connection to succeed. + +This is a *local* MCP server (stdio transport) -- it only works from a Claude Desktop +session running on the same Windows machine as Igor Pro, not from a cloud/Cowork sandbox. +""" + +import ctypes +import sys +import time + +import pywintypes +import win32com.client + +from mcp.server.fastmcp import FastMCP + +IGOR_COM_PROGID = "IgorPro.Application" + +# IgorProDataType enum (confirmed values, see module docstring) +IP_DATATYPE_TEXT = 0 +IP_DATATYPE_COMPLEX_FLAG = 0x01 + +mcp = FastMCP("igor-pro") + +_igor = None + + +def _is_current_process_elevated(): + """Return True/False if this Python process itself is running elevated (as + Administrator), or None if that can't be determined. + + Uses ctypes.windll.shell32.IsUserAnAdmin() -- the standard, minimal way to check + *this* process's own elevation on Windows. This is deliberately much simpler than + the OpenProcessToken/GetTokenInformation dance needed to check an *arbitrary* other + process's elevation (e.g. Igor Pro's) from outside; for our own process, this one + call is sufficient and doesn't need that machinery. + + This check exists because an elevation mismatch between this process and Igor Pro + is a real, easy-to-miss failure mode confirmed during development: Claude Desktop + can appear to be "running as Administrator" while the specific child process + running this script is not, if Claude Desktop itself was reopened normally rather + than explicitly relaunched via "Run as administrator" (Windows does not persist + elevation across relaunches by default). + """ + try: + return bool(ctypes.windll.shell32.IsUserAnAdmin()) + except Exception: + return None + + +_elevated_at_startup = _is_current_process_elevated() +if _elevated_at_startup is False: + print( + "WARNING: this MCP server process is NOT running elevated (as Administrator). " + "Igor Pro's COM Automation Server requires BOTH Igor Pro and this process to be " + "elevated, or every tool call will fail with a COM/RPC error. Relaunch Claude " + "Desktop specifically via 'Run as administrator' -- reopening it normally does " + "not preserve elevation across restarts.", + file=sys.stderr, + ) +elif _elevated_at_startup is None: + print( + "NOTE: could not determine whether this process is running elevated.", + file=sys.stderr, + ) + + +def _get_igor(force_reconnect=False): + """Attach to an already-running Igor Pro instance via COM. + + Uses GetActiveObject (not Dispatch) deliberately: GetActiveObject only attaches to + an instance that's already running and initialized, matching WaveMetrics' own + documented VB pattern `GetObject(, "IgorPro.Application")`. Dispatch() would instead + launch a brand-new Igor instance if one isn't already registered, which requires + extra initialization-wait handling this file doesn't implement. + + force_reconnect=True discards any cached connection first. Needed because this + process caches _igor for its whole lifetime (it may serve many tool calls): if Igor + Pro is closed/restarted/crashes in between, the cached COM reference goes stale and + every subsequent call fails with a COM/RPC-transport error (e.g. "The RPC server is + unavailable") -- not a normal Igor-level failure. See _run_with_reconnect below. + """ + global _igor + if force_reconnect: + _igor = None + if _igor is None: + try: + _igor = win32com.client.GetActiveObject(IGOR_COM_PROGID) + except Exception as e: + raise RuntimeError( + "Could not attach to a running Igor Pro instance via COM. Make sure: " + "(1) Igor Pro is already running, (2) BOTH Igor Pro and this Python " + "process are running as Administrator (Windows requires this for COM " + "Automation), and (3) Igor Pro 10 (or later) is installed with the " + "Automation Server component." + ) from e + return _igor + + +def _run_with_reconnect(work_fn): + """Run work_fn() once, retrying exactly once with a fresh COM connection if the + cached connection turns out to be stale. + + work_fn should call _get_igor() itself (not close over a stale `igor` variable) so + the retry actually picks up a freshly reconnected object. + + Why this is safe to do unconditionally: Execute2 reports Igor-level command + failures via pIgorErrorCode, not exceptions (see module docstring) -- so a + pywintypes.com_error escaping from here always means the COM/RPC transport itself + broke (most commonly: Igor Pro was closed or restarted since the last call, leaving + a dead reference cached), never that an Igor command merely failed. If Igor is + genuinely not reachable at all, the retry's _get_igor() call raises a plain + RuntimeError (see above), which is not caught here and propagates immediately -- + so this never turns into a silent retry loop. + """ + try: + return work_fn() + except pywintypes.com_error: + _get_igor(force_reconnect=True) + return work_fn() + + +def _get_wave_ref(wave_path: str): + """(Re)derive the IWave COM object for wave_path from the *current* Igor connection. + + This goes through DataFolder() and Wave() -- two more COM calls, same reconnect + risk as everything else here. Factored out so both the initial fetch and any + post-reconnect re-fetch (in get_wave below) call the exact same path, and never + accidentally keep using a `wave` object derived from a now-dead `igor`/`root`. + """ + igor = _get_igor() + root = igor.DataFolder("root:") + wave = root.Wave(wave_path) + if wave is None: + raise RuntimeError(f"Wave not found: {wave_path}") + return wave + + +def _read_wave_point(wave, index: int, is_text: bool): + """One point-value COM call -- GetTextWavePointValue or GetNumericWavePointValue.""" + if is_text: + return wave.GetTextWavePointValue(index, 0) + return wave.GetNumericWavePointValue(index) + + +def _execute2(command: str): + """Run `command` via Execute2 and return (errorCode, errorMsg, history, results). + + See the calling-convention caveat in the module docstring -- this unpacking is the + one thing to verify empirically on first real run. + """ + def work(): + igor = _get_igor() + return igor.Execute2(0, 0, command) + + errorCode, errorMsg, history, results = _run_with_reconnect(work) + return errorCode, errorMsg, history, results + + +# --- Igor runtime error model (how errors surface through Execute2) ----------------- +# +# Confirmed empirically this session, against a live Igor Pro instance, by +# instrumenting test functions with checkpoints (a global string variable, since +# GetRTError does not expose "where in the call did this happen", only "what/whether"): +# +# - With the Debugger disabled (the state required for unattended use -- see +# "Debugger control" below), an unhandled runtime error (e.g. indexing a wave +# reference that doesn't exist, or Make with invalid parameters) does NOT stop +# execution. It sets Igor's internal runtime-error flag (readable via GetRTError(0) +# without clearing it) and execution continues completely normally -- every +# subsequent line in the function runs, including any side effects (prints, wave +# writes, global variable assignments), all the way to the function's natural end, +# unless something explicitly checks the flag. +# - AbortOnRTE is that explicit check: placed after a command that might set the +# flag, it raises an Igor abort if the flag is set, which unwinds the *entire* +# current function immediately (nothing after it in that function runs, not even +# the rest of the function) and propagates to the nearest enclosing +# try-catch-endtry, exactly like a normal exception -- confirmed with a runtime +# error inside a *called* function: the abort skipped the rest of that function +# entirely and was caught by a try/catch in the *caller*. Nested try-catch-endtry +# behaves as expected too: an inner catch fully absorbs an abort (the outer catch +# never triggers), and a bare `Abort` (no arguments) re-raised from inside a catch +# unwinds past that catch's own endtry to the next enclosing catch, still carrying +# the original pending error code if it was only peeked (GetRTError(0)) and not +# cleared (GetRTError(1)) beforehand. +# - If nothing ever checks the flag (no AbortOnRTE, no try-catch), execution reaches +# the top-level command boundary -- i.e. this bridge's Execute2 call -- with the +# flag still set. Igor's command-line evaluator checks for this at that boundary +# and reports it as the Execute2 call's own failure (pIgorErrorCode/errorMsg), +# confirmed to carry the *original* error code and message, not a generic one. +# This boundary check also clears the flag afterward -- confirmed by checking +# GetRTError(0) in a completely separate subsequent call and seeing 0 -- so a +# failure here never contaminates the next command. +# - The flag is "sticky": if two *different* unhandled runtime errors occur in +# sequence with nothing checking/clearing in between, GetRTError keeps reporting +# only the *first* one throughout -- confirmed by triggering two distinct errors +# (a null-wave read, then an invalid Make) and seeing the reported code/message +# stay fixed at the first error the whole time, including in the final Execute2 +# result. This matches Igor's own documented caveat that GetErrMessage can be +# "incomplete" when multiple errors occur. +# +# Net effect for this bridge: a successful (errorCode 0) unattended call is a +# reliable clean signal -- the boundary check guarantees no lingering error. A +# failed call reliably reports the *first* unhandled runtime error's code and +# message, but does NOT mean execution stopped there -- everything before and after +# it in the procedure code likely still ran to completion -- and does NOT mean it +# was the *only* problem, since any later distinct error would be silently masked +# by the same stuck flag. Because of this, _format_execute2_error below includes +# whatever `results` (fprintf output) was captured, not just the error itself -- +# that's often the only way to tell how far execution actually got. + + +def _format_execute2_error( + command: str, errorCode: int, errorMsg: str, results: str, history: str = "" +) -> str: + """Build the message for a RuntimeError raised after a failed Execute2 call, + including any partial `results` (fprintf output) and/or `history` captured + before/around the error -- see the runtime error model notes above for why that + matters: the procedure code very likely kept running after the error, so there + may be diagnostic output that would otherwise be silently discarded. + + DIAGNOSTIC NOTE (temporary, being verified live): confirmed empirically that + Igor's Execute2 returns an EMPTY `results` string whenever pIgorErrorCode is + nonzero, even when an fprintf 0, ... earlier in the same command definitely ran + (confirmed via a separate global-variable checkpoint) -- so `results` alone does + NOT recover anything in the common case of a single top-level command/function + call failing. Including `history` here as well to check whether it fares better.""" + parts = [ + f"Igor command failed (error code {errorCode}): {errorMsg or '(no error message)'}" + ] + if results: + parts.append(f"Partial results captured before/around the error: {results!r}") + if history: + parts.append(f"History captured for this call: {history!r}") + parts.append(f"Command was: {command}") + return "\n".join(parts) + + +@mcp.tool() +def execute_igor_command(command: str) -> str: + """Execute a single Igor Pro command string in the running Igor instance. + + To get data back (not just run a command for its side effect), include an + `fprintf 0, "..."` call in `command` -- its output is captured and returned. + + Example: execute_igor_command('WaveStats/Q jack; fprintf 0, "%g", V_avg') + + **Caution:** if `command` calls user-defined procedure code (e.g. a MIES or test + function) and Igor Pro's Debugger is currently enabled, a breakpoint/runtime + error/abort/stale-reference pause in that code will hang this call indefinitely -- + there is no scriptable way to resume or dismiss the Debugger window (see + set_debugger_enabled's docstring). This happened for real during development. + Whenever nobody is watching who could close that popup manually, use + execute_igor_command_unattended instead, which disables the Debugger for the + duration of the call automatically. Only use this plain version when you + deliberately want the Debugger available (e.g. interactively testing a + breakpoint). + + **On failure:** a nonzero error code means at least one unhandled runtime error + occurred somewhere in `command` -- it does NOT mean execution stopped there, and + it does NOT mean it was the only problem (see the runtime error model notes + above `_format_execute2_error`). The raised error includes any partial `results` + captured, since that's often the only way to tell how far execution actually got. + """ + errorCode, errorMsg, history, results = _execute2(command) + if errorCode != 0: + raise RuntimeError( + _format_execute2_error(command, errorCode, errorMsg, results, history) + ) + return results + + +@mcp.tool() +def get_wave(wave_path: str) -> list: + """Return the data of an existing 1D Igor wave as a list of numbers or strings. + + wave_path should be an absolute Igor path, e.g. "root:testWave" or + "root:myFolder:testWave". + + Limitation: only 1D, real (non-complex) waves are supported. Multi-dimensional or + complex waves will raise an error. + + Every COM call below is individually reconnect-protected (DataFolder/Wave/ + GetDimensions as one unit via _run_with_reconnect, then each point read + separately) rather than only wrapping the function as a whole. The point-level + wrapping matters for large waves specifically: if the connection drops on point + 4000 of 5000, this resumes from point 4000 after reconnecting instead of + re-fetching the wave and re-reading points 0-3999 again. + """ + def get_dims(): + return _get_wave_ref(wave_path).GetDimensions() + + dataType, numRows, numCols, numLayers, numChunks = _run_with_reconnect(get_dims) + + if numCols or numLayers or numChunks: + raise RuntimeError( + f"{wave_path} is not 1D (dims: rows={numRows}, cols={numCols}, " + f"layers={numLayers}, chunks={numChunks}) -- only 1D waves are supported." + ) + if dataType & IP_DATATYPE_COMPLEX_FLAG: + raise RuntimeError(f"{wave_path} is complex-valued -- not supported yet.") + + is_text = dataType == IP_DATATYPE_TEXT + wave = _get_wave_ref(wave_path) + values = [] + for i in range(numRows): + try: + values.append(_read_wave_point(wave, i, is_text)) + except pywintypes.com_error: + _get_igor(force_reconnect=True) + wave = _get_wave_ref(wave_path) + values.append(_read_wave_point(wave, i, is_text)) + return values + + +@mcp.tool() +def check_bridge_health() -> dict: + """Check whether the Igor Pro bridge is actually able to reach Igor Pro right now, + and report exactly which requirement is unmet if not. + + Call this first whenever a command fails or behaves unexpectedly. This session's + own debugging hit three distinct failure modes that all needed different fixes: + (1) this Python process not running elevated, (2) no Igor Pro COM object + registered at all (Igor not running), and (3) a registered-but-dead COM object + (Igor crashed/was force-closed, leaving a stale registration that reconnecting + alone can't fix -- Igor itself needs relaunching). This check distinguishes all + three rather than surfacing one generic failure. + + Returns a dict with at least a "status" key ("OK" or "FAIL") and, on FAIL, a + "problem" key with a specific, actionable description. + """ + report = {"python_process_elevated": _is_current_process_elevated()} + + if report["python_process_elevated"] is False: + report["status"] = "FAIL" + report["problem"] = ( + "This Python process is not running elevated (as Administrator). Igor " + "Pro's COM Automation Server requires both Igor Pro and this process to " + "be elevated. Relaunch Claude Desktop specifically via 'Run as " + "administrator' -- reopening it normally does not preserve elevation -- " + "then retry." + ) + return report + + try: + _get_igor() + except RuntimeError as e: + report["status"] = "FAIL" + report["problem"] = ( + f"No running Igor Pro instance found via COM ({e}). Make sure Igor Pro 10 " + "or later is open and running elevated." + ) + return report + + def try_call(): + igor = _get_igor() + return igor.Execute2(0, 0, 'fprintf 0, "%s", IgorInfo(1)') + + try: + errorCode, errorMsg, history, results = try_call() + report["reconnect_was_needed"] = False + except pywintypes.com_error: + report["reconnect_was_needed"] = True + try: + _get_igor(force_reconnect=True) + errorCode, errorMsg, history, results = try_call() + except pywintypes.com_error as e2: + report["status"] = "FAIL" + report["problem"] = ( + f"Found a registered Igor Pro COM object, but calls to it fail with a " + f"COM/RPC-transport error even after reconnecting ({e2}). This means a " + "stale/dead COM registration, most likely because Igor Pro crashed or " + "was force-closed previously. Check Task Manager for Igor64.exe -- " + "there should be exactly one -- fully close it, and relaunch Igor Pro " + "fresh, as Administrator." + ) + return report + + if errorCode != 0: + report["status"] = "FAIL" + report["problem"] = f"Igor-level command failed (code {errorCode}): {errorMsg}" + return report + + report["status"] = "OK" + report["igor_info"] = results + return report + + +# Confirmed against a live Igor Pro instance during development: this is exactly the +# method used by IsProcGlobalCompiled() in +# Packages/igortest/procedures/igortest-test-compilation.ipf. FunctionInfo() for a +# deliberately non-existing function returns an empty string when procedure code is +# compiled, and a non-empty string (observed: "Procedures Not Compiled") when it is +# not. The expression is inlined directly into the fprintf call (no intermediate +# variable) deliberately: an earlier version assigned to a local first, but Igor's +# command line persists local variables across separate command-line invocations, so +# a *second* call declaring the same variable name again failed with "the name +# already exists as a variable" -- confirmed empirically. Inlining the expression +# sidesteps that entirely, since there is no variable to persist or collide with. +_PROCEDURES_COMPILED_CHECK_CMD = ( + 'fprintf 0, "%s", FunctionInfo("ProcGlobal#NON_EXISTING_FUNCTION")' +) + + +@mcp.tool() +def check_compilation_state() -> dict: + """Check whether Igor Pro's procedure code is currently compiled or uncompiled. + + Functions from procedure code can only be called while compiled. Igor Pro enters + the uncompiled state when procedure code is edited inside Igor (only possible + while nothing is running), or when nothing is running and an included procedure + file changed on disk. Use reload_and_compile_procedures to get back to compiled + after editing a .ipf file on disk. + """ + errorCode, errorMsg, history, results = _execute2(_PROCEDURES_COMPILED_CHECK_CMD) + if errorCode != 0: + raise RuntimeError( + f"Could not check compilation state (error code {errorCode}): {errorMsg}" + ) + return {"compiled": results == "", "raw_function_info": results} + + +_COMPILE_POLL_INTERVAL_SECONDS = 0.2 +_COMPILE_POLL_TIMEOUT_SECONDS = 5.0 +# Number of consecutive "compiled" reads required before trusting the FunctionInfo-based +# fallback signal -- see the false-positive race explained in +# reload_and_compile_procedures's docstring. Not needed for the AfterCompiledHook-based +# counter signal, which is race-free by construction (see _read_claude_helper_compile_counter). +_COMPILE_CONFIRM_CHECKS = 2 + +# MIES_ClaudeHelper.ipf's AfterCompiledHook (gated behind #ifdef IGOR_PRO_BRIDGE -- see +# SESSION_NOTES.md) increments root:gClaudeHelperCompileCounter every time Igor calls it, +# which only happens once ALL procedure windows have genuinely compiled successfully +# (confirmed from Igor Pro Folder/Igor Help Files/Advanced Topics.ihf). Unlike the +# FunctionInfo-based poll below, there is no staleness/race concern reading this: the +# counter only ever changes at the exact moment Igor itself confirms a successful +# compile, so any observed increase over a baseline is unconditionally trustworthy, no +# repeated-confirmation dance required. NumVarOrDefault's own -1 default is used as the +# "unavailable" sentinel (a real counter value can never be negative), which handles two +# unavailability cases identically: IGOR_PRO_BRIDGE not defined for this experiment (the +# hook doesn't exist at all), or MIES_ClaudeHelper.ipf not included in the first place -- +# this bridge has to keep working either way, so the counter is only ever an optional +# extra confirmation, never a requirement. +_CLAUDE_HELPER_COMPILE_COUNTER_CMD = ( + 'fprintf 0, "%g", NumVarOrDefault("root:gClaudeHelperCompileCounter", -1)' +) + + +def _read_claude_helper_compile_counter(): + """Read root:gClaudeHelperCompileCounter, or None if it's unavailable for any reason + (COM/Igor-level error, or the sentinel -1 meaning the variable doesn't exist -- see + the constant's comment above for why both are treated as simply "unavailable", never + fatal).""" + errorCode, errorMsg, history, results = _execute2(_CLAUDE_HELPER_COMPILE_COUNTER_CMD) + if errorCode != 0: + return None + try: + value = float(results) + except (TypeError, ValueError): + return None + return None if value < 0 else value + +_COMPILE_ERROR_DIALOG_NOTE = ( + "One confirmed cause if this is unexpected (e.g. you just fixed a known syntax " + "error and expected this to succeed): a compile-error dialog left open in Igor " + "from a PREVIOUS failed attempt blocks Igor's operation queue from ever draining " + "-- confirmed from Igor Pro Folder/Igor Help Files/Advanced Topics.ihf, " + "'Operation Queue' section: 'Igor services the operation queue when no " + "procedures are running and the command line is empty.' A modal dialog means " + "Igor is never idle, so RELOAD CHANGED PROCS/COMPILEPROCEDURES queued by a later " + "call sit there without ever actually running, even though this bridge's own COM " + "calls keep responding normally throughout (confirmed empirically: this hang " + "does not show up as a hung tool call, only as 'compiled' staying stuck at False " + "no matter how many times this is retried). There is no documented way to " + "detect or dismiss that dialog via COM. " + "ACTION FOR WHATEVER IS CALLING THIS TOOL: do not just log this and retry silently " + "-- explicitly ask the human operator right now whether a compile-error dialog is " + "showing in Igor Pro, and if so, to close it, before retrying. This was confirmed " + "during development to be the only thing that reliably un-sticks this state -- " + "passively worded advice in a note is easy to skip past; an explicit prompt to the " + "human is what actually keeps an unattended/agent-driven workflow moving." +) + + +@mcp.tool() +def reload_and_compile_procedures() -> dict: + """Force Igor Pro to reload procedure code from the .ipf files on disk and attempt + a fresh compilation, then report whether it ended up compiled. + + Use this after editing a .ipf file directly on disk -- the correct way to change + MIES/Igor procedure code -- to make Igor pick up the change. Only call this while + Igor Pro is not currently running other procedure code; reloading/compiling while + code is running is not supported. + + Mirrors the exact method used by CompileAndRestart() in igortest-tracing.ipf: + + Execute/P "RELOAD CHANGED PROCS " + Execute/P "COMPILEPROCEDURES " + + Both commands go through Igor's operation queue, not immediate execution -- + confirmed from Igor Pro Folder/Igor Help Files/Advanced Topics.ihf, "Operation + Queue" section (COMPILEPROCEDURES and RELOAD CHANGED PROCS are documented there; + neither has its own entry in the main Igor Reference): "Igor services the + operation queue when no procedures are running and the command line is empty. If + the operation queue is not empty, Igor then executes the oldest command in the + queue." /P only appends to that queue -- it does NOT guarantee either command has + actually run by the time this function starts checking, only that Igor will get + to it once genuinely idle. + + Because of that, a single immediate compiled-state check was observed (against a + live Igor Pro instance) to occasionally report "compiled: true" on the very + first read even though the queue had not drained yet -- a false positive, reading + stale pre-reload state rather than the real post-compile result -- as well as the + opposite false negative (briefly still "not compiled" right after a compile that + actually succeeded). To guard against both, this checks two independent signals on + every poll (every 0.2s, up to 5s total): + + 1. root:gClaudeHelperCompileCounter, incremented by MIES_ClaudeHelper.ipf's + AfterCompiledHook (see _read_claude_helper_compile_counter) -- authoritative and + race-free when available (requires #define IGOR_PRO_BRIDGE in the experiment's + Procedure window), since it only changes at the exact moment Igor itself confirms + a successful compile. Any increase over the baseline read before issuing RELOAD/ + COMPILE is trusted immediately, no repeated confirmation needed. + 2. The original FunctionInfo-based check_compilation_state poll, kept as a fallback + for when the counter is unavailable (IGOR_PRO_BRIDGE not defined, or + MIES_ClaudeHelper.ipf not included at all) -- still requires + _COMPILE_CONFIRM_CHECKS consecutive "compiled" reads in a row before trusting it, + since this signal alone doesn't rule out the staleness race described above. + + If neither signal confirms compilation after the full 5s, that's a much stronger + signal of a genuine compile error -- check Igor's history/procedure window + directly. See _COMPILE_ERROR_DIALOG_NOTE for one confirmed, concrete cause: a + compile-error dialog left open from an earlier failed attempt blocks the + operation queue from ever draining, so this will keep reporting "not compiled" + even after the underlying .ipf file is genuinely fixed, until a person closes + that dialog by hand. This happened for real during development. + + **If the returned dict has "prompt_user_to_check_for_dialog": True, whatever is + calling this tool should explicitly ask the human operator to check Igor Pro's + screen for a stuck compile-error dialog and close it, before retrying** -- not + just read the accompanying "note" text and move on. Confirmed directly during + development: silently retrying or only logging the note left the workflow stuck; + explicitly prompting the human at this point is what actually un-stuck it. + """ + baseline_counter = _read_claude_helper_compile_counter() + + errorCode, errorMsg, history, results = _execute2('Execute/P "RELOAD CHANGED PROCS "') + if errorCode != 0: + raise RuntimeError( + f"RELOAD CHANGED PROCS failed (error code {errorCode}): {errorMsg}" + ) + + errorCode, errorMsg, history, results = _execute2('Execute/P "COMPILEPROCEDURES "') + if errorCode != 0: + raise RuntimeError( + f"COMPILEPROCEDURES failed (error code {errorCode}): {errorMsg}" + ) + + deadline = time.monotonic() + _COMPILE_POLL_TIMEOUT_SECONDS + lastErrorCode = None + lastErrorMsg = None + lastResults = None + attempts = 0 + consecutive_compiled = 0 + + while True: + attempts += 1 + + current_counter = _read_claude_helper_compile_counter() + if ( + baseline_counter is not None + and current_counter is not None + and current_counter > baseline_counter + ): + return { + "reload_triggered": True, + "compile_triggered": True, + "compiled": True, + "poll_attempts": attempts, + "confirmed_via": "AfterCompiledHook counter (root:gClaudeHelperCompileCounter)", + } + + compiledErrorCode, compiledErrorMsg, _, compiledResults = _execute2( + _PROCEDURES_COMPILED_CHECK_CMD + ) + if compiledErrorCode == 0: + lastErrorCode = None + lastResults = compiledResults + if compiledResults == "": + consecutive_compiled += 1 + if consecutive_compiled >= _COMPILE_CONFIRM_CHECKS: + return { + "reload_triggered": True, + "compile_triggered": True, + "compiled": True, + "poll_attempts": attempts, + "confirmed_via": ( + "FunctionInfo poll (AfterCompiledHook counter unavailable " + "or unchanged)" + ), + } + else: + consecutive_compiled = 0 + else: + lastErrorCode, lastErrorMsg = compiledErrorCode, compiledErrorMsg + consecutive_compiled = 0 + + if time.monotonic() >= deadline: + break + time.sleep(_COMPILE_POLL_INTERVAL_SECONDS) + + if lastErrorCode is not None: + return { + "reload_triggered": True, + "compile_triggered": True, + "compiled_state_known": False, + "poll_attempts": attempts, + "prompt_user_to_check_for_dialog": True, + "note": ( + f"Reload/compile commands ran, but checking the resulting state kept " + f"failing (last error code {lastErrorCode}): {lastErrorMsg}. " + + _COMPILE_ERROR_DIALOG_NOTE + ), + } + + return { + "reload_triggered": True, + "compile_triggered": True, + "compiled": False, + "poll_attempts": attempts, + "raw_function_info": lastResults, + "prompt_user_to_check_for_dialog": True, + "note": ( + f"Still not compiled after polling for {_COMPILE_POLL_TIMEOUT_SECONDS:.0f}s " + f"(requiring {_COMPILE_CONFIRM_CHECKS} consecutive confirmations). This is " + "more likely a genuine compile error in the procedure code than a timing " + "artifact -- check Igor's history/procedure window directly. " + + _COMPILE_ERROR_DIALOG_NOTE + ), + } + + +# --- Debugger control --------------------------------------------------------------- +# +# Confirmed against a live Igor Pro instance during development, and against Igor +# Reference.ihf / Debugging.ihf directly (not guessed): +# +# - DebuggerOptions [enable=en, debugOnAbort=doa, debugOnError=doe, +# NVAR_SVAR_WAVE_Checking=nvwc] is the only operation that changes debugger settings. +# All parameters are optional; calling it with none just (re)sets its V_enable / +# V_debugOnError / V_debugOnAbort / V_NVAR_SVAR_WAVE_Checking output variables to the +# current state without changing anything -- confirmed verbatim from the docs: "All +# parameters are optional. If none are specified, no action is taken, but the output +# variables are still set." Multiple keyword arguments are comma-separated, confirmed +# from a real doc example: "DebuggerOptions enable=1, debugOnError=1". +# - "If the debugger is disabled then the other settings are cleared even if other +# settings are on" (verbatim from the docs) -- so enable=0 always clears everything. +# - **Why this matters for unattended/automated use, confirmed empirically this +# session**: there is no scriptable/COM way to resume, step, or dismiss the Debugger +# window once something pauses it (no such operation exists in Igor Reference.ihf, +# and the Debugger panel itself doesn't even show up as a window in +# WinList("*", ";", "WIN:65535")). If a breakpoint, a runtime error (debugOnError), +# a user abort (debugOnAbort), or a stale NVAR/SVAR/WAVE reference +# (NVAR_SVAR_WAVE_Checking) trips the debugger during an automated run, the specific +# COM call that triggered it hangs forever -- Execute2 is synchronous, and only a +# human clicking "Go" in the Debugger window can unblock it. (Other new COM calls +# still get answered while paused, since Igor's command line stays reentrant -- but +# the original call, and anything waiting on it, is stuck for good.) So: the debugger +# must be disabled before any unattended/automated session. +# - **This is not hypothetical -- it happened during development of this bridge**: a +# plain execute_igor_command call ran a test function while the Debugger was still +# enabled from earlier interactive use, Igor paused with the Debugger window open, +# and the call hung until a person closed the window by hand. That's exactly why +# execute_igor_command_unattended exists below: it disables the Debugger, runs the +# command, and restores the Debugger afterward automatically (in a try/finally, so +# it restores even if the command errors), rather than depending on whoever/whatever +# is calling this bridge to remember the manual get_debugger_state() / +# set_debugger_enabled(False) / restore_debugger_settings() dance every time. Use +# execute_igor_command_unattended by default for anything that might call +# user-defined procedure code unattended; reach for plain execute_igor_command only +# when a Debugger pause is deliberately wanted (e.g. interactively testing a +# breakpoint). + +_DEBUGGER_STATE_CHECK_CMD = ( + 'DebuggerOptions; fprintf 0, "enable=%d,debugOnError=%d,debugOnAbort=%d,' + 'NVAR_SVAR_WAVE_Checking=%d", V_enable, V_debugOnError, V_debugOnAbort, ' + "V_NVAR_SVAR_WAVE_Checking" +) + +# Snapshot captured by get_debugger_state(), consumed by restore_debugger_settings(). +# Process-lifetime state is fine here: one bridge process serves one Claude Desktop +# session, and this is meant to bracket exactly one unattended run within that. +_saved_debugger_settings = None + + +def _read_debugger_options() -> dict: + """Read the four DebuggerOptions settings without changing them. Returns + {"enable": bool, "debug_on_error": bool, "debug_on_abort": bool, + "nvar_svar_wave_checking": bool}.""" + errorCode, errorMsg, history, results = _execute2(_DEBUGGER_STATE_CHECK_CMD) + if errorCode != 0: + raise RuntimeError( + f"Could not read Debugger settings (error code {errorCode}): " + f"{errorMsg or '(no error message)'}" + ) + values = {} + for pair in results.split(","): + key, _, value = pair.partition("=") + values[key] = value + return { + "enable": values.get("enable") == "1", + "debug_on_error": values.get("debugOnError") == "1", + "debug_on_abort": values.get("debugOnAbort") == "1", + "nvar_svar_wave_checking": values.get("NVAR_SVAR_WAVE_Checking") == "1", + } + + +def _apply_debugger_options(state: dict): + """Issue a DebuggerOptions command that sets all four settings to `state` + (enable/debug_on_error/debug_on_abort/nvar_svar_wave_checking), used by both + set_debugger_enabled and restore_debugger_settings so they can't drift apart.""" + parts = [f"enable={1 if state['enable'] else 0}"] + if state["enable"]: + parts.append(f"debugOnError={1 if state['debug_on_error'] else 0}") + parts.append(f"debugOnAbort={1 if state['debug_on_abort'] else 0}") + parts.append( + f"NVAR_SVAR_WAVE_Checking={1 if state['nvar_svar_wave_checking'] else 0}" + ) + cmd = "DebuggerOptions " + ", ".join(parts) + + errorCode, errorMsg, history, results = _execute2(cmd) + if errorCode != 0: + raise RuntimeError( + f"Could not set Debugger settings (error code {errorCode}): " + f"{errorMsg or '(no error message)'}\nCommand was: {cmd}" + ) + + +@mcp.tool() +def get_debugger_state() -> dict: + """Read Igor Pro's current Debugger settings (enable, debugOnError, debugOnAbort, + NVAR_SVAR_WAVE_Checking) without changing them, and save a snapshot inside this + bridge process for restore_debugger_settings to restore later. + + **Call this before starting any unattended/automated session**, immediately before + calling set_debugger_enabled(False) to actually turn the debugger off. See + set_debugger_enabled's docstring for why the debugger must be off for unattended + execution -- a pause it causes cannot be resumed or dismissed remotely. + """ + global _saved_debugger_settings + state = _read_debugger_options() + _saved_debugger_settings = dict(state) + return state + + +@mcp.tool() +def set_debugger_enabled( + enabled: bool, + debug_on_error: bool = None, + debug_on_abort: bool = None, + nvar_svar_wave_checking: bool = None, +) -> dict: + """Turn Igor Pro's Debugger on or off (and optionally its debugOnError/ + debugOnAbort/NVAR_SVAR_WAVE_Checking sub-settings), via DebuggerOptions. + + **For any unattended/automated session -- running tests, scripted builds, anything + without a person watching -- the debugger MUST be disabled: call + set_debugger_enabled(False) before starting.** Confirmed empirically this session: + there is no scriptable/COM way to resume, step, or dismiss the Debugger window once + something pauses it (no such operation is documented in Igor Reference.ihf, and the + Debugger panel doesn't even appear as a window in WinList). If a breakpoint, a + runtime error (debugOnError), a user abort (debugOnAbort), or a stale NVAR/SVAR/WAVE + reference (NVAR_SVAR_WAVE_Checking) trips the debugger mid-run, the specific COM + call that triggered it hangs forever -- Execute2 is synchronous, and only a human + clicking "Go" in the Debugger window can unblock it. Other new COM calls still get + answered while paused (Igor's command line stays reentrant), but the original call, + and anything waiting on it, is stuck for good. + + enabled=False clears all four settings regardless of the other arguments -- this is + Igor's own documented behavior ("If the debugger is disabled then the other + settings are cleared even if other settings are on"), not a limitation of this + function -- so debug_on_error/debug_on_abort/nvar_svar_wave_checking are only + applied when enabled=True. + + Recommended pattern around an unattended session: + get_debugger_state() # read + save the current settings + set_debugger_enabled(False) # disable for the unattended run + ... run the unattended session ... + restore_debugger_settings() # put the saved settings back + """ + _apply_debugger_options( + { + "enable": enabled, + "debug_on_error": bool(debug_on_error), + "debug_on_abort": bool(debug_on_abort), + "nvar_svar_wave_checking": bool(nvar_svar_wave_checking), + } + ) + return _read_debugger_options() + + +@mcp.tool() +def restore_debugger_settings() -> dict: + """Restore Igor Pro's Debugger settings to whatever get_debugger_state last + captured. + + **Call this when an unattended/automated session ends**, to put the debugger back + the way it was before set_debugger_enabled(False) turned it off for the run. + + Raises if get_debugger_state was never called in this bridge process (nothing has + been saved to restore). + """ + if _saved_debugger_settings is None: + raise RuntimeError( + "No saved Debugger settings to restore -- call get_debugger_state() " + "before starting the unattended session so there is something to restore " + "afterward." + ) + _apply_debugger_options(_saved_debugger_settings) + return _read_debugger_options() + + +@mcp.tool() +def execute_igor_command_unattended(command: str) -> str: + """Run `command` exactly like execute_igor_command, but automatically disable + Igor's Debugger before running it and restore it again afterward -- even if + `command` raises an Igor-level error. Uses its own local snapshot rather than the + get_debugger_state/restore_debugger_settings pair, so it's self-contained and + won't clash with a separate manual bracket around a longer session. + + **This is the tool to reach for whenever `command` might call user-defined + procedure code (e.g. any MIES or test function) and nothing is watching that could + close a Debugger popup by hand.** It exists because of a concrete failure hit + during development of this bridge: a plain execute_igor_command call ran a test + function while the Debugger was still enabled from earlier interactive use; Igor + Pro paused with the Debugger window open, and the call hung until a person closed + it manually -- there is no scriptable way to resume or dismiss a Debugger pause + (see set_debugger_enabled's docstring for the full explanation of why). Wrapping + the call so the Debugger is guaranteed off first removes that failure mode + entirely, instead of relying on remembering to call set_debugger_enabled(False) + beforehand every time. + + Only reach for plain execute_igor_command when you deliberately want the Debugger + available -- e.g. interactively testing a breakpoint, as done earlier in this + bridge's own development. + + For a longer unattended session made of many calls, prefer bracketing the whole + session with get_debugger_state() / set_debugger_enabled(False) once at the start + and restore_debugger_settings() once at the end, rather than paying the extra + disable/restore COM round-trip on every single command via this tool. + + **On failure:** a nonzero error code means at least one unhandled runtime error + occurred somewhere in `command` -- it does NOT mean execution stopped there, and + it does NOT mean it was the only problem (see the runtime error model notes + above _format_execute2_error, right before execute_igor_command). The raised + error includes any partial `results` captured, since that's often the only way + to tell how far execution actually got. + """ + saved = _read_debugger_options() + _apply_debugger_options( + { + "enable": False, + "debug_on_error": False, + "debug_on_abort": False, + "nvar_svar_wave_checking": False, + } + ) + try: + errorCode, errorMsg, history, results = _execute2(command) + finally: + _apply_debugger_options(saved) + + if errorCode != 0: + raise RuntimeError( + _format_execute2_error(command, errorCode, errorMsg, results, history) + ) + return results + + +# --- Environment summary ----------------------------------------------------------- +# +# Confirmed against a live Igor Pro instance during development (Igor Pro 10.03, build +# 30115). These are ordinary Igor built-in functions -- not part of the COM Automation +# Server API itself -- run the same way as any other command, via _execute2/fprintf: +# +# - IgorInfo(n) for n in 0-18 (n outside that range raises an Igor-level error, e.g. +# "expected value between 0 and 18"). The indices used below were identified +# empirically by probing all valid values against a live instance and matching each +# returned string to its evident meaning -- there is no single confirmed index for +# "the experiment's name", for example, so this was found by inspection, not assumed: +# IgorInfo(0) -- system report string (IGORVERS/BUILD/COMMIT/memory/screen info) +# IgorInfo(3) -- OS name/version/locale string +# IgorInfo(10) -- semicolon-separated list of loaded XOPs +# IgorInfo(11) -- experiment file kind (e.g. "Packed") +# IgorInfo(12) -- experiment file name (e.g. "Basic.pxp") +# - WinList("*", ";", "WIN:128") -- lists currently included procedure windows/files. +# The "128" bit was confirmed empirically (tested directly against a live instance, +# not looked up in documentation) to mean "procedure windows"; it reliably returns a +# complete, sensible-looking list of every included .ipf plus the special "Procedure" +# window (see below). +# - ProcedureText(macroOrFunctionNameStr, flags, winTitleStr) -- retrieves procedure +# text. IMPORTANT, confirmed the hard way this session: to get the *entire contents* +# of a named procedure window, the window name goes in winTitleStr (the third +# argument), with macroOrFunctionNameStr left as "" -- i.e. +# ProcedureText("", 0, "Procedure"), NOT ProcedureText("Procedure", 0, ""). The first +# argument instead names one specific macro/function *within* a window; passing a +# window name there matches nothing and silently returns "" rather than raising an +# error, which produced an incorrect "the Procedure window is empty" result during +# development until the user caught and corrected it. +# - The always-present "Procedure" window matters because Igor experiments (.pxp) can +# carry additional #include/#define directives there beyond what's in any on-disk +# .ipf file in the repo -- e.g. this project's experiments were found to #include +# ":UTF_Basic" and #define AUTOMATED_TESTING directly in that window. So the live +# in-memory environment is experiment-dependent, not fully determined by the repo +# file system alone. +# - DataFolderDir(3) -- returns "FOLDERS:name1,name2,...;WAVES:name1,name2,...;" +# (bitmask 3 = folders + waves) for the current data folder; confirmed empirically +# against root: to list top-level data folders and top-level waves. + +_ENV_SUMMARY_COMMANDS = { + "igor_version_info": 'fprintf 0, "%s", IgorInfo(0)', + "os_info": 'fprintf 0, "%s", IgorInfo(3)', + "loaded_xops_raw": 'fprintf 0, "%s", IgorInfo(10)', + "experiment_file_kind": 'fprintf 0, "%s", IgorInfo(11)', + "experiment_file_name": 'fprintf 0, "%s", IgorInfo(12)', + "included_procedure_windows_raw": 'fprintf 0, "%s", WinList("*", ";", "WIN:128")', + "data_folders_raw": 'fprintf 0, "%s", DataFolderDir(3)', + "procedure_window_text": 'fprintf 0, "%s", ProcedureText("", 0, "Procedure")', +} + + +def _categorize_procedure_file(name: str) -> str: + """Bucket an included procedure file name into a coarse category, purely to make a + ~250-entry file list skimmable in a summary. Buckets reflect this specific repo's + naming conventions (MIES_*, UTF_* unit tests, igortest-* test framework, IPNWB_*), + not a general Igor Pro convention.""" + if name.startswith("igortest"): + return "igortest_framework" + if name.startswith("UTF_"): + return "unit_tests" + if name.startswith("IPNWB"): + return "ipnwb" + if name.startswith("MIES_"): + return "mies_production" + return "other" + + +@mcp.tool() +def get_environment_summary() -> dict: + """Summarize the current Igor Pro instance's live environment: Igor version, the + loaded experiment, loaded external operations (XOPs), which procedure files are + actually included right now, the contents of the always-present "Procedure" window + (which can carry experiment-specific #include/#define directives not present in any + on-disk .ipf file), and the top-level global data folder layout. + + This queries the live instance directly rather than assuming the repo's file system + determines what's loaded -- which experiment (.pxp) is open changes all of this. + + Returns a dict with: + - igor_version_info: raw IgorInfo(0) string (version/build/commit/memory/screen) + - os_info: raw IgorInfo(3) string (OS name/version/locale) + - experiment_file_name / experiment_file_kind: e.g. "Basic.pxp" / "Packed" + - loaded_xops: list of loaded external operations (e.g. NIDAQmx64, itcXOP2-64) + - procedure_window_text: raw contents of the special "Procedure" window -- + inspect this for experiment-specific #include/#define directives + - included_procedure_file_count: total number of currently included .ipf files + (excluding the "Procedure" window entry itself) + - included_procedure_files_by_category: counts per category (see + _categorize_procedure_file) + - included_procedure_files: the full list of currently included .ipf file names + - data_folders: top-level data folder names under root: + - top_level_waves: top-level wave names directly under root: (usually empty -- + MIES keeps its data organized into subfolders) + - debugger_settings: current enable/debugOnError/debugOnAbort/ + NVAR_SVAR_WAVE_Checking state (see _read_debugger_options). If "enable" is + True here, any unattended/automated session must call + get_debugger_state() + set_debugger_enabled(False) first -- see + set_debugger_enabled's docstring for why. + """ + raw = {} + for key, cmd in _ENV_SUMMARY_COMMANDS.items(): + errorCode, errorMsg, history, results = _execute2(cmd) + if errorCode != 0: + raise RuntimeError( + f"Could not retrieve '{key}' (error code {errorCode}): " + f"{errorMsg or '(no error message)'}\nCommand was: {cmd}" + ) + raw[key] = results + + included_procedure_files = [ + name for name in raw["included_procedure_windows_raw"].split(";") if name + ] + included_procedure_files = [ + name for name in included_procedure_files if name != "Procedure" + ] + + loaded_xops = [x for x in raw["loaded_xops_raw"].split(";") if x] + + folders_part, waves_part = "", "" + for part in raw["data_folders_raw"].split("\r"): + part = part.strip() + if part.startswith("FOLDERS:"): + folders_part = part[len("FOLDERS:"):].rstrip(";") + elif part.startswith("WAVES:"): + waves_part = part[len("WAVES:"):].rstrip(";") + data_folders = [f for f in folders_part.split(",") if f] + top_level_waves = [w for w in waves_part.split(",") if w] + + category_counts: dict = {} + for name in included_procedure_files: + category = _categorize_procedure_file(name) + category_counts[category] = category_counts.get(category, 0) + 1 + + return { + "igor_version_info": raw["igor_version_info"], + "os_info": raw["os_info"], + "experiment_file_name": raw["experiment_file_name"], + "experiment_file_kind": raw["experiment_file_kind"], + "loaded_xops": loaded_xops, + "procedure_window_text": raw["procedure_window_text"], + "included_procedure_file_count": len(included_procedure_files), + "included_procedure_files_by_category": category_counts, + "included_procedure_files": included_procedure_files, + "data_folders": data_folders, + "top_level_waves": top_level_waves, + "debugger_settings": _read_debugger_options(), + } + + +if __name__ == "__main__": + mcp.run() From 775be406fe18236da2ba06c1cd971e8f8e3719f4 Mon Sep 17 00:00:00 2001 From: Michael Huth Date: Wed, 22 Jul 2026 12:47:03 +0200 Subject: [PATCH 02/12] MCP: Update to version 1.13.0 of the Igor Pro MCP Bridge New features are: - automatic detection and dismissal of compilation errors, Igor Pro does not need to be the foreground window. - functionality to retrieve the history window content --- Packages/doc/igor-pro-bridge.rst | 103 ++- .../igor-pro-bridge-1.13.0.mcpb | Bin 0 -> 32023 bytes tools/igor-mcp-bridge/server.py | 657 +++++++++++++++--- 3 files changed, 656 insertions(+), 104 deletions(-) create mode 100644 tools/igor-mcp-bridge/igor-pro-bridge-1.13.0.mcpb diff --git a/Packages/doc/igor-pro-bridge.rst b/Packages/doc/igor-pro-bridge.rst index 470e14f3af..62c1723ff3 100644 --- a/Packages/doc/igor-pro-bridge.rst +++ b/Packages/doc/igor-pro-bridge.rst @@ -77,12 +77,16 @@ Available tools ``execute_igor_command(command)`` Runs a command string on Igor's command line via ``Execute2``. Include an - ``fprintf 0, "..."`` call to get data back. **Caution**: if ``command`` calls - user-defined procedure code and the Debugger is enabled, a breakpoint/runtime - error/abort/stale-reference pause will hang this call indefinitely -- there is no - scriptable way to resume or dismiss the Debugger window. Prefer - ``execute_igor_command_unattended`` whenever nobody is watching who could close that - popup by hand. + ``fprintf 0, "..."`` call to get data back. Returns a dict with ``"results"`` + (the ``fprintf`` output) and ``"history"`` (anything ``command`` sent to Igor's + history area during this call, e.g. ``print`` output or the command echo itself + -- confirmed from ``Automation Server.ihf``), so a ``print`` statement's output + can be verified directly from the return value, without needing a human to look + at Igor's screen. **Caution**: if ``command`` calls user-defined procedure code + and the Debugger is enabled, a breakpoint/runtime error/abort/stale-reference + pause will hang this call indefinitely -- there is no scriptable way to resume + or dismiss the Debugger window. Prefer ``execute_igor_command_unattended`` + whenever nobody is watching who could close that popup by hand. ``execute_igor_command_unattended(command)`` Same as ``execute_igor_command``, but automatically disables Igor's Debugger before @@ -92,6 +96,17 @@ Available tools since Igor typically keeps running after an unhandled runtime error rather than stopping (see :ref:`igor_pro_bridge_runtime_errors`). +``read_session_history(stop=False)`` + Reads back everything sent to Igor's history area since this bridge process + first talked to Igor, via Igor's built-in ``CaptureHistoryStart()``/ + ``CaptureHistory()`` functions (confirmed from ``Igor Reference.ihf``) -- a + capture starts automatically on first use. Unlike the per-call ``history`` field + above, this can verify *past* executions retroactively (e.g. if a command's + return value wasn't captured at the time, or to see everything printed across + many separate calls at once). Each call returns the full accumulated text since + the capture started, so repeated calls are always safe. ``stop=True`` ends the + current capture and starts a fresh one on next use. + ``get_wave(wave_path)`` Returns the data of an existing 1D Igor wave (numeric or text) as a list. Complex and multi-dimensional waves are not supported. @@ -115,7 +130,33 @@ Available tools commands go through Igor's operation queue rather than running immediately (see "Operation Queue" in ``Advanced Topics.ihf``), so this cross-checks two independent signals before trusting a "compiled" result -- see - :ref:`igor_pro_bridge_claude_helper` and :ref:`igor_pro_bridge_compile_dialog`. + :ref:`igor_pro_bridge_claude_helper` and :ref:`igor_pro_bridge_compile_dialog`. If + compilation still isn't confirmed after the initial poll, this automatically makes + one attempt to dismiss a possible stuck compile-error dialog (see + ``dismiss_compile_error_dialog``) before falling back to + ``"prompt_user_to_check_for_dialog": true``. **Caution**: twice during this bridge's + development, a real Igor Pro 10.03 instance became unreachable via COM (crashed or + was closed) shortly after a reload/compile attempt -- no root cause was confirmed, + and it isn't established whether this is related to the bridge at all versus a + pre-existing Igor Pro stability issue (a subsequent retest against Igor Pro 9.06 + ran the same sequence -- broken code, reload/compile, fix, reload/compile again -- + without a repeat crash, which is reassuring but not conclusive either way). If a + subsequent call fails with a COM/RPC error, check ``check_bridge_health()`` and be + prepared to relaunch Igor Pro. + +``dismiss_compile_error_dialog()`` + Attempts to close a stuck Igor Pro dialog by posting a simulated Escape key press + directly to it (via ``PostMessage``), targeting a visible window owned by an Igor + Pro process that either has class ``"#32770"`` (the standard Windows dialog class) + or a title matching a known stuck-dialog title -- this does **not** require or + change OS focus/foreground state. **Confirmed live against both Igor Pro 10.03 and + Igor Pro 9.06**: the compile-error dialog is titled exactly *"Function Compilation + Error"* and is a Qt window (class ``"Qt693QWindowIcon"`` on 10.03), not a native + ``"#32770"`` dialog as originally assumed -- title matching is what actually finds + it on both major versions, and a *posted* (not real hardware) Escape successfully + closed it in both cases, with no focus/foreground change needed. Does **not** + recover the actual error message -- it only clears the dialog so work can + continue. See :ref:`igor_pro_bridge_compile_dialog`. ``get_debugger_state()`` / ``set_debugger_enabled(enabled, ...)`` / ``restore_debugger_settings()`` Read, change, and restore Igor's Debugger settings (``DebuggerOptions``). Use @@ -171,8 +212,36 @@ ever actually running -- ``reload_and_compile_procedures`` will keep reporting "not compiled" even after the underlying ``.ipf`` file is genuinely fixed, until a person closes that dialog by hand. -There is no documented way to detect or dismiss this dialog via COM. When -``reload_and_compile_procedures`` times out, its result includes +There is no documented way to detect or dismiss this dialog via COM, but Escape +closes it. ``dismiss_compile_error_dialog()`` exploits that: it enumerates top-level +windows for a visible one owned by an Igor Pro process that matches either window +class ``"#32770"`` (the standard native Windows dialog class) or a known +stuck-dialog title, then posts ``WM_KEYDOWN``/``WM_KEYUP`` for Escape directly to it +via ``PostMessage`` -- no foreground switch, no stolen focus. This works despite +Igor Pro's elevated status specifically because this bridge's own process is also +required to run elevated (see Requirements above) -- Windows' UIPI blocks simulated +input from a lower-privilege process reaching a higher-privilege one, but not +between two equally elevated processes. + +**Confirmed live against a real stuck dialog on both Igor Pro 10.03 and Igor Pro +9.06**: the original assumption that this dialog is an ordinary ``"#32770"`` +native dialog was wrong -- Igor Pro's UI (on both major versions tested) is +Qt-based, and the compile-error dialog is a Qt window titled exactly *"Function +Compilation Error"* on both (observed class on 10.03: ``"Qt693QWindowIcon"``, a +version-hash-looking string not worth matching on directly). Title matching is what +actually finds it, and a *posted* Escape (not a real hardware key press) was +confirmed to close it on both versions -- Qt's Windows platform layer reacts to the +posted message the same way it would a real key press. If a future window doesn't +match either signature, dismissal safely reports "not found" (along with a +diagnostic list of +every window Igor currently owns) rather than doing something incorrect. The +trade-off either way: this recovers the ability to continue working, not the actual +error message -- check the ``.ipf`` file's syntax directly, or have a human read the +dialog text, if the exact message matters. ``reload_and_compile_procedures`` now +calls this automatically once, before falling back to asking a human. + +If the automatic attempt doesn't resolve it (or wasn't possible -- e.g. no matching +dialog window was found), ``reload_and_compile_procedures``'s result includes ``"prompt_user_to_check_for_dialog": true``; whatever is driving the bridge (e.g. an AI agent) should use this as an explicit instruction to ask the human operator to check for and close a stuck dialog, rather than silently retrying or only logging advisory text -- @@ -254,8 +323,12 @@ too-old-Igor warning panel) without colliding. Known limitations ------------------ -- No scriptable way to resume a Debugger pause or to detect/dismiss a compile-error - dialog -- both require a human, as described above. +- No scriptable way to resume a Debugger pause -- this requires a human, as described + above. A compile-error dialog can usually be auto-dismissed via a posted Escape key + press (see ``dismiss_compile_error_dialog``, confirmed live), but that recovers the + ability to continue, not the error message itself; if a genuinely new/different + Igor popup shows up (not the known "Function Compilation Error" dialog or a native + ``"#32770"`` one), dismissal safely reports "not found" and a human is still needed. - ``get_wave`` supports 1D, real-valued waves only. - The pywin32 dynamic-dispatch calling convention for ``Execute2``'s multiple ``[out]`` parameters is assumed to follow the standard IDispatch convention (parameters come @@ -264,3 +337,11 @@ Known limitations - This is a *local* MCP server (stdio transport): it only works from a Claude Desktop session running on the same Windows machine as Igor Pro, not from a cloud/sandboxed session. +- **Observed twice during development on Igor Pro 10.03, root cause unconfirmed**: + Igor Pro became unreachable via COM (crashed or was closed) shortly after a + ``reload_and_compile_procedures`` call. It isn't established whether this is + related to the bridge's own actions or a pre-existing Igor Pro stability issue + independent of it. A repeat of the same broken-code/reload/fix/reload sequence + against Igor Pro 9.06 did not reproduce it, which doesn't rule out a 10.03-specific + or environment-specific cause -- treat any COM/RPC failure after a compile attempt + as a signal to check ``check_bridge_health()`` and be prepared to relaunch Igor Pro. diff --git a/tools/igor-mcp-bridge/igor-pro-bridge-1.13.0.mcpb b/tools/igor-mcp-bridge/igor-pro-bridge-1.13.0.mcpb new file mode 100644 index 0000000000000000000000000000000000000000..89adf7d8aebe5250238551e260cce66dd15685ba GIT binary patch literal 32023 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 dict: + """Read back everything sent to Igor's history area (print output, command + echoing, error messages, etc.) since this bridge process first talked to Igor + -- the reliable way to verify a PAST execute_igor_command/ + execute_igor_command_unattended call's `print` output actually happened, + without asking a human to look at Igor's screen or needing to have captured + the per-call `history` field at the time. + + Backed by Igor's built-in CaptureHistoryStart()/CaptureHistory() functions + (confirmed from Igor Reference.ihf). A capture is started automatically the + first time any command runs through this bridge in this process's lifetime, + so this always has something to report. Each call returns the FULL + accumulated text since that start point, not just what's new since the last + read -- so calling this repeatedly with stop=False (the default) is always + safe and simply returns more (growing) text as more commands run in between. + + stop=True stops the capture (no further text will be recorded for it) and + returns whatever was captured up to that point; a subsequent call to this + tool (or the next command run through this bridge) then starts a brand-new + capture automatically, covering only from that point forward -- use this to + intentionally "reset" what counts as history for a fresh phase of work. + + Raises if no capture is currently active, which should only happen if + CaptureHistoryStart() itself failed when first attempted (e.g. an + unexpectedly old Igor version) -- in that case, fall back to reading the + `history` field returned directly by execute_igor_command/ + execute_igor_command_unattended for that specific call instead. + """ + global _session_history_capture_refnum + if _session_history_capture_refnum is None: + raise RuntimeError( + "No history capture is currently active in this bridge process. This " + "starts automatically on first use, so this likely means " + "CaptureHistoryStart() failed earlier (see server logs) or nothing " + "has been executed yet -- try running check_bridge_health() first, " + "then retry this call." + ) + + refnum = _session_history_capture_refnum + stop_flag = 1 if stop else 0 + cmd = f'fprintf 0, "%s", CaptureHistory({refnum:.0f}, {stop_flag})' + errorCode, errorMsg, history, results = _execute2(cmd) + if errorCode != 0: + raise RuntimeError( + f"Could not read history capture (error code {errorCode}): " + f"{errorMsg or '(no error message)'}" + ) + + if stop: + _session_history_capture_refnum = None + + return {"history_text": results, "capture_stopped": stop} + + # --- Igor runtime error model (how errors surface through Execute2) ----------------- # # Confirmed empirically this session, against a live Igor Pro instance, by @@ -325,7 +432,7 @@ def _format_execute2_error( @mcp.tool() -def execute_igor_command(command: str) -> str: +def execute_igor_command(command: str) -> dict: """Execute a single Igor Pro command string in the running Igor instance. To get data back (not just run a command for its side effect), include an @@ -344,18 +451,30 @@ def execute_igor_command(command: str) -> str: deliberately want the Debugger available (e.g. interactively testing a breakpoint). + Returns a dict with: + - "results": the fprintf output captured, if any (empty string otherwise). + - "history": any text `command` sent to Igor's history area during this + specific call -- confirmed from Automation Server.ihf: "history [output] is + a Basic string. On output it contains any text sent to Igor's history area + by the commands." This is exactly how to verify a `print` statement inside + `command` actually ran, without needing a human to look at Igor's screen or + calling the separate read_session_history tool. (Note: the command itself + is also normally echoed into history unless Silent 2 is in effect, so this + may include more than just explicit `print` output.) + **On failure:** a nonzero error code means at least one unhandled runtime error occurred somewhere in `command` -- it does NOT mean execution stopped there, and it does NOT mean it was the only problem (see the runtime error model notes above `_format_execute2_error`). The raised error includes any partial `results` - captured, since that's often the only way to tell how far execution actually got. + and `history` captured, since that's often the only way to tell how far + execution actually got. """ errorCode, errorMsg, history, results = _execute2(command) if errorCode != 0: raise RuntimeError( _format_execute2_error(command, errorCode, errorMsg, results, history) ) - return results + return {"results": results, "history": history} @mcp.tool() @@ -375,6 +494,7 @@ def get_wave(wave_path: str) -> list: 4000 of 5000, this resumes from point 4000 after reconnecting instead of re-fetching the wave and re-reading points 0-3999 again. """ + def get_dims(): return _get_wave_ref(wave_path).GetDimensions() @@ -435,8 +555,8 @@ def check_bridge_health() -> dict: except RuntimeError as e: report["status"] = "FAIL" report["problem"] = ( - f"No running Igor Pro instance found via COM ({e}). Make sure Igor Pro 10 " - "or later is open and running elevated." + f"No running Igor Pro instance found via COM ({e}). Make sure Igor Pro " + "9.00 or later is open and running elevated." ) return report @@ -539,7 +659,9 @@ def _read_claude_helper_compile_counter(): (COM/Igor-level error, or the sentinel -1 meaning the variable doesn't exist -- see the constant's comment above for why both are treated as simply "unavailable", never fatal).""" - errorCode, errorMsg, history, results = _execute2(_CLAUDE_HELPER_COMPILE_COUNTER_CMD) + errorCode, errorMsg, history, results = _execute2( + _CLAUDE_HELPER_COMPILE_COUNTER_CMD + ) if errorCode != 0: return None try: @@ -548,6 +670,7 @@ def _read_claude_helper_compile_counter(): return None return None if value < 0 else value + _COMPILE_ERROR_DIALOG_NOTE = ( "One confirmed cause if this is unexpected (e.g. you just fixed a known syntax " "error and expected this to succeed): a compile-error dialog left open in Igor " @@ -559,17 +682,359 @@ def _read_claude_helper_compile_counter(): "call sit there without ever actually running, even though this bridge's own COM " "calls keep responding normally throughout (confirmed empirically: this hang " "does not show up as a hung tool call, only as 'compiled' staying stuck at False " - "no matter how many times this is retried). There is no documented way to " - "detect or dismiss that dialog via COM. " - "ACTION FOR WHATEVER IS CALLING THIS TOOL: do not just log this and retry silently " - "-- explicitly ask the human operator right now whether a compile-error dialog is " - "showing in Igor Pro, and if so, to close it, before retrying. This was confirmed " - "during development to be the only thing that reliably un-sticks this state -- " - "passively worded advice in a note is easy to skip past; an explicit prompt to the " - "human is what actually keeps an unattended/agent-driven workflow moving." + "no matter how many times this is retried). " + "reload_and_compile_procedures already attempts an automatic fix for exactly this " + "case (posting an Escape key press directly to Igor's dialog window, via " + "dismiss_compile_error_dialog's underlying logic, without needing OS focus/" + "foreground) before returning this note -- see the 'auto_dismiss_attempted' " + "field for what that attempt found and did. " + "ACTION FOR WHATEVER IS CALLING THIS TOOL: if the automatic attempt did not " + "resolve it (or was not attempted, e.g. because no matching dialog window was " + "found), do not just log this and retry silently -- explicitly ask " + "the human operator right now whether a compile-error dialog is showing in Igor " + "Pro, and if so, to close it, before retrying. Explicitly prompting the human is " + "what actually keeps an unattended/agent-driven workflow moving when the " + "automatic attempt isn't enough." ) +# --- Compile-error dialog dismissal (posted Escape key message) --------------------- +# +# Added after a user-proposed mitigation for the compile-error-dialog problem +# documented above: there is still no documented COM operation to detect or dismiss +# that dialog, but Escape closes it, and a simulated key press can be delivered to +# it directly. +# +# **Confirmed live against real Igor Pro instances -- both Igor Pro 10.03 and Igor +# Pro 9.06 (this is no longer a guess for either)**: the original assumption that +# this dialog is an ordinary "#32770" Win32 dialog was WRONG -- Igor Pro's UI (both +# major versions tested) is Qt-based, and the compile-error dialog is a Qt window +# with a version-hash-looking class name (observed on 10.03: "Qt693QWindowIcon"; +# not re-checked on 9.06 since title matching alone was already sufficient there). +# Since that class name likely varies across Igor/Qt builds and isn't a stable +# thing to match on, this instead matches on the dialog's window TITLE, which was +# directly observed to be exactly "Function Compilation Error" on BOTH Igor Pro +# 10.03 and 9.06 -- a stable, Igor-chosen string, not a toolkit implementation +# detail, and apparently stable across at least these two major versions. The +# "#32770" class check is kept as a second, OR'd condition (harmless, and covers +# the case of a genuinely native Win32 dialog for some other Igor-raised error). +# +# PostMessage(hwnd, WM_KEYDOWN/WM_KEYUP, VK_ESCAPE, ...) is used rather than a +# hardware-level input simulation so this never needs to steal OS focus/ +# foreground from whatever the user is doing. **Confirmed live against a real +# stuck "Function Compilation Error" dialog on BOTH Igor Pro 10.03 and Igor Pro +# 9.06: a POSTED (not real hardware) WM_KEYDOWN/WM_KEYUP for VK_ESCAPE +# successfully closed it in both cases** -- Qt's Windows platform plugin +# intercepts native window messages in its own WndProc regardless of a message's +# origin, so it reacted the same way a real key press would, with no +# foreground/focus change needed. (If a future Igor/Qt version doesn't react the +# same way, the fallback would be a hardware-level simulation -- +# SetForegroundWindow + keybd_event/SendInput -- targeted at this same window, at +# the cost of stealing focus.) +# +# Targeting no longer relies on the OS foreground window at all (the very first, +# now-superseded approach): it enumerates all top-level windows and keeps visible +# ones belonging to an Igor Pro process (exe name starting with "igor") that either +# have window class "#32770" or a title matching a known stuck-dialog title (see +# _KNOWN_STUCK_DIALOG_TITLES). If neither ever matches, dismissal safely reports +# "not found" (see "igor_windows_seen" in that result for exactly what windows +# exist, to extend this list further if a new stuck-dialog title shows up). +# +# Trade-off, confirmed to be acceptable by the user who proposed this mitigation: +# this recovers the ability to continue working, but does NOT recover the actual +# compile-error message -- Escape just closes the dialog, it doesn't read it. If the +# exact error text matters, check Igor's procedure window/history directly (or ask a +# human to read the dialog) before this or reload_and_compile_procedures's automatic +# call to it dismisses it. + +_IGOR_PROCESS_NAME_PREFIX = "igor" +_DIALOG_WINDOW_CLASS = "#32770" # standard Windows "Dialog" window class +# Known titles of Igor Pro popups that block the operation queue and are safe to +# dismiss with Escape. Confirmed live against both Igor Pro 10.03 and Igor Pro +# 9.06: the compile-error dialog is titled exactly "Function Compilation Error" +# and is a Qt window, NOT a "#32770" native dialog -- so title matching is the +# primary signal for this one, and it appears stable across major versions. +_KNOWN_STUCK_DIALOG_TITLES = ("Function Compilation Error",) +_POSTED_KEY_GAP_SECONDS = 0.05 +_POSTED_KEY_SETTLE_SECONDS = 0.2 + + +def _is_stuck_dialog_window(class_name: str, title: str) -> bool: + """True if a window looks like one of the known stuck-dialog cases this bridge + knows how to dismiss -- either the standard Win32 dialog class, or a title + matching a known Igor popup (see _KNOWN_STUCK_DIALOG_TITLES).""" + if class_name == _DIALOG_WINDOW_CLASS: + return True + return any(known.lower() in title.lower() for known in _KNOWN_STUCK_DIALOG_TITLES) + + +def _get_process_exe_name(pid: int): + """Best-effort lookup of the executable file name (e.g. "Igor64.exe") owning + `pid`, or None if it can't be determined. Returns just the base file name, not + the full path, so callers can do a simple case-insensitive prefix check.""" + ACCESS = win32con.PROCESS_QUERY_INFORMATION | win32con.PROCESS_VM_READ + hProcess = None + try: + hProcess = win32api.OpenProcess(ACCESS, False, pid) + path = win32process.GetModuleFileNameEx(hProcess, 0) + return os.path.basename(path) + except Exception: + return None + finally: + if hProcess is not None: + win32api.CloseHandle(hProcess) + + +def _find_igor_dialog_window(): + """Find a visible top-level window that looks like a known stuck Igor Pro + dialog (see _is_stuck_dialog_window) and is owned by an Igor Pro process, + without regard to OS foreground/focus state. + + Returns (hwnd, title, exe_name) for the first match found, or None if no such + window exists right now. EnumWindows's callback is never made to return False + (pywin32 raises a spurious error if it does -- the underlying Win32 call reports + that as a failure even though it just means "the callback asked to stop early"), + so this always enumerates every top-level window and collects all matches, then + returns the first one -- windows are typically (though not strictly guaranteed) + reported in top-to-bottom Z-order, so in the common case of a single dialog this + is simply that dialog. + """ + matches = [] + + def _callback(hwnd, _extra): + if win32gui.IsWindowVisible(hwnd): + title = win32gui.GetWindowText(hwnd) + class_name = win32gui.GetClassName(hwnd) + if _is_stuck_dialog_window(class_name, title): + _, pid = win32process.GetWindowThreadProcessId(hwnd) + exe_name = _get_process_exe_name(pid) + if exe_name and exe_name.lower().startswith(_IGOR_PROCESS_NAME_PREFIX): + matches.append((hwnd, title, exe_name)) + return True + + win32gui.EnumWindows(_callback, None) + return matches[0] if matches else None + + +def _list_igor_top_level_windows() -> list: + """Diagnostic helper: list EVERY visible top-level window owned by an Igor Pro + process, regardless of class -- title, class name, and exe name for each. + + Used only when _find_igor_dialog_window() finds no match, to surface what's + actually there instead of just reporting "not found" with no further + information -- this is exactly how the compile-error dialog's real title + ("Function Compilation Error") and class ("Qt693QWindowIcon", a Qt window, NOT + a native "#32770" dialog) were identified live, without needing a separate + one-off diagnostic tool. Useful again if some other stuck dialog shows up with + a title not yet in _KNOWN_STUCK_DIALOG_TITLES. + """ + windows = [] + + def _callback(hwnd, _extra): + if win32gui.IsWindowVisible(hwnd): + _, pid = win32process.GetWindowThreadProcessId(hwnd) + exe_name = _get_process_exe_name(pid) + if exe_name and exe_name.lower().startswith(_IGOR_PROCESS_NAME_PREFIX): + windows.append( + { + "title": win32gui.GetWindowText(hwnd), + "class_name": win32gui.GetClassName(hwnd), + "process": exe_name, + } + ) + return True + + win32gui.EnumWindows(_callback, None) + return windows + + +def _attempt_dismiss_compile_error_dialog() -> dict: + """Post a simulated Escape key press directly to an Igor Pro dialog window (if + one can be found), without requiring it to be focused or in the OS foreground. + Returns a dict describing what was found and whether anything was actually sent + -- see the module-level comment above this function for the reasoning, the + unverified assumptions, and the trade-offs. + """ + found = _find_igor_dialog_window() + if found is None: + return { + "attempted": False, + "reason": ( + "No visible window matching a known stuck-dialog signature " + '(class "#32770", or title containing one of ' + f"{_KNOWN_STUCK_DIALOG_TITLES}) owned by an Igor Pro process was " + "found. Either there is no stuck dialog right now, or it's a kind " + "not seen before -- see 'igor_windows_seen' below for every " + "visible window Igor currently owns, to identify it." + ), + "igor_windows_seen": _list_igor_top_level_windows(), + } + + hwnd, window_title, exe_name = found + + try: + win32api.PostMessage(hwnd, win32con.WM_KEYDOWN, win32con.VK_ESCAPE, 0) + time.sleep(_POSTED_KEY_GAP_SECONDS) + win32api.PostMessage(hwnd, win32con.WM_KEYUP, win32con.VK_ESCAPE, 0) + time.sleep(_POSTED_KEY_SETTLE_SECONDS) + except Exception as e: + return { + "attempted": False, + "reason": f"Posting the simulated Escape key press failed: {e}", + "dialog_window_title": window_title, + "dialog_window_process": exe_name, + } + + return { + "attempted": True, + "dialog_window_title": window_title, + "dialog_window_process": exe_name, + "note": ( + "Posted a simulated Escape key press directly to this dialog window " + "(no OS foreground/focus change was made or needed) -- confirmed live " + 'to close Igor\'s "Function Compilation Error" Qt dialog the same way ' + "a real key press would. This does NOT recover the actual " + "compile-error message -- it only closes whatever modal dialog was " + "showing. Follow up with check_compilation_state() or " + "reload_and_compile_procedures() to see whether this actually " + "un-stuck anything." + ), + } + + +@mcp.tool() +def dismiss_compile_error_dialog() -> dict: + """Attempt to close a stuck Igor Pro modal compile-error dialog by posting a + simulated Escape key press directly to it, WITHOUT recovering the actual error + message and WITHOUT requiring or changing OS focus/foreground state. + + Use this manually when you suspect Igor Pro has a compile-error dialog open (e.g. + reload_and_compile_procedures kept reporting "not compiled" even after fixing a + known syntax error) and want to try clearing it yourself, separately from + reload_and_compile_procedures's own automatic attempt at the same thing (see its + docstring -- it already calls this same logic once before giving up and asking a + human). + + Mechanism: enumerates top-level windows for a visible one, owned by a process + whose exe name starts with "igor" (e.g. Igor64.exe), that either has window + class "#32770" (the standard Windows Dialog Box class) OR a title matching a + known stuck-dialog title. **Confirmed live against both Igor Pro 10.03 and + Igor Pro 9.06: the compile-error dialog is titled exactly "Function + Compilation Error" and is a Qt window (class observed as "Qt693QWindowIcon" on + 10.03), NOT a native "#32770" dialog** -- so title matching is what actually + finds it, on both major versions tested. Once found, this posts + WM_KEYDOWN/WM_KEYUP for VK_ESCAPE directly to that window via PostMessage, + without requiring it to be focused or in the foreground. + + **Confirmed live on both Igor Pro 10.03 and Igor Pro 9.06: a POSTED (not real + hardware) Escape key event is enough to make Qt's Windows platform layer + close this dialog the same way a real key press would** -- verified against a + real stuck "Function Compilation Error" dialog on each version, with no + foreground/focus change needed. If no matching window is found at all (e.g. a + different, not-yet-seen Igor popup), this reports "attempted": false (safe + failure) along with "igor_windows_seen": every visible top-level window + currently owned by an Igor Pro process (title/class/process), so a new stuck + dialog's real title/class can be identified and added to + _KNOWN_STUCK_DIALOG_TITLES instead of guessing. + + This works despite Igor Pro's elevated status because this bridge's own process + is also required to run elevated (see the module docstring) -- Windows blocks + simulated input from a lower-privilege process reaching a higher-privilege + window (UIPI), but does not block it between two equally elevated processes. + + **Trade-off: this does not tell you what the error was.** It only clears + whatever dialog is blocking Igor's operation queue so work can continue. If the + actual error text matters, check the .ipf file directly or ask a human to read + the dialog before calling this. + """ + return _attempt_dismiss_compile_error_dialog() + + +_COMPILE_POLL_TIMEOUT_AFTER_DISMISS_SECONDS = 3.0 + + +def _poll_for_compile_confirmation(baseline_counter, timeout_seconds: float) -> dict: + """Poll for up to timeout_seconds for confirmation that Igor Pro's procedure code + compiled successfully, checking both signals described in + reload_and_compile_procedures's docstring. Returns one of: + + - {"compiled": True, "poll_attempts": N, "confirmed_via": "..."} + - {"compiled": False, "compiled_state_known": False, "poll_attempts": N, + "last_error_code": ..., "last_error_msg": ...} -- the compiled-state check + itself kept failing throughout the poll. + - {"compiled": False, "poll_attempts": N, "raw_function_info": ...} -- the + compiled-state check succeeded but never confirmed a compile within the + timeout. + + Factored out of reload_and_compile_procedures so it can be called a second time, + with a shorter timeout, after an automatic compile-error-dialog dismissal + attempt, without duplicating the polling logic. + """ + deadline = time.monotonic() + timeout_seconds + lastErrorCode = None + lastErrorMsg = None + lastResults = None + attempts = 0 + consecutive_compiled = 0 + + while True: + attempts += 1 + + current_counter = _read_claude_helper_compile_counter() + if ( + baseline_counter is not None + and current_counter is not None + and current_counter > baseline_counter + ): + return { + "compiled": True, + "poll_attempts": attempts, + "confirmed_via": "AfterCompiledHook counter (root:gClaudeHelperCompileCounter)", + } + + compiledErrorCode, compiledErrorMsg, _, compiledResults = _execute2( + _PROCEDURES_COMPILED_CHECK_CMD + ) + if compiledErrorCode == 0: + lastErrorCode = None + lastResults = compiledResults + if compiledResults == "": + consecutive_compiled += 1 + if consecutive_compiled >= _COMPILE_CONFIRM_CHECKS: + return { + "compiled": True, + "poll_attempts": attempts, + "confirmed_via": ( + "FunctionInfo poll (AfterCompiledHook counter unavailable " + "or unchanged)" + ), + } + else: + consecutive_compiled = 0 + else: + lastErrorCode, lastErrorMsg = compiledErrorCode, compiledErrorMsg + consecutive_compiled = 0 + + if time.monotonic() >= deadline: + break + time.sleep(_COMPILE_POLL_INTERVAL_SECONDS) + + if lastErrorCode is not None: + return { + "compiled": False, + "compiled_state_known": False, + "poll_attempts": attempts, + "last_error_code": lastErrorCode, + "last_error_msg": lastErrorMsg, + } + + return { + "compiled": False, + "poll_attempts": attempts, + "raw_function_info": lastResults, + } + + @mcp.tool() def reload_and_compile_procedures() -> dict: """Force Igor Pro to reload procedure code from the .ipf files on disk and attempt @@ -580,6 +1045,16 @@ def reload_and_compile_procedures() -> dict: Igor Pro is not currently running other procedure code; reloading/compiling while code is running is not supported. + **Caution, observed twice during this bridge's development against a real Igor + Pro 10.03 instance**: Igor Pro became unreachable via COM (crashed or was + closed) shortly after a reload/compile attempt, in two separate incidents -- + once with broken procedure code present, once immediately after fixing it. No + root cause has been confirmed (no Windows crash logs were accessible from + here), and it's not established whether this bridge's own actions are involved + at all versus a pre-existing Igor Pro stability issue independent of it. If a + tool call after this one starts failing with a COM/RPC error, check + check_bridge_health() and be prepared for Igor Pro to need relaunching. + Mirrors the exact method used by CompileAndRestart() in igortest-tracing.ipf: Execute/P "RELOAD CHANGED PROCS " @@ -623,16 +1098,31 @@ def reload_and_compile_procedures() -> dict: even after the underlying .ipf file is genuinely fixed, until a person closes that dialog by hand. This happened for real during development. + Before giving up, if compilation isn't confirmed within the initial timeout, this + automatically makes ONE attempt to dismiss a possible stuck compile-error dialog + by posting an Escape key press directly to it (see dismiss_compile_error_dialog), + then polls again briefly. This does not require or change OS focus/foreground + state; it only fires if a matching Igor Pro dialog window can actually be found + -- see dismiss_compile_error_dialog's docstring for the full mechanism and its + trade-off (it recovers the ability to continue, not the error message). The + returned dict's "auto_dismiss_attempted" field always reports what that attempt + found/did, even when it wasn't needed or no matching window was found. + **If the returned dict has "prompt_user_to_check_for_dialog": True, whatever is calling this tool should explicitly ask the human operator to check Igor Pro's screen for a stuck compile-error dialog and close it, before retrying** -- not - just read the accompanying "note" text and move on. Confirmed directly during - development: silently retrying or only logging the note left the workflow stuck; - explicitly prompting the human at this point is what actually un-stuck it. + just read the accompanying "note" text and move on. This only happens after the + automatic dismissal attempt above has already been tried and didn't resolve it + (or wasn't possible, e.g. no matching dialog window was found). + Confirmed directly during development: silently retrying or only logging the + note left the workflow stuck; explicitly prompting the human at this point is + what actually un-stuck it. """ baseline_counter = _read_claude_helper_compile_counter() - errorCode, errorMsg, history, results = _execute2('Execute/P "RELOAD CHANGED PROCS "') + errorCode, errorMsg, history, results = _execute2( + 'Execute/P "RELOAD CHANGED PROCS "' + ) if errorCode != 0: raise RuntimeError( f"RELOAD CHANGED PROCS failed (error code {errorCode}): {errorMsg}" @@ -644,87 +1134,62 @@ def reload_and_compile_procedures() -> dict: f"COMPILEPROCEDURES failed (error code {errorCode}): {errorMsg}" ) - deadline = time.monotonic() + _COMPILE_POLL_TIMEOUT_SECONDS - lastErrorCode = None - lastErrorMsg = None - lastResults = None - attempts = 0 - consecutive_compiled = 0 + poll_result = _poll_for_compile_confirmation( + baseline_counter, _COMPILE_POLL_TIMEOUT_SECONDS + ) + if poll_result["compiled"]: + return {"reload_triggered": True, "compile_triggered": True, **poll_result} - while True: - attempts += 1 + dismiss_result = _attempt_dismiss_compile_error_dialog() - current_counter = _read_claude_helper_compile_counter() - if ( - baseline_counter is not None - and current_counter is not None - and current_counter > baseline_counter - ): + if dismiss_result.get("attempted"): + poll_result = _poll_for_compile_confirmation( + baseline_counter, _COMPILE_POLL_TIMEOUT_AFTER_DISMISS_SECONDS + ) + if poll_result["compiled"]: return { "reload_triggered": True, "compile_triggered": True, - "compiled": True, - "poll_attempts": attempts, - "confirmed_via": "AfterCompiledHook counter (root:gClaudeHelperCompileCounter)", + **poll_result, + "auto_dismiss_attempted": dismiss_result, + "note": ( + "Compilation only succeeded after automatically simulating an " + "Escape key press to close what was very likely a stuck " + "compile-error dialog. The dialog's exact error message was NOT " + "recovered -- if this keeps happening, check the .ipf file's " + "syntax directly, or ask a human to read the dialog text before " + "it gets dismissed next time." + ), } - compiledErrorCode, compiledErrorMsg, _, compiledResults = _execute2( - _PROCEDURES_COMPILED_CHECK_CMD + if "compiled_state_known" in poll_result: + note = ( + f"Reload/compile commands ran, but checking the resulting state kept " + f"failing (last error code {poll_result.get('last_error_code')}): " + f"{poll_result.get('last_error_msg')}. " + _COMPILE_ERROR_DIALOG_NOTE + ) + else: + note = ( + f"Still not compiled after polling for {_COMPILE_POLL_TIMEOUT_SECONDS:.0f}s" + + ( + f" plus a further {_COMPILE_POLL_TIMEOUT_AFTER_DISMISS_SECONDS:.0f}s " + "after an automatic Escape-key dismissal attempt" + if dismiss_result.get("attempted") + else "" + ) + + f" (requiring {_COMPILE_CONFIRM_CHECKS} consecutive confirmations). This is " + "more likely a genuine compile error in the procedure code than a timing " + "artifact -- check Igor's history/procedure window directly. " + + _COMPILE_ERROR_DIALOG_NOTE ) - if compiledErrorCode == 0: - lastErrorCode = None - lastResults = compiledResults - if compiledResults == "": - consecutive_compiled += 1 - if consecutive_compiled >= _COMPILE_CONFIRM_CHECKS: - return { - "reload_triggered": True, - "compile_triggered": True, - "compiled": True, - "poll_attempts": attempts, - "confirmed_via": ( - "FunctionInfo poll (AfterCompiledHook counter unavailable " - "or unchanged)" - ), - } - else: - consecutive_compiled = 0 - else: - lastErrorCode, lastErrorMsg = compiledErrorCode, compiledErrorMsg - consecutive_compiled = 0 - - if time.monotonic() >= deadline: - break - time.sleep(_COMPILE_POLL_INTERVAL_SECONDS) - - if lastErrorCode is not None: - return { - "reload_triggered": True, - "compile_triggered": True, - "compiled_state_known": False, - "poll_attempts": attempts, - "prompt_user_to_check_for_dialog": True, - "note": ( - f"Reload/compile commands ran, but checking the resulting state kept " - f"failing (last error code {lastErrorCode}): {lastErrorMsg}. " - + _COMPILE_ERROR_DIALOG_NOTE - ), - } return { "reload_triggered": True, "compile_triggered": True, - "compiled": False, - "poll_attempts": attempts, - "raw_function_info": lastResults, + **poll_result, + "auto_dismiss_attempted": dismiss_result, "prompt_user_to_check_for_dialog": True, - "note": ( - f"Still not compiled after polling for {_COMPILE_POLL_TIMEOUT_SECONDS:.0f}s " - f"(requiring {_COMPILE_CONFIRM_CHECKS} consecutive confirmations). This is " - "more likely a genuine compile error in the procedure code than a timing " - "artifact -- check Igor's history/procedure window directly. " - + _COMPILE_ERROR_DIALOG_NOTE - ), + "note": note, } @@ -909,7 +1374,7 @@ def restore_debugger_settings() -> dict: @mcp.tool() -def execute_igor_command_unattended(command: str) -> str: +def execute_igor_command_unattended(command: str) -> dict: """Run `command` exactly like execute_igor_command, but automatically disable Igor's Debugger before running it and restore it again afterward -- even if `command` raises an Igor-level error. Uses its own local snapshot rather than the @@ -937,12 +1402,18 @@ def execute_igor_command_unattended(command: str) -> str: and restore_debugger_settings() once at the end, rather than paying the extra disable/restore COM round-trip on every single command via this tool. + Returns a dict with "results" (fprintf output) and "history" (anything + `command` sent to Igor's history area during this call, e.g. `print` output or + the command echo itself) -- see execute_igor_command's docstring for exactly + what "history" contains and why it's the reliable way to verify a `print` + actually happened. + **On failure:** a nonzero error code means at least one unhandled runtime error occurred somewhere in `command` -- it does NOT mean execution stopped there, and it does NOT mean it was the only problem (see the runtime error model notes above _format_execute2_error, right before execute_igor_command). The raised - error includes any partial `results` captured, since that's often the only way - to tell how far execution actually got. + error includes any partial `results` and `history` captured, since that's often + the only way to tell how far execution actually got. """ saved = _read_debugger_options() _apply_debugger_options( @@ -962,7 +1433,7 @@ def execute_igor_command_unattended(command: str) -> str: raise RuntimeError( _format_execute2_error(command, errorCode, errorMsg, results, history) ) - return results + return {"results": results, "history": history} # --- Environment summary ----------------------------------------------------------- @@ -1088,9 +1559,9 @@ def get_environment_summary() -> dict: for part in raw["data_folders_raw"].split("\r"): part = part.strip() if part.startswith("FOLDERS:"): - folders_part = part[len("FOLDERS:"):].rstrip(";") + folders_part = part[len("FOLDERS:") :].rstrip(";") elif part.startswith("WAVES:"): - waves_part = part[len("WAVES:"):].rstrip(";") + waves_part = part[len("WAVES:") :].rstrip(";") data_folders = [f for f in folders_part.split(",") if f] top_level_waves = [w for w in waves_part.split(",") if w] From 824bb94b59cf7f26ebf107b85a4ff8c8024adbff Mon Sep 17 00:00:00 2001 From: Michael Huth Date: Wed, 22 Jul 2026 16:25:04 +0200 Subject: [PATCH 03/12] MCP: Added four new functions - start Igor Pro unattended, which makes handling compile errors much easier and the client can read back the error message. The client asks the user on session start for the path to Igor64.exe - load experiment files - close DataBrowser, a function to close the DataBrowser in Igor Pro - get igor pro mcp bridge version that allows the client to get the currently loaded version --- Packages/MIES/MIES_ClaudeHelper.ipf | 18 +- Packages/doc/igor-pro-bridge.rst | 162 +++++- .../igor-pro-bridge-1.13.0.mcpb | Bin 32023 -> 0 bytes .../igor-pro-bridge-1.22.0.mcpb | Bin 0 -> 36504 bytes .../igor-pro-bridge-1.9.0.mcpb | Bin 22218 -> 0 bytes tools/igor-mcp-bridge/server.py | 545 ++++++++++++++++-- 6 files changed, 670 insertions(+), 55 deletions(-) delete mode 100644 tools/igor-mcp-bridge/igor-pro-bridge-1.13.0.mcpb create mode 100644 tools/igor-mcp-bridge/igor-pro-bridge-1.22.0.mcpb delete mode 100644 tools/igor-mcp-bridge/igor-pro-bridge-1.9.0.mcpb diff --git a/Packages/MIES/MIES_ClaudeHelper.ipf b/Packages/MIES/MIES_ClaudeHelper.ipf index 80b21466ff..6c4d432b51 100644 --- a/Packages/MIES/MIES_ClaudeHelper.ipf +++ b/Packages/MIES/MIES_ClaudeHelper.ipf @@ -18,7 +18,7 @@ /// for the too-old-Igor warning panel) without colliding. /// /// It records a monotonically increasing counter in root:gClaudeHelperCompileCounter -/// each time it fires. This gives the Igor Pro Bridge bridge a compile confirmation +/// each time it fires. This gives the Igor Pro Bridge a compile confirmation /// driven by Igor itself, rather than only inferred by polling FunctionInfo() for a /// non-existing function -- which can read stale state before Igor's operation queue /// (RELOAD CHANGED PROCS / COMPILEPROCEDURES) has actually drained. There is no @@ -27,6 +27,18 @@ static Function AfterCompiledHook() + variable modifiedBefore + + // Creating/incrementing a global marks the experiment as modified, same as any + // other data change. Captured/restored here so this hook never flips an + // otherwise-unmodified experiment to modified, matching the existing convention + // in MIES_IgorHooks.ipf's own AfterCompiledHook -- flagged by a Copilot PR + // review as a real risk otherwise: an experiment spuriously marked modified can + // trigger a "Save changes?" prompt later, which is exactly the kind of dialog + // this bridge (built around unattended operation) cannot dismiss remotely. + ExperimentModified + modifiedBefore = V_flag + // Bare Variable/G (no initializer) is safe to call unconditionally: per Igor // Reference.ihf, /G "overwrites any existing variable" but "the variable is // initialized when it is created if you supply the initial value" -- i.e. the @@ -38,6 +50,10 @@ static Function AfterCompiledHook() gClaudeHelperCompileCounter += 1 + if(!modifiedBefore) + ExperimentModified 0 + endif + return 0 End #endif // IGOR_PRO_BRIDGE diff --git a/Packages/doc/igor-pro-bridge.rst b/Packages/doc/igor-pro-bridge.rst index 62c1723ff3..b9aed52732 100644 --- a/Packages/doc/igor-pro-bridge.rst +++ b/Packages/doc/igor-pro-bridge.rst @@ -46,8 +46,9 @@ Requirements - Igor Pro 9.00 or later, running on Windows. The Automation Server is already included in Igor Pro 9; ``RELOAD CHANGED PROCS``, which ``reload_and_compile_procedures`` depends on, was introduced in Igor Pro 9.00 and sets the actual minimum version. -- Igor Pro must already be running before a tool call is made; the bridge attaches to - the running instance, it does not launch Igor. +- Most tools require Igor Pro to already be running; the bridge attaches to the + running instance via COM. If needed, ``launch_igor_pro_unattended`` can start + Igor Pro itself (after ``configure_igor_launch``) -- see below. - **Both Igor Pro and the bridge's Python process must run elevated (as Administrator)**. This is a hard Windows COM requirement documented verbatim in Igor's own Automation Server reference and is not optional. Note that reopening @@ -111,6 +112,22 @@ Available tools Returns the data of an existing 1D Igor wave (numeric or text) as a list. Complex and multi-dimensional waves are not supported. +``load_experiment(file_path)`` + Loads an Igor Pro experiment file (``.pxp``) into the running instance, replacing + whatever experiment is currently open -- equivalent to File -> Open Experiment. + Calls the COM ``IApplication.LoadExperiment`` method directly (with + ``loadType=ipLoadTypeOpen``) rather than going through ``Execute2``, since neither + ``LoadExperiment`` nor ``OpenFile`` exist anywhere in Igor's own procedure/macro + language (confirmed against ``Igor Reference.ihf``) -- they are Automation-only + methods, the same way ``Quit`` turned out to be. Does **not** save changes to the + currently-open experiment first; call ``execute_igor_command('SaveExperiment')`` + beforehand if that matters. Disables the Debugger for the duration of the call + and restores it afterward, since loading an experiment runs its recreation + procedures and startup hooks (e.g. MIES's ``IgorStartOrNewHook``) and this call + bypasses the usual ``_execute2``-based Debugger protection. Call + ``get_environment_summary()`` afterward, since loading a different experiment can + change everything about the live environment. + ``check_bridge_health()`` Diagnoses exactly why the bridge can't reach Igor Pro, distinguishing three separate failure modes: this process not running elevated, no Igor Pro COM object registered @@ -118,6 +135,25 @@ Available tools leaving a stale registration that reconnecting alone can't fix). Run this first whenever something doesn't work. +``get_bridge_version()`` + Returns the version of this Igor Pro Bridge build that is actually running in the + current Claude Desktop session (``{"version": "1.22.0"}``). Added because there was + previously no way to confirm from inside a conversation which ``.mcpb`` build ended + up loaded after an install/restart -- useful before relying on a specific recent + fix or behavior change. + +``close_data_browser()`` + Closes Igor Pro's own built-in (stock) Data Browser window, if one is currently + open, via ``ModifyBrowser close``. **Not** the MIES-specific ``DB_*`` DataBrowser + panel (``DB_OpenDataBrowser`` in ``MIES_DataBrowser.ipf``) -- this targets only + Igor's integrated Data Browser feature, which exists even without MIES loaded. + Added as a precaution after an open Data Browser was reported to sometimes cause + Igor Pro to crash while procedure code (e.g. a reload/compile cycle or a test run) + is running. On-demand only -- not called automatically by any other tool here. + Returns ``{"was_open": ..., "closed": ...}``; calling it when no Data Browser is + open is a safe no-op (``ModifyBrowser close``'s "The Data Browser must be active." + error is caught and treated as an expected outcome, not a failure). + ``check_compilation_state()`` Reports whether Igor's procedure code is currently compiled or uncompiled, using the same technique as ``IsProcGlobalCompiled()`` in @@ -147,16 +183,20 @@ Available tools ``dismiss_compile_error_dialog()`` Attempts to close a stuck Igor Pro dialog by posting a simulated Escape key press directly to it (via ``PostMessage``), targeting a visible window owned by an Igor - Pro process that either has class ``"#32770"`` (the standard Windows dialog class) - or a title matching a known stuck-dialog title -- this does **not** require or - change OS focus/foreground state. **Confirmed live against both Igor Pro 10.03 and - Igor Pro 9.06**: the compile-error dialog is titled exactly *"Function Compilation - Error"* and is a Qt window (class ``"Qt693QWindowIcon"`` on 10.03), not a native - ``"#32770"`` dialog as originally assumed -- title matching is what actually finds - it on both major versions, and a *posted* (not real hardware) Escape successfully - closed it in both cases, with no focus/foreground change needed. Does **not** - recover the actual error message -- it only clears the dialog so work can - continue. See :ref:`igor_pro_bridge_compile_dialog`. + Pro process whose title matches a known stuck-dialog title -- this does **not** + require or change OS focus/foreground state. **Confirmed live against both Igor + Pro 10.03 and Igor Pro 9.06**: the compile-error dialog is titled exactly + *"Function Compilation Error"* and is a Qt window (class ``"Qt693QWindowIcon"`` + on 10.03), not a native ``"#32770"`` dialog -- title matching is what actually + finds it on both major versions, and a *posted* (not real hardware) Escape + successfully closed it in both cases, with no focus/foreground change needed. An + earlier version also matched any generic native ``"#32770"`` dialog regardless of + title; removed after a Copilot PR review correctly flagged it as a real risk (this + is called automatically from ``reload_and_compile_procedures``, so it could have + Escape-dismissed an unrelated native dialog, e.g. a save-changes confirmation) and + it was never actually needed, since the real dialog isn't ``"#32770"`` anyway. + Does **not** recover the actual error message -- it only clears the dialog so work + can continue. See :ref:`igor_pro_bridge_compile_dialog`. ``get_debugger_state()`` / ``set_debugger_enabled(enabled, ...)`` / ``restore_debugger_settings()`` Read, change, and restore Igor's Debugger settings (``DebuggerOptions``). Use @@ -171,6 +211,43 @@ Available tools ``#include``/``#define`` directives not present in any on-disk ``.ipf`` file), the top-level global data folder layout, and the current Debugger settings. +``configure_igor_launch(exe_path)`` + Records the full path to the Igor Pro executable to use for + ``launch_igor_pro_unattended``, for the rest of this bridge process's session. + There is no default or guessed path -- whatever agent is driving the bridge should + ask the user for this once, at the start of a session that might need to launch + Igor Pro, since the install location and version vary (this repo alone has been + tested against separately-named Igor Pro 9 and Igor Pro 10 installs). Session-scoped + like the history-capture refnum: resets if the bridge process itself restarts. + Returns the resolved path plus an ``"elevation_plan"`` describing which of the two + launch paths ``launch_igor_pro_unattended`` will take (see below) based on whether + this Python process is currently elevated. + +``launch_igor_pro_unattended(wait_for_ready_seconds=30.0)`` + Launches the configured executable with the ``/UNATTENDED`` command-line flag (see + :ref:`igor_pro_bridge_unattended_flag` below) and polls for it to become reachable + via COM. Requires ``configure_igor_launch`` to have been called first in the same + session. Refuses to launch (returns ``"launched": false`` rather than raising) if + an Igor Pro instance is already reachable via COM, since launching the executable + again with only ``/UNATTENDED`` (no ``/I``, ``/X``, ``/SN``, or file-path argument) + is documented to start a genuinely new instance rather than reuse the existing one + -- see "Calling Igor from Scripts" in ``Advanced Topics.ihf``. If this process is + already elevated, Igor Pro is launched as a direct child process, which inherits + that elevation automatically with no prompt; if not, it launches via + ``ShellExecute``'s ``"runas"`` verb instead, triggering a normal Windows UAC consent + dialog -- but this process itself remains unelevated afterward, so COM calls will + keep failing (see ``check_bridge_health``) until Claude Desktop itself is + relaunched as Administrator. The direct-child-process path also patches + ``COMSPEC`` into the child's environment if this Python process's own + environment is missing it -- confirmed necessary this session: without it, + MIES's own startup hook (``IgorStartOrNewHook`` -> ... -> + ``ExecuteGitForMIESVersion``, which shells out to git via + ``ExecuteScriptText`` using ``GetCmdPath()``/``COMSPEC`` to find ``cmd.exe``) + asserted on every launch via this path with *"We have git installed but could + not regenerate version.txt"*, even though a normal double-click/Start Menu + launch never hits it (an interactive login session always has ``COMSPEC`` + set). See ``SESSION_NOTES.md`` for the full diagnosis. + .. _igor_pro_bridge_unattended: Unattended execution caveats @@ -214,8 +291,7 @@ person closes that dialog by hand. There is no documented way to detect or dismiss this dialog via COM, but Escape closes it. ``dismiss_compile_error_dialog()`` exploits that: it enumerates top-level -windows for a visible one owned by an Igor Pro process that matches either window -class ``"#32770"`` (the standard native Windows dialog class) or a known +windows for a visible one owned by an Igor Pro process whose title matches a known stuck-dialog title, then posts ``WM_KEYDOWN``/``WM_KEYUP`` for Escape directly to it via ``PostMessage`` -- no foreground switch, no stolen focus. This works despite Igor Pro's elevated status specifically because this bridge's own process is also @@ -231,9 +307,14 @@ Compilation Error"* on both (observed class on 10.03: ``"Qt693QWindowIcon"``, a version-hash-looking string not worth matching on directly). Title matching is what actually finds it, and a *posted* Escape (not a real hardware key press) was confirmed to close it on both versions -- Qt's Windows platform layer reacts to the -posted message the same way it would a real key press. If a future window doesn't -match either signature, dismissal safely reports "not found" (along with a -diagnostic list of +posted message the same way it would a real key press. An earlier version also +matched any window with the generic native ``"#32770"`` dialog class regardless of +title; removed after a Copilot PR review correctly flagged it as a real risk, since +this is called automatically from ``reload_and_compile_procedures`` and could have +Escape-dismissed an unrelated native dialog (e.g. a save-changes confirmation) -- +and it was never actually needed, since the real dialog isn't ``"#32770"`` on +either version tested. If a future window's title doesn't match, dismissal safely +reports "not found" (along with a diagnostic list of every window Igor currently owns) rather than doing something incorrect. The trade-off either way: this recovers the ability to continue working, not the actual error message -- check the ``.ipf`` file's syntax directly, or have a human read the @@ -248,6 +329,47 @@ and close a stuck dialog, rather than silently retrying or only logging advisory that distinction was confirmed in practice to be what actually keeps an agent-driven/unattended workflow moving. +.. _igor_pro_bridge_unattended_flag: + +The ``/UNATTENDED`` launch flag and compile errors +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Igor Pro's own ``/UNATTENDED`` command-line flag (added in Igor Pro 9.00; see +"Calling Igor from Scripts" in ``Advanced Topics.ihf``) is documented only as +suppressing "certain interactions that are inconvenient for unattended +operations," with two concrete documented examples: the About Autosave dialog, and +(Igor Pro 10+) the license activation dialog. Nothing in Igor's help files ties it +to compile errors specifically. + +Empirically confirmed this session, however, against a live Igor Pro 9.06 instance +launched with ``/UNATTENDED``: it *also* suppresses the modal "Function Compilation +Error" dialog described above. A genuine syntax error introduced into an +actually-loaded procedure file produced ``reload_and_compile_procedures`` results of +``compiled: false`` / ``raw_function_info: "Procedures Not Compiled"``, while +``dismiss_compile_error_dialog``'s diagnostic window enumeration found no dialog +window at all -- only Igor's main window was visible. Instead, the compile error +appears as a plain line in Igor's history area, in the form +``::: error: `` (e.g. +``MIES_ClaudeHelper.ipf:46:7: error: expected terminating quote``), fully readable +via ``read_session_history``/the per-call ``history`` field. + +This makes an Igor Pro instance started with ``/UNATTENDED`` (e.g. via +``launch_igor_pro_unattended``) strictly better for this bridge's purposes than one +started normally: there is no dialog to dismiss at all, and the exact error message +is available programmatically, which the dialog-dismissal path never provided (it +only recovers the ability to continue, never the message itself). A bridge session +driving an ``/UNATTENDED`` Igor Pro instance should never need +``dismiss_compile_error_dialog`` in the first place. + +Methodological note: verify a target ``.ipf`` file is actually part of the +currently-loaded environment (``get_environment_summary()``'s +``included_procedure_files``) before editing it to test compile behavior. An +earlier attempt at this same test edited a file that turned out not to be included +in the loaded environment at all (no experiment file was open), so no compile ever +actually occurred -- which superficially looked like a real ``/UNATTENDED`` +behavior change but was really a no-op test. See ``SESSION_NOTES.md`` for the full +account. + .. _igor_pro_bridge_runtime_errors: Igor's runtime error model (why a failure doesn't mean execution stopped) @@ -327,8 +449,10 @@ Known limitations above. A compile-error dialog can usually be auto-dismissed via a posted Escape key press (see ``dismiss_compile_error_dialog``, confirmed live), but that recovers the ability to continue, not the error message itself; if a genuinely new/different - Igor popup shows up (not the known "Function Compilation Error" dialog or a native - ``"#32770"`` one), dismissal safely reports "not found" and a human is still needed. + Igor popup shows up (not the known "Function Compilation Error" title), dismissal + safely reports "not found" and a human is still needed -- title matching only, + deliberately, since matching any generic native dialog risked dismissing an + unrelated one (e.g. a save-changes confirmation). - ``get_wave`` supports 1D, real-valued waves only. - The pywin32 dynamic-dispatch calling convention for ``Execute2``'s multiple ``[out]`` parameters is assumed to follow the standard IDispatch convention (parameters come diff --git a/tools/igor-mcp-bridge/igor-pro-bridge-1.13.0.mcpb b/tools/igor-mcp-bridge/igor-pro-bridge-1.13.0.mcpb deleted file mode 100644 index 89adf7d8aebe5250238551e260cce66dd15685ba..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32023 zcmV)9K*hgMO9KQH00008090i5Tnr2=x;YjA05(4W01W^D0BvDzX=Y_}bS`RhZ*IL? zYi}Fbk^Mfu;zF~4DXmEQkz_U+WC4xr84FmCy|RM@SS`pV`;zRa+0FDrGBfah&pB0f zyPFgr@=TBqGqA|+d+XM%$Ej1rU%qkfWEo}2%$L>Z-WO$(XD1i#;3ko!>N?uX7pomF29Uc4GSw|9LxEs|AbUtQ%{ zRphCQTv2CPlFePSSdx`hluf<6Pa>C8rMr6n&Ry13zKr;``{)boalseeG+*L4ap`(x zH1p~D+|^lBRX&S-O!4#CxqC>eg`1{6DtzI5QDB?Gukxbeu6WrklCsK+H9n2vNi@B4 zRqn7uGF!VuOb3r|0s7dx-A6i=!$!ci20cdpwm-eOwow@~o0n<>b=U!n@ag zQqSkuyC;$?^8Btp&9hljEPZ_L7EuxV%*Xbw@8k3p*38qC0#=KtauH77hxV6AS^5%6 zE2|oEhBx$3aT2BZT(lP9^|Hd6Z^~)3^6t*BUB8d#ZC95jI0IyCl>SE%_r>#bm*wvL zM>or-b%|~A)MtE^-}=w1G?^wu`PARb;q$b- zxXt=*>_7WyUHK6`XJmFc7Axh2*$2=lENUK9|Ez4jz-4>aqoCtAy3)(>>GwFzG=*Mx z;_*zZaprJIgTY|zrcs*W0nu()2FjUx=oaSbXv+h41>?a1E?{UG?hG37vm{F@c#BJB zn=eD%&~Mjf`Eiy{@)!@}!lP=s5Ub4LQF-a1Ra!^s{cZ(}T6Y{nfqT@{ed@xMh%PQF z_h|IoO=@)rULIAjh9#BJB&B0O+5(|1$N#p@QRKDxBi|Xgv$H=#{0hGBCRoAGqB^Z$ z_C+Ux$fM=p?99E-+$>5`xH$ID;l*Uh7c|yM3pFYW9*OR%JFb)!DVbg1WT?e zECa_c@upY1z4IQ552hT=D$zV7!JWk^1i@ET0<~E9ig1pCp&_G-u2y(UwTrtSF+S1o z;2yNR7q4{>ehhmP%jjkG5&>@N@OkAwSEphtRC)wMEGu1Xy71GxktTrA!bfSfz`6)C z^9)xHANU-J|2{0%qF8+rc9K1-+(Ta6@i4tCuf$sr)Np7Kg&)~r&nW`qS(N%=%S*pZ#u(&C7 zDSCld>EmunXT^f!L9&{SaWGo9#7WpXE=ug&);p*2vqo;-gqj~u-iRj0Lo_Jzvh2(0 z_0jPqt?kMldyC{W?#Bh9BmxfdXLT{x$o$r)tLC{YI1}C)yIv7ZeScPXNKyE|*9hrw z+;1hsz_x#Se>3`U^M3Th&GqY_-i%LOG>;gcaf{<0p(!f=^u*1eu#D~?OAayU&XS*UYL7Wo6J(hnXdFtVJ&9}fPIG5Ea7kJK2kjIDZ$d{-5< zN3eT9`WIuS!QosA3GfF4kSiB?oo*!N2-eZ$VF}Y*I~Xw%s>j7L${oZIw674~H{Gwq zvtsjwuZlI}&e@sR`9UpRggcPztt$rsn#8?R62xXg83~Knd|AU5HgU}gT1Hq4U;^t? zW|X(9KC8pwly2$#bc(+FFQzkT)cyYHUE z_^D`ujTjKF!5SvGJbUW$2SPiX0=jEXDgmG@=`tyGNI1Z?qSXqQw8U3Lm1&-@9AuNj z^Xu?MwZNzI1#n9FS~h#9U-VAL^4vp&?HK>lj)ESq$R|ecHG{$U7otM+^3#D8e98 z6J2&G4Ch6p4tRw?5@!&|4```}#u0a%7JrrPHFO~VNGpOhL0+g0^oUX%G{_x@QjE_^ z$#ZBHrXz6|_l{EQUlBXm|cY6YR){k#b%&}n$a8mcJy*UiY zOVtUv_-?-H6Y!^Qp5_zSr=X}=p2m<+i=DmpBz42t88aseZ>Z}J zNZ5EeCoWvLLd6f@wDFx0D z_>MFapnd8WGLOGaW>^EYk{})};?B-)6squG<%K4&wh|sZ zf6inf-i{D$puDXn5y>F=`09a3$nqV1+~^tx zXbPxVRH1ga4P1tV7)^Od@m!YrEBP2ja^^l< zetLVVq@6^w<#aWc%fUkk60cs4QD$q*RLe>clpEFLU`GacEc5?k? zcwnUXQeW)bKgM)&hD=;PL~M)}Wl_vRZ9xRvicyt5oyqym&VD4g1{csK#-SA^Mkt(9 zturRwV_XP`haOgYgfK#)6(#LR-2j5aov@+FcMZ%0dBP zT9|h9*2=s_5p7f{9aLPVfqHNRr4roVYhN?fXl)Rx0mcSldKw6YNPQ$0v)+XK8tl6C zEFEMY1*-wTA}Y&V39!aEqjJ+KM@lzKRQE3i&j&A&qPB`#LS~~w!Qr*sCvYu`tJoL< zh5-D5^ui1&2|WaBJZfI*2P2}`S- za4XQJq$Y))3R_}RIa`eVcG686mW3ZGs(OW>Tu4j>7;yGDPK^^S@K!x6U0@?uw@Cm#>>ad7Vw?P9M`{urzUivzz|vALHUBIREtU} z0X5UX!*c-6N`1qJP=s~~nzBYz_ryJsTY`MU?Q6}1ctVFEy`$+ER|!-4J#Gv}28A>1 z(oR@pQvgL=)5cKHMrto-UsZuRfOTZk4KSQDY*Fm+-&!>&AAg{72&enq_UX1NvGYAe zqHb}Z)DvB^GDfS-s3T*Xw~6tc*~;>a9fd+4s+-~L`TolZXZ?Nmtj7{yK*qn8?k8oG z%@HO-hl7bN76Vm|U0IUwT3<7rBq&RFu?82hWyvoFEx~I~N7e9a`V1OLlF;!C+!3)|Xk<_!F=#M59JLgvRk0CkI#~W)B3`B8K;PMco{%M5 zEdD@_FN4d2(gNDF-KaWcW-Kv+`3!{L?MWx<=vm>mrf;|r(M%tCHjxF%MK=ud?%W^b z|F5UZFMz@b^QSQ4@CO1->#JGKuNC{F!k!iC<7yo=Ib(Z=TIXQ`E8nz<8Elc&%4D8J zNWR;6f&%0~SP8yuy2)S|i=4SxTemc5gnCMD*)h6@bsf|?|H*^GsOzn;C*xT;DmuEhrQFVjin$4-ELHyc@VKr zDEpJz0-J=m&=mMokYp4CmvEGTO|{LA`@VRBC7J`7c+xUkfj*{DbIGX(m{GRQT7}Xa zY=}2vjTuTD_$ZqY4Z3>XjkO00)evX{dVDH3Z;ZUUqr_55&IaF(Wt}FX5p!?V;YgU z1|WD5>tMUVB8@4<&eWU937{e&YD1Ht=^bo7ZOsDd{(r!xM(7Li*8rpTs6z5L92_dI zo!S%7)s&1_MX(q2A{2pJBTj7%glx4uI~A7@NH}hgzb_JNt;ZIHSef!b$M0OC0AR}7$F_tiZBtP!aKGnXbPn8UBVZ5G zAlBWvJ<&7Xo2?+YUPCaC;JZ)=u-a0*g0%3%4s>8D8H1A~jhQ z;O$2ak(vT^O{?j&0-{)wmhP=EC?N4jF>gGtD%9!>yWDvs3c>`?F)@lr2tp9>{^l76 z$xxgtjfdkXIVcFCh!Xm&iK7xUMF_6ok!Kyb4rCu;1cCed6xOTel;uLl^`1+Ay*R*q5ACuTaO4 z@tUP9_d-+Nq>~3h^QFi^xP1=?2t!znVwm*uiSVw_#{+VFfC$>&H{+G~4(+3Dli`dI ziC)Y^LD~g2TuA7#YUUW8Rhot%mJ*@V;j~InDB+5j*G?uzw2xE+b)x#vJRAt%4gqD9 z%K|_XL{rDMgv6T4^oiBw93j9dxi8lAQgPBSct_wo^E;nmP7<}X8Q~D5$FP}tK~*nB z6J{Dv4i^bf-nO3uy8glS8V)Xa2)Iy+?>UiIAp63>AO5?Ee?{!4EU%`pMR5PouzuhOIZ=h z7UxTKeq}7Ok9Dxcdbmaoz>j6XIq7nbvbT;ciEi$rv_?h^YvWM}7=$7OB4dz7ft`RAvX>z+!`_K4xYVcN{{Yc7JIvJavbIN6A{s&yL zr8}{<8@5+G=|d_m)HX&kusjw_q_=kHrW8zcd*=ZT>7*f=4?Ih#NT7ejRD>_dyE;M6 zEl|o~1X8S0Waj7;ig2w-IdEO+h02T%OAcHJrIJBa9f(HyK&VGq%nAO^nEpE)ECQH& zi|Q^X70a$fC(`uOt!?8IBf9ulb4ELuEc$BjM}46c zN#=i>Wt+Fcz3hrXt&+AT+il*vWh=X7cXw*s%Djz{_W-RhE!mEWZ%&G9ckS_H_>K5^ z>3*>|f*}&M{x3A;;0dIUnOI3YeqEsJ`2i}Lt)EaJI7t>>=XLmz1*-d|z*OL%<3^b> zv0DG{m`%6A^CR(oU{|Nwf6`5L-rjp@A8kr~*Zcpf8TI`aq|;}|2G9#OZ~ zx{g+Z#3O8#6_(w>@^*BbPhq-z0*ps;z@WqtQFZ{%y;6Z(kooh5t>}!Na}ZEaa_d4U zt`@>ZD#+^2tTXVcapApZ)stj=)Dqk)=apdTE~P?we%&PbVaX1H{sXtWF=^guIqsP= zM`GWwXl;KVst~1xM-?BK;+84z{ln5nm}2FLakab0o@LOvnPomS^4$5IOk+9=7`0Yr z6V3{?)!J9oWLg$7{`Pj4U8hGb_mF<~-tA>O@xEu+I!w4rh~8K8fZusa#e*66&XhJj ze(G>`fRww6?;}4~W2vLRSVI*zaPL|^=nlTl9|n*y;PA}aDT$&CikKU!m!{e94Z0g7T!?Iz=Jv5^4ada_>xt9NPl(c+?cf6*4S#;}c=Psj^WHA<*GxJeIn1su z_TJ-sV)C)$bVi_gshEPso!c0iWmd8iI;yyD_$rJ|$#j#pdLIujg?Xi;F?Wx)4&08s zv6=GQi51oeycQ{q9$)&`j$a=)svS-G{a|O~mz;Zla(U5CZ2#s_?d{268@LFG0lyWK z$>85On|;}SWTGU`gw(a0cm|?~6 z43^8-IOS?>!6jAN399FNdZ=qX~V) zxqvvb)$X9?Hp`i~;Y((_*@GYa-8E&1Ra#IA7875@Tp@l?O9KQH00008090i5T!x9A zY~W-700=w*01W^D0CRFSip2h;;k zk6u=LtL5x^xE#->)kFLB!)kdoTvp5BW7;dp`(>dEZ3x~=DRRexB{F`If+jmGo(d^!18 zUCd|KczazbZ^JYtLmG2Y5RP1_7DEBZ*#+;V256PTwcwl*qr)5 zSL64?2@Z~j&96qY^VK!}#`M1by1E%Iv6yM~7Q@*<{YTxC+hl+g!(^>h@}UeuXW@Bb~M;`1x=$;b~ph*qqT~yV}`V+r`CrRAc@(+(wW&UWb{1_P`snDgGbZH9Wu4na?ot zV#dM5GLt!0{W0<6a5~Dc2E{E0G@ciAA3R}xzFK)o)%ZutUSYt34^xle;Cig5-`^&e+4170d z8}HmERU8y7Ja~qcnTYI9hu8Jd{MB%I1vdDP>L7nLX#Bx>^>6V%hm9Zb4EYtdvH9iO z>Us!n0WnOg;n`v~0ZCHGTvh#ga5<=Y^Vw|qVN77Z?P;TRjKk_;1r5@C57=WkzPwtJ#%Gl+z_vmY2E#uaV`C|*7kw9WT=2=q zn;JY3|9k{3GrhzIhpX#jD39{*r+D4<6dDsJm_B?l{8-PMk6o>%?-us%=6fJlR@W8e z@(K*pKhOFiG#r)DBOW|N(pCMzAKf-}H%^m0fxP4=_Mfk+$F1+5&PX^P^7kJee~y3P zjn`-6sWB-<#iQ!z_yI&V^~!KM!wly3%I(&G&nGiT#o~n@9)Hn#HmMtr{J!=dQm8RrkTP zlO1g}dAIp32prVCSl!%UYM>72AW$y|i_eglU(G<{-}A?2IsKV3RR>O{TtJp$%rwLQ zX~z!%OZ7^zeFaXU%*DWxNwlb{kJLho8dlc$db~6m0ZI5Pnw-=*mp(ZF+&8K}Jc1Y_ zHGm)Vl~gWGh)KP3!ym+oF;9vwh&5mCf`3m;7E|%|Ah|)5VH22)yq(QQ7!1+_|AIM6 zOMtd6e_HJ3Vs*CU2J|mTy}LH0UA%p_UY-x|>(8J0hVe~}1(Lz;SlzmQ-MCWBhjih~ zY`pPN8sif>3uE+8!YUe0ATmQskT8yiw=rE(?r!sZ_|e|Nm@^Eo8%a-@iL!GvCzv>x z72f`SJcNXOz4v^7@A!D{$49X5VGwZjVQDJ%cWX z@?Z2se4Bretgrl5sFV5P3N{or{XZcBNsclU-7u5l$i&OH;7|BRVTTT?AFgIN1T8HO zJCyl=ji-@Hbu}7^11CdhWb=P6h8SRON`=dq3}Khu)AMxtzWEh(* zN`(mPvb2P`m*z26Qf21ajjm}0Lk+@e8}z8EDXG3bI{DUi%A5me+L`uX@9H6>Fz2L&iIF=12qM% zhDq!b+0`aBW8) zVK70IjNH#LLIYK71?ft6Uc>MjLjYHQgI)f2kj@;25r}BP)z`2tr!@CqV8t-S%bYb} ziEg#LtFHIwA8K`AOT`4>4q=?};)%^*hvH=`PlKDato}BhKH|5a7FPrmFrf!*C+hj1 zlP8DIK2ozl1;8qX7?9T35|b=fT<-AZ266x+V8=OvZ3^ZMX?BeT&ujQAQ!^>J6`*E! zRErJZ>1>IMF&hXjJ}{rHF0U|Gw;tchqcCqJ`6<~yamyDfdJIAsB#-Fvqb*p>u+rtj z1|8A~9l;`pG(@am2eJi31Md;b@Ph{jTn*oksi!&D1$Hqx%pic#7R7=lw*LX~2CTPm ze!|P7&jwB5S3zb&Rt$!MBCXTd%!fRm%vPh_ zr!&3>znqTFW*-JyjJa-&uQ{9QoEG6C{+gwKLG#8xp|b1ryr{478&Tu%X59E2Th;nI zgm~lQ%T@DdesEh`nm@>H;1og|gv9R;KgXZgy&@s%R>mLt*xz5eNml`7pLc|Iijq~Q{Rcxpv}Jf zyg0jKQz9qxRlWNh)-U86Bbw_5jZ-|n=%RED*Q9nCFPxB#2sw(t1H>VC1(E4`j%P*i z{N)>}S)-l-&TcfB44^V5lP|v*94zqVd~d435eWssi*l2m;XA1gZxJG)0AhGcgFfUk z;5yg>z|iz=Y|>-z&^Uv?ftMdaG2`?(?8)q1 zJ*8Kme;iD~;;yPjiYik=n2fNv8)7dHhx4;BHfjD5fqO;;Ofsiw<|&$f&dC7R5m4PU zuKp}}f=Hbf=D&YrhetZ#s8S?UYO9M2co<>chH5hT#j|ndpTkqr549sU<@y88lxYMD zHev;NhA=I$Ti@$M5Bca_2ef^5^B177RINzfLGW%rlr#IG~l6! zcvf7P;usFi0^e)Qd=kU+`3yUm2E~DW#Lis|XlOLdxD|MpSxn?}xo(=5^?c z;K4#|CK={|^-rs6#8mURMK-RLzB5B6yOeSVlW}QIdBG1(oq6P#fn+79d`=NIn;6?B zFV91WN(3CgdTK^G?H4=hpvPj|AkCn0ks^ugK^Kudpini^<+i|{NaQP`D+U(*VWnJm z7xC$!7K3D~Rf-^d1p{CJ(*+Z^FSfTJXg8lRX{Y;$aeVtvJix%YBuxUwhHD@}U2J%V zwz|jMT!A8OA-&p%m_4sg8JUsE_LWF5tI$1VuRO=-8LOSmS!5mqPO{W|YgtDwE!=3Nl(au^zlM+`}gfv!Kh5IX<_*%jVb}=*7NcF~L;UM}K7<%&QY|zz4FpCbG;xZvoQuCC zv(q=ElDES#nS@yH0=rA=L6V6=$%8XVD(xhv#50thnqr8CLMyy%+lSRO1(bTS>CBz? z$Of0S1AeXTRj>>O5Vdu63fev8YxAL40KKW-E+?}yjH1movjd6H=2>QS)i9m?Vf(Bh zh}QIG`P+8lzQv;bmSbTWv`dRBNYH$TWq~2KNfS!gW^tIr;?pD$O%O;J-GO8Y8C1O- zzJn!%4ThV&K5YNg z;d*cy{V$8z|7q~}v0yujMFUWwL^Il z2#A%LJtB^Gk>{^Qq)8MdZqJGk+h(?|6qJNbHRF~5_$0CMmXE!K_d6exwYL#NOHPwk zkweEf<8wqSpznPNp8L=&G2fXu$7l7!mFLoEjt?&sgC5NwMDY9wBHrS)PZC-KBU!qu z5F{Q?GQn;_Wg$Lj12(uc<4a~m5OV2q;gyI2wVO~+VT=LZa%$qQhMKtHp@}FIR3A>@ zqaw~?`Amxg$)L-k)Am=Rp3UJ=R(<7pIC<*%Cz+t>ZkV{I?U*FKC%FXZLZm<^i1c}l zh^tULFb!Z&ApV@izEKE6s+9KT4#IwDz)6ttqDyRW5(PL-N1HT92z~_{3=eSgAS2GN zlq%pre0@58m?0%KiM$dv=VpQ^t;Q>*ogKjN40Ohnq*T4od|^JjL8y00 zeTN*{Xl#DGhdv{v<*O7E2+aWE#j2p&j6x?f1P4FqFC`u=y`e5bC^XGRgOY|tRtjrx zDC|)t%J9Prp+`;``+P!dMwTD_<9ZvmEOd`{T{%}t2V*D+(Dd7!JbUQQ*uyyXkQQY) zK=y&B52^z8f|Lh#-!LJH&)g#CL~NL(60H>k!Wd~xD>e~1URYF|Hb#-v=Xnm`=bb|j zkh&qLaCVB(a;k3BRj`t1HZt}-{b+m$R@upx0We_p48U8&AZhbF;qDRkl(UK~#FeQI z(TH)&`8h%@Q!0v&)w>#5)D;uZAR1wG-p0P#o;%othRp4b*vus2?pva4-Zsg~jfa9E zY+LYTBwLD2cASnqqm_K6M>Cj6N9x!Th9mIEnhDckOc(e3J;2ORq5dlmYnOb^te2jEE`AN#vQe$a(7!#)3(9(CVAa-Nvj2+ExA?JA% z2%|J7ie%RXUI=l3=!AWxfcR$twllFzb3&0nure^r=D%ktv7oT_q-eZXGYZ`|Tw=t$ zL<7a(uzE;`HDAqP{;P&M#s5?MkMWu`rl2K$mLrS^DRePJ;N|&hu26&F6^ROOEHayu zF7#Sk&A5){Cu);UGH}z)WMyt8a1(CTH0L5bMd6JJ!6U>JRW8Ofh3dP=89aaU;ss^c z3Q2T@x-?F<5iB#N4Km9|Swxx$ljZP{tr^A?vY_}aS>DWci;8DaDTqM^XJv1*$PCjU z{j3NhfCv%@%VYNfdMX`$OCv;tFUDbvj<(uc{q>eW5VPh!t7-rl<%vsxVBNCw&#aI+`Wm&h_|nA4aA{)iN!7Dtwl_R z|1~Cd#*B7NL3v?0^QU#PO;<8JEGF<-5%Y2M!Cxlafv4zWIw!y|G zH9;J=o~2?olam?ojH=d7d!7tV8U&I#Sz0dlpeDEh;Is%<13<>N_1yc{sx0w%PV>K^ z!h^Q3F;;z%*BJYODhEr6@PG$f8<%LpqLtRV-?kJPE(u4KW)zaTTYjcP?T9#)i2+2k z%0$uP7TZE!mAfOOPiTbjSRj&No$u5au)Vlr#AYB&aBlwD1$av!beY-YKI5n}ycOnA zghav+Z{fbV%b-dcCSl*F2o3cI=vW9z48|0^#bgZst7MQ^j7Mm&BBO>BtqRp4IWBB= zi2%tpBqftwk{gqin8`i@lkaC^N)p(cvZKW9GQ%y|LWvo#?T+f)O)V*e^9iDI7ouey zNph~+{2MGaS^mEXJJX0807ZlUK4`$u2ahiRMDL{12kFOo7Fckah=FDeH?Oz1VpgBz zHUT;vma_O%wud06#G0`@C)Sp0!q5ha3G@|W1Q6$ml5Dophy8o)C^A|amQle#SAm>% z3gpu{>^6dT%ywye@QFbaS|)T9hb8wXS!qUGRbSsgL|Q5(0=b-@OErOdz zZ!d(_CrHjWs*T1X;rQB4{U)k)V(QU~mfngW;B%(Xgho?$lTs(P0RP ziYU?*7L3!%uzI!k`UGsF)6oF}3T2kEGt~MwrwEE8t0KrB$qegrdnfq~5j+D9OnGL@ zScTl~wpbX2@$JgS4HN%P-T>o~f85HAXdRNwJJKX?eUIN_FKUDYS*D2ZX{b04evLCS zoTI-cc_5wgYq#YRAZ+UL-jq(ea%0{7fja6mz=j^GH69IKE{rgxY+RAb3=1_?9(~{d z*9|k7o*>oDQp{yUX0An_#xA20>CHQ9KBJmkV!Wid-Q} z*?5M@wsWJ29#DoQ_n3c&F)kkosqiUhkY%CjH9ReF1L}lhW9r4v;n?D>Fd4Av1s8b) zkjZ4_t|Hj#sQ}H{hIFQEUk4f+Hw!Y_}i zpVPR%xMy&0t1p}D!0-nhW;f#B;6~h^+%Ae7v{cbrEYwUd7G+gGr?>n997A*OFM9SZ zrp*(7K-JK&+O{A3vUU=_okgzYo-u{O!2P)w&r$;cU+!ClW5{;<4Dyedbp8wYat{Ja zVu8+NWcXyUIH(ksKzl?09s%1MByAF=dl~-ijslmSN3%r13mYKs6~S6d^rO6t;adJV zSJ+Lm648ikbD32j=8zsj#&a05%Uz{N=YqRwcVIvk#+_JtF2NG5TBhnj;#1UkNfpK8 z2p+%YZR!IgEoFiyQ8?(htsZRCAg_!ucQ!CC_J*o+p49Z^7@PShIO}rk@n)H-lP<5000Exl#jx91=uUAU**q zw58)I@*7xrE))WiD{A{mKN`@OP#rD~pjK*6TEos$Lkxz1eQrB8H;E8dxQ^s^(2dTg z(MKkOqlQOtj>NBy&`qcOI_<}J%2H`7Z&f5K;r_6N^13uyVL8FBOlO-D3oxab`Z=Gh zq>O21tNIHeFDPA5ltdbyN&9#^M;Q%1a6$_TW04Z7HA$(g@rfLrJ6GjPX$UiLh9BeL z8L6fvF`>_vt?>eED*`v8lRJnoE=XG zv(PZps)%RH7mIB}c&Y3gzmBA~r_qtVW^&(ph3Evu#UL58u~t*&O>BA(tqSv@26T0u z_qt4M6NO=IGBi89HY}yq(TF_}7txUBwn7AW>yi}YYv<5?$EP46J`Sn}+<;L=jWYiT z{}9>L9c(}T@Va`;ikd&iEfbQ0B$Yio2d_A38z+doz54^IUsNzRdYI;CC5E<>aC9k% zTWcv6ZX-sY5iZR90|5rul7|b})dGT??S=D)1ndCZsYV$^zlmwu4TEaioQYk&v_UNr}Tlu}m381Az@qLkHV%(<8G607r=~`aDksOh$A*tx7sMRv-p#7Ifa(@@7E?Tm=70`jpTuN3 zI=M0I1D5Pds{i&TyNHnyh&8~j!cYLO*mN}{jK3*FmsR##TLn2o5^rYE(8Mt&_RTga z?5SK5&8`!St$PXWeKGz(Eo!kta~F-qq~3*lR)npvBErg$tO;s&BaDW*yQo5~;|!hA zT3voT2_UAzW@0N}+SF7ubmm4PUh3q=lDr0(#me`(k)yZ_FOJB%S_lY<(COWZBE3ww86ovfFS&XSesBKJqfg4orhy5hPL@`p@qSk(*N$p;UK=`Y`e~eWi zelDDcSL`bnxmd2S^)eZ&uRZN5V`B`v>eQL0>`C%rdgL8ok3P>6l?tZ8NO^*v({=ZK?Mbz$(Q-rR2dSKx2`z>s)|&_GJ>! zLkT2i2X}=^@vkG*Xn(mQt-n}B+uKrl2Y;pP&ezBhnEmEd=5fmg^wUk8Fut9-8=B8z z@=f0Z>Oly>J9e89KfEA^3mXX#qubpf1gZB`u_G|Y(&Sx-x6g(?U~58E4QDCXGQdb2 z6WdH5(5%=N?W@Sb8Q>igtqB&N&@6h7hu6SpKBSt0!5u(@8!D*C6G_||fvdpuma&_X z)V+u$)!#ll*!$-2=r#JTkh%6@rXuDrV~Hx5Uou{atuaw92s9vQll4k7I^nBzXC~hibqc04Ta!n!_W3{4Sc%p?8th5NS{q7$R#SSbPD93b33q(zpHw;b8iV=(dC=AX=mDo4g4D*-zF#f0Z0{#DA^iu42Cb&>V`4_z$?>jyLlZr* zmKJxG%EXg`esOBfP)>7=BA|#12C943?9FI(9nw2$o018^E7Xdp4xB32aFNK_(9w|- z$*>Q>##DVmL8T4%Bghz#B$oE0M`PzcJs>g{*@NX`_q*yJm_7>PJ6kzE@F+2QOCfc^ zP{PB9q>?-9`#Pk|e}g*D%NZ|<@cA!xQTRicjCm7Usz z(X{%YAfEt3SuYQ!qbV`uXNCj>_SxkmTv)kP4TGq#Ji!;Vq54eK(j2#hbyyMJNk9X- zjKqK{O{M>wIqEK;PSM{v3Fu}*wc)rb!qwSX8o8qwn4P(>kiUn$V3kf^4;O4zs73IU zlH3svlhASlTpIvm^OVz&(ag({4uf8TH%IuBhIa`B3J8zV7V0W!!MZeL-m~0RZ3y2# zV)y~oxd*H#jhPW3%%Qqb(}lzm@?lW*4`I3*VKf^EXFQtsMobw3LaM}0gb?{)9)N#s z6V1kB%mn&AnC2cHik&7-&eo#oKPT z&DyYG2=kgEo^D=fw`1hJ0#K&3<_LuYeQ#H@kiP5gW(kb=4GHRiHU5)H>J9F$4_`RB z<;+qV*r%9*mfuURZOKP+ zJNlo>{;-Cl)C2rApR&pmoFd@h5{B#t8HHq43g|GOR+DKf!BAaX(P^5BNG+yA=Cm46 zr|KjXfF5@B#o0oql6UB*C^T48}nl!l;0ow2x`QM8xFkNl<0w!E@~ zyk)whRl|yud!5S6JUTW|J)7Y&wx0 zSXz&uGVkC}FK;SNJIZA94b_0J$g|S!4eGF^cCwMt{KndxI%67`@$`tx8W>#3HTT91-hyIWuh% zWtp)uqgEOJuyhU1mlY>vJ zEjj*ye&qI#=t0iU)O>scWIE=PF)2QZkV`R+|6wEj8hlMrl^N9wVE8`Y2#JGPfAzCZ zAF!ZvyjFkn{rJ0$v6*Ntdcvl~-)KtqEB*5q1v{K!JDIZt;`o}2&e)qe+I^xNwY5-Z zbMNOJy+U&B7@%GQ_P{eMMc^GAP}xGf-EOZeq1{9&dd7RnBSK;U47tHH3t*sK)_vdZ zg8bGxgxClXH`r7(n`&jfxz@c)%70B+`F$7t&rQ8-O50Tq(2c@t2RWWL;#VqUk0?i= zv_XntVMH;XQoH#{B)gF8M+3JnPL1vq?AO!FcgU3ey2^K#^zaXZrQmnFw zkf$tQ~eaSkY;PHO2A#4iZLI{@xnW2C$|? zqg`6UqS+m5WRdz*q<$hcr2F66fl{;Bu`2v(ttGJ&ln)l_${kvBSG*!LVIfl6Y;+D) zmn5vwVFxa+)^^cypqS?ss-S(BF?9;0o>9-=XnyzjyQ(T2EE)`QE_` zv_O*miAC~HY^pJWdk6Lths=cf#zWHAB7P8vLON?jb58JI!92Atx@+C`V4B|Dj1LxT z3f^+Czik|e&Ue|LFHhiavG~6;9i%0%JrRrLBP}Si4y@gew416sd-;hVS`goR_48BH zi97rGwMK>coEpuoHR$_fSH9L@zp6>!hR%Gfdf$X?UQ^$N0Jl@Iqi<-nc14=&sYc$e z*~RK#K>`$RW(D; zwXMo_Vd_K?tdoxF7}o5OGSa$j4Kay>z18p9D9SG~%-2l}x>@>^>r7!9OX`c!I*bRz zG>6Nh(7Cqx#g#)jWQTRPS`oVVgj_42-Ohi-Bz^YQMtK)VqdZ#@NwqzKsMiBn_<3Wx&TScb8M}y_9=&(RRv_aDoq(UC+s~K{##8U0nVezx&h3^(8Wa2YQ>q|7 z0J1F)lv-2-ix>tdq&x_P*G zbp5RZ3xvS+bNc?CJzP!>E*ip2KZ+y@!|xq*Z2~X- zI{hamMJSv+;}G%n!Y1fW3`ecH0n8Dbbc%pbDljT0;_IK3PU!M%2XbG4ca-Q8yVkA) zC*dBO)claNhedI?Q?^|vv$%Mr@7>lVafM0x`JHBnQQf3hT2&;6^ z5An1oV-}DM2^5jFrCHvw)`Zqc{yaqqc`Nc5sh%vSj;b&WiGLN~bt_k{OREit`DP|`hW%kG5A*0yjiY63+Uj$YRN5NsS%FAxQlM;1?n-NtkxTJI?qtNg_ zhe2Q!GSsx8?V6Z4nNT^Z2CY9dj^{8M)!d%wp(QDw*k72*E;s`_KTfReAT^IK zLGQZJF6mjWQF)1b-71NyE65pvgJCJar6f~%vw+(0vZ9Q*R7W{<1D=>?VZ=uU2yZ#v zI6em-r;o+z-0UE~WF`!dn=SrR6HXxYkXH5XvPdq703iyJU{%eJrKiANg~np>|M&Vh z*lhpi+<0{TvtCcAEDt&0!z&<3x4AQvq=E5loqnqQBU@l+8>){tu+liXQKP?{go@wB zD+?bF!J?j>Jr z2eN>v^+(qeKaO1?;&EvuU!-u)SroLV#Ag zZyioS)VoE-6`)6aix1n#<90!n@^~QDY;?@q zVydAh$RJl6n(caJfK1%i-XliFfgaP;_3OvMpT*zYhpYG0@a)O*w9p-r6_@7S+qBuG zK*yu0=0yRD{bjt%QAyAXh-qykFgWrB7LDlx1ibGwWT``-Y?y(x29;#ozA{1A7P(tz z!w{ewq{Ae~XRb6ibUKvIJum$qJg08 z=}OZzuL%~=7&)Zqs?#0)Es|4UK1L|SPFmb9i9*LcER;L|UN(1q>$0f`98>Q=$DGl% zKJPK|wat<-B0ujPD!gJtKJ#MF;K1j3me1xCMeVv4WIERF-fDZzj7YT&5um|W>tAm^ zyZkY_pOe;m=wDnRcT4qRZrFE&j~_g(DD-f2fo)kYGJN%#{TebFa^%N2IiezL?IFH5 zMg@?u9<6$x&=;KfhN#gCj~8?4=^(dyqZnvC>vV+0t6`_c2qE;^-V4T@Mmx_d;Rdsb3W?|29E;iPtnB8eM*R{xCCC?*!OSpo_M3UNDJQ$ zcj5#4#iZok8{XkDqyK}u?*njR-?|vz0_TcQ=lKqxd5vbbGGY)4yucATc=#GSj-!+% z7(N*Mq9@dcB(SHg&@>pG8c%|DZCn90sc8I*KJG0~^}mR4f1CYbzI|L=Uh9_U+5BwN zp!N#}2q1dmtMPHq$g~3hVORpSbwsV*l_(@6%d>E^br#K}i(71jM*7b@;^@10N^u7@)lYtqIYwOwx90M9axq&@bYxjJC_IV<-q0S+SCSBQ=J26vmUoV^v zA1%Pl64&JJf&hnJx*I{n4zG($oI(-|N!rV1(~*=vg95@>1cI2om%ykqH(dwUhvr z=C16rfLWgLZ!F)Cc>TWU_;q}%7? z_?l7#Oc}UCv3FZ`_AmDaB7{V~v4inh8AY-79bAex$l=mX5x?>uD%*ns9B9Hc0t1=ED{IxOf5vpk?F4MRsu2{3i1)NwpK38Aag zx1A+i%kACPW;*g)NsdlQ9!^e= zKtJMhJfr)QV#Ppy^dVsVB^L3U-sdVsGilmVXVZ{2} zkWtMB-zwwucjL@RW zL4p|f!z1GtMoQe!-FQqT6Q}7#Q?c1$F@a*|ASW!7Xwk&flUt4Y1h0?_e1I*)-I1D| z9}Sulni=4dC7N*WN$B?5*kW5x@{@5RW8+q+QNo68)NLzBtZMwbt+8FnEXg@*iR>`g z)ilu|CJjVm6^4PUe&h1IcfP_3W|K~_dQgFxRCR9(&Mp?JUeVhJG#R!yon=tdhvxna zxs2YTFhFP2OeKrR{na|flGmIRH=3X?6rV|XNhh$-;}cC&ORBt@g=wwQ=N;6~uNFn& z&5B!~a-$cz3`)r&2;+&Uu?}H+w})LH?fQj?fF3IKLBAu(eo#}&G0^EaJ@d#+w>5v_ zq&cv2mpHZV`ob!m393xX5!b*Z&b80|^8>K2>$)rWYf?m1YIfMvqoZh;HX_Fu-YHi| zzT!<9DVyY1?!eI-ltMi)`rBd}R>p4O2=+F}N*evk(tj*LxmM2+vcnU|BPhIj zs|rh6?ah8FUMuwUNo{@iKF^IlP55wDxtpOuIIw6dB~B7B^$KrD(~-h!K;*#c_Ht`= zUgBexQ2J9x{*7unXnD$BdteOisp)51Y!&EF3QrXJwPzKnLUxSJYIMrlO`BZ{&;mg3 zl?^O^VuouE4SwM_>I_P|Y6J=1UfMZxvnJnZNWFpP3VInSNh#t~y`OfH@Ne9OqU-Nv zrbzS2?vR^gFq1QF-bl3Oi&^|pE~%A=d($Tt^6dHhJ*XcAkI-lf4)EjP2njR5{L7MT z86iSdP1`-nFY&w2B#UK}0aZ67@<%+5h$5L_K5l~9F+_%1at-S14T%R+FRb>4i;nBTiR(>WWO+RUr17q$+3e;#_Of zf)J#CyGPhaJENlKI7;wAXcVCSg*!5u83Acyuah0glhv5jf<#zHU!<+5WUfI_=7pz8 z^$jnhxEq=F7E%#H7q2q~M50;$6KiR#=Y&CkShS5_LC>-L7w}@TJfiMCQ`0laqzdm- z!P@^ji=n{g#yr7W6lI<4!K2ZQA5E%GGfsjNo1;{%Fgu`dVLd|$CAxP6+QC_qSpU?8 zIEwL2lz1GGkR zT!Zv+Iq;UJlywGOS{{thu;07M|BHx$2!g7AN7?i7w}$`k_w@4dcaIs?l~jhnqM>86 zE0K)~Z$Y4=#x3AwXodf7TPFa*m;lRtIkxwEE9+2z;NN|bArBR`{#6`q86#Yy>=*vO z$2gZ^BSE$1(STu?j&`@-N+8>3Tj5)g)GS~kBz?V*6tp$Z(d6CqqibKO(VtqUEz7z{ z3C6dHt_YSuw5LW^XBet_FTq~){1ZvnXdTaPq*bD}SwYxO?HG?prue3|&GRZaA zjk8dOvu_0=)){+u6%aqGRk}`Znm4s|mB%ti54j5M5)m`g;6jKQjl#bgd#3jR#w1Fj z7!BGQclCFfl`#M0?^A1xP(NJac`kL_7tp2f6B%|edbqkSK2Y!`^73%DJ@FG5)U7c2QdA1e(2j(dxD~-APfpg`5|Qzo zw1C~2G={&bzwTjc^_s&S@~rgGQ2bin3qdWp-iRMI#UMl_WkcJlY(-13_O;0Wit;o> zHAR5EXFQ-N*0FPFV8Hs?&y?q*#9vvM!gOXF*%K7qi1$FQ`7M43R?=Yi1N|B{7RX$G zAl~17ln5T-tY#HV>5yn~hg8m^q(%sfGCe#FRviEAxe}N4Dz?}CMFj+Z6uyV8;}@=h zyX}I9O8OUf?l+bkg%o>v1Z!w2LpAXV`E0zQ9~I?OfQ&Iz^aqZ-u>amNRw2+Mg+ewc z=;2H1ua)qo z`g9GbPa{J2BX>^C5&X*|E-rDo!yIad?~kXs@PAKlmH$aW;us30x8xzpFGSJBh>MFtIJ3sOJmf+oFM-$z z;r{cJ%yb7Yh(}Kdf7OnUq*dBcOlwq4>)2dw$cbIl3jI~{@i+Sb2$NQw#h6G435d{Z zn6eKmAU$BbONJ4N(pA+$c`_v^el38o#1QKoq()tgDGR063BH2$cBa~5w(WW)5UPi` zOlcZEgN@s%kRoa_kgwC1K66E^m3kAX3J{qswDGg29!S5Q86Ub=Xi2>9WG<+~NN=LO z9?gcIA(J-R4=6`!8C8A8`(vmSO#e-lUq3Zee_nh;jckXHF!#RlK?)<0*`&G> z^xCC+h&M023_~em)E{!?F(cHq74;`IY(1|yLs>J~Lzdl#8j)G?-iVY!ycVhFS zKR*tRPc+e3$0b6U?*`}aH4e=!hEI0Gp-VPW1rK0U9Rav03vy{ATJM|NV&!BrkI|dC zV^;$mmg*3;0(DjKf|_9I%x;Etw|e2_;JMdKQFFKw&?h#wD}0!xllDx_dLtp1XzAY$ z5CO9P_CH@s8bb5+3WH0~4{!0x%-PtG(Fm;!Y+*ca>QTz~6dAws98LT{yyWVhvYXH^g@fmdK!%M|>)n2<%`w%)Y8Yls;f?t0da z9dI`RNw^|97$j+>%w^bQ1CEzljPoc7Y4-9csiQ4TPM#1Gg$)H=pBvv9KCJ1L?Px3b zJYuZtE`Y=q^d8fV6i|;a7ZgS*MgGu5Xq4D`C3adFSh3vqdNS`e$&29PRGp^`mip{&ow-M1U2RL2p z9UFqU+H7v3mWXx}VsdMqs6X_u1+MhY&>hI?N{%be-a&jkX)>-tQh?)n1iftS$f;;4XYudbt#9@Hot z2m6xXZ2o1o7(O6nWh|_llJIM*8oVD~*Ic~RIDyHLoEUHI>bP&r^ndXLQJ`IT88ns48HboUpi~bpUugS~Nr%<6>1K@xR$mf7Ysj7IqUzK0m;6w-NQrGo zQ3QU6)IT!)kbL}nJzC=XB{cwlhCts;*&KJ$1;4RlE*cb-9&P zug^4+`hBa56kTL_4OP zic&)ChGNl_`~HsN01dNx6&q=+J*jn3=ZfA&V%Z9byVaZnh*7J-9+}@gKbb4DT6P=% zJd|i%uppkiz-~ZjCM~)|i3_UK{Afs4zLQXtIo8|!qb9zr9VE4#OwiV1q3%fjm=1rk*?P7u_fz|1wUxW2*-YNmf7gIK0^I;D$JmindDeaL3Ca&d%d{BiI( zmA$4~6v0$Rbg$jAM@i2R4^Jjs@p1fin>;26{u4aL+4P0ZiDssxk;7dAf4n zcTqPB^@5TrY$cz@jP2$T524*lk2`qW5MjAN+>pR^RWzp8h&zOYD=u6^F#Fj2d9&+v zuDr`TP>_L#1Ke6fH+Z?4w`M@NQ#cxK0p=-B5w<*!u`}TZzMO&Rx%kf_d=+D8(LfPVBbtLLk9CKj{=0d@&Pc+;* zoITXCsbW8CErlqIasG)aMWQ~2vxjiH7u~2iw=EmoTX{=Ar(zZ0)NJ!&aN>}FXeYgW z+S}CruCK~V7q$Q@yU4?-ms(|B>|3k#$nTcy4h5#%Qy5?>i-3fR=8n|G={McwRO+^a zr9{iAaj?i8M^_&jzQUPa!b{W~wJ_k<<~*3E!!(Futs^OmbW2Vme)d->KloN+sa-ctk&$)W``ro7#a&Cy*h}bMl&rT z^Qx$TBdxc@mDIe-E`mfCCZ^YVC>!(pwaLg;#&+l`2c7ul^ry?|TZSDLsPPKNmH~L8 zXQwJCcRr`H?+v0AYCSAQM`TxoY;LXs#t~Ao^(7lJ>CW#H2=xF+uFW1K2+`aDS@2IYoy^4DJ7qz-*A~e`mCac19 z1f2K+g3^GDZ8F9}#ShtCr_J;jEGT22&$_nv-E+s=PSp^D6+fV=>wM>V=f1aXFd>7T zSRg=kC9!5T=7j1{r0B9KR<>Sb`Qbv8WMRg!T_sZYWjCT`P9% zg8i5#ItazITKTf%9_G9i9PrwNI=Js_v5X1q4xvz9N`s#=;T$^9yZ9KXpPkL$))Jb* zi`B^f#4BpZ?joj0q!%wIwj?9FC++T z0$u0IkVh>Kcyus)fp`Em$Hxh~iJW4%6x;%Wd37hJvqK!4#8qPBqPV0i>tIY7H>w$z z2o*;mrExO#Y6S@F!W+qzNV@JfA zc-0cKf*rg!*&_J_t=Xs8ZYkMD+$yAyVtq2x5p%{8k%9<v&UXiMjrbC1^os^yL^?&+Q~z!*NP;g1Ic! zB;A{U2P;jB&7u-xp0F|kf$Zy|gqQ}xP?C3$14z~jqLj1T7&oCPE*6_LkihUs#cLH5 z_3Z|9s6;2tL8Xa zK+Mz}%E>a$mYcW0wnQBY0;dl#kDCL4+KN#s@bv&QA+8n}8swiaj~xP)#A=k0K|BDo zX+j$QZ`GYpk?t!jJwcArN-2^bitiO*DpLZ)IXi(ltR6*!A+6?;jleglL{RbYoLFe< z4n?wrQ->xIc&(BSBo-T5jJZU!=ssA}=`+e%-9`kIA9zI-+nHhKEbYTu0BJMPv7eb0 z&N>Q#tVPk1<@U2U-PK_a>Dt$mPBC~Do1d#_w%NSmIJDTl+w{VdB zi6=?^cGC5)mQO3-OCL}K;bHW42H(RqLn)qFKol> z>>M167Ta>fQoU(Pjf5lIo#U}MsS21&iDaOu}fR;4sTVlmb2u*9;((_ypX%+~&{uPFsZWNLL zq!WR8h3qK?mmY3G)i%xH2*zrk%zadwjx4dbIav&K?koQ5*OX z0$FSP+#ot9_L7&zbVO`czS@$s`v10Wx%onePB;9Rsz zBHq&0w(N>U3k{X7Kzk}_i5_oD@ z{A+Ky8&11M&S$2fz6^W9(uk5Uo>bmpqX;Wq7w8Rkdb_|Vd5h?`&9}!Di1v4JY0U3S zG>EZpFxWR7)cbCGy*3H=|LJ?hwu@0d7eD{=^M}|Vey>WrMR&yvNxlzrcAa+Zn4R5rXE3O0Me%$sIYrus621M&@h6%OKCCGIp)5Yz)9|xwGomAVbdQPcY@NW zehD?Jqz5Y#7N$+6a_ZEl7O(N+og$WM@WQd5`j(-4FQWe)mA zT4><(l!qVk0mX|RQ7G-2Xs}lV)^gD}%h9*`VfAv=MxinDeng?cz{#GUl!SzV)PnKN zxM;CI{b>~+jZ9QC8N@KFYwfJimuF`zCJ~q;WI!}ULcYpl;y1z0MQ&w08Y{Nx(so|z zR6ts1-Ii!z)Vd51^)s;vn1M6QB7l`LK=BeWuEsOY+wbW8cSdHAsvG;Dx6wp%KcZuv zc2kpBSMwbQWS7@cErKZRM*-E2k!`GjTzryEnmyBDHLP-LO23oH$bJCDuw#i<5v4)4 z#AoE#DAIf~(9rF~)Od2m1~4F@0`X=N{+Pt(riU!ria^xZQ4p7~@q9!*CXgG3&8H}d zk*DE4kfU2kg4bSD#f>^?12QizcYOIt@3HLBGPKnK|B3Ek!0zw$a4-&D%pFD_my`-4 zlTsO4XT>pfO2EFxNjHxmq8vLL&}mpZR2m2FGFq!vOr?5|^ITW|e}=KBZ`zd!j{xMs z;+Pvplbg(*r7M~Z#)^}1p`D|6q%Q5dj!4{M1PV{$mw;CxGKnzBW6y3A;m1&XM10^j z*hJD87!8=9Z`O@JL~yFa>cu(qxIBGZIfKZ4j`%Mc6`;*E9^){b*qOOvNFZrOAk!p3 zIfYcMbMUtP9W6Rnl>y8eZ1GPzA)ySUxhXa|pDHgFHILb8F*01@=v!h0BS1_*!OFHr z?Vg?vP9eb9T<9s(_*V5kr6fP1bFf^;&~TN>?Dd5@?VaOrj! zLH0Ycyas37jW3GE%xdwunF#!6ZYr@e_(Euma4M(D7}AE(&<>tw>s+?naXEVCMEL>*FQL{+&cY4QeX?=X-+awJT-qO}F`qCy!@fZWnCAr7(0 zhf~) z0$8y4K!u$K8zi17>ujd-@5yW>AR4m$ zgZ@Bl4bevlO?z~lH1C_^9$BgKabCyqBl%2w4_^`_S8FXiQnA=>g0rjh%2K%QCBH0~ zWH(E$Zj$pcE;2*D#vzxk$ssZkT`hg+7fJixy|NK@$k)HGdCx4XXxVr1{(ZBK287=Y zw>xA^SY8Rhi+>80zhb-Pywun+;zl`4qi9)&zdD^7NUBC~uFI#*5nna?Q+Y?zk8H7i z`y}w+r^SI>@Jne#Q;pdG8+d5woN!_AQq{=3#jRUBfyrxO9<`N{-~w42g;D=K#Wjty zMd5V+Vp3+Oh8US0b4bLF6;aiW4U^a2m_|UX;x6SFO=PDF?BDHkDLeLf$Y1HT{+_o-5`?UZah&HL7 z_;WI4RyEHqcde@tXr)a`tno#4XOX@kA8`N@B~PxDL5%oPx;Z6iy$Vehw5;xiu+1Vg zX%(DaAh5|L|8loWcc!@0 z>9T%lyXGKJ4(>|bj?aV=(zkcKr*Zr53gcSX?BO~aI3vJTau6@`cD=CX(vKk5`+t#xFb&3{x(*|`pYpHdsY`fvLVn6P(t!Y#1ur+RyZKhSWyGFNC zvu#s*%k-!2A4}g19XBtp*6pUD<)zHX8}_E*EZDVXU3i|Vk-re&JkZ-#UyV%T9 zyRO4$nJ#(zV*4&z^DXbOU}|eX@r0Cuz%;C+9PLBI;pu(!4;g-YNrF~Jb3i9P9V+$r4Hz9s zcd6PRtV<~)XWua642-Ib4q-Pa5(Jq<{Dp%FICak+oX46l4^dx51t-PJo3!DKqOj)X z(BAsa;6ez-{gV0VlT3m=hmLd$_X8>^S3NM1xVEK~iV#{3{(c>cO9Cfm;VJbF_ zy6SWt1Ne0)*GP`4ss|-+e^@eNdwvPs3Kn!~k!1w{+kbC=RQ=e$8pBeud#fpD^Dj>G zwJ}La{vJ+qI_YeP+34q1L3;cr57H{q;kvqEyWqPSaPypT^<0bgZ8Y%U#I{?w@L zb@59vb2As(6E8_jpBeTo^8aZU*$tBdlU~12DKstmEQQ1VyL(Rbi5i@vYq5BlDGDw7wlESscz9@BHWFLS4$n|slU1OLuKj3Jr z8mt9?jfAXT_i~=6l9JZVu&2@cQ{*Cm;}Buml^s+>vEC2tHoOn<%}|Y`2jY@+;jpyZ z9U6H=p@2P)5XmrudD*HHAu1DvoKI_OQ62XXVw1MVR2p%tA1%?rx3b zLJJ3NhY%`yg)%grm%GSkx1W1=rLUwx#5j+HLQkld{gzFBu0cKJBqowKG@p`6XMepF zC9_)@eeyiN+otZKWf;k?=6)(%wAF@(rEbnGmo90kN_K~;&$+kjruZ%p&Z^E$hPS(( zx5698Or9^~#?2GWQg_QzqlR{7iczvVCyVH^6}!%vczFHZD`(JG4HFm^$;gOa*PIvJV?DJKKpFmb(I*Jsc-@S7i!qMeEV1Y3Uz`2 z=dnB8S0hbYrZRTdZOg`hB8jrByt3I3peJymrXgsLV|J!?c->@gM6P_)C40}FU3HVf?h7-}HrzT5pKev_MO-WJ6Z^#xa}y0L_v_b%0qkFy z9R{X=8ic+jcut7d7+(sE4!DJhWK12O(wqqvS2fD){5~H*ZD!Ea5q~I+z?$% zK{w8Z)DaDBIF!IZ$vPD8_%DH6Oi-Bst=cEL;M2ET=2zBPU8w$vBKa#a6$rUxqe5mr zp{7{6#$$RlhNmoQcKK(xI{LO6;K&H_EZ3a?i`@LnUhsEul8*ZDfcqBwkY0{4r>HCp z^q`pD<36u{=hxN!dekm6iO2<7D~O~ZWly#agBkE+B#JUF$?$SWQ?Z?i@!|W0eaFY1 zPZGDWJ>{o?b~K}w3scX_l!Tf`L=OY=q@2||_?}>9S|aEID@C@gPTekKc&cHy=SFX{ z`dQ<5HxHhQ+O`WzRVkAbcU%f~zeubr4~Bpmtw_pnC+&a`Bh?D}vyuvZ45Cz`)(~0K z7f-AOrGbnqDZ{a{KCyPAS?n>kaNr-1D>k;1i*SYl)Yf@m1dY@-XuC8=)(9tZT+pMP ztJaf!bNNJ)bUfXSOb}BE=fuDC%AGmJ^@XPj1w#pso>M)P*W^?IR55DDFPTCEY;x8v z;ql7du^;LyH*t_)GmYUTcfZ>;;FI|X(23wW{4)=S@5E4^ikQI0g%3$H|F|Zcb^oe) zpn7Tp0($x+(?J0JI=bIbz0{|Kak4t=90+w%F(Vw?=hZuYGNN*|bh@ziS1b1~&LP`; z8Mpkq8$Rzx=Q2CIr5ovB3%ur!gcVJv!OyBj5{f&`rsKU*#zC~pkN7cIRW@+#2v#w+ z#!;3r7+T->Ww$bl9Iovi@~`_hSiiT)OVrMsMKtc*4U*A7E9S``+ z^m~8n6+0Lyeo~Xcc&f;mJVihd7Vs|N)q2Tk6>LHbMT<56SN3|b0-r~ z_A(dr)Ts|6GEg4h$8D4I)#r2V{$Kd#!hCzpRg&fOnvYE9R{%qOx@4ffey~l7kQh_= z?Y<7ZgGc=3qrsJo1qGg%ex1lM3k22D^Ud-`o|2yeaOT;7{@>FzrkKrU>!Ah zuGh1RJc0&O zIsw%mTasI3fa6(+ouEB=pK$Eo58R(1W39!dJR>+VvdhkkmBt_C)v1T=PP`;YP^A)NX*wNy{!n1u#W zmmBj)XeKfb?imROtzaY+CWM25KeO5xHzUUy&|$+>C7lGBJ~07!p$>Lhg0)uUhy^Hj zC6i_Y*m@gDy50|E&ThauCu>{30s7lPC|h~aN+Wf(CG=2 zxUwn@43Z6%t$Fwrotv`Pa~+`XF)79tMVCI#wyqnO3aRIzh!SAHWfBc?CPi1bH?04V z2IGXlhIoEuJpX&iiO_I2r!`Gd9Z69?Jh|Krc07$rrV2Rd4)cuWg!2~#iHRy9Qc?OW zheFNA#VMN!LjmW*AdXC!!2DHnuFs}-FbgrEUM^C8LS65bl2^GRR8AZ4%%Ymanu3uz zy#1t_|F87vtfLdn(Zxl%XV#&mSV0Yvwj9i+8Yl-L=f3r13-)46$Kia<7J8vnStG|2+?b>G4RP*Q@3BB~`uDo*p ze9~(Mo)lk~@XMdxu)`Xw2Q%3A^!fvwXeHm;_YA_~uQaQFpSYKYzt~f*3R&f8P_7ve z;JM*CB*e~J%l~@qbs0pC_d6JoPN|BR?{iKs$?u??n6$Cy zq_;5#u0Lk9H$P=FM>Rh~K|Y03!fwx%+hv&CM7{ZYD4VjI1;yaU$Da}by(9AMf7z-V zwT-f3U$r+oI^t`d()m|0++~l$_7;8{eMV&-&^)P!oEe^3equEcLLmjQ>$;Cx-7G;Iaj4WMzb)}Uvx?r4{N-3#ktWhSF zbzEhFEhT5tS$6gHGaFS-FE~lF2Vro>B?+=jFJMk?`^*TQvlU+omUGAZ43GIt(1B7G zUa-#!jWgn<6h7D|n@{dFOs-MKSHOTpE$NV$G&TFW421B8U1s4NSa`7`Nh@}!>Bqt=DJ zd{P6JiWiYpPK+sX!*yCKVw2P80xshBs#-pD|^%_C4d{PKIgc8UKTZ6t9lWI1D7rD_Dz&j`; z5x5P`qHGykVJTC`@mg{OB@sbn0s&aNIXS{2Rmg^@IG%=H!I_2e0=lJTp->oVCWBsh zksBv)FE5@+OH>g3cg0H>FFplb4|FyiZanoN23GiPOo?6Ny3J?m2+LUkhn%bDJ;*B?jXPJ)`>yG7(AQD%|_j`wMu+*Y{NmpWTqmQ;%E%DbEa? zmD*D7+}Mnlf<23MpRFXkS~;c!fC1Lm2R7ANW!oIdW5#zpV!MxpqDl#z5UZslBOF;v zZlWaX!&zL-L}18xdaJZ*p%#NsOKG&CJQ&*wwBgGt{3ZWIl_HRhAn8sfg?jFz~&1L-;d1hVl(o6qz=G zI2tk!TuOdqB?pITG68sTrRS199anWrkaU2zN*4kq;tUa`;UuD4PvbHGyaP8w_Jn67Bgs>Mke2>&N=Dy} z9zUIS+`q`m)GNlJl~f-tQbaI&#*AP?G8U+fOT3k3{hL9C)u*W?5z7MvF3UB1 zY$3*-Q>nKK{RrWM_8@TiU3@XjbvE1>trBBhiYqdw+SdA%j1X!8|9v+UU0N z!0%g+?B^f&19kFncXI{DqZFiP%Q${w1e$Aup_AtM&d~l@0+}e-C%o_B)vG^keTBED z-MaLrL$P>`cnW-GJuWqRKE1Ri8wSCoT-Rq%4+tLRS)&b4<9N8Gl7O0-#q+<%-+1!d zZ(~H<5aQS2+e_l1e>Bo)CUnoM|4sh}dnkS2@`)^@TeoH*++Zl0%88)}&7c2to}9(~ z0|$`*4)wI?BI^0#4p+yEd*C#Oc+s)O#%P#V`XTu-2(*wKjp2>jMyig>t_i$#(#9SU zM{AlJkdLU*hKDx>^hg>j@yff zmW1!#Gq#pz+V4@nnePh}=nAY4@ea8jhPh)8*$eghzt~RLNz*y|G_th7X_N?ik`c2o zAK}pHcj4XIa}ET&)CS|`9LpC9BxYbcFlQQEfZU6YgHg@W0|U^7;;*AMa$v5`j)~PVThfgcCQ2DAQ%2<@BSUjio?9Im;nu?6iUY{wRr0Cb%oPN`8+di~DmPjCJud8!RxYYARbJq6SNc`?EwuhIn!wS+FF%j&#}D++FQcDt##fY(5IvT-yH?Kqz0SX<42)F2$@G1u z{bB#D$LCMze7Ri8yy68`%gLZwLyzU|o}KrAD-Z9z{!<@CaVd3Wi}iq}LnNZ&rJd{V ziRq4gd4Ip$-lDg7@)8Tl|81Qiqks=6;+E z+9n(O`LQOpSL6PE7FK-UjB3L}-EAUocF@ijfdBjhKs$2!IkgA=Siynq)agIjQ%vi5 zmzq1i#o?P^o#8Y(H{Va&(cfkg=^>~1ct}K6QLyQ{a1r)Dp={VMZXbI;y#5;g8S9E6izlS_W-RVB0igYB)t3T-icTB~b8iyqw|C1>2U> z^$j`trC|JY#OM<@gx74{t4+Ece>pT28cpaU&IQDgt#*5rrK{LxITJU0$!yj;@Pog* zrVO!43rfLa;)|Fo#1BwQ0|XQR000O8`fc}IYEsH8s)GOkYMlW94FCWDb8=%Zb7gXN zWpXZXd9;0NcU#wy<#+yyJMfwj#1=u>PSO*3)N5#ovbiFWGD+ESCs`0kT#|@DfQ^SJ zw%h-G_TE)>&bb!=#W+1)Sqe#9oO>R1>ba|`)9Gv-Tux`z+u5{w^WtrFHX98u>uO%l zKGw6X=Ntdq+S>ngGo9D-YEaFVlgVguS*3wSllfvWIj^h5w0bcfEQfW~#Ycn5o!)Ui z9@Ue@<7&V!&+A1sz0ltm)9HBrWb5p%8k{dUpdNT}^rqTdE~eLm#b`RIw(Z;7)#7Tf zs1}3qhdEyp?|w9|&X%L`VrMj|_AtuF`p;YCw@sejZpB6LBrXE(q(X2jSjPI(8+4LH3udDI&d@!yqM&tU~R)zo4 z`d>}Q!+Q4DzrGpG&+GAcFsY}@`CtF)FDzE`*S{8Xv#Iq)R~K8|p9UZ6H}zsRI-ftP zrni%7R$pK`+|vuZWN|f`W0x28>|$_UKdy$$8TU(n#+l49T(5dDom`A&*O)pUTFjqq zZS8P&mj^GORUOV1zx4KQZpNc?O`y}m1NhPe%jA|0u!FCU_xE1@TphnVJUlr3&Q_RC z_ja_ns=liiw$Ddrf9DswHa8p!cIfTh;%Yj<=G6bO9DN*&ad13rel(n(FR$@CruXBw z)y-gm#Z0RA7|sUjK3PA>$L}Bas&{iN7sK9;CSQMbKE3W4QS@Gp<{0Vxs{2^8tOvvD zcDfu7t8Gx>pNtkd7mbUpyBJMI^DDezv=|M>qyMzop4E5;UmuLeJgw^*n=_n0u6A}- zc5yx$)|medw-IEH*I{O$J=~4y1pkli8k}G0%%>Q6KILG#GUFLmeV6X!U^2|psE0h_ z{ZIAza#44`JvlwD&aW|pXPEG;-odjwnBir;cv8&;N*u>%=GZ+)Fy3n-YqVcP5 zy|}?GUtCmAaW#6qUgy0?iHv~cg4beC-|NIl16<>^F{}KJ!3C}v2zfY97ZoFqNBAp- zx~o?BPH|1^PlM|l@Ve$i{^w=qN%doY@bU7!T?W3JvyFE?CRH31SGe~QD>D|^oeZw) zquJZR;tJQ`->QTBQLpg}=heN%KZlJk@C^A8wz2u)`|5gt+X7;kRD-kmbPSRtlewz8 zb?>rQb!OA);@S0`4UZo%Mmt~ta{#cM#lx8-rdx=6Y=8{NSVnc{&2XwK8Elp ze}943T~8n}af0cOuLpPatohj0a`Itr?{2;ad}VoEfiEv{fx72eUWA0BFnYp+2TQuD zKl!7NP27#rBu~LF`4{`|dG)mQ`HLwD=TrXt)6*~U1KxOjHk#NaC98N+9UVUc%cfiz zOs1H@%wGApHQ=l96kM@*;isoxwH|pnU7n5W#v{LLJ#vDpSr5Ox!;!@!|9kBtCpdY& zj*ooVUYtGh8rvPCeARyB9gaD_ijVxR{i1lpUd2a#ziAYE6(9Kn9~t#P{$v(9QShXv zkSv1*MTK*(aml&sA3@b!T-x!Dwwiah`6&n-)IDF`++b>; z4#*%-F9?gzkeJ_2LE}I2%cePfL!PPw=cSy3mtxE`!~bQ+4+cx|O16E4n?#Y3*FPyaBj zN%jS{=F6S)_p$L}3f>MlH;6KH0^^al)7cP%fqUR5sH4;bsO$2p#cs}*XA5pX_kz^B zYg5|A+jr~5c@N+I@}+MW-_%$jFW5b+Ta~XHSBm+NE_|7dH$F*Yd`4zrjP7Y@MT0R| zW=IJV#_`}brc27*ZJrPRv^TfQ8H(49xTn-a={cGcR2<9-Z~r(NfWw~bz1rV9KHmHJ z3G{m?M4u>5MpVb4l|zEpV8%QZEz&4C6fBI};gMWVA<}W1eE58-uWH!Ho z4uwts53oRzqZCC~%p^E6@$xPB74}i+p}p#-t0@jaOUuI!q&{Hdsiaa|4Trjc;{hbH z**_Np3@|gH!exvH(9rDIdXGUQ+uJXW4^9tW?7gl|_D|ovt&aD9cz1BT|7QR2lt&s& z9k#_8u92$*LBZSGFa~&-Zk$k;1BK&3taN^dS6?T^*mO|}L}-_#Cd|Dwi?Na@Gt+K( zO)VH=5L#QWLs3mm_3hE=_qJ1J96-`ewFi5LFAEFOlrgDa*cK@2Vm%mkuN)Q>f^oyH z`ts?&JgHwsM*#T5TtGq{W4GfXRO(b#aI8Waq%N9o|7<0xKE1onwRss@MwBmF&ER z;xz&TuKohO{BIzg85AQB(VVNVp?m@wdVTzae)qp0t)v~VEy+40ZtHZUFPXOi+ z#u?3@*$j5bUbgZyxLJ$pFQds5ehO-FK|lr*a=>6 z73S*N<9k^YX00SUCEF*i`9egGKnT5L5j}mf1&tY6x@_2>LmHt&Xyo9A@D=QUw}5D1 zJz^Pt@VEh2gO4N1Y0hn)6*urg`0;imE?46949Fv?}| zQXJM5Hjy8n)l}edxd)d6ZaYY|HVK$+CNGxXb+}kckAv|*Jgp%yBELMHfCZ9?;7FVo z@zUb|akDitRsdc9g5RMbH+cjmJ-OitV+arwZg7pwk;wr)coI zp)E{}f$y~oSAl0k;>JGO`IC=;!UscHgnZcEhg*j`e}ZRjws!cR^sk%Ijd=<%{`L7y zjJ36OTwmhAA=hwOb?e;I^|GFSSWJD0d;Z&cIW?oz$FaQx?ZX;1UQrKWx04lU;67v$ z2kvVdFMEHV+iew7mI;g#+@c18A;%*S5nNT27xGfkzh=iqd$BrS&fra2wB{_=*$`af zz^)v29s2@>KrG8WU?6dK%r(Ee|7l_7Ox5jSqt0-x<$JKL!|76xV|TeE(B8knf#_f0 zZ{X`6>5QQuNskm`zN|k&@1yQTGtmc4AH%oBv*K$tWpgF&^7`%Ik2~(wT>Dr19a6}D z-}!Uz=ia|-38V%%v1||K(Adt~5BEsM-HuPlJyhuxH1_Qe~gEhC+d=h->0V=?xHhzvZg-mJ%h-twTe( zmeY=sC9Tu=vJZJao-T*GFQ&T1a~#~+^iyw(PWr9UHD^j0@kz^q0Hz zBecF6BV6!ry&I4pcImqMAJ40fcv;8ndGnqfFChl6YdN~Q`aA#YtiL+8XT6&{Zl^C& z!k!kTD~-W?Nruz{eZazD-}3OA9~{(kfaMM*+#ZS~7tK|ge;(|j)BUAf+W(mSs$-K( zjPt-5Dzb16EsJhw+#ziJWGBvMSbr?%7e^&WarNS$hk2Q3xtFBf(jpNhC3~ok^-$Avl-$Maw{P}X({92s;mPiR_)bqok{XoIx55K~% zAos1UgYS-x``G&a+vB6}4qifAS?h1#+WKO>+o6xpHJl=D*uQ{ynVg@=l-kLd*8WtsCet^>0r3_ty<{X_z8Hf>5&|z^gMDb#H}E- zc|_2`M>jG25mZOK{3}?~>2D|Icc>xpo>#wng3@!P z45NdNZnGE}`_KS=jMiVmGx9Xjt|4z+;F9_|H(Okp%R85l|=vz96{rXUlr`6?{O@A{f_OHS1lBbE)gZ zH-~GAaGCI&kd26wfe?UE00jW?*?NX&CB^y8cQiIlB=*1shvRV%hSPZb^;f-vIX;~2 zO*A;-fzq>d!ftKxoiqw>5h$Q>!XS?Z-R3gjKubhTAD>N{$5%v-lMOd4ugLWh`q&)o z7S^liMx*lOv|iY*&|sMspi9=j$MeZpC94`yd0Y|`uw;cF75 z*(Yh5d5UJGW(n(-5)4?n7`_qhBVMJ2`S+j1Wnf&aGINTi-SXlBeztJ(gn?#C8PCSW zEMp4B=qGa(Yc12}_T*nQ9q zv=~ke4+VCGjtcqSRt9udlz2{mu;_~|L%n+b8SX?CckLH(2x3z9HghFpRqv<0W1<@< zJMds(3nU)q;pH_9ti`+#S$w#Pn;oliiY8ueuIb`FICbWcV+P`t6rN;Z)3IIKc6 zn!@47Z(o?Jibk0obQrZJ%)+f2amVFXg8lRX=nWr^ie_Y$yOu-Wsv&-jGw(B~q-EJF8$;c%pl<2H6SW2txyILT7-tzd7sv~cPI6;w#2 zXDbgEI1>6ejnQHz8N3Dl4$}P$q6C7JZ{sIz@PJ1A?KmH3o*B+#FdlaWYhWj0U?gCw z4|i5zJ>P)jBNr){TVn3=4~I0AsOK8}Y})5eC5hTD z@I>&?GctJ2O&^vH3OPi3)uD;~3{w$4BT0Gt;v8&{E2S8R5WzYSS<32m7qcb$!7dxT zi3M|D+yVQ-jncTHFbq3}+kUv~_Ygmw&jw)QauvFSrh-7tS`#Nb%ZRi~UUur9@-^8$ ziYLJ-F~{ywdk|+LQ}Pf+;z~Qo)8aTC2WT)*L8cYHX4?mtE)iCNpAGECoZ9Z+=dyNi zUu%06Gy}8R`=H%EUz-ob!pKeeb~&D&VH9nysU3g=HP14OtA;)158K}wjA%t}mfvl| z*IHM!+cMEjg07RIa+NpVai2FQvBejOAsQnPGr9-Q89b@mn^ z|0B31ZCb{mKzWj3j531T%hdJjcY(cu3ea?u%Hj$Opv|HhNU*F9*%7P&OL zmkMcg*E+-G(g3v^Q%?@T9^TS7_6LtYJc9ISkSG*d9*kk5!g*|{9&>I>q06k(_E)2x z&0tYhU7;N~dCK``nV@NI7`vzLm^i+BEd*h51ZI8#jl0W(ge(56H<|V?c{oP+sHp>G+|Bl++~hN(fbW zeFfR88r;qfU~mRHBQQsz-lP7Itpw)T-V($&o8G|H0=El9Iqb)g+3_9#3}A<^QkOuO zJ`gWf1=(g4I-X8%K5s9jJ6c*p>jKFyKjpx~eoyQO*sT+(6XD8nor|L>w1ucncqx@%g zb`Nn?cCuywB(yyP2s4qm+B}Dyd7L>RHzF1fw_kE(+HLVTgLu}58d#zwVOSsL+OPh;AW8f^2{WwD$Wh2E&436t0NJ3LPUtC__X$yB z%y2r+6nX?*z65IpkvM#Q6(UFewh0{AOPfj8q`hzDB5elWIqvT1$|^Ub5%m?`$uBVB z9q0t+N53WJ$B`pg&J6+eRToq=x{&CWA?xsZLvLGT8N|mslfVRDVB8|%gVdxOc^vGT zaS?dI>DSTxL~W8u2JyWaFU_n3deF6+=3Im- zEUYo%ScMy=$Yp#*0@ZhsGkEpx^=tC5C7@(-b!nWeBUqqB8)TY~uryQ=#)|<1Wmq$m zDPV!{U0!)p+s!Lp#C+t%O%Ua5Z5H?_Dx_Z&ZUhiPf+D-`Ug-`3h22sK5#j6N&__pI z?Y(|`PdKD$b6-?7;QX>h1io!uE#|-Eu#Y??XSHsCht{ZDz|0;~UMlnhZ!g{0#Qn{6 zAqLge60hRJ&3qDnnD8Q~hs<@Sv>}E6r<+{Xnjtg+zG32xx6uqA`nBSnFZi zQbf4K9CIMhIQ4q|nGSW0$Egeqv}38a*cRHV+#M->84hGD8QS?yeF5EzJ4S>e+yv)l zpIzW?DHTgQT5vLJ@CeKXI2ql~hbZzfT?-$`2wMh_u9DL>rk;#rY6n*Hz7zHegj}h@xMonWUSuP3#4PMrP4>~ z%Xt=9aGHpvj#&1X-rn+AefG8~t--;W^UpY(+CWO3K7x+`>^xDD z%~tZT`=A{~L`%a4mdB9-I|cst9C{letfsrHfPLbECd_EaC=ScppLnGyaaDbN0~Tqa zlyLMK@)ca6NvaQKQnm5bwnS|rJ>*GF@Ha9t@8d~v#1_LGhUqY7X zr^bxAu(g7TI+)o7g2^zPLOkej6rNcj6G9nSqxz9=L1Z?xziz2xXB9y^k(30v6aWUF z6A2nh!-~whS7K>Ihan_zrfV=xD}(Co-pQ%McpU5!nzCQ0)lUjMkLVfebA2b-4dFZk z8d`B?i&zEUUazsx3*+0Bl^X{BoxA}_=lyXlH@tNSGVe%|y!SnRkG-e~zcGXj6&1gO z-{Oo6A?2@07RXxuwc9cY;5K!BZ$hVy3|lpSAdc25U;}s6+8vF%Tqt1**|;E;8Wv)z zfue1K7z)MAc02q?_p4UG@Y4^!Rv{0^!g{3~8@ygCr%ucq2n5KQPCmb;xGv_E4A@iv zlcc>9^-8;hSI;7to)Yg60rT&3Nw!M5S(Oh&RiD0DTZ8;M-OJe&h92CYW|sJ$eQOnS zEo=ZM6TMg=H2&o28eu2{)O_Jqm4;O8n^0jeq!}-S#mMAoD!3A3W>_5cCHX_0is@FC zMuEN5E`CG(uNA~{mBT2WoQTl@PHG~vFO$hSD#6NWuv)mgj9t14X3J_2Bsmht$#^0fg%SJ*d92iv|LGVIlEqYks2Gj||#>9)C!?F2W zp)z38i(BL#Kn9Z)y9#HkhXOQb<2(4SM%v(nXyo4`EFZFx%9tw=)5DHej zsnkggwap_Cer%3eUGVTE7u;fWw}l34bE}jol5|zDso&0)4Q$98n!rPb)U({c|G)a` zXFYihJ+E^J*52|*tvThy?wW$n&Gu%1%d9{UTZ7LgT?ka0(59L z0L6tMs=NI?Xgr=?0{cd>1t&0%yER}!T>AM9nRP4?jm1ZZQDC;dEwg_Gy=_4}f~F3Q zyE$NZqv&`M5DqIXqJ6GkC5`zZXH2HoXL3BA|B@%pKbXVY62y^DM&-L{2h3NjfbK6g z=rfoIzdoscN#p+NmcfIyzO1ezlP%~ln-NGWc=~X1yC`DN5=AS%P*c5_nq2*o-tsGO z49&g2=-8*2Hc$K!MMFbtd;I9vm6P!8EMhGW^eGey?k}BqmJ+D*EVc^AkoEQ{_}>tp z@uy#t6IdJzbS5RkCyT{Fq%cLtJp!-@SmY;3lTh7D_wRZyaA|oo6ZHaEJTMQ|N}wM} z)^ykM*O}aI;+615WSz^j0$mQtAw)a}!Mog4ig2z&qA?)T!%r=B9K#`iKOP6^KuO z3T^3Vg7^kjp3`N4UOuGB|2j1m_6+>IhwR%CFOYj3z7?VnrV0$x66CCf(5L(rAU|1idnyZ4N9zm1gMY ze7uw}rkbrBXt=x}bU{&KX?P~>Ofs?pwtyouF8p2x(sX+g<-{_EYHuK9hh0`XmmZ{7txSrwt@wC0k7obYsb)i$0r~mJ`Sn}+<;z2 zjWWA~eTeAl4z?e^cy$@fijqJ3EfbQ0B$X{Y2d_A3+f5L8dkrL{^eJt8xSi%_DIvC# zFm%a?TWcv6ZX-suEiTOL0|5q@KE?&?Y5_sc_QKgV!F!h4(psX%R0=mwHs>_cdU$4t zO91Hc5)Nvql`JJRI};t)#KpmIGvhgp>4=2ch>S{f&!9GRT=3ENqS5!VkWer}OdN~^ zv2Br<+sJ9x@}NOdaL|!Hr9R`cNQ2Od@)5)0N#|=1GDb%u3}8orj}q-nF;5ix8N)>Y z@C|QS7VM0|!g#~xh{NC1H9Z5}27~fcA>=rVz{#On&wPnzti~GIvt3mnPJ-ICTdn)7 z{mIu^h0W6WVMjCH6FLPb(O3=`aT6f~w-ln_$j*@I!QfhUE$)3h53)nnt6 z`5H1Uh39J*AWqhcw%L&y`bU~qlpavs3D`c+pm;cr4oTbQi7u=sqth);DU~g2asymQ zeJRMSK`*?YV!R=wQh2Iu(OdZv#rK{jfR+x$_eGkC-8My*0XQ^H*RuMJtnE!|1kMCL9xuytoXRdeMoZV8)kTu(N|V0er*0E*bX=~`8;!WVP^QEgZ$3?t6p@jq0U2KO z8{(DjFfi7Mby}ejok~&8ra6ldWeBB>u`k>PrTbw&2{Dn6l)9)DVQNylCn6C3D)1kD z74V-6=izm06pNfMme_i!jMcXuc9pU*f?n0PY`R2t zt-KNJ6FUan~MgYXea>U2+XiFez(TjXIVR3YeH2ckJ-RT zs%fH(8SKy++ES?q0)1^C49l6XpQQyiq_!oJj1>2wVxKxAfhV zr0z*9DgXB6!QOX=M<=L0#>=%2H5ERGDN|H&`NiWE*cuV#0!IU!Hd(GTy%WA#=f(vf z+mT(9YX8mK)1Qm8aVG=$uI@;yKC64^QQ?6&Ef5~;7-P~HtMHYfkZSTBI)~8(4y_L4 zPUI-J7~G{DX|m#UNwYAs6aYmxOS5|+Yehy1#{&ZUvH{IrOaH7oezb{pyCBGDUPYTuDs9A4RPY2dm;W!GJcsCXjk* z_$11?*BI0X$b+^9KqXY|5V$s;_Wf$1WjlXGDek|6Xi#twUnXX>9UNa5Z)lIRmdHRFtXI4MTQrb)00xuCO zd`L|an6u%5HB_%+G8W7LDrM*&0_>tCm1R5v_8B+@5s+12T}kP;sM$}!{wLG3DN@Yv z<=eDzm{_3LDL&Vd85XfD#bh~gy*rk@VvQixwT%eK4s_}I2o;Ag!#+N)GR|_ zUO6U3ZI#H9QFWA6D7NWPp8_{{3vIsn-dt-kT+l4&tw0^6E9;dCqiOj;K|TS7vRWPt zN0Vd7&I}F+?6dPpn6NUd8VXUNdE#DBhw3s^OLg1=)}cjsB>@fSJQ4#cHI?>rzESr9 zb&4(ri9t65stv<+bA2Ghj$&Zer^G`37W#r^I(<8svtX|l!Ba|nM>tGE%MEaC0F2F3 zPD4gXOGi5NItkt!?oSnED}X=&;ZfK^wRO!|mxj!HmWzuHVEcy;KcG4ffc2y?GXjJ$ zR99-&A+dyf=vCcAsIFdfr%^^fT8G);YWlYmGnto_$B}fb#k4N<0##!Ltd@|V1n@%nu>*TxwUH1?tASD0}aWxc)#9kvtpze!mOt7r<)bJ z-Z1i70Z3C?F@(Z^ez2)oaNkvPvjj%`h5&WI8vn^4^#*g-hcArWa%L$E>{HA@%!ao2 zOTl1sVBvoTS6i|Wd!zyDw&g4Tkw&$9aX@ke^}XV$^m|x zPg&*(P7!c$F+)~Gjzls`1$3BCtJQ*~U??xH=(j*Yq(%$Db6RZ}(&zq`*QxaTGb5ib zUvghTiBLEv0PU*EJbfSvtuVnPQbUlUPG8*4Fq$mjOa9VkaeC=N-Z;e3s-Z>7yiRFm z?wuiP8je1WhR$*W`N}$N3ybS?bNbybmtz>AM#l$kvvC$LjJgI(>mF3b9USWAtu?4e z8E+onA*9pJRJol&dkQX|B6>Pkc}(rLTPmHbM?2B@mOC8UwZp%VxHPVQ3$UG!rrg?8b7xNM~9eMZblVu;b`gH^VJ2mORMX~N2_yKJ>?1yN@ z!M~{o#|F%F%qN3Vd=x>KVjTa)M*2qpHbq)yq%Yvcclk!J9L)OpFFt+Xg3j?;{m%E} z_cq98thwk3n-;&LDcQI5qgCJZ8@u*)mPj05bI~ATQ*p@8i68+T12ssoID1s zSC5qyO-&Jc2M1Jk(r8z%EOThr9prd$eKs6C+ek&WR`0h0|B1ObWF%8*fckb>cl|61 z+FYq0Vk3lCVN*(N%B1!8J~-k0A4xhdcVxq={~M-W7H(gcEP%|DA*6E@_Uy7ePfH*+ zoFOdL*(jfwziXv^y1koEHw+rLFrNchGI6KIopE0Mx$#=4iXSbM0bX5=tg zN8NfRvDdq{6UmU}mH^$5VP`3)@_AD`PX!Nb?3OB%1~1Ey1x-C~)xbrSA6VKiX~-0~ zo5QUq}Z&%w> zw!uqgNrzB_p9Sz2+Slkd=%@k$jF-Q-lE&o5O7D;hcBO!lk zH0S5APGf}9#8{`ZKKdm;`|G0;6)h87Q2;|vX`Z34l0$HIra&c^y7e=neIgwWl4pFk z`e~ZcFU{-)bI8Nzpfs;s?ouCWls-IwQWih=kirlf9s(R_)}QkqDSjcCSD*gX>uM$) zIDN7%^Yt^?ukmrK20RmBSBkO2>_H5uIK=$s`*U6eI)ZteAK!2UI=_?-;*KD)4AeB` zL1Qxtx=_M^f#m{CNZraK1y3yY4+CO;H#zqXP=g7Q4^dH)%|h^a$Di$ZeiOT*I!+sW z2aZlDWGM;5dI*$bP^k(ZW6?wv1Hr`tK?u80siY3qX_q>-l6lkv)L4aeHodWn({}4vLdQf@Bcu%h;Scqa%CW9?$)BvEZhZ8CxH3T%U(T@|yV`i^h1nt&m<3!cCQ|SzJUtAFM?D6?P2Ku_z>NS|?FeO^7X1%x7~GHx@*)y-?E~ zxp$NA(SnOi!|MT4hnNmE7*+$c!+VgQ~JA_pPbMwyTnsj6oFr*JQ{6 z(RgK22_?4}lD1J=bQ`_2-R?3)j3LH|I;zyF`OAt1S9Ufw1A94-F&dJX!sb_htSV1x zMv_8VJSV}yyz7@s?&x+^pHrl#%P+u?+l>k4ncU(2B^{Kdo*=K6_-?&Yj{S3F6GI`x zOAWG<&e^0QoP{X>Y%N)x&~puCE3vte+6nzit%NKl2CqIeY8YxAo(MR-CFDnvG~Q&u9H;y&@;;E{0PG%0tT^Iak3K2;!)E&MgAXV<3J z7m7OhIr7q5Iv%IGP{A zGHx2L%Z^{Qf?@GksQkZh0_xVcdP=*~at|8&K3Y^pYe+35s?g(;>{h za)<~!Z*NBolwmH{uDL;cIF@mP=J{h-bV`7D`E7sOHwaG}LV(RnWNCccr_oq`7hB9x z`^DdSqniuw1>uY#;X9w`;T=Ti(Sp|jeR{j6Wf6UX$!UL3whYu67t}!uN(MJ*wQop1 z_>_))Nw@v5dE}{-kMRw6ycl53w_5O#AY*fH*e05oDtau)FiQn{vomJX%dP!@yF@*2 zT$xai^vZlG2v|Ihm7%>ILJR9sR82QA6LKPWT_JC(^E*J7RWHk!6TWGNfG3j>d}ijS zJUmFeMejZEK11RJYw}7-jC=rrV<}c~4Cb1$bf8x5F*A7(*EDvfFGOK2Lr6%10Mede z7ns2m?Z6d{PNsvJVm-=?I%)65v4hYzWn|(Y*I(LZ7MMf)JjIZOv5nx%Q;bHbCTd_N zc%FXap%phrqs*C|d_sE4SrjbSfaOUH6kKQb-*@JVJDenVIIL6UJp?E&_?21s-f%NX zIgc}^?5b3bRV^f~*=x9sn22LZQ#R&up+2A~k&Sf8txH`lJ;)FBFCBAwU&?{n41Yn6 z>0l0J9Z9EEH>*)A7bi&=-8xPlQET&*sKl;;yNss}F_gR(@?yKv?)WC;WCEoL+>B>B z$Qniw#lX${_7Hey4S{96W3%|vN;K@3ybaIP>W4Yawif%{EHn(%X%b*ZN`%j_hqx)F z%?N^MiKiCWIi*a3aUK!fd%S4pqup+%40!ek~={nfu`ceQZLcal`ZS?2L4HpyT zL`6a6m*pp=pGr4PSPQSc7l5Hg_kwU^F5x(l#}3zEMQb&FnsnC!rm4F8p>l%rl(jWn zy16RsqD<*BGHSWsS76rgE5is#$r5E}9#4_wJiM(@(Vq$yoJTWRHGW+ODQ^JS=*D3s zeQB+xZs;K1=fUNrIrz)LigNGNs^j?`xT~xNZ)->!z&axgcdg+jLSs8dv8h;g zDr#;+x_eVQP=>edScL(%{#I1Lqb5Mn`!cuYDe#eo)3(h?@wP8_XXM$$Kf{wv-3#f?Z}rR5J*^?N%4L32+Cat`*FjUy2OST+gD z6ZlIk{%^Ekwd9p2VzGRrRZm3+)^3Wno~qY26cRzS>Wn<7sgMFp_cj%3jSBXiy4|dF z3Hoenpw?jjP^X{`?SWW&cT<6t6$n8gSnO2n=o`WFwqwx4;f>^>W-E>V4idOvO`Xk6 zHLPu~x7Mh-zd#%n4L0%@T3eyc6w#Q16qR(4)z(E1+bW7UY!_;tj3M#<*_wi@1!=ev zcDe(j4`_YkbRMeRwQjb_-wY`&vx=nU-+&cYjNB@|g#Vl_)y;yzTfGpy` z)+n8s_wAlba{)k3)hcZ-Fd_Zc~YtxR3BpSB-W_OAT43PCmR--Nxk#}Sh zV2ka$P%AY)gBu)^291ELuuSl5#EwS55H;%bCCFBYAKB7OF;r5FSHZ!6x!F*&Paaq4 z`JmxZw(3F_n!4A(8up9in^U-n;ozyIP-{TFZ8r`)8`s(#U+aM6|BqAv~+mfHXG0rHd& zzw5txclhEIPW=w*-fcGWi|fO(%L}!rx!2Gyy!@+v(d>7NyTNs{5taSsR#>bc@X^h2 zh~n^G1mV^+KWrMM05p==y+|pWUZ*#C9klg#x5Dk99pYgejn=h<6Av#s+L3P!=UaCB zTJL5Jz4~FatX@Txc<5aD()61uJk6+bkN7fo9QTRq@kN;RW;_FwL|9A|_Kam_%#hY( zb~zcLy(nJrPkPtxC)NMf;)rJQ7i8%N`EZZcn$(lnt(<#8MXCV|s?+ zuvl*Y?a}Mk{e#2P{o^0^UiVM-UmP92JYg6A-tSuvogTc|KYDjsJoEeBQwcR{ThRy_ zIi%fTpBF)yK`dUt!9Pav?Fw+9zchqpA*J;YB_9 z-M5xcbuYoeBa1#Bin_>ahhTXWwzkqG(a##ztg7WC2l$TOn5!Kdb*4Vo5Fg%j6W&r# zs7puy=cQ=3!YA^x(qmi}cC_iY1Mv*2m;kl7taEp!-k{DE_a3gNC(*$r$5?EA!zFmQ zk`w8XG^K@+CQOk9@X5ORc2xb-qkQ0d?@soMlSa@Vd>xe{Ha_vCXaT*G*y;vhD`GX2 z69(-n)2a88NmU#j{D>-*9+6?G>_QCeoB`TG@Cd8_ibv#9IgvVtkLLA`H7#QCA{N_Z zA|a%xSfOELHd92Iz>Fer*k<=~2F7uZ2OWA10fT8)s=`djoF-MYspBMepseFwP-LhM zrzvCBtt-cstP)@2jDejfW%5(M1Rr&+VHy1S)#)PlS@~!?Ra5`;;ce5tW$o#F= zKxDxZWVSs;MQOqqa9Vq4R+ME~1c+6jKDQXm0x;EDLP-6K^>yPZLLK(3-#Jx5ljpm8 z!;e((hp2*pUX*h$EdS21?55GLQXu4Kq>u@}np5g&l}<37UC|xpa?bw9xjyyCO1~7u zqV;<*JA>SM)W}-WT_Z9U9a`Lv>?>4*thD&7rz@zbKw0n{9hj<(SfGGwF!hwNfkyXC zyL*7ZEv^)C)oroT@$9O>_nNpG$}EbTi;J?n{&6rnn!U6_@*ex&`7$cY>s>126Fs0h zW%pmDUGroar&Ug1?2omFqq5{3aag1c6wCrNam6g37X#mN!f9L!!m-W@NRI9MBHgV` z?AAlvDn6tdAO+x-a0LuZ_;9n>t4AYDQ(AiiHFi>xQZ+M%Zd6n>DmmpW0G6>1&>XRd z^8G14fl!?&{*(5HuEIn@LJZhhvMH9@Uzf)g-95R-DbhsN@_O&x%l-cM`>)^bt4Faj zns?L$?$Og(T%&dJ-lFgxrhxD4z-K6RJvR}&m;DuuoYWBHbFa{Ou(q?+q69S-#Ae1a zE{L^5vS<+fRPFG*MzQ31Cr;2aeNrKtk^$5t3g?}ntp`rg3qX>fn$@ACz)%Div2t&? z(6}UidfvPitmbhl;`5IyyV;BW*~2e@yZ5?^q=u#r+6_0V-QxSY3U!%iBCQ$qTKKen zUJWy+@r&O%EqlZNLCr@Y%Jgfg+3p`7?s~2S!8>U1lO~U*;Ieh9s z80L6Lzw&y9)dxg&h1Xp`eY6AVxZhUQmZL(f;fjoF6HM+EEtw-@kHedRD))%fU06a5 z8AcH?g2PYvzqiNxKOP*tI|;Xd2g|E7I46+c5D8nMs+w}_eW`i3lJV7-jrwg9VMz>H5c zJBqm6%+1X;?A)XQHUTZirAefKTT$n;P<;wtkNYWA8_0)DK{|M(Zw0NSE`eNM!$5hr zXh{`(@+ed-oFj^Os{fM8Awu?92N@q0hi2$m85lSgQ~bPVyP!I3tvQ5#<=?p97ZAQJ zr*5mC9KbIH*vUY%_ce8@z@D+M4I2R)tyKGL!(A(OEmv1|Fy^#PCzU{K(vpn*vSLVS zj=HPJ@#gaqEw~|u{KX#_4sRE2Il@f^$ZEbD*BDj~n&;q)QR290%WCbZkr7l{HIKM( zywWC;dZ;`~j6rz-CQzc%X>bLLNUHkX2%4IIV^40g>ebP4_0#uzr~3fMRtG253uxc) zt5v7p}BO!Um}K zFl4uB>k$sYiaR$IWJN5Lj_y0qD8rpv8z$d^*hrQb(w*MvHWo7i?6@>*MEK5L2Hi5*KGS~R~C2mr{g1V zj6ge?)BN2*)#dD!>GSv8%86Z$NkZy0_QS&L>By;UoqahGJnQU%o&$?F`;a@Zn67qM z4NIl8(F=`TAqXf<7BCj5%|n3dhs7WM`1KE11BN<)#O!?I-ZTJ7<(0L^wXGiVz>Q^k zqw^|5c?mD-0&2dz0E9-JR7Hw%DxTyaU@1?y-fXe&jT>-Hq1^ht zyFqZx7`%-bJC`tR*+JW+HgD9tCJ4wfW=F@5WRt@sf=UJ-5O_#L9!+LgdK*&CGSZA{ zZ1Y5!I$3lKCfx`#C*Sc^t(t1eNw7*^=uI`dW>qxvcw0uWRfi=y9|IbyIs3Q#joLUb z?LyLgez^@Zgdi)Fr{w+)AUQ|=!o;;Z*vj;fnX zvji2%OQvD6V*@GzwF>DHL&ahgB9 z>Hlf}=a)x69q!tX@7}_G{!@SdX4{zfmDp8`nZM23IY!YnBMIU-gvin!HoU1of zF{~P%s94Att`aoC72DE2TBTHop)qze#u;4aZ< zQaMrdFZ+jkZxEqA-hXxQXF>*b1Mu}fiPUxW z;;H!#v8c4;pX5?q>!_4sN?0d*B+KLrRYn)hj-V`C)ev^~S#}Mvb)QwFxzoY?d5xXQ zjqHljltSrKR)>o)p3izQ@e0S2soiCWT_!{p{lp_^3AM- zj`{nR^5n6B4*<@ApzeRS_qG94c?zUT@$t$2DF$skfq1zEo{(%JKH$DQXFXkM^?fo= z?_{Da$i?T_u|CD4)XQ3o#l8(1Ep|+D!+XWRU$V+vEjrmwR^@~6*;*0lAgj-EU@y{C zLO?Z@9Uc|zh7DFwM4hO>hX*d2nW@s4rx-PjpoR&A19RFz`Va-Lu0)!7LwB*@khX}} zXERsKpT26>G|d+`TP(fd9;fX6CQVLX-e}bL)i;}PwiGCjT8lskO~XY!3%rH(!_Ak6 z7%#29*i8dQjY6#!w}r*&%A>uIdyU+vX8gHV^md^UC39EUNtSWqQy`8_EK26RGZF(3 zIx{hgzRg8UH_CBq(u*ik?b6vDKX(>S;Z5^$6g@lLV`|z;1uk#w(B@HOP4CMMbY&evwW-g}YU9(JDN$_n+Ennv)bxe0fmW zQjL5MRWslaa-bHh5{7i7W2CV5-U|wFFl~_7*I$7|dm+<*c(;H2GmwgiU%c5%l2vSj6ASY^#byV2 zzreO$l?nM?Xqq(FM5GvdXodNj;xg0cD+0>kmo-~u#hx*?O2W0y@J(|gt3Jlwjdx1s zt(Vi5j!(7mmcSiVfmktwEa5r=awdogyWttiW9&+{ zOyfck61Mb$Hogar)K9@DgoR)Qx=Qsu+CnQDFkj1$vrSN2#N1(>hoMJL3nKN2&V{a>}Q}A@EWIWTXxC1;v zq93-oR-m{yT+q^(bDgq)QhVtXa`gXK+_&)7QDs^G6@Nx8I|17XG-xI?NQMN6L>^z9 zz{~{XIJOf!9otcy5NO2w_E~G~$9dGLd#?j%Mw;%6+-?n!Fd3A9?nKrlyFSXpT_7l~; zpx{Untpj1|Z;E>O14YE}^X;R-^SuE@y!Nbao!&)oQ{3on()Eu}>)Q&YTu_*7&U%=G zPpVcpsIq6o%0wxsFg3)7O@AdWHgku-lf&+B>H++TkFTK?{#ouYfup1@T&gobVp1I& zeqV<$LNHb&)AXLWS-K1wQtyUESO_2Z&w0T=lF&xU6#B3YZ<CHI$&t#p;t|PfY1_b6`_iTdW%qNjah-;0Of=m zRh5$$2(>XGg)|JK+<@H*qDK(x4GAnwf)&&Y^iBXzaP%$OkI-Z-JM6u2B4<@e;G*qH z@*g(-RaED7d@2Eon_*S9FTfwChmZ6lp=j%HT?j~TX%_@R&{ZQ|hTrCeCwjmv^r$L) zUdu+H4Y791Y8BeDm`b!4E-Y;-rb^)l(sLY)DQle-c%CNkU9mCA6p^=&5I7kG@+k(R zb<<`QNrgl2EjI-738w8(eU6qgEtNvIOw)uFD2x_bR5c&A#U-SC?C<-XJxIVjCw0KC zk}mrc5|Ck#lGLt#tgr!941z%2?TZ&kHKGc37+WvbuKs9Z5#H^=ZPb1o1(|UB{Go$} zuEUNOR^EJG+8XAb1-ZYk_k`)SWU$3F6;{@E(G82VmhYCyM>ZcQQor)2%e>AdUWJE} z19i+;oIX`Utf;AKsG%RojuVMA52HAZ0~08Q6dNHCoInajIobxY+c7s1q07Vn1G&@} z!AxK&r#D}LDK@%2HpEsqhs}ZvZvVZr>0D@IIW?ZjO>|9D<1m%V>yBCmTL?la2KAS! zm?uS;l9MgWl;95U;7|w>YdXYktT$(%7ggzi>;9U{$~M@DocWRl)2@S@bE%0#tOJZb zZ^JYQbY9jCIEU&=yO|K-?{7c+1|{*a(jiNHu}hQDSXtU0<(@FTf+D;r9H<~Cb(E5X z3I;Ur4#?SO7~U+o-Mn&?AI-YL8GyS=jg&1|o*Xl#)6VlP72OrD6TRI>fyLM>m&;x22$y zWFvNY_dc&Wz}9|>9|bVYVvCYd_j5Bw3v0W#NMU~Y03&R%m`PH7SwdZ{wr4ZwPKrC1 zLHEL;1sU|jo)j=|6TWi_hYrm<#_zJH+rh6u&E88oN5k4%z!J&<*v@5e(azFIn3~GU zD!aK)Pp#m8*Uq;y?{`OYLGHdgkyA{p39ZD%Ryt3JO9d@eHe|I3NaBsbuIemWxU(B; zZv|kg9lheR2SnI+F~NkF0SSwZIeTmb=GEJEE=0Wo7FQRl`6@E#U;zyWe%c20%%!s- zD=of-Sml;HSZkt8Xc@$gLjfHKS=;d9FCn-1GtcN+77p5yh|fy2_kO2&y>lV>w&B}C z2Op_23X>g)Q=_Cq7T9Pdyh9hJzRJ=z5o)_FB1~1_SgO&PcC4Y*JOZo=w5y7VrlM_j z+0=wcVmF~27W`Iq)lpLK8bx5$+o-Rwt(9nWwND-zCR;&+TZnwyNK~;DYV)<|0}vJ4e1I2DOOp1u#^;SYZGw#Y~PlZ4svlP6;c&@3MH(h5e#|^lCBN34>OU zW9@!L3Wb}u_wL+A(7j*~KLS0HM}NpNo*qM2;M9}21#~i^ zR2YSgv5eG7uu7?e+S~M(hR{VyYL@k`#*MX{JH8%b%Tn=*5TQDq3<4YH(keE4#kv&n zWQ$*Vr?{<_;mjKtX!@c;96z($TSfr(p>6ToI*MS{E!5u)l5Gn7C-Z~%Wd>J&o<1bp zR&>xtu+Me_=Lvtr!*EOW`{@O{;o;}s1BDTfgEwrDK1`sG=o~-RP6DF8YVsQG9r-r4 zTW7vzG$$hhxayVs>7y45x(ezGW&FfdRH#$qCaET~QG7JtbHRp)Bh6Mk?Ds*zzSzsO zQWn=Df#oET1a_|mE|wF2N0T2Ky#7R(gJ%EMUf{5-IExcZ=-8ZSDRf`uiFagviZ+j z;eCfzR3)h9Qy9y#D82QLx>p3B>P~$&LQnE6$FgySRH&*ZzH+$j8yrEUegQS>u)%4g zHBaPlRGI2sH#+k;a-sSkqpvHQXSt zy|vh3(y#fXSXC^X3Qz2WN9b;kH&+R^%Hl_kTify5;Z5N+NvC3TVYP^1%(L!K7Fnwk z4sSqvC#7wBT)26#dq(?6SQ9a}?L9j(#&=?2#J0}3TR*UiO45ZUQi}*dyq4V_?i(n=WGT*9-ea zUEQJ#{VYb<%b5~O{Om1B$N=E=6K&`X$h<=CL(WM*Twi#ET6KH}j9f$+N!gsks0=e* zsaiJM1j)FGjj$T$hp1NxvS-)(X%rz83>QQcHo;`!xlNYWsC2*Otz@HE&b0N+$6S+G z6z@ZTwg{jsn-Og6g!xOgMpdS|4x@pq**5lhHAzjT>X>%(COG^>Ob~nk1wN%^y@mBO zCxTC|Xt0ria_gBGq_{pEY{5W@p#$g=|0OekRMW3(f1>EASpCIlGDG8CLiZtpjrSRSD@e{pzlL zLe4HC0zB)lRv1BeF9}i{MNt2GKg0KoaC^eV?P9C2Z1g~ zv_?ru+Bcx1pTTo!XL)O4ujn(Zdc2?Ug*sVe6=thl`^^Pp=|u3xSSzrfJ6XD*1v-SW z8DfmN76%KvM~J@-4S^@!BiLoY2dVeN0dZ{Am=W(nHP37 z;v*(4f8Et3%s4NL{@A$%gn2$iWhhG##WLh+Lye?jLe~~4i!2jHd}GbR;-o93aaf)1 zP5wzRDdAAhGhuTds4O6i9#P(4=TqBIJ;52CXAUTA)t2Tg@Rl(>^_;OW<)?Pi0J?SD zE>40xMxKR0t>?U|b}jxXMk|_F4Bim9$UtIGsEZX#I)$?=<*)@C$W8+e?6jU0ZbloP zWYQ!qm3#{mgP4w(rA>fAP>Ri2QYqNpP@PJyduk)0C@l(yUh>7-y}P$^x+?+>e3^HA za@@pdA&r2f!apwIO(xGTwZ)>`sE71q+ff?fRaE-r%DBeJ8UjTEAu*6%ZPFqj!+3ZN zXFVi8=8fDxB1Q|W#!MXyb4sbtqX?!hIaxyYTwsD(1J0v;8rVK}F@5_)mL26c)-ie! z_mg=gCFLI6y3znuKucthhm<1X_MP%VuVX?;2<`RO0W_I#Lzfbl9gsOYD|s~PgN4am z>DVcC+nxYnva~1T9+tyzUsT=+i?WzXCsh_IBlb;cG4pLxn>pQt6Zk%_v-IX6dne6~ z>3Kl$tTsOVptEdZzK2$R&J{uwHOfp5+cFjb&L79 ztyIh`k^(nNaWLwSnWWq?VcbLdT8wSnb^e`VJC_>a+$Ej07Ut7>nMbtQ!S?qO^pff> z8IY86$`f}^aoO3soY{ZgYVedRq z_52KCK4>`o$D8p@hW)c9Wxg|-U#v`nd$IWH#Sox;(i97{up?i)f2v15E++Jk(`--_ zd_a%@8H_Mt!P>;@P5*TDYBj#+UP39A?_3p3<*(chu#JeyW`&fj()jN-otK%?A&zUD z{9ZcIi$Mo-vp)+@SmDxUbp|8N*bq0m6&Tz(tOU!y){tCd3rr-DM_L*SHt1i?^c<&& z*C!TfR{3P9u>vVhv>w`hX0(ygH+MVDy_ya)3U~MvMigg8bu$m2$q1x35;7WB)AMp| z1~5h%bmJkta3=JvN@oW+#~R}(bHh!RnNeZn;Z)J84=Ovb%|V?~mQ8Qz((``I@qOc@ zuqf!tzAFbn+P?2X4?Wdpo-&q?n60;+51gUdmmJiZ(J<7ZAV>So&(cu2jHo7eI#udu@e!PXZAsvmoz_$jt*SxRY35^z(Ff%iI-qxacyMBHGjz&B7w}r~! z#`d*mzhAW9^Zwx6K3gYEwPHoinL9J?P$Jq>MwQnCJf0VU@FY*H+vy0)^}&<`NWF;` z^}>Ij-|Hbs&SUx3HURZ|6M?GL2^z?n6`##p@{><_@KOwo!#VEbcd1h2XOAAV-LNEc z22G8^-z$ZJsxVN${wreH+u7zOy@D#2x3cnZRJ_<@4~`NxuGHocAV3@ zlux{8G#G1-!8iy{@Uvrx+9Uq$-r4R`gbY4AIzPx?WJTi_OxSyvu)E*wKG@&me|*3D z9Zrl(=g-c5etQVYDNo^1?N{x0)DFd?^ct4)d~y7mlAO&tn3E8Fq?37CzA<`QewwiW zh9|6w2WLvIH;(ZY3}ji*@AmD3*4Uh24gah^0dti<=qvTZ%;zvF%nFXu@LxGb5+El& zHkz5fS==oZcjp9DcmlBH??*I*sC9)zv7AU=RgO zI>LaiFE#&qdktH@K=(_hV8W0^y+ch(QclQXINzkTey&urT`EnC8YV-2SZvt7MdNyH zi$y@ur~n2N89wnr{VwxOcD>tC*Vno5TgXXU=qUGvD#k8&j|>}=l};ArTgG0Hr6?>O zW%LS)LYL^>n#)-IxjCMF%So`$zTWU!1kS9;#qxAku&y_~di8s>?exNHRi#*^Mb)3n z)EqHXgyau9^I0UsQ&G{!{+P_qXrf_~he{Kl*t6iZv5JS017u71v>299el~<4tvL)_Vx1sou?a z_z@bzp^gi5#H~ZG8WLfj^Q`aitncuwV@CZeHOS_SBtZHc%i%=&@$oLJi(x|q%^Snj z#Tg}4y>fv8Q)9#`bRyS*@iy;{ip5Zcd{5_y<$UpGz857JU|iv07erGL8bymm`zBlBxY5xdKiD3U z!Dix5Sk})5W-ff1wqw>`QYx)*KpBvbLX2oLlCjcn2&N!=6I$$*$^9v730~pMNzBy0J0bl zP|T8%t(tL1WJ^LyY=+x)Bab@QLW7!MaxhY)*VIRbq9102#EgL?wN6RlKG_M()U`)h zuch{%qdb^h<2P3+Fl8}ICk{4W7l+;Cv-itFgnSWKlUkto!nM*1F~K*+>}qP(#<48} z@y9Zv_6GBZP_o<;zHcf!jPUc0Gb1yls6myE@u5O-)L14-8J4})coai&_(lbd0uSs? zXnpt{PReEBy8S;iId>ebDz>2SZb zW-p!h#k#oC>R@%FUAyS{pJknF}>odW7qOpLK_aJZsCNWrmx^VS!SzWy41>)u0jJS}A`rMr1& zvV6K$w>x+1;BQACH19~(MvMhYduFhO!f$k2wkyYD^W^1W98P1lV=c(#xX+tFBBGKKFyUI0uT63 zkD6v7(+s?7s1Pjb#XnJ&&gMHNgoAz0L@+6(jr9Pl$-WCATR23uNeRx&{P+R8*OM0F zX!`z9RaAGG&x_2e9zpNbVBUxp-n^Q4N;&w%+Q%)YS-Pshe)AmzKa(v(u!HpYec*E> zT?W6)bcx@+tuhSv{R{rbTYRUvwp?_(==VR+0)F;9{2AF5WoVsM;;0(_+#2%>4wx z@kE0@u{^g;@J>6M<)50LotZC&gbZxEde{mwFwn=fK{Aj(*71bZDRha@tQiUJ18%20 zv;2p6iJygX`R%%Bk@+EyH|)2KA1#!5?f3N`<6znav5Ttgl12>T-e=8zX~%9Srg}!* z)nYW3DY@}jn$_iFbq9P-O*YS%S*q$`oe(%YWdYtK2*L0?-OcU)hFO3f8+fgE&QjwM zf2yd&&iRWa9tS`|A-Pln3z>%R}`DMvzZ+hmwBC6ufnyh zDxd8B2IREJ>?gf*64F!2G@%H9%mnnDl~xn%8l1Lvi14O~S8D@WPQY4>ssx){0U4iyNNZ1(J<}3uXjccjffVJ>)l3XD-hgv+ddrC<~!)S9g|4byRXTebhES!xrODirLoj zTJM^(Qp{EW+N=#5hi)U*;~fH<&}|d5wdRdo$Tq^Y!?>&+sYS8bcZ|#WS@qAjTwM0_ z@2Dv=Yg6({r3XsZYJ0V9wUwV9gDvue`{@#?p*ZME(nFKf5GKp|7rsE+ zofSn;hK0)MXHtn&eKJ(5X%BO+x1ZAybIj|&4eK5CiyITPo|7wTwMGAQKJ(KiO@ zgFsWLmcQ8J{#v#BYJZEaB<3o&e_Pn!ViHbTEWEC6$FRj|-ENbQ>3v!0k( z+rh^rO>|HT%33Swx@5wIRF-`d6alKSF_BT?16~#r5`zs8=bX`Ld!QxZA$I$*=M7gd zA{0azaao2kyF(E6QHvRAYSBw`Y>FpRJ+lWUMhdoW>ab8Y&dt)h9+4&K%}xKQG;(hb z4HU~0zvM|KpVX&zG_cv$ZMNCfSZ&hgszvcw@CI2aSj;8=slv#T$i|Yo#cJJ}^VOo; zt2uhk@+?`dOmE=rq8^g#gB%lHU>$9C zfAR{wrBD;nwyzE{X`PovK$Dd~CZD#1R4ai^Rt1`v^d!@>H#4!&&LFnZ##QQoN{=0| zWDv`GR|<({IWLXw19l-WuLGe{1V6ri1xXefK0nAC+vjsxV4i)2O*=V%3JX8Eow0*r z)xjuk??|3>V5*U6sL0RxgS+JcA8{dUPoCG0uB|9@xDjhOh#fESrmY0d$d)n_3`Pq% zlyjN95fonw;2CWUiOWE{F!x*^vyb4wq_kea#t3!v&b@=9bs6xK7f`c-f-yL;3CDF$jp5Htq_&7-^1W5z z1izjU3OuHw!81IdcFPkz=t-ciUD7nTtizwEfT(>8wM#yy!M2?oZP)0fJh*&-Ycc9n zZg-lYxZTQ(UI;WD0!JMw820lQZP|eWE|?nZ9%{_(px=NdK=*|6vws~!Y*raP>!i-U z_~4nb9_DY(cw{Mwo3N^pW*o}F!3Kv(hLb5yx@yil&MS^ylA_fm+dK*>ontTvtX7qE>oU26U&_O-OM+e4$JGrj|}b3F6p)iNTI z;WFWsXuZOic;dVCi5BmPuj!L?TLQ4^v@7SZuOfKkALmP}z6V4sKSQ*qOj$1O^YgBF z+|2U7DlRZ$RGmZx7G4xS`{<&1kGi|Mc3PVTl<$x&_tlMdNu1R(-Yigrvs{XT-!;uX z-i+~1{tf--_*dk26*xoVqOfM#B;}2AOG=j;ad)XOx^?#R2~r8N?O?#19zZJopB5Nd zI-yXE(z!tr4#R$Q?fUhP?LgAb6*mh2uzX|P!s}jdK?H?~sn|Xv={%qj{-zK3VDzW*_ZcRZ0a+IaJ6exR{B&dP1)!fpM}@uq$x_T%nI8sBOR-8%|F&c zT|+>Ydi+;dz3d|%rd67msd#dy^^EJGUS1z!GMqqqDe>hxtlB>PI#z z=!5veJDxz;kxNXgjKlZ$T*s@(qBuP{JcmhHYM!tF)XkvPM$IGs`5*W0TeU=e_jTP} zCd=ri)Y;g)&Gq{S3R<-o=TfYEnpRw9F`ZFFwpb&j$2)J=eK<XdwQaB8ZCwmcZiIQUQH;e?yK&ep&oH17D@QSPEILd})YW1pyRfvK6 zXS(2)6ZenQ7JE+H56e=$vt+ zGonF4npNCJ+LHL<+Fzquv1?7@n?gbK1@1N$U^ zw+5~Cx5ZNw$tBXlv!V(I+4u^T+dk$TT74thjER)HUW<5g6gEcCO4`0iD8$Aojjj;T zV5jlm6@9xSGQum@`JNxUonZPjVFB@=2Z5BQ%Set@Ltr^L%l ze>G{h2}yrSDv^)WZJr~e4G?4w*2zq$I<8{!p^1&?Dxpnuu{k=zb?(9DJPx2Jp*|pYb<}XSx~>tl z+tCFY4hv&PszG!=Z+p#NfFP-7FDUp;vxmCk`FD~+ya!bXP8mC|%C*`Q%Px|slwnsP z^@&O`_++x}U-e7|Nd!oxhh1|2?)P8pU-eY9p`6rQ*xO6U?<ICM+G03(No>Osd z>fCYsx@^YX;QZTuK6`!~e!yr9eiC*EK5715eb4X(UY@;uI)9%gZ*<@3$?*ku5Y#d! zplJcQF7Fdv28PJ^O5X4L3J>z}d-@BuX(LM~jyaT=tmkS_Z}5?=Ya?&xen=4%XkZjS z5#mD@;Q)wY+t6Q8<8QY=s_iZSTpPyBgJF?6usNB{7$#uRS7P#bsh zWD5}$4{WWj9lYn7-B?$-w6;Fb*ES)Qx96r5$v{b?oHXVL2zcsmK1?6G8RmAc5)q9$ zc-uJI;Z}z`tSApDw*Yx;z3K>cZLPV&>;{A75rd6=>@vVen<0ZT(vW~db>1#647WK4 ztaLKkFSVZeaQkb}ySH!d{{PcoKKVPlkssnje7N<=-^*JLRg`^nzwZZ*V_41JJwy`7 z;vWuJY>elX_I+8`sY2I;iL;Q_6|mE5XKz$njyD|m{G~ZmzTM{WFZ^Td;=!kQ7ZJ;} zEPI)Y{inZ%I{p=id%hZsbGah+qx|U-?VVNK%O7Pl5X#cV;xGDUa|(0R+K%Dm4NOvZ zBgGzWCT!k~>-TlN;^xf&{K4-`sZkBFw)y75n=;d(t9ObJ2*wYn3Jt!=`K#xM*ZoTY zIf7B&JoR@j^D{{zV%}Y~)Wwfc>(P{XTs|+{kExw^|BhN^9+#JGQ(ynR zSk@7EsK@AE)5m*^z*&96+KnH#aeVmAo+yl(Z`0lV))y~SqG@oP85whp*jcvI%J-*!g+N*C z50|;^Uf^;3o9D9on`-`x`wJWL{pb)qWjYuIW$chn{_tbPJ5WM0x;Xw>UosOB(e=Z> zExOrbF~ghnxBBMlM6%gu`B%$6ISg;;3FAN3k638n@uAqva~q+;m(PvU*k%~6Tj3Q3 zz)f?1s<|hsBSo&x>YvqB-Pyr~-7h_xMQ!2W>N&JUM>`?U#!w~J&ycQqXlX&@wRwq- zsY13oh&+F=w;Th$eP2GAKycX^vH0#xQCH1&;Je|ufgeY>DAKez%di;s?%(<{8KM0m zhqkqmV+~0_qxZy2?0tq@<)_wJAy`{&=TQG{%>kZO6Ft%50%KXzCl&7@L$vDk$Ar9U zGhe+%;_fH)2jk^?{N>)OyT@W~4fOT#>pSqGzw=rx1VO&Ud%Qlbe!P4BCDPRZ z2Yw#4m0zE{2853f_DhI=%&xHeU(0<*Y6_t{fFH~x2o>x&|ma3wCmwwA@-iX1-)^SGmG^BkO&O z)M(5MDPJ*p*hle;uMhVvUIWv;rPH(}k_|WQ@#?@Ex>!1RK`<4eb(6CtxT6(F+v(fg z+q?DP;pYGQZl8&9CKi~~#?14K`jV>V-$C{6vy2WWHkqBo)g(i{-9*Gh#drW@z2ua= zv3+q9B5SK;4b=8-?d{#ZcUSz_d)sgc8^Aa?J)WPT^JIkTgs+zPhak<#_qdOPD1<<) zz&BeV@q?>3=?9}18FAoJZ4GL<*)!TG2;%Y4UPR*LwN|yZ$-`&b^7!>3icEO`qlb_m zL~jb{kRJcEXh@iZV`o3ZUOd0p_NQ+jp1(x&JHzkn?|SyLkw&GQD2A&AOZU~~`LlB6 zo_>CSU*X5RTL1FK4}Q{X-{9=$$2la8R=fnJ`X}S`sRqV=v++LqkY15XT_0UtcQhPa z(_hi+5?1d-?=^&w=n=9Ac~$4!9CC>$cxNpy{tz_0B ztQ#b{5^s+MsvI9#QU9Y=7A~+2R4GN=Vvf!iSQAbouRSc=S>(n8d4DzYl+kHQeXMP} ze2gJC4;;6il3PzmHv7!7MC&qpoT&52P~Jd}US#NVIRqM!Y;y|nkCqRf{UO_xRCi3x z4T@r4-_*nM_`FOttn`#^L?Z&cE9Jp)lyf-xzM@FZt7oUY*5)flxR7kOV( z8WYYT$Crqkg%*U%d(>;MUzycLn1%i!Z_~$H1qUT#Gc8$i-yrJP>jieLJoIL{be6fGh0cA;lQgQJAT&F}YHOCB4v8 zKfi%!8kOVo*>u~P;Jcv!xr~i@bj@N?mm7P8xJM37C7j=j)5yt#Fl<{> z{h_yH6F}+rwBI*R->cv>Gusp57|kvxrcfR0VTKC^{f;s4opm?aloL-2WoHucr@v9Y*`9^d!(Ofw~!JXL#*`Cg>7upJ6a;921 zq_-=1{*8#0WXS2skYks-&|{|<)ALJv@By8ySyZ|t%HkWRGos2A6p}~~;SWrGQJ+wW z0N-Eqe8!OX%3wo)0c1sZuNfsZ!LIup0ilN7eWkeh3iCVnJCgDw{w%D2_E%x0uDcZ@ zRQ7xAO@j%1veD}(yb%pPARe8F{Z&~lU^JOdLGQ3gPE!DGyE59Sl}_IHN(15C09_sp z(TaY$H5n$w?(Vw3uy)Y_4rhhkdWw=qB#HV4DIJB5u}_qRmj;-gOzaJZ)klx)0W=tP z;gQt=xNzwy_!3qOyS3>6UM)*H|4XT^tkAyDqk0+6+P0`tp{E$1TdA2z7C_}if)b69NR^fdpM?Ap-sH1YW< zG!jjax6Z@LEg;j}vKF>3N2N0y<@E7J!BdlG`@uJZylhu^8A-WpN~CkdnufrSmFkWP zIXm}-p%hvDHMHJct2NvnU;6lTXbz<kE zzA4RYkfno3lt$5e7aM~)K?%0&_-NILPY;jCb83yv_*-1QF{z{zq{uGaATDOs#Cz*& zqFRrNSvLJT*EM;bVo&y07l~~0FZJ4W$kdyWX}0QPq*kF(7!gwgwcX&DXQS>5vA$dd zFj36Lv35aqQFhL$+s@3|S}YK@)*bJGX1CJtBW$g6v24rr=XfMZEEQ|5g}WuPsF(;| zT2{WwJ|CP}QTvBGHZHLEH%qZQd(+H3lGs%#@e6nI`I9rUeFj(|;SvYc$kiVZ`KE4M z-m!ukW9?IKeA0pq8T$_%iVK8Z{IF76uV2re@R(s(0nr3$*$;|rQ>3fYWJrBwe_o@H zB_4Ukaf{2DjG$T@*$zx$#?damzh7!9qBjeK6K^gK+zQeoWV27_e$TJYDjz?)&RL0_ z=%+OHE6E0Du@FGFODosMFKO^14a5ltB+a&5wRwJtuP{izkEblLH*RBYVj|gId@1Kz z@7(vQmFY&rY$Vh_$}sWmdq?v`;W>ZXK+70Ca^j`)TjJisGZx$iMSgsOzKP?o^ zaHlZY2$^r{L{wSYt*GamIO{-DSzedhTYdionY8^QX=x?Fho2Hws}aUP6e#Szhc33= zLixxF#A%qBb2ozLEGy|+j3ua$l{*AE757Tq5e;6p>UL=?U>SjMb71d>x{sx*8L9$j zmG&0F$)t$Yc1oK|jz75WPFtZL;Y)9w@rL8>4uEHukQ8PD>gxHKH~I_q=_?y1O_2BEtSWyCjn`7UR~vcY-?s23U2pbdpo1F zFzHo@7Mne%^z$b@9I!HoKnN(cikPv)+l+Bc@T%J)7=Fu3Ws#{$}F7t%0vA30gL0|_g3)X07-l^mJnm{r)=26s$ zL0SwfRK2HdC-ZHOx9A`oI*jm7AxTfrXd^Lk?gR5q(x$u*mF3f*wA4Lw;-M|MLPcZ% zJUIWvxyPNd=Gvv1HtAheHBtY_dHSu-=$|hrnlNV8w_Nawd!nF09AnZ0`3gp;(+F$& zAh&yUpad>0^Q#1z$!(+-u@s8&CT5h^lSn*@#bwz2EJ;-z9~l|H&&;iEOQyFC*zz7y z<;rl32%eAzLVuQ7Q)dWN7Ewp9eTo%<=bJU@<=HP@k=uxZkkP`s;0DMj5@s7M&ct2| z=EJs)^dA2F^^u;4k4m|>nmZ=2#vl=U%U6n0ErZcl4dcwUT%;EA=~~oks}Bub=aVp9 zcg;EmqX{kyF&)Fj`GDJS9B;)OsuxPU1?6{1GFq^YZAHGZ&LP3p`wbf;W8^DJ2NfAC zuDVm(g#i!CaCyhWUh__omzg|CZ0&{j@!sWL3D>I**k?UId8}>r#oD>71u%8Yz}H>l z0}%)Da<&h5hn?Z^eH5mA5t&&(+ZAf20ZDibgcJl*>_g=>l+O)6!D1kL8UL;<#Joj zpE_VKcu_>LHBzbzUQH{sv|dNL9~N)_Vom=nVQc;4u`82lK`xsEg%bQst&EFXO+V^q z2G6M2Vj7Skg`}`@H#qA^23@sQTC`1#HsG#{_fPK(D!c0cuit0?K+Guj&FVK zyN%eDx#%8R34f10iQCHe<+NU7Pz&dNgKn`Yl9;{aT9sS$Ic>nlWFmbf<^2?=cfh>K zpKWv7*4cn7uSL7U$1WuyNUe}y==h^+#cXV- zTKqcVLNB$tsT%bx?P{HN6A@)sw6y7@$s3g5pmR;3sxo<~mh3Nil- zc@v7^5mIKn=t;>6xm|(&9WUQn_CMS)Jw+TQlu4VEfy30dqtHN|Yq-i|+RPywRJ?hm zuFNr#FZPTK-G4GZv0d?Y88d}7uTmtQX!SGmi%v-vF#w(yN9KCsdu9sLLpuQd_X)|M z2mXTz?uRit>oT)}IykvB+;Y!3){nP;_MW0cDa3&6;a2{^QX4Rr;j_w&f%HBv88M^2 znm~TMvMa~KI12KCG_&`>L8gio0{ca=l3o+*a$`B8(xGA1Y$#`FUmM_^^eDITOKFNk z2uUdps}B%;SHdh1IVOTknjH?he(z*8fkXc#Na~ z0Nfw$1#@+<^AYobxkLXK>uNYATp0-fxaQ;iSJ*zh>pQHsmz}t`gBQZVOAPK0)+HqR Q*E9YdH{8kTq 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 BgbM%w diff --git a/tools/igor-mcp-bridge/server.py b/tools/igor-mcp-bridge/server.py index a0a7671c7c..dd71fe3a4e 100644 --- a/tools/igor-mcp-bridge/server.py +++ b/tools/igor-mcp-bridge/server.py @@ -79,15 +79,20 @@ Registering with Claude Desktop -------------------------------- -Add to claude_desktop_config.json under "mcpServers": +Do NOT register this by manually editing claude_desktop_config.json -- that does not +work reliably for local MCP servers in current Claude Desktop builds. Instead, package +this directory as a Claude Desktop Extension (.mcpb) and install it via Settings -> +Extensions -> Advanced settings -> Extension Developer -> Install Extension: - "igor-pro": { - "command": "python", - "args": ["C:\\path\\to\\server.py"] - } + mcpb pack tools/igor-mcp-bridge tools/igor-mcp-bridge/igor-pro-bridge-X.Y.Z.mcpb + +See Packages/doc/igor-pro-bridge.rst ("Installation") for the full, up-to-date +installation steps -- this docstring previously described the config.json approach, +which was found to be unreliable and is no longer how this bridge is distributed. -Then restart Claude Desktop. Remember: both Claude Desktop's Python process AND Igor Pro -itself need to be running elevated (as Administrator) for the COM connection to succeed. +After installing (or updating), fully restart Claude Desktop (elevated). Remember: both +Claude Desktop's Python process AND Igor Pro itself need to be running elevated (as +Administrator) for the COM connection to succeed. This is a *local* MCP server (stdio transport) -- it only works from a Claude Desktop session running on the same Windows machine as Igor Pro, not from a cloud/Cowork sandbox. @@ -95,9 +100,18 @@ import ctypes import os +import subprocess import sys import time +if sys.platform != "win32": + raise RuntimeError( + "tools/igor-mcp-bridge/server.py is Windows-only (requires pywin32 and " + "Igor Pro's COM Automation Server). It cannot run on this platform " + f"({sys.platform!r}) -- e.g. running it by accident on a non-Windows dev " + "machine or in CI. See the module docstring for setup requirements." + ) + import pywintypes import win32api import win32con @@ -113,10 +127,24 @@ IP_DATATYPE_TEXT = 0 IP_DATATYPE_COMPLEX_FLAG = 0x01 +# IgorProLoadType enum (confirmed from Automation Server.ihf, used by +# IApplication.LoadExperiment): ipLoadTypeOpen = 2, ipLoadTypeStationery = 4, +# ipLoadTypeMerge = 5. Only ipLoadTypeOpen is used by this bridge so far. +IP_LOAD_TYPE_OPEN = 2 + mcp = FastMCP("igor-pro") _igor = None +# Path to the Igor Pro executable to use for launch_igor_pro_unattended, set via +# configure_igor_launch(). Deliberately session-scoped (this process's in-memory +# lifetime only, not persisted to disk) and never defaulted/guessed -- see +# configure_igor_launch's docstring for why: the calling agent should ask the user +# for this once at the start of a session rather than assume a default installation +# path, since Igor Pro version/location varies (this repo alone has been tested +# against both an Igor Pro 9 and an Igor Pro 10 install in different folders). +_configured_igor_exe_path = None + def _is_current_process_elevated(): """Return True/False if this Python process itself is running elevated (as @@ -509,7 +537,7 @@ def get_dims(): raise RuntimeError(f"{wave_path} is complex-valued -- not supported yet.") is_text = dataType == IP_DATATYPE_TEXT - wave = _get_wave_ref(wave_path) + wave = _run_with_reconnect(lambda: _get_wave_ref(wave_path)) values = [] for i in range(numRows): try: @@ -521,6 +549,131 @@ def get_dims(): return values +@mcp.tool() +def load_experiment(file_path: str) -> dict: + """Load an Igor Pro experiment file (.pxp) into the running instance, replacing + whatever experiment is currently open -- equivalent to Igor's File -> Open + Experiment menu command. + + Confirmed from Automation Server.ihf: LoadExperiment(flags, loadType, + symbolicPathName, filePath) exists ONLY as a COM Automation method, not as part + of Igor's own procedure/macro language -- unlike execute_igor_command's Execute2 + path, this cannot be run as a command string at all. Confirmed by checking Igor + Reference.ihf (Igor's full operations/functions reference): neither + "LoadExperiment" nor "OpenFile" appear there anywhere; both exist exclusively in + Automation Server.ihf. So this bridge calls the COM method directly instead of + going through _execute2, the same way get_wave calls DataFolder/Wave directly. + + Uses loadType=ipLoadTypeOpen (2): "Does a normal experiment open, like Igor's + File->Open Experiment menu command." Per the docs, this does **not** ask to save + changes to whatever experiment is currently open first: "LoadExperiment does not + ask if you want to save changes to the previous current experiment. If you do + want to save changes, call the SaveExperiment method before calling the + LoadExperiment method." Call execute_igor_command('SaveExperiment') first if the + currently-open experiment's changes matter. + + Disables Igor's Debugger for the duration of the call and restores it + afterward, the same way execute_igor_command_unattended does -- an experiment's + recreation procedures and startup hooks (e.g. MIES's IgorStartOrNewHook) run as + part of loading it, and this call bypasses _execute2 entirely so it would not + otherwise get that protection. + + Loading a different experiment can change everything about the live environment + (included procedure files, XOPs, data folders, Debugger settings persist but + everything else may not) -- call get_environment_summary() afterward to see the + new state. + + Raises if file_path does not point to an existing file. + """ + normalized = os.path.abspath(file_path) + if not os.path.isfile(normalized): + raise RuntimeError(f"'{normalized}' does not exist or is not a file.") + + saved = _read_debugger_options() + _apply_debugger_options( + { + "enable": False, + "debug_on_error": False, + "debug_on_abort": False, + "nvar_svar_wave_checking": False, + } + ) + try: + + def work(): + igor = _get_igor() + igor.LoadExperiment(0, IP_LOAD_TYPE_OPEN, "", normalized) + + _run_with_reconnect(work) + finally: + _apply_debugger_options(saved) + + return {"loaded_file": normalized} + + +# This bridge's own version, kept in sync with manifest.json's "version" field on every +# release -- not read from manifest.json at runtime because the on-disk layout after +# Claude Desktop installs a .mcpb extension is not guaranteed to keep server.py and +# manifest.json at a fixed relative path to each other; a hardcoded constant avoids that +# assumption entirely. Added specifically because a prior session had no way to confirm +# from inside a conversation which .mcpb build was actually loaded/active in Claude +# Desktop, which made it impossible to verify whether a given fix (e.g. the reload/compile +# timing relaxation) was actually in effect during a test -- see SESSION_NOTES.md. +_BRIDGE_VERSION = "1.22.0" + + +@mcp.tool() +def get_bridge_version() -> dict: + """Return the version of this Igor Pro Bridge build that is actually running right + now, in this Claude Desktop session. + + Call this whenever it matters to confirm which build is active -- e.g. before + relying on a specific fix or behavior change from a recent version, or when + reporting results from a test that depends on a particular fix being in effect. + There is no other way to determine this from inside a conversation: installing a + newer .mcpb requires restarting Claude Desktop, and nothing else surfaces which + version ended up actually loaded afterward. + """ + return {"version": _BRIDGE_VERSION} + + +@mcp.tool() +def close_data_browser() -> dict: + """Close Igor Pro's own built-in (stock) Data Browser window, if one is currently + open, via the documented `ModifyBrowser close` command (confirmed from Igor + Reference.ihf: "close | Closes the Data Browser."; without /M this targets the + regular, non-modal Data Browser). + + **This is NOT MIES's own DataBrowser panel** (the DB_* windows opened via + DB_OpenDataBrowser in MIES_DataBrowser.ipf) -- that is a distinct, MIES-authored + panel with its own close/hide behavior. This tool only targets Igor Pro's + integrated Data Browser feature, which exists even without MIES loaded at all. + + Added because an open instance of Igor's integrated Data Browser was reported to + sometimes cause Igor Pro to crash while procedure code is running (e.g. during a + reload/compile cycle or a test run) -- closing it first is a cheap precaution. + This is an on-demand tool only, not called automatically by any other tool in this + bridge (a deliberate choice, so existing tools' behavior does not change). + + Confirmed empirically that `ModifyBrowser close` raises an Igor-level error ("The + Data Browser must be active.") if no Data Browser is currently open, rather than + silently doing nothing -- there is no documented /Z-style quiet flag for this + operation. That specific error is caught here and treated as a normal, expected + outcome (nothing to close), not a failure; any other error is re-raised. + + Returns a dict with "was_open" (whether a Data Browser was actually open and got + closed) and "closed" (same value, kept for readability at the call site). + """ + errorCode, errorMsg, history, results = _execute2("ModifyBrowser close") + if errorCode == 0: + return {"was_open": True, "closed": True} + if "must be active" in errorMsg.lower(): + return {"was_open": False, "closed": False} + raise RuntimeError( + f"Failed attempting to close the Data Browser (error code {errorCode}): {errorMsg}" + ) + + @mcp.tool() def check_bridge_health() -> dict: """Check whether the Igor Pro bridge is actually able to reach Igor Pro right now, @@ -628,8 +781,15 @@ def check_compilation_state() -> dict: return {"compiled": results == "", "raw_function_info": results} -_COMPILE_POLL_INTERVAL_SECONDS = 0.2 +_COMPILE_POLL_INTERVAL_SECONDS = 0.5 _COMPILE_POLL_TIMEOUT_SECONDS = 5.0 +# Pause between issuing "RELOAD CHANGED PROCS " and "COMPILEPROCEDURES ", and after +# issuing "COMPILEPROCEDURES ", both queued via Execute/P (see reload_and_compile_procedures). +# Added to relax the timing between these two operation-queue commands after Igor Pro +# crashes were observed around reload/compile activity this session (see SESSION_NOTES.md) -- +# not a confirmed root-cause fix, just a precaution to reduce how tightly these are packed. +_RELOAD_TO_COMPILE_PAUSE_SECONDS = 2.0 +_POST_COMPILE_PAUSE_SECONDS = 1.0 # Number of consecutive "compiled" reads required before trusting the FunctionInfo-based # fallback signal -- see the false-positive race explained in # reload_and_compile_procedures's docstring. Not needed for the AfterCompiledHook-based @@ -715,9 +875,17 @@ def _read_claude_helper_compile_counter(): # thing to match on, this instead matches on the dialog's window TITLE, which was # directly observed to be exactly "Function Compilation Error" on BOTH Igor Pro # 10.03 and 9.06 -- a stable, Igor-chosen string, not a toolkit implementation -# detail, and apparently stable across at least these two major versions. The -# "#32770" class check is kept as a second, OR'd condition (harmless, and covers -# the case of a genuinely native Win32 dialog for some other Igor-raised error). +# detail, and apparently stable across at least these two major versions. +# +# An earlier version also OR'd in a blanket "#32770" (the standard native Windows +# dialog class) check, on the theory that it was "harmless" and would cover some +# other genuinely native Igor-raised dialog. A Copilot PR review correctly flagged +# this as a real risk instead: since this is called automatically from +# reload_and_compile_procedures, matching ANY "#32770" window regardless of title +# could Escape-dismiss an unrelated native dialog (e.g. a save-changes +# confirmation), causing data loss or unexpected state changes -- and it was never +# actually needed, since the real compile-error dialog isn't "#32770" on either +# version tested. Removed; title matching alone is both sufficient and safer. # # PostMessage(hwnd, WM_KEYDOWN/WM_KEYUP, VK_ESCAPE, ...) is used rather than a # hardware-level input simulation so this never needs to steal OS focus/ @@ -734,11 +902,11 @@ def _read_claude_helper_compile_counter(): # # Targeting no longer relies on the OS foreground window at all (the very first, # now-superseded approach): it enumerates all top-level windows and keeps visible -# ones belonging to an Igor Pro process (exe name starting with "igor") that either -# have window class "#32770" or a title matching a known stuck-dialog title (see -# _KNOWN_STUCK_DIALOG_TITLES). If neither ever matches, dismissal safely reports -# "not found" (see "igor_windows_seen" in that result for exactly what windows -# exist, to extend this list further if a new stuck-dialog title shows up). +# ones belonging to an Igor Pro process (exe name starting with "igor") whose title +# matches a known stuck-dialog title (see _KNOWN_STUCK_DIALOG_TITLES). If it never +# matches, dismissal safely reports "not found" (see "igor_windows_seen" in that +# result for exactly what windows exist, to extend this list further if a new +# stuck-dialog title shows up). # # Trade-off, confirmed to be acceptable by the user who proposed this mitigation: # this recovers the ability to continue working, but does NOT recover the actual @@ -748,23 +916,32 @@ def _read_claude_helper_compile_counter(): # call to it dismisses it. _IGOR_PROCESS_NAME_PREFIX = "igor" -_DIALOG_WINDOW_CLASS = "#32770" # standard Windows "Dialog" window class # Known titles of Igor Pro popups that block the operation queue and are safe to # dismiss with Escape. Confirmed live against both Igor Pro 10.03 and Igor Pro # 9.06: the compile-error dialog is titled exactly "Function Compilation Error" -# and is a Qt window, NOT a "#32770" native dialog -- so title matching is the -# primary signal for this one, and it appears stable across major versions. +# and is a Qt window, NOT a native "#32770" dialog -- so title matching is the +# only signal used, and it appears stable across major versions. _KNOWN_STUCK_DIALOG_TITLES = ("Function Compilation Error",) _POSTED_KEY_GAP_SECONDS = 0.05 _POSTED_KEY_SETTLE_SECONDS = 0.2 def _is_stuck_dialog_window(class_name: str, title: str) -> bool: - """True if a window looks like one of the known stuck-dialog cases this bridge - knows how to dismiss -- either the standard Win32 dialog class, or a title - matching a known Igor popup (see _KNOWN_STUCK_DIALOG_TITLES).""" - if class_name == _DIALOG_WINDOW_CLASS: - return True + """True if a window's title matches one of the known stuck-dialog cases this + bridge knows how to dismiss (see _KNOWN_STUCK_DIALOG_TITLES). + + Deliberately title-only. An earlier version also treated ANY window with the + generic native Windows dialog class ("#32770") as safe to dismiss regardless of + title -- flagged by a Copilot PR review as a real risk: since + dismiss_compile_error_dialog is called automatically from + reload_and_compile_procedures, that blanket rule could have Escape-dismissed an + unrelated native dialog (e.g. a save-changes confirmation), causing data loss or + unexpected state changes. It also never bought anything in practice: the actual + compile-error dialog confirmed live on both Igor Pro 10.03 and 9.06 is a Qt + window, not a "#32770" dialog at all, so the class-only branch could only ever + match something else. class_name is accepted as a parameter for signature + stability / potential future use, but is currently unused. + """ return any(known.lower() in title.lower() for known in _KNOWN_STUCK_DIALOG_TITLES) @@ -860,9 +1037,8 @@ def _attempt_dismiss_compile_error_dialog() -> dict: return { "attempted": False, "reason": ( - "No visible window matching a known stuck-dialog signature " - '(class "#32770", or title containing one of ' - f"{_KNOWN_STUCK_DIALOG_TITLES}) owned by an Igor Pro process was " + "No visible window with a title containing one of " + f"{_KNOWN_STUCK_DIALOG_TITLES}, owned by an Igor Pro process, was " "found. Either there is no stuck dialog right now, or it's a kind " "not seen before -- see 'igor_windows_seen' below for every " "visible window Igor currently owns, to identify it." @@ -916,14 +1092,18 @@ def dismiss_compile_error_dialog() -> dict: human). Mechanism: enumerates top-level windows for a visible one, owned by a process - whose exe name starts with "igor" (e.g. Igor64.exe), that either has window - class "#32770" (the standard Windows Dialog Box class) OR a title matching a + whose exe name starts with "igor" (e.g. Igor64.exe), whose title matches a known stuck-dialog title. **Confirmed live against both Igor Pro 10.03 and Igor Pro 9.06: the compile-error dialog is titled exactly "Function Compilation Error" and is a Qt window (class observed as "Qt693QWindowIcon" on 10.03), NOT a native "#32770" dialog** -- so title matching is what actually - finds it, on both major versions tested. Once found, this posts - WM_KEYDOWN/WM_KEYUP for VK_ESCAPE directly to that window via PostMessage, + finds it, on both major versions tested. (An earlier version also matched any + generic native "#32770" dialog regardless of title; removed after a Copilot PR + review correctly flagged it as a real risk -- since this is called + automatically from reload_and_compile_procedures, it could have Escape-dismissed + an unrelated native dialog, e.g. a save-changes confirmation, and it was never + actually needed since the real dialog isn't "#32770" anyway.) Once found, this + posts WM_KEYDOWN/WM_KEYUP for VK_ESCAPE directly to that window via PostMessage, without requiring it to be focused or in the foreground. **Confirmed live on both Igor Pro 10.03 and Igor Pro 9.06: a POSTED (not real @@ -1128,12 +1308,16 @@ def reload_and_compile_procedures() -> dict: f"RELOAD CHANGED PROCS failed (error code {errorCode}): {errorMsg}" ) + time.sleep(_RELOAD_TO_COMPILE_PAUSE_SECONDS) + errorCode, errorMsg, history, results = _execute2('Execute/P "COMPILEPROCEDURES "') if errorCode != 0: raise RuntimeError( f"COMPILEPROCEDURES failed (error code {errorCode}): {errorMsg}" ) + time.sleep(_POST_COMPILE_PAUSE_SECONDS) + poll_result = _poll_for_compile_confirmation( baseline_counter, _COMPILE_POLL_TIMEOUT_SECONDS ) @@ -1335,18 +1519,35 @@ def set_debugger_enabled( function -- so debug_on_error/debug_on_abort/nvar_svar_wave_checking are only applied when enabled=True. + debug_on_error/debug_on_abort/nvar_svar_wave_checking are truly optional: any left + as None (the default) fall back to Igor's CURRENT setting for that specific + sub-flag (read via _read_debugger_options) rather than being forced off. (Fixed + from an earlier version of this function, caught by code review: bool(None) is + False, so leaving a sub-flag unspecified used to silently clear it to off, even + though the docstring described these as optional -- i.e. "leave unchanged", not + "turn off".) Pass explicit True/False for any you want to actually change. + Recommended pattern around an unattended session: get_debugger_state() # read + save the current settings set_debugger_enabled(False) # disable for the unattended run ... run the unattended session ... restore_debugger_settings() # put the saved settings back """ + current = _read_debugger_options() _apply_debugger_options( { "enable": enabled, - "debug_on_error": bool(debug_on_error), - "debug_on_abort": bool(debug_on_abort), - "nvar_svar_wave_checking": bool(nvar_svar_wave_checking), + "debug_on_error": ( + current["debug_on_error"] if debug_on_error is None else debug_on_error + ), + "debug_on_abort": ( + current["debug_on_abort"] if debug_on_abort is None else debug_on_abort + ), + "nvar_svar_wave_checking": ( + current["nvar_svar_wave_checking"] + if nvar_svar_wave_checking is None + else nvar_svar_wave_checking + ), } ) return _read_debugger_options() @@ -1586,5 +1787,279 @@ def get_environment_summary() -> dict: } +def _build_igor_launch_env(): + """Return an environment dict for subprocess.Popen when launching Igor Pro as a + direct child process, patching in COMSPEC if this process's own environment is + missing it. + + **Confirmed live this session, against a real launch_igor_pro_unattended call**: + Igor Pro's MIES procedures run a startup hook (IgorStartOrNewHook -> + GetMiesVersion -> CreateMiesVersionNoCache -> ExecuteGitForMIESVersion, in + MIES_GlobalStringAndVariableAccess.ipf) that shells out to git via + `ExecuteScriptText` to regenerate version.txt, using `GetCmdPath()` + (`GetEnvironmentVariable("COMSPEC")`, in MIES_Utilities_File.ipf) to find + cmd.exe. A child process launched via subprocess.Popen with no explicit env + inherits THIS Python process's own environment -- and querying the live + instance directly (`GetEnvironmentVariable("COMSPEC")`) showed it came back + empty, even though PATH itself was intact (including a working git + installation). Windows normally sets COMSPEC automatically for every + interactive login session, so a normal double-click/Start Menu launch of Igor + Pro never hits this; it only showed up because this bridge process's own + environment (inherited from whatever launched Claude Desktop) apparently never + had COMSPEC set. With COMSPEC empty, MIES's git-shell-out command becomes + malformed, ExecuteScriptText fails, and + `ASSERT(!V_flag, "We have git installed but could not regenerate version.txt")` + (MIES_GlobalStringAndVariableAccess.ipf, ExecuteGitForMIESVersion) trips on every + fresh launch via this bridge. See SESSION_NOTES.md for the full diagnosis. + + Only patches COMSPEC specifically (the one confirmed-missing variable), rather + than rebuilding the whole environment from scratch -- everything else (PATH, + etc.) was already intact when this was diagnosed. + """ + env = os.environ.copy() + if not env.get("COMSPEC"): + system_root = env.get("SystemRoot", r"C:\Windows") + env["COMSPEC"] = os.path.join(system_root, "System32", "cmd.exe") + return env + + +@mcp.tool() +def configure_igor_launch(exe_path: str) -> dict: + """Record the full path to the Igor Pro executable (e.g. "...\\IgorBinaries_x64\\ + Igor64.exe") to use for launch_igor_pro_unattended, for the rest of this bridge + process's session. + + **Whatever agent is calling this tool should ask the user for this path (and + confirm they understand the elevation requirement below) once, at the start of + a session that might need to launch Igor Pro -- do not guess or default to a + typical installation path.** This repo alone has been tested against Igor Pro + installed in more than one differently-named folder (an Igor Pro 9 install and a + separate Igor Pro 10 install), so there is no single reliable default. This + setting is intentionally session-scoped, matching this bridge's other + session-scoped state (e.g. the history capture refnum) -- it resets if this + bridge process itself restarts (e.g. Claude Desktop fully restarts), so ask + again in a new session rather than assuming a previous answer still applies. + + Raises if exe_path does not point to an existing file. Does not otherwise + validate that the file is actually Igor Pro (beyond a soft filename check) -- + launch_igor_pro_unattended will simply fail informatively if it isn't. + """ + global _configured_igor_exe_path + + normalized = os.path.abspath(exe_path) + if not os.path.isfile(normalized): + raise RuntimeError( + f"'{normalized}' does not exist or is not a file. Ask the user for the " + "exact full path to the Igor Pro executable (typically something like " + r'"C:\Program Files\WaveMetrics\Igor Pro 9 Folder\IgorBinaries_x64\Igor64.exe"' + " -- the exact folder name varies by Igor Pro version) and try again." + ) + + note = None + if "igor" not in os.path.basename(normalized).lower(): + note = ( + "This file name does not look like a typical Igor Pro executable " + "(expected something like 'Igor64.exe'). Proceeding anyway in case the " + "user has a renamed executable, but this is worth double-checking with " + "them if launch_igor_pro_unattended behaves unexpectedly." + ) + + _configured_igor_exe_path = normalized + elevated = _is_current_process_elevated() + + # _is_current_process_elevated() can return True, False, OR None (undetermined + # -- see its own docstring). A plain `if elevated` treats None the same as + # False, which would misreport "NOT currently elevated" as a confirmed fact + # when it's actually unknown -- flagged by a Copilot PR review as genuinely + # misleading guidance. Branched three ways explicitly instead. + if elevated is True: + elevation_plan = ( + "This bridge process is already elevated: launch_igor_pro_unattended " + "will start Igor Pro as a direct child process, which inherits this " + "process's elevation automatically -- no UAC prompt expected." + ) + elif elevated is False: + elevation_plan = ( + "This bridge process is NOT currently elevated: " + "launch_igor_pro_unattended will request elevation via Windows' UAC " + "('Run as administrator') when launching Igor Pro, which requires the " + "user to approve a consent dialog themselves. Even after that succeeds, " + "THIS Python process will still not be elevated, so COM calls will keep " + "failing with the usual elevation-mismatch error (see " + "check_bridge_health) until Claude Desktop itself is relaunched as " + "Administrator -- make sure the user understands this before relying " + "on launch_igor_pro_unattended to get a fully working bridge." + ) + else: + elevation_plan = ( + "Could not determine whether this bridge process is currently " + "elevated. launch_igor_pro_unattended treats this the same as " + "'not elevated' as a conservative default (requesting UAC elevation " + "via ShellExecute's 'runas' verb rather than risking a silently " + "unelevated direct launch) -- if COM calls fail afterward, check " + "check_bridge_health and make sure both Claude Desktop and Igor Pro " + "are running as Administrator." + ) + + return { + "configured_exe_path": normalized, + "python_process_elevated": elevated, + "note": note, + "elevation_plan": elevation_plan, + } + + +_POST_LAUNCH_POLL_INTERVAL_SECONDS = 1.0 + + +@mcp.tool() +def launch_igor_pro_unattended(wait_for_ready_seconds: float = 30.0) -> dict: + """Launch the configured Igor Pro executable with the /UNATTENDED command-line + flag. + + Per Igor Pro Folder/Igor Help Files/Advanced Topics.ihf ("Calling Igor from + Scripts"), /UNATTENDED "suppresses certain interactions that are inconvenient + for unattended operations" -- documented examples are the About Autosave dialog + and (Igor Pro 10+) the license activation dialog. Empirically confirmed this + session against a live Igor Pro 9.06 instance, but NOT documented anywhere in + Igor's help files: /UNATTENDED also suppresses the modal "Function Compilation + Error" dialog that normally appears on a bad procedure compile, and instead + reports the error as a plain line in Igor's history area (format + "::: error: ", readable via read_session_history) -- + see SESSION_NOTES.md for the full finding. This means a bridge session started + this way should never need dismiss_compile_error_dialog at all. + + **Requires configure_igor_launch(exe_path) to have been called first in this + same bridge session** -- there is no default or guessed path. Raises immediately + with an actionable message if it hasn't been. See configure_igor_launch's + docstring for why the calling agent should ask the user for this rather than + assume it. + + Refuses to launch (returns "launched": False rather than raising) if an Igor Pro + instance is already reachable via COM right now: launching the executable again + with only /UNATTENDED (no /I, /X, /SN, or file-path argument) is documented to + start a genuinely new instance rather than reusing an existing one (Advanced + Topics.ihf, "Calling Igor from Scripts", Details section: an existing instance + is only reused if you include /X, /SN, or a path to a file), which would leave + two Igor64.exe processes running -- contradicting this bridge's existing + guidance elsewhere (check_bridge_health) that there should be exactly one. + + Elevation handling: if this Python process is itself already elevated, Igor Pro + launches as a direct child process, which inherits that elevation automatically + -- no prompt, no separate step. If this process is NOT elevated, launches via + ShellExecute's "runas" verb instead, which triggers a normal Windows UAC consent + dialog the user must approve -- but even after that succeeds, THIS process will + still not be elevated, so COM calls will keep failing (the classic + elevation-mismatch failure mode -- see check_bridge_health) until Claude Desktop + itself is relaunched as Administrator. configure_igor_launch's own return value + already surfaces which of these two paths will be taken -- check that first. + + The direct-child-process path also patches COMSPEC into the child's environment + if this Python process's own environment is missing it (see + _build_igor_launch_env) -- confirmed necessary this session: without it, MIES's + own startup code (IgorStartOrNewHook -> ... -> ExecuteGitForMIESVersion, which + shells out to git via ExecuteScriptText using GetCmdPath()/COMSPEC to find + cmd.exe) hits an assertion ("We have git installed but could not regenerate + version.txt") on every launch via this path, even though a normal + double-click/Start Menu launch never hits it (a real interactive login session + always has COMSPEC set). + + After launching, polls for the new instance to become reachable via COM (every + ~1s) up to wait_for_ready_seconds, since Igor Pro can take several seconds to + finish initializing its Automation Server. Returns whether it became ready and + how many polling attempts that took. + """ + if not _configured_igor_exe_path: + raise RuntimeError( + "No Igor Pro executable path configured yet. Ask the user for the full " + "path to their Igor Pro executable (e.g. " + r'"C:\Program Files\WaveMetrics\Igor Pro 9 Folder\IgorBinaries_x64\Igor64.exe")' + ", then call configure_igor_launch(exe_path) with it before calling " + "this tool." + ) + + try: + _get_igor(force_reconnect=True) + already_running = True + except RuntimeError: + already_running = False + + if already_running: + return { + "launched": False, + "reason": ( + "An Igor Pro instance is already running and reachable via COM. " + "Refusing to launch a second one -- close the existing instance " + "first if a genuinely fresh one (e.g. relaunched with /UNATTENDED) " + "is actually wanted." + ), + } + + elevated = _is_current_process_elevated() + # _is_current_process_elevated() can return True, False, OR None (undetermined -- + # see its own docstring). A plain `if elevated`/`elevated else ...` treats None the + # same as False -- behaviorally fine here since undetermined should conservatively + # fall back to the not-elevated (UAC-prompting) path anyway, but written as an + # explicit `is True` check for clarity, matching the same fix already applied to + # configure_igor_launch's elevation_plan text above. + launch_method = ( + "direct_child_process" if elevated is True else "shell_execute_runas" + ) + + try: + if elevated is True: + subprocess.Popen( + [_configured_igor_exe_path, "/UNATTENDED"], + env=_build_igor_launch_env(), + ) + else: + win32api.ShellExecute( + 0, + "runas", + _configured_igor_exe_path, + "/UNATTENDED", + None, + win32con.SW_SHOWNORMAL, + ) + except Exception as e: + return { + "launched": False, + "reason": f"Failed to start the process ({e}).", + "launch_method": launch_method, + } + + deadline = time.monotonic() + wait_for_ready_seconds + attempts = 0 + while time.monotonic() < deadline: + attempts += 1 + try: + _get_igor(force_reconnect=True) + return { + "launched": True, + "launch_method": launch_method, + "com_ready": True, + "poll_attempts": attempts, + } + except RuntimeError: + time.sleep(_POST_LAUNCH_POLL_INTERVAL_SECONDS) + + return { + "launched": True, + "launch_method": launch_method, + "com_ready": False, + "poll_attempts": attempts, + "note": ( + f"The process was started, but no COM connection became reachable " + f"within {wait_for_ready_seconds:.0f}s. Igor Pro may still be " + "initializing (slower on first launch or a cold machine) -- try " + "check_bridge_health() again after waiting longer. If launch_method is " + "'shell_execute_runas', also consider that this Python process itself " + "is not elevated, which will prevent a COM connection indefinitely " + "regardless of how long you wait, until Claude Desktop is relaunched " + "as Administrator." + ), + } + + if __name__ == "__main__": mcp.run() From 4409302ba01dfa7ecaf5d6c9ce0fb440e50ec6ab Mon Sep 17 00:00:00 2001 From: Michael Huth Date: Thu, 30 Jul 2026 16:55:26 +0200 Subject: [PATCH 04/12] MCP: Cleanup of globals after DebuggerOptions is called - removed function to close databrowser, it was effectively never used --- Packages/doc/igor-pro-bridge.rst | 14 +--- .../igor-pro-bridge-1.22.0.mcpb | Bin 36504 -> 0 bytes .../igor-pro-bridge-1.23.0.mcpb | Bin 0 -> 36616 bytes tools/igor-mcp-bridge/server.py | 62 +++++++----------- 4 files changed, 24 insertions(+), 52 deletions(-) delete mode 100644 tools/igor-mcp-bridge/igor-pro-bridge-1.22.0.mcpb create mode 100644 tools/igor-mcp-bridge/igor-pro-bridge-1.23.0.mcpb diff --git a/Packages/doc/igor-pro-bridge.rst b/Packages/doc/igor-pro-bridge.rst index b9aed52732..0438bfbed8 100644 --- a/Packages/doc/igor-pro-bridge.rst +++ b/Packages/doc/igor-pro-bridge.rst @@ -137,23 +137,11 @@ Available tools ``get_bridge_version()`` Returns the version of this Igor Pro Bridge build that is actually running in the - current Claude Desktop session (``{"version": "1.22.0"}``). Added because there was + current Claude Desktop session (``{"version": "1.23.0"}``). Added because there was previously no way to confirm from inside a conversation which ``.mcpb`` build ended up loaded after an install/restart -- useful before relying on a specific recent fix or behavior change. -``close_data_browser()`` - Closes Igor Pro's own built-in (stock) Data Browser window, if one is currently - open, via ``ModifyBrowser close``. **Not** the MIES-specific ``DB_*`` DataBrowser - panel (``DB_OpenDataBrowser`` in ``MIES_DataBrowser.ipf``) -- this targets only - Igor's integrated Data Browser feature, which exists even without MIES loaded. - Added as a precaution after an open Data Browser was reported to sometimes cause - Igor Pro to crash while procedure code (e.g. a reload/compile cycle or a test run) - is running. On-demand only -- not called automatically by any other tool here. - Returns ``{"was_open": ..., "closed": ...}``; calling it when no Data Browser is - open is a safe no-op (``ModifyBrowser close``'s "The Data Browser must be active." - error is caught and treated as an expected outcome, not a failure). - ``check_compilation_state()`` Reports whether Igor's procedure code is currently compiled or uncompiled, using the same technique as ``IsProcGlobalCompiled()`` in diff --git a/tools/igor-mcp-bridge/igor-pro-bridge-1.22.0.mcpb b/tools/igor-mcp-bridge/igor-pro-bridge-1.22.0.mcpb deleted file mode 100644 index 237a2cba6b7549e2c23e643b7da189ec73fde0c5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36504 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 literal 0 HcmV?d00001 diff --git a/tools/igor-mcp-bridge/server.py b/tools/igor-mcp-bridge/server.py index dd71fe3a4e..b4266c20ca 100644 --- a/tools/igor-mcp-bridge/server.py +++ b/tools/igor-mcp-bridge/server.py @@ -619,7 +619,7 @@ def work(): # from inside a conversation which .mcpb build was actually loaded/active in Claude # Desktop, which made it impossible to verify whether a given fix (e.g. the reload/compile # timing relaxation) was actually in effect during a test -- see SESSION_NOTES.md. -_BRIDGE_VERSION = "1.22.0" +_BRIDGE_VERSION = "1.23.0" @mcp.tool() @@ -637,43 +637,6 @@ def get_bridge_version() -> dict: return {"version": _BRIDGE_VERSION} -@mcp.tool() -def close_data_browser() -> dict: - """Close Igor Pro's own built-in (stock) Data Browser window, if one is currently - open, via the documented `ModifyBrowser close` command (confirmed from Igor - Reference.ihf: "close | Closes the Data Browser."; without /M this targets the - regular, non-modal Data Browser). - - **This is NOT MIES's own DataBrowser panel** (the DB_* windows opened via - DB_OpenDataBrowser in MIES_DataBrowser.ipf) -- that is a distinct, MIES-authored - panel with its own close/hide behavior. This tool only targets Igor Pro's - integrated Data Browser feature, which exists even without MIES loaded at all. - - Added because an open instance of Igor's integrated Data Browser was reported to - sometimes cause Igor Pro to crash while procedure code is running (e.g. during a - reload/compile cycle or a test run) -- closing it first is a cheap precaution. - This is an on-demand tool only, not called automatically by any other tool in this - bridge (a deliberate choice, so existing tools' behavior does not change). - - Confirmed empirically that `ModifyBrowser close` raises an Igor-level error ("The - Data Browser must be active.") if no Data Browser is currently open, rather than - silently doing nothing -- there is no documented /Z-style quiet flag for this - operation. That specific error is caught here and treated as a normal, expected - outcome (nothing to close), not a failure; any other error is re-raised. - - Returns a dict with "was_open" (whether a Data Browser was actually open and got - closed) and "closed" (same value, kept for readability at the call site). - """ - errorCode, errorMsg, history, results = _execute2("ModifyBrowser close") - if errorCode == 0: - return {"was_open": True, "closed": True} - if "must be active" in errorMsg.lower(): - return {"was_open": False, "closed": False} - raise RuntimeError( - f"Failed attempting to close the Data Browser (error code {errorCode}): {errorMsg}" - ) - - @mcp.tool() def check_bridge_health() -> dict: """Check whether the Igor Pro bridge is actually able to reach Igor Pro right now, @@ -1418,10 +1381,22 @@ def reload_and_compile_procedures() -> dict: # when a Debugger pause is deliberately wanted (e.g. interactively testing a # breakpoint). +# The trailing KillVariables/Z is not optional cleanup -- it's load-bearing. Igor's +# DebuggerOptions operation creates V_enable/V_debugOnError/V_debugOnAbort/ +# V_NVAR_SVAR_WAVE_Checking as output variables in whatever data folder happens to be +# current *every single time it's invoked*, regardless of which arguments (if any) were +# passed. Confirmed via a live A/B test: running an identical test suite via +# execute_igor_command_unattended (which calls this query, and _apply_debugger_options +# below, on every call) left those four variables behind in root:, which made the next +# hardware test case's CHECK_EMPTY_FOLDER() teardown check fail spuriously -- while the +# same test suite run via plain execute_igor_command (no DebuggerOptions call involved) +# left root: untouched. The values are captured into `results` via fprintf on the same +# line, before the KillVariables/Z runs, so nothing is lost by cleaning up immediately. _DEBUGGER_STATE_CHECK_CMD = ( 'DebuggerOptions; fprintf 0, "enable=%d,debugOnError=%d,debugOnAbort=%d,' 'NVAR_SVAR_WAVE_Checking=%d", V_enable, V_debugOnError, V_debugOnAbort, ' - "V_NVAR_SVAR_WAVE_Checking" + "V_NVAR_SVAR_WAVE_Checking; " + "KillVariables/Z V_enable, V_debugOnError, V_debugOnAbort, V_NVAR_SVAR_WAVE_Checking" ) # Snapshot captured by get_debugger_state(), consumed by restore_debugger_settings(). @@ -1464,6 +1439,15 @@ def _apply_debugger_options(state: dict): f"NVAR_SVAR_WAVE_Checking={1 if state['nvar_svar_wave_checking'] else 0}" ) cmd = "DebuggerOptions " + ", ".join(parts) + # See the comment above _DEBUGGER_STATE_CHECK_CMD: DebuggerOptions always creates + # these four globals in the current data folder as a side effect of being called at + # all. Clean them up immediately so every caller of this helper (execute_igor_ + # command_unattended, load_experiment, set_debugger_enabled, restore_debugger_ + # settings) never leaves them behind as stray root: globals. + cmd += ( + "; KillVariables/Z V_enable, V_debugOnError, V_debugOnAbort, " + "V_NVAR_SVAR_WAVE_Checking" + ) errorCode, errorMsg, history, results = _execute2(cmd) if errorCode != 0: From 3837155a31a8cb468945552938471785369aa532 Mon Sep 17 00:00:00 2001 From: Michael Huth Date: Fri, 31 Jul 2026 13:14:16 +0200 Subject: [PATCH 05/12] MCP: Add function to retrieve help information from Igor Pro --- Packages/MIES/MIES_ClaudeHelper.ipf | 384 ++++++++++++++++++ Packages/doc/igor-pro-bridge.rst | 25 +- .../igor-pro-bridge-1.23.0.mcpb | Bin 36616 -> 0 bytes .../igor-pro-bridge-1.24.0.mcpb | Bin 0 -> 40520 bytes tools/igor-mcp-bridge/server.py | 252 +++++++++++- 5 files changed, 659 insertions(+), 2 deletions(-) delete mode 100644 tools/igor-mcp-bridge/igor-pro-bridge-1.23.0.mcpb create mode 100644 tools/igor-mcp-bridge/igor-pro-bridge-1.24.0.mcpb diff --git a/Packages/MIES/MIES_ClaudeHelper.ipf b/Packages/MIES/MIES_ClaudeHelper.ipf index 6c4d432b51..40e34c483f 100644 --- a/Packages/MIES/MIES_ClaudeHelper.ipf +++ b/Packages/MIES/MIES_ClaudeHelper.ipf @@ -11,6 +11,43 @@ #ifdef IGOR_PRO_BRIDGE +// PE (Portable Executable) format constants used by the CH_PE* helpers below to +// parse a compiled .xop's headers and resource directory. Offsets/sizes are +// per the documented Microsoft PE/COFF format (IMAGE_DOS_HEADER, +// IMAGE_FILE_HEADER, IMAGE_OPTIONAL_HEADER64, IMAGE_SECTION_HEADER, +// IMAGE_RESOURCE_DIRECTORY(_ENTRY), IMAGE_RESOURCE_DATA_ENTRY) unless noted as +// XOP Toolkit-specific. +static Constant CH_PE_DOS_SIGNATURE = 0x5A4D // "MZ" -- IMAGE_DOS_HEADER.e_magic +static Constant CH_PE_SIGNATURE = 0x00004550 // "PE\0\0" +static Constant CH_PE_SIGNATURE_SIZE = 4 // sizeof(uint32), size of the "PE\0\0" signature itself +static Constant CH_PE_E_LFANEW_OFFSET = 0x3C // IMAGE_DOS_HEADER offset to e_lfanew (PE header file offset) +static Constant CH_PE_OPTIONAL_HEADER_MAGIC_PE32PLUS = 0x20B // IMAGE_OPTIONAL_HEADER64.Magic +static Constant CH_PE_FILE_HEADER_SIZE = 20 // sizeof(IMAGE_FILE_HEADER) +static Constant CH_PE_FILE_HEADER_NUM_SECTIONS_OFFSET = 2 // offset within IMAGE_FILE_HEADER +static Constant CH_PE_FILE_HEADER_SIZE_OF_OPT_HDR_OFFSET = 16 // offset within IMAGE_FILE_HEADER +static Constant CH_PE_OPT_HEADER_DATA_DIR_OFFSET = 112 // PE32+ Optional Header offset to the Data Directory array +static Constant CH_PE_DATA_DIRECTORY_ENTRY_SIZE = 8 // sizeof(IMAGE_DATA_DIRECTORY): uint32 RVA + uint32 Size +static Constant CH_PE_RESOURCE_TABLE_DIR_INDEX = 2 // index of the Resource Table entry within the Data Directory array +static Constant CH_PE_SECTION_HEADER_SIZE = 40 // sizeof(IMAGE_SECTION_HEADER) +static Constant CH_PE_SECTION_VIRTUAL_SIZE_OFFSET = 8 // offset within IMAGE_SECTION_HEADER +static Constant CH_PE_SECTION_VIRTUAL_ADDRESS_OFFSET = 12 // offset within IMAGE_SECTION_HEADER +static Constant CH_PE_SECTION_RAW_DATA_PTR_OFFSET = 20 // offset within IMAGE_SECTION_HEADER +static Constant CH_PE_RESDIR_NUM_NAMED_ENTRIES_OFFSET = 12 // offset within IMAGE_RESOURCE_DIRECTORY +static Constant CH_PE_RESDIR_NUM_ID_ENTRIES_OFFSET = 14 // offset within IMAGE_RESOURCE_DIRECTORY +static Constant CH_PE_RESDIR_ENTRIES_OFFSET = 16 // offset within IMAGE_RESOURCE_DIRECTORY to its entry array +static Constant CH_PE_RESDIR_ENTRY_SIZE = 8 // sizeof(IMAGE_RESOURCE_DIRECTORY_ENTRY) +static Constant CH_PE_RESDIR_ENTRY_OFFSET_FIELD_OFFSET = 4 // offset within IMAGE_RESOURCE_DIRECTORY_ENTRY to its Offset field +static Constant CH_PE_RESOURCE_HIGH_BIT_FLAG = 0x80000000 // marks a named Name field, or a subdirectory Offset field +static Constant CH_PE_RESOURCE_OFFSET_MASK = 0x7FFFFFFF // strips CH_PE_RESOURCE_HIGH_BIT_FLAG off a Name/Offset field +static Constant CH_PE_DATA_ENTRY_SIZE_FIELD_OFFSET = 4 // offset within IMAGE_RESOURCE_DATA_ENTRY to its Size field +static Constant CH_XOP_RESOURCE_ID = 1100 // resource ID the XOP Toolkit uses for XOPI/XOPC/XOPF (XOPMan8.pdf) +static Constant CH_PE_PAD_CHAR = 0x20 // ASCII space, used to pre-size a string buffer for FBinRead +static Constant CH_UINT16_BYTE_SIZE = 2 // bytes per uint16 field (also one UTF-16LE code unit) +static Constant CH_BYTE_SHIFT_8BIT = 256 // multiplier for the high byte of a little-endian uint16 +static Constant CH_CSTRING_SEARCH_INITIAL_CHUNK = 256 // initial read size when hunting for a null terminator +static Constant CH_CSTRING_SEARCH_MAX_CHUNK = 8192 // give up (return -1) once search size exceeds this many bytes +static Constant CH_CMPSTR_CASE_SENSITIVE = 1 // CmpStr's caseSensitive flag (1 = case-sensitive, per Igor Reference) + /// AfterCompiledHook() is a predefined Igor hook: Igor calls it after ALL procedure /// windows have compiled successfully (confirmed from Igor Pro Folder/Igor Help /// Files/Advanced Topics.ihf). It is declared static so it coexists with any other @@ -56,4 +93,351 @@ static Function AfterCompiledHook() return 0 End + +/// CH_ListXOPExports(xopPath) reads the operations and functions a compiled .xop +/// file (a Windows DLL) adds to Igor, straight from the file's own bytes -- no +/// vendor documentation or source needed, and no live Igor Pro instance needs to +/// have that XOP loaded. +/// +/// Background (confirmed this session against WaveMetrics' own XOP Toolkit 8.01 +/// manual, XOPMan8.pdf, and cross-checked live against real, closed-source .xop +/// files including this repo's own XOPs-64bit/JSON-64.xop and .../ZeroMQ-64.xop, +/// whose decoded names matched FunctionList/OperationList's live results exactly): +/// every .xop declares the operations and functions it adds via two custom +/// resources, XOPC (operations) and XOPF (functions), each with resource ID 1100. +/// On Windows these are ordinary named Windows PE resources (resource TYPE name +/// literally "XOPC"/"XOPF", not a numeric type, and not a proprietary format) -- +/// the standard Windows resource compiler embeds them directly in the .xop/DLL's +/// own .rsrc section, the same mechanism used for any other named Win32 resource. +/// +/// Binary layout of the resource data itself (confirmed from the manual's Windows +/// .rc source examples for XOPC/XOPF): +/// XOPC: repeating {null-terminated name string; little-endian uint16 category +/// bitmask}, terminated by an empty-name (single 0x00 byte) record. +/// XOPF: repeating {null-terminated name string; uint16 category bitmask; uint16 +/// return-type code; uint16 parameter-type code * N; uint16 0 to terminate that +/// function's parameter list}, terminated the same way as XOPC. +/// This implementation only extracts the name lists (category/type bit values are +/// documented in the manual but not decoded here, since the name list is what's +/// actually useful for "what does this XOP add"). +/// +/// Only supports 64-bit (PE32+) XOPs -- true of every XOP actually in use in this +/// repo (all "-64.xop"/"64.xop" files) -- and aborts with a clear message for a +/// 32-bit (PE32) XOP rather than silently misreading it. +/// +/// Returns "operations:op1;op2;...\rfunctions:func1;func2;..." (either list may be +/// empty -- not every XOP adds both kinds). + +Function/S CH_ListXOPExports(string xopPath) + + string ops, funcs, result + + ops = CH_PEListXOPResource(xopPath, "XOPC") + funcs = CH_PEListXOPResource(xopPath, "XOPF") + + sprintf result, "operations:%s\rfunctions:%s", ops, funcs + + return result +End + +static Function CH_PEReadU16(variable refNum, variable pos) + + variable v + + FSetPos refNum, pos + FBinRead/F=2/U/B=3 refNum, v + + return v +End + +static Function CH_PEReadU32(variable refNum, variable pos) + + variable v + + FSetPos refNum, pos + FBinRead/F=3/U/B=3 refNum, v + + return v +End + +static Function/S CH_PEReadBytes(variable refNum, variable pos, variable numBytes) + + string s = PadString("", numBytes, CH_PE_PAD_CHAR) + + FSetPos refNum, pos + FBinRead refNum, s + + return s +End + +// Returns the length (not including the terminator) of a null-terminated string +// starting at pos, or -1 if no null byte turns up within a generous search cap +// (CH_CSTRING_SEARCH_MAX_CHUNK bytes -- far more than any real operation/function +// name). +static Function CH_PECStringLen(variable refNum, variable pos) + + string nul = num2char(0) + string chunk + variable chunkSize = CH_CSTRING_SEARCH_INITIAL_CHUNK + variable idx + + do + chunk = CH_PEReadBytes(refNum, pos, chunkSize) + idx = strsearch(chunk, nul, 0) + if(idx >= 0) + return idx + endif + chunkSize *= 2 + while(chunkSize <= CH_CSTRING_SEARCH_MAX_CHUNK) + + return -1 +End + +// Interprets two bytes at 0-based index offset within s as a little-endian uint16. +static Function CH_BytesToU16(string s, variable offset) + + return char2num(s[offset]) + char2num(s[offset + 1]) * CH_BYTE_SHIFT_8BIT +End + +// Reads a UTF-16LE resource name (as used for named PE resource directory +// entries, e.g. the "XOPC"/"XOPF" type names themselves) and returns it as a +// plain ASCII string -- sufficient here since every name this code looks for is +// pure ASCII. +static Function/S CH_PEReadUnicodeName(variable refNum, variable pos, variable numChars) + + string result = "" + variable i, code + + for(i = 0; i < numChars; i += 1) + code = CH_PEReadU16(refNum, pos + i * CH_UINT16_BYTE_SIZE) + result += num2char(code) + endfor + + return result +End + +// Converts an RVA (relative virtual address, relative to the module's load +// address) to a file offset, by finding which section contains it. Returns -1 if +// no section contains rva. +static Function CH_PERVAToFileOffset(variable refNum, variable sectionTableOffset, variable numSections, variable rva) + + variable i, secPos, secVA, secVirtSize, secRawPtr + + for(i = 0; i < numSections; i += 1) + secPos = sectionTableOffset + i * CH_PE_SECTION_HEADER_SIZE + secVirtSize = CH_PEReadU32(refNum, secPos + CH_PE_SECTION_VIRTUAL_SIZE_OFFSET) + secVA = CH_PEReadU32(refNum, secPos + CH_PE_SECTION_VIRTUAL_ADDRESS_OFFSET) + secRawPtr = CH_PEReadU32(refNum, secPos + CH_PE_SECTION_RAW_DATA_PTR_OFFSET) + if(rva >= secVA && rva < secVA + secVirtSize) + return secRawPtr + (rva - secVA) + endif + endfor + + return -1 +End + +// Walks the PE resource directory tree (Type -> ID -> Language, 3 levels) rooted +// at resourceSectionFileOffset, looking for a named type entry matching typeName +// containing a numeric-ID entry matching resourceID. All offsets inside the +// resource directory tree itself (including the string offsets for named +// entries) are relative to resourceSectionFileOffset -- confirmed from the PE +// format's own documented convention; only the leaf IMAGE_RESOURCE_DATA_ENTRY's +// own OffsetToData field is a true RVA (handled by the caller via +// CH_PERVAToFileOffset, not here). Returns the file offset of the matching +// IMAGE_RESOURCE_DATA_ENTRY structure, or -1 if typeName/resourceID isn't present +// (not every XOP has both XOPC and XOPF). +static Function CH_PEFindResourceOffset(variable refNum, variable resourceSectionFileOffset, string typeName, variable resourceID) + + variable numNamed, numIds, numEntries, i + variable entryPos, nameField, offsetField + variable subdirOffset, level2Base, level3SubdirOffset, level3Base + variable nameLen, strPos, dataEntryOffset + string entryName + + // --- Level 1: resource TYPE directory --- + numNamed = CH_PEReadU16(refNum, resourceSectionFileOffset + CH_PE_RESDIR_NUM_NAMED_ENTRIES_OFFSET) + numIds = CH_PEReadU16(refNum, resourceSectionFileOffset + CH_PE_RESDIR_NUM_ID_ENTRIES_OFFSET) + numEntries = numNamed + numIds + + subdirOffset = -1 + for(i = 0; i < numEntries; i += 1) + entryPos = resourceSectionFileOffset + CH_PE_RESDIR_ENTRIES_OFFSET + i * CH_PE_RESDIR_ENTRY_SIZE + nameField = CH_PEReadU32(refNum, entryPos) + offsetField = CH_PEReadU32(refNum, entryPos + CH_PE_RESDIR_ENTRY_OFFSET_FIELD_OFFSET) + + if(nameField & CH_PE_RESOURCE_HIGH_BIT_FLAG) + strPos = resourceSectionFileOffset + (nameField & CH_PE_RESOURCE_OFFSET_MASK) + nameLen = CH_PEReadU16(refNum, strPos) + entryName = CH_PEReadUnicodeName(refNum, strPos + CH_UINT16_BYTE_SIZE, nameLen) + if(CmpStr(entryName, typeName, CH_CMPSTR_CASE_SENSITIVE) == 0) + subdirOffset = offsetField + break + endif + endif + endfor + + if(subdirOffset < 0 || !(subdirOffset & CH_PE_RESOURCE_HIGH_BIT_FLAG)) + return -1 + endif + + // --- Level 2: resource ID directory --- + level2Base = resourceSectionFileOffset + (subdirOffset & CH_PE_RESOURCE_OFFSET_MASK) + numNamed = CH_PEReadU16(refNum, level2Base + CH_PE_RESDIR_NUM_NAMED_ENTRIES_OFFSET) + numIds = CH_PEReadU16(refNum, level2Base + CH_PE_RESDIR_NUM_ID_ENTRIES_OFFSET) + numEntries = numNamed + numIds + + level3SubdirOffset = -1 + for(i = 0; i < numEntries; i += 1) + entryPos = level2Base + CH_PE_RESDIR_ENTRIES_OFFSET + i * CH_PE_RESDIR_ENTRY_SIZE + nameField = CH_PEReadU32(refNum, entryPos) + offsetField = CH_PEReadU32(refNum, entryPos + CH_PE_RESDIR_ENTRY_OFFSET_FIELD_OFFSET) + + if(!(nameField & CH_PE_RESOURCE_HIGH_BIT_FLAG) && nameField == resourceID) + level3SubdirOffset = offsetField + break + endif + endfor + + if(level3SubdirOffset < 0 || !(level3SubdirOffset & CH_PE_RESOURCE_HIGH_BIT_FLAG)) + return -1 + endif + + // --- Level 3: language directory -- take the first (only, in every case seen + // so far) entry regardless of its language ID --- + level3Base = resourceSectionFileOffset + (level3SubdirOffset & CH_PE_RESOURCE_OFFSET_MASK) + numNamed = CH_PEReadU16(refNum, level3Base + CH_PE_RESDIR_NUM_NAMED_ENTRIES_OFFSET) + numIds = CH_PEReadU16(refNum, level3Base + CH_PE_RESDIR_NUM_ID_ENTRIES_OFFSET) + numEntries = numNamed + numIds + + if(numEntries == 0) + return -1 + endif + + entryPos = level3Base + CH_PE_RESDIR_ENTRIES_OFFSET + dataEntryOffset = CH_PEReadU32(refNum, entryPos + CH_PE_RESDIR_ENTRY_OFFSET_FIELD_OFFSET) + + return resourceSectionFileOffset + dataEntryOffset +End + +// Parses raw XOPC/XOPF resource bytes (already read into memory) into a +// semicolon-separated list of names, per the binary layout documented above +// CH_ListXOPExports. +static Function/S CH_ParseXOPResourceBlob(string raw, string resourceType) + + variable pos = 0 + variable len = strlen(raw) + string nul = num2char(0) + string names = "" + variable nameEnd, nameLen + string name + variable category, retType, paramType + + do + nameEnd = strsearch(raw, nul, pos) + if(nameEnd < 0) + break + endif + nameLen = nameEnd - pos + if(nameLen == 0) + break + endif + name = raw[pos, nameEnd - 1] + pos = nameEnd + 1 + + category = CH_BytesToU16(raw, pos) + pos += CH_UINT16_BYTE_SIZE + + if(CmpStr(resourceType, "XOPF", CH_CMPSTR_CASE_SENSITIVE) == 0) + retType = CH_BytesToU16(raw, pos) + pos += CH_UINT16_BYTE_SIZE + do + paramType = CH_BytesToU16(raw, pos) + pos += CH_UINT16_BYTE_SIZE + while(paramType != 0) + endif + + if(strlen(names) > 0) + names += ";" + endif + names += name + while(pos < len) + + return names +End + +// Opens xopPath, parses its PE headers, locates the named resourceType +// (CH_XOP_RESOURCE_ID) resource if present, and returns its decoded name list +// ("" if that XOP has no resource of that type). +static Function/S CH_PEListXOPResource(string xopPath, string resourceType) + + variable refNum + variable dosSig, peOffset, peSig + variable fileHeaderOffset, numSections, sizeOfOptHeader, optHeaderOffset, magic + variable resourceEntryOffset, resourceRVA, sectionTableOffset, resourceSectionFileOffset + variable dataEntryFileOffset, dataRVA, dataSize, dataFileOffset + string raw + + Open/R/Z refNum as xopPath + if(V_flag != 0) + Abort "CH_ListXOPExports: could not open file: " + xopPath + endif + + dosSig = CH_PEReadU16(refNum, 0) + if(dosSig != CH_PE_DOS_SIGNATURE) // "MZ" + Close refNum + Abort "CH_ListXOPExports: missing MZ/DOS signature, not a PE file: " + xopPath + endif + + peOffset = CH_PEReadU32(refNum, CH_PE_E_LFANEW_OFFSET) + peSig = CH_PEReadU32(refNum, peOffset) + if(peSig != CH_PE_SIGNATURE) // "PE\0\0" + Close refNum + Abort "CH_ListXOPExports: missing PE signature: " + xopPath + endif + + fileHeaderOffset = peOffset + CH_PE_SIGNATURE_SIZE + numSections = CH_PEReadU16(refNum, fileHeaderOffset + CH_PE_FILE_HEADER_NUM_SECTIONS_OFFSET) + sizeOfOptHeader = CH_PEReadU16(refNum, fileHeaderOffset + CH_PE_FILE_HEADER_SIZE_OF_OPT_HDR_OFFSET) + optHeaderOffset = fileHeaderOffset + CH_PE_FILE_HEADER_SIZE + magic = CH_PEReadU16(refNum, optHeaderOffset) + + if(magic != CH_PE_OPTIONAL_HEADER_MAGIC_PE32PLUS) + Close refNum + Abort "CH_ListXOPExports: only 64-bit (PE32+) XOPs are supported; magic=0x" + num2istr(magic) + " for " + xopPath + endif + + // PE32+ Data Directory array starts at offset CH_PE_OPT_HEADER_DATA_DIR_OFFSET + // within the Optional Header; CH_PE_RESOURCE_TABLE_DIR_INDEX (0-based) is the + // Resource Table entry -- confirmed from the PE format's own documented + // Optional Header layout. + resourceEntryOffset = optHeaderOffset + CH_PE_OPT_HEADER_DATA_DIR_OFFSET + CH_PE_RESOURCE_TABLE_DIR_INDEX * CH_PE_DATA_DIRECTORY_ENTRY_SIZE + resourceRVA = CH_PEReadU32(refNum, resourceEntryOffset) + + sectionTableOffset = optHeaderOffset + sizeOfOptHeader + resourceSectionFileOffset = CH_PERVAToFileOffset(refNum, sectionTableOffset, numSections, resourceRVA) + if(resourceSectionFileOffset < 0) + Close refNum + Abort "CH_ListXOPExports: could not locate the .rsrc section: " + xopPath + endif + + dataEntryFileOffset = CH_PEFindResourceOffset(refNum, resourceSectionFileOffset, resourceType, CH_XOP_RESOURCE_ID) + if(dataEntryFileOffset < 0) + Close refNum + return "" + endif + + dataRVA = CH_PEReadU32(refNum, dataEntryFileOffset) + dataSize = CH_PEReadU32(refNum, dataEntryFileOffset + CH_PE_DATA_ENTRY_SIZE_FIELD_OFFSET) + + dataFileOffset = CH_PERVAToFileOffset(refNum, sectionTableOffset, numSections, dataRVA) + if(dataFileOffset < 0) + Close refNum + Abort "CH_ListXOPExports: could not locate raw " + resourceType + " data: " + xopPath + endif + + raw = CH_PEReadBytes(refNum, dataFileOffset, dataSize) + Close refNum + + return CH_ParseXOPResourceBlob(raw, resourceType) +End #endif // IGOR_PRO_BRIDGE diff --git a/Packages/doc/igor-pro-bridge.rst b/Packages/doc/igor-pro-bridge.rst index 0438bfbed8..d9ad2fa5be 100644 --- a/Packages/doc/igor-pro-bridge.rst +++ b/Packages/doc/igor-pro-bridge.rst @@ -137,7 +137,7 @@ Available tools ``get_bridge_version()`` Returns the version of this Igor Pro Bridge build that is actually running in the - current Claude Desktop session (``{"version": "1.23.0"}``). Added because there was + current Claude Desktop session (``{"version": "1.24.0"}``). Added because there was previously no way to confirm from inside a conversation which ``.mcpb`` build ended up loaded after an install/restart -- useful before relying on a specific recent fix or behavior change. @@ -199,6 +199,29 @@ Available tools ``#include``/``#define`` directives not present in any on-disk ``.ipf`` file), the top-level global data folder layout, and the current Debugger settings. +``read_help_file(file_path)`` + Reads an Igor Pro help file (``.ihf``) as structured, formatted text -- e.g. to + confirm an operation's exact flags/behavior straight from Igor's own docs -- without + leaving any lasting change to Igor's help-window state. Better than an OS-level file + read for two reasons. First, Igor pre-registers every ``.ihf`` file in the Help Files + folder as an open help window (visible or hidden, ``WinList``'s ``WIN:512`` bit), and + a help-file view and a plain-notebook view of the same file are mutually exclusive + (``OpenNotebook/R`` fails with error 251 otherwise) -- this tool handles the required + ``CloseHelp/ALL`` -> ``OpenNotebook/R`` -> ``SaveNotebook`` export -> ``KillWindow/Z`` + -> ``OpenHelp`` restore dance, entirely in a ``finally`` block so a mid-sequence + failure still restores whatever help state existed beforehand. Second, and more + importantly: the returned ``"paragraphs"`` list (``[{"style": "Topic", "text": + "Debugging"}, ...]``) preserves the paragraph style name WaveMetrics' own help + authoring convention assigns to nearly every paragraph (e.g. ``"Topic"`` for a section + heading, ``"Code1"`` for a line of example code, ``"Steps"`` for a bullet item) -- + genuine content-block structure, not just flat prose. Not every XOP ships its own + help file this way -- some (e.g. the JSON XOP used elsewhere in this codebase) have + none at all and require external documentation instead; check + ``get_environment_summary()``'s ``loaded_xops`` field plus the global (``Igor + Application``) and user-specific (``Igor Pro User Files``) ``Igor Help Files`` + folders (both resolved via Igor's own ``SpecialDirPath`` function) before assuming a + given XOP has one. + ``configure_igor_launch(exe_path)`` Records the full path to the Igor Pro executable to use for ``launch_igor_pro_unattended``, for the rest of this bridge process's session. diff --git a/tools/igor-mcp-bridge/igor-pro-bridge-1.23.0.mcpb b/tools/igor-mcp-bridge/igor-pro-bridge-1.23.0.mcpb deleted file mode 100644 index 7312f1586a0eea15911df06ddbfa8cd6c1f8bd59..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36616 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 diff --git a/tools/igor-mcp-bridge/igor-pro-bridge-1.24.0.mcpb b/tools/igor-mcp-bridge/igor-pro-bridge-1.24.0.mcpb new file mode 100644 index 0000000000000000000000000000000000000000..9566adcff581aaaa94c9849229dd5ed154c310bc GIT binary patch literal 40520 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 dict: return {"results": results, "history": history} +# --- Reading .ihf help files as formatted notebooks --------------------------------- +# +# Confirmed live this session against Igor Pro 9 Nightly. An .ihf file is itself an +# Igor formatted-text notebook, and Igor pre-registers every one in the Help Files +# folder as "open as a help file" via an ordinary (often hidden) help window -- +# WinList("*", ";", "WIN:512") is the correct bit for these (confirmed against +# WinList's own documented bit table; an earlier guess of WIN:1024 was wrong and +# matches no window type at all). A help-file view and a plain-notebook view of the +# same file are mutually exclusive: OpenNotebook/R on a file whose help window +# (hidden or not) is currently open fails with error 251 ("already open but as a help +# file"). CloseHelp/ALL releases every currently-open help file so OpenNotebook/R can +# succeed; OpenHelp/V=.../INT=0 re-opens a specific file afterward to restore it. +# +# Reading the exported HTML (SaveNotebook/S=5) rather than the plain-text selection +# (Notebook .../GetSelection) matters because WaveMetrics' own help-authoring +# convention assigns a semantic paragraph style class to nearly every paragraph -- +# e.g. "Topic" for a heading, "Code1" for a line of example code, "Steps" for a +# bullet item -- confirmed live against several real .ihf files. This is a direct, +# reliable signal for a paragraph's content role that a flat plain-text read can't +# provide. + + +def _fprintf_query(expr: str) -> str: + """Run `fprintf 0, "%s", ` and return the resulting string, raising on + failure. Deliberately avoids ever declaring an intermediate `String` variable + for this: an Execute2 command runs as top-level interpreted code, so `String x = + ...` creates a process-lifetime global -- confirmed this session to collide with + "error 25: the name already exists as a variable" the second time the same + command runs in one Igor session. A bare fprintf has no such state to collide + with.""" + cmd = f'fprintf 0, "%s", {expr}' + errorCode, errorMsg, history, results = _execute2(cmd) + if errorCode != 0: + raise RuntimeError( + _format_execute2_error(cmd, errorCode, errorMsg, results, history) + ) + return results + + +def _winlist(match: str, options: str) -> list: + return [ + name + for name in _fprintf_query(f'WinList("{match}", ";", "{options}")').split(";") + if name + ] + + +def _igor_quote_path(path: str) -> str: + """Double every backslash so `path` is safe inside an Igor command string + literal -- Igor treats a single backslash as an escape character (Path + Separators, Advanced Topics.ihf).""" + return path.replace("\\", "\\\\") + + +def _resolve_help_file_path(bare_name: str): + """Resolve a bare help-file name (WinList's help-window bit never includes a + path -- "Procedure windows and help windows don't have names. WinList returns + the window title instead", confirmed this session) back to a full path, by + checking the two folders Igor Pro itself loads help files from: the global + `Igor Help Files` folder and the user-specific `Igor Help Files` folder. Both roots come from Igor's own + SpecialDirPath function rather than any hardcoded/guessed path, so this works + regardless of the specific Igor Pro version/install location. Returns None if + not found in either -- e.g. a third-party XOP's help file installed somewhere + else entirely.""" + for special_dir in ("Igor Application", "Igor Pro User Files"): + try: + base = _fprintf_query(f'SpecialDirPath("{special_dir}", 0, 1, 0)') + except RuntimeError: + continue + if not base: + continue + candidate = os.path.join(base, "Igor Help Files", bare_name) + if os.path.isfile(candidate): + return candidate + return None + + +class _NotebookHTMLParser(html.parser.HTMLParser): + """Extracts one {"style": ..., "text": ...} record per

paragraph from a + notebook's HTML export (SaveNotebook/S=5).""" + + def __init__(self): + super().__init__(convert_charrefs=True) + self.paragraphs = [] + self._in_paragraph = False + self._style = "" + self._text_parts = [] + + def handle_starttag(self, tag, attrs): + if tag.lower() == "p": + self._in_paragraph = True + self._style = "" + self._text_parts = [] + for key, value in attrs: + if key.lower() == "class" and value: + self._style = value + + def handle_endtag(self, tag): + if tag.lower() == "p" and self._in_paragraph: + text = "".join(self._text_parts).strip() + self.paragraphs.append({"style": self._style, "text": text}) + self._in_paragraph = False + + def handle_data(self, data): + if self._in_paragraph: + self._text_parts.append(data) + + +@mcp.tool() +def read_help_file(file_path: str) -> dict: + """Read an Igor Pro help file (.ihf) as structured, formatted text -- e.g. to + look up an operation's exact flags/behavior straight from Igor's own docs -- + without leaving any lasting change to Igor's help-window state. + + Better than an OS-level file read for two reasons. First, .ihf files are + themselves Igor formatted-text notebooks, and Igor pre-registers every one in + the Help Files folder as an open help window (visible or hidden); this tool + handles the required CloseHelp/ALL -> OpenNotebook/R -> ... -> OpenHelp restore + dance so the caller doesn't have to. Second, and more importantly: the returned + "paragraphs" list preserves the paragraph style name WaveMetrics' own help + authoring convention assigns to each paragraph (e.g. "Topic" for a section + heading, "Code1" for a line of example code, "Steps" for a bullet item) -- + genuine content-block structure, not just flat prose. + + file_path must be a full path to an existing .ihf file (e.g. one found by + listing the Help Files folder -- see get_environment_summary's "loaded_xops" + field plus the global/user Help Files folders under Igor's own installation for + XOP-supplied help files, which don't all exist -- some XOPs ship none at all, + and their functions/operations then require external documentation instead). + + Full sequence, entirely reversible even if a step fails partway through: + 1. Snapshot every currently open help file (visible or hidden, via WinList's + WIN:512 bit) and every currently open plain-notebook window. + 2. CloseHelp/ALL (required: an .ihf file can't be opened as a notebook while + Igor considers it already open as a help file). + 3. OpenNotebook/R file_path, then diff WinList's notebook list against the + step-1 snapshot to find the name Igor assigned the new window (e.g. + "Notebook0") -- OpenNotebook doesn't return this directly. + 4. SaveNotebook/O/S=5/H=... export to a local temporary HTML file, parsed + here into the returned "paragraphs" list, then delete the temp file. + 5. KillWindow/Z the temporary notebook. + 6. Restore every help file captured in step 1 via OpenHelp/V=.../INT=0, + re-resolving each bare file name back to a full path via + SpecialDirPath("Igor Application"/"Igor Pro User Files", ...) + "Igor Help + Files" (Igor's global vs. user-specific include folders). + + Steps 5-6 run in a `finally` block, so a failure in step 3 or 4 still restores + whatever help state existed before this call. Returns a dict with: + - "paragraphs": [{"style": "Topic", "text": "Debugging"}, ...] -- "style" is + "" for a paragraph with no explicit class. + - "restore_failures": bare file names from step 1 that could not be resolved + back to a full path (e.g. a help file supplied from somewhere other than + the two standard Help Files folders) -- these were NOT reopened, unlike + every other file captured in the snapshot. + + Raises if file_path does not exist, or if OpenNotebook/SaveNotebook fail (e.g. + file_path is not actually a notebook-compatible file). + """ + normalized = os.path.abspath(file_path) + if not os.path.isfile(normalized): + raise RuntimeError(f"'{normalized}' does not exist or is not a file.") + quoted_path = _igor_quote_path(normalized) + + # Step 1: snapshot before touching anything. + help_all = _winlist("*", "WIN:512") + help_visible = set(_winlist("*", "WIN:512,VISIBLE:1")) + notebooks_before = set(_winlist("*", "WIN:16")) + + tmp_fd, tmp_html_path = tempfile.mkstemp(suffix=".html", prefix="igor_help_") + os.close(tmp_fd) + os.remove(tmp_html_path) # SaveNotebook must create it fresh; only the name is reused + quoted_tmp_html = _igor_quote_path(tmp_html_path) + + notebook_name = None + try: + # Step 2. + errorCode, errorMsg, history, results = _execute2("CloseHelp/ALL") + if errorCode != 0: + raise RuntimeError( + _format_execute2_error( + "CloseHelp/ALL", errorCode, errorMsg, results, history + ) + ) + + # Step 3. + open_cmd = f'OpenNotebook/R "{quoted_path}"' + errorCode, errorMsg, history, results = _execute2(open_cmd) + if errorCode != 0: + raise RuntimeError( + _format_execute2_error(open_cmd, errorCode, errorMsg, results, history) + ) + new_names = [n for n in _winlist("*", "WIN:16") if n not in notebooks_before] + if not new_names: + raise RuntimeError( + "OpenNotebook/R succeeded but no new notebook window was found via " + f"WinList -- before: {sorted(notebooks_before)!r}" + ) + notebook_name = new_names[0] + + # Step 4. + export_cmd = ( + f'SaveNotebook/O/S=5/H={{"UTF-8", 3, 7, 0, 0.9, 32}} {notebook_name} ' + f'as "{quoted_tmp_html}"' + ) + errorCode, errorMsg, history, results = _execute2(export_cmd) + if errorCode != 0: + raise RuntimeError( + _format_execute2_error( + export_cmd, errorCode, errorMsg, results, history + ) + ) + with open(tmp_html_path, "r", encoding="utf-8") as f: + html_text = f.read() + parser = _NotebookHTMLParser() + parser.feed(html_text) + finally: + # Step 5 (best-effort -- must not skip step 6). + if notebook_name: + try: + _execute2(f"KillWindow/Z {notebook_name}") + except Exception: + pass + try: + if os.path.isfile(tmp_html_path): + os.remove(tmp_html_path) + except Exception: + pass + + # Step 6. + restore_failures = [] + for name in help_all: + resolved = _resolve_help_file_path(name) + if resolved is None: + restore_failures.append(name) + continue + visible_flag = 1 if name in help_visible else 0 + restore_cmd = ( + f'OpenHelp/V={visible_flag}/INT=0/Z=1 "{_igor_quote_path(resolved)}"' + ) + try: + _execute2(restore_cmd) + except Exception: + restore_failures.append(name) + + return {"paragraphs": parser.paragraphs, "restore_failures": restore_failures} + + # --- Environment summary ----------------------------------------------------------- # # Confirmed against a live Igor Pro instance during development (Igor Pro 10.03, build From 875e7fe423856f3aa1b6bdcd19aabd6a36862e66 Mon Sep 17 00:00:00 2001 From: Michael Huth Date: Mon, 3 Aug 2026 16:10:27 +0200 Subject: [PATCH 06/12] MCP: Add installer script using a requirements.txt files --- Packages/doc/igor-pro-bridge.rst | 71 ++++- .../igor-pro-bridge-1.24.0.mcpb | Bin 40520 -> 0 bytes .../igor-pro-bridge-1.25.0.mcpb | Bin 0 -> 46721 bytes tools/igor-mcp-bridge/install.ps1 | 258 ++++++++++++++++++ tools/igor-mcp-bridge/requirements.txt | 200 ++++++++++++++ tools/igor-mcp-bridge/server.py | 39 ++- 6 files changed, 554 insertions(+), 14 deletions(-) delete mode 100644 tools/igor-mcp-bridge/igor-pro-bridge-1.24.0.mcpb create mode 100644 tools/igor-mcp-bridge/igor-pro-bridge-1.25.0.mcpb create mode 100644 tools/igor-mcp-bridge/install.ps1 create mode 100644 tools/igor-mcp-bridge/requirements.txt diff --git a/Packages/doc/igor-pro-bridge.rst b/Packages/doc/igor-pro-bridge.rst index d9ad2fa5be..a018172660 100644 --- a/Packages/doc/igor-pro-bridge.rst +++ b/Packages/doc/igor-pro-bridge.rst @@ -17,6 +17,10 @@ The code lives in ``tools/igor-mcp-bridge/``: - ``server.py``: the MCP server implementation (Python, using ``pywin32`` for COM). - ``igor-pro-bridge-*.mcpb``: packaged Claude Desktop Extension bundles built from ``server.py`` via the `mcpb `__ CLI. +- ``requirements.txt``: pinned Python dependency versions (see :ref:`igor_pro_bridge_requirements`). +- ``install.ps1``: installs those pinned dependencies into the correct Python + environment and completes pywin32's post-install step -- see + :ref:`igor_pro_bridge_installation`. The companion procedure file ``Packages/MIES/MIES_ClaudeHelper.ipf`` (included from ``MIES_Include.ipf``) provides an ``AfterCompiledHook`` used by the bridge to get a more @@ -40,6 +44,8 @@ failed -- the bridge checks the returned error code itself and raises a Python ``RuntimeError`` when appropriate. Data is retrieved by including ``fprintf 0, "..."`` calls in the command string and reading the result back. +.. _igor_pro_bridge_requirements: + Requirements ------------ @@ -54,10 +60,21 @@ Requirements Igor's own Automation Server reference and is not optional. Note that reopening Claude Desktop normally does not preserve elevation from a previous launch -- it must be relaunched via "Run as administrator" each time. -- Python, accessible as ``python`` on ``PATH``, with the ``mcp`` and ``pywin32`` - packages installed (``pip install mcp pywin32``, followed by - ``python -m pywin32_postinstall -install``). The packaged extension does not vendor - these. +- Python 3.10 or later, accessible as ``python`` on ``PATH``, with the pinned packages + in ``requirements.txt`` (``mcp==1.29.0``, ``pywin32==312``) installed into that same + environment -- see :ref:`igor_pro_bridge_installation` below for how. The packaged + extension does not vendor these. + + ``mcp`` is pinned below its breaking v2.0.0 line (released 2026-07-27/28, protocol + revision 2026-07-28): v2 renamed ``FastMCP`` to ``MCPServer`` and moved it from + ``mcp.server.fastmcp`` to ``mcp.server.mcpserver``, among other changes, while + ``server.py`` still uses the v1 ``from mcp.server.fastmcp import FastMCP`` API. An + unpinned ``mcp`` dependency (or a plain ``pip install mcp`` today) resolves to v2 and + breaks the bridge outright (``ModuleNotFoundError``) -- confirmed directly from both + wheels' contents. Do not lift the ``<2`` upper bound without migrating ``server.py`` + to the v2 API first. + +.. _igor_pro_bridge_installation: Installation ------------ @@ -69,9 +86,28 @@ servers in current Claude Desktop builds). - Build: ``mcpb pack tools/igor-mcp-bridge tools/igor-mcp-bridge/igor-pro-bridge-X.Y.Z.mcpb`` - Install: Claude Desktop -> Settings -> Extensions -> Advanced settings -> Extension Developer -> Install Extension, then select the ``.mcpb`` file. -- After installing a new version, fully restart Claude Desktop (elevated) so the - updated server code is actually loaded -- newly added tools can otherwise lag behind - what's installed. +- **Before first use (or whenever a Python dependency changes), run + ``tools/igor-mcp-bridge/install.ps1`` from an elevated PowerShell** to install the + pinned packages and complete pywin32's required post-install step + (``Scripts\pywin32_postinstall.py -install``, which a plain ``pip install`` does not + do). Run it elevated specifically because Claude Desktop's manifest invokes the bridge + as the bare command ``python``, resolved via whatever ``PATH`` Claude Desktop's own + *elevated* process environment has at launch time -- not necessarily the same + interpreter an interactive elevated console session would resolve (e.g. a + PowerShell-profile-only conda activation, or a Microsoft Store app-execution-alias + stub that behaves differently once elevated). ``install.ps1`` resolves ``python.exe`` + from the Machine/User ``PATH`` registry values directly rather than trusting the + invoking shell's own possibly-customized ``$env:Path``, to mirror what a freshly + launched elevated Claude Desktop process actually sees; pass ``-PythonPath`` to + override this if needed. See ``Get-Help ./install.ps1 -Full`` for the complete + rationale and all steps performed. +- After installing (or after running ``install.ps1``), fully restart Claude Desktop + (elevated) so the updated server code/environment is actually picked up -- newly added + tools, or a freshly installed dependency, can otherwise lag behind what's on disk. +- Call ``get_bridge_version()`` afterward and confirm its ``python_executable`` field + matches the interpreter ``install.ps1`` installed into. If it doesn't, Claude Desktop + resolved a different Python than ``install.ps1`` guessed -- re-run ``install.ps1 + -PythonPath ``. Available tools ---------------- @@ -137,10 +173,23 @@ Available tools ``get_bridge_version()`` Returns the version of this Igor Pro Bridge build that is actually running in the - current Claude Desktop session (``{"version": "1.24.0"}``). Added because there was - previously no way to confirm from inside a conversation which ``.mcpb`` build ended - up loaded after an install/restart -- useful before relying on a specific recent - fix or behavior change. + current Claude Desktop session, plus which Python interpreter/packages it's actually + running with:: + + { + "version": "1.25.0", + "python_executable": "C:\\Python312\\python.exe", + "python_version": "3.12.4", + "mcp_package_version": "1.29.0", + "pywin32_build": "312" + } + + Added because there was previously no way to confirm from inside a conversation + which ``.mcpb`` build ended up loaded after an install/restart -- useful before + relying on a specific recent fix or behavior change. The ``python_executable`` field + is also the authoritative way to confirm which Python environment Claude Desktop + actually launched the bridge with, e.g. to cross-check against what + ``install.ps1`` installed into -- see :ref:`igor_pro_bridge_installation`. ``check_compilation_state()`` Reports whether Igor's procedure code is currently compiled or uncompiled, using the diff --git a/tools/igor-mcp-bridge/igor-pro-bridge-1.24.0.mcpb b/tools/igor-mcp-bridge/igor-pro-bridge-1.24.0.mcpb deleted file mode 100644 index 9566adcff581aaaa94c9849229dd5ed154c310bc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40520 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 -m pip install --upgrade pip` + 4. ` -m pip install --require-hashes -r requirements.txt` (pinned, + hash-verified versions -- see that file's own comments, notably why "mcp" is + pinned below its new v2 line, and why every transitive dependency is listed + and hashed too, not just mcp/pywin32 themselves). + 5. Run pywin32's required post-install step + (Scripts\pywin32_postinstall.py -install), which `pip install pywin32` alone + does not do -- it registers pywin32's COM-support DLLs, without which + win32com.client calls can fail unpredictably. + 6. Import-check mcp and win32com.client with the same interpreter, and print + its full path/version for you to cross-check. + + After this script finishes, fully restart Claude Desktop (elevated), then call the + bridge's get_bridge_version tool from a conversation -- its "python_executable" + field is the authoritative answer for which interpreter Claude Desktop actually + launched. If it doesn't match what this script installed into, re-run this script + with -PythonPath pointing at that reported path. + +.PARAMETER PythonPath + Full path to a specific python.exe to install into, bypassing auto-resolution + entirely. Use this if auto-resolution picks the wrong interpreter, or if you + already know the right one (e.g. from get_bridge_version()'s "python_executable"). + +.PARAMETER RequirementsFile + Path to the requirements.txt to install from. Defaults to requirements.txt next to + this script. + +.EXAMPLE + .\install.ps1 + Auto-resolve python.exe and install. + +.EXAMPLE + .\install.ps1 -PythonPath 'C:\Python312\python.exe' + Install into a specific, already-known-correct interpreter. +#> + +[CmdletBinding()] +param( + [string]$PythonPath, + [string]$RequirementsFile = (Join-Path $PSScriptRoot 'requirements.txt') +) + +$ErrorActionPreference = 'Stop' + +function Test-IsElevated { + $identity = [Security.Principal.WindowsIdentity]::GetCurrent() + $principal = New-Object Security.Principal.WindowsPrincipal($identity) + return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) +} + +function Test-IsWindowsAppsStub { + <# + Detects the Microsoft Store "app execution alias" placeholder for python.exe + (typically under %LOCALAPPDATA%\Microsoft\WindowsApps\python.exe): a small + reparse-point stub that, when run without a real Store Python install behind + it, just opens the Store instead of running anything. A real install (Store + Python or otherwise) landing in that same folder is not rejected -- only the + tiny placeholder is, distinguished here by file size. + #> + param([string]$Path) + + if ($Path -notlike '*\WindowsApps\*') { + return $false + } + + $item = Get-Item -LiteralPath $Path -ErrorAction SilentlyContinue + return ($item -and $item.Length -lt 100KB) +} + +function Resolve-ClaudeDesktopPython { + <# + Mirrors how Claude Desktop's own elevated process resolves the bare command + "python" from its manifest.json, without trusting this interactive + PowerShell session's own $env:Path -- see the script's top-level comment-based + help for the full rationale. Returns the first matching python.exe found by + searching the Machine PATH, then the User PATH, in that order (the same + composition order Windows uses to build a fresh process's environment block), + or $null if none is found. + #> + $machinePath = [Environment]::GetEnvironmentVariable('Path', 'Machine') + $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') + $combined = @($machinePath, $userPath) -join ';' + $dirs = $combined -split ';' | Where-Object { $_ } + + foreach ($dir in $dirs) { + $candidate = Join-Path $dir 'python.exe' + if ((Test-Path -LiteralPath $candidate) -and -not (Test-IsWindowsAppsStub $candidate)) { + return (Resolve-Path -LiteralPath $candidate).ProviderPath + } + } + + return $null +} + +function Invoke-Checked { + <# + Runs an external executable and throws if it exits nonzero. Needed because + PowerShell's $ErrorActionPreference does not apply to native command exit + codes -- only to PowerShell-native errors -- so failures here would otherwise + be silently ignored and the script would carry on as if each step succeeded. + #> + param( + [Parameter(Mandatory)][string]$Exe, + [Parameter(Mandatory)][string[]]$Arguments + ) + + & $Exe @Arguments + if ($LASTEXITCODE -ne 0) { + throw "Command failed (exit code $LASTEXITCODE): `"$Exe`" $($Arguments -join ' ')" + } +} + +# --- 1. Elevation check ----------------------------------------------------------- + +if (-not (Test-IsElevated)) { + Write-Error ( + "This script must run elevated (as Administrator) -- both because the " + + "pywin32 post-install step below requires it, and because it needs to " + + "match the same elevated environment Claude Desktop itself runs in. " + + "Re-run this script from an elevated PowerShell (right-click PowerShell -> " + + "Run as administrator)." + ) + exit 1 +} + +# --- 2. Resolve the target python.exe ---------------------------------------------- + +if ($PythonPath) { + if (-not (Test-Path -LiteralPath $PythonPath)) { + throw "Specified -PythonPath does not exist: $PythonPath" + } + $python = (Resolve-Path -LiteralPath $PythonPath).ProviderPath + Write-Host "Using explicitly specified python: $python" +} else { + $python = Resolve-ClaudeDesktopPython + if (-not $python) { + throw ( + "Could not find python.exe on either the Machine or User PATH " + + "(registry-level, not this session's own `$env:Path). Install Python " + + "first, or pass -PythonPath explicitly." + ) + } + Write-Host "Auto-resolved python (Machine/User PATH): $python" + Write-Host ( + "If Claude Desktop actually uses a different interpreter (e.g. one only " + + "available via a shell profile/conda environment, not the registry PATH " + + "searched here), re-run this script with -PythonPath after confirming the " + + "real one via the bridge's get_bridge_version tool (its " + + "'python_executable' field)." + ) +} + +& $python --version +if ($LASTEXITCODE -ne 0) { + throw "`"$python`" --version failed (exit code $LASTEXITCODE) -- is this a valid Python interpreter?" +} + +# --- 3./4. Install pinned requirements ---------------------------------------------- + +if (-not (Test-Path -LiteralPath $RequirementsFile)) { + throw "requirements.txt not found at: $RequirementsFile" +} + +Write-Host "`nUpgrading pip..." +Invoke-Checked -Exe $python -Arguments @('-m', 'pip', 'install', '--upgrade', 'pip') + +Write-Host "`nInstalling pinned, hash-verified packages from $RequirementsFile ..." +# --require-hashes is passed explicitly (pip would already enter hash-checking mode +# automatically the moment any requirement has a --hash) so that if requirements.txt +# is ever accidentally edited down to unhashed entries, this fails loudly with a clear +# pip error instead of silently installing an unverified package. +Invoke-Checked -Exe $python -Arguments @('-m', 'pip', 'install', '--require-hashes', '-r', $RequirementsFile) + +# --- 5. pywin32 post-install --------------------------------------------------------- + +# Scripts\ is a sibling of python.exe's own directory for both a full Python install +# and a virtual environment -- the standard Windows layout pywin32's installer targets. +$scriptsDir = Join-Path (Split-Path -Parent $python) 'Scripts' +$postInstall = Join-Path $scriptsDir 'pywin32_postinstall.py' + +if (-not (Test-Path -LiteralPath $postInstall)) { + throw ( + "pywin32_postinstall.py not found at expected path: $postInstall -- the " + + "pywin32 install above may have failed, or landed in an unexpected layout " + + "for this Python installation." + ) +} + +Write-Host "`nRunning pywin32 post-install step ($postInstall -install)..." +Invoke-Checked -Exe $python -Arguments @($postInstall, '-install') + +# --- 6. Verify ------------------------------------------------------------------------- + +Write-Host "`nVerifying the installed packages import correctly..." +# Written to a temp .py file and run as a script, rather than passed via `python -c +# `: PowerShell's argument-quoting for native executables is unreliable for a +# single argument that itself contains both embedded double quotes and newlines (a +# multi-line Python snippet with f-strings hits both) -- confirmed for real, this +# mangled the double quotes around the f-strings below into a Python SyntaxError when +# first tried as `-c $verifyScript`. A temp file sidesteps command-line +# quoting/escaping entirely. +$verifyScript = @' +import importlib.metadata +import sys +import mcp +import win32com.client + +print(f"python_executable: {sys.executable}") +print(f"python_version: {sys.version.split()[0]}") +print(f"mcp_package_version: {importlib.metadata.version('mcp')}") +print(f"pywin32_build: {importlib.metadata.version('pywin32')}") +print("win32com.client import OK") +'@ +$verifyScriptPath = Join-Path ([System.IO.Path]::GetTempPath()) "igor-bridge-verify-$([guid]::NewGuid()).py" +try { + Set-Content -LiteralPath $verifyScriptPath -Value $verifyScript -Encoding utf8 + Invoke-Checked -Exe $python -Arguments @($verifyScriptPath) +} finally { + Remove-Item -LiteralPath $verifyScriptPath -ErrorAction SilentlyContinue +} + +Write-Host ( + "`nDone. Fully restart Claude Desktop (elevated), then call the bridge's " + + "get_bridge_version tool and confirm its 'python_executable' field matches: $python`n" + + "If it does not match, re-run this script with -PythonPath set to the path " + + "get_bridge_version() actually reports." +) diff --git a/tools/igor-mcp-bridge/requirements.txt b/tools/igor-mcp-bridge/requirements.txt new file mode 100644 index 0000000000..5bc999efb0 --- /dev/null +++ b/tools/igor-mcp-bridge/requirements.txt @@ -0,0 +1,200 @@ +# Pinned dependencies for the Igor Pro Bridge MCP server (tools/igor-mcp-bridge/server.py). +# Install with install.ps1 (recommended -- also runs pywin32's required post-install +# step), or manually via: +# -m pip install --require-hashes -r requirements.txt +# \pywin32_postinstall.py -install +# +# Every package below -- direct AND transitive -- is pinned to an exact version with +# one or more --hash=sha256:... values. This is pip's "hash-checking mode": it is +# triggered automatically the moment any requirement has a --hash, and once active, +# EVERY package pip would install (the whole dependency tree, not just mcp/pywin32 +# themselves) must appear here, pinned and hashed, or the install is refused outright. +# This is a supply-chain-integrity measure (protects against a compromised/tampered +# package on PyPI, or a MITM'd download) on top of the version pins already protecting +# against unwanted upgrades (see the mcp v1/v2 note below). +# +# Regenerating this file (e.g. after deliberately bumping a version, or to add a newly +# released Python version): resolve the full dependency tree for the target platform/ +# Python versions with `pip download --platform win_amd64 --python-version +# --implementation cp --abi cp --only-binary=:all: -d

-r requirements.in`, +# where is each Python minor version actually in use (currently 310/311/312/313/ +# 314 -- pyproject.toml declares an open-ended `>=3.10` floor, so a *new* Python release +# can reach this bridge before its wheels are covered here; if install.ps1/pip reports a +# hash mismatch or "no matching distribution", that's the likely cause -- add that +# version the same way, don't just widen/drop the hash pin). Repeat per Python version to +# catch any per-version wheel/version divergence -- rpds-py below is a real example of a +# package whose resolved *version*, not just its wheel file, differs between Python 3.10 +# and 3.11+ -- then sha256sum every downloaded wheel. Verify with +# `pip download --require-hashes -r requirements.txt ...` (same flags, run under an +# actual interpreter of that target version -- environment markers like python_version/ +# sys_platform are evaluated against the REAL running interpreter, not these download +# target flags, which only affect wheel-tag selection) before committing -- this +# round-trips both the hash values and pip's ability to parse the file. +# +# mcp: pinned to the last 1.x release. The MCP Python SDK's v2 line (2.0.0, released +# 2026-07-27/28 alongside MCP protocol revision 2026-07-28) is a deliberate breaking +# rework: FastMCP was renamed to MCPServer and moved from mcp.server.fastmcp to +# mcp.server.mcpserver, among other changes. server.py still uses the v1 API +# (`from mcp.server.fastmcp import FastMCP`), so an unpinned or `>=1.0.0` requirement +# would silently resolve to 2.x today and break the bridge outright (ModuleNotFoundError +# on import) -- confirmed by inspecting both the 1.29.0 and 2.0.0 wheels directly. +# Do not remove the exact pin without migrating server.py to the v2 API first. + +mcp==1.29.0 \ + --hash=sha256:f5a075bb611f23d6f4d080c6a1699fa62772eebc562ba9e66b306ddde1c755f7 + +# pywin32: provides win32com.client (COM automation), win32api/win32con/win32gui/ +# win32process (elevation and window handling), and pywintypes (COM error types) -- +# all used directly by server.py. Also requires the separate post-install step +# (Scripts\pywin32_postinstall.py -install) to register its COM-support DLLs; both +# install.ps1 and a plain `pip install` alone are not sufficient without that step. +# Hashes cover the cp310/cp311/cp312/cp313/cp314 win_amd64 wheels. +pywin32==312; sys_platform == "win32" \ + --hash=sha256:5dbc35d2b5320dc07f25fa31269cfb767471002b17de5eb067d03da68c7cb2db \ + --hash=sha256:a4dd3a848290ef724347b19f301045831d8e802fa4464f491b98b1e0a081432e \ + --hash=sha256:b457f6d628a47e8a7346ce22acb7e1a46a4a78b52e1d17e1af56871bd19a93bc \ + --hash=sha256:c53e878d15a1c44788082bfe712a905433473aa38f86375b7cf8b45e3acbaaf9 \ + --hash=sha256:d11417d84412f859b722fad0841b3614459ed0047f7542d8362e77884f6b6e8a + +# --- Transitive dependencies (mcp's own dependency tree) ------------------------- +# Required by pip's hash-checking mode (see the top comment) -- pip resolves these +# via mcp's own pyproject.toml at install time regardless, this just makes each one +# an explicit, hash-verified pin instead of an unverified floating resolution. + +# async I/O abstraction -- required by mcp, httpx, starlette, sse-starlette +anyio==4.14.2 \ + --hash=sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494 + +# anyio's exceptiongroup backport (Python < 3.11 only, installed unconditionally here +# for simplicity -- harmless no-op on newer Python) +exceptiongroup==1.3.1 \ + --hash=sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598 + +# required by anyio and httpx +idna==3.18 \ + --hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 + +# HTTP client -- required by mcp +httpx==0.28.1 \ + --hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad + +# required by httpx +httpcore==1.0.9 \ + --hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 + +# HTTP/1.1 protocol implementation -- required by httpcore +h11==0.16.0 \ + --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 + +# Server-Sent Events support for httpx -- required by mcp +httpx-sse==0.4.3 \ + --hash=sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc + +# default CA bundle -- required by httpx +certifi==2026.7.22 \ + --hash=sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 + +# data validation -- required by mcp, pydantic-settings +pydantic==2.13.4 \ + --hash=sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba + +# pydantic's compiled (Rust) backend -- per-Python-version wheels (cp310/311/312/313/314) +pydantic-core==2.46.4 \ + --hash=sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292 \ + --hash=sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33 \ + --hash=sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac \ + --hash=sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf \ + --hash=sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89 + +# required by pydantic +annotated-types==0.8.0 \ + --hash=sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0 + +# required by pydantic (unconditionally) and starlette (Python < 3.13) +typing-extensions==4.16.0 \ + --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 + +# required by pydantic and pydantic-settings +typing-inspection==0.4.2 \ + --hash=sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7 + +# required by mcp +pydantic-settings==2.14.2 \ + --hash=sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440 + +# required by pydantic-settings +python-dotenv==1.2.2 \ + --hash=sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a + +# validates tool input schemas -- required by mcp +jsonschema==4.26.0 \ + --hash=sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce + +# required by jsonschema +attrs==26.1.0 \ + --hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 + +# required by jsonschema +referencing==0.37.0 \ + --hash=sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231 + +# required by referencing/jsonschema +jsonschema-specifications==2025.9.1 \ + --hash=sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe + +# referencing/jsonschema's Rust-backed persistent data structures. Split into two +# version-pinned lines because the current release (2026.6.3, below) dropped Python +# 3.10 support -- pip resolves an older version on 3.10 instead, confirmed by +# resolving this exact dependency tree separately for each target Python version. +rpds-py==0.30.0; python_version < '3.11' \ + --hash=sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169 + +# same package, for Python 3.11+ (cp311/312/313/314 wheels) +rpds-py==2026.6.3; python_version >= '3.11' \ + --hash=sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5 \ + --hash=sha256:2c54a076ca4d370980ab57bc0e31df57bbe8d41340436a90ef8b1219a3cbb127 \ + --hash=sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f \ + --hash=sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80 + +# required by mcp (with the [crypto] extra) +pyjwt==2.13.0 \ + --hash=sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728 + +# required by pyjwt's [crypto] extra -- two hashes because this release ships two +# abi3 wheels with different minimum-ABI baselines (cp39-abi3 and cp311-abi3) +cryptography==50.0.0 \ + --hash=sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30 \ + --hash=sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba + +# required by cryptography -- per-Python-version wheels (cp310/311/312/313/314) +cffi==2.1.0 \ + --hash=sha256:1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb \ + --hash=sha256:4f26194e3d95e06501b942642855aed4f953d55e95d7d01b7c4483db3ecff458 \ + --hash=sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512 \ + --hash=sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384 \ + --hash=sha256:fb62edb5bb52cca65fab91a63afa7561607120d26090a7e8fda6fb9f064726da + +# required by cffi +pycparser==3.0 \ + --hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 + +# required by mcp +python-multipart==0.0.32 \ + --hash=sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23 + +# Server-Sent Events transport support -- required by mcp +sse-starlette==3.4.6 \ + --hash=sha256:56217ab4c9a9f9c5db7b21e08732d3e7c2b807f45231ad23de0551a24c4a41f6 + +# required by mcp and sse-starlette +starlette==1.3.1 \ + --hash=sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6 + +# ASGI server -- required by mcp (unused by this bridge's stdio transport, but still +# an unconditional mcp dependency) +uvicorn==0.52.1 \ + --hash=sha256:e4403f9d93188cf9d1088e9f40e3acd12630e2df8675316704379a7fc20fff6a + +# required by uvicorn +click==8.4.2 \ + --hash=sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76 diff --git a/tools/igor-mcp-bridge/server.py b/tools/igor-mcp-bridge/server.py index 523a6cedc5..99f93928f6 100644 --- a/tools/igor-mcp-bridge/server.py +++ b/tools/igor-mcp-bridge/server.py @@ -100,6 +100,7 @@ import ctypes import html.parser +import importlib.metadata import os import subprocess import sys @@ -621,13 +622,26 @@ def work(): # from inside a conversation which .mcpb build was actually loaded/active in Claude # Desktop, which made it impossible to verify whether a given fix (e.g. the reload/compile # timing relaxation) was actually in effect during a test -- see SESSION_NOTES.md. -_BRIDGE_VERSION = "1.24.0" +_BRIDGE_VERSION = "1.25.0" + + +def _installed_package_version(distribution_name: str) -> str | None: + """Return the installed version of a distribution (e.g. "mcp", "pywin32"), or None + if it isn't installed/resolvable. Best-effort only -- wrapped in + get_bridge_version() to help diagnose *which* Python environment this process is + actually running in, not to be relied on for anything else. + """ + try: + return importlib.metadata.version(distribution_name) + except importlib.metadata.PackageNotFoundError: + return None @mcp.tool() def get_bridge_version() -> dict: """Return the version of this Igor Pro Bridge build that is actually running right - now, in this Claude Desktop session. + now, in this Claude Desktop session, plus which Python interpreter and package + versions it's actually running with. Call this whenever it matters to confirm which build is active -- e.g. before relying on a specific fix or behavior change from a recent version, or when @@ -635,8 +649,27 @@ def get_bridge_version() -> dict: There is no other way to determine this from inside a conversation: installing a newer .mcpb requires restarting Claude Desktop, and nothing else surfaces which version ended up actually loaded afterward. + + The "python_executable" field is also the authoritative answer to a separate, + easy-to-get-wrong question: *which* Python environment Claude Desktop actually + launched this process with. Claude Desktop's manifest.json only specifies the bare + command "python", resolved via whatever PATH Claude Desktop's own (elevated) + process environment has at launch time -- which is not guaranteed to match the + Python an interactive elevated console session resolves (e.g. a PowerShell profile + activating a conda environment, or a per-user Microsoft Store "app execution + alias" stub that behaves differently once elevated). install.ps1 makes its own + best-effort guess at install time; after installing and restarting Claude Desktop, + call this tool to confirm "python_executable" actually matches what install.ps1 + installed into -- if it doesn't, re-run install.ps1 with an explicit -PythonPath + pointing at the path reported here. """ - return {"version": _BRIDGE_VERSION} + return { + "version": _BRIDGE_VERSION, + "python_executable": sys.executable, + "python_version": sys.version.split()[0], + "mcp_package_version": _installed_package_version("mcp"), + "pywin32_build": _installed_package_version("pywin32"), + } @mcp.tool() From e70fc5f22bff7d4e726ee0f627c70f540002235a Mon Sep 17 00:00:00 2001 From: Michael Huth Date: Wed, 22 Jul 2026 17:28:54 +0200 Subject: [PATCH 07/12] Add a session note file for use with AI client. An AI client, like claude desktop can build upon these session notes for better Igor Pro code generation. This SESSION_NOTES also includes MIES specific information. When using with another project instruct your client to extract all generic Igor Pro and workflow information. --- SESSION_NOTES.md | 2426 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 2426 insertions(+) create mode 100644 SESSION_NOTES.md diff --git a/SESSION_NOTES.md b/SESSION_NOTES.md new file mode 100644 index 0000000000..dadb2e5cf7 --- /dev/null +++ b/SESSION_NOTES.md @@ -0,0 +1,2426 @@ +## Purpose + +Facts, corrections, and findings from an extended Claude/Igor Pro working session on this +repository. Kept here so both the user and Claude can recall them accurately in later +sessions rather than re-deriving or re-arguing them from scratch. Entries are grouped by +topic, not chronology. + +## Standing instruction + +Verify findings carefully before reporting an error — read the documentation, trace the +actual code, or find corroborating evidence in the existing codebase, rather than asserting +Igor Pro semantics from memory. Several entries below exist because that wasn't done +carefully enough the first time. + +**The user usually says when they've switched git branches, but may occasionally forget. +If something inconsistent turns up after a new user message** (a function/file that should +exist doesn't, a line number or piece of code referenced earlier in the conversation no +longer matches what's on disk, an unexpected compile/test result, etc.) **— check `git +branch --show-current` (and/or `git log -1`) before assuming a code regression or a mistake +on Claude's part.** Concretely confirmed once already this session: mid-conversation +assumptions about a file's content/line numbers (carried forward from earlier turns) turned +out to be stale after the user switched branches without an explicit callout, since the same +physical working directory is shared between the bash tool and the Read/Edit/Grep file +tools — a branch change changes what both see identically and immediately. + +**Whenever a branch switch is confirmed or discovered (see above), check +`Packages/MIES/MIES_ClaudeScrapCode.ipf` and clean it up if needed.** This file is +intentionally never committed (untracked, `#include`d from `MIES_Include.ipf` purely for +interactive Claude Desktop sessions -- see the file's own header comment). Scratch helper +functions written for one branch's task frequently call `static`/module-qualified functions +or reference window/structure names from the main MIES codebase that may not exist, or may +behave differently, on the next branch checked out -- so old scratch code can silently fail +to compile, or "succeed" while doing something meaningless, after a switch. No fixed +required approach: remove the stale functions entirely, or adapt them to the new branch's +actual code, whichever fits -- judge case by case; some scratch helpers may simply no longer +have a meaningful purpose once the branch changes and are better deleted than patched. + +**`read_session_history()` should always be saved straight to a file, not read inline.** +The user has seen Igor's history area grow past 10,000 lines in normal use, especially +during test runs with heavy log output — inline reads reliably exceed the context window +past a certain point in any long session. Default to writing the result to a file first +(e.g. via the bash tool) and `grep`/search it for the specific markers needed (`error:`, +`Finished with no errors`, a specific test-case name, etc.), the same technique already +worked out and written up further below, rather than waiting to hit the token-limit error +first. + +**Reach for `ipt`/`ipt.exe` (the Igor Programming Tool -- `tools/ipt` (Linux), +`tools/ipt.exe` (Windows), `tools/run-ipt.sh` picks the right one) proactively in any Igor +Pro related workflow where it can plausibly sharpen understanding, not only when explicitly +asked for a parse/AST** — use it as a standing habit alongside direct reading of the source, +not as a replacement for it. Concretely, per subcommand (see the "`tools/ipt`" section below +for the full write-up of each, including confirmed gaps/bugs): +- `ipt check [--print-ast] ` — confirm a file/edit actually parses, or get an + authoritative AST (node types, precise line:column spans) for understanding a function's + real structure without needing a live Igor Pro instance. +- `ipt lint ` — catch known real bug/style patterns (e.g. + `BugproneReservedKeywordsAsIdentifier`) before or instead of relying on manual review for + those specific cases; know its confirmed gap (no built-in-*function*-name-shadowing + detection, since it has no symbol-resolution semantics for that). +- `ipt rename --print-symbol-table ` (plus a full `-f`/`-l`/`-c`/`-n` target to avoid + the no-target crash bug noted below) — get a genuine cross-referenced symbol table + (declarations, every read/write/definition point with exact spans, function signatures) + for a file when tracing how a variable/function is actually used matters more than just + seeing the parse tree; also usable for the rename itself (preview without `-y`, apply + with it). +- `ipt analyze` — available but not yet evaluated this deeply in this repo; worth trying + when a task's shape matches its evident purpose (broader rule-based analysis) rather than + assuming it's irrelevant. +- `ipt format` — confirmed in real use this session: the user ran it directly against + `MIES_ClaudeHelper.ipf` right after moving a `static Constant` block to a new location by + hand. The resulting file was consistently formatted throughout (aligned `=` signs across + runs of consecutive assignment/declaration lines, a blank line separating a function's + local-variable declarations from its first statement), not just around the lines that had + just been edited by hand -- worth reaching for after any manual restructuring, not only + after small line-level edits. +- **`ipt.exe` only ever knows about the file(s) explicitly passed via `files`/`-f`** — it + does not resolve `#include`s itself, so pass every file actually relevant to the question + at hand rather than assuming cross-file context comes for free. **In practice this is a + non-issue when a live Igor Pro instance is available through the bridge, and should not + be treated as a reason to skip `ipt` or reach for it half-heartedly**: `get_environment_ + summary()`'s `included_procedure_files` field already reports the complete, authoritative + list of every procedure file actually included in the running instance right now (derived + from `WinList(..., "WIN:128")`) -- exactly the context `ipt` needs, obtained without + guessing or grepping for `#include` lines by hand. The one extra step: that field returns + bare file names (e.g. `MIES_ForeignFunctionInterface.ipf`), not filesystem paths, so + resolve each name to its actual on-disk path (a repo-wide filename search/glob; names are + effectively unique across this codebase) before passing the list to `ipt` as its `files` + argument. Combine the two rather than treating them as unrelated tools. + +**When Igor's own `.ihf` help files are relevant to a question (e.g. confirming an +operation's exact flags/behavior), prefer reading them as Igor notebooks through the bridge +over an OS-level file read** -- snapshot currently-open help windows (`WinList("*", ";", +"WIN:512")`), `CloseHelp/ALL`, `OpenNotebook/R ""`, `SaveNotebook/O/S=5/H=...` to +export as HTML (surfaces genuine content-block structure via named paragraph classes like +`Topic`/`Code1`/`Steps`, not just flat text), `KillWindow/Z` the temporary notebook, then +restore each snapshotted file via `OpenHelp/V=.../INT=0` -- see the dedicated section below +for the full non-destructive, repeatable workflow. + +**Commands sent to Igor Pro through the bridge's COM interface (`execute_igor_command(_unattended)`) +run the same way as Igor's own command line: as *interpreted* code, not compiled procedure +code.** Several Igor language features only work inside compiled functions and are rejected +(often with an unhelpful generic error) when attempted directly this way -- confirmed this +session: `Make/FREE ...` (free waves have no valid scope outside a function), `WAVE ref = +SomeFunc()` (assigning a wave reference from a function call), and calling a `static` +function by its bare name (it's scoped to its file's `#pragma ModuleName` and needs +`ModuleName#FunctionName` from outside that module). **Workaround: create/extend a small +compiled scratch procedure file (e.g. `Packages/MIES/MIES_ClaudeScrapCode.ipf`), `#include` +it from `Packages/MIES_Include.ipf`, `reload_and_compile_procedures`, then call a single +compiled helper function from that file via the command line/Execute2** -- everything inside +the function body runs as compiled code, so none of the above restrictions apply. (A +differently-named prior-art scratch file, `MIES_ClaudeHelper.ipf`, was used the same way on +an earlier branch for a compile-confirmation hook -- same pattern, different specific +filename/purpose; there is no single fixed required name, just the `#include`-a-scratch-file +approach.) See the bridge section below for the concrete Analysis Browser example. + +**Whenever writing or editing code in ANY Igor Pro procedure file (`.ipf`) in this repo -- +not just Claude-authored scratch/bridge-helper files like `MIES_ClaudeHelper.ipf`, but +existing production MIES code too -- apply these three style conventions as standard +practice, not just when a specific function happens to prompt them (confirmed as the user's +explicit, general preference, applying to any `.ipf` file; first applied to +`CH_ListXOPExports`/the `CH_PE*` helpers in `MIES_ClaudeHelper.ipf`, see that section below +for the worked example):** +1. Use lowercase type keywords (`variable`, `string`, not `Variable`/`String`), and declare + all of a function's local variables at the very top of its body, before the first + statement -- matching Igor's own function-level (not block-level) scoping. +2. Use Igor 7+ inline parameter-type declarations in the function signature itself + (`Function/S Foo(string bar, variable baz)`), not the old two-part style + (`Function/S Foo(bar, baz)` followed by separate `string bar` / `variable baz` lines). +3. Replace unexplained numeric *and* string literals with named `static Constant`s / + `static StrConstant`s declared at the top of the file, rather than leaving "magic + numbers" (or magic strings) inline -- one constant per distinct meaning, with a trailing + comment explaining what it represents when that isn't obvious from the name alone. This + covers both kinds of literal equally; don't treat strings as exempt just because their + meaning often looks self-evident in context. + +**Always run `ipt format` (see the `tools/ipt` section below) immediately after editing any +`.ipf` file in this repo, every time, not as an optional habit** -- the user relies on its +canonical formatting to make the diff easier to review on their end. Re-verify behavior is +unchanged afterward (recompile, re-run whatever live test previously established +correctness) before considering the edit done. + +## Igor Pro language facts (confirmed this session) + +- **Reference-typed locals have function-level scope, not block scope.** `WAVE`, `NVAR`, + `SVAR`, `DFREF`, `FUNCREF` locals are recognized by the compiler across the whole + function body (e.g. a `WAVE test1 = data` inside an `if` block is still a valid, + in-scope local after the `endif`), and default to a null/non-existent reference at + function entry if the assigning line is never reached. This is *not* an error condition — + referencing such a variable later without `/Z` only fails if the `WAVE` statement itself + executes and its right-hand side fails to resolve, not merely because the statement was + skipped by control flow. +- **A bare `String` defaults to a null string, not `""`.** These are distinct states, + distinguishable via `strlen()`: `NaN` for a null string, `0` for `""`. +- **`Make` without `/N` defaults to a 1D wave with 128 points**, not 0. If an initializer + list is given instead (`Make wv = {1, 2, 3}`), the wave is sized from the list, not + defaulted to 128. Curly-brace initializer lists always require at least one operand — + `Make wv = {}` is not valid syntax. +- **`Concatenate` (e.g. with `/NP=dim`) always leaves the destination wave existing**, even + if the source has zero rows, even repeated across every iteration of a loop — the + destination ends up as a valid 0-row wave, never an unbound/null reference. This does + *not* apply if the outer wave-reference-wave being iterated (e.g. `sources` in + `for(WAVE/T src : sources) ... endfor`) itself has zero rows: then the loop body never + runs, `Concatenate` is never called, and the destination stays a null/non-existent + reference per the default-initialization rule above. +- **Auto-indexing in a waveform assignment (e.g. `Make/WAVE/N=(n) w = SomeFunc(p)`) runs + strictly in increasing index order when `Multithread` is *not* used.** With `Multithread`, + execution order for a given index is not guaranteed, and the right-hand side must be + threadsafe. This matters when the called function has order-dependent side effects. +- **The Igor compiler disallows a `WAVE name = expr` *declaration* statement where `name` + also appears inside `expr`** (e.g. as a function argument), even if `name` was already + declared earlier in the function. Workaround: introduce a second reference to the same + wave under a different name (`WAVE/Z tmp = name; WAVE name = Func(tmp)`). This is + different from a destructuring *reassignment* like `[out, outT] = Func(out, outT)`, which + is legal because it updates already-declared references rather than re-declaring them. +- **`FindValue /TXOP` bit flags**: `4` = case-insensitive whole-cell text match (the + pervasive default throughout this codebase), `5` = `4 | 1` = case-sensitive. Confirmed via + existing code: `GetDecimalMultiplierValue` (`IPNWB_Utils.ipf`) uses `TXOP=(1+4)` for SI + unit-prefix matching specifically because case matters there (`m` vs `M`). +- **`ListToTextWave` never returns a null wave.** An empty `listStr` input produces a + 0-row text wave, not a 1-row wave containing a single empty string. +- **`Make/N=(...)`: an explicit dimension size of `0` means "this dimension does not + exist"** (same convention documented for `Redimension`), while an explicit `1` creates a + real, if trivial, additional dimension. `Make/N=(n, 1, 1, 1)` is *not* equivalent to a + true 1D wave — `DimSize(wv, COLS)` is `1`, not `0`. This codebase has an established + convention that wave-of-waves values must be strictly 1D (e.g. `GetSetIntersectionWaves` + asserts `DimSize(wv, COLS) == 0`), so creators of such waves must pass `0`-equivalent + (or simply omit trailing dimensions) rather than `1`. +- **`Variable/G name = value` (with an explicit initializer) overwrites the global's value + every time that line executes, even if the global already existed** — confirmed from Igor + Reference.ihf: "/G Creates a variable with global scope and overwrites any existing + variable," and "The variable is initialized when it is created if you supply the initial + value." Bare `Variable/G name` (no initializer) is the safe, standard idiom to call + unconditionally on every invocation instead: it creates the global at `0` only if missing, + and leaves an existing value untouched otherwise — no `NVAR_Exists`-style guard needed. + Caught in `MIES_ClaudeHelper.ipf`'s `AfterCompiledHook()`, which originally used a guarded + `Variable/G root:gClaudeHelperCompileCounter = 0` and was simplified to the bare form. +- **`#define` symbols meant to control cross-file conditional compilation (`#ifdef`/ + `#ifndef`) must be set in the experiment's special "Procedure" window, not in a regular + included `.ipf` file.** Confirmed from Programming.ihf: "Although it is difficult to + determine the order in which procedure files are compiled, the main procedure window is + always first." Since the Procedure window compiles before every other included file, a + `#define` placed there (e.g. this experiment's existing `#define AUTOMATED_TESTING`) is + reliably visible to every file's `#ifdef` checks; a `#define` in an ordinary `.ipf` file + has no such guarantee and should not be relied on for this purpose. +- **The Igor compiler does not stop a local variable/string/`WAVE` reference from + being named the same as a built-in Igor function or reserved keyword** (e.g. + `string log` shadows the built-in `log()` function; the same applies to names + like `return` or other keywords/function names). This compiles without error but + is a real footgun: within that variable's scope, every reference to the name + resolves to the local variable instead of the built-in function, silently + breaking any code in that scope that expected to call the actual function/keyword + behavior. **Rule: never name a variable, string, or `WAVE` reference after an + Igor built-in function or reserved keyword**, even though the compiler allows it. +- **`NewPath`/`PathInfo`'s `S_path` always returns Igor's colon-separated native path + notation, even on Windows** -- confirmed live: normalizing `"C:\Projects\mies_data\ + ivscc_apfrequency"` via `NewPath` + `PathInfo $symbPath; S_path` produced + `"C:Projects:mies_data:ivscc_apfrequency:"` (colon-delimited, trailing colon), not a + backslash Windows path. This is the same normalized form MIES's own Analysis Browser + stores internally (e.g. in its folder-list wave), so anything comparing against or + displaying that value should expect colon notation regardless of host OS, not assume + Windows paths stay backslash-separated after a round-trip through a symbolic path. +- **Variables declared on the Igor command line via `execute_igor_command(_unattended)` + (e.g. `String win`) persist as global command-line variables across separate Execute2 + calls within the same Igor session** -- they are not scoped to a single bridge tool call. + Re-declaring the same name in a later call fails with `"the name already exists as a + variable"`; just assign to it directly (skip the `String`/`Variable` declaration) in + follow-up calls, or expect this persistence when debugging multi-call command-line + sequences. +- **Igor's command line does not support multi-line control-flow blocks (`if`/`else`/ + `endif`, `for`/`endfor`) the way compiled functions do.** A command sent via Execute2 + containing such a block fails as a whole (every line reported `NOT EXECUTED`, with a + generic `expected wave name, variable name, or operation` error), even though each + individual line would be valid inside a real function. This is a second, independent + reason (beyond free waves/`WAVE ref = func()`/`static` scoping) to move any nontrivial + logic into a compiled scratch-file helper function rather than a raw multi-statement + Execute2 command -- see the standing-instruction note above. + +## MIES wave-versioning convention + +Located in `MIES_WaveDataFolderGetters.ipf`: `WAVE_NOTE_LAYOUT_KEY = "WAVE_LAYOUT_VERSION"`, +with helpers `GetWaveVersion`, `SetWaveVersion`, `WaveVersionIsAtLeast`, `WaveVersionIsSmaller`, +`IsWaveVersioned`, `ExistsWithCorrectLayoutVersion`. `WaveVersionIsSmaller(wv, N)` returns +true if the wave is unversioned (`NaN`) or its version is `< N`. Correct migration idiom is a +sequence of independent `if(WaveVersionIsSmaller(wv, N))` blocks (N increasing), each +performing exactly the upgrade needed for that step — *not* an exclusive `if/elseif` chain, +which can skip needed migration steps for very old wave versions. + +**Open bug, not yet fixed as of last check**: `GetAnalysisBrowserMap()` in +`MIES_WaveDataFolderGetters.ipf` (branch `feature/2737-prepare2_ivscc_apfrequency`) writes to +column index 3 (`wv[][3] = ANALYSISBROWSER_FILE_TYPE_IGOR`) inside its +`WaveVersionIsSmaller(wv, 1)` block *before* the wave is ever redimensioned beyond its +original 3 columns (widening to 5 columns only happens later, in the +`WaveVersionIsSmaller(wv, 4)` block). For a genuinely unversioned pre-2016 `experimentMap` +wave (confirmed via git history to have exactly 3 columns: +`ExperimentDiscLocation`/`ExperimentName`/`ExperimentFolder`), this throws an +index-out-of-range runtime error instead of migrating, because Igor bounds-checks wave +assignments. `GetSweepBrowserMap()` and `GetExperimentBrowserGUIList()` in the same diff +both redimension correctly before/with their writes — `GetAnalysisBrowserMap()` is the +outlier and needs the same treatment (redimension to at least 4 columns before writing +column 3). + +**Fixed correctly in later commits on that branch** (for reference, not action items): +`GetSweepBrowserMap()` now uses the `WaveVersionIsSmaller`-gated pattern with +`SetWaveVersion`; the `SweepFormula.rst` doc wording around `seltag("")` matching all +sweeps in DataBrowser context was corrected to be precisely scoped and now matches the code; +`seltag`'s `SFH_CheckArgumentCount` minArgs was fixed from 0 to 1. + +## SweepFormula dataset/datatype architecture + +- Every SweepFormula operation result is a "dataset": a single-element `WAVE/WAVE` + container, typically created via `SFH_CreateSFRefWave`, with an `SF_META_DATATYPE` JSON + wave note (`JWN_SetStringInWaveNote`/`JWN_GetStringFromWaveNote`) identifying its kind + (`SF_DATATYPE_SELECTCOMP`, `SF_DATATYPE_SELECTTAG`, etc.). +- `SFH_GetOutputForExecutorSingle(data, ..., dataType=X)` wraps whatever `data` it's given + in a *new* single-element `WAVE/WAVE`, setting the note on that new wrapper — it does not + tag `data` itself. Operations that call this directly on their own final payload (most + `select*` filter operations) get one level of wrapping, note on the outside. +- `select()` itself is the counter-example: `SFOS_OperationSelect` builds its own composite + (`GetSFSelectDataComp`), sets `SF_META_DATATYPE = SF_DATATYPE_SELECTCOMP` directly on it, + and returns it via `SFH_GetOutputForExecutor(output, ...)` directly — skipping + `SFH_GetOutputForExecutorSingle` entirely, so there's no extra wrapper for the note to get + lost behind. +- `seltag` needs *two* levels of wrapping around its `tags` text wave specifically to stop + the array-literal executor from treating a multi-tag `seltag([a, b])` result as a plain + text wave and array-expanding its elements (see below). The `SF_META_DATATYPE` note must + be set on the *inner* wrapper (the one that becomes `genericElement[0]` when the call + appears inside an array literal), not only on the outer one — otherwise the note is lost + the moment `seltag(...)` appears inside `[...]`. + +## SweepFormula `and`/`with` keywords are a plotter-targeting concern, not an executor one + +Clarified by the user: a SweepFormula expression itself may not contain line +breaks, so `and`/`with` (which must each stand alone on their own line) can +never appear *inside* an expression parsed/run by `SFE_ExecuteFormula`/ +`SFE_ExecuteVariableAssignments` -- that's why those two functions' doc +comments say they don't support `and`/`with`, and why the JSON-based executor +(`SFE_FormulaExecutor`) never has to know about them at all. `and`/`with` are +recognized in an earlier, separate post-processing step that splits the SF +notebook text into individual single-expression formulas, and they solely +control *where the plotter puts each expression's result* -- `with` targets +the same plot sub-window as the previous expression, `and` targets a new one. +The executor always just returns the result of one already-isolated +expression; the plotter is what reads `and`/`with` to decide placement. So +there is no way to feed `and`/`with` through `SFE_ExecuteFormula` even +indirectly (e.g. via a nested/generated formula string) -- it would need to +go through the notebook-level splitting step first, which these two +executor-only entry points never invoke. + +## SweepFormula executor position trackers are not restored on nested-call return + +Working out `TestAssertDataStack3OP` (see the assert-data-stack test +consolidation above) raised whether two nested (`newFrame = 1`) calls made +sequentially from the same outer/dispatched operation instance could ever +freeze the outer frame's `LOCMSG` with two different, correct texts (one per +nested call). Traced through the code: `SFE_FormulaExecutor` unconditionally +overwrites the global `GetSweepFormulaJSONPathTracker` (and similarly the +`SRCLOCID`/`STEP` fields via `SFH_StoreAssertInfoExecutor`) on every call, and +nothing restores the outer frame's own former tracker value after a nested +call returns -- the tracker is simply left holding whatever the nested call +last set. Consequence: a single dispatched operation making two sequential +nested calls has no way to get the outer frame's *own* position re-frozen +correctly a second time (it would still reflect the first nested call's +position) -- the only way the outer frame's position genuinely changes +between two freezes is if something *external* to the operation (e.g. the +`SFE_ExecuteVariableAssignments` assignment loop moving to the next +assignment) re-stamps it via `SFH_StoreAssertInfoParser`/`Executor` in +between. The user confirmed there's currently no use case needing two +different frozen texts from the same outer frame (the final SFH_ASSERT +message is a one-shot terminal event), but flagged this as something to +revisit if a non-terminal message type (e.g. warnings that must be kept +correct across multiple points) is ever introduced. + +## Igor Pro Universal Testing Framework: `IUTF_TD_GENERATOR`/`UTF_TD_GENERATOR` tag scanning + +The advanced.rst docs are ambiguous/contradictory about how far above a +multi-data test case's `Function` line the tag comment can be (one place says +"within four lines", another says "all lines above `Function` up to the +previous `Function`"). Checked the actual implementation, +`GetFunctionTagWave` in `Packages/igortest/procedures/igortest-functiontags.ipf`: +it uses `ProcedureText(funcName, -1, ...)` minus `ProcedureText(funcName, 0, +...)` to isolate every comment line between the previous function's `End` and +this function's `Function` line, then scans *all* of those lines (looping +backwards), trying every known tag pattern against each non-empty line and +silently skipping (no error) any line that doesn't match one. There is no +four-line cutoff in the code -- the "all lines up to the previous Function" +description is the accurate one. This means ordinary `///` doc-comment lines +can be freely mixed in above a `// IUTF_TD_GENERATOR ...` tag line (they just +won't match any tag pattern and are skipped), so there was no need for the +earlier caution of keeping the tag as the only comment line directly above +`Function`. + +## SweepFormula executor: array-literal handling of dataset elements + +This session added support, in `SFE_FormulaExecutor`'s `JSON_ARRAY` branch +(`MIES_SweepFormula_Executor.ipf`), for array literals whose elements are datasets (e.g. +`[seltag(a), seltag(b)]`), where previously any non-text/non-numeric array element was +encoded as a stringified `wRefPath` marker (via `SFH_GetOutputForExecutor`) and placed into +a plain text accumulator — which silently discarded each element's own `SF_META_DATATYPE` +note, since the note lived on a wrapper level that got peeled away and never re-attached to +the marker. + +Fix, in outline: + +1. Introduce a genuine `WAVE/WAVE` accumulator (`outW`), alongside the existing numeric + (`out`) and textual (`outT`) ones, used specifically for dataset array elements. Each + element is stored as a direct wave reference (`outW[index] = subArray`) — never a + stringified marker — so it keeps its own note natively; no marker-resolution helper is + needed by consumers. +2. New helper `SFE_ExecutorCreateOrCheckWaveRef(WAVE/Z/WAVE outW, variable size0)` — + deliberately takes only one size parameter, since `outW` should always stay strictly 1D + (datasets are never spread across the outer array's other dimensions; see the `Make/N` + dimension-size fact above for why `0`/omitted, not `1`, matters here). +3. `SFE_PlaceSubArrayAt` gained a `WAVE/WAVE` branch that assigns `outW[index] = subArray` + directly — no `Multithread`, no elementwise copy, since a dataset occupies exactly one + opaque slot regardless of its own internal shape. +4. The dimension-widening logic (`effectiveArrayDimCount` bump, `topArraySize[1,*] = + max(...)`) must be guarded with `if(!WaveExists(outW))` — a dataset's own internal + dimensionality must never influence the outer array's shape. This was an actual bug + caught by testing: `[dataset(1,"abcd"), dataset(2,"cdef")]` produced a `(2,2)`-shaped + `outW` instead of a flat 2-element one, because `dataset(...)`'s own multi-row payload + leaked into `topArraySize` before this guard was added. +5. To allow *mixed* arrays like `["text", dataset(2, "cdef")]` (previously a hard + `"mixed array types"` assertion failure): the loop was restructured into a prescan that + resolves every element exactly once via `SF_ResolveDatasetFromJSON` (stored once, reused + by both possible downstream branches — resolving twice was flagged as potentially + unsafe, since resolution can execute arbitrary operations with side effects), determines + whether *any* element is dataset-kind, and only then decides the accumulation strategy: + if any dataset is present, the whole array is promoted to a uniform wave-of-datasets, + with plain text/numeric elements individually wrapped into their own single-element + `"PromotedArrayElement"` dataset (no `SF_META_DATATYPE` note attached to that wrapper). + Otherwise, it falls through to the original `out`/`outT` accumulation logic, still reusing + the already-resolved elements rather than re-resolving from JSON. +6. `SFH_GetArgumentSelect` (`MIES_SweepFormula_Helpers.ipf`) needs a matching update: check + `IsWaveRefWave(array)` instead of `IsTextWave(array)`, and use + `Duplicate/FREE/WAVE array, selectArray` directly instead of resolving each element via + `SFH_AttemptDatasetResolve(WaveText(array, row = p), ...)` — array elements are now real + wave references, not stringified markers, so there's nothing left to string-parse. + +**Follow-up cleanup, not yet done** (tracked as session TODO items, not written to disk): +the fallback (`containsDataset == 0`) loop still carries the full original per-element +dispatch logic, including now-unreachable "mixed array types" asserts and the dataset/`else` +branch — harmless (dead code, since `containsDataset` is guaranteed false there) but worth +trimming down to just the text/numeric paths, reusing `IsTextWave(preResolved[i])` / +`IsNumericWave(preResolved[i])` directly instead of re-deriving `subArray` and re-running +`SFE_ConvertNonFiniteElements` a second time. + +## SweepFormula operation pattern: backup/restore the variable storage to run +## nested formula code in a scratch environment + +`ivscc_apfrequency()` (`SFO_OperationIVSCCApFrequency`/`Impl2` in +`MIES_SweepFormula_Operations.ipf`) implements itself partly by composing +*other* SweepFormula operations (`select`, `merge`, `prepareFit`, `fit2`, ...) +rather than reimplementing their logic directly, using a +backup/mutate/restore pattern on the per-graph SweepFormula variable storage +(`GetSFVarStorage(exd.graph)`, a `WAVE/WAVE` keyed by variable name, populated +by `$varName`-style references between formula lines): + +1. `SFO_OperationIVSCCApFrequencyPrepareVariables` takes `WAVE/WAVE varStorage + = GetSFVarStorage(exd.graph)` and makes a `Duplicate/FREE` copy, + `varBackup`, preserving the caller's existing variables untouched. +2. It then builds an ordinary SweepFormula source string on the fly (e.g. + `"sel = select(selsweeps(), selstimset(...), selvis(all), + selivsccsweepqc(passed))\r"` plus per-experiment/avg-plot expressions, + assembled via `SF_AddExpressionToFormula`) and runs it for real through + `SFE_ExecuteVariableAssignments(exd.graph, formula, allowEmptyCode = 1)` -- + i.e. it re-enters the actual formula executor with dynamically generated + code, exactly as if the user had typed those lines into the SweepFormula + notebook themselves. This mutates the *live* `varStorage` in place with all + of that scratch computation's intermediate variables (`sel`, + `ivsccavg_merged`, per-experiment `freqNorm`/`currentNormMerged`, + etc.). +3. `SFO_OperationIVSCCApFrequencyImpl2` reads whatever it needs back out of + that now-mutated `varStorage` (by name, e.g. `varStorage[%ivsccavg_norm_y]`) + to build the actual plot trace data. +4. Before returning, it restores the original state with `Duplicate/O + varBackup, varStorage` -- wiping out all of its own scratch/intermediate + variables -- and only *afterward* re-adds the specific outputs it actually + wants to persist for the user (`SFH_AddVariableToStorage(exd.graph, + "ivscc_apfrequency_explist_" + tagSuffix, ...)`, + `"ivscc_apfrequency_fit_" + tagSuffix`, one set per tag group). + +Net effect: the operation gets to reuse the real formula executor and other +real operations as implementation building blocks, using the shared variable +storage as a scratch workspace, without leaking any of its own internal +temporary variable names into the user's persistent SweepFormula environment +once it's done -- only the deliberately-named, explicitly re-added outputs +survive. Confirmed directly from source (`MIES_SweepFormula_Operations.ipf` +lines ~3339-3365 and ~3390-3509), per the user's own explanation of the +approach. + +**Two identified architectural gaps in generalizing this pattern (user's own +analysis, verified against source):** + +1. ~~**No exception safety.**~~ **Reassessed by the user: this is a non-issue, + not a gap.** Originally flagged: neither `SFO_OperationIVSCCApFrequencyPrepareVariables` + nor anything above it in the call chain (`Impl2`, the operation dispatch, + all the way up) wraps the nested `SFE_ExecuteVariableAssignments` call in a + `try`/`catch` -- the *only* `try`/`catch` in the whole path is the one in + `SF_button_sweepFormula_display` itself -- so if the scratch formula aborts + via `SFH_ASSERT`, that level's own `Duplicate/O varBackup, varStorage` + restore step never runs, leaving `varStorage` in its mutated/scratch state. + **The user's correction**: this is fully OK given how SweepFormula's + execution model actually works. A failed evaluation simply means there is + no valid result -- SweepFormula has no concept of updating an + already-displayed plot in place, so there is no code path that could ever + observe or render the stale `varStorage` contents between the abort and + the next run. And as already noted above, `SFE_ExecuteVariableAssignments` + unconditionally wipes `varStorage` back to 0 rows at the start of its own + next invocation regardless, so the leftover state doesn't linger or + accumulate either. No fix needed here; not pursuing this further. + +2. **No per-call-level source-location tracking, so nested-operation errors + get misattributed to the wrong place in the notebook.** Traced precisely: + `GetSFAssertData()` (`MIES_WaveDataFolderGetters.ipf`) is a single flat, + *non-stacked* per-graph text wave (`SFAssertData`, 8 fields: `JSONID`, + `SRCLOCID`, `JSONPATH`, `STEP`, `LINE`, `OFFSET`, `FORMULA`, + `INFORMULAOFFSET`), written in place by `SFH_StoreAssertInfoParser`/ + `SFH_StoreAssertInfoExecutor` (`MIES_SweepFormula_Helpers.ipf`) every time + *any* formula gets parsed/executed -- including a nested + `SFE_ExecuteVariableAssignments` call like the one inside + `SFO_OperationIVSCCApFrequencyPrepareVariables`. When that inner call + parses its own dynamically-built scratch formula (e.g. `"sel = + select(...)"`), it overwrites the *same* global `LINE`/`OFFSET`/`FORMULA` + fields with values relative to *that* internal string, clobbering + whatever the outer (real, user-visible) formula's own values were. + Crucially, `SF_CalculateErrorLocationInNotebook` (`MIES_SweepFormula.ipf`) + *always* re-reads the real, on-screen SF notebook's text + (`GetNotebookText(BSP_GetSFFormula(win), mode = 2)`) and blindly indexes + into it using whatever `info[%LINE]`/`info[%OFFSET]` currently hold -- + with no way to know those numbers actually describe a position inside an + entirely different, invisible string (`ivscc_apfrequency`'s own generated + formula) rather than the real notebook. The result: an assert inside a + nested operation call resolves to some essentially coincidental position + in the *outer*, user-visible formula (in this repo's typical case, the + `ivscc_apfrequency()` call site itself, since the inner formula's line 0 + gets misread as notebook line 0) -- not the actual failing sub-expression, + which was never textually present in the notebook at all. + + **User's proposed fix**: replace the single-frame `SFAssertData` wave with + a wave-reference wave used as a LIFO stack -- each nested formula-execution + episode pushes its own independent frame (mirroring today's 8 fields) on + entry, and error-message construction needs extending to walk *every* + frame on the stack (not just the current/top one) so a nested failure's + message can show the full call chain, e.g. innermost failing sub-formula + plus which outer operation (`ivscc_apfrequency()`) invoked it and at what + notebook location. **Noted interaction with gap 1**: the aggregated + error message must be built by walking the stack *at the moment + `SFH_ASSERT` fires*, before any unwinding -- relying on a normal + push-on-entry/pop-on-return discipline alone would never populate a + correct message on the failure path itself (that's precisely the path + where "pop" never executes, per gap 1), and conversely, if the stack isn't + explicitly reset somewhere (e.g. alongside `SF_ClearSFOutputState()`), + stale frames from a previous aborted run would corrupt the *next* run's + tracking. Also worth covering when implementing: `SRCLOCID` is a JSON id + requiring `JSON_Release` (currently released once, in + `SFH_GetAssertLocationMessage`/`SF_MarkErrorLocationInNotebook`'s cleanup) + -- with multiple stacked frames, every frame's JSON id needs releasing + during error handling/reset, not just the top one. + + Not yet implemented -- discussed/designed only so far in this session; the + user has not yet asked for code changes. + +### Gap 2 fix implemented: LIFO assert-data stack + +Implemented per the design above, with one refinement discovered while +implementing: the *global* execution-position trackers +(`GetSweepFormulaJSONPathTracker()`/`GetSweepFormulaBufferOffsetTracker()`, +`MIES_GlobalStringAndVariableAccess.ipf`) only ever reflect whatever is +executing *right now* -- they're updated on every recursion level/token, +unconditionally, not scoped per formula-execution episode. So a naive +"walk the stack and read the live trackers for each frame" would give every +frame the *innermost* (currently-failing) position, not its own. Fix: freeze +each frame's rendered location message into a new `LOCMSG` field on that frame +at the moment a *deeper* frame gets pushed on top of it (i.e. while the live +trackers still reflect its position) -- see `SFH_PushAssertDataFrame`. + +Files changed: +- `MIES_WaveDataFolderGetters.ipf`: `GetSFAssertDataStack()` (new, `WAVE/WAVE` + LIFO, lazily created), `GetSFAssertData()` rewritten to return the + top-of-stack frame (auto-pushing a base frame if the stack is empty). + `SF_ASSERTDATA_NUMFIELDS` bumped 8 -> 9 for the new `LOCMSG` field. +- `MIES_SweepFormula_Helpers.ipf`: `SFH_PushAssertDataFrame()` (freezes the + outer frame's `LOCMSG` first, then pushes a blank frame), + `SFH_PopAssertDataFrame()` (asserts against popping the base frame; + deliberately does *not* release JSON ids -- a normal return already released + them via the ordinary executor success path, so releasing again here would + double-release), `SFH_GetOutermostAssertDataFrame()` (stack[0], for + notebook-position lookups), `SFH_ResetAssertDataStack()` (releases every + remaining frame's `JSONID`/`SRCLOCID` via `JSON_Release(..., ignoreErr=1)`, + then empties the stack). `SFH_GetAssertLocationMessage` refactored: the old + per-frame logic moved unchanged into `SFH_GetAssertLocationMessageForFrame` + (returns the frozen `LOCMSG` if present, otherwise computes fresh from the + live trackers -- exactly right for whichever frame is currently on top), and + the public function now walks the stack top-to-bottom, joining more than one + non-empty frame message with `"\rCalled from:"`. +- `MIES_SweepFormula.ipf`: `SF_CalculateErrorLocationInNotebook` now reads + `SFH_GetOutermostAssertDataFrame()` instead of `GetSFAssertData()` (only the + outermost frame's `LINE`/`OFFSET` are ever real notebook positions); + `SF_ClearSFOutputState()` now also calls `SFH_ResetAssertDataStack()`. + `SF_MarkErrorLocationInNotebook`/`SF_IsExecutionErrorInVariable` needed **no + change** -- both correctly keep top-of-stack semantics via the unchanged + `GetSFAssertData()`. +- `MIES_SweepFormula_Operations.ipf`: the nested + `SFE_ExecuteVariableAssignments` call inside + `SFO_OperationIVSCCApFrequencyPrepareVariables` is now wrapped with + `SFH_PushAssertDataFrame()`/`SFH_PopAssertDataFrame()`. No `try`/`catch` -- + on an abort, the pop is simply skipped, deliberately leaving that frame's + data on the stack for the aggregate error message (gap 1, the exception + -safety issue, is still open/unaddressed; this only fixes gap 2). + +**Verified compiling and working live** (Igor Pro 9.06 Nightly, via the +bridge): added a temporary `ClaudeScrap_TestAssertStack()` smoke test to +`MIES_ClaudeScrapCode.ipf` simulating the exact scenario -- outer formula +reaches an operation call site (line 5, offset 2) -> operation pushes a frame +-> nested formula hits its own parser error and `SFH_ASSERT` fires (caught +here instead of propagating). Confirmed: the outer frame's `LOCMSG` freezes on +push; the outer frame's own `LINE`/`OFFSET` (5/2) are untouched by the nested +frame's write (0/3) -- the actual bug being fixed; the pushed frame is left on +the stack after the simulated abort (pop correctly skipped); the final +aggregated message is `"Nested op failed\r 1 +\rCalled from:\r +ivscc_apfrequency()"` -- both levels present, joined as designed; and +`SFH_ResetAssertDataStack()` empties the stack back to 0. This test function +is left in `MIES_ClaudeScrapCode.ipf` (harmless, no window/GUI interaction) in +case it's useful again. + +**Pitfall hit and fixed while getting a clean compile, unrelated to this +task**: `MIES_ClaudeScrapCode.ipf` (the scratch file from earlier in this +session) wouldn't compile for two separate, unrelated reasons, both now fixed: +1. `Make/FREE/T/N=1 wFolder = {folder}` -- turned out to be a red herring; the + idiom itself is valid (confirmed against `DAP_GetRadioButtonCoupling` in + `MIES_DAEphys.ipf`), the real error was #2 below, just cascading to a + nearby line. Split into `Make/FREE/T/N=1 wFolder` + `wFolder[0] = folder` + anyway (harmless simplification). +2. **The actual cause, identified by the user**: every `MIES_AB#...`/ + `MIES_SF#...` module-qualified call in the scratch file relies on + `#pragma ModuleName = MIES_AB`/`MIES_SF` etc., which are themselves gated + behind `#ifdef AUTOMATED_TESTING` in each source file (e.g. + `MIES_AnalysisBrowser.ipf` lines 4-6). `AUTOMATED_TESTING` is a test-only + define (unlocks otherwise-`static`/private functions for test code) and is + **not** defined in a regular MIES session -- so those module namespaces + don't exist at all right now, and any `ModuleName#Function(...)` call + referencing them is simply unresolvable. Fixed by stripping the reliance + per-call: `AB_GetExperimentsIndices()`'s one-line body + (`FindIndizes(expBrowserSel, col = 0, var = LISTBOX_TREEVIEW, prop = + PROP_MATCHES_VAR_BIT_MASK)`) was reproduced inline in + `ClaudeScrap_TagExperimentsViaGUI`/`ClaudeScrap_DumpExperimentTags` (its + dependencies are non-static/non-gated); the `AB_UpdateColors()`/ + `AB_UpdateTagList()` calls were dropped entirely after confirming from + source they're purely cosmetic (folder-list background highlighting and + the tag-list summary panel respectively) and already re-triggered by the + real button-click handler where it matters; + `ClaudeScrap_AddAnalysisBrowserFolder`/`ClaudeScrap_GetSFPlotWindowInfo` + were stubbed out (disabled, with an explanatory note) since their + remaining dependencies (`AB_AddExperimentEntries`/`AB_CollapseAll`/ + `SF_GetDataDisplayWindowName`, the latter pulling in several more gated + helpers/constants) weren't worth reproducing inline for disposable scratch + code unrelated to the current task. + +**Separate live-session pitfall hit while testing** (not a code bug): calling +`SFE_ExecuteFormula(formula, "test", ...)` with a made-up, nonexistent window +name triggers `DoAbortNow("The main panel is too old to be usable...")` from a +panel-version check deep in `MIES_BrowserSettingsPanel.ipf`/ +`MIES_AnalysisBrowser_SweepBrowser.ipf`. Unlike a normal `Abort`, `DoAbortNow` +shows its alert dialog *synchronously before* unwinding, so it is not +suppressed by a `try`/`catch` around the call, and it blocks Igor's operation +queue the same way a stuck compile-error dialog does -- the bridge has no +auto-dismiss logic for this dialog title (only for "Function Compilation +Error"), so it required the user to close it by hand twice before this was +understood and the offending test function was deleted rather than retried. +Lesson: never pass a fabricated window name to SF/BSP-layer entry points in +this bridge; use a real, currently-open panel or avoid the panel-version- +checked code paths entirely (as `ClaudeScrap_TestAssertStack()` does, by +exercising `SFH_*`/`GetSFAssertData*` directly instead of going through +`SFE_ExecuteFormula`). + +### Gap 2 fix: real UTF tests added, reviewed, and iterated on with the user + +The user added their own real tests in `UTF_SweepFormula.ipf` (`TestAssertDataStack`/ +`TestAssertDataStackOP`, then `TestAssertDataStack2`/`TestAssertDataStack2OP`), +using the test-only `testop(...)` SweepFormula operation +(`SF_OP_TESTOP`/`SFO_OperationTestop`, `#ifdef AUTOMATED_TESTING`-gated, with its +implementation swapped in per-test via the `GetSFTestopName(graph)` SVAR/`FUNCREF` +indirection) to exercise a **real 4-level-deep recursive** nested-call chain +(`testop(0)` -> `testop(1)` -> `testop(2)` -> `testop(3)`, failing at level 3), +rather than the single hand-simulated level in `ClaudeScrap_TestAssertStack()`. +Both now pass. Findings and fixes along the way: + +- **How to actually run a single UTF test case via the bridge**: not by calling + the (static, module-scoped) test function directly -- that bypasses the + igortest framework's fixture setup/teardown and is unreliable (see the next + bullet). The correct invocation, per the user: `RunWithOpts(testcase="")`. +- **Pitfall hit calling a test function directly** (bypassing `RunWithOpts`): + got `RTE 27 "MoveWave...the name already exists"` from + `CreateEmptyUnlockedDataBrowserWindow()`/`CreateFakeSweepData()`, traced to + leftover `DB_ITC16_Dev_0`/`DB_ITC16_Dev_02` DataBrowser windows already open in + the session from earlier manual work -- an artifact of skipping the test + runner's normal per-test cleanup, not a bug in the test itself. Resolved by + using `RunWithOpts` instead, which passed cleanly. +- **Missing cleanup bug (found by review, fixed by user)**: `TestAssertDataStack()` + originally never called `SFH_ResetAssertDataStack()` after its check. Since + the assert-data stack lives at `GetSweepFormulaPath()` -- a single *global* + path (`root:MIES:SweepFormula`), not per-graph -- leftover frames from an + intentionally-aborted nested test like this one would persist for the rest of + the Igor session and could corrupt any *later* test that also inspects + `SFH_GetAssertLocationMessage()`'s output (stale frames' frozen `LOCMSG` + would get walked and appended as spurious "Called from:" segments). Fixed by + adding `SFH_ResetAssertDataStack()` + a `DimSize(...)==0` check at the end of + the test; both `TestAssertDataStack`/`TestAssertDataStack2` now do this. +- **Wiring bug (found by review, fixed by user)**: `TestAssertDataStack2()` + initially set `funcName = "UTF_SWEEPFORMULA#TestAssertDataStackOP"` (the + *original*, `SFE_ExecuteVariableAssignments`-based operation) instead of + `TestAssertDataStack2OP` (the new `SFE_ExecuteFormula`-based one) -- silently + testing the same code path twice rather than the new one. One-line fix. +- **Trailing-space bug (diagnosed, fixed by user)**: after fixing the wiring + bug, `TestAssertDataStack2` failed with two extra trailing spaces in the + innermost `"testop(3)"` line of the message. Root cause: + `formula = SF_AddExpressionToFormula("", expr)` appends a trailing + `SF_CHAR_CR` (`return formula + expr + SF_CHAR_CR`) -- fine for the + assignment-extraction path (`SF_GetVariableAssignments` pulls lines via + `StringFromList`, which strips the separator, so the CR never survives into + the stored formula text), but fatal for a bare-expression string fed straight + into `SFE_ExecuteFormula`: since `"testop(3)\r"` contains no `=`, + `SF_GetVariableAssignments` finds zero assignments and takes its early-return + path, `return [$"", preProcCode]`, handing back the *whole string unchanged, + CR intact*. That CR rides into `SFP_ParseFormulaToJSON`, ends up baked into + the source-location JSON's stored formula text, and + `SFH_FormatSourceLocationError`'s `ReplaceString("\r", formula, " ")` turns it + into a trailing space when rendering the error message. Fixed by dropping + `SF_AddExpressionToFormula` entirely in `TestAssertDataStack2OP` and + `sprintf`-ing straight into the formula string passed to `SFE_ExecuteFormula` + (which doesn't need/want a trailing CR -- other call sites in this file pass + it bare strings like `"testop()"`). + +### Design refactor by the user: push/pop centralized into `SFE_ExecuteVariableAssignments`/`SFE_ExecuteFormula` via a new `newFrame` flag + +Rather than every caller manually bracketing its own nested-execution call with +`SFH_PushAssertDataFrame()`/`SFH_PopAssertDataFrame()` (the original design, +easy to forget), the user added an optional `newFrame` parameter (default 0) to +both `SFE_ExecuteVariableAssignments` and `SFE_ExecuteFormula` +(`MIES_SweepFormula_Executor.ipf`): when set, the function pushes a frame on +entry and pops it on every normal-return path itself. `SFO_OperationIVSCCApFrequencyPrepareVariables`, +`TestAssertDataStackOP`, and `TestAssertDataStack2OP` were all updated to just +pass `newFrame = 1` instead of the manual push/pop wrapper. + +Reviewed this in detail and confirmed it's correct: the push happens before +anything that could consume the live execution-position trackers (and, in +`SFE_ExecuteVariableAssignments`'s case, after the harmless +`SF_GetVariableAssignments` parse, which never touches those trackers); pop +happens on every normal-return branch in both functions, including the +`singleResult` branch of `SFE_ExecuteFormula`; on abort, the pop is correctly +skipped everywhere (frame deliberately left for the aggregate message, matching +the original design intent); and `SFE_ExecuteFormula`'s own internal call to +`SFE_ExecuteVariableAssignments(graph, formula)` (inside its `preProcess` +block) correctly omits `newFrame`, since the outer push already covers that +inner call -- no double-push. + +**Bug found (since fixed)**: right after this refactor, `TestAssertDataStack2OP` +still had a leftover manual `SFH_PopAssertDataFrame()` call immediately after +`SFE_ExecuteFormula(formula, exd.graph, newFrame = 1)` -- a copy-paste artifact +from before push/pop was centralized. Since `SFE_ExecuteFormula(..., newFrame=1)` +now pops its own frame internally on normal return, this extra call would have +double-popped (removing a frame belonging to a *different*, outer level) on any +non-aborting run. It was invisible in this specific test only because every +recursive call always ends in an abort at `result=3`, which skips straight past +it. Removed. + +### Code style / convention fixes from the user + +- Constants must be `static` "when possible" (i.e. whenever not needed + cross-file) and, by convention, declared at the *top* of the procedure file, + not inline near their first use. `SF_ASSERTDATA_NUMFIELDS` + (`MIES_WaveDataFolderGetters.ipf`) was moved from an inline declaration next + to `GetSFAssertDataStack()` up to the file's top-of-file constants block, and + changed from a bare `Constant` to `static Constant` (confirmed it's only used + within this one file). Non-static "global" constants are by convention only + supposed to live in `MIES_Constants.ipf` -- audited every file touched by + this task's diff and confirmed no other new global constants were + introduced anywhere else. +- The new cross-file functions (`GetSFAssertDataStack`, `GetNewSFAssertDataFrame`, + `SFH_PushAssertDataFrame`, `SFH_PopAssertDataFrame`, + `SFH_GetOutermostAssertDataFrame`, `SFH_ResetAssertDataStack`) were checked + against the same "static when possible" rule -- all of them are genuinely + called across multiple files (`MIES_SweepFormula_Executor.ipf`, + `MIES_SweepFormula.ipf`, `MIES_SweepFormula_Operations.ipf`, the UTF test + file), so none can be made `static` without breaking those call sites. The + one truly file-local helper, `SFH_GetAssertLocationMessageForFrame`, is + already `static`. +- **Redundant getter-call pattern, caught by the user, fixed in two places**: + both `GetSFAssertData()` and `SFH_GetOutermostAssertDataFrame()` originally + re-called `GetSFAssertDataStack()` a second time *inside* the + `if(DimSize(...)==0) SFH_PushAssertDataFrame() ... endif` block, apparently + to "refresh" the reference after the push. This is unnecessary: + `SFH_PushAssertDataFrame()` grows the *same* underlying wave via + `Redimension`, it does not replace/recreate it, so the `WAVE/WAVE` reference + obtained *before* the `if` already reflects the new size afterward -- Igor + wave references stay valid across `Redimension` of the same wave. Removed + the redundant second call in both functions. + **Standing lesson to avoid repeating this**: before re-calling a getter a + second time just to "pick up" a change made by an intervening function call, + check what that intervening call actually does to the wave/data structure. + If it only mutates or resizes the *same* object (`Redimension`, in-place + wave-note edits, etc.) rather than replacing it (a fresh `Make` under the + same name, or swapping in a different wave reference), the original + reference obtained from the first getter call is still valid and current -- + a second call is dead weight and, worse, invites the reader to wonder if it + matters (or to copy the pattern elsewhere believing it's necessary). Only + re-fetch when the intervening call could plausibly have replaced the + underlying object, not merely resized/mutated it. +- **Standing convention: unconditional `SFH_ASSERT(0, ...)` calls must use + `SFH_FATAL_ERROR(...)` instead.** `SFH_FATAL_ERROR(message, [jsonId])` is + exactly `SFH_ASSERT(0, message) // NOLINT` under the hood (see + `MIES_SweepFormula_Helpers.ipf`), but its name and the (linter-suppressed) + `SFH_ASSERT(0, ...)` inside make the "this always aborts, there is no + condition to evaluate" intent explicit at the call site, rather than + requiring the reader to notice a literal `0` first argument. Caught in + `UTF_SweepFormula.ipf`'s `TestAssertDataStack3OP` test-op, which had + `SFH_ASSERT(0, "TestOP result threshold reached")` in its unconditional + failure branch -- changed to `SFH_FATAL_ERROR("TestOP result threshold + reached")`. Apply this whenever writing a new unconditional abort in + SweepFormula code, test or production. + +## Igor Pro COM Automation Server bridge (this session's later work) + +Goal: let Claude control a running Igor Pro instance directly, from a local MCP server +(`tools/igor-mcp-bridge/server.py`) acting as a COM client on Windows. + +- **Ruled out**: Igor 10's built-in Python bridge (`igorpro` module, `Python`/`PythonFile` + operations) is documented by WaveMetrics as usable only *from within* Igor Pro itself -- + it cannot be used by an external process to control a running Igor instance. +- **Viable mechanism**: Igor's separate ActiveX/COM Automation Server (Windows-only). Igor + can act as a COM *server*; it cannot act as a COM *client*. All details below were + extracted directly from the local `Igor Pro Folder\Miscellaneous\Windows + Automation\Automation Server.ihf` file (not secondhand/forum info). +- ProgID: `"IgorPro.Application"`. Connect to an already-running instance with + `win32com.client.GetActiveObject("IgorPro.Application")` (Python equivalent of the + documented VB `GetObject(, "IgorPro.Application")`). Using `Dispatch()` instead would + launch a new instance and require handling Igor's post-launch initialization delay. +- `Execute2(int flags, int codePage, BSTR cmds, int* pIgorErrorCode, BSTR* errorMsg, BSTR* + history, BSTR* results)`: does not raise a COM error on Igor-level command failure -- + check `pIgorErrorCode` (0 = success). `codePage` ignored since Igor 7 (pass 0). To get + data back, put `fprintf 0, "..."` inside `cmds` and read it from `results` (WaveMetrics' + own documented example: `WaveStats/Q jack; fprintf 0, "%g", V_avg`). +- `IApplication.DataFolder(nameOrPath)` -> `IDataFolder`; `IDataFolder.Wave(waveNameOrPath)` + -> `IWave`. `waveNameOrPath` accepts an absolute path directly, so `root:` can be used as + a fixed anchor and any full path passed straight into `.Wave(...)`. +- `IWave.GetDimensions(IgorProDataType* pDataType, long* pNumRows, long* pNumColumns, long* + pNumLayers, long* pNumChunks)`. +- `IgorProDataType` enum (confirmed exact values): `ipDataTypeText = 0`, + `ipDataTypeComplex = 0x01` (OR'd combination flag), `ipDataTypeFloat = 0x02`, + `ipDataTypeDouble = 0x04`, `ipDataTypeSignedByte = 0x08`, `ipDataTypeSignedShort = 0x10`, + `ipDataTypeSignedLong = 0x20`, `ipDataTypeUnsignedByte = 0x48`, + `ipDataTypeUnsignedShort = 0x50`, `ipDataTypeUnsignedLong = 0x60`. So `dataType == 0` + means text; anything else is some real numeric subtype (or has the complex flag set). +- `IWave.GetNumericWavePointValue(long index, double* pValue)` and + `IWave.GetTextWavePointValue(long index, int codePage, BSTR* pValue)`: single-point + reads, 1D waves only, real data only for the numeric one. The docs also document + whole-wave SAFEARRAY methods (`GetNumericWaveDataAsDouble`, `GetRawTextWaveData`) but + explicitly recommend the point-value methods "for most uses" -- and the point methods + avoid SAFEARRAY marshaling questions entirely, so the bridge uses those for now (a + whole-wave SAFEARRAY path could be added later for speed on large waves). +- **Critical setup requirement (verbatim from the docs)**: "The Windows operating system + requires that you run the client and server (Igor) as administrator." Both Igor Pro + and the Python client process must run elevated on Windows 10+, or the COM connection + fails. +- **Not verifiable from this session (no Windows/Igor available here to run it)**: the + exact Python-side tuple-unpacking shape pywin32's dynamic dispatch produces for + multi-`[out]`-parameter methods like `Execute2`. The implementation assumes the standard + IDispatch/pywin32 convention (`[out]`-only params come back as a tuple appended to the + return value, e.g. `errorCode, errorMsg, history, results = igor.Execute2(0, 0, cmd)`) -- + this is well-established pywin32 behavior generally, but has not been run against the + real Igor COM server yet. This is the one thing to verify first when testing + `tools/igor-mcp-bridge/server.py` for real. +- `tools/igor-mcp-bridge/server.py` now has a real (not placeholder) implementation of + `execute_igor_command` (via `Execute2`) and `get_wave` (via `DataFolder`/`Wave`/ + `GetDimensions`/point-value methods, 1D real waves only for now). It has grown + substantially since: `execute_igor_command_unattended` (auto-disables/restores the + Debugger around a call — a Debugger pause has no scriptable resume and hangs the + triggering call forever otherwise), `check_bridge_health`, `check_compilation_state` / + `reload_and_compile_procedures` (requires two consecutive "compiled" reads before + trusting one, since `RELOAD CHANGED PROCS`/`COMPILEPROCEDURES` only run once Igor's + operation queue drains — see Advanced Topics.ihf, "Operation Queue" section — and a + single immediate check can race ahead of that), `get_debugger_state`/ + `set_debugger_enabled`/`restore_debugger_settings`, and `get_environment_summary`. + Confirmed live: a leftover compile-error dialog from a failed compile blocks the + operation queue from ever draining (so a later, genuinely-fixed reload/compile keeps + reporting "not compiled") without hanging the bridge's own COM calls directly — there is + no *documented* (COM-level) way to detect or dismiss that dialog. However, it's an + ordinary modal Windows dialog that closes on a real Escape key press, and since this + bridge's Python process and Igor Pro are both required to run elevated anyway (see + above), Windows' UIPI doesn't block a simulated Escape key press from this process + reaching Igor Pro's window (unlike the usual low-to-high-privilege case). v1.10.0 first + added `dismiss_compile_error_dialog` using a hardware-level simulated key press + (`keybd_event`) sent to whatever window was currently in the OS foreground — requiring + a `SetForegroundWindow` call first, i.e. stealing focus. **v1.11.0 replaced this**, per + the user's suggestion, with a `PostMessage(WM_KEYDOWN/WM_KEYUP, VK_ESCAPE)` sent + directly to Igor's dialog window, found by enumerating top-level windows for one with + class `"#32770"` (the standard Windows dialog class) owned by an Igor Pro process — no + foreground/focus change needed at all. + **Live-tested end to end against a real Igor Pro 10.03 instance (v1.12.0), and it + worked.** First finding: the `"#32770"` assumption was wrong — `dismiss_compile_error_dialog` + correctly and safely reported "not found" the first time, with no crash or bad + side effect, and its diagnostic `"igor_windows_seen"` fallback (added specifically + for this) revealed the real window: titled exactly `"Function Compilation Error"`, + class `"Qt693QWindowIcon"` — Igor Pro 10's UI is Qt-based, not native Win32 dialogs. + Switched targeting (v1.12.0) to match by that title (keeping `"#32770"` as a second, + OR'd condition for any genuinely native dialog). Retested: `dismiss_compile_error_dialog` + found the Qt window and posted Escape to it — **user confirmed the dialog actually + closed on screen**. Confirms a *posted* (not real hardware) key event is enough for + Qt's Windows platform layer to react the same as a real key press, with zero + foreground/focus disruption. `reload_and_compile_procedures` calls this automatically + once before giving up and asking a human. + **Separately, twice during this same testing session, Igor Pro became unreachable + via COM (crashed or was closed) shortly after a `reload_and_compile_procedures` + call** — once with broken code present, once right after fixing it back. No root + cause confirmed (no Windows crash logs accessible from here); not established + whether this is related to the bridge's own actions (e.g. the new dismiss logic) + or a pre-existing Igor Pro stability issue independent of it. Documented as a + caution in the tool's docstring and the RST docs. Worth keeping an eye on in + future sessions — if it recurs a third time with a clearer trigger, that would be + worth isolating further. + **Cross-version retest (user's request): closed Igor Pro 10 and opened Igor Pro + 9.06 (build 56685) instead, then repeated the entire scenario from scratch** — + broke `test()`, `reload_and_compile_procedures`, confirmed the same + `"Function Compilation Error"` Qt dialog title, ran `dismiss_compile_error_dialog` + (found it, posted Escape, **user confirmed it closed** — same result as on 10.03), + fixed the code, `reload_and_compile_procedures` succeeded via the + `AfterCompiledHook` counter with **no crash this time**, and `test()` executed + correctly (`"Hello World"` printed, user-confirmed). So both the dialog-title/Qt + behavior and the PostMessage-Escape mechanism are now confirmed across both major + Igor Pro versions (9.06 and 10.03) — updated `server.py`'s docstrings/comments and + `igor-pro-bridge.rst` accordingly (including softening the crash note to note the + 9.06 retest didn't reproduce it, without claiming that rules anything out). +- **`MIES_ClaudeHelper.ipf`** (new file, included from `MIES_Include.ipf`) holds a `static + Function AfterCompiledHook()` that increments `root:gClaudeHelperCompileCounter` on every + successful compile — a compile-confirmation signal driven by Igor itself, as a more + reliable alternative to polling `FunctionInfo()` for a non-existing function. The whole + function body is gated behind `#ifdef IGOR_PRO_BRIDGE ... #endif`, so it compiles out + entirely for a normal end-user build; a developer wanting it active must add + `#define IGOR_PRO_BRIDGE` to the experiment's "Procedure" window (see the `#define` + ordering fact above for why it has to go there specifically, not in the `.ipf` file + itself). Current implementation (as of the v1.18.0 fix below) also captures + `modifiedBefore` via `ExperimentModified`/`V_flag` before touching the counter and + restores unmodified state (`ExperimentModified 0`) afterward if it wasn't modified + before — matching the sibling `AfterCompiledHook` in `MIES_IgorHooks.ipf`, so this + hook never spuriously flips an otherwise-unmodified experiment to "modified" (see the + v1.18.0 Copilot-review entry further down for why this matters specifically for this + bridge). +- **History readback (v1.13.0)**: `execute_igor_command`/`execute_igor_command_unattended` + now return `{"results": ..., "history": ...}` instead of a plain results string -- + `history` is Execute2's own `history` out-parameter ("any text sent to Igor's history + area by the commands", confirmed from `Automation Server.ihf`), so a `print` + statement's output can be verified directly from the return value instead of asking + the user to look at Igor's screen. Also added `read_session_history(stop=False)`, + backed by Igor's built-in `CaptureHistoryStart()`/`CaptureHistory()` functions + (confirmed from `Igor Reference.ihf`) -- a capture starts automatically the first + time `_execute2` runs in the bridge process's lifetime, and each read returns the + full accumulated text since then. Live-tested against Igor Pro 9.06: confirmed + `history` correctly showed `test()`'s "Hello World" output, and separately used both + mechanisms to verify a full `RunWithOpts(testsuite="UTF_Utils_Algorithm")` MIES test + suite run completed with no real failures (distinguishing the suite's own deliberate + fail-path test cases, which print `"!!! ... assertion FAILED !!!"` as *expected* + output, from an actual suite failure -- the suite's own closing "Finished with no + errors" / "Test finished with no errors" lines are the authoritative signal). +- **PR #2754 opened** (`AllenInstitute/MIES` on GitHub) for this bridge. GitHub + Copilot's automated PR review caught several real issues, all fixed (v1.14.0): + (1) the module crashed with a raw ImportError on non-Windows platforms instead of + failing clearly -- added an early `sys.platform != "win32"` check with an actionable + message; (2) `set_debugger_enabled`'s optional sub-flags (`debug_on_error`/ + `debug_on_abort`/`nvar_svar_wave_checking`) were documented as "leave unchanged if + omitted" but `bool(None)` silently forced them to `False` whenever the debugger was + enabled -- fixed to read Igor's current setting and fall back to that instead of + `False`; (3) `get_wave`'s docstring claimed every COM call was individually + reconnect-protected, but the initial post-`GetDimensions` `_get_wave_ref` call + wasn't actually wrapped in `_run_with_reconnect` -- fixed to match the claim; (4) the + module docstring's "Registering with Claude Desktop" section still described editing + `claude_desktop_config.json` directly, contradicting `igor-pro-bridge.rst`'s + documented (and correct) `.mcpb`-install process -- updated to match; (5) a + duplicated-word typo in `MIES_ClaudeHelper.ipf` ("Igor Pro Bridge bridge"). Two + other Copilot comments (both about a "Make sure Igor Pro 10 (or later)..." message, + in `_get_igor` and `check_bridge_health`) were already fixed earlier in this session + when the Igor Pro 9 minimum-version requirement was confirmed -- verified those two + specific strings already said "Igor Pro 9.00" before concluding no further change was + needed. Note: GitHub's PR page loads inline review comment bodies via JavaScript: a + plain `WebFetch`/`api.github.com` fetch only returned the file/line ranges, not the + actual comment text, and the Claude in Chrome extension wasn't connected to render + it -- the user pasted each comment's text manually instead. + +- **`Quit/N` via `Execute2` logs `NOT EXECUTED: Quit/N` to history but Igor quits anyway.** + Confirmed live: `execute_igor_command_unattended("Quit/N")` returned history text + `" NOT EXECUTED: Quit/N\r"`, and a subsequent `check_bridge_health()` call confirmed no + COM object was reachable -- Igor had genuinely quit. Per `Automation Server.ihf`, `Quit` + is exposed as its own dedicated `IApplication.Quit()` method, distinct from + `Execute`/`Execute2`'s command-string interface -- consistent with Igor deferring the + actual quit until after the in-flight `Execute2` RPC call returns (it can't tear down the + process from inside the call servicing it), and logging the deferred line as + "NOT EXECUTED" from the perspective of the synchronous command interpreter, even though + the quit still happens moments later. Practical upshot: don't treat a `NOT EXECUTED:` line + in `history` as proof a command had no effect for operations like `Quit` that are + legitimately deferred/special-cased -- verify with an independent check + (`check_bridge_health`) rather than trusting the history text alone. The bridge has no + dedicated `quit_igor_pro()` tool wrapping the real `IApplication.Quit()` COM method; + `execute_igor_command_unattended("Quit/N")` is sufficient in practice and no new tool was + added for this. + +- **`/UNATTENDED` suppresses the modal "Function Compilation Error" dialog entirely and + reports the error via history instead.** Confirmed live against Igor Pro 9.06 + launched with `/UNATTENDED`: introducing a genuine syntax error into an actually-loaded + file (`MIES_ClaudeHelper.ipf`) and running `reload_and_compile_procedures` gave + `compiled: false`, `raw_function_info: "Procedures Not Compiled"`, and + `dismiss_compile_error_dialog` found no dialog window at all (only Igor's main window + was visible) -- unlike the interactive/non-`/UNATTENDED` case, where that same dialog + reliably appears (confirmed earlier this session on both Igor Pro 10.03 and 9.06). The + exact compile error is readable directly from history via `CaptureHistory`: + `MIES_ClaudeHelper.ipf:46:7: error: expected terminating quote` (format + `::: error: `). This is strictly better for the bridge than + the dialog path: nothing to dismiss, and the real error text is available + programmatically, which the dialog-dismissal path never provided. Not documented + anywhere in Igor's help files (the `/UNATTENDED` flag's own doc entry only mentions the + About Autosave dialog and, as of Igor Pro 10, skipping license activation) -- + this compile-error behavior was inferred and confirmed empirically, not from docs. + +- **Methodology error, caught by the user: verify a target .ipf file is actually loaded + before editing it to test a bridge behavior.** While testing how Igor Pro's + `/UNATTENDED` command-line flag affects the compile-error case, a syntax error was + deliberately introduced into `Packages/tests/Basic/UTF_Basic_Includes.ipf` (the file + used for this in earlier sessions), but this Igor Pro 9 instance had been started + without loading `Basic.pxp` -- so that file was never `#include`d by anything + actually loaded, and `get_environment_summary()`'s `included_procedure_files` list + (fetched earlier in the same session) did not contain it. `RELOAD CHANGED + PROCS`/`COMPILEPROCEDURES` therefore never touched the file at all: no compile error + ever occurred, which is why no dialog appeared, no error text showed up in history, + and `test()` merely failed as "not a recognized command" rather than "broken + function." All of this looked superficially like a real `/UNATTENDED` behavior + change but was actually a no-op test. **Lesson: before editing any procedure file + to probe or reproduce bridge/compile behavior, cross-check the file's name against + the current `included_procedure_files` list from `get_environment_summary()` -- + do not assume a file on disk is part of the live compiled environment just because + it exists in the repo or was used successfully in a previous session (a different + experiment, or no experiment at all, may be loaded now).** Redone correctly on + `MIES_ClaudeHelper.ipf` (confirmed present in `included_procedure_files`), which + gave the real, useful result -- see the `/UNATTENDED` entry above. + +- **v1.15.0: added `configure_igor_launch(exe_path)` / `launch_igor_pro_unattended(...)`**, + letting the bridge start Igor Pro itself with `/UNATTENDED` rather than requiring a + human to do it. `configure_igor_launch` deliberately has no default/guessed + executable path -- the calling agent must ask the user for it once per session (this + repo alone has been tested against two differently-located Igor Pro installs), and + the setting is session-scoped like the history-capture refnum (resets if the bridge + process restarts). `launch_igor_pro_unattended` refuses to launch a second instance + if one is already reachable via COM (launching with only `/UNATTENDED`, no `/I`/`/X`/ + `/SN`/file argument, is documented to start a genuinely new instance rather than + reuse an existing one), and handles elevation two ways: if this Python process is + already elevated, Igor launches as a direct child process (inherits elevation, no + prompt); if not, it launches via `ShellExecute`'s `"runas"` verb (triggers a UAC + consent dialog) -- but the bridge process itself remains unelevated either way in + that second case, so COM calls will keep failing until Claude Desktop is itself + relaunched as Administrator. Live-tested end-to-end the same session -- see the + next entry. + +- **`launch_igor_pro_unattended` (v1.15.0) confirmed working end-to-end**, live-tested + against the Igor Pro 9 nightly install (`...\Igor Pro 9 Folder Nightly\ + IgorBinaries_x64\Igor64.exe`): with the bridge process already elevated, + `configure_igor_launch` + `launch_igor_pro_unattended` launched Igor Pro as a direct + child process with no UAC prompt (exactly as `configure_igor_launch`'s + `"elevation_plan"` predicted), and `check_bridge_health` confirmed COM reachable + afterward. + **New finding from this test**: the initial readiness poll (30s) timed out even + though the launch itself worked, because Igor Pro's Debugger popped up during its + own startup and blocked the COM Automation Server from responding until the user + manually closed it. The Debugger's enabled state is a persistent Igor Pro + preference (confirmed: `get_environment_summary()` showed + `debugger_settings.enable: true` immediately after this fresh launch, with no + experiment loaded) -- it is not reset by `/UNATTENDED` and carries over from + whatever it was left at in a previous Igor Pro session. So a fresh `/UNATTENDED` + launch can still hit the already-documented "Debugger pauses" failure mode (no + scriptable way to dismiss it) during Igor's own startup, before the bridge ever + gets a chance to call `set_debugger_enabled(False)` -- only a human closing it + manually unblocks the COM connection at that point. Disabled the Debugger + afterward via `set_debugger_enabled(False)` for the rest of this session. + **Not yet implemented**: having `launch_igor_pro_unattended` automatically call + `set_debugger_enabled(False)` right after a successful COM connection, to prevent + this recurring on the *next* launch (it can't help the *current* launch, since the + Debugger pause happens before a connection exists to call it through) -- suggested + to the user, not yet actioned. + +- **Confirmed the Debugger-enable preference is genuinely persistent across a full + quit/relaunch cycle, not just within one running instance.** After disabling it + (`set_debugger_enabled(False)`, see entry above), quit Igor Pro via + `execute_igor_command_unattended("Quit/N")` (again showed the misleading + `NOT EXECUTED: Quit/N` history line, again actually quit -- see the earlier `Quit/N` + entry) and relaunched fresh via `launch_igor_pro_unattended`. This time + `com_ready: true` came back in 25 poll attempts (~25s) with no manual intervention + needed -- no Debugger popup -- and `get_environment_summary()` confirmed + `debugger_settings.enable: false` on the freshly-launched instance. So the fix + from the previous entry wasn't a one-time fluke of that running instance; it holds + across restarts, as expected for a genuine Igor Pro preference rather than + per-session state. + +- **Diagnosed and fixed (v1.16.0): `launch_igor_pro_unattended`'s direct-child-process + path triggered a real MIES startup assertion, "We have git installed but could not + regenerate version.txt", that never happens on a normal user launch.** Full chain, + confirmed by reading the actual code (not guessed) and querying the live instance + directly: + - The assertion's stacktrace pointed to `IgorStartOrNewHook` (`MIES_IgorHooks.ipf`, + runs on every Igor Pro launch) -> `GetMiesVersion` -> `CreateMiesVersion` -> + `CreateMiesVersionNoCache` -> `ExecuteGitForMIESVersion` + (`MIES_GlobalStringAndVariableAccess.ipf`). + - `ExecuteGitForMIESVersion` shells out to git via `ExecuteScriptText/B/Z`, + building the command as ` /C " -C describe ... > + version.txt"`, where `shellPath = GetCmdPath()` (`MIES_Utilities_File.ipf`) is + just `GetEnvironmentVariable("COMSPEC")`. `ASSERT(!V_flag, "We have git + installed but could not regenerate version.txt")` follows each + `ExecuteScriptText` call. + - Queried the live bridge-launched instance directly: + `GetEnvironmentVariable("COMSPEC")` came back **empty**, while `PATH` was intact + (including a working git-for-Windows install) -- ruling out a missing/ + unfindable git and pointing specifically at `COMSPEC`. + - Root cause: `launch_igor_pro_unattended`'s direct-child-process path used + `subprocess.Popen([exe_path, "/UNATTENDED"])` with no explicit `env`, so the + child inherits this Python process's own environment -- which, inherited in + turn from whatever launched Claude Desktop, apparently never had `COMSPEC` set. + Windows normally sets `COMSPEC` automatically for every interactive login + session, so a normal double-click/Start Menu launch of Igor Pro never hits + this; it only surfaced via this bridge's non-interactive launch path. + - Fix: added `_build_igor_launch_env()`, which copies `os.environ` and patches in + `COMSPEC` (falling back to `\System32\cmd.exe`) if missing, passed + as `env=` to the `subprocess.Popen` call. Only patches this one confirmed-missing + variable, not a full environment rebuild. + - **Re-tested live after the fix (v1.16.0 installed): confirmed working.** Quit + Igor Pro, relaunched via `launch_igor_pro_unattended` -- `com_ready` in 12 poll + attempts, no Debugger popup, and `GetEnvironmentVariable("COMSPEC")` queried + directly on the fresh instance now returns `C:\Windows\System32\cmd.exe` + (previously empty). User confirmed no assertion appeared on screen this time. + Note: history-based verification still can't retroactively prove the assertion + text is absent (the capture only starts once this bridge process first talks to + a fresh instance, which is necessarily after its startup hook already ran) -- + the fix is confirmed at the root-cause level (COMSPEC populated) plus the + user's direct visual confirmation, not via history text. + - Separately observed mid-test: a `configure_igor_launch` tool call failed with + "Tool permission stream closed before response received", and Claude Desktop + itself relaunched (not Igor Pro) shortly after -- cause not established, but + unrelated to the COMSPEC fix itself (this bridge process's own session state, + e.g. the configured exe path, was simply reset by the restart, same as any + other Claude Desktop restart; re-ran configure_igor_launch and proceeded + normally afterward). + - The `ShellExecute`/`"runas"` path (used when this process isn't elevated) was + not touched -- `ShellExecute` goes through the shell (similar to a normal + double-click), so it's expected to already inherit a proper interactive-session + environment including `COMSPEC`; this was not independently verified, though. + +- **v1.17.0: added `load_experiment(file_path)`** to open a `.pxp` experiment (e.g. + MIES's `Basic.pxp`) into the running instance. Like `Quit` earlier this session, + `LoadExperiment` turned out to exist only as a COM Automation method + (`IApplication.LoadExperiment(flags, loadType, symbolicPathName, filePath)`, + confirmed from `Automation Server.ihf`) -- confirmed absent from `Igor + Reference.ihf` (neither `LoadExperiment` nor `OpenFile` appear there at all), so + it cannot be run as an `Execute2` command string the way most other tools in this + bridge work. Implemented by calling the COM method directly (same pattern as + `get_wave`'s direct `DataFolder`/`Wave` calls), using `loadType=ipLoadTypeOpen` + (2). Per the docs, this does not prompt to save the previously-open experiment's + changes -- left to the caller to do explicitly via + `execute_igor_command('SaveExperiment')` first if needed. Wrapped with the same + Debugger disable/restore bracket as `execute_igor_command_unattended`, since + loading an experiment runs its recreation procedures and MIES's + `IgorStartOrNewHook` startup hook, and this call bypasses `_execute2` entirely so + it wouldn't otherwise get that protection. **Live-tested successfully**: loaded + `Packages/tests/Basic/Basic.pxp`, confirmed via `get_environment_summary()` + (`experiment_file_name: "Basic.pxp"`, 252 procedure files included, Debugger + stayed disabled, no COM reconnect needed) and again later with + `Packages/tests/HistoricData/HistoricData.pxp` to run the + `UTF_HistoricSweepBrowser` test suite (passed -- "Finished with no errors"). + +- **v1.18.0: fixed 2 real issues from a fresh Copilot PR review on #2754**, triggered + by a third commit (`3eca418`, "MCP: Added two new functions") that had been pushed + to the PR branch independently of this session's own (still-uncommitted) local + edits. Same pattern as the first review: user pasted each comment, each was + verified against the actual current code before fixing, both turned out real. + 1. `_is_stuck_dialog_window` (`server.py`) unconditionally treated ANY window with + the generic native Windows dialog class `"#32770"` as safe to dismiss, + regardless of title. Since `dismiss_compile_error_dialog` is called + automatically from `reload_and_compile_procedures`, this could have + Escape-dismissed an unrelated native dialog (e.g. a save-changes + confirmation) -- and it was never actually needed, since the real + compile-error dialog (confirmed live on both Igor Pro 10.03 and 9.06) is a Qt + window, not `"#32770"` at all. Fixed by removing the class-based branch + entirely -- title matching alone (already confirmed sufficient) is what's + used now. Removed the now-unused `_DIALOG_WINDOW_CLASS` constant and updated + every docstring/comment that described the old OR'd-class behavior + (`_attempt_dismiss_compile_error_dialog`'s "reason" message, + `dismiss_compile_error_dialog`'s docstring, the module-level comment block). + 2. `MIES_ClaudeHelper.ipf`'s `AfterCompiledHook` incremented a global variable + without capturing/restoring `ExperimentModified` state first, unlike the + sibling `AfterCompiledHook` in `MIES_IgorHooks.ipf` which already does exactly + this. Left as-is, this could flip an otherwise-unmodified experiment to + "modified," risking a "Save changes?" prompt later -- particularly bad for + this bridge specifically, since that's exactly the kind of dialog it has no + way to dismiss remotely (unlike the compile-error dialog). Fixed to match the + established convention: capture `modifiedBefore` via `ExperimentModified`/ + `V_flag` before the increment, restore to unmodified afterward if it wasn't + modified before. + - Packaged and delivered as v1.18.0. User then committed and force-pushed + directly (outside this session's own git actions) -- PR branch confirmed via + the PR page to now be at commit `af09a1f` (3 commits: `8ae3cf7`, `357ca75`, + `af09a1f`), closing the previously-tracked gap between local fixes and the + GitHub branch. Both of this entry's fixed comments now show "Show resolved" on + the PR page. A new Copilot review was triggered by this push but had not + produced visible results yet as of the last check -- worth checking back for + new comments. + - **Follow-up Copilot comment on this same push, also real**: `igor-pro-bridge.rst` + still described the old, now-removed class-based `"#32770"` matching in the + `dismiss_compile_error_dialog()` tool entry -- the `server.py` code and its + docstrings were updated when the fix was made, but this RST doc was missed. + Fixed all three stale mentions (the tool entry, the "Compile-error dialogs" + narrative section, and the "Known limitations" bullet) to describe title-only + matching, with the removed class-check kept only as explanatory history. Doc-only + change, no new `.mcpb` package needed -- just needs committing alongside the code. + - **Next Copilot comment on the same push, also real**: the RST "Requirements" + section still said "Igor Pro must already be running before a tool call is made + ... it does not launch Igor," predating the v1.15.0 launch tools entirely. Fixed + to note most tools require an already-running instance, with + `launch_igor_pro_unattended` (after `configure_igor_launch`) as the exception. + Checked `server.py`'s own module docstring for the same stale claim -- not + present there, so this was the only spot. Doc-only, no new package needed. + - **v1.19.0: fixed a real Copilot comment on `configure_igor_launch`'s + `elevation_plan` text.** `_is_current_process_elevated()` can return `True`, + `False`, or `None` (undetermined), but the plain `if elevated ... else ...` + ternary treated `None` the same as `False`, reporting "NOT currently elevated" + as a confirmed fact when it was actually unknown. Fixed with explicit + three-way branching (`is True` / `is False` / else-unknown), the unknown case + explaining that `launch_igor_pro_unattended` conservatively treats undetermined + the same as not-elevated (safer than risking a silently unelevated direct + launch). `launch_igor_pro_unattended`'s own launch-path selection was left + unchanged initially -- that fallback behavior is a deliberate, safe default, not + a documentation-accuracy bug. + - **v1.20.0: follow-up Copilot comment, also acted on**: `launch_igor_pro_unattended`'s + own `elevated else ...` ternary and `if elevated:` check had the same + None-treated-as-False pattern. Behaviorally identical either way (None was + already falsy), but changed to explicit `is True` checks anyway so the + deliberate unknown-treated-as-not-elevated choice is unambiguous in the code, + not just documented in prose. Packaged and delivered as v1.20.0, sha256-verified + between build output and the repo copy, same as every prior version. + - **Note**: this copy of SESSION_NOTES.md was carried over by the user from a + different branch's working tree partway through a later session; entries for + bridge v1.21.0 (relaxed reload/compile timing + the `is True` elevation fix), + v1.22.0 (`get_bridge_version()`/`close_data_browser()`), and a clean 25-step + Igor Pro crash stress test (0 crashes) exist in that other branch's copy but are + not reflected here. Not re-added on this branch since they describe bridge/tool + work rather than anything specific to this branch's code. + - **Analysis Browser: programmatically added a folder to the source list** + (`C:\Projects\mies_data\ivscc_apfrequency`), reproducing what the "Add Folder" + button does after its (non-scriptable, OS-native) folder-picker dialog returns a + path -- `AB_ButtonProc_AddFolder` in `MIES_AnalysisBrowser.ipf` calls + `AB_AddElementToSourceList(folder)` then `AB_AddExperimentEntries(win, wFolder)` + then `AB_CollapseAll()`. Hit exactly the three interpreted-vs-compiled + restrictions noted above in immediate succession while trying to do this as raw + Execute2 command-line statements: `Make/FREE/T wFolder = {folder}` failed + (`/FREE` outside a function), `WAVE/T folderList = GetAnalysisBrowserGUIFolderList()` + failed as a command-line assignment, and `AB_AddExperimentEntries`/`AB_CollapseAll` + both failed by bare name because they're `static` and scoped to + `#pragma ModuleName = MIES_AB` (active because this experiment's Procedure window + has `#define AUTOMATED_TESTING`) -- calling them from outside that module needs + `MIES_AB#AB_AddExperimentEntries(...)`/`MIES_AB#AB_CollapseAll()`. Resolved per + the user's direct instruction: created `Packages/MIES/MIES_ClaudeScrapCode.ipf` + (`#include`d from `Packages/MIES_Include.ipf`) holding one compiled function, + `ClaudeScrap_AddAnalysisBrowserFolder(nativeFolderPath)`, that does the whole + sequence internally (including the module-qualified calls) and returns + `"|"`; called via `execute_igor_command_unattended` + after a `reload_and_compile_procedures`. Confirmed working: folder list ended at + exactly 1 entry (no duplicates from the earlier partial command-line attempts, + thanks to a `FindValue` dedup check before adding), `get_environment_summary()` + showed `MIES_ClaudeScrapCode.ipf` in `included_procedure_files` (252 total, up + from 251) with a clean compile. Also had to clean up a stray global `wFolder` + wave left behind at `root:` by an earlier failed command-line attempt (a + `KillWaves/Z` line that never ran because the same command errored on an + earlier statement) -- `top_level_waves` in `get_environment_summary()` is a good + place to spot this kind of debris after an interrupted multi-statement + Execute2 command. + - **Analysis Browser: tagging experiments -- redone via actual GUI controls, + not the internal static function, per explicit user correction.** First + attempt tagged experiments by calling `MIES_AB#AB_AddTagToRow(idx, tag)` + directly for each target row -- this produced the correct result but the + user pointed out it "is not the way a human user would interact with the + AnalysisBrowser Panel" and asked for it to be redone driving the panel's + actual GUI controls via `MIES_ProgrammaticGUIControl.ipf` + (`PGC_SetAndActivateControl`), after manually clearing the tags added the + first way. Redone as `ClaudeScrap_TagExperimentsViaGUI` in + `MIES_ClaudeScrapCode.ipf`: (1) click `button_show_tagcontrol` via PGC to + reveal the Tag Control subpanel (`AnalysisBrowser#TagControl`, hidden by + default on panel open), (2) type each tag into `setvar_tagcontrol_tagname` + and click `button_tagcontrol_addtag` via PGC (both real controls -- this is + exactly what `AB_ButtonProc_AddTagControl`/`AB_SetVarProc_TagNameControl` + do, reached this time through genuine control interaction rather than by + calling `AB_AddTagToSelectedExperiments`/`AB_AddTagToRow` directly). + **Confirmed empirically, and user-confirmed as a known, correct limitation**: + `list_experiment_contents` is a "mode=9" (treeview + checkbox-style) + ListBox, and its multi-row *selection* cannot be driven through + `PGC_SetAndActivateControl(win, control, val=row)` or the raw + `ListBox ..., selRow=row` command at all -- both were tested directly + (writing a small debug dump of the selWave's column-0 bits before/after) + and neither changed the selection bit for this control style; there is no + synthetic-mouse-click primitive available for this kind of listbox through + the bridge. **`MIES_ProgrammaticGUIControl.ipf` does not support every + possible GUI interaction -- ListBox row selection is a confirmed, known gap + in `PGC_*`, not a bug in how it was called.** Consistent with the existing + `ListBoxSelectAll` (Ctrl+A handler) convention already in this codebase: + multi-select state for this listbox style is managed by writing + `LISTBOX_SELECT_OR_SHIFT_SELECTION` directly into the selWave, not through + any higher-level control API. So the final approach sets that selection bit + directly for the target rows (the same mechanism `ListBoxSelectAll` uses), + then performs the actual tag-adding step entirely through the real + SetVariable/Button controls via PGC -- selection state is the one piece not + driven through a GUI-control function, because no such function exists for + it yet. Verified end-to-end: rows 0-1 tagged "a", rows 2-4 tagged "b", + `hideState` for `button_show_tagcontrol` read back as `0` (subpanel + genuinely shown, not just internally flagged). + - **Select all + Load Sweeps into a new SweepBrowser + enable SweepFormula + + execute a formula, all via real GUI controls/documented APIs.** Selection: + `ListBoxSelectAll(GetExperimentBrowserGUISel())` -- the exact function + `AB_ListBoxProc_ExpBrowser`'s own Ctrl+A handler calls, confirmed still + present (non-static) on this branch too. Loading: set + `popup_SweepBrowserSelect` to `"New"` and click `button_load_sweeps` via + PGC -- `AB_ButtonProc_LoadSweeps` opens a new `SweepBrowser` window and + loads sweeps for every selected+expanded experiment + (`AB_GetExpandedIndices` starts from the same selection bit). The new + window was identified by diffing `WinList(SWEEPBROWSER_WINDOW_NAME+"*", ";", + "WIN:1")` before/after the click (loop-based list diff -- no ready-made + "list difference" utility found). SweepFormula: enable via PGC on + `check_BrowserSettings_SF` in `BSP_GetPanel(win)` -- confirmed correct + against this codebase's own `TestDefaultFormula` test, which uses the + identical control/value. Formula text: **`SF_SetFormula(win, formula)`** + (non-static, in `MIES_SweepFormula.ipf`) is the documented, intended way to + set the SweepFormula notebook's contents (`ReplaceNotebookText` under the + hood) -- notebooks aren't one of PGC's supported control types, so this + isn't a PGC call, but it's a real public API, not a bypass of anything; + used once with `""` to clear, once with the real formula. Execute: click + `button_sweepFormula_display` via PGC (matches `TestDefaultFormula` again). + - **`GetNotebookText(win, mode=N)` mode pitfall, caught via a false alarm**: + initially verified the notebook's content using `mode=4` (copied from + unrelated `BSP_*` help-notebook code elsewhere in `MIES_BrowserSettingsPanel.ipf`) + and got an empty string back even though `SF_SetFormula` had just set real + text -- looked like `SF_SetFormula` was silently failing. **It wasn't**: + `SF_GetCode` (`MIES_SweepFormula.ipf`), the function the Display button + itself uses to read the notebook, explicitly calls + `GetNotebookText(formula_nb, mode = 2)`. Reading back with `mode=2` + correctly showed the real text every time. Lesson: don't assume a + `getData`/similar mode number from one call site transfers to a different + notebook/purpose -- check the specific reader the real code path uses. + - **`ivscc_apfrequency()` executed with no error and DID produce real plotted + output -- initial verification methodology was wrong, corrected by the + user.** `GetSweepFormulaOutputSeverity()`/`GetSweepFormulaOutputMessage()` + correctly showed a clean run (`SF_MSG_OK`, no error). But the "no visible + output" conclusion drawn at the time was wrong, for two compounding + reasons: (1) **SweepFormula plots into a separate, dedicated plotter + panel, not into the SweepBrowser's own graph** -- so checking + `TraceNameList("SweepBrowser", ...)` for new traces was checking the wrong + window entirely (user explicitly corrected this: "The sweepformula + plotter does not change the traces in the SweepBrowser graph but creates + a new panel with graph or table subwindows"). The actual window is named + `SweepFormula_plotsweepBrowser_graph`, and its traces live in a *child + subwindow* (`ChildWindowList(...)` -> `graph0`), not at the panel's own + top level -- `TraceNameList("SweepFormula_plotsweepBrowser_graph", ...)` + alone is also empty; the real call is + `TraceNameList("SweepFormula_plotsweepBrowser_graph#graph0", ";", 1)`. + (2) The before/after `WinList("*", ";", "WIN:65535")` window-diff was run + too late (in a separate debug call after the plotter window had already + been created by the first real run), so by the time that diff ran, the + window already existed in both the "before" and "after" snapshots and + correctly showed as "not new" -- a false negative caused by diffing at + the wrong point in time, not by the operation failing. + **Confirmed working correctly**: `TraceNameList("SweepFormula_plotsweepBrowser_graph#graph0", ";", 1)` + returned 13 real traces, including per-experiment traces + (`T000000d0_a__Scn1a_R613X_B6_825669_02_09_02_nwb`, etc.) split into `_a__`/`_b__` + groups matching the "a"/"b" tags applied earlier in this session (via + `ClaudeScrap_TagExperimentsViaGUI`, still in effect -- confirming + `ivscc_apfrequency()` auto-groups by existing experiment tags when no + explicit `seltag` argument is given), plus computed + `ivscc_apfrequency_concat`/`_DAScale`/`_DAScale_Avg`/`_avg_bins` traces per + group. **Lesson for future verification of SweepFormula operations: check + the dedicated `SweepFormula_plot*` panel and its child graph/table + subwindows (via `ChildWindowList`), not the host DataBrowser/SweepBrowser's + own graph** -- and take a window-list snapshot immediately before the + triggering action, not in a later, separate diagnostic call. + - **Better yet, per the user's follow-up explanation: don't discover the + SweepFormula plot window via `WinList`/diffing at all -- derive its name + deterministically.** MIES allows multiple simultaneous SweepBrowsers, each + accepting its own SweepFormula input and creating its own independently- + named SweepFormula plot window, precisely so they don't collide -- the + naming is generated from the specific SweepBrowser/DataBrowser `graph` + argument, not a global counter. Chain: `SF_FormulaPlotter` -> + `SF_CreateDataDisplayWindow` -> `SF_GetDataDisplayWindowName`/ + `SF_NewSweepFormulaBaseWindow` (all `static`, module `MIES_SF` under + `#pragma ModuleName`, since `MIES_SweepFormula.ipf` also gets that pragma + when `AUTOMATED_TESTING` is defined -- same pattern as `MIES_AB` elsewhere + in this file). Added `ClaudeScrap_GetSFPlotWindowInfo(graph)` to + `MIES_ClaudeScrapCode.ipf`, calling + `MIES_SF#SF_GetDataDisplayWindowName(graph, SF_DISPLAYTYPE_GRAPH, SF_DM_SUBWINDOWS, 0)` + directly (module-qualified, all-global constants) -- confirmed it returns + exactly `SweepFormula_plotsweepBrowser_graph#graph0` for `graph="SweepBrowser"`, + matching the real window found manually, with the same 13 real traces. + Note the `SF_DM_NORMAL`-mode variant (no `idx` suffix child window) is a + *different*, non-existent name for this case (`WindowExists` false) -- + `SF_DM_SUBWINDOWS` is the mode actually used by the real plotter code, and + already returns the fully-qualified `host#graph0` reference in one call + (no need to manually append `"#" + SF_WINNAME_SUFFIX_GRAPH + "0"` on top + of it -- that would double up the suffix). This is now the correct, + robust way to locate a specific SweepBrowser's SweepFormula plot output, + including when more than one SweepBrowser/plot is open at once. + +## `tools/ipt` (Igor Programming Tool) evaluated for AST/code understanding + +The repo ships `tools/ipt` (Linux ELF, statically linked), `tools/ipt.exe` (Windows), and +`tools/run-ipt.sh` (a git-root-relative wrapper picking the right binary by `uname`). Docs at +docs.byte-physics.de/ipt. Investigated whether it genuinely improves understanding of Igor +Pro source beyond manual reading -- conclusion: **yes, with real value and one confirmed +gap**, based on live runs against this repo (not assumed from the docs alone). + +- **`ipt check --print-ast ` parses actual Igor Pro source into a real AST** (node + types like `Function`, `Declaration`, `Assignment`, `OperationStatement`, each with + precise line:column spans) -- confirmed by running it against + `Packages/MIES/MIES_GlobalStringAndVariableAccess.ipf` (the exact file investigated + earlier this session for the COMSPEC/git bug): it parses cleanly with **zero errors**, + including the tricky nested-quote `sprintf`/`ExecuteScriptText` command-building lines, + confirming the parser handles real, non-trivial MIES code correctly, not just toy + examples. +- **`--print-symbol-table` lives under `ipt rename`, not `ipt check`** (corrected after + actually running it -- `ipt check --help` has no such flag; `ipt rename --help` does, + alongside its own `--print-ast`, which per its help text prints "each AST after symbol + table creation"). **`ipt.exe` only ever knows about the procedure file(s) explicitly passed + via the `files`/`-f` arguments** -- it does not resolve `#include`s or otherwise pull in + the rest of the codebase itself, so a symbol table (or an AST) requested for one file only + reflects that file's own top-level declarations, not anything defined in files it + `#include`s or that `#include` it. Pass every file actually needed for a complete picture + explicitly. + - **Verified the output format is genuinely parseable**, live, against a small throwaway + test file (`Function IPTSymTestAdd(variable a, variable b)` with a local `result`, plus + a `static Function/S IPTSymTestGreet`, plus one global `Constant`). `ipt rename + --print-symbol-table ` prints a structured cross-reference table, not a flat list: + a top-level `files: [...]` block per input file (module name(s), and reference-only + lists of its constants/structs/functions), followed by global `structures:`/`constants: + `/`functions:`/`variables:` sections holding the *actual* records, each tagged with an + `id: [address/counter]` (an ephemeral, process-memory-derived identifier -- confirmed by + running twice and seeing different numbers both times; **not** stable across runs, so + don't rely on it for anything beyond within-a-single-invocation cross-referencing). + Records elsewhere just hold a `-> [id] kind: name` pointer back to the real one. Each + function record carries its full signature (required/optional args, return type, `multi + return types` for the `[a, b] = Func()` destructuring style) and a `variables:` list of + pointers to every one of its params/locals. Each variable record carries `write points`/ + `read points`/`definition points` -- each a `statement: [line:col - line:col]` + span -- distinguishing where a variable is declared/assigned versus merely read. + - **Cross-checked this understanding against real rename behavior**: the test file's + `result` local (`variable result = a + b`) showed exactly one `write points`/ + `definition points` entry at its `Declaration` statement (line 9) and one `read points` + entry at its `ReturnStatementNormal` (line 10). Running an actual `ipt rename -f + -l 9 -c 11 -n resultRenamed ` (targeting that same declaration) previewed renames + at exactly `9:11` and `10:9` -- matching the symbol table's own write/read points + precisely, confirming the table was read correctly rather than just superficially. + - **Gotcha hit while testing**: `ipt rename --print-symbol-table ` with *no* rename + target (`-f`/`-l`/`-c`/`-n` all omitted) prints the full symbol table correctly, then + **crashes** (`Bug: target file was not parsed`, `terminate called without an active + exception`, exit code 134/SIGABRT) instead of exiting cleanly -- a real bug in `ipt` + itself (`rename` apparently always expects a valid target even when only the debug + printout is wanted). The printed symbol table content before the crash is complete and + trustworthy regardless; just don't rely on the process's exit code in that no-target + case, and prefer always supplying a valid target if a clean exit matters. +- **Whole-codebase check**: ran `ipt check` (no `--print-ast`, batched to fit the shell's + per-call time budget) over all 487 `.ipf` files under `Packages/`. Result: **484 parse with + zero errors; the only 3 parsing errors are the same deliberately-malformed fixture file** + (`test-input-function-params.ipf`, a doxygen-filter test input, vendored/duplicated under + `doc/`, `igortest/docu/`, and `unit-testing/docu/`) -- not real MIES source bugs. So `ipt` + is practically usable across this entire real codebase, not just isolated files. +- **Directly relevant to the shadowing rule just added above**: `ipt` ships a lint rule + named exactly for this, `BugproneReservedKeywordsAsIdentifier` (confirmed via `ipt lint + --list`). Live-tested its actual scope with three throwaway test files: it correctly + flags a variable named after a genuine reserved **keyword/type name** (e.g. `variable + wave` -> "Use of reserved keyword as identifier. Please rename it."), **but it does + NOT flag a variable/string named after a built-in **function** name** (`variable abs`, + `string print`, `string log` all passed both `ipt check` and `ipt lint` -- including + `--include BugproneReservedKeywordsAsIdentifier` explicitly -- with zero warnings). This + is a real, confirmed gap: `ipt`'s existing tooling would not have caught the user's + original `string log` example, which is exactly why that rule was worth writing down by + hand in this file rather than assuming `ipt lint` already covers it. +- **The AST itself has no built-in-name-resolution semantics** -- confirmed from the + printed tree for a `string log` test case: the declaration, assignment target, `print` + argument, and `return` value all show up as plain `(Id \`log\` ...)` nodes with no + annotation distinguishing "shadows a built-in" from "an ordinary local name." This is + consistent with `ipt` being a syntax-level tool (parser + lints operating on the parse + tree), not a full semantic/symbol-resolution engine against Igor's built-in function + table -- explains why the lint gap above exists rather than being an oversight. +- **Practical takeaway for future sessions**: `ipt check`/`ipt check --print-ast` is a fast, + reliable way to get an authoritative parse of a `.ipf` file's structure (function + signatures, statement nesting, operation-argument shape) without needing a live Igor Pro + instance, and is trustworthy against this codebase (near-100% clean parse rate). `ipt + lint` catches genuine keyword-as-identifier misuse and a range of other real style/bug + patterns (see `ipt lint --list` / the docs' rule list), but does not catch built-in + *function*-name shadowing -- that class of bug still needs to be caught by review/manual + attention (or a new custom rule), not by relying on existing `ipt` output. +- Performance note: parsing this repo's ~487 `.ipf` files is not uniformly fast -- most + batches process in the range of tens-of-milliseconds-per-file, but at least one file in + the tree parses roughly 10x slower than the rest (bisection pinned it to a ~125-file + slice without identifying the specific file); worth keeping invocations chunked/batched + rather than assuming a single whole-codebase call finishes quickly. + +## Reading Igor Pro `.ihf` help files as formatted notebooks (better than an OS-level file read) + +The user pointed out a second way to read Igor's own `.ihf` help files, beyond just reading +the raw file bytes at the OS level: `.ihf` files are themselves Igor formatted-text +notebooks, and any such notebook can be opened directly via `OpenNotebook`, then read back +through Igor's own notebook operations via the bridge -- confirmed live end-to-end against +`Igor Pro 9 Folder Nightly:Igor Help Files:Debugging.ihf`. + +- **Igor pre-registers `.ihf` files as "open as a help file" via ordinary (often hidden) + help windows -- `WIN:512` is the correct `WinList` bit for these, confirmed against + `WinList` operation's own bit table** (1=graphs, 2=tables, 4=layouts, 16=notebooks, + 64=panels, 128=procedure windows, **512=help windows**, ...). An earlier pass through this + investigation guessed `WIN:1024` for help windows and got an empty result, which was + wrongly read as "the file is registered with no window object at all" -- corrected after + actually checking `WinList`'s documented bit values: `1024` isn't a defined window type at + all, so that query was meaningless, not evidence of anything. Re-tested with the correct + bit: `WinList("*", ";", "WIN:512")` reliably lists every currently-open help file + (including invisible ones), while adding `,VISIBLE:1` restricts to only the visible ones + -- e.g. `Igor Reference.ihf` showed up in the plain `WIN:512` list but not the + `VISIBLE:1`-qualified one, i.e. it was open as a hidden help window (Igor appears to open + it in the background on its own, e.g. for command-line help lookups, independent of + anything this session did explicitly). `OpenNotebook/R ""` fails + with error 251 ("The file ... is already open but as a help file") whenever the target + file's help window (hidden or not) is currently open -- a help-file view and a + plain-notebook view of the same file are mutually exclusive. +- **The user's own manual technique**: hold Alt (Option on Mac) and click a help window's + close button to close and unregister it, then reopen the same file via File > Open > + Notebook. **Programmatic equivalent, found in `Igor Reference.ihf`**: the `CloseHelp` + operation (added in Igor Pro 7.00). `CloseHelp/ALL` (closes every registered help window) + confirmed working live -- immediately unblocks `OpenNotebook/R` on any `.ihf` file + afterward. `CloseHelp/FILE=""` (meant to close just one specific file) instead threw + `error 140: expected window title` when tried against a full HFS-style path -- not yet + resolved why; `/ALL` is the confirmed-working option and is harmless to use even when only + one file matters, since Igor's help windows are cheap to reopen on demand. +- **Full plain-text readback**: after `OpenNotebook/R ""` succeeds, find the resulting + window's actual name via `WinList("*", ";", "WIN:16")` (Igor auto-assigns + `Notebook0`/`Notebook1`/... unless `/N=name` was given to `OpenNotebook`), then: + `Notebook selection={startOfFile, endOfFile}` followed by `GetSelection notebook, + , 2` sets `S_Selection` to the notebook's entire text in one call (paragraph breaks + come through as `\r`). Confirmed against `Debugging.ihf`: 20,554 characters, first ~400 + matched the file's actual visible content exactly. +- **Formatted-text export reveals genuine content-block structure, not just prose (the + user's key hint)**: `.ihf` files are *formatted* notebooks, and WaveMetrics' own help + authoring convention uses named paragraph styles for different content roles, not just ad + hoc bold/italic/font-size choices. Exporting via `SaveNotebook/O/S=5/H={"UTF-8", + writeParagraphProperties, writeCharacterProperties, PNGOrJPEG, quality, bitDepth} as + ".html"` (saveType 5 = HTML export; confirmed with + `writeParagraphProperties=3`/`writeCharacterProperties=7`, `SaveNotebook` V-823) produces a + `

` tag on every single paragraph, and the class name itself directly + identifies the paragraph's semantic role -- confirmed live against `Debugging.ihf`'s + export: + - `Topic` -- a section heading (e.g. `

Debugging

`). + - `Subtopic` / `Subtopic-Indented` -- a sub-heading. + - `TopicBody1` / `TopicBody1a` -- ordinary body prose. + - `Steps` / `ListNumbered` -- bullet/numbered list items (e.g. `

• Using + print statements

`). + - `Code1` / `Code1a` / `Code-Indented1` -- a line of example code (e.g. `

Function Test(w, num, str)

`). + - `SeeAlso`, `NOTE`, `Table2Col`, `Table3Col`, `RelatedTopics` -- self-explanatory by name. + This is strictly better than inferring structure from raw font weight/size, since the + class names already encode the author's intended block type -- heading vs. body vs. code + vs. list item vs. note -- with no guessing required. +- **Non-destructive, repeatable workflow (user-specified, live-verified end-to-end)**: the + first pass through this technique left `CloseHelp/ALL` in effect permanently and never put + the previously-open help file(s) back -- a real gap, since Igor may have had help windows + open (visibly or in the background) before this workflow ever touched anything, and those + shouldn't be lost as a side effect of reading a different file. Corrected workflow: + 1. **Snapshot what's currently registered as open help**, before touching anything: + `String helpAll = WinList("*", ";", "WIN:512")` and `String helpVisible = WinList("*", + ";", "WIN:512,VISIBLE:1")` (the latter needed to restore visible ones as visible, not + just hidden). + 2. `CloseHelp/ALL`. + 3. `OpenNotebook/R ""`; find the resulting window's actual name + via `WinList("*", ";", "WIN:16")` (Igor auto-assigns `Notebook0`/`Notebook1`/... unless + `/N=name` was given). + 4. `SaveNotebook/O/S=5/H={"UTF-8", 3, 7, 0, 0.9, 32} as ".html"` (path + must be reachable by whatever reads it back -- e.g. somewhere under the bash-mounted + repo), then read/parse the exported HTML for the `

` per-paragraph + structure. + 5. `KillWindow/Z ` to close the temporary notebook. + 6. Repeat steps 3-5 for any other `.ihf` files that need reading in the same session -- + no need to re-run `CloseHelp/ALL` again since it's already in effect. + 7. **Restore**: for each file name captured in step 1, `OpenHelp/V=(1 if it was in + helpVisible else 0)/INT=0 ""` (`/INT=0` suppresses any + recompile-confirmation dialog; `WinList` only returns bare file names for help/procedure + windows, so resolve each back to a full path -- e.g. by prefixing the known Help Files + folder path, or matching against an `IndexedFile` listing of that folder). + - Live-verified full round trip: before touching anything, `helpAll = "Igor + Reference.ihf;"` (not in `helpVisible`, i.e. open hidden in the background). `CloseHelp/ + ALL` -> `OpenNotebook/R` on `Igor Shortcuts.ihf` (a file not touched earlier this + session) opened as `Notebook1` -> HTML export showed new paragraph classes specific to + this file's content (`ShortcutHow`, `ShortcutHow-7`, `ShortcutTo`, + `SeeAlsoIndented`, alongside the already-known `Topic`/`TopicBody1`/`TopicBody1a`) -- + confirming the class-naming convention is genuinely per-content-role, not just a fixed + set from one file. `KillWindow/Z Notebook1` closed it cleanly. `OpenHelp/V=0/INT=0 + "...Igor Reference.ihf"` restored it: returned `V_Flag=0` (success) and + `WinList("*", ";", "WIN:512")` immediately showed `Igor Reference.ihf;` again -- state + fully restored. Scratch HTML export files deleted after each read; they have no lasting + purpose once parsed. + - **Caveat: expect drift between the snapshot and later checks.** `Igor Reference.ihf` + reappeared in the `WIN:512` list at one point in this session even though nothing in this + workflow had explicitly reopened it -- ordinary Igor Pro activity (e.g. command-line + help lookups) can silently open/reopen certain help files in the background outside of + any explicit `OpenHelp` call. Always take the snapshot (step 1) immediately before + intervening, rather than trusting an earlier snapshot or assuming the set of open help + files is static. +- **This entire technique only applies to XOPs that ship their own `.ihf` help file -- + not every XOP does.** Some XOPs that extend Igor Pro's built-in pool of + functions/operations bundle a compiled `.ihf` help file of their own (readable via this + same `CloseHelp`/`OpenNotebook`/`SaveNotebook` workflow, same as any of WaveMetrics' own + help files) -- e.g. National Instruments' `DAQmx_*` operations. **Other XOPs ship no help + file at all** -- per the user, the JSON XOP (used elsewhere in this codebase) is one of + these; its documentation is only available externally, at + . +- **Igor Pro loads XOPs/help files/fonts/procedure files from two places, joined together + at startup into one environment (per the user, matching the "Igor Pro User Files" help + topic)**: a **global** location, subfolders of the Igor Pro program folder itself (e.g. + `...Igor Pro 9 Folder Nightly:Igor Extensions (64-bit):`), and a **user-specific** + location, `:WaveMetrics:Igor Pro User Files:` (confirmed live: + `C:Users:enigm:Documents:WaveMetrics:Igor Pro 9 User Files:` exists and, via + `IndexedDir`, mirrors the exact same subfolder set as the global program folder -- + `Igor Extensions`, `Igor Extensions (64-bit)`, `Igor Fonts`, `Igor Help Files`, `Igor + Procedures`, `User Procedures`). This resolves the open question from earlier this + session about where `DAQmx`'s own help file actually lives, and turned up two more + concrete, useful facts from the user's own installation (`IndexedFile(..., "????")` to + list all files regardless of type, since `.ihf`-only filtering misses shortcuts): + - `Igor Extensions (64-bit):` (user-specific) contains `NIDAQmx64.XOP - Shortcut.lnk` + (a second reference to the same DAQmx XOP already found in the global folder) and + `Debug JSONXOP- Shortcut.lnk.dis`. **Corrected by the user**: the `.dis` suffix here + is *not* Igor's disable-an-extension convention (initial guess, wrong) -- it's the + user's own unrelated helper file for switching which JSON XOP build (release vs. + debug) gets included. Lesson: don't assume a plausible-sounding Igor convention + without checking -- a `.dis`-suffixed file sitting in an XOP folder isn't + self-explanatory and can just as easily be project-specific tooling. + - `Igor Help Files:` (user-specific) contains `NIDAQ Tools MX Help - Shortcut.lnk` (a + shortcut to DAQmx's real help file -- confirming it does ship one, as expected) and + `ZeroMQ.ihf` (a real, non-shortcut `.ihf` file) -- a second concrete, directly-readable + example of an XOP-supplied help file via this same technique, alongside DAQmx. + **Practical upshot**: before assuming an XOP operation/function can be looked up via this + `.ihf`-reading workflow, check both the global and user-specific `Igor Help Files:` + folders for it (`IndexedFile`/`IndexedDir`, `"????"` wildcard to include shortcuts), or + just try `OpenNotebook`/`OpenHelp` and see if it resolves -- if neither location has one, + fall back to that XOP's own external documentation instead of assuming no help exists at + all. +- **`FunctionList`/`OperationList` (per the user) enumerate every function/operation + actually available in the running instance, including ones added by XOPs -- a + complementary way to know the live environment's real capabilities, independent of + whether any given one happens to have a help file.** Confirmed live: + `FunctionList("*", ";", "KIND:4")` (KIND:4 = "external functions, defined by an XOP") + returned 244 functions this session, including the `fDAQmx_*` family (e.g. + `fDAQmx_ReadChan`, `fDAQmx_ScanStart`); `OperationList("*", ";", "external")` returned 80 + operations, including the `DAQmx_*` family (e.g. `DAQmx_AI_SetupReader`, + `DAQmx_CTR_CountEdges`). Cross-checked against the ZeroMQ XOP-help finding above and it + matched exactly as expected: `FunctionList("*ZeroMQ*", ";", "KIND:4")` returned 21 real + `zeromq_*` functions, consistent with `ZeroMQ.ihf` actually being present and loadable. + **Corrected by the user on the JSON XOP check specifically**: the JSON XOP adds + *operations*, not functions -- `FunctionList("*JSON*", ";", "KIND:4")` returning nothing + was simply the wrong list to check, not evidence the XOP wasn't loaded (see the `.dis` + correction above -- that file was never actually a disable marker in the first place). + `OperationList("*JSON*", ";", "external")` is the correct check and returns 13 real, + currently-loaded `JSONXOP_*` operations (`JSONXOP_Parse`, `JSONXOP_GetValue`, + `JSONXOP_Dump`, `JSONXOP_AddTree`, etc.) -- the JSON XOP is loaded and fully functional in + this instance; the earlier "not loaded" conclusion was wrong on two independent counts at + once. **Practical use, corrected**: when unsure whether a given function/operation is + actually available in the current live instance, check `FunctionList` for XOP-added + *functions* (`KIND:4`) and `OperationList("*", ";", "external")` for XOP-added + *operations* -- an XOP can contribute either or both, so check both list types rather + than assuming from one empty result that an XOP contributes nothing at all. +- **Even more direct and conclusive, per the user: if currently-compiled code calls an + XOP's operation/function and the instance is in compiled state, the XOP must be loaded -- + no separate enumeration needed at all.** `Packages/MIES/json_functions.ipf` (confirmed + present in `included_procedure_files`) calls `JSONXOP_Parse`, `JSONXOP_Dump`, + `JSONXOP_New`, `JSONXOP_Release`, `JSONXOP_Remove`, etc. directly (e.g. line 85: + `JSONXOP_Parse/Z=1/Q=(JSON_QFLAG_DEFAULT) jsonStr`); Igor cannot compile a call to an + operation that doesn't exist, so a clean `check_compilation_state()` (`compiled: true`) + together with this file being included is already airtight proof the JSON XOP is loaded + -- stronger and simpler than inferring it from an `OperationList` scan. **Even simpler + still, and the one to reach for first**: `get_environment_summary()` already has a + dedicated `loaded_xops` field for exactly this question -- confirmed live it lists + `"JSON-64"` (and `"NIDAQmx64"`, `"ZeroMQ-64"`, etc.) directly by name. No need for + `FunctionList`/`OperationList` scans, or the compiled-code inference above, when the + question is simply "is XOP X loaded right now" -- `loaded_xops` answers that in one call. + **Corrected by the user: `FunctionList`/`OperationList` do *not* actually solve the other + question either (which specific entries a given XOP contributes) -- overclaimed above.** + Both return a flat name list with no per-entry attribution back to the XOP that defined + it (confirmed: neither operation's documented output includes an owning-XOP field, and no + `IgorMan.md` search turned up any dedicated name-to-XOP lookup). In practice, attributing + a specific function/operation name to a specific XOP relies entirely on already knowing + that XOP's naming convention (e.g. `JSONXOP_*`, `DAQmx_*`, `zeromq_*` -- all human + knowledge, not queryable from Igor). Diffing the list before/after loading only the XOP in + question isn't a practical workaround either: no `IgorMan.md` search found any operation + for loading/unloading a specific XOP at runtime, so XOPs are apparently fixed for an + entire Igor Pro session (set only via which files sit in the Extensions folders at + startup) -- there's no in-session way to toggle just one and diff. **Bottom line, per the + user: with no documentation or source available for a given XOP, there is no simple way + to determine which operations/functions it specifically contributes** -- `loaded_xops` + and `FunctionList`/`OperationList` only answer "is this XOP loaded" and "what's available + in total," not "which of these came from XOP X." + +## Solved: extracting an XOP's operations/functions from its compiled binary, no source/docs needed + +Follow-up to the "no simple way" conclusion just above. The user explained the actual mechanism: +an XOP is a DLL with a specific structure, and the list of operations/functions it adds to Igor is +encoded in that DLL's resources -- and gave read access to WaveMetrics' own XOP Toolkit 8.01 +(`c:\download\XOP8.01\`, containing `XOPMan8.pdf` plus buildable sample-XOP source). Working +through the toolkit manual (`pdftotext -layout` extraction, `poppler-utils`'s `pdftotext` already +available in the sandbox) confirmed this in full, and a live test against real, closed-source, +already-compiled `.xop` files proved it works with zero vendor documentation or source needed: + +- **The relevant resources are `XOPI` 1100 (required, general XOP info), `XOPC` 1100 (operations + the XOP adds), and `XOPF` 1100 (functions it adds)** -- confirmed from the manual's "XOP + Resources" chapter (`XOPMan8.pdf`, "There are three types of resources... XOP-specific + ...WaveMetrics..."). On Windows, unlike the Macintosh `.r`/Rez-resource-fork mechanism, these + are compiled by the **standard Windows resource compiler** from a `.rc` file (e.g. + `WaveAccessWinCustom.rc`) directly into the `.xop`/DLL's own PE resource section, using the + literal strings `"XOPC"`/`"XOPF"`/`"XOPI"` as the resource *type* name (not a numeric type) and + `1100` as the resource ID -- confirmed directly from the manual's own words: "Igor examines + these custom resources to determine what operations, functions and menus the XOP adds." This + means any XOP's `.xop` file, being an ordinary PE/DLL, can have these extracted by any generic + PE resource reader -- no proprietary format, no vendor cooperation needed. +- **Exact binary layout, confirmed from the manual's Chapter 5/6 (`XOPC`/`XOPF` Windows `.rc` + source examples)**: + - `XOPC` (operations): repeating `{null-terminated name string; int16 little-endian category + bitmask}` records, terminated by a record whose name is the empty string (a single `0x00` + byte) with no trailing bitmask after it. + - `XOPF` (functions): repeating `{null-terminated name string; int16 category bitmask; int16 + return-type code; int16 parameter-type code}*N; int16 `0` to terminate that function's + parameter list}` records, the whole resource terminated the same way as `XOPC` (an empty-name + record). + - Category/type bit meanings (`XOPOp`/`ioOp`/`compilableOp`/... for `XOPC`; `NT_FP64`/ + `WAVE_TYPE`/`HSTRING_TYPE`/... for `XOPF` return/parameter types; `F_UTIL`/`F_EXTERNAL`/... + for `XOPF`'s own category bitmask) are all documented in the manual with their exact decimal + values -- not needed just to get the name list, but available for full interpretation. +- **Live-verified against real, compiled, closed-source `.xop` files already present in this + machine's global Igor Pro 10 install** (`More Extensions (64-bit)/.../*.xop` -- no need to build + anything from the toolkit's own sample sources): using Python's `pefile` library (pure Python, + `pip install pefile`, works even in this Linux sandbox against a Windows PE file) to walk the PE + resource directory for a type entry named `"XOPC"`/`"XOPF"`, then the above byte-layout parser + (implemented and run directly, not just theorized): + - `NIGPIB2-64.xop`'s `XOPC` resource decoded to exactly the 10 operations already known from its + `.r` source seen earlier in the XOP Toolkit (`NI4882`, `GPIB2`, `GPIBRead2`, `GPIBWrite2`, + `GPIBReadWave2`, `GPIBWriteWave2`, `GPIBReadBinary2`, `GPIBWriteBinary2`, + `GPIBReadBinaryWave2`, `GPIBWriteBinaryWave2`), each with category `0x1060` -- exactly + `XOPOp(0x20) | ioOp(0x1000) | compilableOp(0x40)`, matching the source's `XOPOp | ioOp | + compilableOp` declaration bit-for-bit. + - `VISA64.xop`'s `XOPF` resource decoded to all 57 `vi*` functions (`viOpenDefaultRM`, `viRead`, + `viWrite`, `viClose`, ...) with full parameter-type lists; `TDM64.xop` decoded to 63 `TDM*` + functions; `SQL64.xop` to 79 `SQL*` functions; `AxonTelegraph64.xop` to 8 + `AxonTelegraph*`/`AxonTelegraphA*` functions -- all with sensible, correctly-decoded return + and parameter type codes matching each function's evident purpose (e.g. `HSTRING_TYPE` + (`0x2000`) return type on `*GetDataString`, `WAVE_TYPE`-flavored params on wave-taking + functions). +- **This fully resolves the "no simple way" conclusion above, with one caveat**: it requires + actual read access to the compiled `.xop` file's bytes (trivial for XOPs sitting in the global + or user Extensions folders on the same machine, as confirmed this session), not just a live + Igor Pro instance talking COM -- the Igor Pro Bridge itself has no channel for reading arbitrary + files' raw bytes off the host disk today. Turning this into a proper bridge tool (e.g. + `list_xop_exports(xop_path)`, using `pywin32`'s own resource APIs or bundling `pefile`) was + not yet done this session -- flagged as a natural next step, not yet actioned. +- **Cross-validated against this repo's own production XOPs (`XOPs-64bit/*.xop`), including the + exact two (`JSON-64.xop`, `ZeroMQ-64.xop`) whose loaded-function/operation lists were already + independently confirmed earlier this session via live `FunctionList`/`OperationList` calls -- + and the static extraction matched the live runtime introspection exactly, both directions**: + - `JSON-64.xop`'s `XOPC` decoded to precisely the same 13 `JSONXOP_*` operations already seen + live via `OperationList("*JSON*", ";", "external")` (`JSONXOP_AddTree`, `JSONXOP_AddValue`, + `JSONXOP_Dump`, `JSONXOP_GetArraySize`, `JSONXOP_GetKeys`, `JSONXOP_GetMaxArraySize`, + `JSONXOP_GetType`, `JSONXOP_GetValue`, `JSONXOP_New`, `JSONXOP_Parse`, `JSONXOP_Release`, + `JSONXOP_Remove`, `JSONXOP_Version`) -- same 13, no more, no fewer. + - `ZeroMQ-64.xop`'s `XOPF` decoded to precisely the same 21 `zeromq_*` functions already seen + live via `FunctionList("*ZeroMQ*", ";", "KIND:4")`. + - Also decoded every other XOP in that folder, several directly relevant to this repo's own + hardware-interface work this session: `MultiClamp700xCommander64.xop` (1 operation, + `MCC_FindServers`, plus 65 `MCC_*` functions -- the amplifier-control layer behind + `MIES_ForeignFunctionInterface.ipf`'s `FFI_*` wrappers this session added hardware tests + for), `itcXOP2-64.xop` (32 `ITC*2` DAQ-hardware operations), `SutterXOP_Win-64.xop` (2 + operations, 69 functions), `TUF-64.xop` (7 `TUFXOP_*` operations -- this repo's own test + framework support XOP), `MIESUtils-64.xop` (3 functions, `MU_GetFreeDiskSpace`/ + `MU_RunningInMainThread`/`MU_WaveModCount` -- this repo's own small utility XOP), + `mies-nwb2-compound-XOP-64.xop` (2 operations, `IPNWB_WriteCompound`/`IPNWB_ReadCompound`). + - **This means the technique isn't just a WaveMetrics-sample-XOP party trick -- it works + identically on this specific codebase's real, in-use, already-compiled dependencies**, + including ones with no external documentation at all (`MIESUtils-64.xop` appears to be + built in-house for this repo specifically). + +### Implemented as a pure Igor Pro procedure function instead of a bridge tool + +Per the user's judgment call (this would be a rarely-used capability, not worth a permanent +bridge Python tool), reimplemented the whole PE-resource extraction from scratch as ordinary +Igor Pro procedure code -- `Function/S CH_ListXOPExports(string xopPath)` plus a dozen +`static` helper functions (`CH_PEReadU16`/`CH_PEReadU32`/`CH_PEReadBytes`/`CH_PECStringLen`/ +`CH_BytesToU16`/`CH_PEReadUnicodeName`/`CH_PERVAToFileOffset`/`CH_PEFindResourceOffset`/ +`CH_ParseXOPResourceBlob`/`CH_PEListXOPResource`) -- added to `MIES_ClaudeHelper.ipf`, inside +the existing `#ifdef IGOR_PRO_BRIDGE ... #endif` block alongside `AfterCompiledHook`. Uses +only `Open/R`, `FSetPos`, `FBinRead` (with `/F=2` or `/F=3`, `/U`, `/B=3` for little-endian +unsigned 16-/32-bit reads) and ordinary string operations (`strsearch`, substring indexing, +`char2num`/`num2char`) to walk the PE header, section table, and 3-level resource directory +tree (Type -> ID -> Language) entirely by hand, find the named `"XOPC"`/`"XOPF"` resource +type's `CH_XOP_RESOURCE_ID` (1100) entry, and decode it per the binary layout documented above +`CH_ListXOPExports` in the file itself. Only supports 64-bit (PE32+) XOPs (every XOP actually +in use in this repo) -- aborts clearly for a 32-bit one rather than misreading it. Returns +`"operations:op1;op2;...\rfunctions:func1;func2;..."`. + +**Three style passes applied after the initial working implementation, each re-verified +end-to-end against `JSON-64.xop`/`ZeroMQ-64.xop` (identical output every time -- no +regressions from any of these):** +1. Lowercased the `Variable`/`String` type keywords to `variable`/`string`, and moved every + function's local-variable declarations to the very top of its body (matching Igor's own + function-level, not block-level, scoping -- see the language-facts note above), per the + user's explicit style preference. +2. Converted every function signature from the old two-part style + (`Function/S Foo(paramName)` + a separate `string paramName` line) to Igor 7+'s inline + parameter-type declarations (`Function/S Foo(string paramName)`), removing the + now-redundant standalone type lines, per the user's explicit request. +3. Replaced every unexplained numeric literal in the PE-parsing code with a named + `static Constant` -- PE/COFF structure signatures and offsets (`CH_PE_DOS_SIGNATURE`, + `CH_PE_SIGNATURE`, `CH_PE_E_LFANEW_OFFSET`, `CH_PE_OPTIONAL_HEADER_MAGIC_PE32PLUS`, the + `IMAGE_FILE_HEADER`/`IMAGE_SECTION_HEADER`/`IMAGE_RESOURCE_DIRECTORY(_ENTRY)`/ + `IMAGE_RESOURCE_DATA_ENTRY` field offsets and struct sizes, the + `CH_PE_RESOURCE_HIGH_BIT_FLAG`/`CH_PE_RESOURCE_OFFSET_MASK` name/subdirectory bit + convention), the XOP Toolkit's own `CH_XOP_RESOURCE_ID` (1100), plus a few + implementation-detail constants (`CH_PE_PAD_CHAR`, `CH_UINT16_BYTE_SIZE`, + `CH_BYTE_SHIFT_8BIT`, `CH_CSTRING_SEARCH_INITIAL_CHUNK`/`_MAX_CHUNK`, + `CH_CMPSTR_CASE_SENSITIVE`) -- per the user's explicit request. Initially added this + `static Constant` block directly above the `CH_PE*` helpers that use it; the user + subsequently moved the whole block to the top of the `#ifdef IGOR_PRO_BRIDGE` section + (before `AfterCompiledHook`) to match this repo's own convention of declaring module-level + constants at the top of a file, then ran `ipt format` (see the `tools/ipt` section above) + over the whole file to reapply canonical formatting after the manual restructuring. + +**Live-verified end-to-end, exact match against the already-validated Python reference, for +every XOP tested**: `JSON-64.xop` -> 13 operations (`JSONXOP_AddValue;JSONXOP_GetValue;...`), +0 functions; `ZeroMQ-64.xop` -> 0 operations, 21 functions (`zeromq_client_connect;...`); +`MultiClamp700xCommander64.xop` -> 1 operation (`MCC_FindServers`), 65 `MCC_*` functions; +`itcXOP2-64.xop` -> 32 `ITC*2` operations, 0 functions -- every single name, in the same +order, for all four. + +**Two real gotchas hit and resolved while testing this, both worth remembering generally, not +just for this function**: +1. **The very first test call failed with `FunctionInfo("CH_ListXOPExports")` returning an + empty string** (function not found at all), even right after a `reload_and_compile_procedures` + that reported `"compiled": true`. Root cause: the experiment's "Procedure" window + (`ProcedureText("", 0, "Procedure")`) had no `#define IGOR_PRO_BRIDGE` in it at all this + session -- so the entire `#ifdef IGOR_PRO_BRIDGE ... #endif` block in + `MIES_ClaudeHelper.ipf` compiled out silently, including the *pre-existing* + `AfterCompiledHook`, not just the newly-added function (confirmed by + `reload_and_compile_procedures`'s own `"confirmed_via"` field saying "AfterCompiledHook + counter unavailable or unchanged" -- a real, self-diagnosing signal that was almost missed). + The user added the `#define` and a subsequent `reload_and_compile_procedures` confirmed via + the `AfterCompiledHook` counter again, and the function became visible. + **Lesson: after adding new code to a `#ifdef`-gated file, don't just check `compiled: true` + -- confirm the specific new function actually exists (`FunctionInfo`/`FunctionList`) before + debugging anything else**, since a successful compile says nothing about which conditional + branches were actually included. +2. **`String result = CH_ListXOPExports(...)` and even a bare + `fprintf 0, "%s", CH_ListXOPExports(...)` both initially failed** with `"expected string + variable or string function"` / `"got ... instead of a string variable or string function + name"` -- but this turned out to be a symptom of gotcha 1 above (the function genuinely + didn't exist yet at that point), not a separate interpreted-vs-compiled restriction. Once + the `#define` was added and recompiled, the exact same `fprintf 0, "%s", + CH_ListXOPExports(...)` form worked without any change -- confirming user-defined + `Function/S` calls work perfectly normally from the command line via `_execute2`, same as + any other function call documented elsewhere in this file. + +Since `MIES_ClaudeHelper.ipf` is the never-committed, per-branch-emptied scratch file (see the +standing instruction near the top of this file), this implementation is session/branch-scoped +like everything else in it -- it will be gone after the next branch switch unless copied +somewhere permanent first, which was not requested this session. + +## Hardware test environment: MultiClamp Commander must run elevated + +Added `FFIGetCurrentClampStateWorks`/`FFIGetCurrentClampStateWorks_REENTRY` to +`Packages/tests/HardwareBasic/UTF_ForeignFunctionInterfaceWithHardware.ipf`, covering the new +`FFI_GetCurrentClampState` (part of the FFI clamp-control PR on +`feature/2559-mh_add_ffi_clamp_control`), following the exact same pattern as +`HardwareSelectionWorks`: a `DeviceNameGeneratorMD1`-driven multi-data test case that configures +one headstage via `InitDAQSettingsFromString`/`AcquireData_NG` (no actual DAQ/TP start needed -- +`_TP0_DAQ0` in the settings string), then a `_REENTRY` function that checks the active +headstage's returned clamp-state wave (`%ClampMode`, a few IC-mode dimension labels via +`IsFinite`), that an inactive headstage returns a null wave, and that an invalid headstage index +aborts (`try`/`FAIL()`/`catch CHECK_NO_RTE()`). + +First run failed immediately in `EnsureMCCIsOpen` (`REQUIRE_EQUAL_VAR(DimSize(ampMCC, ROWS), 2)` +-- found 0), before the new test's own body ever ran. Confirmed this was an environment issue, +not a bug in the new test, by running the pre-existing `StartingStoppingTestPulseWorks` (same +`AcquireData_NG` settings string, same default `s.amp = 1`) and seeing the identical failure. +Root cause (per the user): MultiClamp Commander -- the process MIES actually talks to for +amplifier control -- needs to be running **elevated** on this machine for MIES to see its +amplifier channels at all; it wasn't. Once started elevated, both tests passed. Worth checking +first (rather than assuming a test-code bug) whenever a hardware test fails specifically inside +`EnsureMCCIsOpen`/`REQUIRE_EQUAL_VAR(DimSize(ampMCC, ROWS), ...)`. + +Extended `FFIGetCurrentClampStateWorks`/`_REENTRY` to cover all three clamp modes (VC, IC, I=0), +parameterized via a new `FFI_ClampModeCases()` data generator in `Packages/tests/UTF_DataGenerators.ipf` +(one `WAVE/T` per mode: `_CM` token for `InitDAQSettingsFromString`, expected `ClampMode` value, +representative dimension labels to check `IsFinite` on). All three subcases' actual assertions +(clamp state wave contents, inactive-headstage null check, invalid-headstage abort) passed. + +## `CHECK_EMPTY_FOLDER()` false-positive on the first hardware test case run interactively + +Running any hardware test case via `RunWithOpts(testcase=...)` from the interactive command +line/bridge -- as opposed to the normal CI harness -- makes the *first* test case of that run fail +its `TestCaseEndCommon` teardown with `Assertion "CHECK_EMPTY_FOLDER()" failed`, reporting stray +root-level variables `V_enable;V_debugOnError;V_NVAR_SVAR_WAVE_Checking;V_debugOnAbort` (sometimes +also `V_Flag`/`interactiveMode`) as the folder's "contents". This looks like a test bug but isn't. + +**First theory (wrong, corrected below after a live A/B test)**: traced into `igortest`'s own +source (`Packages/igortest/procedures/igortest-debug.ipf`) and initially blamed `RunTest`/ +`RunWithOpts`'s unconditional `SetDebugger(debugMode)` setup call (which chains into +`SetIgorDebugger()` -> `GetCurrentDebuggerState()`, whose body is a bare, argument-less +`DebuggerOptions` call) for leaving `V_enable`/`V_debugOnError`/`V_debugOnAbort`/ +`V_NVAR_SVAR_WAVE_Checking` in `root:`. This didn't actually explain why the user's own manual +Igor-command-line usage never sees this residue, since that same igortest setup chain runs +identically either way. + +**Corrected root cause**, found by a controlled A/B test (clean `root:`, running the identical +`RunWithOpts(testcase=...)` call two different ways): the residue comes from the **Igor Pro +Bridge's own `execute_igor_command_unattended` tool**, not from anything in igortest. That tool +documents itself as disabling the Debugger for the duration of the call and restoring it after -- +which means it issues its own `DebuggerOptions enable=0` call *immediately before* running the +user's actual command. That bare-argument-style `DebuggerOptions` invocation is exactly what +leaves the four/six stray variables in whatever data folder is current (`root:`, for an +interactive/bridge call) -- confirmed live: identical `RunWithOpts(...)` calls, from a freshly +cleaned `root:`, **failed** via `execute_igor_command_unattended` and **passed** ("Finished with no +errors") via the plain `execute_igor_command` (with the Debugger separately confirmed off +beforehand via `get_debugger_state()`), reproduced for `FFIGetCurrentClampStateWorks`, +`HardwareSelectionWorks`, and other test cases across the session. Igor's own `DebuggerOptions` +operation, called without arguments (or to set a value), sets those output variables **in whatever +the current data folder is at that moment** -- so this is a genuine, confirmed side effect of the +bridge's implementation, not of igortest or of any test's own code. + +Only the first test case in a run shows the `CHECK_EMPTY_FOLDER()` failure; subsequent +subcases/test cases in the same run pass clean (IUTF appears to only count/report the first +occurrence of an identical failure message per run -- `"Failed with 1 errors"` regardless of how +many subcases hit it). + +**Workaround used for the remainder of the affected part of this session (no longer needed as of +bridge v1.23.0, see below)**: use plain `execute_igor_command` (never `_unattended`) for +`RunWithOpts(...)` test invocations, after confirming the Debugger is off via +`get_debugger_state()`, and run `KillVariables/Z root:V_Flag, root:V_enable, root:V_debugOnError, +root:V_debugOnAbort, root:V_NVAR_SVAR_WAVE_Checking, root:interactiveMode` before/after each run +for hygiene (harmless if some don't exist, since `/Z` suppresses the error). This avoided the +false positive entirely rather than just tolerating/ignoring it, while the bridge itself still +had the bug. + +**Fixed properly in Igor Pro Bridge v1.23.0** -- see the "Igor Pro Bridge v1.23.0" section below +for the implementation, and the live confirmation that `RunWithOpts(testcase= +"HardwareSelectionWorks")` run through `execute_igor_command_unattended` now finishes with +`"Finished with no errors"`, no `CHECK_EMPTY_FOLDER()` failure. **The `execute_igor_command` +-only workaround above is no longer necessary** -- `execute_igor_command_unattended` is safe to +use directly for test runs again. + +## Igor Pro Bridge v1.23.0: stray debugger-globals fix + `close_data_browser` removal + +Two changes made on branch `feature/2754-add-basic-igor-pro-mcp-server` (`tools/igor-mcp-bridge/` +in this repo, a separate codebase from the MIES procedures themselves, packaged as its own +`.mcpb` Claude Desktop extension) after the corrected `CHECK_EMPTY_FOLDER()` root-cause analysis +above pinned the actual bug on this bridge's own code. + +**Fix**: every tool that touches Igor's Debugger settings funnels through exactly two shared +helpers in `server.py` -- `_read_debugger_options()` (a read-only query, used by +`get_debugger_state`, `set_debugger_enabled`, `restore_debugger_settings`, +`execute_igor_command_unattended`, `load_experiment`, `get_environment_summary`) and +`_apply_debugger_options(state)` (the write/set command, used by +`execute_igor_command_unattended`, `load_experiment`, `set_debugger_enabled`, +`restore_debugger_settings`). Both build an Igor command string containing a bare/argument +`DebuggerOptions` invocation -- confirmed (see the `CHECK_EMPTY_FOLDER()` section above) that +this operation *always* creates `V_enable`/`V_debugOnError`/`V_debugOnAbort`/ +`V_NVAR_SVAR_WAVE_Checking` as output variables in whatever data folder is current, purely as a +side effect of being called, regardless of arguments. Fix: both helpers now append +`; KillVariables/Z V_enable, V_debugOnError, V_debugOnAbort, V_NVAR_SVAR_WAVE_Checking` onto the +*same* command string (one Execute2 round-trip, not a separate call) -- for the read-only query, +this runs after the `fprintf` that captures the values into `results`, so nothing is lost by +cleaning up immediately. Fixing it at this shared-helper level (rather than only inside +`execute_igor_command_unattended`, as the original TODO was phrased) means every one of the six +tools listed above gets the fix, not just one. + +**`close_data_browser` removed** (per explicit user request, no longer wanted): deleted the tool +function from `server.py` and its entry from `Packages/doc/igor-pro-bridge.rst`. It was a +precautionary tool (close Igor's built-in Data Browser before a reload/compile cycle, added +after Igor Pro was reported to sometimes crash with one open) that turned out to not be needed +in practice. + +**Packaged as v1.23.0 using the official `mcpb` CLI** (`@anthropic-ai/mcpb`, available via +`npx mcpb`), replacing an earlier ad hoc/undocumented packaging process -- no build script or +`manifest.json` existed anywhere in this repo before this session; both were reconstructed by +unpacking the prior `igor-pro-bridge-1.22.0.mcpb` (a plain zip of `manifest.json` + +`pyproject.toml` + `src/server.py`) with `unzip`, editing in place, and repacking with +`npx mcpb pack . .mcpb`. Kept `_BRIDGE_VERSION` (`server.py`), `manifest.json`'s +`"version"`, and `pyproject.toml`'s `version` all in sync at `1.23.0` (found and fixed a +pre-existing drift while at it: `pyproject.toml` had been stuck at `"1.19.0"` since at least +v1.22.0). Also found and fixed a pre-existing gap in `manifest.json`'s `"tools"` array: the +`get_bridge_version` tool has existed in `server.py` since v1.22.0 but was never actually listed +there -- added it. `npx mcpb validate manifest.json` and a `python -m py_compile` of the packaged +`src/server.py` both passed before delivering the bundle. **Gotcha**: the first `npx mcpb pack` +attempt swept a stray `src/__pycache__/*.pyc` (left over from an earlier local `py_compile` check +in the same build directory) into the archive -- deleted it and repacked; always check `npx mcpb +pack`'s own "Archive Contents" listing for anything unexpected like this before shipping. + +**Live-verified after the user installed the new build and restarted Claude Desktop**: +`get_bridge_version()` confirmed `{"version": "1.23.0"}` actually loaded; `check_bridge_health()` +returned `"status": "OK"`. Confirmed the fix directly: ran a trivial +`execute_igor_command_unattended('print "hello from unattended"')` from a freshly-cleaned `root:` +and checked `VariableList("*", ";", 4)` immediately after -- empty, no stray globals, where the +old (pre-fix) bridge would have left the same four variables behind on every such call. Then ran +the actual regression case: `RunWithOpts(testcase="HardwareSelectionWorks")` via +`execute_igor_command_unattended` (the exact call shape that reliably failed with +`CHECK_EMPTY_FOLDER()` before the fix) -- result: `"Finished with no errors"` / +`"Test finished with no errors"`, no assertion failure. Confirms the fix is real and the +`execute_igor_command`-only workaround from the section above can be retired. + +## `ListBoxSelectAll` test coverage added, live-verified + +`MIES_Utilities_GUI.ipf`'s `ListBoxSelectAll(WAVE selWave)` had no test coverage. Added +`TestListBoxSelectAll` and `TestListBoxSelectAllOnPlainSelectionWave` to +`Packages/tests/Basic/UTF_Utils_GUI.ipf`, designed from actually reading the function body +(`selWave[][0][0] = selWave[p][0][0] | LISTBOX_SELECT_OR_SHIFT_SELECTION`) plus corroborating +evidence from Igor's own `Igor Reference.ihf` `ListBox` operation docs (`selWave` is "a +numeric wave with the same dimensions as listWave," bit 0 = selected, "additional dimensions +are used for color info," "in modes 3 and 4 bit 0 is set only in column zero") and from how +MIES itself builds a real selWave (`GetAnalysisBrowserGUIFolderSelection`: `Make/N=(1,1,3)`, +layer 0 = selection, layers 1/2 dim-labeled `foreColors`/`backColors`) -- so the first test's +3-layer shape mirrors production usage rather than being an arbitrary shape. + +- **Live-tested via the Igor Pro Bridge** (launched Igor Pro 9 nightly with + `launch_igor_pro_unattended`, loaded `Basic.pxp`, ran `RunWithOpts(testsuite= + "UTF_Utils_GUI")`). First run caught a real bug in the second test, not in + `ListBoxSelectAll` itself: `TestListBoxSelectAllOnPlainSelectionWave` built `selWave` as + `Make/FREE/N=(numRows, 1)` (2D) but `expected` as `Make/FREE/N=(numRows)` (1D) -- + `CHECK_EQUAL_WAVES` failed on `DIMENSION_SIZES`/`DIMENSION_LABELS` even though the actual + data values matched. Fixed by making `expected` explicitly `(numRows, 1)` too. Re-ran after + fixing and reloading/recompiling: **"Test finished with no errors."** +- **`RunWithOpts` also accepts a single `testcase=` name** (in addition to `testsuite=`), to + run one specific test function without the rest of its suite -- confirmed live: `RunWithOpts + (testcase="TestListBoxSelectAll")` ran only that one case ("Entering test case + \"TestListBoxSelectAll\"" / "Finished with no errors"), still reporting "Entering test suite + \"UTF_Utils_GUI.ipf\"" around it (the suite file is still scanned to locate the named case, + but only that case actually runs). Useful for iterating on a single new/failing test without + re-running an entire suite. +- **Fuller option reference for `RunWithOpts`**, read directly from its own source + (`Packages/tests/UTF_HelperFunctions.ipf`) and the underlying `RunTest` it calls + (`Packages/igortest/procedures/igortest-basics.ipf` -- this experiment's compiled + environment uses the `igortest` framework, not the older, also-present `unit-testing` + package; confirmed by `included_procedure_files` listing `igortest-basics.ipf` but not + `unit-testing-basics.ipf`). `RunWithOpts` is a thin MIES-specific wrapper: `testsuite` + defaults to `GetDefaultTestSuitesForExperiment()` if omitted, `traceWinList` defaults to + `"MIES_.*\.ipf"` (only used if `instru=1`), and it otherwise forwards straight to `RunTest`. + Named parameters, all optional: + - `testsuite` -- semicolon-separated list of procedure files to treat as test suites (e.g. + `"UTF_Utils_GUI"` -- `RunWithOpts` appends `.ipf` automatically unless `enableRegExp=1`). + Defaults to this experiment's full default suite list if omitted entirely. + - `testcase` -- semicolon-separated list of test-case function names to run within + `testsuite` (default: all). Confirmed live above for a single name. + - `enableRegExp` -- when `1`, both `testsuite` and `testcase` are matched as (anchored, + case-insensitive) regular expressions instead of literal/list names, and `testsuite` is + matched against the full file name **including** `.ipf` (confirmed live: `testsuite= + "UTF_Utils_GUI"` with `enableRegExp=1` failed with "A procedure window matching the + pattern \"^(?i)UTF_Utils_GUI$\" could not be found" -- needed `testsuite= + "UTF_Utils_GUI.ipf"`). Combining both let one `RunWithOpts(testsuite="UTF_Utils_GUI.ipf", + testcase="TestListBoxSelectAll.*", enableRegExp=1)` call run both new `ListBoxSelectAll` + tests together without the rest of the suite or a semicolon-joined exact-name list -- + confirmed live, both passed. + - `allowDebug` -- leave Igor's Debugger in whatever state it's already in for the run + (normally overridden off); ignored if `debugMode` is also given. Not relevant to this + bridge's own calls, since `execute_igor_command_unattended`/`load_experiment` already + force the Debugger off for the duration of the call regardless. + - `instru` -- turns on execution tracing/coverage instrumentation (RTF + optionally + Cobertura output) over `traceWinList` (defaults to all `MIES_*.ipf` files); off by + default. Unrelated to pass/fail reporting -- a coverage feature, not needed just to + check correctness. + - `ITCXOP2Debug` -- hardware (ITC) XOP debug mode passthrough via `HW_ITC_DebugMode`; not + relevant without real DAQ hardware attached. + - `keepDataFolder` -- don't clean up each test case's temporary data folder afterward, to + allow inspecting produced data by hand; off by default. + - `enableJU` -- write a JUnit-compatible XML report at the end; defaults to on only when + `IsRunningInCI()` is true, off in an interactive/bridge-driven run like this session's. + - All of the above are also documented with more nuance directly on `RunTest` itself + (`igortest-basics.ipf` around line 1490), including two options `RunWithOpts` doesn't + expose at all: `shuffle` (randomize suite/test-case execution order, useful for catching + order-dependent test bugs) and `retry`/`retryMaxCount` (rerun flaky tests tagged + `IUTF_RETRY_FAILED` up to N times) -- call `RunTest` directly instead of `RunWithOpts` if + either of those is needed. +- **Note on `TestRemoveAllColumnsFromTable`'s console output**: this pre-existing, + unmodified test deliberately prints two `"!!! Assertion FAILED !!!"` lines (from + `RemoveAllColumnsFromTable`'s own internal `ASSERT` firing inside a `try/catch` the test + sets up on purpose, to confirm the function rejects a non-table window) -- this is expected + output, not a real failure, and correctly does not appear in the suite's final failure list + (consistent with the fail-path-test convention already noted elsewhere in this file for + UTF test suites generally). +- `ipt check`/`ipt lint` were run against the edited test file both before and after the fix + (per the new standing `ipt` rule above) and reported zero errors/warnings each time -- a + reminder that a clean `ipt` parse does not guarantee the test's *assertions* are actually + correct (that dimension-mismatch bug parsed and linted cleanly); only the live Igor Pro run + caught it. + +## Git note + +`.git/packed-refs` was observed truncated (trailing NUL bytes, "unterminated line" error +blocking all git commands) partway through an earlier session. **Resolved/non-issue as of +this session**: `git status`/`git log` ran cleanly (checked while investigating whether +this session's local fixes had reached the PR branch), confirming it was a transient +artifact of the folder mount rather than lasting repo damage. + +## FFI hardware test coverage: remaining write functions (branch `feature/2559-mh_add_ffi_clamp_control`) + +Added tests for every remaining `FFI_Set*`/`FFI_TriggerAutoClampControl` write function in +`Packages/tests/HardwareBasic/UTF_ForeignFunctionInterfaceWithHardware.ipf`, each following the +established setup/`_REENTRY` two-function pattern (`InitDAQSettingsFromString`/`AcquireData_NG` in +setup, assertions in `_REENTRY`), all live-verified via the bridge ("Finished with no errors" for +each), in order added: + +- **`FFIGetClampStateWorks`/`_REENTRY`**: covers `FFI_GetClampState` (the *unfiltered* clamp state, + containing both VC and IC fields regardless of active mode -- unlike `FFI_GetCurrentClampState`, + which is filtered to the active mode only), plus a `GetWaveDimensionality(clampState) == ROWS` + check confirming the returned wave is 1D (`GetWaveDimensionality`, `MIES_Utilities_WaveHandling.ipf` + line 142, returns the highest dimension index with `DimSize > 1`, or `ROWS` if none -- the + established MIES idiom for a 1D-wave assertion). +- **`FFISetGetHeadstageActiveWorks`/`_REENTRY`**: `FFI_SetHeadstageActive`/`FFI_GetHeadstageActive` + round-trip on HS1 (enable, verify, disable, verify), plus invalid-headstage abort for both. +- **`FFISetClampModeWorks`/`_REENTRY`**: cycles HS0 through VC -> I=0 -> back to IC via + `FFI_SetClampMode`, verifying `FFI_GetCurrentClampState(...)[%ClampMode]` after each; invalid + clamp mode (`-1`) and invalid headstage both abort. Refactored (user request) to introduce a + `variable headstage = 0` local instead of repeating the literal `0` at every call site. +- **`FFISetHoldingPotentialWorks`/`_REENTRY`**: sets/verifies/disables a VC holding potential + (`CHECK_CLOSE_VAR` for the float value, `tol = 1e-6`); NaN potential aborts; calling it while not + in VC aborts (`"Attempt to set holding potential but current clamp mode is not VC !"`); invalid + headstage aborts. Also given the `headstage` variable refactor. +- **`FFISetBiasCurrentWorks`/`_REENTRY`**: same shape as holding-potential, for IC/bias current + (`"Attempt to set bias current but current clamp mode is not IC !"`). +- **`FFISetAutoBiasWorks`/`_REENTRY`**: same shape again, for `FFI_SetAutoBias`'s target + potential/enable. Clamp-state field names for this one are **`AutoBiasVcom`**/`AutoBiasEnable` + (not e.g. "AutoBiasPotential") -- confirmed from `AI_MapFunctionConstantToName`'s + `MCC_NO_AUTOBIAS_V_FUNC`/`MCC_NO_AUTOBIAS_ENABLE_FUNC` cases in `MIES_AmplifierInteraction.ipf`. + Note: unlike `FFI_SetHoldingPotential`/`FFI_SetBiasCurrent`, `FFI_SetAutoBias`'s source has **no + `IsNaN` guard** on its `potential` argument -- so this test intentionally has no NaN-abort case + (there is nothing to assert there); the positive-path enable/disable/verify assertions cover it + instead. +- **`FFITriggerAutoClampControlWorks`/`_REENTRY`**: exercises all three `FFI_TriggerAutoClampControl` + auto-control kinds -- auto pipette offset (works in either clamp mode), auto bridge balance + (IC-only, `"MCC_AUTOBRIDGEBALANCE_FUNC works only in IC clampMode"` if not), auto capacitance + (VC-only, `"MCC_AUTOWHOLECELLCOMP_FUNC works only in VC clampMode"` if not) -- plus an unknown + auto-control value (`"Unknown auto clamp control"`, via `FATAL_ERROR`) and the usual invalid + -headstage abort. `AUTO_PIPETTE`/`AUTO_CAPACITANCE`/`AUTO_BRIDGEBALANCE` are `static Constant`s + private to `MIES_ForeignFunctionInterface.ipf` (values `1`/`2`/`3`), so the test uses the numeric + literals directly with an explanatory comment rather than referencing the (inaccessible-from-here) + named constants. + +**Gotcha hit and fixed once (`FFISetHoldingPotentialWorks`)**: an early version tried to force HS1 +into IC (to test the "wrong mode" abort path) via `FFI_SetClampMode(device, 1, I_CLAMP_MODE)`. This +doesn't abort -- it just prints `"(Dev1) Could not switch the clamp mode to I_CLAMP_MODE as no DA +and/or AD channels are associated with headstage 1."` and returns normally, since HS1 has no DA/AD +channels associated in this suite's standard single-headstage setup (only HS0 does) -- +`DAP_SetClampMode` requires associated channels to actually perform the switch and just logs and +returns otherwise, it does not `ASSERT`/abort. This made the subsequent `try +FFI_SetHoldingPotential(device, 1, 0, 1); FAIL()` block spuriously fail, since HS1's mode never +actually changed. **Fix, and standing pattern for every later "wrong mode" test in this group**: use +HS0 itself for the mode-mismatch check (switch HS0 to the wrong mode via `FFI_SetClampMode`, which +does work on HS0, run the abort-expecting `try`/`catch`, then switch HS0 back) -- never rely on HS1 +for anything that requires an actual clamp-mode change. + +## Rebase verification (`feature/2559-mh_add_ffi_clamp_control` onto latest `main`) + +After the user rebased this branch onto the latest `origin/main`, verified the rebase was resolved +correctly rather than just trusting a clean `git status`: + +- No conflict markers (`<<<<<<<`/`>>>>>>>`) anywhere in the repo; no `.git/rebase-merge`/ + `rebase-apply` in progress -- the rebase had genuinely completed. +- `git merge-base HEAD origin/main` equals `origin/main`'s own tip exactly, confirming the 3 + feature commits (`DAP: Refactor...`, `AI: Add range check...`, `FFI: Add functions for clamp and + headstage control`) sit directly on latest `main` with nothing missing or duplicated. +- `git diff --stat ..origin/main` showed upstream only touched + `MIES_SweepFormula_Parser.ipf` and its tests/docs in the interim -- completely disjoint from + every file this branch touches (`MIES_DAEphys.ipf`, `MIES_AmplifierInteraction.ipf`, + `MIES_ForeignFunctionInterface.ipf`, the FFI hardware test file, `UTF_DataGenerators.ipf`). So + there was essentially no real content to conflict on in the first place. +- `DAP_SetClampMode` (`MIES_DAEphys.ipf`) picked up an added `AI_AssertOnInvalidClampMode(mode)` + call at its top -- redundant with (but harmless alongside) `FFI_SetClampMode`'s own + `ASSERT(AI_IsValidClampMode(...))` check one level up; confirmed harmless since the FFI-level + assert still fires first with its own more specific message ("Invalid clamp mode: -1"), verified + by the still-passing `FFISetClampModeWorks` test. +- Working tree was clean for every FFI-related file (no diff vs. `HEAD`) -- all 16 FFI test + functions this session added were confirmed present and byte-identical to what had been tested. +- **Transient, self-resolved compile error observed mid-verification, not a real problem**: one + `read_session_history()` dump showed `UTF_ForeignFunctionInterfaceWithHardware.ipf:35:8: error: + No such structure exists.` sandwiched between two full-suite runs that both completed with + "Finished with no errors." `check_compilation_state()` immediately after reported clean, and two + subsequent full-suite runs both passed cleanly -- the error did not recur and was very likely a + transient artifact of file-on-disk churn during the rebase (e.g. Igor's file-watcher catching a + momentarily-inconsistent file state), not a lasting defect. Lesson: don't treat one compile-error + line found in a long, cumulative history dump as necessarily current -- corroborate with a fresh + `check_compilation_state()` and/or another clean run before concluding something is actually + broken. +- Two pieces of uncommitted, unrelated-to-the-rebase local state, initially just flagged for the + user -- **now clarified by the user as permanent, intentional, and never to be committed**: + - `Packages/tests/Basic/UTF_Basic_Includes.ipf`'s commented-out `example-stimulus-set-api` + include: the file is only present in CI, not locally, so this line must stay commented out + for local test runs to work at all -- but must equally never be committed that way, since CI + needs it active. Leave this uncommitted local diff alone; don't "fix" it by committing either + state. + - `Packages/MIES_Include.ipf`'s `#include "MIES_ClaudeScrapCode"` line and + `Packages/MIES/MIES_ClaudeScrapCode.ipf` itself: **never commit**, purely a scratch file used + only during interactive Claude Desktop sessions (see the dedicated section below for the + required per-branch-switch cleanup step this implies). +- Practical technique for reading a very large `read_session_history()` dump without exceeding the + context window: save it to a file, convert the JSON-escaped `\r` sequences to real newlines + (`sed 's/\\r/\n/g'`, **not** `tr '\r' '\n'` -- the saved file contains the literal two-character + escape sequence, not an actual carriage-return byte, since it's JSON text written verbatim), then + `grep -n` for just the markers of interest (`error:`, `Finished with no errors`, `RunWithOpts(`) + instead of reading the whole thing. + +## `tools/check-code.sh`'s "trailing semicolon" check has a comment-detection blind spot + +The check (`git grep --perl-regexp '^[[:space:]]*[^\/].*;$' ...`) is meant to flag lines of actual +Igor code with a stray trailing `;` (Igor doesn't need semicolons to terminate statements, only to +separate multiple statements on one line), while skipping `//`-comment lines via the `[^\/]` +right after the leading whitespace. **This comment-exclusion doesn't reliably work for indented +comments**: `[^\/]` matches any single character that isn't a literal `/`, including whitespace +itself. Since `[[:space:]]*` is greedy but backtracks on failure, the regex engine can satisfy +`[^\/]` by consuming the leading tab/space instead of requiring `[[:space:]]*` to do it -- so an +indented `//` comment line still matches the overall pattern as long as it happens to end in a +literal `;`. Net effect: only comments with *zero* leading indentation are reliably excluded; +virtually every real comment in this codebase is indented and so is not actually protected by this +guard. + +Hit this for real: three new comments in `UTF_ForeignFunctionInterfaceWithHardware.ipf` used a +semicolon as a prose separator ("... aborts; use HS0 itself for this ...", written as two +sentences split across lines with the first ending in `;`) and were flagged as "trailing +semicolon" hits even though there was no actual code semicolon anywhere nearby -- confirmed by +inspecting the flagged lines directly, all three were `//`-comments. Fixed by rewording (no longer +ending any comment in `;`); `tools/check-code.sh` reported no more trailing-semicolon failures +afterward. + +**Standing rule (user's instruction) going forward**: never end a comment with a semicolon. +`tools/check-code.sh` runs as part of this repo's pre-push git hook, and a "trailing semicolon" +hit blocks `git push` outright -- so this isn't just a style nit, it's a hard gate. Prefer a +period, comma, or just restructuring the sentence instead of `;` at a comment's line-end. + +## Igor Pro Bridge v1.25.0: pinned `install.ps1`/`requirements.txt`, and the MCP Python SDK's +## breaking v1 -> v2 transition + +**Dated finding, confirmed via web search this session (2026-08-03) and by directly inspecting +both wheels' contents**: the MCP Python SDK's v2 line (`mcp` on PyPI) went stable at `2.0.0`, +released 2026-07-27/28 alongside MCP protocol revision `2026-07-28` -- a deliberate breaking +rework, not a routine minor bump. Most relevant to this bridge: `FastMCP` was renamed to +`MCPServer` and moved from `mcp.server.fastmcp` to `mcp.server.mcpserver`. `server.py` still +uses the v1 API (`from mcp.server.fastmcp import FastMCP`), confirmed still present by +unzipping the `mcp==1.29.0` wheel directly (`mcp/server/fastmcp/server.py` exists; the `2.0.0` +wheel does not have that path at all). **This means the bridge's previous unpinned dependency +spec (`mcp>=1.0.0` in `pyproject.toml`, and the manifest's own documented `pip install mcp +pywin32` instruction) was a live, undiscovered bug as of this finding**: running either command +today resolves to `2.0.0` and breaks the bridge outright with a `ModuleNotFoundError` on +import, with no warning beforehand. Fixed by pinning `mcp==1.29.0` (the last 1.x release, +confirmed via `pip index versions mcp`) in a new `requirements.txt`, and tightening +`pyproject.toml`'s spec to `mcp>=1.29.0,<2` so a future `pip install -e .`-style install can't +silently repeat the same mistake. `pywin32` pinned to `312` (confirmed latest via web search) +in the same file for the same "don't drift silently" reason, even though it wasn't at similar +risk of a breaking rename. + +**The user's request that prompted this**: an installation script for the bridge that (a) +installs pinned versions from a `requirements.txt`, (b) installs specifically into the Python +environment Claude Desktop itself uses when run elevated -- explicitly *not* assumed to be the +same as whatever Python an elevated console resolves by default -- and (c) runs pywin32's +required post-install step. Delivered as `tools/igor-mcp-bridge/install.ps1`. + +**Design rationale for (b), the "which Python does Claude Desktop actually use" problem**: +Claude Desktop's `manifest.json` invokes the bridge as the bare command `"python"`, resolved by +Claude Desktop's own (elevated) process via whatever `PATH` its environment has at launch time. +This is a different resolution mechanism than an interactive elevated PowerShell/cmd session, +which can have extra `PATH` entries injected only for that session (a PowerShell profile +script activating a conda environment, pyenv-win shims, etc.) that a plain elevated GUI-app +launch never picks up -- so trusting an elevated console's own `$env:Path` to decide where to +`pip install` can silently target the wrong interpreter entirely. There's also a known Windows +elevation-specific quirk with per-user Microsoft Store "app execution alias" stubs (a +placeholder `python.exe` that just opens the Store) behaving differently once elevated. +`install.ps1` avoids all of this by reading `[Environment]::GetEnvironmentVariable('Path', +'Machine')` and `...('Path', 'User')` directly (the same two registry-backed sources, in the +same order, Windows composes into a freshly created process's environment block) instead of +the invoking shell's own `$env:Path`, and explicitly rejects a Microsoft Store app-execution- +alias stub found that way (detected by path containing `\WindowsApps\` and a small file size). +An `-PythonPath` parameter bypasses this resolution entirely for a known-correct interpreter. + +**Because auto-resolution is still a best-effort guess, not a guarantee**, also added ground- +truth verification: `get_bridge_version()` now additionally reports `python_executable` +(`sys.executable`), `python_version`, `mcp_package_version`, and `pywin32_build` (all via +`importlib.metadata.version(...)`, since the `mcp` package itself has no `__version__` +attribute -- confirmed by inspecting its `__init__.py`). The documented workflow: run +`install.ps1`, restart Claude Desktop (elevated), then call `get_bridge_version()` and confirm +`python_executable` matches what `install.ps1` installed into; if not, re-run `install.ps1 +-PythonPath `. This closes the loop with an authoritative answer from inside the +actual process Claude Desktop launched, rather than relying on `install.ps1`'s guess alone. + +Repackaged as `igor-pro-bridge-1.25.0.mcpb` (bumped from 1.24.0), now including +`requirements.txt` and `install.ps1` alongside `server.py` in the bundle. Also bumped +`requires-python`/the manifest's `compatibility.runtimes.python` from `>=3.9` to `>=3.10` to +match `mcp==1.29.0`'s own floor (confirmed via that wheel's `METADATA`: `Requires-Python: +>=3.10`). Verified: `python3 -m py_compile` on `server.py` inside the repacked bundle, and a +byte-for-byte diff against the repo's own copy, both clean. Could not test-run `install.ps1` +itself end-to-end (no Windows/PowerShell available in this session's sandbox; downloading a +portable `pwsh` build failed -- GitHub's release-asset CDN host was unreachable from here, +unlike `github.com` itself) -- reviewed by hand instead (brace/paren/bracket/here-string +balance checked programmatically, backtick-escape usage traced line by line). **Should be +smoke-tested on a real elevated Windows session before being treated as fully proven.** + +## Igor Pro Bridge `requirements.txt`: added pip hash-pinning (no version bump, per user request) + +Follow-up to the above: the user asked to additionally pin every package in `requirements.txt` +to its cryptographic hash, explicitly **without** bumping the bridge version or repackaging the +`.mcpb` -- so this only touched `tools/igor-mcp-bridge/requirements.txt` and `install.ps1` +(added `--require-hashes` to the latter's `pip install` call for a clear failure instead of a +silent unverified install if the hashes are ever accidentally stripped later). **The already- +packaged `igor-pro-bridge-1.25.0.mcpb` still contains the old, unhashed `requirements.txt`** -- +that's a deliberate consequence of not repackaging, not an oversight; only the working-tree copy +(which is what `install.ps1` actually reads via `$PSScriptRoot`, whether run from the repo or +copied out of an already-installed extension folder) is updated. Bundling the hashed version +requires a future repackage. + +**Why this is more involved than pinning just the two direct dependencies**: pip's hash-checking +mode (triggered automatically the instant any requirement has a `--hash`) requires **every** +package that would actually be installed -- not just the top-level ones -- to be pinned to an +exact version with a hash, including the entire transitive dependency tree. Since `mcp==1.29.0` +alone pulls in roughly two dozen packages (`anyio`, `httpx`/`httpcore`/`h11`, `pydantic`/ +`pydantic-core`, `jsonschema` and its own tree, `pyjwt`+`cryptography`+`cffi`+`pycparser`, +`starlette`/`sse-starlette`/`uvicorn`, etc.), the whole tree had to be resolved and hashed, not +just `mcp`/`pywin32` themselves. + +**Resolving a Windows dependency tree from this session's Linux sandbox**: used +`pip download --platform win_amd64 --python-version --implementation cp --abi cp +--only-binary=:all: -r requirements.in -d

`, which lets pip's real resolver fetch metadata +and wheels for a *different* target platform/Python version than the host is actually running, +entirely through wheel-tag matching (no code execution/building needed, since every package in +this tree ships wheels for Windows). Ran this once per Python version (310/311/312/313, the +range this bridge's `pyproject.toml` currently declares, `>=3.10`) to catch any per-version +divergence, then `sha256sum` on every downloaded wheel file directly (not via PyPI's JSON API -- +see the gotcha below) to get the hash values themselves. + +**Real, non-obvious finding surfaced by doing this per-Python-version rather than just once**: +`rpds-py` (a transitive dependency of `jsonschema`/`referencing`) resolved to a **different +version**, not just a different wheel file, depending on target Python version -- `0.30.0` for +Python 3.10, `2026.6.3` for 3.11+ -- because the current `rpds-py` release has dropped Python +3.10 support entirely (no cp310 wheel published for it), so pip's resolver falls back to the +newest version that still has one. This is a real version-vs-version divergence, not just a +platform/ABI-tag difference (contrast with `cryptography`, which stays at one version, `50.0.0`, +but ships two different abi3 wheels -- `cp39-abi3` and `cp311-abi3` -- covering the same version +across the whole 3.10-3.13 range with two hashes on one pinned line). Handled by splitting +`rpds-py` into two separate pinned+hashed lines gated by `python_version < '3.11'` / +`>= '3.11'` markers in the requirements file -- valid, standard pip syntax, confirmed working +(see verification below). + +**Gotcha hit and worth remembering generally**: plain `curl`/Python `urllib` requests to +`pypi.org`'s JSON API (`https://pypi.org/pypi///json`) failed outright from this +sandbox (TLS handshake reset partway through) even though `pip download` itself worked fine -- +this environment's outbound network evidently only allow-lists pip's own configured index +traffic, not arbitrary HTTPS to `pypi.org`. Worked around it by computing `sha256sum` directly on +the wheel files `pip download` had already fetched, which is equally correct (identical bytes, +identical hash) and doesn't depend on being able to query PyPI's API directly. + +**Second gotcha, more subtle and specifically relevant to how this was verified**: pip's +`--platform`/`--python-version`/`--implementation`/`--abi` flags (as used with `download`) +**only affect wheel-tag matching, not PEP 508 environment-marker evaluation** -- markers like +`python_version` and `sys_platform` are always evaluated against the *actual, real* running +interpreter, never the target flags. First noticed when a `pip download --require-hashes` +verification pass targeting `cp312` (from this sandbox's real Python 3.10) tried to install +`rpds-py==0.30.0` (the marker-selected, `python_version < '3.11'` line -- true for the *real* +host interpreter, 3.10) while simultaneously asking for a `cp312`-tagged wheel of it -- a +self-inflicted, impossible-in-real-life combination (a real Python 3.12 install would never +evaluate that marker true) that only arises from mixing this sandbox's real 3.10 interpreter +with cross-platform *download* target overrides. **This is a testing-methodology artifact only, +not a bug in the requirements.txt itself** -- on a real target machine, there is no +cross-compilation happening at all; pip runs natively on the real interpreter, so markers and +wheel-tag selection are automatically consistent. Verified correctly instead by: (1) a full, +un-confounded `pip download --require-hashes` round-trip for the complete dependency tree, +restricted to `--python-version 310 --abi cp310` (an exact match for this sandbox's real +interpreter -- zero host/target mismatch), which succeeded end-to-end with zero hash or +"missing pin" errors across all ~30 packages; (2) isolated single-package hash checks (each +package alone in a throwaway requirements file, no markers) for `pywin32` (cp310), `rpds-py` +(cp311, the *other* branch, confirming its hash independently of marker evaluation), and a +`pydantic-core` cp313 wheel. All passed. The cp311/cp312/cp313-specific wheel hashes for +`pydantic-core`/`cffi`/`cryptography`/`pywin32` that couldn't be round-tripped this way (no +3.11+ interpreter available in this sandbox, and installing one would have required root/apt +access this sandbox doesn't have) were still computed via direct `sha256sum` on the actual +downloaded wheel bytes, which is the authoritative hash regardless of pip's marker-evaluation +quirks -- just not independently re-verified through pip's own hash checker for those specific +combinations. **Should still be smoke-tested with a real `pip install --require-hashes -r +requirements.txt` on an actual elevated Windows machine with each supported Python version, the +same caveat as install.ps1 itself above.** + +**That real-machine smoke test happened, and it failed exactly the way the caveat above +predicted it could.** The user ran `install.ps1` for real (elevated PowerShell): auto-resolution +correctly found their actual Python (`C:\Users\enigm\AppData\Local\Programs\Python\Python314\ +python.exe`, i.e. **Python 3.14.0**), pip upgraded cleanly, then failed with +`THESE PACKAGES DO NOT MATCH THE HASHES FROM THE REQUIREMENTS FILE` on `pywin32==312` -- +because this session's resolution pass had only covered cp310/311/312/313, never cp314, so +there was no hash for it at all yet Python 3.14 exists and was in real, current use. Exactly the +gap called out in the requirements.txt regeneration comment: `pyproject.toml` declares an +open-ended `>=3.10` floor, so a newly released Python version can reach real users before its +wheels are covered here -- this isn't a hypothetical, it happened on the very first real test. + +Fixed by re-running the same `pip download --platform win_amd64 --python-version 314 +--implementation cp --abi cp314 --only-binary=:all:` resolution pass and adding the resulting +hashes for the four version-specific compiled packages needing them (`pywin32`, `pydantic-core`, +`cffi`, `rpds-py`'s 2026.6.3 branch) to the existing lines -- confirmed all four packages still +resolve to the exact same *versions* already pinned (no new divergence beyond the pre-existing +rpds-py 3.10 split), `cryptography`'s existing `cp311-abi3` wheel already covers 3.14 (stable +ABI, no new file needed, confirmed no new cryptography download occurred for the cp314 target). +The `pywin32==312` cp314 hash added +(`a4dd3a848290ef724347b19f301045831d8e802fa4464f491b98b1e0a081432e`) was cross-checked against +the exact "Got" value in the user's own error message -- an exact match, confirming both that +the file pip fetched really is the genuine, untampered PyPI artifact and that the fix is +correct. Verified the regenerated file with the same methodology as before: a full +`pip download --require-hashes` round-trip for the complete tree at `--python-version 310` +(unchanged, zero errors), plus isolated single-package hash checks for the four new cp314 +hashes at `--python-version 314` (three passed to completion; the `cffi`/`pydantic-core` +isolated checks correctly got past the hash check itself -- reaching "Using cached ...whl" before +failing only on their own *unpinned* transitive deps in the deliberately minimal, single-package +test file, which the real requirements.txt already pins). + +**Lesson for future maintenance**: an unbounded `requires-python` floor (`>=3.10`, no upper +bound) means this hash-pinned file needs updating **every time a new CPython minor version is +released and gains adoption**, not just when a dependency version is deliberately bumped -- +there's no automatic detection of this short of someone actually hitting the gap, as happened +here. Worth checking for a new Python release's wheel availability periodically rather than only +reactively. + +## `install.ps1`: PowerShell mangles a multi-line, quote-containing argument passed to a native exe + +After the cp314 hash fix above, the user's real elevated run got all the way through -- +`pip install --require-hashes` succeeded (all packages installed/upgraded cleanly, including +correctly *skipping* the `rpds-py==0.30.0; python_version < '3.11'` line on their real Python +3.14, exactly as designed) and the `pywin32_postinstall.py -install` step also completed +successfully (DLLs copied to `system32`, COM registrations done) -- **only the final +verification step failed**, with a Python `SyntaxError` on a line that should have read +`print(f"python_executable: {sys.executable}")` but arrived as `print(fpython_executable:` -- +every embedded `"` character had vanished, and the embedded newlines had become literal raw +newlines inside what pip/Python received as a single command-line argument. + +**Root cause**: `install.ps1`'s verification step built a multi-line Python snippet (containing +several f-strings, i.e. embedded double quotes) as a single PowerShell string and passed it as +one argument to `python.exe -c` via `& $Exe @Arguments` (the script's own `Invoke-Checked` +helper). PowerShell's argument-to-native-command-line conversion is a known weak point exactly +for this combination -- a single argument containing both embedded double quotes *and* +newlines -- and mangled the quotes when re-serializing the argument array into the actual Win32 +process command-line string. This is a genuine, confirmed-in-practice limitation, not a +one-off typo: the other `Invoke-Checked` call sites in this same script (`pip install ...`, +`pywin32_postinstall.py -install`) never hit this because none of their arguments contain both +quotes and newlines together -- only the verification step's inline multi-line snippet did. + +**Fix**: stopped passing the Python snippet via `-c` entirely. Instead, `Set-Content` writes it +to a temp file (`Join-Path ([System.IO.Path]::GetTempPath()) "igor-bridge-verify-.py"`, +via `[guid]::NewGuid()` for a collision-free name) inside a `try`/`finally`, then +`python .py` is run as an ordinary script argument (just a path -- no quotes, no +newlines, no PowerShell native-argument-quoting risk at all), with the temp file always removed +afterward regardless of success/failure. This is the standard, robust pattern for handing a +non-trivial script to a subprocess from PowerShell -- avoid command-line argument quoting +entirely for anything beyond simple flags/paths, and use a temp file instead. + +**Confirmed this really was the whole remaining problem**: everything upstream of the +verification step (Python resolution, elevation check, `pip install --require-hashes` against +the newly-fixed hashed `requirements.txt`, `pywin32_postinstall.py -install`) succeeded cleanly +on this real Python 3.14/Windows run with zero other issues -- a good sign that the rest of the +script's design (registry-PATH-based Python resolution, hash-pinned installs, elevation +handling) is sound in practice, not just in this session's own sandboxed review. Could not +re-run the fixed version against the same real machine this session (no live access) -- the fix +itself (write-to-temp-file instead of inline `-c` argument) is a well-established pattern for +this exact class of problem, but it should still be re-confirmed on a real elevated Windows +session before being treated as fully proven, same standing caveat as the rest of this script. From b1d533f8a0cfeee70fc02a070670fb774bfafca7 Mon Sep 17 00:00:00 2001 From: Michael Huth Date: Wed, 5 Aug 2026 15:48:58 +0200 Subject: [PATCH 08/12] v1.27 --- Packages/doc/igor-pro-bridge.rst | 99 ++++-- .../igor-pro-bridge-1.25.0.mcpb | Bin 46721 -> 0 bytes .../igor-pro-bridge-1.27.0.mcpb | Bin 0 -> 50859 bytes tools/igor-mcp-bridge/install.ps1 | 81 +++-- tools/igor-mcp-bridge/server.py | 327 +++++++++++++++--- 5 files changed, 395 insertions(+), 112 deletions(-) delete mode 100644 tools/igor-mcp-bridge/igor-pro-bridge-1.25.0.mcpb create mode 100644 tools/igor-mcp-bridge/igor-pro-bridge-1.27.0.mcpb diff --git a/Packages/doc/igor-pro-bridge.rst b/Packages/doc/igor-pro-bridge.rst index a018172660..0c958a8730 100644 --- a/Packages/doc/igor-pro-bridge.rst +++ b/Packages/doc/igor-pro-bridge.rst @@ -55,11 +55,16 @@ Requirements - Most tools require Igor Pro to already be running; the bridge attaches to the running instance via COM. If needed, ``launch_igor_pro_unattended`` can start Igor Pro itself (after ``configure_igor_launch``) -- see below. -- **Both Igor Pro and the bridge's Python process must run elevated (as - Administrator)**. This is a hard Windows COM requirement documented verbatim in - Igor's own Automation Server reference and is not optional. Note that reopening - Claude Desktop normally does not preserve elevation from a previous launch -- it must - be relaunched via "Run as administrator" each time. +- **Igor Pro and the bridge's Python process must run at the same privilege + level** -- both elevated (as Administrator), or both not. Igor's own Automation + Server reference documents the both-elevated case, but elevation itself is not + the actual requirement: confirmed empirically (both processes running as an + ordinary, non-elevated user; a standalone ``win32com.client.GetActiveObject`` + test attached and ran commands successfully) that a *matching* privilege level + is what's needed. A mismatch -- one elevated, one not -- is what breaks the COM + connection; this most often shows up after Claude Desktop is reopened normally + (which does not preserve elevation from a previous launch) while Igor Pro is + still running elevated from before, or vice versa. - Python 3.10 or later, accessible as ``python`` on ``PATH``, with the pinned packages in ``requirements.txt`` (``mcp==1.29.0``, ``pywin32==312``) installed into that same environment -- see :ref:`igor_pro_bridge_installation` below for how. The packaged @@ -90,20 +95,23 @@ servers in current Claude Desktop builds). ``tools/igor-mcp-bridge/install.ps1`` from an elevated PowerShell** to install the pinned packages and complete pywin32's required post-install step (``Scripts\pywin32_postinstall.py -install``, which a plain ``pip install`` does not - do). Run it elevated specifically because Claude Desktop's manifest invokes the bridge - as the bare command ``python``, resolved via whatever ``PATH`` Claude Desktop's own - *elevated* process environment has at launch time -- not necessarily the same - interpreter an interactive elevated console session would resolve (e.g. a + do). The script itself must run elevated only because that post-install step + registers COM-support DLLs into protected system locations -- it is *not* because + Claude Desktop or Igor Pro need to be elevated at runtime (they don't; see + Requirements above). ``install.ps1`` resolves ``python.exe`` from the Machine/User + ``PATH`` registry values directly rather than trusting the invoking shell's own + possibly-customized ``$env:Path``, to mirror what a freshly launched Claude Desktop + process actually sees regardless of its own elevation state (not necessarily the + same interpreter an interactive console session would resolve, e.g. a PowerShell-profile-only conda activation, or a Microsoft Store app-execution-alias - stub that behaves differently once elevated). ``install.ps1`` resolves ``python.exe`` - from the Machine/User ``PATH`` registry values directly rather than trusting the - invoking shell's own possibly-customized ``$env:Path``, to mirror what a freshly - launched elevated Claude Desktop process actually sees; pass ``-PythonPath`` to - override this if needed. See ``Get-Help ./install.ps1 -Full`` for the complete - rationale and all steps performed. -- After installing (or after running ``install.ps1``), fully restart Claude Desktop - (elevated) so the updated server code/environment is actually picked up -- newly added - tools, or a freshly installed dependency, can otherwise lag behind what's on disk. + stub that behaves differently once elevated); pass ``-PythonPath`` to override this + if needed. See ``Get-Help ./install.ps1 -Full`` for the complete rationale and all + steps performed. +- After installing (or after running ``install.ps1``), fully restart Claude Desktop (at + whichever privilege level you intend to run it and Igor Pro at -- both must match each + other, but neither has to be elevated) so the updated server code/environment is + actually picked up -- newly added tools, or a freshly installed dependency, can + otherwise lag behind what's on disk. - Call ``get_bridge_version()`` afterward and confirm its ``python_executable`` field matches the interpreter ``install.ps1`` installed into. If it doesn't, Claude Desktop resolved a different Python than ``install.ps1`` guessed -- re-run ``install.ps1 @@ -166,9 +174,10 @@ Available tools ``check_bridge_health()`` Diagnoses exactly why the bridge can't reach Igor Pro, distinguishing three separate - failure modes: this process not running elevated, no Igor Pro COM object registered - at all, and a registered-but-dead COM object (Igor crashed or was force-closed, - leaving a stale registration that reconnecting alone can't fix). Run this first + failure modes: a privilege-level mismatch between this process and Igor Pro (one + elevated, one not), no Igor Pro COM object registered at all, and a registered-but-dead + COM object (Igor crashed or was force-closed, leaving a stale registration that + reconnecting alone can't fix). Run this first whenever something doesn't work. ``get_bridge_version()`` @@ -217,6 +226,28 @@ Available tools subsequent call fails with a COM/RPC error, check ``check_bridge_health()`` and be prepared to relaunch Igor Pro. +``ensure_igor_pro_bridge_defined(marker_function="")`` + Checks whether the ``IGOR_PRO_BRIDGE`` conditional-compilation symbol is defined in the + current Igor Pro instance and, if not -- e.g. a fresh Igor Pro environment this bridge + has never touched before, such as a bare "Untitled" experiment -- defines it itself via + ``SetIgorOption poundDefine=IGOR_PRO_BRIDGE`` and forces a recompile + (``COMPILEPROCEDURES``, reusing ``reload_and_compile_procedures``'s own two-signal + polling), rather than requiring a human to hand-edit the experiment's Procedure window + first. **Generic by design**: this tool has no built-in knowledge of MIES or any other + specific codebase -- it only manages the ``IGOR_PRO_BRIDGE`` symbol itself, so it works + for any Igor Pro experiment that adopts the ``#ifdef IGOR_PRO_BRIDGE`` convention for its + own bridge-support code, not just this repo's ``MIES_ClaudeHelper.ipf`` (see + :ref:`igor_pro_bridge_claude_helper`). The optional ``marker_function`` argument lets a + caller who *does* know about a specific gated function (e.g. + ``"CH_ListXOPExports"`` for ``MIES_ClaudeHelper.ipf``) get an extra before/after + ``FunctionInfo(...)`` confirmation that it actually became available, without that name + being hardcoded into the bridge -- if ``IGOR_PRO_BRIDGE`` is already defined but the named + function still doesn't resolve, that means whatever procedure file defines it simply + isn't ``#include``-d by whatever is currently loaded, which this tool cannot fix by + redefining the symbol again. Only ever adds the define, never calls ``poundUndefine``. + See :ref:`igor_pro_bridge_claude_helper` for the full ``SetIgorOption``/global-symbol-list + background. + ``dismiss_compile_error_dialog()`` Attempts to close a stuck Igor Pro dialog by posting a simulated Escape key press directly to it (via ``PostMessage``), targeting a visible window owned by an Igor @@ -296,8 +327,9 @@ Available tools that elevation automatically with no prompt; if not, it launches via ``ShellExecute``'s ``"runas"`` verb instead, triggering a normal Windows UAC consent dialog -- but this process itself remains unelevated afterward, so COM calls will - keep failing (see ``check_bridge_health``) until Claude Desktop itself is - relaunched as Administrator. The direct-child-process path also patches + keep failing (see ``check_bridge_health``) due to the resulting privilege-level + mismatch, until Claude Desktop itself is relaunched at a matching level (elevated, + to match the now-elevated Igor Pro). The direct-child-process path also patches ``COMSPEC`` into the child's environment if this Python process's own environment is missing it -- confirmed necessary this session: without it, MIES's own startup hook (``IgorStartOrNewHook`` -> ... -> @@ -355,9 +387,9 @@ windows for a visible one owned by an Igor Pro process whose title matches a kno stuck-dialog title, then posts ``WM_KEYDOWN``/``WM_KEYUP`` for Escape directly to it via ``PostMessage`` -- no foreground switch, no stolen focus. This works despite Igor Pro's elevated status specifically because this bridge's own process is also -required to run elevated (see Requirements above) -- Windows' UIPI blocks simulated +running elevated to match (see Requirements above) -- Windows' UIPI blocks simulated input from a lower-privilege process reaching a higher-privilege one, but not -between two equally elevated processes. +between two processes at the same privilege level. **Confirmed live against a real stuck dialog on both Igor Pro 10.03 and Igor Pro 9.06**: the original assumption that this dialog is an ordinary ``"#32770"`` @@ -498,6 +530,23 @@ Procedure window first, so only a ``#define`` placed there is reliably visible t other file's ``#ifdef`` checks; a ``#define`` in an ordinary ``.ipf`` file has no such guarantee. +That manual step can be skipped entirely by calling +``ensure_igor_pro_bridge_defined(marker_function="CH_ListXOPExports")`` instead (the +``marker_function`` argument is specific to this example -- the tool itself has no +knowledge of ``MIES_ClaudeHelper.ipf`` or ``CH_ListXOPExports`` built in; see the tool +reference above): per the "Conditional Compilation" topic in ``Programming.ihf``, +``SetIgorOption poundDefine=IGOR_PRO_BRIDGE`` adds the symbol to a separate *global* +list "available in all procedure windows (including independent modules)" -- broader +than even the Procedure-window scope described above -- queryable via +``SetIgorOption poundDefine=IGOR_PRO_BRIDGE?`` (sets ``V_flag``) and reversible via +``SetIgorOption poundUndefine=IGOR_PRO_BRIDGE``. Confirmed from the same topic and +cross-checked in ``Advanced Topics.ihf``: this is session-only (not saved into the +experiment, lost on Igor Pro restart) and itself triggers a recompile +(``BeforeUncompiledHook`` fires with ``changeCode`` 6 for ``poundDefine``/7 for +``poundUndefine``). Per ``Igor Reference.ihf``'s ``SetIgorOption`` entry, the operation +"is not compilable" and needs ``Execute`` from inside compiled code -- irrelevant here, +since the bridge always sends it as an interpreted command-line statement. + ``AfterCompiledHook`` is declared ``static`` so it coexists with any other file's own static ``AfterCompiledHook`` (e.g. the one in ``MIES_Include.ipf`` used only for the too-old-Igor warning panel) without colliding. diff --git a/tools/igor-mcp-bridge/igor-pro-bridge-1.25.0.mcpb b/tools/igor-mcp-bridge/igor-pro-bridge-1.25.0.mcpb deleted file mode 100644 index bbbd29ca43bed1cc2f049caff74bf9e2dceb03e7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 46721 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 JV|5h70{{hFQ^o)Q literal 0 HcmV?d00001 diff --git a/tools/igor-mcp-bridge/install.ps1 b/tools/igor-mcp-bridge/install.ps1 index daa79bb636..5dd81f389c 100644 --- a/tools/igor-mcp-bridge/install.ps1 +++ b/tools/igor-mcp-bridge/install.ps1 @@ -2,32 +2,38 @@ .SYNOPSIS Installs the pinned Python dependencies for the Igor Pro Bridge MCP server (tools/igor-mcp-bridge/server.py) into the same Python environment Claude Desktop - itself resolves when it launches the bridge elevated. + itself resolves when it launches the bridge. .DESCRIPTION Claude Desktop's manifest.json invokes the bridge as the bare command "python", - resolved via whatever PATH Claude Desktop's own elevated process environment has - at launch time. That is NOT guaranteed to be the same Python an interactive - elevated console session resolves -- e.g. a PowerShell profile script activating a - conda environment only for that session, or a per-user Microsoft Store "app - execution alias" stub (a placeholder python.exe that just opens the Store) which - is known to behave differently once elevated. Installing packages into whatever - Python a manually-opened elevated console happens to find can therefore silently - install into the wrong environment. + resolved via whatever PATH Claude Desktop's own process environment has at launch + time. That is NOT guaranteed to be the same Python an interactive console session + resolves -- e.g. a PowerShell profile script activating a conda environment only + for that session, or a per-user Microsoft Store "app execution alias" stub (a + placeholder python.exe that just opens the Store) which is known to behave + differently once elevated. Installing packages into whatever Python a + manually-opened console happens to find can therefore silently install into the + wrong environment. This script instead resolves python.exe from the Machine and then User PATH registry values directly (via [Environment]::GetEnvironmentVariable(..., target)), the same two sources and order Windows composes into a freshly created process's - environment block -- deliberately ignoring this session's own possibly-customized - $env:Path, to mirror what a freshly launched, elevated Claude Desktop process - actually sees. Pass -PythonPath explicitly to skip this resolution entirely if you - already know the right interpreter (e.g. from a prior get_bridge_version() call -- - see below). + environment block regardless of that process's elevation state -- deliberately + ignoring this session's own possibly-customized $env:Path, to mirror what a + freshly launched Claude Desktop process actually sees, whether or not it happens + to be elevated. Pass -PythonPath explicitly to skip this resolution entirely if + you already know the right interpreter (e.g. from a prior get_bridge_version() + call -- see below). Steps performed, in order: - 1. Confirm this script itself is running elevated (required: both the package - install destination and the later pywin32 post-install step must match/target - the same elevated environment Claude Desktop uses). + 1. Confirm this script itself is running elevated. This is required only for + step 5 below (pywin32's post-install step, which registers COM-support DLLs + into protected system locations) -- it is NOT because Claude Desktop or Igor + Pro themselves need to be elevated at runtime. Confirmed empirically (see + igor-pro-bridge.rst, "Requirements"): Claude Desktop and Igor Pro just need to + run at the SAME privilege level as each other (both elevated, or both not); + this script needing elevation is a one-time, install-time requirement of its + own, independent of whichever level you later choose to run the bridge at. 2. Resolve python.exe (or use -PythonPath). 3. ` -m pip install --upgrade pip` 4. ` -m pip install --require-hashes -r requirements.txt` (pinned, @@ -41,11 +47,13 @@ 6. Import-check mcp and win32com.client with the same interpreter, and print its full path/version for you to cross-check. - After this script finishes, fully restart Claude Desktop (elevated), then call the - bridge's get_bridge_version tool from a conversation -- its "python_executable" - field is the authoritative answer for which interpreter Claude Desktop actually - launched. If it doesn't match what this script installed into, re-run this script - with -PythonPath pointing at that reported path. + After this script finishes, fully restart Claude Desktop (at whichever privilege + level you intend to run it and Igor Pro at -- both must match each other, but + neither has to be elevated), then call the bridge's get_bridge_version tool from a + conversation -- its "python_executable" field is the authoritative answer for + which interpreter Claude Desktop actually launched. If it doesn't match what this + script installed into, re-run this script with -PythonPath pointing at that + reported path. .PARAMETER PythonPath Full path to a specific python.exe to install into, bypassing auto-resolution @@ -100,10 +108,12 @@ function Test-IsWindowsAppsStub { function Resolve-ClaudeDesktopPython { <# - Mirrors how Claude Desktop's own elevated process resolves the bare command - "python" from its manifest.json, without trusting this interactive - PowerShell session's own $env:Path -- see the script's top-level comment-based - help for the full rationale. Returns the first matching python.exe found by + Mirrors how Claude Desktop's own process resolves the bare command "python" + from its manifest.json (regardless of whether that process happens to be + elevated or not -- Machine/User PATH registry values are the same either way), + without trusting this interactive PowerShell session's own $env:Path -- see + the script's top-level comment-based help for the full rationale. Returns the + first matching python.exe found by searching the Machine PATH, then the User PATH, in that order (the same composition order Windows uses to build a fresh process's environment block), or $null if none is found. @@ -145,11 +155,14 @@ function Invoke-Checked { if (-not (Test-IsElevated)) { Write-Error ( - "This script must run elevated (as Administrator) -- both because the " + - "pywin32 post-install step below requires it, and because it needs to " + - "match the same elevated environment Claude Desktop itself runs in. " + - "Re-run this script from an elevated PowerShell (right-click PowerShell -> " + - "Run as administrator)." + "This script must run elevated (as Administrator) because the pywin32 " + + "post-install step below registers COM-support DLLs into protected system " + + "locations, which requires admin rights regardless of how you plan to run " + + "Claude Desktop/Igor Pro afterward. This is a one-time, install-time " + + "requirement only -- Claude Desktop and Igor Pro do NOT both need to be " + + "elevated at runtime, they just need to match each other's privilege level " + + "(see igor-pro-bridge.rst, 'Requirements'). Re-run this script from an " + + "elevated PowerShell (right-click PowerShell -> Run as administrator)." ) exit 1 } @@ -251,8 +264,10 @@ try { } Write-Host ( - "`nDone. Fully restart Claude Desktop (elevated), then call the bridge's " + - "get_bridge_version tool and confirm its 'python_executable' field matches: $python`n" + + "`nDone. Fully restart Claude Desktop (at whichever privilege level you intend " + + "to run it and Igor Pro at -- both must match each other, but neither has to be " + + "elevated), then call the bridge's get_bridge_version tool and confirm its " + + "'python_executable' field matches: $python`n" + "If it does not match, re-run this script with -PythonPath set to the path " + "get_bridge_version() actually reports." ) diff --git a/tools/igor-mcp-bridge/server.py b/tools/igor-mcp-bridge/server.py index 99f93928f6..db6bcc02c8 100644 --- a/tools/igor-mcp-bridge/server.py +++ b/tools/igor-mcp-bridge/server.py @@ -55,10 +55,15 @@ "for most uses" -- and the point-value methods sidestep SAFEARRAY marshaling questions entirely, so this file uses those instead. Whole-wave SAFEARRAY access could be added later as a faster path for large waves.) -- **CRITICAL SETUP REQUIREMENT, confirmed verbatim from the docs**: "The Windows - operating system requires that you run the client and server (Igor) as administrator." - I.e. BOTH this Python process AND Igor Pro itself must be started as Administrator on - Windows 10+, or the COM connection will fail. This is not optional and is easy to miss. +- **CRITICAL SETUP REQUIREMENT, per the docs**: "The Windows operating system requires + that you run the client and server (Igor) as administrator." Confirmed empirically, + however, that elevation itself is not the actual requirement -- this Python process + and Igor Pro must run at the SAME privilege level (both elevated as Administrator, or + both not); Igor's docs only document/test the both-elevated case. A mismatch between + the two, not non-elevation per se, is what breaks the COM connection, and is easy to + miss (e.g. after Claude Desktop is reopened normally, which does not preserve + elevation from a previous launch, while Igor Pro is still running elevated from + before). ONE THING THIS FILE CANNOT VERIFY FROM here (no Windows/Igor available to actually run this): the exact Python-side calling convention pywin32's dynamic dispatch uses for @@ -90,9 +95,10 @@ installation steps -- this docstring previously described the config.json approach, which was found to be unreliable and is no longer how this bridge is distributed. -After installing (or updating), fully restart Claude Desktop (elevated). Remember: both -Claude Desktop's Python process AND Igor Pro itself need to be running elevated (as -Administrator) for the COM connection to succeed. +After installing (or updating), fully restart Claude Desktop. Remember: Claude Desktop's +Python process and Igor Pro itself need to be running at the SAME privilege level (both +elevated as Administrator, or both not) for the COM connection to succeed -- elevation +itself is not the requirement, a mismatch between the two is what breaks it. This is a *local* MCP server (stdio transport) -- it only works from a Claude Desktop session running on the same Windows machine as Igor Pro, not from a cloud/Cowork sandbox. @@ -102,6 +108,7 @@ import html.parser import importlib.metadata import os +import re import subprocess import sys import tempfile @@ -175,11 +182,12 @@ def _is_current_process_elevated(): _elevated_at_startup = _is_current_process_elevated() if _elevated_at_startup is False: print( - "WARNING: this MCP server process is NOT running elevated (as Administrator). " - "Igor Pro's COM Automation Server requires BOTH Igor Pro and this process to be " - "elevated, or every tool call will fail with a COM/RPC error. Relaunch Claude " - "Desktop specifically via 'Run as administrator' -- reopening it normally does " - "not preserve elevation across restarts.", + "NOTE: this MCP server process is NOT running elevated (as Administrator). " + "This is fine as long as Igor Pro is ALSO not running elevated -- COM requires " + "this process and Igor Pro to be at the SAME privilege level, not elevation " + "specifically. If Igor Pro is running elevated while this process isn't, every " + "tool call will fail with a COM/RPC error; either relaunch Claude Desktop via " + "'Run as administrator' to match, or restart Igor Pro non-elevated instead.", file=sys.stderr, ) elif _elevated_at_startup is None: @@ -213,10 +221,11 @@ def _get_igor(force_reconnect=False): except Exception as e: raise RuntimeError( "Could not attach to a running Igor Pro instance via COM. Make sure: " - "(1) Igor Pro is already running, (2) BOTH Igor Pro and this Python " - "process are running as Administrator (Windows requires this for COM " - "Automation), and (3) Igor Pro 9.00 (or later) is installed with the " - "Automation Server component." + "(1) Igor Pro is already running, (2) Igor Pro and this Python process " + "are running at the SAME privilege level -- both elevated (as " + "Administrator), or both not; a mismatch, not elevation itself, is what " + "breaks the COM connection -- and (3) Igor Pro 9.00 (or later) is " + "installed with the Automation Server component." ) from e return _igor @@ -622,7 +631,7 @@ def work(): # from inside a conversation which .mcpb build was actually loaded/active in Claude # Desktop, which made it impossible to verify whether a given fix (e.g. the reload/compile # timing relaxation) was actually in effect during a test -- see SESSION_NOTES.md. -_BRIDGE_VERSION = "1.25.0" +_BRIDGE_VERSION = "1.27.0" def _installed_package_version(distribution_name: str) -> str | None: @@ -653,9 +662,10 @@ def get_bridge_version() -> dict: The "python_executable" field is also the authoritative answer to a separate, easy-to-get-wrong question: *which* Python environment Claude Desktop actually launched this process with. Claude Desktop's manifest.json only specifies the bare - command "python", resolved via whatever PATH Claude Desktop's own (elevated) - process environment has at launch time -- which is not guaranteed to match the - Python an interactive elevated console session resolves (e.g. a PowerShell profile + command "python", resolved via whatever PATH Claude Desktop's own process + environment has at launch time (regardless of whether that process happens to be + elevated) -- which is not guaranteed to match the Python an interactive console + session resolves (e.g. a PowerShell profile activating a conda environment, or a per-user Microsoft Store "app execution alias" stub that behaves differently once elevated). install.ps1 makes its own best-effort guess at install time; after installing and restarting Claude Desktop, @@ -679,35 +689,35 @@ def check_bridge_health() -> dict: Call this first whenever a command fails or behaves unexpectedly. This session's own debugging hit three distinct failure modes that all needed different fixes: - (1) this Python process not running elevated, (2) no Igor Pro COM object - registered at all (Igor not running), and (3) a registered-but-dead COM object - (Igor crashed/was force-closed, leaving a stale registration that reconnecting - alone can't fix -- Igor itself needs relaunching). This check distinguishes all - three rather than surfacing one generic failure. + (1) this Python process and Igor Pro running at mismatched privilege levels (one + elevated, one not), (2) no Igor Pro COM object registered at all (Igor not + running), and (3) a registered-but-dead COM object (Igor crashed/was + force-closed, leaving a stale registration that reconnecting alone can't fix -- + Igor itself needs relaunching). This check distinguishes all three rather than + surfacing one generic failure. + + Note on (1): elevation itself is not the requirement -- empirically confirmed + (both this bridge process and Igor Pro running non-elevated, as an ordinary + user, via a standalone win32com.client.GetActiveObject test) that COM attaches + fine as long as client and server share the same privilege level. This check + therefore always attempts the real COM call rather than pre-emptively failing + based on this process's own elevation state -- a mismatch, if present, shows up + as the COM/RPC-transport error below. Returns a dict with at least a "status" key ("OK" or "FAIL") and, on FAIL, a "problem" key with a specific, actionable description. """ report = {"python_process_elevated": _is_current_process_elevated()} - if report["python_process_elevated"] is False: - report["status"] = "FAIL" - report["problem"] = ( - "This Python process is not running elevated (as Administrator). Igor " - "Pro's COM Automation Server requires both Igor Pro and this process to " - "be elevated. Relaunch Claude Desktop specifically via 'Run as " - "administrator' -- reopening it normally does not preserve elevation -- " - "then retry." - ) - return report - try: _get_igor() except RuntimeError as e: report["status"] = "FAIL" report["problem"] = ( f"No running Igor Pro instance found via COM ({e}). Make sure Igor Pro " - "9.00 or later is open and running elevated." + "9.00 or later is open, and that it and this Python process are running " + "at the same privilege level (both elevated, or both not) -- a mismatch " + "is the most common cause of this failure, not elevation itself." ) return report @@ -727,11 +737,12 @@ def try_call(): report["status"] = "FAIL" report["problem"] = ( f"Found a registered Igor Pro COM object, but calls to it fail with a " - f"COM/RPC-transport error even after reconnecting ({e2}). This means a " - "stale/dead COM registration, most likely because Igor Pro crashed or " - "was force-closed previously. Check Task Manager for Igor64.exe -- " - "there should be exactly one -- fully close it, and relaunch Igor Pro " - "fresh, as Administrator." + f"COM/RPC-transport error even after reconnecting ({e2}). This usually " + "means a stale/dead COM registration (Igor Pro crashed or was " + "force-closed previously -- check Task Manager for Igor64.exe, there " + "should be exactly one, fully close it, and relaunch Igor Pro fresh), " + "or a privilege-level mismatch between this process and Igor Pro (both " + "must be elevated, or both not -- elevation itself is not required)." ) return report @@ -1375,6 +1386,207 @@ def reload_and_compile_procedures() -> dict: } +# --- Defining IGOR_PRO_BRIDGE without manual experiment setup ---------------------- +# +# Some procedure files wrap bridge-support helper functions in +# "#ifdef IGOR_PRO_BRIDGE / ... / #endif" so those helpers don't get silently +# compiled into an ordinary end-user build -- this repo's own +# Packages/MIES/MIES_ClaudeHelper.ipf is one example (see that file's own header +# comment), but nothing about this convention or this tool is specific to MIES: ANY +# Igor Pro experiment/procedure tree can adopt the same "#ifdef IGOR_PRO_BRIDGE" +# pattern for its own bridge-support code. The catch is the same regardless of whose +# code is gated: a freshly opened Igor Pro environment this bridge has never touched +# before (a bare "Untitled" experiment, or any experiment/branch whose Procedure +# window was never hand-edited for this) will not have IGOR_PRO_BRIDGE defined, so +# that gated code stays uncompiled until someone adds "#define IGOR_PRO_BRIDGE" to +# the experiment's Procedure window by hand and recompiles. +# +# Confirmed from Igor Pro Folder/Igor Help Files/Programming.ihf, "Conditional +# Compilation" topic: an ordinary "#define symbol" inside a procedure file is scoped +# to that file (or, for the main Procedure window specifically, to every +# non-independent-module file) -- but "SetIgorOption poundDefine=symb" instead adds +# symb to a separate *global* symbol list, "available in all procedure windows +# (including independent modules)". Queried via "SetIgorOption poundDefine=symb?" +# (sets V_flag to 1/0), reversed via "SetIgorOption poundUndefine=symb". "A symbol +# defined in a global list is not undefined by a #undef in a procedure window." +# +# Also confirmed there and cross-checked in Advanced Topics.ihf: this change is +# temporary -- it lasts only until Igor Pro quits (not saved into the experiment, +# must be redone every fresh Igor session) -- and itself triggers a recompile: the +# BeforeUncompiledHook table lists "SetIgorOption poundDefine" as changeCode 6 and +# "SetIgorOption poundUndefine" as changeCode 7, each described as "causes a +# recompile". Per Igor Reference.ihf's own SetIgorOption entry: "SetIgorOption is +# not compilable. To use it in a user-defined function, you need to use Execute" -- +# a non-issue here, since this bridge always sends it as an interpreted command-line +# statement via Execute2, never from inside compiled code. +# +# This tool deliberately has NO built-in knowledge of MIES or any other specific +# codebase -- it only manages the IGOR_PRO_BRIDGE symbol itself (defining it, +# recompiling, reporting the result), so it's equally useful for any Igor Pro +# experiment that adopts this convention for its own bridge-support code. An +# optional caller-supplied marker_function argument lets a caller who *does* know +# about a specific gated function (e.g. this repo's own "CH_ListXOPExports") get an +# extra confirmation that it actually became available, without that name being +# hardcoded into the bridge itself. +_IGOR_PRO_BRIDGE_DEFINE = "IGOR_PRO_BRIDGE" +_IGOR_PRO_BRIDGE_DEFINE_QUERY_CMD = ( + f'SetIgorOption poundDefine={_IGOR_PRO_BRIDGE_DEFINE}?; fprintf 0, "%d", V_flag' +) +# Function names in Igor are restricted to letters/digits/underscore (and can't start +# with a digit); this is just a defensive check against a caller-supplied string +# breaking out of the quoted FunctionInfo(...) call built below, not a claim about +# every valid Igor identifier rule. +_SAFE_IGOR_IDENTIFIER_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$") + + +def _is_igor_pro_bridge_defined() -> bool: + """Query Igor's global #define symbol list (see the block comment above) for + IGOR_PRO_BRIDGE.""" + errorCode, errorMsg, history, results = _execute2(_IGOR_PRO_BRIDGE_DEFINE_QUERY_CMD) + if errorCode != 0: + raise RuntimeError( + f"Could not query {_IGOR_PRO_BRIDGE_DEFINE} define state (error code " + f"{errorCode}): {errorMsg or '(no error message)'}" + ) + return results == "1" + + +def _function_resolves(function_name: str) -> bool: + """True if FunctionInfo(function_name) is non-empty, i.e. Igor currently has a + compiled function/operation by that name -- no assumption about which procedure + file or codebase defines it.""" + if not _SAFE_IGOR_IDENTIFIER_RE.match(function_name): + raise ValueError( + f"Not a plausible Igor function name: {function_name!r}" + ) + errorCode, errorMsg, history, results = _execute2( + f'fprintf 0, "%s", FunctionInfo("{function_name}")' + ) + if errorCode != 0: + raise RuntimeError( + f"Could not check FunctionInfo({function_name!r}) (error code " + f"{errorCode}): {errorMsg or '(no error message)'}" + ) + return results != "" + + +@mcp.tool() +def ensure_igor_pro_bridge_defined(marker_function: str = "") -> dict: + """Make sure the IGOR_PRO_BRIDGE conditional-compilation symbol is defined in the + current Igor Pro instance -- defining it and forcing a recompile if it wasn't + already, instead of requiring a human to hand-edit the experiment's Procedure + window first. See the block comment above this tool in server.py for the full + SetIgorOption/Conditional-Compilation background, confirmed directly from Igor + Pro Folder/Igor Help Files/Programming.ihf and Advanced Topics.ihf. + + This tool has NO built-in knowledge of MIES or any other specific codebase -- it + only manages the IGOR_PRO_BRIDGE symbol itself, so it's equally useful for any + Igor Pro experiment that adopts the "#ifdef IGOR_PRO_BRIDGE" convention for its + own bridge-support procedure code, not just this repo's own + MIES_ClaudeHelper.ipf. + + Call this proactively whenever a fresh/unfamiliar Igor Pro environment is being + used with this bridge for the first time in a session, or whenever some + IGOR_PRO_BRIDGE-gated behavior you rely on (e.g. + reload_and_compile_procedures's AfterCompiledHook-counter signal, if the + procedure file providing it is gated this way) looks unavailable and you want to + try fixing that rather than just accepting a weaker fallback. + + Args: + marker_function: optional. If you know of a *specific* function that should + become available once IGOR_PRO_BRIDGE is defined and compiled in (e.g. + "CH_ListXOPExports" for this repo's own MIES_ClaudeHelper.ipf), pass its + name here to get an extra before/after FunctionInfo(...) confirmation + layered on top of the define/recompile result. Leave blank to just + manage the define/recompile with no such check -- this is the fully + generic mode, appropriate when you don't know (or don't need to know) + about any specific gated function. + + Steps taken: + 1. If marker_function is given, checks FunctionInfo(marker_function) now, before + doing anything else (recorded as "marker_function_available_before"). + 2. Checks IGOR_PRO_BRIDGE's current state in the global #define list + (SetIgorOption poundDefine=IGOR_PRO_BRIDGE?). If already defined, returns + immediately with "igor_pro_bridge_defined": True and no recompile triggered -- + if marker_function was given and still doesn't resolve, that means whatever + procedure file defines it simply isn't #include-d by whatever is currently + loaded (e.g. a bare "Untitled" experiment) -- NOT something this tool can fix + by redefining IGOR_PRO_BRIDGE, so it's reported via + "marker_function_available_before"/"_after" rather than retried. + 3. If IGOR_PRO_BRIDGE is genuinely undefined, runs + 'SetIgorOption poundDefine=IGOR_PRO_BRIDGE' then 'COMPILEPROCEDURES ' (both + via Execute/P, the same operation-queue mechanism reload_and_compile_procedures + uses -- RELOAD CHANGED PROCS is deliberately skipped here since no on-disk + .ipf file changed, only Igor's in-memory global symbol list), then polls for + compile confirmation exactly the way reload_and_compile_procedures does, + reusing _poll_for_compile_confirmation (see that function's docstring for why + two independent signals are checked). + 4. If marker_function was given, re-checks FunctionInfo(marker_function) one + final time ("marker_function_available_after") and reports the outcome. + + Only ever ADDS the define, never calls poundUndefine -- there is currently no + known reason for this bridge to want IGOR_PRO_BRIDGE turned back off within a + session, and doing so would itself force yet another recompile for no benefit. + """ + marker_before = _function_resolves(marker_function) if marker_function else None + + was_defined = _is_igor_pro_bridge_defined() + + if was_defined: + result = {"igor_pro_bridge_defined": True, "define_set": False} + if marker_function: + result["marker_function"] = marker_function + result["marker_function_available_before"] = marker_before + result["marker_function_available_after"] = marker_before + if not marker_before: + result["note"] = ( + f"{_IGOR_PRO_BRIDGE_DEFINE} is already defined globally, but " + f'FunctionInfo("{marker_function}") still does not resolve. ' + "This means whatever procedure file defines that function is " + "not #include-d by whatever is currently loaded (e.g. a bare " + f"'Untitled' experiment) -- defining {_IGOR_PRO_BRIDGE_DEFINE} " + "again cannot fix that. Load an experiment/procedure file that " + "actually includes it instead (see load_experiment)." + ) + return result + + baseline_counter = _read_claude_helper_compile_counter() + + errorCode, errorMsg, history, results = _execute2( + f'Execute/P "SetIgorOption poundDefine={_IGOR_PRO_BRIDGE_DEFINE}"' + ) + if errorCode != 0: + raise RuntimeError( + f"SetIgorOption poundDefine={_IGOR_PRO_BRIDGE_DEFINE} failed (error code " + f"{errorCode}): {errorMsg}" + ) + + time.sleep(_RELOAD_TO_COMPILE_PAUSE_SECONDS) + + errorCode, errorMsg, history, results = _execute2('Execute/P "COMPILEPROCEDURES "') + if errorCode != 0: + raise RuntimeError( + f"COMPILEPROCEDURES failed (error code {errorCode}): {errorMsg}" + ) + + time.sleep(_POST_COMPILE_PAUSE_SECONDS) + + poll_result = _poll_for_compile_confirmation( + baseline_counter, _COMPILE_POLL_TIMEOUT_SECONDS + ) + + result = { + "igor_pro_bridge_defined_before": False, + "define_set": True, + **poll_result, + } + if marker_function: + result["marker_function"] = marker_function + result["marker_function_available_before"] = marker_before + result["marker_function_available_after"] = _function_resolves(marker_function) + return result + + # --- Debugger control --------------------------------------------------------------- # # Confirmed against a live Igor Pro instance during development, and against Igor @@ -2152,10 +2364,13 @@ def configure_igor_launch(exe_path: str) -> dict: "('Run as administrator') when launching Igor Pro, which requires the " "user to approve a consent dialog themselves. Even after that succeeds, " "THIS Python process will still not be elevated, so COM calls will keep " - "failing with the usual elevation-mismatch error (see " - "check_bridge_health) until Claude Desktop itself is relaunched as " - "Administrator -- make sure the user understands this before relying " - "on launch_igor_pro_unattended to get a fully working bridge." + "failing due to the resulting privilege-level mismatch (see " + "check_bridge_health) until Claude Desktop itself is relaunched at a " + "matching level (elevated, to match the now-elevated Igor Pro) -- make " + "sure the user understands this before relying on " + "launch_igor_pro_unattended to get a fully working bridge. " + "Alternatively, Igor Pro can simply be launched non-elevated by hand " + "instead, which needs no elevation match at all." ) else: elevation_plan = ( @@ -2164,8 +2379,9 @@ def configure_igor_launch(exe_path: str) -> dict: "'not elevated' as a conservative default (requesting UAC elevation " "via ShellExecute's 'runas' verb rather than risking a silently " "unelevated direct launch) -- if COM calls fail afterward, check " - "check_bridge_health and make sure both Claude Desktop and Igor Pro " - "are running as Administrator." + "check_bridge_health and make sure Claude Desktop and Igor Pro are " + "running at the same privilege level (both elevated, or both not; " + "elevation itself is not required)." ) return { @@ -2217,9 +2433,11 @@ def launch_igor_pro_unattended(wait_for_ready_seconds: float = 30.0) -> dict: ShellExecute's "runas" verb instead, which triggers a normal Windows UAC consent dialog the user must approve -- but even after that succeeds, THIS process will still not be elevated, so COM calls will keep failing (the classic - elevation-mismatch failure mode -- see check_bridge_health) until Claude Desktop - itself is relaunched as Administrator. configure_igor_launch's own return value - already surfaces which of these two paths will be taken -- check that first. + privilege-level-mismatch failure mode -- see check_bridge_health; elevation + itself is not the requirement, matching levels is) until Claude Desktop itself + is relaunched at a matching level (elevated, to match the now-elevated Igor + Pro). configure_igor_launch's own return value already surfaces which of these + two paths will be taken -- check that first. The direct-child-process path also patches COMSPEC into the child's environment if this Python process's own environment is missing it (see @@ -2321,9 +2539,10 @@ def launch_igor_pro_unattended(wait_for_ready_seconds: float = 30.0) -> dict: "initializing (slower on first launch or a cold machine) -- try " "check_bridge_health() again after waiting longer. If launch_method is " "'shell_execute_runas', also consider that this Python process itself " - "is not elevated, which will prevent a COM connection indefinitely " - "regardless of how long you wait, until Claude Desktop is relaunched " - "as Administrator." + "is not elevated while Igor Pro (just launched via the UAC prompt) now " + "is, which will prevent a COM connection indefinitely regardless of how " + "long you wait, until Claude Desktop is relaunched at a matching " + "(elevated) level." ), } From 69adc926c8775692ac213f2281c115a67c514ae3 Mon Sep 17 00:00:00 2001 From: Michael Huth Date: Wed, 5 Aug 2026 22:49:13 +0200 Subject: [PATCH 09/12] MCP: Add v2.2.3 Igor Pro Bridge, using ZeroMQ XOP The new major version interfaces to Igor Pro through the zeromq XOP instead of the deprecated COM interface. --- Packages/doc/igor-pro-bridge.rst | 808 ++-- tools/igor-mcp-bridge/ZMQ_BridgeHelpers.ipf | 827 ++++ .../igor-pro-bridge-1.27.0.mcpb | Bin 50859 -> 0 bytes .../igor-pro-bridge-2.2.3.mcpb | Bin 0 -> 33896 bytes tools/igor-mcp-bridge/install.ps1 | 61 +- tools/igor-mcp-bridge/manifest.json | 120 + tools/igor-mcp-bridge/requirements.txt | 24 +- tools/igor-mcp-bridge/server.py | 3554 ++++++----------- 8 files changed, 2862 insertions(+), 2532 deletions(-) create mode 100644 tools/igor-mcp-bridge/ZMQ_BridgeHelpers.ipf delete mode 100644 tools/igor-mcp-bridge/igor-pro-bridge-1.27.0.mcpb create mode 100644 tools/igor-mcp-bridge/igor-pro-bridge-2.2.3.mcpb create mode 100644 tools/igor-mcp-bridge/manifest.json diff --git a/Packages/doc/igor-pro-bridge.rst b/Packages/doc/igor-pro-bridge.rst index 0c958a8730..ab990dfaf5 100644 --- a/Packages/doc/igor-pro-bridge.rst +++ b/Packages/doc/igor-pro-bridge.rst @@ -14,7 +14,9 @@ human needing to click anything in Igor Pro for routine steps. The code lives in ``tools/igor-mcp-bridge/``: -- ``server.py``: the MCP server implementation (Python, using ``pywin32`` for COM). +- ``server.py``: the MCP server implementation (Python, using ``pyzmq`` to talk to + Igor Pro's ZeroMQ-XOP, and ``pywin32`` for OS-level window handling and process + launching only -- see :ref:`igor_pro_bridge_v2_migration` below). - ``igor-pro-bridge-*.mcpb``: packaged Claude Desktop Extension bundles built from ``server.py`` via the `mcpb `__ CLI. - ``requirements.txt``: pinned Python dependency versions (see :ref:`igor_pro_bridge_requirements`). @@ -22,53 +24,98 @@ The code lives in ``tools/igor-mcp-bridge/``: environment and completes pywin32's post-install step -- see :ref:`igor_pro_bridge_installation`. -The companion procedure file ``Packages/MIES/MIES_ClaudeHelper.ipf`` (included from -``MIES_Include.ipf``) provides an ``AfterCompiledHook`` used by the bridge to get a more -reliable compile-success signal; see :ref:`igor_pro_bridge_claude_helper` below. +The companion procedure file ``Packages/MIES/ZMQ_BridgeHelpers.ipf`` (included from +``MIES_Include.ipf``, independent module ``ZBR``) provides the Igor-side functions the +bridge calls into, including an ``AfterCompiledHook`` used to get a more reliable +compile-success signal and to (re)bind the ZeroMQ server socket on every compile; see +:ref:`igor_pro_bridge_zbr_helpers` below. This is Windows-only tooling for MIES development, not something end users of MIES interact with. +.. _igor_pro_bridge_v2_migration: + Architecture ------------ -Igor Pro can act as a COM *server* on Windows via its built-in ActiveX Automation -Server (``IgorPro.Application``), documented in ``Igor Pro Folder/Miscellaneous/Windows -Automation/Automation Server.ihf``. Igor Pro cannot act as a COM *client*. The bridge is -a Python COM *client* process that attaches to an already-running Igor Pro instance via -``win32com.client.GetActiveObject("IgorPro.Application")`` and issues commands through -``Execute2``. - -``Execute2`` does not raise a COM/Automation error just because the Igor-level command -failed -- the bridge checks the returned error code itself and raises a Python -``RuntimeError`` when appropriate. Data is retrieved by including ``fprintf 0, "..."`` -calls in the command string and reading the result back. +As of v2.0.0, the bridge talks to Igor Pro over the +`ZeroMQ-XOP `__'s ``CallFunction`` +JSON protocol, over a plain localhost TCP socket (``tcp://127.0.0.1:5680``) -- not +COM. Versions through 1.27.0 instead used Igor Pro's built-in ActiveX Automation +Server (``IgorPro.Application``, documented in ``Igor Pro Folder/Miscellaneous/Windows +Automation/Automation Server.ihf``): the bridge was a COM *client* process that +attached via ``win32com.client.GetActiveObject`` and issued commands through +``Execute2``. See :ref:`igor_pro_bridge_v1_history` for that transport's full design +history, now superseded. + +Why this changed: COM required the bridge's Python process and Igor Pro to run at the +*same* Windows privilege level (both elevated, or both not) -- an easy-to-miss +mismatch, e.g. Claude Desktop reopened normally after Igor Pro was left running +elevated from before. ZeroMQ is a plain TCP socket with no such requirement at all -- +elevation no longer matters in any way for this bridge, in either direction. + +Wire protocol, in brief (see ``server.py``'s own module docstring for the full +detail): the bridge sends one JSON request per round trip -- +``{"version": 1, "messageID": ..., "CallFunction": {"name": ..., "params": [...]}}`` +-- and receives ``{"errorCode": {"value": ..., "msg": ...}, "result": ...}`` back. A +fresh ZeroMQ REQ socket is created for every call (a REQ socket that times out cannot +send again without being recreated). Function names for anything in the ``ZBR`` +independent module must be ``#``-qualified (``"ZBR#ZBR_Ping"``) -- the XOP's own +README says to omit the ``#`` for independent-module functions; that is confirmed +empirically to be wrong. Wave return values carry the *entire* wave (dimensions, +units, note, complex/text/wave-ref support) natively serialized as JSON, and +multi-return Igor functions (``Function [a, b] Foo()``) are supported directly as a +JSON array of typed values. + +A central constraint carried over unchanged from the COM design: Igor's ``Execute`` +operation cannot run unqueued from inside a Function -- only ``Execute/P`` (deferred) +can. There is therefore no single-round-trip equivalent of COM's ``Execute2`` for +arbitrary free-form command text; ``execute_igor_command``/ +``execute_igor_command_unattended`` instead use a submit-then-poll pattern +(``ZBR_SubmitCommand``/``ZBR_SubmitCommandUnattended`` queue the command and return a +token immediately; ``ZBR_PollCommand``, called repeatedly, reports completion and the +captured text). Every other tool that doesn't need arbitrary free-form text is backed +by a small, purpose-built, directly-callable ``ZBR_*`` function instead and needs no +polling. + +The bridge checks the reply's ``errorCode.value`` itself and raises a Python +``RuntimeError`` subclass (``IgorZmqError``, or ``IgorZmqUnreachable`` if no reply +arrives at all) when appropriate -- a malformed or failing Igor-level call does not +otherwise surface as a Python exception on its own. .. _igor_pro_bridge_requirements: Requirements ------------ -- Igor Pro 9.00 or later, running on Windows. The Automation Server is already included - in Igor Pro 9; ``RELOAD CHANGED PROCS``, which ``reload_and_compile_procedures`` - depends on, was introduced in Igor Pro 9.00 and sets the actual minimum version. -- Most tools require Igor Pro to already be running; the bridge attaches to the - running instance via COM. If needed, ``launch_igor_pro_unattended`` can start - Igor Pro itself (after ``configure_igor_launch``) -- see below. -- **Igor Pro and the bridge's Python process must run at the same privilege - level** -- both elevated (as Administrator), or both not. Igor's own Automation - Server reference documents the both-elevated case, but elevation itself is not - the actual requirement: confirmed empirically (both processes running as an - ordinary, non-elevated user; a standalone ``win32com.client.GetActiveObject`` - test attached and ran commands successfully) that a *matching* privilege level - is what's needed. A mismatch -- one elevated, one not -- is what breaks the COM - connection; this most often shows up after Claude Desktop is reopened normally - (which does not preserve elevation from a previous launch) while Igor Pro is - still running elevated from before, or vice versa. +- Igor Pro 9.00 or later, running on Windows, with the ZeroMQ-XOP installed and + loaded. ``RELOAD CHANGED PROCS``, which ``reload_and_compile_procedures`` depends + on, was introduced in Igor Pro 9.00 and sets the actual minimum version. +- **``Packages/MIES/ZMQ_BridgeHelpers.ipf`` must be ``#include``-d and compiled into + whichever Igor Pro experiment the bridge talks to.** Unlike the old COM transport + (which worked against a stock Igor Pro installation with zero custom procedure + code), there is no bootstrap path over ZeroMQ itself -- if this file isn't + included/compiled, nothing is listening on the port at all, and every tool call + fails with ``IgorZmqUnreachable``. This repo's own ``Packages/MIES_Include.ipf`` + already does this permanently. For any *other* Igor Pro experiment: copy + ``ZMQ_BridgeHelpers.ipf`` onto its own procedure search path, add + ``#include "ZMQ_BridgeHelpers"`` to that experiment's own include list by hand, and + recompile -- a one-time, per-experiment setup step. See + :ref:`igor_pro_bridge_zbr_helpers` for what this file provides. +- Most tools require Igor Pro to already be running; the bridge connects to the + running instance's ZeroMQ socket. If needed, ``launch_igor_pro_unattended`` can + start Igor Pro itself (after ``configure_igor_launch``) -- see below. +- **No privilege-matching requirement of any kind.** This is the change v2.0.0 makes + over the old COM transport: Igor Pro and the bridge's Python process can each run + elevated or not, independently, with no effect on connectivity -- ZeroMQ is a plain + localhost TCP socket, not a Windows COM/RPC channel. - Python 3.10 or later, accessible as ``python`` on ``PATH``, with the pinned packages - in ``requirements.txt`` (``mcp==1.29.0``, ``pywin32==312``) installed into that same - environment -- see :ref:`igor_pro_bridge_installation` below for how. The packaged - extension does not vendor these. + in ``requirements.txt`` (``mcp==1.29.0``, ``pyzmq==27.1.0``, ``pywin32==312``) + installed into that same environment -- see :ref:`igor_pro_bridge_installation` + below for how. The packaged extension does not vendor these. ``pywin32`` is still a + dependency in v2.0.0 -- it is no longer used to talk to Igor Pro, but is still used + for ``dismiss_compile_error_dialog``'s window enumeration and for launching the + Igor Pro process. ``mcp`` is pinned below its breaking v2.0.0 line (released 2026-07-27/28, protocol revision 2026-07-28): v2 renamed ``FastMCP`` to ``MCPServer`` and moved it from @@ -96,10 +143,13 @@ servers in current Claude Desktop builds). pinned packages and complete pywin32's required post-install step (``Scripts\pywin32_postinstall.py -install``, which a plain ``pip install`` does not do). The script itself must run elevated only because that post-install step - registers COM-support DLLs into protected system locations -- it is *not* because - Claude Desktop or Igor Pro need to be elevated at runtime (they don't; see - Requirements above). ``install.ps1`` resolves ``python.exe`` from the Machine/User - ``PATH`` registry values directly rather than trusting the invoking shell's own + registers COM-support DLLs into protected system locations -- pywin32 is still a + dependency for OS-level window handling and process launching (see + :ref:`igor_pro_bridge_requirements`), even though this bridge no longer uses COM to + talk to Igor Pro. This elevation requirement is purely install-time and unrelated to + how you run Claude Desktop or Igor Pro afterward -- neither needs to be elevated at + runtime. ``install.ps1`` resolves ``python.exe`` from the Machine/User ``PATH`` + registry values directly rather than trusting the invoking shell's own possibly-customized ``$env:Path``, to mirror what a freshly launched Claude Desktop process actually sees regardless of its own elevation state (not necessarily the same interpreter an interactive console session would resolve, e.g. a @@ -107,11 +157,14 @@ servers in current Claude Desktop builds). stub that behaves differently once elevated); pass ``-PythonPath`` to override this if needed. See ``Get-Help ./install.ps1 -Full`` for the complete rationale and all steps performed. -- After installing (or after running ``install.ps1``), fully restart Claude Desktop (at - whichever privilege level you intend to run it and Igor Pro at -- both must match each - other, but neither has to be elevated) so the updated server code/environment is - actually picked up -- newly added tools, or a freshly installed dependency, can - otherwise lag behind what's on disk. +- Separately, ensure ``Packages/MIES/ZMQ_BridgeHelpers.ipf`` is ``#include``-d and + compiled into whatever Igor Pro experiment you intend to use with the bridge -- see + :ref:`igor_pro_bridge_requirements`. This repo's own experiments already have this + via ``MIES_Include.ipf``; any other experiment needs the one-time manual setup step + described there. +- After installing (or after running ``install.ps1``), fully restart Claude Desktop so + the updated server code/environment is actually picked up -- newly added tools, or a + freshly installed dependency, can otherwise lag behind what's on disk. - Call ``get_bridge_version()`` afterward and confirm its ``python_executable`` field matches the interpreter ``install.ps1`` installed into. If it doesn't, Claude Desktop resolved a different Python than ``install.ps1`` guessed -- re-run ``install.ps1 @@ -120,65 +173,197 @@ servers in current Claude Desktop builds). Available tools ---------------- -``execute_igor_command(command)`` - Runs a command string on Igor's command line via ``Execute2``. Include an - ``fprintf 0, "..."`` call to get data back. Returns a dict with ``"results"`` - (the ``fprintf`` output) and ``"history"`` (anything ``command`` sent to Igor's - history area during this call, e.g. ``print`` output or the command echo itself - -- confirmed from ``Automation Server.ihf``), so a ``print`` statement's output - can be verified directly from the return value, without needing a human to look - at Igor's screen. **Caution**: if ``command`` calls user-defined procedure code - and the Debugger is enabled, a breakpoint/runtime error/abort/stale-reference - pause will hang this call indefinitely -- there is no scriptable way to resume - or dismiss the Debugger window. Prefer ``execute_igor_command_unattended`` - whenever nobody is watching who could close that popup by hand. - -``execute_igor_command_unattended(command)`` +``execute_igor_command(command, timeout_seconds=30.0)`` + Runs a command string on Igor's command line. **Include a `print` call -- not + `fprintf 0, ...` -- to get data back** (confirmed live in v2.0.1: Igor's + ``CaptureHistory``, which this bridge's capture mechanism depends on, captures + ``print`` output but never captures ``fprintf``-to-history output at all, whether + directed at refnum 0, -1, or -2 -- the command runs without error either way, but an + ``fprintf``-only command silently returns empty ``"results"``/``"history"`` every + time). Returns a dict with ``"results"`` and ``"history"`` -- both now hold the + *same* captured text (see :ref:`igor_pro_bridge_v2_migration`: this transport + cannot isolate ``print``-only output from the full echoed history the way COM's + ``Execute2`` could isolate ``fprintf``-only output). Prefix ``command`` with + ``Silent 1;`` to suppress the echoed command text from the result. Implemented as + submit-then-poll + (``ZBR_SubmitCommand``/``ZBR_PollCommand``), since Igor's ``Execute`` cannot run + unqueued from inside a Function; ``timeout_seconds`` bounds how long this polls + before giving up. **Caution**: if ``command`` calls user-defined procedure code and + the Debugger is enabled, a breakpoint/runtime error/abort/stale-reference pause will + hang this call indefinitely (the poll loop keeps timing out and retrying, never + seeing it finish) -- there is no scriptable way to resume or dismiss the Debugger + window. Prefer ``execute_igor_command_unattended`` whenever nobody is watching who + could close that popup by hand. + +``execute_igor_command_unattended(command, timeout_seconds=30.0)`` Same as ``execute_igor_command``, but automatically disables Igor's Debugger before running the command and restores it afterward, even if the command raises. This is - the default choice for any unattended/automated call. On failure, both this and - ``execute_igor_command`` include any partial ``results``/``history`` output captured, - since Igor typically keeps running after an unhandled runtime error rather than - stopping (see :ref:`igor_pro_bridge_runtime_errors`). + the default choice for any unattended/automated call. + + **Neither of the two tools above is suitable for a command with an unknown or long + runtime** (more than roughly a minute) -- both block the entire MCP tool call while + polling, and the MCP transport itself has been observed to time out a single tool + call well under a minute regardless of ``timeout_seconds``. Use + ``submit_igor_command``/``poll_igor_command`` instead for anything long-running. + +``submit_igor_command(command)`` + Queues ``command`` for deferred execution (via ``ZBR_SubmitCommand``) and returns a + token immediately, without waiting for it to finish. **The tool to reach for when a + command's runtime is unknown or could be long -- minutes, hours, even weeks.** + Follow up with ``poll_igor_command(token)``, as many times as needed, spaced + however far apart in time is convenient. + + Reliable over arbitrarily long horizons because all of the actual state (a done + flag and the captured output text) lives entirely in Igor Pro's own data waves + (``root:Packages:ZBR:done``/``resultText``), not in this bridge's own Python + process -- polling later does not depend on this bridge process staying alive, on + Claude Desktop staying open, or on any particular amount of time having passed. The + one thing that *does* end the job is Igor Pro itself quitting, crashing, or + restarting -- at that point the underlying computation is gone regardless of + whether the token can still technically be looked up. + + **Caution**: same Debugger-pause risk as ``execute_igor_command``, but more + consequential here, since a long-running job is by definition likely to be + unattended -- a pause partway through leaves ``poll_igor_command`` reporting + "not done" forever, indistinguishable from the command still genuinely running. + **Use ``submit_igor_command_unattended`` instead for anything long-running.** + + **v2.2.0 reliability fix**: separately from the Debugger, ``command`` failing to + parse or hitting a genuine Igor-level runtime error partway through used to have + the exact same "hangs forever" effect, for a different reason -- confirmed live. + Fixed by queuing the command and the internal finish-callback as independent + ``Execute/P`` entries instead of one joined string (see SESSION_NOTES.md for the + full live-tested root cause). ``poll_igor_command`` is now guaranteed to + eventually report ``done: true`` regardless of whether ``command`` succeeded, + errored, or failed to parse. **Known residual gap**: there is still no generic + way to tell "ran and legitimately printed nothing" apart from "errored with no + output" -- both come back as an empty/short result with no error indication + (an attempted ``GetRTError()``-based fix was tried and confirmed live not to + work). Have ``command`` ``print`` an explicit sentinel if success needs to be + verifiable. + + **v2.2.3 fix**: the finish-callback (``ZBR_FinishToken``) captures its target row + index at submission time but runs later, in its own deferred entry -- if the + underlying storage waves are ever resized smaller in between (confirmed live + during this bridge's own maintenance/cleanup work), the callback's write used to + throw an uncaught "Index out of range" error and pop a modal dialog, exactly like + the v2.2.1 bug. Now bounds-checked: if the row no longer exists, the callback + silently does nothing, and a later ``poll_igor_command`` on that token correctly + reports ``"ERROR: unknown token ..."`` instead of hanging or crashing. + +``submit_igor_command_unattended(command)`` + Same as ``submit_igor_command``, but disables Igor's Debugger for the duration of + ``command`` and restores it afterward (via ``ZBR_SubmitCommandUnattended``). **The + recommended tool for anything long-running** -- without it, a Debugger pause has no + periodic signal distinguishing it from genuine progress, only silence. + +``poll_igor_command(token)`` + Checks whether a command submitted via ``submit_igor_command``/ + ``submit_igor_command_unattended`` has finished, and returns its captured output if + so. Returns ``{"done": false}`` while still pending -- call again later, with no + limit on how long to wait or how many times to poll. Returns + ``{"done": true, "results": , "history": }`` once finished (both keys + hold the same text, matching ``execute_igor_command``'s own return shape -- see its + entry above for why "results"/"history" can't be kept separate over this + transport, and why ``print``, not ``fprintf``, is what actually gets captured). + Raises if ``token`` isn't recognized -- e.g. a typo, or a token from an Igor Pro + instance that has since quit/restarted (tokens do not survive Igor Pro itself + restarting, only this bridge process or Claude Desktop restarting). For a job + expected to run over hours to weeks, consider a scheduled task that calls this + periodically rather than relying on the conversation staying open. Does **not** + raise just because the submitted command itself failed to parse or errored -- + see ``submit_igor_command``'s v2.2.0 note above. ``read_session_history(stop=False)`` - Reads back everything sent to Igor's history area since this bridge process - first talked to Igor, via Igor's built-in ``CaptureHistoryStart()``/ - ``CaptureHistory()`` functions (confirmed from ``Igor Reference.ihf``) -- a - capture starts automatically on first use. Unlike the per-call ``history`` field - above, this can verify *past* executions retroactively (e.g. if a command's - return value wasn't captured at the time, or to see everything printed across - many separate calls at once). Each call returns the full accumulated text since - the capture started, so repeated calls are always safe. ``stop=True`` ends the - current capture and starts a fresh one on next use. + Reads back everything sent to Igor's history area since this bridge's capture + started (via ``ZBR_ReadSessionHistory``, backed by Igor's built-in + ``CaptureHistoryStart()``/``CaptureHistory()`` functions) -- a capture starts + automatically, Igor-side, on first use. This can verify *past* executions + retroactively (e.g. to see everything printed across many separate calls at once). + Each call returns the full accumulated text since the capture started, so repeated + calls are always safe. ``stop=True`` ends the current capture and starts a fresh + one on next use. ``get_wave(wave_path)`` - Returns the data of an existing 1D Igor wave (numeric or text) as a list. Complex and - multi-dimensional waves are not supported. - -``load_experiment(file_path)`` - Loads an Igor Pro experiment file (``.pxp``) into the running instance, replacing - whatever experiment is currently open -- equivalent to File -> Open Experiment. - Calls the COM ``IApplication.LoadExperiment`` method directly (with - ``loadType=ipLoadTypeOpen``) rather than going through ``Execute2``, since neither - ``LoadExperiment`` nor ``OpenFile`` exist anywhere in Igor's own procedure/macro - language (confirmed against ``Igor Reference.ihf``) -- they are Automation-only - methods, the same way ``Quit`` turned out to be. Does **not** save changes to the - currently-open experiment first; call ``execute_igor_command('SaveExperiment')`` - beforehand if that matters. Disables the Debugger for the duration of the call - and restores it afterward, since loading an experiment runs its recreation - procedures and startup hooks (e.g. MIES's ``IgorStartOrNewHook``) and this call - bypasses the usual ``_execute2``-based Debugger protection. Call - ``get_environment_summary()`` afterward, since loading a different experiment can - change everything about the live environment. + Returns an existing Igor wave's full data and metadata: ``type``, ``dim_size`` + (1 to 4 dimensions), ``data`` (nested Python lists matching the wave's own + dimensionality), ``unit``, ``note``, and per-dimension ``dimension`` info. Supports + any dimensionality, and real, complex, text, and wave-reference waves -- a + **capability expansion over v1.x**, which was limited to 1D real-valued waves read + one point at a time via COM. The entire wave comes back in a single round trip, + natively serialized by the ZeroMQ-XOP. + +``load_experiment(file_path, wait_for_ready_seconds=30.0, process_exit_timeout_seconds=30.0)`` + Loads an Igor Pro experiment file (``.pxp``), replacing whatever is currently open. + **Behavior change from v1.x**: COM's ``IApplication.LoadExperiment`` hot-swapped the + experiment inside the same running Igor Pro process; that method has no + procedure-language equivalent (confirmed: neither ``LoadExperiment`` nor + ``OpenFile`` appear anywhere in ``Igor Reference.ihf``, only in + ``Automation Server.ihf``), so this transport cannot replicate it. Instead, this + tool asks the running instance to quit (``Quit/N``, submitted via + ``ZBR_SubmitCommand``), waits for the underlying Igor64.exe **OS process** to fully + exit, then relaunches the configured Igor Pro executable (see + ``configure_igor_launch``) with ``/UNATTENDED`` plus the target file path as a + launch argument, and polls for the new instance to become reachable. This is a + genuine process restart, not an in-place swap -- **unsaved changes in the + currently-open experiment are lost**; call ``execute_igor_command('SaveExperiment')`` + first if that matters. Requires ``configure_igor_launch`` to have been called first. + Call ``get_environment_summary()`` afterward, since loading a different experiment + changes everything about the live environment. + + **v2.0.1 fix, root-caused from a live silent-failure report**: an earlier version of + this tool waited only for Igor Pro to stop answering ``ZBR_Ping`` over ZeroMQ as its + signal that the old process was gone, then immediately launched the replacement + ``Igor64.exe /UNATTENDED `` command line. This does not work: ZeroMQ goes + quiet well before the underlying OS process actually terminates (Igor can take + several seconds to fully exit after ``Quit/N`` runs), and launching the replacement + command line while the old process is still alive -- even mid-shutdown -- does not + spawn a new process at all. Windows/Igor's single-instance-per-user behavior instead + either (a) redirects the launch into the still-live old instance, which can pop an + unhandled "save changes?" dialog if it happens to have unsaved edits, or (b) if the + old instance is already mid-quit, silently drops the request altogether -- which + looks exactly like the relaunch had no effect whatsoever, with nothing to diagnose + (no error, no new process, `check_bridge_health()` just reports unreachable + indefinitely). Fixed by polling the actual Windows process list (matching the + configured executable's own file name, e.g. ``"Igor64.exe"``) until no such process + remains, up to ``process_exit_timeout_seconds``, before ever invoking the relaunch + command line; if that timeout elapses with the process still present, the tool now + raises rather than silently proceeding into the same failure mode -- the most likely + cause being a stuck "save changes?" dialog on the *old* instance, which needs a human + to resolve by hand. + + **v2.2.1 fix, root-caused from another live report**: reloading a saved experiment + via this tool (or a human manually reopening a ``.pxp``) could leave the whole + bridge unreachable, requiring a human to dismiss a modal Igor error dialog reading + "While executing CaptureHistory, the following error occurred: there is no open + file with this reference number". Root cause: this bridge's history-capture + mechanism stores its ``CaptureHistoryStart()`` reference number in a plain + ``Variable/G`` global, which Igor persists into a saved experiment like any other + global -- but the refnum is only meaningful within the OS process that created it, + so reloading brought back a stale-but-present value that the old code trusted + simply because it existed. Fixed Igor-side (``ZBR_EnsureCaptureStarted`` in + ``ZMQ_BridgeHelpers.ipf``): the stored refnum is now validated with a + ``try``/``catch``/``endtry`` block before being trusted, and silently replaced with + a fresh capture if it's stale, rather than ever surfacing this to the user. No + Python-side change was needed. See ``SESSION_NOTES.md`` for the full live-tested + root cause and the two Igor syntax mistakes caught and fixed along the way. + + **v2.2.2 refinement** (contributed by the repo owner after installing v2.2.1): the + stale-refnum probe call and its ``AbortOnRTE`` are kept on the SAME line rather + than split across two, because Igor's Debug on Error check happens at the end of + each *line*, not each statement -- on separate lines, a user with Debug on Error + enabled would get a Debugger popup right when the stale refnum's runtime error + occurred, before ``AbortOnRTE`` had a chance to convert it into a catchable abort. + Confirmed live (before and after) by temporarily enabling ``debug_on_error`` and + repeating the corrupted-refnum test: only the same-line version stays silent. ``check_bridge_health()`` - Diagnoses exactly why the bridge can't reach Igor Pro, distinguishing three separate - failure modes: a privilege-level mismatch between this process and Igor Pro (one - elevated, one not), no Igor Pro COM object registered at all, and a registered-but-dead - COM object (Igor crashed or was force-closed, leaving a stale registration that - reconnecting alone can't fix). Run this first - whenever something doesn't work. + Diagnoses whether the bridge can reach Igor Pro's ZeroMQ server right now. Unlike + the old COM-based version, this can no longer cleanly distinguish "Igor Pro isn't + running" from "``ZMQ_BridgeHelpers.ipf`` isn't included/compiled/bound" from "wrong + port/firewall" -- a ZeroMQ REQ socket that gets no reply at all looks the same in + all three cases; the ``"problem"`` field lists all three as things to check by + hand. Run this first whenever something doesn't work. ``get_bridge_version()`` Returns the version of this Igor Pro Bridge build that is actually running in the @@ -186,67 +371,46 @@ Available tools running with:: { - "version": "1.25.0", + "version": "2.1.0", "python_executable": "C:\\Python312\\python.exe", "python_version": "3.12.4", "mcp_package_version": "1.29.0", - "pywin32_build": "312" + "pyzmq_version": "27.1.0" } - Added because there was previously no way to confirm from inside a conversation - which ``.mcpb`` build ended up loaded after an install/restart -- useful before - relying on a specific recent fix or behavior change. The ``python_executable`` field - is also the authoritative way to confirm which Python environment Claude Desktop - actually launched the bridge with, e.g. to cross-check against what - ``install.ps1`` installed into -- see :ref:`igor_pro_bridge_installation`. + Useful before relying on a specific recent fix or behavior change, or to confirm + which ``.mcpb`` build ended up loaded after an install/restart. The + ``python_executable`` field is also the authoritative way to confirm which Python + environment Claude Desktop actually launched the bridge with, e.g. to cross-check + against what ``install.ps1`` installed into -- see + :ref:`igor_pro_bridge_installation`. ``check_compilation_state()`` - Reports whether Igor's procedure code is currently compiled or uncompiled, using the - same technique as ``IsProcGlobalCompiled()`` in - ``Packages/igortest/procedures/igortest-test-compilation.ipf``. + Reports whether Igor's procedure code is currently compiled or uncompiled, via + ``ZBR_IsCompiled()`` (the same ``FunctionInfo``-based technique as + ``IsProcGlobalCompiled()`` in + ``Packages/igortest/procedures/igortest-test-compilation.ipf``). Since ``ZBR`` + compiles as its own independent module, a ``true`` result here specifically reflects + ProcGlobal's compile state -- reaching ``ZBR`` at all over ZeroMQ already implies + ``ZBR`` itself is compiled. ``reload_and_compile_procedures()`` Forces Igor to reload changed ``.ipf`` files from disk (``RELOAD CHANGED PROCS``) and - attempt a fresh compilation (``COMPILEPROCEDURES``), then reports the resulting - compiled state. Use this after editing a ``.ipf`` file directly on disk. Both - commands go through Igor's operation queue rather than running immediately (see - "Operation Queue" in ``Advanced Topics.ihf``), so this cross-checks two independent - signals before trusting a "compiled" result -- see - :ref:`igor_pro_bridge_claude_helper` and :ref:`igor_pro_bridge_compile_dialog`. If - compilation still isn't confirmed after the initial poll, this automatically makes - one attempt to dismiss a possible stuck compile-error dialog (see - ``dismiss_compile_error_dialog``) before falling back to - ``"prompt_user_to_check_for_dialog": true``. **Caution**: twice during this bridge's - development, a real Igor Pro 10.03 instance became unreachable via COM (crashed or - was closed) shortly after a reload/compile attempt -- no root cause was confirmed, - and it isn't established whether this is related to the bridge at all versus a - pre-existing Igor Pro stability issue (a subsequent retest against Igor Pro 9.06 - ran the same sequence -- broken code, reload/compile, fix, reload/compile again -- - without a repeat crash, which is reassuring but not conclusive either way). If a - subsequent call fails with a COM/RPC error, check ``check_bridge_health()`` and be - prepared to relaunch Igor Pro. - -``ensure_igor_pro_bridge_defined(marker_function="")`` - Checks whether the ``IGOR_PRO_BRIDGE`` conditional-compilation symbol is defined in the - current Igor Pro instance and, if not -- e.g. a fresh Igor Pro environment this bridge - has never touched before, such as a bare "Untitled" experiment -- defines it itself via - ``SetIgorOption poundDefine=IGOR_PRO_BRIDGE`` and forces a recompile - (``COMPILEPROCEDURES``, reusing ``reload_and_compile_procedures``'s own two-signal - polling), rather than requiring a human to hand-edit the experiment's Procedure window - first. **Generic by design**: this tool has no built-in knowledge of MIES or any other - specific codebase -- it only manages the ``IGOR_PRO_BRIDGE`` symbol itself, so it works - for any Igor Pro experiment that adopts the ``#ifdef IGOR_PRO_BRIDGE`` convention for its - own bridge-support code, not just this repo's ``MIES_ClaudeHelper.ipf`` (see - :ref:`igor_pro_bridge_claude_helper`). The optional ``marker_function`` argument lets a - caller who *does* know about a specific gated function (e.g. - ``"CH_ListXOPExports"`` for ``MIES_ClaudeHelper.ipf``) get an extra before/after - ``FunctionInfo(...)`` confirmation that it actually became available, without that name - being hardcoded into the bridge -- if ``IGOR_PRO_BRIDGE`` is already defined but the named - function still doesn't resolve, that means whatever procedure file defines it simply - isn't ``#include``-d by whatever is currently loaded, which this tool cannot fix by - redefining the symbol again. Only ever adds the define, never calls ``poundUndefine``. - See :ref:`igor_pro_bridge_claude_helper` for the full ``SetIgorOption``/global-symbol-list - background. + attempt a fresh compilation (``COMPILEPROCEDURES``, via ``ZBR_SubmitReloadAndCompile``), + then reports the resulting compiled state. Use this after editing a ``.ipf`` file + directly on disk. Both commands go through Igor's operation queue rather than + running immediately (see "Operation Queue" in ``Advanced Topics.ihf``), so this + cross-checks two independent signals before trusting a "compiled" result -- see + :ref:`igor_pro_bridge_zbr_helpers` and :ref:`igor_pro_bridge_compile_dialog`. Poll + errors (the bridge briefly unable to reach Igor mid-recompile) are treated as "not + ready yet" rather than fatal. If compilation still isn't confirmed after the poll + times out, this automatically makes one attempt to dismiss a possible stuck + compile-error dialog (see ``dismiss_compile_error_dialog``). **Caution**: on more + than one occasion during this bridge's development (both under the COM transport + and, circumstantially, still suspected under ZeroMQ), Igor Pro became unreachable + shortly after a reload/compile attempt -- root cause unconfirmed; treat any failure + on a subsequent call as a signal to check ``check_bridge_health()`` and be prepared + to relaunch Igor Pro. See ``SESSION_NOTES.md`` for the ongoing investigation. ``dismiss_compile_error_dialog()`` Attempts to close a stuck Igor Pro dialog by posting a simulated Escape key press @@ -279,10 +443,17 @@ Available tools ``#include``/``#define`` directives not present in any on-disk ``.ipf`` file), the top-level global data folder layout, and the current Debugger settings. -``read_help_file(file_path)`` +``read_help_file(file_path, timeout_ms=30000)`` Reads an Igor Pro help file (``.ihf``) as structured, formatted text -- e.g. to confirm an operation's exact flags/behavior straight from Igor's own docs -- without - leaving any lasting change to Igor's help-window state. Better than an OS-level file + leaving any lasting change to Igor's help-window state. ``timeout_ms`` defaults to + 30s rather than this bridge's usual 5s (**fixed in v2.0.1** after a live timeout + reading the entire "Igor Reference.ihf" manual -- exporting a genuinely large help + file as HTML can take longer than 5s even though the export itself succeeds + Igor-side regardless; a timed-out client also leaves the ZeroMQ-XOP logging a + harmless but noisy "Host unreachable" error to history when it tries to reply to a + socket that already gave up, visible via ``read_session_history`` if this happens). + Pass a larger value still for unusually large help files. Better than an OS-level file read for two reasons. First, Igor pre-registers every ``.ihf`` file in the Help Files folder as an open help window (visible or hidden, ``WinList``'s ``WIN:512`` bit), and a help-file view and a plain-notebook view of the same file are mutually exclusive @@ -304,41 +475,42 @@ Available tools ``configure_igor_launch(exe_path)`` Records the full path to the Igor Pro executable to use for - ``launch_igor_pro_unattended``, for the rest of this bridge process's session. - There is no default or guessed path -- whatever agent is driving the bridge should - ask the user for this once, at the start of a session that might need to launch - Igor Pro, since the install location and version vary (this repo alone has been - tested against separately-named Igor Pro 9 and Igor Pro 10 installs). Session-scoped - like the history-capture refnum: resets if the bridge process itself restarts. - Returns the resolved path plus an ``"elevation_plan"`` describing which of the two - launch paths ``launch_igor_pro_unattended`` will take (see below) based on whether - this Python process is currently elevated. + ``launch_igor_pro_unattended``/``load_experiment``, for the rest of this bridge + process's session. There is no default or guessed path -- whatever agent is driving + the bridge should ask the user for this once, at the start of a session that might + need to launch Igor Pro, since the install location and version vary (this repo + alone has been tested against separately-named Igor Pro 9 and Igor Pro 10 installs). + Session-scoped: resets if the bridge process itself restarts. ``launch_igor_pro_unattended(wait_for_ready_seconds=30.0)`` Launches the configured executable with the ``/UNATTENDED`` command-line flag (see :ref:`igor_pro_bridge_unattended_flag` below) and polls for it to become reachable - via COM. Requires ``configure_igor_launch`` to have been called first in the same - session. Refuses to launch (returns ``"launched": false`` rather than raising) if - an Igor Pro instance is already reachable via COM, since launching the executable + over ZeroMQ. Requires ``configure_igor_launch`` to have been called first in the + same session. Refuses to launch (returns ``"launched": false`` rather than raising) + if something already answers ``ZBR_Ping`` right now, since launching the executable again with only ``/UNATTENDED`` (no ``/I``, ``/X``, ``/SN``, or file-path argument) is documented to start a genuinely new instance rather than reuse the existing one - -- see "Calling Igor from Scripts" in ``Advanced Topics.ihf``. If this process is - already elevated, Igor Pro is launched as a direct child process, which inherits - that elevation automatically with no prompt; if not, it launches via - ``ShellExecute``'s ``"runas"`` verb instead, triggering a normal Windows UAC consent - dialog -- but this process itself remains unelevated afterward, so COM calls will - keep failing (see ``check_bridge_health``) due to the resulting privilege-level - mismatch, until Claude Desktop itself is relaunched at a matching level (elevated, - to match the now-elevated Igor Pro). The direct-child-process path also patches - ``COMSPEC`` into the child's environment if this Python process's own - environment is missing it -- confirmed necessary this session: without it, - MIES's own startup hook (``IgorStartOrNewHook`` -> ... -> - ``ExecuteGitForMIESVersion``, which shells out to git via - ``ExecuteScriptText`` using ``GetCmdPath()``/``COMSPEC`` to find ``cmd.exe``) - asserted on every launch via this path with *"We have git installed but could - not regenerate version.txt"*, even though a normal double-click/Start Menu - launch never hits it (an interactive login session always has ``COMSPEC`` - set). See ``SESSION_NOTES.md`` for the full diagnosis. + -- see "Calling Igor from Scripts" in ``Advanced Topics.ihf``. + + **Always launches as a plain child process** (``subprocess.Popen``), at whatever + privilege level the bridge's own Python process is running at -- no elevation + request, no UAC prompt, ever. This is the key v2.0.0 change: the old COM-based + version branched on whether this process was already elevated, using a direct + child-process launch (inheriting elevation with no prompt) when it was, or + ``ShellExecute``'s ``"runas"`` verb (triggering a UAC consent dialog, and leaving + this process itself still unelevated afterward -- see :ref:`igor_pro_bridge_v1_history`) + when it wasn't. Neither branch is needed anymore: ZeroMQ has no privilege-matching + requirement at all, so there is nothing left to branch on. + + Patches ``COMSPEC`` into the child's environment if this Python process's own + environment is missing it -- confirmed necessary during this bridge's development: + without it, MIES's own startup hook (``IgorStartOrNewHook`` -> ... -> + ``ExecuteGitForMIESVersion``, which shells out to git via ``ExecuteScriptText`` + using ``GetCmdPath()``/``COMSPEC`` to find ``cmd.exe``) asserted on every launch via + this path with *"We have git installed but could not regenerate version.txt"*, even + though a normal double-click/Start Menu launch never hits it (an interactive login + session always has ``COMSPEC`` set). See ``SESSION_NOTES.md`` for the full + diagnosis. .. _igor_pro_bridge_unattended: @@ -346,7 +518,7 @@ Unattended execution caveats ----------------------------- Two independent things can silently stall an automated Claude/Igor session. Neither -hangs the bridge's own COM calls directly -- both instead leave Igor showing a GUI +hangs the bridge's own ZeroMQ calls directly -- both instead leave Igor showing a GUI element that only a human can dismiss. Debugger pauses @@ -356,9 +528,11 @@ If the Debugger is enabled and something trips it (a breakpoint, a runtime error "Debug on Error", a user abort, or a stale NVAR/SVAR/WAVE reference), Igor pauses and opens the Debugger window. There is no documented operation to programmatically resume, step, or dismiss that pause -- ``Debugger``/``DebuggerOptions`` are the only two -documented operations, and neither has a "continue" mode. The specific COM call that -triggered the pause then blocks forever, since ``Execute2`` is synchronous. Other new -COM calls still get answered while paused (Igor's command line stays reentrant), but the +documented operations, and neither has a "continue" mode. The submitted command that +triggered the pause never reports as finished, so +``execute_igor_command``/``execute_igor_command_unattended``'s poll loop keeps timing +out and retrying forever. Other new ``CallFunction`` calls still get answered while +paused (Igor's command line stays reentrant), but the original call, and anything waiting on it, is stuck for good. Mitigation: ``execute_igor_command_unattended`` disables the Debugger for the duration @@ -372,24 +546,30 @@ Compile-error dialogs ~~~~~~~~~~~~~~~~~~~~~~ Separately, a failed ``COMPILEPROCEDURES`` can leave a compile-error dialog open. This -does not hang the bridge's COM calls (they keep returning normally), but it does block -Igor's operation queue from ever draining -- confirmed from ``Advanced Topics.ihf``, -"Operation Queue": "Igor services the operation queue when no procedures are running -and the command line is empty." A modal dialog means Igor is never idle, so -``RELOAD CHANGED PROCS``/``COMPILEPROCEDURES`` queued by a later call sit there without -ever actually running -- ``reload_and_compile_procedures`` will keep reporting -"not compiled" even after the underlying ``.ipf`` file is genuinely fixed, until a -person closes that dialog by hand. - -There is no documented way to detect or dismiss this dialog via COM, but Escape -closes it. ``dismiss_compile_error_dialog()`` exploits that: it enumerates top-level -windows for a visible one owned by an Igor Pro process whose title matches a known -stuck-dialog title, then posts ``WM_KEYDOWN``/``WM_KEYUP`` for Escape directly to it -via ``PostMessage`` -- no foreground switch, no stolen focus. This works despite -Igor Pro's elevated status specifically because this bridge's own process is also -running elevated to match (see Requirements above) -- Windows' UIPI blocks simulated -input from a lower-privilege process reaching a higher-privilege one, but not -between two processes at the same privilege level. +does not hang the bridge's ZeroMQ calls (they keep returning normally), but it does +block Igor's operation queue from ever draining -- confirmed from ``Advanced +Topics.ihf``, "Operation Queue": "Igor services the operation queue when no +procedures are running and the command line is empty." A modal dialog means Igor is +never idle, so ``RELOAD CHANGED PROCS``/``COMPILEPROCEDURES`` queued by a later call +sit there without ever actually running -- ``reload_and_compile_procedures`` will keep +reporting "not compiled" even after the underlying ``.ipf`` file is genuinely fixed, +until a person closes that dialog by hand. + +There is no documented way to detect or dismiss this dialog via the ``CallFunction`` +protocol (Igor's own compiled code can't observe or interact with its own modal +dialogs), but Escape closes it. ``dismiss_compile_error_dialog()`` exploits that: it +enumerates top-level windows for a visible one owned by an Igor Pro process whose +title matches a known stuck-dialog title, then posts ``WM_KEYDOWN``/``WM_KEYUP`` for +Escape directly to it via ``PostMessage`` -- no foreground switch, no stolen focus. +This is pure OS-level window handling (``pywin32``), independent of whichever +transport talks to Igor Pro's procedure code. **One caveat carried over from the old +COM-based elevation requirement, now the other way around**: since v2.0.0 no longer +requires the bridge to run elevated, if a user chooses to run Igor Pro elevated for +some unrelated reason while the bridge itself is not, Windows' UIPI will block this +posted key press from reaching Igor Pro's window at all (simulated input from a +lower-privilege process cannot reach a higher-privilege one) -- in that specific case, +the bridge process itself would need to be run elevated too for this one tool to +keep working, even though nothing else about the ZeroMQ transport requires it. **Confirmed live against a real stuck dialog on both Igor Pro 10.03 and Igor Pro 9.06**: the original assumption that this dialog is an ordinary ``"#32770"`` @@ -415,11 +595,15 @@ calls this automatically once, before falling back to asking a human. If the automatic attempt doesn't resolve it (or wasn't possible -- e.g. no matching dialog window was found), ``reload_and_compile_procedures``'s result includes -``"prompt_user_to_check_for_dialog": true``; whatever is driving the bridge (e.g. an AI -agent) should use this as an explicit instruction to ask the human operator to check for -and close a stuck dialog, rather than silently retrying or only logging advisory text -- -that distinction was confirmed in practice to be what actually keeps an -agent-driven/unattended workflow moving. +``"auto_dismiss_attempted"`` (the full ``dismiss_compile_error_dialog()`` result, so +its ``"attempted"``/``"igor_windows_seen"`` fields can be inspected directly) plus a +``"note"`` explaining that this looks like a genuine compile error rather than a +timing artifact. Whatever is driving the bridge (e.g. an AI agent) should treat a +``"compiled": false`` result with ``"auto_dismiss_attempted": {"attempted": false, ...}`` +as a signal to ask the human operator to check for and close a stuck dialog by hand, +rather than silently retrying or only logging advisory text -- that distinction was +confirmed in practice to be what actually keeps an agent-driven/unattended workflow +moving. .. _igor_pro_bridge_unattended_flag: @@ -433,16 +617,16 @@ operations," with two concrete documented examples: the About Autosave dialog, a (Igor Pro 10+) the license activation dialog. Nothing in Igor's help files ties it to compile errors specifically. -Empirically confirmed this session, however, against a live Igor Pro 9.06 instance -launched with ``/UNATTENDED``: it *also* suppresses the modal "Function Compilation -Error" dialog described above. A genuine syntax error introduced into an -actually-loaded procedure file produced ``reload_and_compile_procedures`` results of -``compiled: false`` / ``raw_function_info: "Procedures Not Compiled"``, while +Empirically confirmed against a live Igor Pro 9.06 instance launched with +``/UNATTENDED``: it *also* suppresses the modal "Function Compilation Error" dialog +described above. A genuine syntax error introduced into an actually-loaded procedure +file produced ``reload_and_compile_procedures`` results of ``compiled: false`` (per +both the ``ZBR_ReadCompileCounter`` and ``ZBR_IsCompiled`` signals), while ``dismiss_compile_error_dialog``'s diagnostic window enumeration found no dialog window at all -- only Igor's main window was visible. Instead, the compile error appears as a plain line in Igor's history area, in the form ``::: error: `` (e.g. -``MIES_ClaudeHelper.ipf:46:7: error: expected terminating quote``), fully readable +``ZMQ_BridgeHelpers.ipf:46:7: error: expected terminating quote``), fully readable via ``read_session_history``/the per-call ``history`` field. This makes an Igor Pro instance started with ``/UNATTENDED`` (e.g. via @@ -474,10 +658,11 @@ including side effects, all the way to the end of the function, unless something explicitly checks the flag (see "Runtime Error / Abort Handling Conventions" in :doc:`developers` for the project's ``AbortOnRTE``/``try``/``catch`` conventions). If nothing ever checks it, the flag persists until execution unwinds all -the way back to the top-level command boundary -- i.e. this bridge's ``Execute2`` -call -- which reports it as that call's own failure, carrying the *original* error code -and message. This boundary check also clears the flag afterward, so a failure here -never contaminates the next command. +the way back to the top-level command boundary -- i.e. the submitted command run via +``ZBR_SubmitCommand``/``ZBR_SubmitCommandUnattended`` -- which reports it as that +command's own failure, carrying the *original* error code and message. This boundary +check also clears the flag afterward, so a failure here never contaminates the next +command. The flag is "sticky": if two *different* unhandled runtime errors occur in sequence with nothing checking/clearing in between, only the *first* one is ever visible -- @@ -489,20 +674,46 @@ Practical consequence: a nonzero error code from ``execute_igor_command``/ but does **not** mean execution stopped there, and does **not** mean it was the only problem. -.. _igor_pro_bridge_claude_helper: +.. _igor_pro_bridge_zbr_helpers: + +ZMQ_BridgeHelpers.ipf and the ZBR module +------------------------------------------- -MIES_ClaudeHelper.ipf and the AfterCompiledHook -------------------------------------------------- +``Packages/MIES/ZMQ_BridgeHelpers.ipf``, included from ``MIES_Include.ipf``, is no +longer a throwaway prototype -- it is the permanent Igor-side dependency of this +bridge as of v2.0.0, providing every ``ZBR_*`` function ``server.py`` calls via +``CallFunction``. Its own header comment documents the manual, one-time ``#include`` +delivery model (see :ref:`igor_pro_bridge_requirements`): this repo's own +``MIES_Include.ipf`` includes it permanently, but any other experiment needs its own +copy plus its own ``#include`` added by hand. -``Packages/MIES/MIES_ClaudeHelper.ipf``, included from ``MIES_Include.ipf``, defines: +It compiles as its own independent module (``#pragma IndependentModule = ZBR``), +which is why every ``CallFunction`` name for one of its functions must be +``#``-qualified (``"ZBR#ZBR_Ping"``, not ``"ZBR_Ping"``) -- see +:ref:`igor_pro_bridge_v2_migration`. This also means a compile error inside +``ZMQ_BridgeHelpers.ipf`` itself fails the *whole* experiment's compile (an +independent module is not somehow exempt from that), even though it compiles +separately from ProcGlobal. + +Compile-confirmation counter, migrated from MIES_ClaudeHelper.ipf +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``ZMQ_BridgeHelpers.ipf`` defines a static ``AfterCompiledHook`` that increments +``root:gClaudeHelperCompileCounter`` (the global name, and the counter's role, carried +over unchanged from this repo's older ``MIES_ClaudeHelper.ipf``, which no longer +exists as a separate file): .. code-block:: igorpro static Function AfterCompiledHook() - + Variable modifiedBefore Variable/G root:gClaudeHelperCompileCounter NVAR gClaudeHelperCompileCounter = root:gClaudeHelperCompileCounter + modifiedBefore = GetDataFolderDF(...)... // ExperimentModified state captured here + + ZBR_EnsureZeroMQBound() + gClaudeHelperCompileCounter += 1 return 0 @@ -513,43 +724,58 @@ procedure windows have compiled successfully. Unlike polling ``FunctionInfo()`` non-existing function (which can read stale state before Igor's operation queue has actually drained -- see :ref:`igor_pro_bridge_compile_dialog`), this counter only ever changes at the exact moment Igor itself confirms a successful compile, so it is a -race-free confirmation signal. ``reload_and_compile_procedures`` reads a baseline before -issuing ``RELOAD CHANGED PROCS``/``COMPILEPROCEDURES`` and treats any increase as -immediate, trustworthy success, falling back to the ``FunctionInfo``-based poll when the -counter is unavailable. There is no equivalent hook for a *failed* compile. +race-free confirmation signal, read back via ``ZBR_ReadCompileCounter()``. +``reload_and_compile_procedures`` reads a baseline before issuing +``RELOAD CHANGED PROCS``/``COMPILEPROCEDURES`` and treats any increase as immediate, +trustworthy success, falling back to the ``ZBR_IsCompiled()``-based poll when the +counter is unavailable. There is no equivalent hook for a *failed* compile. Declared +``static`` so it coexists with any other file's own static ``AfterCompiledHook`` +without colliding. + +Auto-binding the ZeroMQ server socket on every compile +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The whole function body is gated behind ``#ifdef IGOR_PRO_BRIDGE`` so it compiles out -entirely for a normal end-user build. To activate it, add: +The same hook also calls ``ZBR_EnsureZeroMQBound()``, which (re)binds the ZeroMQ-XOP's +server socket to ``ZBR_ZEROMQ_ENDPOINT`` (``tcp://127.0.0.1:5680``) and starts its +handler every time Igor finishes a successful compile: .. code-block:: igorpro - #define IGOR_PRO_BRIDGE - -to the experiment's special "Procedure" window specifically -- Igor always compiles the -Procedure window first, so only a ``#define`` placed there is reliably visible to every -other file's ``#ifdef`` checks; a ``#define`` in an ordinary ``.ipf`` file has no such -guarantee. - -That manual step can be skipped entirely by calling -``ensure_igor_pro_bridge_defined(marker_function="CH_ListXOPExports")`` instead (the -``marker_function`` argument is specific to this example -- the tool itself has no -knowledge of ``MIES_ClaudeHelper.ipf`` or ``CH_ListXOPExports`` built in; see the tool -reference above): per the "Conditional Compilation" topic in ``Programming.ihf``, -``SetIgorOption poundDefine=IGOR_PRO_BRIDGE`` adds the symbol to a separate *global* -list "available in all procedure windows (including independent modules)" -- broader -than even the Procedure-window scope described above -- queryable via -``SetIgorOption poundDefine=IGOR_PRO_BRIDGE?`` (sets ``V_flag``) and reversible via -``SetIgorOption poundUndefine=IGOR_PRO_BRIDGE``. Confirmed from the same topic and -cross-checked in ``Advanced Topics.ihf``: this is session-only (not saved into the -experiment, lost on Igor Pro restart) and itself triggers a recompile -(``BeforeUncompiledHook`` fires with ``changeCode`` 6 for ``poundDefine``/7 for -``poundUndefine``). Per ``Igor Reference.ihf``'s ``SetIgorOption`` entry, the operation -"is not compilable" and needs ``Execute`` from inside compiled code -- irrelevant here, -since the bridge always sends it as an interpreted command-line statement. - -``AfterCompiledHook`` is declared ``static`` so it coexists with any other file's own -static ``AfterCompiledHook`` (e.g. the one in ``MIES_Include.ipf`` used only for the -too-old-Igor warning panel) without colliding. + static Function ZBR_EnsureZeroMQBound() + Variable err + zeromq_server_bind(ZBR_ZEROMQ_ENDPOINT); err = GetRTError(1) + zeromq_handler_start(); err = GetRTError(1) + return 0 + End + +Two corrections were made to the first draft of this function, both confirmed to +matter in practice: it does **not** call ``zeromq_stop()`` first (that would tear down +and corrupt any *other* ZeroMQ binds already active in the same experiment on every +recompile -- e.g. MIES's own, currently short-circuited, ZeroMQ subsystem on a +different port); and each XOP call is followed by ``; err = GetRTError(1)`` on the +*same line* specifically to suppress a theoretical Debugger popup, since "Debug on +Error" only checks for a pending runtime-error state at the end of a line, not +mid-statement. ``ZBR_EnsureZeroMQBound()`` is called from ``AfterCompiledHook`` +immediately after the pre-compile ``ExperimentModified`` state is captured (needed so +that binding a socket -- itself an experiment-modifying action, from Igor's +perspective -- doesn't get misattributed as user-driven unsaved-changes state). + +Why this replaces the old ``#ifdef IGOR_PRO_BRIDGE`` convention +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The v1.x COM-based bridge's ``MIES_ClaudeHelper.ipf`` gated its entire function body +behind ``#ifdef IGOR_PRO_BRIDGE``, activated either by hand-editing the experiment's +Procedure window or via the (now-retired) ``ensure_igor_pro_bridge_defined`` tool -- +see :ref:`igor_pro_bridge_v1_history`. ``ZMQ_BridgeHelpers.ipf``'s independent-module +architecture has no equivalent gating at all: everything in the ``ZBR`` module +compiles unconditionally whenever the file is included, by design (the whole point of +an independent module is that its own compilation doesn't depend on ProcGlobal's +``#define`` state). This is strictly simpler for this bridge's own purposes, but it is +also *why* the one-time manual ``#include`` step in +:ref:`igor_pro_bridge_requirements` can't be skipped programmatically the way the old +``#define`` could -- there is no bootstrap tool call over ZeroMQ that could add an +``#include`` directive to a `.ipf` file on disk and trigger Igor to notice it; that +step is inherently a one-time, human, on-disk action. Known limitations ------------------ @@ -562,19 +788,73 @@ Known limitations safely reports "not found" and a human is still needed -- title matching only, deliberately, since matching any generic native dialog risked dismissing an unrelated one (e.g. a save-changes confirmation). -- ``get_wave`` supports 1D, real-valued waves only. -- The pywin32 dynamic-dispatch calling convention for ``Execute2``'s multiple ``[out]`` - parameters is assumed to follow the standard IDispatch convention (parameters come - back as a tuple appended to the return value); this matches observed behavior in - practice. +- ``execute_igor_command``/``execute_igor_command_unattended`` can no longer separate + ``print``-only output from the full echoed history the way COM's ``Execute2`` could + isolate ``fprintf``-only output -- both ``"results"`` and ``"history"`` now hold the + same captured text (see :ref:`igor_pro_bridge_v2_migration`). Also, + **use `print`, never `fprintf 0/-1/-2, ...`, to get data back** -- confirmed live + (v2.0.1) that ``fprintf``-to-history output is never captured at all by this + transport's ``CaptureHistory``-based mechanism, regardless of which refnum it + targets. +- ``load_experiment`` needs the OLD Igor Pro process to fully exit (not just stop + answering over ZeroMQ) before relaunching with the new file -- fixed in v2.0.1 after + a live silent-failure report; see that tool's reference entry above for the full + mechanism. +- ``load_experiment`` is a real process restart (quit + relaunch with a file-path + argument), not an in-place experiment swap -- unsaved changes in the previously-open + experiment are lost. See that tool's own reference entry above. +- The ZeroMQ-XOP's Router (server) socket documents a default + ``ZMQ_MAXMSGSIZE`` of 1024 bytes; whether this caps incoming requests only, or also + outgoing replies, has not been confirmed. Treated as a risk specifically for + ``read_help_file``'s underlying ``ZBR_ReadHelpFile``, which sidesteps the question + entirely by writing the exported HTML to a local temp file (both Igor Pro and the + bridge run on the same machine) and reading it back off disk, rather than returning + the HTML content through the ``CallFunction`` reply itself. No other tool in this + bridge returns a payload large enough for this to plausibly matter, but it hasn't + been stress-tested. - This is a *local* MCP server (stdio transport): it only works from a Claude Desktop session running on the same Windows machine as Igor Pro, not from a cloud/sandboxed session. -- **Observed twice during development on Igor Pro 10.03, root cause unconfirmed**: - Igor Pro became unreachable via COM (crashed or was closed) shortly after a - ``reload_and_compile_procedures`` call. It isn't established whether this is - related to the bridge's own actions or a pre-existing Igor Pro stability issue - independent of it. A repeat of the same broken-code/reload/fix/reload sequence - against Igor Pro 9.06 did not reproduce it, which doesn't rule out a 10.03-specific - or environment-specific cause -- treat any COM/RPC failure after a compile attempt - as a signal to check ``check_bridge_health()`` and be prepared to relaunch Igor Pro. +- **Observed on more than one occasion during this bridge's development (both under + the v1.x COM transport, and circumstantially suspected under v2.0.0's ZeroMQ + transport), root cause unconfirmed**: Igor Pro became unreachable (crashed or was + closed) shortly after a ``reload_and_compile_procedures`` call. It isn't established + whether this is related to the bridge's own actions (either transport) or a + pre-existing Igor Pro stability issue independent of both -- fresh Igor Pro launches + followed by ordinary (non-reload/compile) tool calls have never reproduced it in this + bridge's development, which is circumstantial evidence pointing away from the new + ZeroMQ code specifically, but is not conclusive. Treat any unreachability after a + compile attempt as a signal to check ``check_bridge_health()`` and be prepared to + relaunch Igor Pro. See ``SESSION_NOTES.md`` for the full, ongoing investigation. + +.. _igor_pro_bridge_v1_history: + +Version history (v1.x, COM-based transport) +---------------------------------------------- + +Versions through 1.27.0 used Igor Pro's COM Automation Server instead of ZeroMQ (see +:ref:`igor_pro_bridge_v2_migration` for why this changed). Key milestones, newest +first, kept here for reference since the design decisions behind them (Igor's runtime +error model, the Debugger/compile-dialog caveats, the submit/poll necessity) all +carried forward unchanged into v2.0.0: + +- **v1.27.0**: corrected an overstated elevation requirement -- confirmed empirically + that COM only required client and server to run at the *same* privilege level, not + that elevation itself was mandatory. +- **v1.26.0**: added ``ensure_igor_pro_bridge_defined`` (retired in v2.0.0 -- see + :ref:`igor_pro_bridge_zbr_helpers`), managing the ``IGOR_PRO_BRIDGE`` + conditional-compilation symbol via ``SetIgorOption poundDefine``. +- **v1.25.0**: pinned Python dependencies via ``requirements.txt`` and added + ``install.ps1``; discovered and worked around the MCP Python SDK's breaking v2.0.0 + line (unrelated to this bridge's own v2.0.0 -- a naming coincidence between the + ``mcp`` package's major version and this project's own). +- **v1.24.0**: added ``read_help_file``. +- **v1.22.0**: added ``get_bridge_version``. +- **v1.17.0**: added ``load_experiment`` (via the COM-only ``IApplication.LoadExperiment`` + method, superseded in v2.0.0 by a relaunch-based implementation -- see that tool's + current reference entry above). +- **v1.15.0 and earlier**: initial ``execute_igor_command``/``get_wave``/ + ``check_compilation_state``/``reload_and_compile_procedures``/ + ``dismiss_compile_error_dialog``/Debugger-control/``launch_igor_pro_unattended`` + tools, all built directly on ``win32com.client.GetActiveObject("IgorPro.Application")`` + and ``Execute2``. diff --git a/tools/igor-mcp-bridge/ZMQ_BridgeHelpers.ipf b/tools/igor-mcp-bridge/ZMQ_BridgeHelpers.ipf new file mode 100644 index 0000000000..ef23494bf6 --- /dev/null +++ b/tools/igor-mcp-bridge/ZMQ_BridgeHelpers.ipf @@ -0,0 +1,827 @@ +#pragma TextEncoding = "UTF-8" +#pragma rtGlobals = 3 +#pragma IndependentModule = ZBR +#pragma version = 1.00 + +// ZMQ_BridgeHelpers.ipf -- Igor Pro-side utility functions backing the Igor Pro Bridge +// (tools/igor-mcp-bridge/) from v2.0.0 onward, which now talks to Igor Pro over the +// ZeroMQ-XOP's CallFunction JSON protocol instead of COM Execute2/IWave/IDataFolder. +// +// **No longer a throwaway prototype**: this file is now a real, permanent dependency of +// the bridge -- see SESSION_NOTES.md for the evaluation that led here and for the +// COM-vs-ZeroMQ trade-offs. #include-d from Packages/MIES_Include.ipf in this repo; any +// OTHER Igor Pro experiment that wants to use this bridge needs this file copied +// somewhere on its own procedure search path with a matching #include added by hand -- +// there is deliberately no auto-load/zero-setup mechanism (see igor-pro-bridge.rst for +// the one-time setup steps). +// +// Why an independent module (#pragma IndependentModule=ZBR): +// Per Igor's own "Advanced Topics.ihf" help (Independent Modules section): "An +// independent module is a set of procedure files that are compiled separately from all +// other procedures. Because it is compiled separately, an independent module can run +// when other procedures are in an uncompiled state because the user is editing them or +// because an error occurred in the last compile." Placing bridge-support code here means +// it stays callable via ZeroMQ's CallFunction even if the rest of the experiment (e.g. +// MIES) currently has a compile error -- unlike today's MIES_ClaudeHelper.ipf, which is +// an ordinary (non-independent) file and therefore goes down along with the rest of +// MIES's compile state. (Nothing stops MIES_ClaudeHelper.ipf from being restructured the +// same way independently of any transport change -- this benefit isn't unique to +// ZeroMQ.) +// +// The central design problem this file has to work around: Igor's `Execute` operation +// (used to run arbitrary command text, replicating COM's Execute2) cannot be called +// unqueued from inside a Function -- only `Execute/P` (deferred: queued to run only +// *after* the calling function returns to Igor's main loop) is legal there. This means a +// single ZeroMQ CallFunction round trip cannot synchronously "run this command and hand +// back what it printed" the way COM's Execute2 can, because the command hasn't actually +// run yet by the time the function returns and the reply is sent. +// +// The pattern used throughout below is submit-then-poll instead of a single blocking +// call: +// 1. ZBR_SubmitCommand(cmd) queues `cmd` and a call back into this module +// (ZBR_FinishToken) as TWO SEPARATE Execute/P entries (not one joined string), then +// returns a token immediately. This split matters: a single joined +// "cmd + ; + finishCall" string was tried first and found, via live testing, to be +// broken -- if `cmd` fails to parse OR hits a genuine runtime error partway through, +// Igor aborts the REST of that same top-level command string, so the appended +// finish-callback would silently never run, leaving ZBR_PollCommand reporting +// done=0 forever (indistinguishable from a job still genuinely running). Queuing +// `cmd` and the finish-callback as independent Execute/P entries avoids this: each +// runs (or fails) on its own, so the finish-callback always fires regardless of what +// happened to `cmd`. ZBR_SubmitCommandUnattended follows the same principle with its +// extra Debugger-disable/restore steps. +// 2. ZBR_PollCommand(token), called via a LATER, separate CallFunction request, reports +// whether it's done yet and returns whatever was printed while `cmd` ran (prefixed +// with "ERROR: ..." if `cmd` left a pending runtime error -- see ZBR_FinishToken). +// This mirrors (and reuses the same underlying mechanism as) how the COM bridge already +// has to defer COMPILEPROCEDURES/RELOAD CHANGED PROCS and SetIgorOption poundDefine via +// Execute/P -- that part is not new or specific to ZeroMQ, it's inherent to Igor's +// compile-safety model. +// +// Limitation of independent modules relevant here (same help topic, "Limitations of +// Independent Modules", #3): "Functions in an independent module can not call functions +// in other modules except through the Execute operation." This is exactly why the +// generic Execute/P-based command submission above is the right general-purpose escape +// hatch here, matching the pattern already proven live this session with the user's own +// ZMQ_TEST#SimpleExecute test function. Direct WAVE/DFREF references are NOT +// module-scoped, so ZBR_GetWaveGeneric below needs no Execute at all. +// +// What is deliberately NOT covered here, because it isn't something Igor procedure code +// can do at all -- these must stay implemented on the CLIENT side (e.g. in Python), +// regardless of which transport (COM or ZeroMQ) carries the request: +// - dismiss_compile_error_dialog: posts a raw Win32 WM_KEYDOWN/WM_KEYUP message to an +// arbitrary OS window handle. No Igor operation does this. +// - configure_igor_launch / launch_igor_pro_unattended: starting a whole new Igor Pro +// *process* has to be done from outside any already-running Igor Pro instance. +// - load_experiment: IApplication.LoadExperiment is COM-only (confirmed: neither +// "LoadExperiment" nor "OpenFile" appear anywhere in Igor Reference.ihf, only in +// Automation Server.ihf) -- there is no procedure-language way to hot-swap the open +// experiment from inside a running instance. The bridge now instead relaunches the +// Igor Pro *process* with the target file path as a launch argument (see +// launch_igor_pro_unattended's docstring) -- a real process restart, not an +// in-place swap, but needs no COM at all. +// - get_bridge_version's python_executable/mcp_package_version/pywin32_build fields: +// these describe the Python process, not Igor Pro. ZBR_Ping below is the Igor-side +// analogue -- confirms this module is loaded and reachable. +// +// read_help_file (CloseHelp/OpenNotebook/SaveNotebook/parse/restore, see +// ZBR_ReadHelpFile below) IS covered here, synchronously: none of those operations are +// subject to the Execute-only-from-top-level restriction that COMPILEPROCEDURES/RELOAD +// CHANGED PROCS need -- that restriction is specific to recompiling procedures while +// procedure code is running, not a blanket rule about every window/notebook operation. +// +// Verification status: the original submit/poll, wave-access, compilation-state, +// debugger-control, and ZeroMQ-bind functions were all written into a live Igor Pro 9 +// Nightly test session (via the v1.27 COM bridge, temporarily included from +// Packages/MIES/, reverted afterward) and exercised directly; a subset was also +// exercised for real over ZeroMQ via tools/zeromq-xop-test/call_igor_function_via_zmq.py. +// The introspection wrappers and ZBR_ReadHelpFile added for the v2.0.0 rewrite are new +// and have only been syntax/compile-checked so far -- see SESSION_NOTES.md for exactly +// what has and hasn't been live-verified. + +// --- Constants ------------------------------------------------------------------------- + +/// Reported by ZBR_Ping -- kept as a named constant (rather than inline in the sprintf +/// call) per this repo's standing convention against unexplained literals, and so it +/// only needs updating in one place if this module's version ever changes independently +/// of the #pragma version above. +static StrConstant ZBR_VERSION_STR = "1.00" + +/// Local endpoint this module's ZeroMQ ROUTER (server) socket (re-)binds to on every +/// compile -- see ZBR_EnsureZeroMQBound/AfterCompiledHook below. Deliberately not +/// MIES_Constants.ipf's own ZEROMQ_BIND_REP_PORT (5670) -- this prototype needs its own +/// port so it can't collide with MIES's real ZeroMQ subsystem +/// (MIES_MiesUtilities_ZeroMQ.ipf's StartZeroMQSockets) if that's ever active in the +/// same experiment. Matches the port already used throughout this session's own manual +/// testing (tools/zeromq-xop-test/call_igor_function_via_zmq.py's default endpoint). +static StrConstant ZBR_ZEROMQ_ENDPOINT = "tcp://127.0.0.1:5680" + +// --- Storage ------------------------------------------------------------------------- + +/// Parallel-array storage for in-flight ZBR_SubmitCommand() calls, indexed by row. +/// A row's index (as a string) is the "token" handed back to the caller. +static Function ZBR_EnsureStorage() + + NewDataFolder/O root:Packages + NewDataFolder/O root:Packages:ZBR + DFREF dfr = root:Packages:ZBR + + if(!WaveExists(dfr:done)) + Make/N=0/O dfr:done // 0 = pending, 1 = done + Make/N=0/O/T dfr:resultText + Make/N=0/O dfr:historyStart + endif +End + +/// One-time CaptureHistoryStart() so ZBR_SubmitCommand/ZBR_FinishToken can diff Igor's +/// history area to recover what a deferred command printed. Mirrors the same mechanism +/// (and the same "start once, read incrementally" usage pattern) the COM bridge already +/// uses for read_session_history. +/// +/// CaptureHistory's real signature -- confirmed from Igor Reference.ihf, since a first +/// draft of this file wrongly assumed a single-argument CaptureHistory(stopCapturing) +/// and failed to compile -- is CaptureHistory(refnum, stopCapturing): refnum must be the +/// value CaptureHistoryStart() returned, not omitted. +/// +/// **Confirmed live bug, now fixed**: `root:Packages:ZBR:captureRefNum` is a plain +/// `Variable/G`, which Igor persists into a saved experiment like any other global -- +/// but the refnum it holds is only meaningful within the OS process that called +/// CaptureHistoryStart() to create it. Reloading a saved experiment (via this bridge's +/// own load_experiment, or the user manually reopening a .pxp) brings the OLD numeric +/// value back even though the process is brand new, so the mere *existence* check this +/// function used to do (`NVAR_Exists(refnum)`) was not enough -- it happily trusted a +/// stale refnum from a now-dead process. Using it then throws a genuine Igor runtime +/// error ("there is no open file with this reference number"), which -- since this can +/// be reached from a plain top-level Execute/P entry, not just from inside a Try/Catch +/// higher up -- pops a real modal error dialog and blocks Igor's whole main thread +/// (and therefore every ZeroMQ reply) until a human dismisses it. Confirmed live by +/// saving+reloading an experiment via load_experiment and then calling +/// execute_igor_command, which triggered exactly this dialog. +/// +/// Fix: don't just check existence, actually try using the stored refnum, wrapped in a +/// try-catch-endtry block (see "Flow Control for Aborts" in Igor's own Programming.ihf +/// help) with an explicit AbortOnRTE right after the risky call -- a runtime error +/// inside a try block does NOT by itself jump to catch, only AbortOnRTE converts it +/// into an abort that does (confirmed from Igor's own help; an earlier version of this +/// fix omitted AbortOnRTE and also used a bare `return` with no value inside a plain, +/// implicit-Variable-returning Function, which is invalid and failed to compile -- +/// see Igor's help, "The Return Statement": "The type of the returned value must +/// agree with the type declared in the function declaration"). try-catch-endtry +/// suppresses the error dialog for anything aborted inside the try block (that's its +/// whole documented purpose), so this also prevents the dialog described above from +/// appearing at all going forward. If the stored refnum turns out stale, silently +/// start a fresh capture and overwrite the stored global instead of ever surfacing +/// this to the user. +/// +/// **User refinement**: CaptureHistory(...) and AbortOnRTE are deliberately kept on +/// the SAME line, not split across two lines the way this was first written. Igor's +/// Debug on Error check happens at the END of each line, not each statement -- if the +/// probe call and AbortOnRTE were on separate lines, Debug on Error (if the user +/// happens to have it enabled) would trigger a Debugger popup right when the stale +/// refnum's runtime error occurs, before AbortOnRTE ever gets a chance to convert it +/// into a catchable abort. Keeping both on one line means the end-of-line check only +/// happens after AbortOnRTE has already run, so there's nothing left pending to +/// trigger the Debugger. +static Function ZBR_EnsureCaptureStarted() + + string dummy + variable err + + NewDataFolder/O root:Packages + NewDataFolder/O root:Packages:ZBR + + NVAR/Z refnum = root:Packages:ZBR:captureRefNum + if(!NVAR_Exists(refnum)) + Variable/G root:Packages:ZBR:captureRefNum = CaptureHistoryStart() + else + NVAR refnumRW = root:Packages:ZBR:captureRefNum + + try + dummy = CaptureHistory(refnumRW, 0);AbortOnRTE + catch + err = GetRTError(1) // clear the trapped error; discard the specific code, we always recover the same way + refnumRW = CaptureHistoryStart() + endtry + endif +End + +static Function ZBR_CaptureRefNum() + + ZBR_EnsureCaptureStarted() + NVAR refnum = root:Packages:ZBR:captureRefNum + + return refnum +End + +static Function ZBR_HistoryLength() + + return strlen(CaptureHistory(ZBR_CaptureRefNum(), 0)) +End + +static Function/S ZBR_HistorySince(variable startLen) + + string full = CaptureHistory(ZBR_CaptureRefNum(), 0) + + if(strlen(full) <= startLen) + return "" + endif + + return full[startLen, Inf] +End + +// --- Generic command execution (submit/poll) ------------------------------------------ + +/// Allocate a new token/storage row for an in-flight submission -- shared by +/// ZBR_SubmitCommand and ZBR_SubmitReloadAndCompile (the latter needs its own submit +/// function since its two commands must be queued separately, not joined into one +/// compound Execute/P string -- see its docstring below). +static Function/S ZBR_AllocateToken() + + variable n + + ZBR_EnsureStorage() + DFREF dfr = root:Packages:ZBR + WAVE done = dfr:done + WAVE/T resultText = dfr:resultText + WAVE historyStart = dfr:historyStart + + n = DimSize(done, 0) + Redimension/N=(n + 1) done, resultText, historyStart + done[n] = 0 + resultText[n] = "" + historyStart[n] = ZBR_HistoryLength() + + return num2istr(n) +End + +/// Queue `cmd` for deferred execution and return a token to poll for its result via +/// ZBR_PollCommand(). Does NOT run `cmd` synchronously -- see the module docstring above +/// for why that's not possible from inside a Function at all, independent module or not. +/// +/// IMPORTANT: `cmd` and the finish-callback are queued as TWO SEPARATE Execute/P entries, +/// not joined into one string with ";". This is deliberate and was learned the hard way: +/// if `cmd` fails to parse, OR hits a genuine runtime error partway through, Igor aborts +/// the REST of that same top-level command string -- so a joined "cmd; finishCall" string +/// would silently drop the finish-callback whenever cmd errors, leaving ZBR_PollCommand +/// reporting done=0 forever with no way to distinguish that from a job still genuinely +/// running. Queuing them as independent Execute/P entries avoids this: each one runs (or +/// fails) on its own, regardless of what happened to the entry before it. This was verified +/// live: two separately-queued Execute/P entries (one invalid, one valid) both ran their +/// own outcome independently, whereas joining them with ";" let a failure in the first +/// swallow the second. +Function/S ZBR_SubmitCommand(string cmd) + + string token, finishCall + + token = ZBR_AllocateToken() + sprintf finishCall, "ZBR#ZBR_FinishToken(%s)", token + Execute/P/Q/Z cmd + Execute/P/Q/Z finishCall + + return token +End + +/// Same as ZBR_SubmitCommand, but disables Igor's Debugger for the duration of `cmd` and +/// restores its exact prior settings afterward -- mirrors execute_igor_command_unattended's +/// reason for existing (a Debugger pause has no scriptable resume and would otherwise hang +/// forever). +/// +/// The disable step, `cmd`, the restore step, and the finish-callback are FOUR SEPARATE +/// Execute/P entries (not one joined string), for the same reason described in +/// ZBR_SubmitCommand's docstring: if `cmd` errors, anything appended after it in the same +/// string would never run. Here that would mean the Debugger stays disabled forever after +/// any erroring `cmd`, in addition to the finish-callback never firing. Because a plain +/// local variable does not survive the boundary between separate top-level Execute/P +/// entries, the saved Debugger settings are stashed in persistent globals under +/// root:Packages:ZBR instead, and the restore entry reads them back from there. +Function/S ZBR_SubmitCommandUnattended(string cmd) + + string token, finishCall, restore + + DebuggerOptions + Variable/G root:Packages:ZBR:savedDebugEnable = V_enable + Variable/G root:Packages:ZBR:savedDebugOnError = V_debugOnError + Variable/G root:Packages:ZBR:savedDebugOnAbort = V_debugOnAbort + Variable/G root:Packages:ZBR:savedDebugNvarCheck = V_NVAR_SVAR_WAVE_Checking + KillVariables/Z V_enable, V_debugOnError, V_debugOnAbort, V_NVAR_SVAR_WAVE_Checking + + restore = "DebuggerOptions enable=root:Packages:ZBR:savedDebugEnable, " + restore += "debugOnError=root:Packages:ZBR:savedDebugOnError, " + restore += "debugOnAbort=root:Packages:ZBR:savedDebugOnAbort, " + restore += "NVAR_SVAR_WAVE_Checking=root:Packages:ZBR:savedDebugNvarCheck; " + restore += "KillVariables/Z V_enable, V_debugOnError, V_debugOnAbort, V_NVAR_SVAR_WAVE_Checking, " + restore += "root:Packages:ZBR:savedDebugEnable, root:Packages:ZBR:savedDebugOnError, " + restore += "root:Packages:ZBR:savedDebugOnAbort, root:Packages:ZBR:savedDebugNvarCheck" + + token = ZBR_AllocateToken() + sprintf finishCall, "ZBR#ZBR_FinishToken(%s)", token + + Execute/P/Q/Z "DebuggerOptions enable=0" + Execute/P/Q/Z cmd + Execute/P/Q/Z restore + Execute/P/Q/Z finishCall + + return token +End + +/// Callback queued by ZBR_SubmitCommand/ZBR_SubmitCommandUnattended as its own, separate +/// deferred entry -- runs after `cmd` (and, for the unattended path, after the Debugger +/// restore entry) regardless of whether those entries succeeded, errored, or failed to +/// parse, so by the time a later ZBR_PollCommand() call sees done[idx] == 1, resultText[idx] +/// is guaranteed fully populated. Public (non-static) because it's invoked via a qualified +/// name (ZBR#ZBR_FinishToken) from a queued Execute/P string. +/// +/// Also checks GetRTError(1), on the theory that a runtime error left pending by `cmd` +/// could be surfaced here as an explicit "ERROR: ..." prefix. **Confirmed live NOT to +/// work**: GetRTError(1) reads 0 here even immediately after a `cmd` that genuinely +/// errored (tested with both an unparseable command and a genuine runtime error -- +/// WaveStats on a non-existent wave). Root cause: each Execute/P entry is dispatched as +/// its own independent top-level execution, the same as if a person had typed it at the +/// command line and pressed enter separately -- Igor resolves and clears any runtime-error +/// state as part of returning that entry to idle, before the queue advances to the next +/// entry, so nothing is left pending for a later, separate entry (like this callback) to +/// read. Also confirmed: Igor does not append anything about the error to the +/// CaptureHistory-tracked history stream either, so ZBR_HistorySince(historyStart[idx]) +/// alone won't reveal it. Net effect: **there is currently no reliable, generic way for a +/// caller to distinguish "cmd ran and legitimately printed nothing" from "cmd errored out +/// partway through with no output"** -- both look identical (done=true, empty result). The +/// check below is kept as a harmless no-op/best-effort in case some other error path does +/// leave state behind, but callers should not rely on it. +/// +/// **Bounds-checks idx before writing, confirmed live necessary**: `idx` is captured by +/// ZBR_AllocateToken at submission time, but this callback runs later, in its own +/// separate deferred Execute/P entry -- if the `done`/`resultText`/`historyStart` waves +/// are ever resized smaller in between (e.g. maintenance code clearing out old/orphaned +/// tokens, as happened live during this bridge's own development), `idx` can end up +/// pointing past the end of the (now-shorter) waves. Writing to an out-of-range wave +/// index throws an uncaught Igor runtime error ("Index out of range for wave..."), which +/// -- exactly like the CaptureHistory bug this module already works around -- pops a +/// real modal dialog and blocks Igor's entire main thread until a human dismisses it. +/// If idx no longer refers to a real row, there is nothing useful left to do (that +/// token's storage is simply gone), so just skip the write silently rather than crash; +/// ZBR_PollCommand already reports "ERROR: unknown token" for exactly this case via its +/// own DimSize check, so the caller still gets a clear, non-hanging answer. +Function ZBR_FinishToken(variable idx) + + variable err + string errMsg + + DFREF dfr = root:Packages:ZBR + WAVE done = dfr:done + WAVE/T resultText = dfr:resultText + WAVE historyStart = dfr:historyStart + + if(idx >= 0 && idx < DimSize(done, 0)) + err = GetRTError(1) + if(err) + errMsg = "ERROR: " + GetErrMessage(err) + "\r" + else + errMsg = "" + endif + + resultText[idx] = errMsg + ZBR_HistorySince(historyStart[idx]) + done[idx] = 1 + endif +End + +/// Poll a token from ZBR_SubmitCommand/ZBR_SubmitCommandUnattended. isDone is 0 while +/// still pending (result is then always ""); once isDone is 1, result holds everything +/// printed to history while the command ran. This is now guaranteed to eventually reach +/// isDone==1 even if the submitted command failed to parse or hit a genuine runtime error +/// partway through (see ZBR_SubmitCommand's docstring) -- but note there is currently no +/// generic way to tell that case apart from "ran fine and simply printed nothing": both +/// come back as an empty result (see ZBR_FinishToken's docstring for why the obvious +/// GetRTError(1)-based approach to detecting this doesn't work). If a command's success +/// needs to be verifiable, have it `print` an explicit sentinel value/message itself. +Function [variable isDone, string result] ZBR_PollCommand(string token) + + variable idx + + DFREF dfr = root:Packages:ZBR + WAVE done = dfr:done + WAVE/T resultText = dfr:resultText + + idx = str2num(token) + if(NumType(idx) != 0 || idx < 0 || idx >= DimSize(done, 0)) + return [1, "ERROR: unknown token " + token] + endif + + if(!done[idx]) + return [0, ""] + endif + + return [1, resultText[idx]] +End + +// --- Wave access ----------------------------------------------------------------------- + +/// Return a wave by its full data-folder path (e.g. "root:MyFolder:mywave"). WAVE/DFREF +/// references are not module-scoped, so this needs no Execute at all -- and unlike the +/// COM bridge's current per-point GetNumericWavePointValue loop, the ZeroMQ-XOP +/// serializes the ENTIRE wave (dimensions, units, note, complex/text/wave-ref support) +/// from this one call, per its documented wave serialization format. +Function/WAVE ZBR_GetWaveGeneric(string wavePath) + + WAVE/Z w = $wavePath + return w +End + +// --- Compilation state ------------------------------------------------------------- + +/// Same trick already used by check_compilation_state (and by +/// Packages/igortest/procedures/igortest-test-compilation.ipf's IsProcGlobalCompiled()): +/// FunctionInfo() for a deliberately non-existent function returns "" when procedures +/// are compiled, and a non-empty string ("Procedures Not Compiled") otherwise. No +/// Execute needed -- FunctionInfo is a plain built-in function. +Function ZBR_IsCompiled() + + return strlen(FunctionInfo("ZBR_DefinitelyNotARealFunctionName_8f3a1c")) == 0 +End + +/// Read root:gClaudeHelperCompileCounter (bumped by AfterCompiledHook below every time +/// Igor confirms a successful compile -- see that function) without creating it if +/// missing. Returns -1 (a real counter value can never be negative) if the global +/// doesn't exist yet, e.g. before this module's own first compile -- mirrors the COM +/// bridge's _read_claude_helper_compile_counter/_CLAUDE_HELPER_COMPILE_COUNTER_CMD +/// exactly, just as a direct typed call instead of an fprintf-wrapped command string. +/// Race-free by construction: unlike ZBR_IsCompiled's FunctionInfo poll, this only ever +/// changes at the exact moment Igor itself confirms a successful compile, so any +/// observed increase over a baseline read before triggering a reload/compile is +/// trustworthy immediately, no repeated-confirmation dance needed. +Function ZBR_ReadCompileCounter() + + return NumVarOrDefault("root:gClaudeHelperCompileCounter", -1) +End + +/// RELOAD CHANGED PROCS / COMPILEPROCEDURES are themselves restricted the same way +/// Execute is -- not a new restriction introduced by this module; the COM bridge +/// already has to defer these exact same operations via Execute/P (see that bridge's +/// reload_and_compile_procedures). +/// +/// **Correction (user-supplied): the two commands must be issued as separate +/// Execute/P calls, not joined into one compound string via ";" the way +/// ZBR_SubmitCommand does for arbitrary commands -- and each needs its own mandatory +/// trailing space ("RELOAD CHANGED PROCS ", "COMPILEPROCEDURES ").** +/// +/// Deliberately does NOT use the ZBR_SubmitCommand/ZBR_PollCommand token+callback +/// mechanism, despite that being the obvious first attempt (and what an earlier +/// version of this function did) -- confirmed live that a finish-callback queued via +/// Execute/P *after* COMPILEPROCEDURES never actually runs: recompiling the whole +/// procedure set appears to discard/invalidate whatever was still pending behind it +/// in Igor's operation queue, rather than letting it complete afterward +/// (ZBR_FinishToken's target row stayed permanently un-done in a live test, with no +/// error reported anywhere). Poll ZBR_IsCompiled() instead -- already a direct, +/// synchronous, standalone check that doesn't depend on anything surviving the +/// recompile -- to find out when this has taken effect. +Function ZBR_SubmitReloadAndCompile() + + Execute/P/Q/Z "RELOAD CHANGED PROCS " + Execute/P/Q/Z "COMPILEPROCEDURES " + + return 0 +End + +// --- Debugger control ---------------------------------------------------------------- + +/// Direct (non-deferred) call -- confirmed live that DebuggerOptions, unlike +/// COMPILEPROCEDURES, is NOT restricted to top-level/Execute-only use. +Function [variable enable, variable debugOnError, variable debugOnAbort, variable nvarChecking] ZBR_GetDebuggerState() + + DebuggerOptions + variable e = V_enable, doe = V_debugOnError, doa = V_debugOnAbort, nv = V_NVAR_SVAR_WAVE_Checking + KillVariables/Z V_enable, V_debugOnError, V_debugOnAbort, V_NVAR_SVAR_WAVE_Checking + + return [e, doe, doa, nv] +End + +Function ZBR_SetDebuggerEnabled(variable enable) + + DebuggerOptions enable=(enable != 0) + KillVariables/Z V_enable, V_debugOnError, V_debugOnAbort, V_NVAR_SVAR_WAVE_Checking + + return 0 +End + +Function ZBR_RestoreDebuggerSettings(variable enable, variable debugOnError, variable debugOnAbort, variable nvarChecking) + + DebuggerOptions enable=enable, debugOnError=debugOnError, debugOnAbort=debugOnAbort, NVAR_SVAR_WAVE_Checking=nvarChecking + KillVariables/Z V_enable, V_debugOnError, V_debugOnAbort, V_NVAR_SVAR_WAVE_Checking + + return 0 +End + +// --- Direct built-in introspection wrappers ------------------------------------------- +// +// Each of these is exactly one synchronous CallFunction round trip: a thin wrapper +// around a single read-only Igor built-in function with no side effects and no +// Execute-restriction, so none of them need the submit/poll pattern above. Deliberately +// generic (parameters passed straight through) rather than one bespoke wrapper per +// COM-bridge tool -- structuring/parsing the returned raw strings into a proper dict +// happens client-side (Python), exactly mirroring how the COM bridge already worked (it +// also just ran fprintf-wrapped built-in calls and parsed the raw string results in +// Python) -- see get_environment_summary in server.py for where these get assembled. + +/// IgorInfo(n) -- e.g. n=0 for the version/build/memory/screen report string, n=3 for +/// OS info, n=10 for the semicolon-separated loaded-XOPs list, n=11/12 for the current +/// experiment's file kind/name. See Igor Reference.ihf for the full index table. +Function/S ZBR_IgorInfo(variable n) + + return IgorInfo(n) +End + +/// WinList(matchStr, ";", options) -- e.g. ZBR_WinList("*", "WIN:128") for included +/// procedure windows/files, ZBR_WinList("*", "WIN:512") for help windows. +Function/S ZBR_WinList(string matchStr, string options) + + return WinList(matchStr, ";", options) +End + +/// ProcedureText(funcName, flags, winTitle) -- pass funcName="" and winTitle=a specific +/// window name (e.g. "Procedure") to retrieve that whole window's contents, per the +/// hard-won finding recorded in the COM bridge's own get_environment_summary comment: +/// the window name goes in the THIRD argument, not the first -- passing it as the first +/// argument instead silently returns "" rather than raising an error. +Function/S ZBR_ProcedureText(string funcName, variable flags, string winTitle) + + return ProcedureText(funcName, flags, winTitle) +End + +/// DataFolderDir(bits) for the CURRENT data folder -- callers wanting a specific folder +/// should set it first via ZBR_SubmitCommand("SetDataFolder ...") since a DFREF argument +/// isn't threaded through here; bits=3 (folders + waves) is what get_environment_summary +/// uses. +Function/S ZBR_DataFolderDir(variable bits) + + return DataFolderDir(bits) +End + +/// FunctionInfo(name) -- generic compiled-function-exists probe. ZBR_IsCompiled() above +/// is really just this called with a deliberately-bogus name; exposed generically here +/// too so a caller can check any specific marker function by name. +Function/S ZBR_FunctionInfo(string name) + + return FunctionInfo(name) +End + +// --- Environment introspection ------------------------------------------------------- + +/// Minimal identity/diagnostic summary -- NOT what get_environment_summary uses (that +/// tool composes its full picture client-side from the granular ZBR_IgorInfo/ZBR_WinList/ +/// etc. wrappers above instead, for the same reason those exist as separate functions: +/// keeping each Igor-side wrapper trivial and generic, with all the actual structuring +/// done in Python). Kept as a quick one-call smoke check alongside ZBR_Ping. +Function/S ZBR_GetEnvironmentSummary() + + string summary + sprintf summary, "igorInfo0=%s;experiment=%s;dataFolder=%s;compiled=%d", IgorInfo(0), IgorInfo(1), GetDataFolder(1), ZBR_IsCompiled() + + return summary +End + +/// Read back everything sent to history since the capture started -- analogue of +/// read_session_history. stop=1 stops the capture (matching that tool's stop=True) -- +/// per CaptureHistory's own docs, a stopped refnum errors if reused, so this kills the +/// stored refnum too; the next call transparently starts a fresh capture, same as the +/// COM bridge's own read_session_history behavior. +Function/S ZBR_ReadSessionHistory(variable stop) + + string text + variable refnum = ZBR_CaptureRefNum() + + text = CaptureHistory(refnum, stop) + + if(stop) + KillVariables/Z root:Packages:ZBR:captureRefNum + endif + + return text +End + +// --- Health / identity ----------------------------------------------------------------- + +/// Minimal health-check/identity analogue of get_bridge_version -- confirms this module +/// specifically (not just "some Igor Pro instance") is loaded and reachable, and gives a +/// per-instance-distinguishing value (same idea as this session's earlier +/// GetInstanceInfo, generalized). +Function/S ZBR_Ping() + + string info + sprintf info, "ZBR_ALIVE|version=%s|experiment=%s|dateTime=%.0f", ZBR_VERSION_STR, IgorInfo(1), DateTime + + return info +End + +// --- Help file reading ------------------------------------------------------------------ +// +// Synchronous equivalent of the COM bridge's read_help_file. CloseHelp/OpenNotebook/ +// SaveNotebook/KillWindow/OpenHelp are ordinary window/notebook operations -- NOT subject +// to the Execute-only-from-top-level restriction that COMPILEPROCEDURES/RELOAD CHANGED +// PROCS need (see the module docstring: that restriction is about recompiling procedures +// while procedure code is running, not a blanket rule about every operation) -- so this +// entire sequence runs as one direct, synchronous CallFunction round trip, no +// submit/poll needed. + +/// Return the first entry in `afterList` (semicolon-delimited) that is not present in +/// `beforeList` -- used to identify which new window WinList assigned to a just-opened +/// notebook (OpenNotebook/R doesn't return this directly). Returns "" if none found. +static Function/S ZBR_FirstNewListEntry(string afterList, string beforeList) + + variable i, n + string name + + n = ItemsInList(afterList) + for(i = 0; i < n; i += 1) + name = StringFromList(i, afterList) + if(WhichListItem(name, beforeList) == -1) + return name + endif + endfor + + return "" +End + +/// Resolve a bare help-file name (WinList's WIN:512 bit never includes a path -- "Procedure +/// windows and help windows don't have names. WinList returns the window title instead") +/// back to a full path, checking the two folders Igor Pro itself loads help files from. +/// Mirrors the COM bridge's _resolve_help_file_path exactly, just in compiled Igor +/// instead of Python + os.path. Returns "" if not found in either location. +static Function/S ZBR_ResolveHelpFilePath(string bareName) + + string specialDirs = "Igor Application;Igor Pro User Files" + string base, candidate + variable i, n + + n = ItemsInList(specialDirs) + for(i = 0; i < n; i += 1) + base = SpecialDirPath(StringFromList(i, specialDirs), 0, 1, 0) + if(strlen(base) == 0) + continue + endif + candidate = base + "Igor Help Files:" + bareName + GetFileFolderInfo/Q/Z candidate + if(V_flag == 0) + return candidate + endif + endfor + + return "" +End + +/// Read filePath (an .ihf help file, itself an Igor formatted-text notebook) and export +/// it as HTML to tmpHtmlPath (caller-supplied -- built by the Python side via +/// tempfile.mkstemp, same as the COM bridge already did), for the caller to parse +/// afterward. tmpHtmlPath is read directly off disk by the caller rather than being +/// serialized back through this reply: both processes run on the same machine, so a +/// local file handoff sidesteps any question about how large a CallFunction reply can +/// carry for a potentially big HTML export (the ZeroMQ-XOP's own default +/// ZMQ_MAXMSGSIZE=1024-byte limit applies to the Router's *incoming* request size, but +/// this avoids relying on any assumption about outgoing reply size limits too). +/// +/// Full sequence, matching the COM bridge's read_help_file exactly, just executed +/// synchronously in compiled Igor code instead of via a client-side finally block: +/// 1. Snapshot every currently open help file (visible or hidden, WIN:512) and every +/// currently open plain-notebook window (WIN:16). +/// 2. CloseHelp/ALL (required: an .ihf can't be opened as a notebook while Igor +/// considers it already open as a help file). +/// 3. OpenNotebook/R filePath, then diff WinList's notebook list against the step-1 +/// snapshot to find the name Igor assigned the new window. +/// 4. SaveNotebook/O/S=5/H=... export to tmpHtmlPath. +/// 5. KillWindow/Z the temporary notebook. +/// 6. Restore every help file captured in step 1 via OpenHelp/V=.../INT=0. +/// Steps 5-6 always run (via try/catch rather than a true finally, since Igor procedure +/// code has no finally block) even if step 3 or 4 failed, so a failure partway through +/// still restores whatever help state existed before this call. +/// +/// Returns a "|"-joined status string: "OK|" on success, +/// or "ERROR||" if OpenNotebook/SaveNotebook +/// itself failed. restoreFailures lists bare file names from step 1 that could not be +/// resolved back to a full path (e.g. a help file supplied from somewhere other than the +/// two standard Help Files folders) -- these were NOT reopened. +Function/S ZBR_ReadHelpFile(string filePath, string tmpHtmlPath) + + string helpAll, helpVisible, notebooksBefore, newName, restoreFailures + string name, resolvedPath, statusStr + variable i, n, visibleFlag, err + + helpAll = WinList("*", ";", "WIN:512") + helpVisible = WinList("*", ";", "WIN:512,VISIBLE:1") + notebooksBefore = WinList("*", ";", "WIN:16") + newName = "" + statusStr = "OK" + + try + CloseHelp/ALL + AbortOnRTE + + OpenNotebook/R filePath + AbortOnRTE + + newName = ZBR_FirstNewListEntry(WinList("*", ";", "WIN:16"), notebooksBefore) + if(strlen(newName) == 0) + Abort "OpenNotebook/R succeeded but no new notebook window was found" + endif + + SaveNotebook/O/S=5/H={"UTF-8", 3, 7, 0, 0.9, 32} $newName as tmpHtmlPath + AbortOnRTE + catch + err = GetRTError(1) + statusStr = "ERROR|" + GetErrMessage(err) + endtry + + if(strlen(newName) > 0) + KillWindow/Z $newName + endif + + restoreFailures = "" + n = ItemsInList(helpAll) + for(i = 0; i < n; i += 1) + name = StringFromList(i, helpAll) + resolvedPath = ZBR_ResolveHelpFilePath(name) + if(strlen(resolvedPath) == 0) + restoreFailures = AddListItem(name, restoreFailures, ";", Inf) + continue + endif + visibleFlag = (WhichListItem(name, helpVisible) != -1) ? 1 : 0 + OpenHelp/V=(visibleFlag)/INT=0/Z=1 resolvedPath + err = GetRTError(1) + if(err) + restoreFailures = AddListItem(name, restoreFailures, ";", Inf) + endif + endfor + + return statusStr + "|" + restoreFailures +End + +// --- ZeroMQ server bind ---------------------------------------------------------------- + +/// (Re-)bind this module's ZeroMQ ROUTER (server) socket and (re-)start the XOP's +/// background message handler, so CallFunction requests (e.g. "ZBR#ZBR_Ping") are served +/// automatically from here on -- no separate manual zeromq_server_bind/ +/// zeromq_handler_start call needed after a recompile, unlike this session's earlier +/// manual testing. +/// +/// **Correction (user-supplied): deliberately does NOT call zeromq_stop() first**, +/// unlike the three-call idiom shown in Igor Pro Folder/Igor Help Files/ZeroMQ.ihf's own +/// introductory example (its ServerSide() function). zeromq_stop() stops *every* ZeroMQ +/// bind/connection/handler for the whole Igor Pro instance, not just this module's own +/// -- calling it unconditionally on every compile would corrupt/tear down any other +/// already-established ZeroMQ binds (e.g. MIES's own real subsystem, +/// MIES_MiesUtilities_ZeroMQ.ipf's StartZeroMQSockets, currently short-circuited via an +/// uncommitted `return 2` in this repo's working tree but not necessarily always so). +/// Calling zeromq_server_bind directly, without stopping first, is safe to repeat on +/// every compile: if this module's own socket is already bound from an earlier compile, +/// the call simply errors ("Address in use"), caught below rather than propagated. +/// +/// `; err = GetRTError(1)` immediately after each XOP call clears any resulting runtime +/// error right there on the same line -- necessary because Igor's Debugger (when +/// "Debug on Error" is enabled) only checks for a pending RTE state at the *end of a +/// line*, so leaving either of these calls' potential error unacknowledged until some +/// later line would risk popping the Debugger window here, which -- same as every other +/// popup this session has hit -- has no scriptable dismissal and would hang unattended +/// operation. +static Function ZBR_EnsureZeroMQBound() + + variable err + + zeromq_server_bind(ZBR_ZEROMQ_ENDPOINT); err = GetRTError(1) + zeromq_handler_start(); err = GetRTError(1) + + return 0 +End + +static Function AfterCompiledHook() + + variable modifiedBefore + + // Creating/incrementing a global marks the experiment as modified, same as any + // other data change. Captured/restored here so this hook never flips an + // otherwise-unmodified experiment to modified, matching the existing convention + // in MIES_IgorHooks.ipf's own AfterCompiledHook -- flagged by a Copilot PR + // review as a real risk otherwise: an experiment spuriously marked modified can + // trigger a "Save changes?" prompt later, which is exactly the kind of dialog + // this bridge (built around unattended operation) cannot dismiss remotely. + ExperimentModified + modifiedBefore = V_flag + + // Make this module's ZeroMQ server listen again immediately after every compile -- + // the whole point of running this from AfterCompiledHook rather than requiring a + // separate manual step each time the code changes. + ZBR_EnsureZeroMQBound() + + // Bare Variable/G (no initializer) is safe to call unconditionally: per Igor + // Reference.ihf, /G "overwrites any existing variable" but "the variable is + // initialized when it is created if you supply the initial value" -- i.e. the + // overwrite-to-a-value only happens when an initializer is given. Without one, + // this creates the global at 0 the first time and leaves an existing value + // alone on every call after that, so no NVAR_Exists guard is needed. + variable/G root:gClaudeHelperCompileCounter + NVAR gClaudeHelperCompileCounter = root:gClaudeHelperCompileCounter + + gClaudeHelperCompileCounter += 1 + + if(!modifiedBefore) + ExperimentModified 0 + endif + + return 0 +End diff --git a/tools/igor-mcp-bridge/igor-pro-bridge-1.27.0.mcpb b/tools/igor-mcp-bridge/igor-pro-bridge-1.27.0.mcpb deleted file mode 100644 index 72e3a5e84ff125a3491eb2bea6966eb91d1ef3dd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 50859 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 JV|5h70{{hFQ^o)Q diff --git a/tools/igor-mcp-bridge/igor-pro-bridge-2.2.3.mcpb b/tools/igor-mcp-bridge/igor-pro-bridge-2.2.3.mcpb new file mode 100644 index 0000000000000000000000000000000000000000..9f92980c8f6ac16c2f2e5f25506e21d328de7d0c GIT binary patch literal 33896 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 E0AvGivH$=8 literal 0 HcmV?d00001 diff --git a/tools/igor-mcp-bridge/install.ps1 b/tools/igor-mcp-bridge/install.ps1 index 5dd81f389c..58c4452122 100644 --- a/tools/igor-mcp-bridge/install.ps1 +++ b/tools/igor-mcp-bridge/install.ps1 @@ -28,32 +28,35 @@ Steps performed, in order: 1. Confirm this script itself is running elevated. This is required only for step 5 below (pywin32's post-install step, which registers COM-support DLLs - into protected system locations) -- it is NOT because Claude Desktop or Igor - Pro themselves need to be elevated at runtime. Confirmed empirically (see - igor-pro-bridge.rst, "Requirements"): Claude Desktop and Igor Pro just need to - run at the SAME privilege level as each other (both elevated, or both not); - this script needing elevation is a one-time, install-time requirement of its - own, independent of whichever level you later choose to run the bridge at. + into protected system locations, even though this bridge no longer uses COM + itself as of v2.0.0 -- see below) -- it is NOT because Claude Desktop or Igor + Pro themselves need to be elevated at runtime. As of v2.0.0, the bridge talks + to Igor Pro over a plain localhost ZeroMQ socket, which has NO privilege- + matching requirement at all (unlike the old COM transport, which needed + Claude Desktop and Igor Pro to run at the same privilege level) -- this + script needing elevation is purely a one-time, install-time requirement of + its own (pywin32's DLL registration), unrelated to how you run the bridge or + Igor Pro afterward. 2. Resolve python.exe (or use -PythonPath). 3. ` -m pip install --upgrade pip` 4. ` -m pip install --require-hashes -r requirements.txt` (pinned, hash-verified versions -- see that file's own comments, notably why "mcp" is pinned below its new v2 line, and why every transitive dependency is listed - and hashed too, not just mcp/pywin32 themselves). + and hashed too, not just mcp/pyzmq/pywin32 themselves). 5. Run pywin32's required post-install step (Scripts\pywin32_postinstall.py -install), which `pip install pywin32` alone - does not do -- it registers pywin32's COM-support DLLs, without which - win32com.client calls can fail unpredictably. - 6. Import-check mcp and win32com.client with the same interpreter, and print - its full path/version for you to cross-check. - - After this script finishes, fully restart Claude Desktop (at whichever privilege - level you intend to run it and Igor Pro at -- both must match each other, but - neither has to be elevated), then call the bridge's get_bridge_version tool from a - conversation -- its "python_executable" field is the authoritative answer for - which interpreter Claude Desktop actually launched. If it doesn't match what this - script installed into, re-run this script with -PythonPath pointing at that - reported path. + does not do -- it registers pywin32's COM-support DLLs. This bridge itself no + longer uses COM (v2.0.0+), but pywin32 is still a dependency for + dismiss_compile_error_dialog's window enumeration and for process launching, + and skipping this step can still leave the install in a broken state. + 6. Import-check mcp, zmq, and win32api/win32gui with the same interpreter, and + print its full path/version (plus pyzmq's version) for you to cross-check. + + After this script finishes, fully restart Claude Desktop, then call the bridge's + get_bridge_version tool from a conversation -- its "python_executable" field is + the authoritative answer for which interpreter Claude Desktop actually launched. + If it doesn't match what this script installed into, re-run this script with + -PythonPath pointing at that reported path. .PARAMETER PythonPath Full path to a specific python.exe to install into, bypassing auto-resolution @@ -159,9 +162,10 @@ if (-not (Test-IsElevated)) { "post-install step below registers COM-support DLLs into protected system " + "locations, which requires admin rights regardless of how you plan to run " + "Claude Desktop/Igor Pro afterward. This is a one-time, install-time " + - "requirement only -- Claude Desktop and Igor Pro do NOT both need to be " + - "elevated at runtime, they just need to match each other's privilege level " + - "(see igor-pro-bridge.rst, 'Requirements'). Re-run this script from an " + + "requirement only -- as of v2.0.0 the bridge talks to Igor Pro over " + + "ZeroMQ, which has no privilege-matching requirement at all, so neither " + + "Claude Desktop nor Igor Pro need to be elevated at runtime (see " + + "igor-pro-bridge.rst, 'Requirements'). Re-run this script from an " + "elevated PowerShell (right-click PowerShell -> Run as administrator)." ) exit 1 @@ -247,13 +251,16 @@ $verifyScript = @' import importlib.metadata import sys import mcp -import win32com.client +import zmq +import win32api +import win32gui print(f"python_executable: {sys.executable}") print(f"python_version: {sys.version.split()[0]}") print(f"mcp_package_version: {importlib.metadata.version('mcp')}") +print(f"pyzmq_version: {importlib.metadata.version('pyzmq')}") print(f"pywin32_build: {importlib.metadata.version('pywin32')}") -print("win32com.client import OK") +print("zmq / win32api / win32gui imports OK") '@ $verifyScriptPath = Join-Path ([System.IO.Path]::GetTempPath()) "igor-bridge-verify-$([guid]::NewGuid()).py" try { @@ -264,10 +271,8 @@ try { } Write-Host ( - "`nDone. Fully restart Claude Desktop (at whichever privilege level you intend " + - "to run it and Igor Pro at -- both must match each other, but neither has to be " + - "elevated), then call the bridge's get_bridge_version tool and confirm its " + - "'python_executable' field matches: $python`n" + + "`nDone. Fully restart Claude Desktop, then call the bridge's get_bridge_version " + + "tool and confirm its 'python_executable' field matches: $python`n" + "If it does not match, re-run this script with -PythonPath set to the path " + "get_bridge_version() actually reports." ) diff --git a/tools/igor-mcp-bridge/manifest.json b/tools/igor-mcp-bridge/manifest.json new file mode 100644 index 0000000000..611325fa37 --- /dev/null +++ b/tools/igor-mcp-bridge/manifest.json @@ -0,0 +1,120 @@ +{ + "manifest_version": "0.4", + "name": "igor-pro-bridge", + "display_name": "Igor Pro Bridge", + "version": "2.2.3", + "description": "Control a running Igor Pro instance via the ZeroMQ-XOP's CallFunction interface: run commands, read wave data, manage compilation, load experiments, and launch Igor Pro itself, directly from Claude.", + "long_description": "v2.2.3: fixed another uncaught-runtime-error-pops-a-dialog bug, this time in `ZBR_FinishToken`, found while cleaning up leftover test-token rows from this bridge's own `root:Packages:ZBR` storage waves. `ZBR_AllocateToken` captures a token's row index (`idx`) at submission time, but `ZBR_FinishToken` -- the callback that actually writes the result and flips the done flag -- runs later, in its own separate deferred `Execute/P` entry (per the v2.2.0 fix). If the `done`/`resultText`/`historyStart` waves are resized smaller in between (e.g. maintenance code clearing out old tokens, exactly as happened live during this session's own cleanup), `idx` can end up pointing past the end of the now-shorter waves. `ZBR_FinishToken` had no bounds check, so writing to `resultText[idx]` in that state threw an uncaught \"Index out of range for wave...\" runtime error -- which, like the v2.2.1 bug, pops a real modal dialog and blocks Igor's entire main thread until a human dismisses it. Fixed by bounds-checking `idx` against `DimSize(done, 0)` before writing anything; if the row no longer exists, the callback now just silently does nothing (there is nothing useful left to recover), and `ZBR_PollCommand`'s own existing bounds check already reports a clean `\"ERROR: unknown token\"` response to the caller instead of a hang. Confirmed live: reproduced the exact original crash (resizing the storage waves to 0 rows while a submitted command's own token was still in flight, which had just thrown the dialog moments earlier during this session's testing), then repeated it after the fix -- this time it returned `\"ERROR: unknown token ...\"` cleanly with no dialog and no hang, and `check_bridge_health()` stayed `OK` throughout. No Python-side changes were needed.\n\nv2.2.2: minor robustness refinement to the v2.2.1 fix, contributed directly by the repo owner after installing it. `ZBR_EnsureCaptureStarted`'s stale-refnum probe (`CaptureHistory(refnumRW, 0)`) and its `AbortOnRTE` were originally written on separate lines; moved onto the SAME line. Reason: Igor's Debug on Error check happens at the END of each line, not each statement -- if the two were on separate lines, a user with Debug on Error enabled (unusual, but this bridge doesn't force the Debugger off during a raw `execute_igor_command`/`submit_igor_command` call the way the `_unattended` variants do) would get a Debugger popup right when the stale refnum's runtime error occurred, before `AbortOnRTE` ever got a chance to convert it into a catchable abort -- defeating the whole point of the v2.2.1 fix in that one specific configuration. Confirmed live both before accepting this change (by temporarily enabling `debug_on_error` via `set_debugger_enabled` and repeating the deliberately-corrupted-refnum test) and after: with the two statements on one line, the corrupted-refnum scenario completes cleanly with no Debugger popup and no hang even with Debug on Error active, and the refnum still self-heals correctly. No other behavior change.\n\nv2.2.1: fixed a bug where `load_experiment` (or a user manually reopening a saved .pxp) could leave the bridge completely unreachable, requiring a human to dismiss a modal Igor error dialog. Root cause: this bridge's history-capture mechanism stores its `CaptureHistoryStart()` reference number in a plain `Variable/G` global (`root:Packages:ZBR:captureRefNum`), which Igor persists into a saved experiment like any other global -- but that refnum is only meaningful within the OS process that created it. Reloading a saved experiment brings the OLD numeric value back even though the process is brand new; the prior code only checked that the global *existed*, not whether its value was still a live capture, so it trusted the stale refnum. Using it then threw a genuine Igor runtime error (\"there is no open file with this reference number\") from a plain top-level `Execute/P` entry -- not caught anywhere -- which popped a real modal dialog and blocked Igor's entire main thread (and therefore every ZeroMQ reply) until a human dismissed it. Confirmed live by saving and reloading an experiment via `load_experiment` and then calling `execute_igor_command`, which reproduced the dialog exactly. Fixed in `ZBR_EnsureCaptureStarted` (`ZMQ_BridgeHelpers.ipf`): the stored refnum is now validated with a `try`/`catch`/`endtry`+`AbortOnRTE` block before being trusted, and silently replaced with a fresh `CaptureHistoryStart()` call if it's stale -- confirmed live afterward, both by deliberately corrupting the refnum to a bogus value and by repeating the exact save-then-`load_experiment`-then-`execute_igor_command` sequence that originally triggered the dialog; neither reproduced the dialog and both recovered silently. (Two syntax mistakes were made and caught along the way while implementing this: a runtime error inside a `try` block does not by itself jump to `catch` without an explicit `AbortOnRTE` immediately after the risky call, and a bare `return` with no value is invalid inside a plain, implicit-`Variable`-returning `Function` -- both confirmed from Igor's own bundled help, \"Flow Control for Aborts\" and \"The Return Statement\" in `Programming.ihf`.) No Python-side (`server.py`) changes were needed for this fix -- it is entirely within the Igor-side `ZMQ_BridgeHelpers.ipf` helper file.\n\nv2.2.0: fixed a serious reliability bug in the v2.1.0 submit/poll primitives, found via live testing of the exact scenario they exist for (a command whose runtime is unknown and could be very long). `ZBR_SubmitCommand`/`ZBR_SubmitCommandUnattended` (Igor-side, in `ZMQ_BridgeHelpers.ipf`) previously joined the submitted command and the internal finish-callback into ONE compound `Execute/P` string (`cmd + \"; \" + finishCall`). Confirmed live: if `cmd` failed to parse, OR hit a genuine Igor-level runtime error partway through (not just a Debugger pause), the ENTIRE joined string aborted, so the appended finish-callback never ran -- leaving `poll_igor_command` reporting `\"done\": false` forever, indistinguishable from a job still genuinely in progress. This directly undermines the reliability guarantee these tools exist to provide. Root-caused via two live tests: (1) queuing an invalid command and a valid one as two SEPARATE `Execute/P` entries showed the second still ran fine despite the first failing, proving Igor's deferred-operation queue processes independently-queued entries regardless of a prior one's failure; (2) the same joined-string command that had a genuine runtime error (not just an invalid command name) also silently swallowed its own appended print statement, confirming the bug applies to runtime errors too, not just parse errors. Fixed by queuing `cmd` and the finish-callback (and, for the `_unattended` variant, a Debugger-disable step and a Debugger-restore step, with the saved settings carried across via persistent globals) as fully independent `Execute/P` entries instead of one joined string -- verified live afterward with an invalid command, a genuine runtime-erroring command, and a normal command, all three now correctly reaching `\"done\": true` instead of hanging. **Known residual limitation, also confirmed live:** there is currently no generic way to tell \"the command ran and legitimately printed nothing\" apart from \"the command errored out with no output\" -- both now come back as `\"done\": true` with an empty/short result and no error indication (an attempted fix using `GetRTError()` in the finish-callback was tested and found not to work: each `Execute/P` entry is dispatched as its own independent top-level execution, so Igor clears any pending runtime-error state before the next queued entry runs). If a submitted command's success needs to be verifiable, have it `print` an explicit sentinel/result value itself.\n\nv2.1.0: added first-class support for commands with an unknown or very long runtime (hours to weeks). `execute_igor_command`/`execute_igor_command_unattended` block the whole MCP tool call while polling, which cannot work for a genuinely long calculation -- the MCP transport itself has been observed to time out a single tool call well under a minute, regardless of the requested `timeout_seconds`. Three new tools expose the existing submit/poll primitives directly: `submit_igor_command`/`submit_igor_command_unattended` queue a command and return a token immediately with no waiting at all, and `poll_igor_command(token)` performs one cheap, instant check for completion, callable any number of times spaced arbitrarily far apart. This is reliable no matter how long the job runs or how many times this bridge process or Claude Desktop itself restarts in between, because all of the actual state (a done flag and the captured text) lives entirely in Igor Pro's own data waves (`root:Packages:ZBR`), not in this bridge's Python process -- the only thing that actually ends the job is Igor Pro itself quitting, crashing, or restarting. As with the existing `_unattended` tools, use `submit_igor_command_unattended` for anything long-running: a Debugger pause partway through leaves `poll_igor_command` reporting \"not done\" forever, indistinguishable from the command still genuinely running.\n\nv2.0.1: three bug fixes found during live v2.0.0 testing. (1) `execute_igor_command`/`execute_igor_command_unattended`: the documented pattern of using `fprintf 0, ...` to get data back does not actually work over this transport -- confirmed live that `CaptureHistory` (which this bridge's capture mechanism relies on) captures `print` output but never captures `fprintf`-to-history output at all (refnum 0, -1, or -2 all silently produce nothing, even though the command itself runs without error). Use `print` instead; all docs/docstrings updated accordingly. (2) `load_experiment`: fixed a silent-failure bug where relaunching Igor Pro with a new experiment file could do nothing at all, with no error reported. Root cause (diagnosed live): the tool was waiting for Igor Pro to stop answering over ZeroMQ as its signal that the old process had quit, but ZeroMQ goes quiet well before the underlying Igor64.exe process actually terminates -- launching the replacement `Igor64.exe /UNATTENDED ` command line while the old process is still alive (even mid-shutdown) doesn't spawn a new process at all; Windows/Igor's single-instance-per-user behavior instead either redirects the load into the still-live old instance (risking an unhandled \"save changes?\" dialog) or silently drops the request if that instance is already mid-quit. Fixed by polling the actual OS process list for the configured executable's own process to fully exit before ever launching the replacement, raising a clear error instead of proceeding if it doesn't exit in time. (3) `read_help_file`: added a `timeout_ms` parameter (default raised from this bridge's usual 5s to 30s) -- confirmed live that exporting a genuinely large help file (e.g. the full \"Igor Reference.ihf\") can take longer than 5 seconds, which previously timed out the call even though the export eventually succeeded Igor-side anyway.\n\nv2.0.0: BREAKING transport change. Replaces the COM Automation Server transport (win32com.client, ProgID `IgorPro.Application`) with the ZeroMQ-XOP's `CallFunction` JSON protocol over a plain localhost TCP socket (tcp://127.0.0.1:5680), talking to a small set of Igor-side helper functions in `Packages/MIES/ZMQ_BridgeHelpers.ipf` (the `ZBR` independent module).\n\nWhy: COM required this bridge process and Igor Pro to run at the SAME Windows privilege level (both elevated, or both not) -- an easy-to-miss mismatch (e.g. Claude Desktop reopened normally after Igor Pro was left running elevated from before). ZeroMQ is a plain TCP socket with no such requirement at all -- elevation no longer matters in any way. `launch_igor_pro_unattended` no longer has an elevation-branching code path: it always launches Igor Pro as a plain, non-elevated child process, at whatever privilege level this bridge process itself runs at.\n\n**New setup requirement**: unlike the COM transport (which worked against a stock Igor Pro installation with zero custom procedure code), this transport requires `Packages/MIES/ZMQ_BridgeHelpers.ipf` to be `#include`-d and compiled into whichever Igor Pro experiment this bridge talks to -- there is no bootstrap path over ZeroMQ itself. This repo's own `Packages/MIES_Include.ipf` already does this permanently. Any OTHER Igor Pro experiment needs `ZMQ_BridgeHelpers.ipf` copied onto its own procedure search path with a matching `#include` added by hand, then a recompile -- see `Packages/doc/igor-pro-bridge.rst` (\"Installation\") for the exact one-time steps.\n\n**Capability improvements**: `get_wave` now supports any wave dimensionality (up to 4D), real and complex numeric waves, text waves, and wave-reference waves -- the entire wave comes back from one round trip, natively serialized (the old COM version was limited to 1D real-valued waves, read one point at a time).\n\n**Behavior changes to be aware of**: `execute_igor_command`/`execute_igor_command_unattended` can no longer separate fprintf-only output from the full echoed history the way COM's `Execute2` could -- both \"results\" and \"history\" now hold the same captured text; prefix a command with `Silent 1;` to suppress the echo. `load_experiment` no longer hot-swaps the open experiment in place (that was a COM-only method with no procedure-language equivalent) -- it now quits the running instance and relaunches Igor Pro with the target file path as a launch argument, a real process restart; unsaved changes in the previously-open experiment are lost. `ensure_igor_pro_bridge_defined` is retired -- it existed to manage a `#ifdef IGOR_PRO_BRIDGE` gating convention that ZBR's always-on independent-module architecture doesn't use.\n\nSee `Packages/doc/igor-pro-bridge.rst` and `SESSION_NOTES.md` in the MIES repository for the full protocol research, design decisions, and confirmed-live test results behind this rewrite.\n\nTools:\n- `execute_igor_command` / `execute_igor_command_unattended`: run a command string on Igor's command line (submitted via `ZBR_SubmitCommand`/`ZBR_SubmitCommandUnattended`, polled via `ZBR_PollCommand`); include a `print` call (NOT `fprintf 0, ...` -- confirmed not captured, see v2.0.1 changelog above) to get data back. Both return `{results, history}`. The `_unattended` variant automatically disables/restores Igor's Debugger around the call.\n- `read_session_history`: read back everything sent to Igor's history area since this bridge's capture started.\n- `get_wave`: read any Igor wave's full data and metadata back (any dimensionality, numeric/complex/text/wave-reference).\n- `load_experiment`: quit the running instance and relaunch Igor Pro with a .pxp file path as a launch argument.\n- `check_bridge_health`: diagnose whether this bridge can reach Igor Pro's ZeroMQ server right now.\n- `check_compilation_state` / `reload_and_compile_procedures`: check and refresh Igor's compiled state after editing a `.ipf` file on disk.\n- `dismiss_compile_error_dialog`: close a stuck \"Function Compilation Error\" dialog via a posted Escape key, without needing OS focus.\n- `get_debugger_state` / `set_debugger_enabled` / `restore_debugger_settings`: read, change, and restore Igor's Debugger settings.\n- `get_environment_summary`: summarize the live instance (version, loaded experiment, XOPs, included procedure files, data folders, Debugger settings).\n- `read_help_file`: read an Igor Pro help file (.ihf) as structured, formatted text.\n- `get_bridge_version`: report the running bridge version and Python/package versions.\n- `configure_igor_launch` / `launch_igor_pro_unattended`: record the Igor Pro executable path once per session, then launch it with the `/UNATTENDED` flag and wait for it to become reachable over ZeroMQ.\n\n**Requirements:**\n- Igor Pro 9.00 or later, running on Windows, with the ZeroMQ-XOP installed and loaded.\n- `Packages/MIES/ZMQ_BridgeHelpers.ipf` (or a copy of it) `#include`-d and compiled into the target experiment -- see `Packages/doc/igor-pro-bridge.rst` (\"Installation\") for the one-time setup steps for experiments other than this repo's own.\n- Python (accessible as `python` on PATH), with the pinned packages in `requirements.txt` installed into that same environment. Run `install.ps1` to do this correctly and also complete pywin32's required post-install step (still needed for window-handling/process-launch helpers, even though this bridge no longer uses COM) -- see that script's own help (`Get-Help ./install.ps1 -Full`).\n- No elevation/privilege-matching requirement of any kind -- this is the change v2.0.0 makes.\n\nPrior COM-based history (v1.x): see `Packages/doc/igor-pro-bridge.rst` for the full v1.x changelog, retained there for reference.", + "author": { + "name": "Michael Huth" + }, + "server": { + "type": "python", + "entry_point": "src/server.py", + "mcp_config": { + "command": "python", + "args": [ + "${__dirname}/src/server.py" + ], + "env": {} + } + }, + "compatibility": { + "claude_desktop": ">=1.0.0", + "platforms": [ + "win32" + ], + "runtimes": { + "python": ">=3.10" + } + }, + "tools": [ + { + "name": "execute_igor_command", + "description": "Execute a single Igor Pro command string in the running Igor instance (submit/poll over ZeroMQ). Include a print call (not fprintf 0, ...) to get data back. Returns {results, history}." + }, + { + "name": "execute_igor_command_unattended", + "description": "Same as execute_igor_command, but disables Igor's Debugger for the duration of the call and restores it afterward." + }, + { + "name": "submit_igor_command", + "description": "Queue a command for deferred execution and return a token immediately, without waiting for it to finish. Use for commands with an unknown or long (hours to weeks) runtime instead of execute_igor_command. Poll completion with poll_igor_command. Guaranteed (as of v2.2.0) to always reach done=true eventually, even if the command fails to parse or errors partway through." + }, + { + "name": "submit_igor_command_unattended", + "description": "Same as submit_igor_command, but disables Igor's Debugger for the duration of the command and restores it afterward. Recommended for anything long-running." + }, + { + "name": "poll_igor_command", + "description": "Check whether a command submitted via submit_igor_command/submit_igor_command_unattended has finished yet, and retrieve its captured output if so. Safe to call any number of times, spaced arbitrarily far apart." + }, + { + "name": "read_session_history", + "description": "Read back everything sent to Igor's history area since this bridge's capture started." + }, + { + "name": "get_wave", + "description": "Return an existing Igor wave's full data and metadata (any dimensionality; numeric, complex, text, or wave-reference)." + }, + { + "name": "load_experiment", + "description": "Quit the running Igor Pro instance and relaunch it with a .pxp experiment file path as a launch argument." + }, + { + "name": "check_bridge_health", + "description": "Diagnose whether this bridge can reach Igor Pro's ZeroMQ server right now." + }, + { + "name": "check_compilation_state", + "description": "Report whether Igor's procedure code is currently compiled or uncompiled." + }, + { + "name": "reload_and_compile_procedures", + "description": "Reload changed .ipf files from disk and attempt a fresh compilation, reporting the resulting compiled state." + }, + { + "name": "dismiss_compile_error_dialog", + "description": "Attempt to close a stuck Igor Pro compile-error dialog by posting a simulated Escape key press to it." + }, + { + "name": "get_debugger_state", + "description": "Read Igor's current Debugger settings (enable/debugOnError/debugOnAbort/NVAR_SVAR_WAVE_Checking)." + }, + { + "name": "set_debugger_enabled", + "description": "Enable or disable Igor's Debugger, optionally changing individual sub-settings." + }, + { + "name": "restore_debugger_settings", + "description": "Restore Igor's Debugger settings to a previously-saved snapshot." + }, + { + "name": "get_environment_summary", + "description": "Summarize the live Igor Pro instance: version, loaded experiment, XOPs, included procedure files, data folders, Debugger settings." + }, + { + "name": "read_help_file", + "description": "Read an Igor Pro help file (.ihf) as structured, formatted text (paragraph style names preserved, e.g. Topic/Code1/Steps), without leaving any lasting change to Igor's help-window state." + }, + { + "name": "get_bridge_version", + "description": "Return the version of this Igor Pro Bridge build that is actually running right now, plus the Python interpreter/package versions it's running with." + }, + { + "name": "configure_igor_launch", + "description": "Record the full path to the Igor Pro executable to use for launch_igor_pro_unattended/load_experiment, for this bridge session." + }, + { + "name": "launch_igor_pro_unattended", + "description": "Launch the configured Igor Pro executable with the /UNATTENDED flag, always as a plain non-elevated child process, and wait for it to become reachable over ZeroMQ." + } + ], + "keywords": [ + "igor pro", + "wavemetrics", + "zeromq", + "scientific computing" + ], + "license": "MIT" +} diff --git a/tools/igor-mcp-bridge/requirements.txt b/tools/igor-mcp-bridge/requirements.txt index 5bc999efb0..7bdc50720b 100644 --- a/tools/igor-mcp-bridge/requirements.txt +++ b/tools/igor-mcp-bridge/requirements.txt @@ -43,11 +43,13 @@ mcp==1.29.0 \ --hash=sha256:f5a075bb611f23d6f4d080c6a1699fa62772eebc562ba9e66b306ddde1c755f7 -# pywin32: provides win32com.client (COM automation), win32api/win32con/win32gui/ -# win32process (elevation and window handling), and pywintypes (COM error types) -- -# all used directly by server.py. Also requires the separate post-install step -# (Scripts\pywin32_postinstall.py -install) to register its COM-support DLLs; both -# install.ps1 and a plain `pip install` alone are not sufficient without that step. +# pywin32: as of v2.0.0, no longer used for talking to Igor Pro itself (that's pyzmq, +# below) -- only for win32api/win32con/win32gui/win32process, used by +# dismiss_compile_error_dialog's window enumeration and by launch_igor_pro_unattended/ +# load_experiment's process launching. Still requires the separate post-install step +# (Scripts\pywin32_postinstall.py -install) to register its COM-support DLLs even +# though this bridge no longer uses COM itself; both install.ps1 and a plain +# `pip install` alone are not sufficient without that step. # Hashes cover the cp310/cp311/cp312/cp313/cp314 win_amd64 wheels. pywin32==312; sys_platform == "win32" \ --hash=sha256:5dbc35d2b5320dc07f25fa31269cfb767471002b17de5eb067d03da68c7cb2db \ @@ -56,6 +58,18 @@ pywin32==312; sys_platform == "win32" \ --hash=sha256:c53e878d15a1c44788082bfe712a905433473aa38f86375b7cf8b45e3acbaaf9 \ --hash=sha256:d11417d84412f859b722fad0841b3614459ed0047f7542d8362e77884f6b6e8a +# pyzmq: provides the `zmq` module server.py uses to talk to Igor Pro's ZeroMQ-XOP +# (new in v2.0.0 -- replaces the COM transport that pywin32.win32com.client used to +# provide; pywin32 is still needed above, but now only for dismiss_compile_error_ +# dialog's window enumeration and for launching the Igor Pro process). No transitive +# dependencies of its own -- the Windows wheels statically bundle libzmq. The +# cp312-abi3 wheel covers 312/313/314; 310 and 311 need their own wheel each (pyzmq +# does not build an abi3 wheel back that far). +pyzmq==27.1.0 \ + --hash=sha256:c0bb87227430ee3aefcc0ade2088100e528d5d3298a0a715a64f3d04c60ba02f \ + --hash=sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97 \ + --hash=sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf + # --- Transitive dependencies (mcp's own dependency tree) ------------------------- # Required by pip's hash-checking mode (see the top comment) -- pip resolves these # via mcp's own pyproject.toml at install time regardless, this just makes each one diff --git a/tools/igor-mcp-bridge/server.py b/tools/igor-mcp-bridge/server.py index db6bcc02c8..327b65c8b0 100644 --- a/tools/igor-mcp-bridge/server.py +++ b/tools/igor-mcp-bridge/server.py @@ -2,85 +2,151 @@ Igor Pro MCP bridge server ========================== -Exposes a running Igor Pro instance to Claude (or any MCP client) as a set of MCP tools, -by acting as a COM Automation *client* that talks to Igor Pro's built-in ActiveX -Automation *Server* on Windows. - -All API details below were extracted directly from the local file: - Igor Pro Folder\\Miscellaneous\\Windows Automation\\Automation Server.ihf -(WaveMetrics' own reference for this interface) during this session. Confirmed facts: - -- ProgID: "IgorPro.Application". -- Connect to an ALREADY RUNNING Igor instance with GetActiveObject (this is the Python - equivalent of the documented VB pattern `GetObject(, "IgorPro.Application")`). Using - win32com.client.Dispatch() instead would *launch* a new Igor instance, which requires - extra care (the docs warn the client must then wait for Igor to finish initializing - before calling methods) -- GetActiveObject sidesteps that entirely by only attaching to - something already running and initialized. -- Execute(BSTR cmds): fire-and-forget, raises a COM error on failure, no structured - output. -- Execute2(int flags, int codePage, BSTR cmds, int* pIgorErrorCode, BSTR* errorMsg, - BSTR* history, BSTR* results): does NOT raise a COM/Automation error just because the - Igor command itself failed -- you must check pIgorErrorCode (0 == success) yourself. - `codePage` is ignored since Igor 7 (pass 0). `results` is how you get data back: put - `fprintf 0, "..."` calls inside `cmds` and read them from `results` afterwards (this is - literally WaveMetrics' own documented example: `fprintf 0, "%g", V_avg` then read - `results`). -- IApplication.DataFolder(nameOrPath) -> IDataFolder. IDataFolder.Wave(waveNameOrPath) -> - IWave. `waveNameOrPath` may be an absolute path (e.g. "root:myFolder:myWave"), so in - practice you can anchor on "root:" and pass a full absolute path straight into .Wave(). -- IWave.GetDimensions(IgorProDataType* pDataType, long* pNumRows, long* pNumColumns, - long* pNumLayers, long* pNumChunks). -- IgorProDataType enum values (confirmed from the .ihf, exact hex values): - ipDataTypeText = 0 - ipDataTypeComplex = 0x01 (combination flag, OR'd with another value) - ipDataTypeFloat = 0x02 - ipDataTypeDouble = 0x04 - ipDataTypeSignedByte = 0x08 - ipDataTypeSignedShort = 0x10 - ipDataTypeSignedLong = 0x20 - ipDataTypeUnsignedByte = 0x48 - ipDataTypeUnsignedShort = 0x50 - ipDataTypeUnsignedLong = 0x60 - i.e. dataType == 0 means text, anything else is some numeric flavor (real-valued - numeric flavors all supported by GetNumericWavePointValue below; complex waves -- - dataType & 0x01 -- are NOT handled by this file yet, see limitation note below). -- IWave.GetNumericWavePointValue(long index, double* pValue) -- single-point numeric - read, "supports real data only" (per the docs' own wording), works for any real - numeric subtype (float/double/int/etc.), 1D waves only. -- IWave.GetTextWavePointValue(long index, int codePage, BSTR* pValue) -- single-point - text read, 1D waves only, codePage ignored since Igor 7 (pass 0). - (The docs also document GetRawTextWaveData/GetNumericWaveDataAsDouble, which pull an - entire wave at once via a SAFEARRAY, but explicitly recommend the point-value methods - "for most uses" -- and the point-value methods sidestep SAFEARRAY marshaling questions - entirely, so this file uses those instead. Whole-wave SAFEARRAY access could be added - later as a faster path for large waves.) -- **CRITICAL SETUP REQUIREMENT, per the docs**: "The Windows operating system requires - that you run the client and server (Igor) as administrator." Confirmed empirically, - however, that elevation itself is not the actual requirement -- this Python process - and Igor Pro must run at the SAME privilege level (both elevated as Administrator, or - both not); Igor's docs only document/test the both-elevated case. A mismatch between - the two, not non-elevation per se, is what breaks the COM connection, and is easy to - miss (e.g. after Claude Desktop is reopened normally, which does not preserve - elevation from a previous launch, while Igor Pro is still running elevated from - before). - -ONE THING THIS FILE CANNOT VERIFY FROM here (no Windows/Igor available to actually run -this): the exact Python-side calling convention pywin32's dynamic dispatch uses for -methods with multiple [out] parameters. The general IDispatch convention -- and how -win32com.client's dynamic dispatch conventionally exposes it -- is: [out]-only -parameters (not [in,out]) are NOT passed by the caller; instead they come back bundled -as a tuple appended to the method's normal return value. That is the convention this -file assumes throughout (e.g. `errorCode, errorMsg, history, results = igor.Execute2(0, -0, cmd)`). This is standard, well-established pywin32 behavior (the same pattern used -for e.g. Excel's Automation methods), not a wild guess -- but it has not been run against -the real Igor Pro COM server in this session, so treat it as the one item to confirm on -first real use. If it doesn't unpack as expected, print(repr(result)) from a raw call to -see the actual shape pywin32 returned and adjust the unpacking. +Exposes a running Igor Pro instance to Claude (or any MCP client) as a set of MCP tools. + +**v2.0.0: transport rewritten from COM to ZeroMQ.** Versions through 1.27.0 talked to +Igor Pro as a COM Automation *client* (win32com, `IgorPro.Application`, `Execute2`). +From 2.0.0 on, this bridge instead talks to Igor Pro's ZeroMQ-XOP +(https://github.com/AllenInstitute/ZeroMQ-XOP) over a plain TCP socket, sending +`CallFunction` JSON requests and calling into Igor-side helper functions in +`Packages/MIES/ZMQ_BridgeHelpers.ipf` (the `ZBR` independent module). See +SESSION_NOTES.md for the full COM-vs-ZeroMQ evaluation that led here, and +Packages/doc/igor-pro-bridge.rst for the up-to-date setup steps. + +Why this changed: COM requires this Python process and Igor Pro to run at the SAME +Windows privilege level (both elevated, or both not) -- a mismatch is a real, +easy-to-miss failure mode (e.g. Claude Desktop reopened normally after Igor Pro was +left running elevated from before). ZeroMQ is a plain localhost TCP socket with no +such requirement at all -- **this bridge no longer cares about elevation in any way**. +That is also why `launch_igor_pro_unattended` no longer has an elevation-branching +code path (see its docstring): it always launches Igor Pro as a plain, non-elevated +child process, at whatever privilege level this bridge process itself is running at. + +**Setup requirement, new in v2.0.0**: unlike the COM transport (which worked against +a completely stock Igor Pro installation with zero custom procedure code), this +transport requires `Packages/MIES/ZMQ_BridgeHelpers.ipf` to be `#include`-d and +compiled into whatever Igor Pro experiment this bridge talks to -- there is no +bootstrap path over ZeroMQ itself (if that file isn't loaded, there is nothing +listening on the port at all). This repo's own `Packages/MIES_Include.ipf` already +does this permanently. Any OTHER Igor Pro experiment that wants to use this bridge +needs `ZMQ_BridgeHelpers.ipf` copied onto its own procedure search path with a +matching `#include` added by hand, then a recompile -- see +Packages/doc/igor-pro-bridge.rst for the exact steps. This is a one-time, +per-experiment setup cost that didn't exist before; it is the price of everything +else this transport buys (see SESSION_NOTES.md's ZeroMQ-evaluation section for the +full trade-off discussion). + +Protocol summary (confirmed empirically this session against a live Igor Pro 9.06 +instance, and against https://github.com/AllenInstitute/ZeroMQ-XOP's own README): + +- Endpoint: `tcp://127.0.0.1:5680` (`IGOR_ZMQ_ENDPOINT` below) -- matches + `ZBR_ZEROMQ_ENDPOINT` in ZMQ_BridgeHelpers.ipf, deliberately NOT MIES's own + `ZEROMQ_BIND_REP_PORT` (5670) so this can coexist with MIES's own (currently + short-circuited) ZeroMQ subsystem in the same experiment. +- One JSON `CallFunction` request per ZeroMQ REQ-socket round trip: send + `{"version": 1, "messageID": ..., "CallFunction": {"name": ..., "params": [...]}}`, + receive `{"errorCode": {"value": ..., "msg": ...}, "result": ...}`. A NEW REQ socket + is created for every single call (see `call_function` below) rather than one + reused across calls -- a REQ socket that times out waiting for a reply is left in a + state where it cannot send again without being recreated (confirmed empirically + this session), so per-call sockets sidestep that fragility entirely at negligible + cost for a local TCP connection. +- **Confirmed documentation bug in the XOP's own README/help**: it says + `CallFunction.name` should be "a ProcGlobal function without module and/or + independent module specification, i.e. without `#`" -- empirically confirmed this + session that this is simply wrong for independent-module functions (like + everything in the `ZBR` module): the qualified form (`"ZBR#ZBR_Ping"`) is what + actually works; the unqualified form fails with `errorCode.value=101` ("Unknown + function"). Every ZBR call in this file uses the qualified form. +- Multi-return Igor functions (`Function [a, b] Foo()`, Igor 8+) are fully supported + over this protocol -- `result` becomes a JSON array of typed values in declaration + order. `call_function` below decodes this into a plain Python list automatically. +- Wave return values carry the ENTIRE wave (dimensions, units, note, complex/text/ + wave-ref support) natively-serialized as JSON -- see `_decode_wave` below. This + replaces the old COM bridge's per-point `GetNumericWavePointValue` loop entirely, + and is why the new `get_wave` supports far more than the old "1D real waves only" + limitation. +- **Central architectural constraint, unchanged from the COM-vs-ZeroMQ evaluation**: + Igor's `Execute` operation (used to run arbitrary free-form command text) cannot be + called unqueued from inside a Function -- only `Execute/P` (deferred: queued to run + only after the calling function returns) is legal there. This means a single + CallFunction round trip cannot synchronously "run this arbitrary command string and + hand back what it printed" -- there is no direct equivalent of the COM bridge's + `Execute2`. `execute_igor_command`/`execute_igor_command_unattended` below instead + use a submit-then-poll pattern (`ZBR_SubmitCommand`/`ZBR_SubmitCommandUnattended` + queue the command and return a token immediately; `ZBR_PollCommand`, called via a + LATER separate request, reports completion and the captured output). Every OTHER + tool below that doesn't need to run arbitrary free-form text (get_wave, + check_compilation_state, get_debugger_state, get_environment_summary's underlying + queries, read_help_file, etc.) is backed by a small, purpose-built, directly-callable + Igor function instead, and is a single synchronous round trip -- no polling needed. +- **Commands with an unknown or long runtime (v2.1.0+)**: `execute_igor_command`/ + `execute_igor_command_unattended` block the whole MCP tool call while polling, up to + their own `timeout_seconds` -- workable for anything expected to finish in seconds, + but not for a calculation that might run for hours or weeks, since the underlying + MCP transport itself has been observed to time out a single tool call well under a + minute regardless of what `timeout_seconds` requests. `submit_igor_command`/ + `submit_igor_command_unattended` expose the same submit step as its own tool + (returns a token immediately, no waiting at all), and `poll_igor_command(token)` + exposes the same poll step as its own tool (one cheap, instant check, callable any + number of times, spaced arbitrarily far apart). Because all of the actual state + (done flag, captured text) lives entirely in Igor Pro's own data waves + (`root:Packages:ZBR`), not in this bridge's Python process, polling stays reliable + no matter how long the job runs or how many times this bridge process/Claude + Desktop itself restarts in the meantime -- the only thing that actually ends the + job is Igor Pro itself quitting, crashing, or restarting. + +**Behavior changes from the COM version worth knowing about**: + +- `execute_igor_command`/`execute_igor_command_unattended` no longer return a clean, + separately-captured "results" (fprintf-only output) distinct from "history" (full + history including the echoed command) -- COM's `Execute2` had special handling to + split these; this transport cannot replicate that, since the submit/poll mechanism + only has Igor's own history-diffing (`CaptureHistory`) to go on. Both keys are now + populated with the same text (everything printed to history while the command ran, + including its own echo). Prefix a command with `Silent 1;` if you want the echo + suppressed from the returned text. +- **Use `print`, not `fprintf 0, ...`, to get data back.** Confirmed live this + session: `CaptureHistory` (which the submit/poll mechanism above relies on) + captures `print` output but does NOT capture `fprintf`-to-history-refnum output at + all, whether directed at refnum 0 (history), -1, or -2 -- a command consisting of + only `fprintf 0, "..."` runs without error but returns empty "results"/"history" + every time, even though the exact same value printed via `print` is captured + correctly. This is a real behavior change from the COM version, whose `Execute2` + had its own dedicated mechanism for capturing `fprintf(0,...)` output specifically + (unrelated to `CaptureHistory`), which is why that was the documented pattern + before. `fprintf` remains fine (and necessary) for anything that ISN'T about + getting data back through this bridge, e.g. writing to a wave or a real file. +- `load_experiment` no longer hot-swaps the open experiment in the running instance + (that was `IApplication.LoadExperiment`, a COM-only method -- confirmed neither + "LoadExperiment" nor "OpenFile" appear anywhere in Igor Reference.ihf, only in + Automation Server.ihf, and there is no procedure-language equivalent). It instead + asks the running instance to quit, then relaunches the configured Igor Pro + executable with the target file path as a launch argument -- a real process + restart, not an in-place swap. **Any unsaved changes in the currently-open + experiment are lost** (the same as before -- COM's LoadExperiment never auto-saved + either -- but now there is also no "hot" instance left to save from afterward if + you forgot). Call `execute_igor_command('SaveExperiment')` first if that matters. +- `ensure_igor_pro_bridge_defined` is retired. It existed to make sure a + `#ifdef IGOR_PRO_BRIDGE`-gated procedure file's optional code got compiled in + without a human hand-editing the experiment's Procedure window. `ZBR`'s own + functions have no such gating (the whole point of an independent module is that it + compiles on its own regardless of ProcGlobal's `#define` state), so this bridge no + longer needs it for its own purposes. +- `check_bridge_health`/`check_compilation_state` can no longer distinguish "Igor Pro + isn't running" from "Igor Pro is running but ZMQ_BridgeHelpers.ipf isn't + included/compiled/bound" from "wrong port" as cleanly as COM's `GetActiveObject` + could (a clean binary "is there a registered COM object" signal) -- a ZeroMQ REQ + socket that gets no reply at all looks the same in all three cases. See + `check_bridge_health`'s docstring for what to check by hand if this happens. Setup ----- - pip install mcp pywin32 + pip install mcp pyzmq pywin32 + +(pywin32 is still needed for `dismiss_compile_error_dialog`'s window enumeration and +for launching the Igor Pro process -- see below. It is no longer needed for, or +involved in, talking to Igor Pro itself.) Registering with Claude Desktop -------------------------------- @@ -92,1858 +158,843 @@ mcpb pack tools/igor-mcp-bridge tools/igor-mcp-bridge/igor-pro-bridge-X.Y.Z.mcpb See Packages/doc/igor-pro-bridge.rst ("Installation") for the full, up-to-date -installation steps -- this docstring previously described the config.json approach, -which was found to be unreliable and is no longer how this bridge is distributed. - -After installing (or updating), fully restart Claude Desktop. Remember: Claude Desktop's -Python process and Igor Pro itself need to be running at the SAME privilege level (both -elevated as Administrator, or both not) for the COM connection to succeed -- elevation -itself is not the requirement, a mismatch between the two is what breaks it. +installation steps, including the one-time ZMQ_BridgeHelpers.ipf setup step for any +experiment other than this repo's own. This is a *local* MCP server (stdio transport) -- it only works from a Claude Desktop session running on the same Windows machine as Igor Pro, not from a cloud/Cowork sandbox. """ -import ctypes import html.parser import importlib.metadata +import json import os -import re import subprocess import sys import tempfile import time +import uuid if sys.platform != "win32": raise RuntimeError( - "tools/igor-mcp-bridge/server.py is Windows-only (requires pywin32 and " - "Igor Pro's COM Automation Server). It cannot run on this platform " + "tools/igor-mcp-bridge/server.py is Windows-only (requires pywin32 for " + "dismiss_compile_error_dialog's window enumeration and for launching the " + "configured Igor Pro executable). It cannot run on this platform " f"({sys.platform!r}) -- e.g. running it by accident on a non-Windows dev " - "machine or in CI. See the module docstring for setup requirements." + "machine or in CI. The ZeroMQ transport itself is cross-platform; this " + "restriction is only about this file's OS-level helper tools." ) -import pywintypes import win32api import win32con -import win32com.client import win32gui import win32process +import zmq + from mcp.server.fastmcp import FastMCP -IGOR_COM_PROGID = "IgorPro.Application" +mcp = FastMCP("igor-pro") -# IgorProDataType enum (confirmed values, see module docstring) -IP_DATATYPE_TEXT = 0 -IP_DATATYPE_COMPLEX_FLAG = 0x01 +# --- ZeroMQ transport ---------------------------------------------------------------- + +# Matches ZBR_ZEROMQ_ENDPOINT in Packages/MIES/ZMQ_BridgeHelpers.ipf. +IGOR_ZMQ_ENDPOINT = "tcp://127.0.0.1:5680" +_ZMQ_DEFAULT_RECV_TIMEOUT_MS = 5000 +_ZMQ_SEND_TIMEOUT_MS = 2000 +_ZMQ_LINGER_MS = 0 + +_zmq_context = None + + +def _get_zmq_context(): + global _zmq_context + if _zmq_context is None: + _zmq_context = zmq.Context() + return _zmq_context + + +class IgorZmqError(RuntimeError): + """Igor Pro replied, but reported errorCode.value != 0 for the CallFunction call.""" + + +class IgorZmqUnreachable(RuntimeError): + """No reply was received at all within the timeout. Could mean: Igor Pro isn't + running, ZMQ_BridgeHelpers.ipf isn't #include-d/compiled in the running instance, + its ZeroMQ server socket isn't bound to IGOR_ZMQ_ENDPOINT, or the reply is simply + slow (e.g. a long-running command; pass a longer timeout_ms).""" + + +def _decode_number(value): + """Non-normal numbers (NaN/Inf/-Inf) are encoded as strings per the ZeroMQ-XOP's + own spec ("Messages consist of JSON ... NaN, Inf and -Inf are not supported by + JSON, so we encode these non-normal numbers as strings"). Decode those back to + real Python floats; pass anything else through unchanged.""" + if isinstance(value, str): + low = value.strip().lower() + if low == "nan": + return float("nan") + if low in ("inf", "+inf"): + return float("inf") + if low == "-inf": + return float("-inf") + return value + + +def _reshape_column_major(flat, dim_size): + """Reshape a flat, column-major list per dim_size (1 to 4 entries, per the + ZeroMQ-XOP's wave serialization spec) into nested Python lists indexed + data[row][col][layer][chunk] -- matches the spec's own worked example + (`np.array(raw).reshape(dim_size, order='F')`) without requiring numpy.""" + if not dim_size or list(dim_size) == [0]: + return [] + + dims = list(dim_size) + [1] * (4 - len(dim_size)) + rows, cols, layers, chunks = dims[:4] + if rows == 0: + return [] + + def at(r, c=0, l=0, k=0): + return flat[r + rows * (c + cols * (l + layers * k))] + + ndims = len(dim_size) + if ndims <= 1: + return [at(r) for r in range(rows)] + if ndims == 2: + return [[at(r, c) for c in range(cols)] for r in range(rows)] + if ndims == 3: + return [ + [[at(r, c, l) for l in range(layers)] for c in range(cols)] + for r in range(rows) + ] + return [ + [ + [[at(r, c, l, k) for k in range(chunks)] for l in range(layers)] + for c in range(cols) + ] + for r in range(rows) + ] -# IgorProLoadType enum (confirmed from Automation Server.ihf, used by -# IApplication.LoadExperiment): ipLoadTypeOpen = 2, ipLoadTypeStationery = 4, -# ipLoadTypeMerge = 5. Only ipLoadTypeOpen is used by this bridge so far. -IP_LOAD_TYPE_OPEN = 2 -mcp = FastMCP("igor-pro") +def _decode_wave(value): + """value is None (an invalid/free wave reference, `$""`) or the wave-serialization + object documented in https://github.com/AllenInstitute/ZeroMQ-XOP's README ("Wave + serialization format"). Returns None, or a dict with the wave's type, shape, data + (reshaped into nested Python lists matching the wave's own dimensionality), unit, + and note. Supports numeric (real and complex), text, and wave-reference waves.""" + if value is None: + return None -_igor = None + wave_type = value.get("type", "") + dimension = value.get("dimension", {}) or {} + dim_size = dimension.get("size", []) + data = value.get("data", {}) or {} + raw = data.get("raw", []) + + if wave_type == "WAVE_TYPE": + decoded_data = [ + _decode_wave(item) if item is not None else None for item in raw + ] + elif isinstance(raw, dict) and "real" in raw: + # Complex wave: raw = {"real": [...], "imag": [...]}. + real = [_decode_number(x) for x in raw.get("real", [])] + imag = [_decode_number(x) for x in raw.get("imag", [])] + decoded_data = [complex(r, i) for r, i in zip(real, imag)] + else: + decoded_data = [_decode_number(x) for x in raw] -# Path to the Igor Pro executable to use for launch_igor_pro_unattended, set via -# configure_igor_launch(). Deliberately session-scoped (this process's in-memory -# lifetime only, not persisted to disk) and never defaulted/guessed -- see -# configure_igor_launch's docstring for why: the calling agent should ask the user -# for this once at the start of a session rather than assume a default installation -# path, since Igor Pro version/location varies (this repo alone has been tested -# against both an Igor Pro 9 and an Igor Pro 10 install in different folders). -_configured_igor_exe_path = None + return { + "type": wave_type, + "dim_size": dim_size, + "data": _reshape_column_major(decoded_data, dim_size), + "unit": data.get("unit"), + "note": value.get("note", ""), + "dimension": dimension, + } + + +def _decode_typed(typed): + """typed is {"type": "variable"|"string"|"wave"|"dfref", "value": ...} -- return + the corresponding plain Python value (numbers get NaN/Inf decoded, waves get fully + decoded via _decode_wave, everything else passes through as-is).""" + if not isinstance(typed, dict): + return typed + kind = typed.get("type") + value = typed.get("value") + if kind == "variable": + return _decode_number(value) + if kind == "wave": + return _decode_wave(value) + return value + + +def call_function(name, params=None, timeout_ms=_ZMQ_DEFAULT_RECV_TIMEOUT_MS): + """Send one CallFunction request to Igor Pro and return its decoded result. + + name must be the FULLY QUALIFIED function name for anything in the ZBR + independent module, e.g. "ZBR#ZBR_Ping" -- see the module docstring's + documentation-bug note for why (the XOP's own docs say to omit the "#"; that is + wrong for independent-module functions, confirmed empirically). + + Returns a plain Python value: None/number/string for a single scalar return, a + dict for a wave return (see _decode_wave), or a list of such values (in + declaration order) for a multi-return ("Function [a, b] Foo()") function. + + Raises IgorZmqUnreachable if no reply arrives within timeout_ms at all, or + IgorZmqError if Igor Pro replied but reported errorCode.value != 0 (the error + message includes any "history" the XOP reports alongside the error, which often + shows exactly where inside the called function things went wrong). + """ + request = { + "version": 1, + "messageID": uuid.uuid4().hex, + "CallFunction": {"name": name, "params": list(params) if params else []}, + } + payload = json.dumps(request) + sock = _get_zmq_context().socket(zmq.REQ) + sock.setsockopt(zmq.RCVTIMEO, timeout_ms) + sock.setsockopt(zmq.SNDTIMEO, _ZMQ_SEND_TIMEOUT_MS) + sock.setsockopt(zmq.LINGER, _ZMQ_LINGER_MS) + sock.connect(IGOR_ZMQ_ENDPOINT) + try: + sock.send_string(payload) + reply_raw = sock.recv_string() + except zmq.error.Again: + raise IgorZmqUnreachable( + f"No reply from Igor Pro within {timeout_ms}ms while calling {name!r}. " + f"Make sure Igor Pro is running, ZMQ_BridgeHelpers.ipf is #include-d and " + f"compiled in the current experiment, and its ZeroMQ server socket is " + f"bound to {IGOR_ZMQ_ENDPOINT!r} (see check_bridge_health)." + ) from None + finally: + sock.close() + + try: + reply = json.loads(reply_raw) + except json.JSONDecodeError as e: + raise IgorZmqError( + f"Malformed reply from Igor Pro for {name!r}: {reply_raw!r}" + ) from e + + error_code = reply.get("errorCode") or {} + if error_code.get("value", 0) != 0: + detail = ( + f"Igor Pro reported an error calling {name!r} (code " + f"{error_code.get('value')}): {error_code.get('msg', '(no message)')}" + ) + history = reply.get("history") + if history: + detail += f"\nIgor history during this call: {history!r}" + raise IgorZmqError(detail) -def _is_current_process_elevated(): - """Return True/False if this Python process itself is running elevated (as - Administrator), or None if that can't be determined. + result = reply.get("result") + if isinstance(result, list): + return [_decode_typed(item) for item in result] + return _decode_typed(result) - Uses ctypes.windll.shell32.IsUserAnAdmin() -- the standard, minimal way to check - *this* process's own elevation on Windows. This is deliberately much simpler than - the OpenProcessToken/GetTokenInformation dance needed to check an *arbitrary* other - process's elevation (e.g. Igor Pro's) from outside; for our own process, this one - call is sufficient and doesn't need that machinery. - This check exists because an elevation mismatch between this process and Igor Pro - is a real, easy-to-miss failure mode confirmed during development: Claude Desktop - can appear to be "running as Administrator" while the specific child process - running this script is not, if Claude Desktop itself was reopened normally rather - than explicitly relaunched via "Run as administrator" (Windows does not persist - elevation across relaunches by default). - """ +def _reachable(timeout_ms=1000): + """True if ZBR#ZBR_Ping answers within timeout_ms, False otherwise. Used for + "is anything listening right now" checks (already_running / poll loops) where the + caller doesn't need the actual reply.""" try: - return bool(ctypes.windll.shell32.IsUserAnAdmin()) - except Exception: - return None + call_function("ZBR#ZBR_Ping", timeout_ms=timeout_ms) + return True + except (IgorZmqError, IgorZmqUnreachable): + return False -_elevated_at_startup = _is_current_process_elevated() -if _elevated_at_startup is False: - print( - "NOTE: this MCP server process is NOT running elevated (as Administrator). " - "This is fine as long as Igor Pro is ALSO not running elevated -- COM requires " - "this process and Igor Pro to be at the SAME privilege level, not elevation " - "specifically. If Igor Pro is running elevated while this process isn't, every " - "tool call will fail with a COM/RPC error; either relaunch Claude Desktop via " - "'Run as administrator' to match, or restart Igor Pro non-elevated instead.", - file=sys.stderr, - ) -elif _elevated_at_startup is None: - print( - "NOTE: could not determine whether this process is running elevated.", - file=sys.stderr, - ) +# --- Command execution (submit/poll) --------------------------------------------------- +_SUBMIT_POLL_INTERVAL_SECONDS = 0.1 +_SUBMIT_POLL_TIMEOUT_SECONDS = 30.0 -def _get_igor(force_reconnect=False): - """Attach to an already-running Igor Pro instance via COM. - Uses GetActiveObject (not Dispatch) deliberately: GetActiveObject only attaches to - an instance that's already running and initialized, matching WaveMetrics' own - documented VB pattern `GetObject(, "IgorPro.Application")`. Dispatch() would instead - launch a brand-new Igor instance if one isn't already registered, which requires - extra initialization-wait handling this file doesn't implement. +def _submit_and_poll(submit_function: str, command: str, timeout_seconds: float) -> str: + """Submit `command` via the given ZBR submit function (ZBR_SubmitCommand or + ZBR_SubmitCommandUnattended) and poll ZBR_PollCommand until it reports done, + returning the captured text. See the module docstring for why this submit/poll + dance is needed at all (Execute cannot run unqueued inside a Function).""" + token = call_function(f"ZBR#{submit_function}", [command]) - force_reconnect=True discards any cached connection first. Needed because this - process caches _igor for its whole lifetime (it may serve many tool calls): if Igor - Pro is closed/restarted/crashes in between, the cached COM reference goes stale and - every subsequent call fails with a COM/RPC-transport error (e.g. "The RPC server is - unavailable") -- not a normal Igor-level failure. See _run_with_reconnect below. - """ - global _igor - if force_reconnect: - _igor = None - if _igor is None: - try: - _igor = win32com.client.GetActiveObject(IGOR_COM_PROGID) - except Exception as e: + deadline = time.monotonic() + timeout_seconds + while True: + is_done, text = call_function("ZBR#ZBR_PollCommand", [token]) + if is_done: + return text + if time.monotonic() >= deadline: raise RuntimeError( - "Could not attach to a running Igor Pro instance via COM. Make sure: " - "(1) Igor Pro is already running, (2) Igor Pro and this Python process " - "are running at the SAME privilege level -- both elevated (as " - "Administrator), or both not; a mismatch, not elevation itself, is what " - "breaks the COM connection -- and (3) Igor Pro 9.00 (or later) is " - "installed with the Automation Server component." - ) from e - return _igor - - -def _run_with_reconnect(work_fn): - """Run work_fn() once, retrying exactly once with a fresh COM connection if the - cached connection turns out to be stale. - - work_fn should call _get_igor() itself (not close over a stale `igor` variable) so - the retry actually picks up a freshly reconnected object. - - Why this is safe to do unconditionally: Execute2 reports Igor-level command - failures via pIgorErrorCode, not exceptions (see module docstring) -- so a - pywintypes.com_error escaping from here always means the COM/RPC transport itself - broke (most commonly: Igor Pro was closed or restarted since the last call, leaving - a dead reference cached), never that an Igor command merely failed. If Igor is - genuinely not reachable at all, the retry's _get_igor() call raises a plain - RuntimeError (see above), which is not caught here and propagates immediately -- - so this never turns into a silent retry loop. - """ - try: - return work_fn() - except pywintypes.com_error: - _get_igor(force_reconnect=True) - return work_fn() + f"Timed out after {timeout_seconds:.0f}s waiting for a submitted " + f"command to finish (token {token!r}). Command was: {command}" + ) + time.sleep(_SUBMIT_POLL_INTERVAL_SECONDS) -def _get_wave_ref(wave_path: str): - """(Re)derive the IWave COM object for wave_path from the *current* Igor connection. +@mcp.tool() +def execute_igor_command(command: str, timeout_seconds: float = _SUBMIT_POLL_TIMEOUT_SECONDS) -> dict: + """Execute a single Igor Pro command string in the running Igor instance. - This goes through DataFolder() and Wave() -- two more COM calls, same reconnect - risk as everything else here. Factored out so both the initial fetch and any - post-reconnect re-fetch (in get_wave below) call the exact same path, and never - accidentally keep using a `wave` object derived from a now-dead `igor`/`root`. + **If `command`'s runtime is unknown or could be long (more than roughly a + minute), use submit_igor_command/poll_igor_command instead.** This tool blocks + the whole MCP call for up to `timeout_seconds` and will itself get killed by + Claude Desktop's own MCP request timeout well before that if `timeout_seconds` + is set too high -- it is only suitable for commands expected to finish quickly. + + **To get data back, include a `print` call in `command` -- NOT `fprintf 0, ...`.** + Confirmed live: this transport's capture mechanism (`CaptureHistory`) picks up + `print` output but does not capture `fprintf`-to-history output at all (refnum 0, + -1, or -2 all silently produce nothing) -- see the module docstring's "Behavior + changes" section for why (the old COM-based Execute2 had its own separate + mechanism specifically for fprintf(0,...), which no longer applies here). + Whatever `print`s (and the echoed command itself, unless prefixed with + `Silent 1;`) is captured and returned in both "results" and "history" (see the + module docstring for why this transport can no longer separate the two the way + Execute2 did). + + Example: execute_igor_command('WaveStats/Q jack; print V_avg') + + Implementation note: this queues `command` (ZBR_SubmitCommand) and polls for + completion (ZBR_PollCommand) rather than running it in one synchronous round trip + -- Igor's Execute operation cannot run unqueued from inside a Function at all, so + there is no direct equivalent of COM's Execute2 here. `timeout_seconds` bounds how + long this will poll before giving up (raises TimeoutError-style RuntimeError). + + **Caution:** if `command` calls user-defined procedure code and Igor Pro's + Debugger is currently enabled, a breakpoint/runtime error/abort/stale-reference + pause in that code will hang the underlying command indefinitely (this poll loop + will keep timing out and retrying, never actually seeing it finish) -- there is no + scriptable way to resume or dismiss the Debugger window (see set_debugger_enabled's + docstring). Use execute_igor_command_unattended instead whenever nobody is + watching who could close that popup manually. + + **If `command` itself fails to parse or hits a genuine Igor-level runtime error + (Debugger not involved), this now still returns normally instead of hanging until + `timeout_seconds` expires** -- confirmed live for both cases. There is currently no + way to tell that apart from "ran fine and printed nothing", though: the returned + text will simply be shorter/emptier than expected, with no error indication. If + verifying success matters, have `command` `print` an explicit sentinel/result + value itself. """ - igor = _get_igor() - root = igor.DataFolder("root:") - wave = root.Wave(wave_path) - if wave is None: - raise RuntimeError(f"Wave not found: {wave_path}") - return wave + text = _submit_and_poll("ZBR_SubmitCommand", command, timeout_seconds) + return {"results": text, "history": text} -def _read_wave_point(wave, index: int, is_text: bool): - """One point-value COM call -- GetTextWavePointValue or GetNumericWavePointValue.""" - if is_text: - return wave.GetTextWavePointValue(index, 0) - return wave.GetNumericWavePointValue(index) +@mcp.tool() +def execute_igor_command_unattended(command: str, timeout_seconds: float = _SUBMIT_POLL_TIMEOUT_SECONDS) -> dict: + """Run `command` exactly like execute_igor_command, but automatically disable + Igor's Debugger before running it and restore it again afterward -- even if + `command` raises an Igor-level error. + **This is the tool to reach for whenever `command` might call user-defined + procedure code and nothing is watching that could close a Debugger popup by + hand.** See set_debugger_enabled's docstring for why a Debugger pause has no + scriptable resume. -# --- Session-wide history capture (verifying print output after the fact) ---------- -# -# Confirmed from Igor Reference.ihf: CaptureHistoryStart() is a built-in Igor -# function returning a reference number marking the CURRENT position in the history -# area (the command/history window's text). CaptureHistory(refnum, stopCapturing) -# then returns a string containing everything sent to the history area since that -# reference point -- "Set stopCapturing to zero to retrieve history text captured -# so far. Further calls to CaptureHistory with the same reference number will -# return this text, plus any additional history text added subsequently" (i.e. each -# call returns the FULL accumulated text since the start point, not just a delta, -# so repeated reads are simple and never miss anything in between). This is the -# documented, supported way to read back what Igor printed (via `print`, command -# echoing, etc.) after the fact, rather than only being able to see it live on -# screen or asking a human to look. -# -# A capture is started lazily, once, the first time _execute2 runs in this -# process's lifetime (see _ensure_session_history_capture_started), so -# read_session_history() always has something to report without requiring a -# separate explicit "start" call first -- it covers everything since this bridge -# process first talked to Igor. -_session_history_capture_refnum = None - - -def _ensure_session_history_capture_started(): - """Start a session-wide CaptureHistoryStart() capture if one isn't already - running. Deliberately swallows all errors -- this is a best-effort convenience - feature, and must never break a normal command just because this bookkeeping - call failed for some reason (e.g. an ancient Igor version without this - function). Calls igor.Execute2 directly rather than going through _execute2 to - avoid recursing back into this same function. + Only reach for plain execute_igor_command when you deliberately want the Debugger + available (e.g. interactively testing a breakpoint). """ - global _session_history_capture_refnum - if _session_history_capture_refnum is not None: - return - try: - igor = _get_igor() - errorCode, errorMsg, history, results = igor.Execute2( - 0, 0, 'fprintf 0, "%.0f", CaptureHistoryStart()' - ) - if errorCode == 0 and results: - _session_history_capture_refnum = float(results) - except Exception: - pass + text = _submit_and_poll("ZBR_SubmitCommandUnattended", command, timeout_seconds) + return {"results": text, "history": text} + + +_UNKNOWN_TOKEN_PREFIX = "ERROR: unknown token " + +@mcp.tool() +def submit_igor_command(command: str) -> dict: + """Queue `command` for deferred execution and return a token immediately, WITHOUT + waiting for it to finish. + + **Use this instead of execute_igor_command whenever a command's runtime is + unknown or could be long -- minutes, hours, even weeks.** execute_igor_command + blocks the entire MCP tool call until the command finishes or timeout_seconds + elapses, which cannot work for a genuinely long-running calculation: it will hit + Claude Desktop's own MCP request timeout (observed in practice to trigger in well + under a minute) long before a multi-hour command finishes, even though the + command itself keeps running in Igor Pro regardless of what the MCP call does. + + Call poll_igor_command(token) afterward -- as many times as needed, spaced + however far apart in time you like -- to check whether it's done yet and + retrieve its output once it is. + + **Why this is reliable even across very long waits**: the token is just a row + index into plain data waves in root:Packages:ZBR (done/resultText), maintained + entirely by Igor Pro itself. Nothing about polling it later depends on this + bridge's own Python process, on any particular MCP/Claude Desktop session + staying open, or on how much time passes between calls -- this bridge process + restarting, or Claude Desktop being closed and reopened, does not lose or + invalidate the token. The one thing that DOES end the underlying job is Igor + Pro itself quitting, crashing, or restarting -- in that case the computation + itself is gone, not just the token, so there is nothing to recover regardless of + transport. + + **Caution:** if `command` calls user-defined procedure code and Igor Pro's + Debugger is enabled, a breakpoint/runtime error/abort/stale-reference pause + leaves poll_igor_command reporting "not done" forever -- indistinguishable from + a command that's still genuinely, legitimately running, since there is no + scriptable way to detect or resume a Debugger pause (see + set_debugger_enabled's docstring). **Use submit_igor_command_unattended instead + for anything long-running -- this matters far more here than for + execute_igor_command, since nobody is likely to be watching a job that might run + for weeks.** + + **Separately (Debugger not involved): `command` failing to parse, or hitting a + genuine Igor-level runtime error partway through, will NOT leave the token stuck + forever** -- confirmed live. The finish-callback that flips poll_igor_command's + "done" flag is queued as its own independent step, so it always runs regardless + of what happens to `command`. There is, however, no reliable generic way to tell + "command ran and legitimately printed nothing" apart from "command errored out + with no output" -- both come back from poll_igor_command as `"done": True` with + an empty/short result and no error indication. If verifying success matters, have + `command` `print` an explicit sentinel/result value itself. + """ + token = call_function("ZBR#ZBR_SubmitCommand", [command]) + return {"token": token} -def _execute2(command: str): - """Run `command` via Execute2 and return (errorCode, errorMsg, history, results). - See the calling-convention caveat in the module docstring -- this unpacking is the - one thing to verify empirically on first real run. +@mcp.tool() +def submit_igor_command_unattended(command: str) -> dict: + """Same as submit_igor_command, but disables Igor's Debugger for the duration of + `command` and restores it afterward -- see execute_igor_command_unattended's + docstring for the general reasoning. + + **This is the recommended tool for anything long-running submitted via + submit_igor_command/poll_igor_command.** Without it, a Debugger pause partway + through a multi-hour or multi-week calculation would silently hang forever with + no way to distinguish it from the command still legitimately running -- there is + no periodic "is this actually still making progress" signal beyond + poll_igor_command's own done/not-done state. """ - _ensure_session_history_capture_started() + token = call_function("ZBR#ZBR_SubmitCommandUnattended", [command]) + return {"token": token} - def work(): - igor = _get_igor() - return igor.Execute2(0, 0, command) - errorCode, errorMsg, history, results = _run_with_reconnect(work) - return errorCode, errorMsg, history, results +@mcp.tool() +def poll_igor_command(token: str) -> dict: + """Check whether a command submitted via submit_igor_command/ + submit_igor_command_unattended has finished yet, and retrieve its captured + output if so. + + Returns `{"done": False}` while still pending -- call again later, there is no + limit on how long you can wait or how many times you poll (see + submit_igor_command's docstring for why this stays reliable no matter how much + time passes or how many times this bridge process itself restarts in the + meantime). Returns `{"done": True, "results": , "history": }` once + finished -- both keys hold the same captured text, matching + execute_igor_command's own return shape (see that tool's docstring for why + "results"/"history" can no longer be kept separate over this transport, and why + `print`, not `fprintf`, is what actually gets captured). + + For a job expected to run over a very long horizon (hours to weeks), consider + setting up a scheduled task that calls this periodically and reports back once + `"done"` flips to true, rather than relying on this conversation staying open. + + Raises if `token` is not recognized -- e.g. a typo, or a token from a + since-quit/since-restarted Igor Pro instance (tokens do not survive Igor Pro + itself restarting, only this bridge process or Claude Desktop restarting). + + Does NOT raise just because the submitted command itself failed to parse or hit + a runtime error -- that case still reports `"done": True`, just with an + empty/shorter-than-expected result and no explicit error indication (see + submit_igor_command's docstring for why: there is currently no generic way to + detect this here). + """ + is_done, text = call_function("ZBR#ZBR_PollCommand", [token]) + if not is_done: + return {"done": False} + if isinstance(text, str) and text.startswith(_UNKNOWN_TOKEN_PREFIX): + raise RuntimeError(text) + return {"done": True, "results": text, "history": text} @mcp.tool() def read_session_history(stop: bool = False) -> dict: """Read back everything sent to Igor's history area (print output, command - echoing, error messages, etc.) since this bridge process first talked to Igor - -- the reliable way to verify a PAST execute_igor_command/ - execute_igor_command_unattended call's `print` output actually happened, - without asking a human to look at Igor's screen or needing to have captured - the per-call `history` field at the time. - - Backed by Igor's built-in CaptureHistoryStart()/CaptureHistory() functions - (confirmed from Igor Reference.ihf). A capture is started automatically the - first time any command runs through this bridge in this process's lifetime, - so this always has something to report. Each call returns the FULL - accumulated text since that start point, not just what's new since the last - read -- so calling this repeatedly with stop=False (the default) is always - safe and simply returns more (growing) text as more commands run in between. - - stop=True stops the capture (no further text will be recorded for it) and - returns whatever was captured up to that point; a subsequent call to this - tool (or the next command run through this bridge) then starts a brand-new - capture automatically, covering only from that point forward -- use this to - intentionally "reset" what counts as history for a fresh phase of work. - - Raises if no capture is currently active, which should only happen if - CaptureHistoryStart() itself failed when first attempted (e.g. an - unexpectedly old Igor version) -- in that case, fall back to reading the - `history` field returned directly by execute_igor_command/ - execute_igor_command_unattended for that specific call instead. + echoing, error messages, etc.) since this bridge (specifically, ZMQ_BridgeHelpers. + ipf's own capture) started tracking it -- the reliable way to verify a PAST + execute_igor_command/execute_igor_command_unattended call's output actually + happened, without asking a human to look at Igor's screen. + + A capture is started automatically, Igor-side, the first time it's needed (see + ZBR_EnsureCaptureStarted in ZMQ_BridgeHelpers.ipf) so this always has something to + report. Each call returns the FULL accumulated text since that start point, not + just what's new since the last read -- calling this repeatedly with stop=False + (the default) is always safe. + + stop=True stops the capture (no further text will be recorded for it) and returns + whatever was captured up to that point; the next call (to this tool, or the next + command run through this bridge) transparently starts a brand-new capture. """ - global _session_history_capture_refnum - if _session_history_capture_refnum is None: - raise RuntimeError( - "No history capture is currently active in this bridge process. This " - "starts automatically on first use, so this likely means " - "CaptureHistoryStart() failed earlier (see server logs) or nothing " - "has been executed yet -- try running check_bridge_health() first, " - "then retry this call." - ) + text = call_function("ZBR#ZBR_ReadSessionHistory", [1 if stop else 0]) + return {"history_text": text, "capture_stopped": stop} - refnum = _session_history_capture_refnum - stop_flag = 1 if stop else 0 - cmd = f'fprintf 0, "%s", CaptureHistory({refnum:.0f}, {stop_flag})' - errorCode, errorMsg, history, results = _execute2(cmd) - if errorCode != 0: - raise RuntimeError( - f"Could not read history capture (error code {errorCode}): " - f"{errorMsg or '(no error message)'}" - ) - if stop: - _session_history_capture_refnum = None +@mcp.tool() +def get_wave(wave_path: str) -> dict: + """Return an existing Igor wave's data and metadata. + + wave_path should be an absolute Igor path, e.g. "root:testWave" or + "root:myFolder:testWave". + + Unlike the old COM-based version of this tool (limited to 1D, real-valued waves, + read one point at a time via GetNumericWavePointValue), this transport's native + wave serialization supports any dimensionality (up to 4D), real and complex + numeric waves, text waves, and wave-reference waves (waves of waves) -- the entire + wave comes back from ONE CallFunction round trip. See the module docstring's wave + serialization notes for the JSON format this is decoded from. - return {"history_text": results, "capture_stopped": stop} + Returns a dict with "wave_path", "type" (e.g. "NT_FP64", "TEXT_WAVE_TYPE", + "WAVE_TYPE"), "dim_size" (1 to 4 numbers), "data" (nested Python lists matching + the wave's own dimensionality -- data[row][col]... for multi-dimensional waves, + or a single flat list for 1D), "unit", "note", and "dimension" (delta/offset/ + label/unit per dimension, if set). + Raises if wave_path does not refer to an existing wave. + """ + wave = call_function("ZBR#ZBR_GetWaveGeneric", [wave_path]) + if wave is None: + raise RuntimeError(f"Wave not found: {wave_path}") + return {"wave_path": wave_path, **wave} -# --- Igor runtime error model (how errors surface through Execute2) ----------------- -# -# Confirmed empirically this session, against a live Igor Pro instance, by -# instrumenting test functions with checkpoints (a global string variable, since -# GetRTError does not expose "where in the call did this happen", only "what/whether"): -# -# - With the Debugger disabled (the state required for unattended use -- see -# "Debugger control" below), an unhandled runtime error (e.g. indexing a wave -# reference that doesn't exist, or Make with invalid parameters) does NOT stop -# execution. It sets Igor's internal runtime-error flag (readable via GetRTError(0) -# without clearing it) and execution continues completely normally -- every -# subsequent line in the function runs, including any side effects (prints, wave -# writes, global variable assignments), all the way to the function's natural end, -# unless something explicitly checks the flag. -# - AbortOnRTE is that explicit check: placed after a command that might set the -# flag, it raises an Igor abort if the flag is set, which unwinds the *entire* -# current function immediately (nothing after it in that function runs, not even -# the rest of the function) and propagates to the nearest enclosing -# try-catch-endtry, exactly like a normal exception -- confirmed with a runtime -# error inside a *called* function: the abort skipped the rest of that function -# entirely and was caught by a try/catch in the *caller*. Nested try-catch-endtry -# behaves as expected too: an inner catch fully absorbs an abort (the outer catch -# never triggers), and a bare `Abort` (no arguments) re-raised from inside a catch -# unwinds past that catch's own endtry to the next enclosing catch, still carrying -# the original pending error code if it was only peeked (GetRTError(0)) and not -# cleared (GetRTError(1)) beforehand. -# - If nothing ever checks the flag (no AbortOnRTE, no try-catch), execution reaches -# the top-level command boundary -- i.e. this bridge's Execute2 call -- with the -# flag still set. Igor's command-line evaluator checks for this at that boundary -# and reports it as the Execute2 call's own failure (pIgorErrorCode/errorMsg), -# confirmed to carry the *original* error code and message, not a generic one. -# This boundary check also clears the flag afterward -- confirmed by checking -# GetRTError(0) in a completely separate subsequent call and seeing 0 -- so a -# failure here never contaminates the next command. -# - The flag is "sticky": if two *different* unhandled runtime errors occur in -# sequence with nothing checking/clearing in between, GetRTError keeps reporting -# only the *first* one throughout -- confirmed by triggering two distinct errors -# (a null-wave read, then an invalid Make) and seeing the reported code/message -# stay fixed at the first error the whole time, including in the final Execute2 -# result. This matches Igor's own documented caveat that GetErrMessage can be -# "incomplete" when multiple errors occur. -# -# Net effect for this bridge: a successful (errorCode 0) unattended call is a -# reliable clean signal -- the boundary check guarantees no lingering error. A -# failed call reliably reports the *first* unhandled runtime error's code and -# message, but does NOT mean execution stopped there -- everything before and after -# it in the procedure code likely still ran to completion -- and does NOT mean it -# was the *only* problem, since any later distinct error would be silently masked -# by the same stuck flag. Because of this, _format_execute2_error below includes -# whatever `results` (fprintf output) was captured, not just the error itself -- -# that's often the only way to tell how far execution actually got. - - -def _format_execute2_error( - command: str, errorCode: int, errorMsg: str, results: str, history: str = "" -) -> str: - """Build the message for a RuntimeError raised after a failed Execute2 call, - including any partial `results` (fprintf output) and/or `history` captured - before/around the error -- see the runtime error model notes above for why that - matters: the procedure code very likely kept running after the error, so there - may be diagnostic output that would otherwise be silently discarded. - - DIAGNOSTIC NOTE (temporary, being verified live): confirmed empirically that - Igor's Execute2 returns an EMPTY `results` string whenever pIgorErrorCode is - nonzero, even when an fprintf 0, ... earlier in the same command definitely ran - (confirmed via a separate global-variable checkpoint) -- so `results` alone does - NOT recover anything in the common case of a single top-level command/function - call failing. Including `history` here as well to check whether it fares better.""" - parts = [ - f"Igor command failed (error code {errorCode}): {errorMsg or '(no error message)'}" - ] - if results: - parts.append(f"Partial results captured before/around the error: {results!r}") - if history: - parts.append(f"History captured for this call: {history!r}") - parts.append(f"Command was: {command}") - return "\n".join(parts) + +# --- Compilation state ----------------------------------------------------------------- @mcp.tool() -def execute_igor_command(command: str) -> dict: - """Execute a single Igor Pro command string in the running Igor instance. +def check_compilation_state() -> dict: + """Check whether Igor Pro's procedure code is currently compiled or uncompiled. + + Functions from procedure code can only be called while compiled. Igor Pro enters + the uncompiled state when procedure code is edited inside Igor (only possible + while nothing is running), or when nothing is running and an included procedure + file changed on disk. Use reload_and_compile_procedures to get back to compiled + after editing a .ipf file on disk. - To get data back (not just run a command for its side effect), include an - `fprintf 0, "..."` call in `command` -- its output is captured and returned. + Note: since ZBR (the independent module this bridge's Igor-side code lives in) + compiles separately from ProcGlobal/regular MIES code, a "true" result here + specifically means ProcGlobal's compile state -- confirmed reachable via ZBR at + all already implies ZBR itself is compiled (otherwise this call would have failed + with IgorZmqUnreachable/IgorZmqError instead of returning a result). + """ + compiled = call_function("ZBR#ZBR_IsCompiled") + return {"compiled": bool(compiled)} - Example: execute_igor_command('WaveStats/Q jack; fprintf 0, "%g", V_avg') - **Caution:** if `command` calls user-defined procedure code (e.g. a MIES or test - function) and Igor Pro's Debugger is currently enabled, a breakpoint/runtime - error/abort/stale-reference pause in that code will hang this call indefinitely -- - there is no scriptable way to resume or dismiss the Debugger window (see - set_debugger_enabled's docstring). This happened for real during development. - Whenever nobody is watching who could close that popup manually, use - execute_igor_command_unattended instead, which disables the Debugger for the - duration of the call automatically. Only use this plain version when you - deliberately want the Debugger available (e.g. interactively testing a - breakpoint). +_COMPILE_POLL_INTERVAL_SECONDS = 0.5 +_COMPILE_POLL_TIMEOUT_SECONDS = 15.0 - Returns a dict with: - - "results": the fprintf output captured, if any (empty string otherwise). - - "history": any text `command` sent to Igor's history area during this - specific call -- confirmed from Automation Server.ihf: "history [output] is - a Basic string. On output it contains any text sent to Igor's history area - by the commands." This is exactly how to verify a `print` statement inside - `command` actually ran, without needing a human to look at Igor's screen or - calling the separate read_session_history tool. (Note: the command itself - is also normally echoed into history unless Silent 2 is in effect, so this - may include more than just explicit `print` output.) - - **On failure:** a nonzero error code means at least one unhandled runtime error - occurred somewhere in `command` -- it does NOT mean execution stopped there, and - it does NOT mean it was the only problem (see the runtime error model notes - above `_format_execute2_error`). The raised error includes any partial `results` - and `history` captured, since that's often the only way to tell how far - execution actually got. - """ - errorCode, errorMsg, history, results = _execute2(command) - if errorCode != 0: - raise RuntimeError( - _format_execute2_error(command, errorCode, errorMsg, results, history) - ) - return {"results": results, "history": history} + +def _read_compile_counter(): + """ZBR_ReadCompileCounter(), or None if the call itself fails (e.g. Igor is + mid-recompile and briefly unreachable) -- treated as "unknown", never fatal, same + as the counter's own -1-means-unavailable convention Igor-side.""" + try: + value = call_function("ZBR#ZBR_ReadCompileCounter") + except (IgorZmqError, IgorZmqUnreachable): + return None + return None if value is None or value < 0 else value @mcp.tool() -def get_wave(wave_path: str) -> list: - """Return the data of an existing 1D Igor wave as a list of numbers or strings. +def reload_and_compile_procedures() -> dict: + """Force Igor Pro to reload procedure code from the .ipf files on disk and attempt + a fresh compilation, then report whether it ended up compiled. - wave_path should be an absolute Igor path, e.g. "root:testWave" or - "root:myFolder:testWave". + Use this after editing a .ipf file directly on disk. Only call this while Igor Pro + is not currently running other procedure code. - Limitation: only 1D, real (non-complex) waves are supported. Multi-dimensional or - complex waves will raise an error. + **Caution, carried over from the COM-based version and confirmed to still apply**: + Igor Pro has been observed becoming unreachable shortly after a reload/compile + attempt on more than one occasion during this bridge's development (crashed or + was closed), root cause unconfirmed. If a tool call after this one starts failing, + check_bridge_health() and be prepared for Igor Pro to need relaunching. - Every COM call below is individually reconnect-protected (DataFolder/Wave/ - GetDimensions as one unit via _run_with_reconnect, then each point read - separately) rather than only wrapping the function as a whole. The point-level - wrapping matters for large waves specifically: if the connection drops on point - 4000 of 5000, this resumes from point 4000 after reconnecting instead of - re-fetching the wave and re-reading points 0-3999 again. + Mechanism: calls ZBR_SubmitReloadAndCompile(), which queues + `Execute/P "RELOAD CHANGED PROCS "` then `Execute/P "COMPILEPROCEDURES "` + Igor-side (both commands need their queue, and their own mandatory trailing + space -- see that function's docstring in ZMQ_BridgeHelpers.ipf), then polls for + completion using two independent signals, same as the COM-based version did: + + 1. root:gClaudeHelperCompileCounter (ZBR_ReadCompileCounter), bumped by + AfterCompiledHook every time Igor confirms a successful compile -- race-free: + any increase over the baseline read before submitting is trusted immediately. + 2. ZBR_IsCompiled() (the FunctionInfo-based check), as a fallback. + + Poll errors (IgorZmqError/IgorZmqUnreachable) during either check are treated as + "not ready yet" rather than fatal, since Igor Pro can be briefly unreachable while + genuinely mid-recompile. + + If this returns "compiled": False, check Igor's history/procedure window directly + -- if Igor Pro was launched with /UNATTENDED, a genuine compile error shows up as + a plain "::: error: " line there (readable via + read_session_history), rather than a modal dialog. If NOT launched /UNATTENDED, a + stuck "Function Compilation Error" dialog is also possible -- see + dismiss_compile_error_dialog. """ + baseline_counter = _read_compile_counter() - def get_dims(): - return _get_wave_ref(wave_path).GetDimensions() + call_function("ZBR#ZBR_SubmitReloadAndCompile") - dataType, numRows, numCols, numLayers, numChunks = _run_with_reconnect(get_dims) + deadline = time.monotonic() + _COMPILE_POLL_TIMEOUT_SECONDS + attempts = 0 + while True: + attempts += 1 - if numCols or numLayers or numChunks: - raise RuntimeError( - f"{wave_path} is not 1D (dims: rows={numRows}, cols={numCols}, " - f"layers={numLayers}, chunks={numChunks}) -- only 1D waves are supported." - ) - if dataType & IP_DATATYPE_COMPLEX_FLAG: - raise RuntimeError(f"{wave_path} is complex-valued -- not supported yet.") + counter = _read_compile_counter() + if ( + baseline_counter is not None + and counter is not None + and counter > baseline_counter + ): + return { + "compiled": True, + "poll_attempts": attempts, + "confirmed_via": "AfterCompiledHook counter (ZBR_ReadCompileCounter)", + } - is_text = dataType == IP_DATATYPE_TEXT - wave = _run_with_reconnect(lambda: _get_wave_ref(wave_path)) - values = [] - for i in range(numRows): try: - values.append(_read_wave_point(wave, i, is_text)) - except pywintypes.com_error: - _get_igor(force_reconnect=True) - wave = _get_wave_ref(wave_path) - values.append(_read_wave_point(wave, i, is_text)) - return values + if call_function("ZBR#ZBR_IsCompiled"): + return { + "compiled": True, + "poll_attempts": attempts, + "confirmed_via": "ZBR_IsCompiled", + } + except (IgorZmqError, IgorZmqUnreachable): + pass # treat as "not ready yet", same as a False result + if time.monotonic() >= deadline: + break + time.sleep(_COMPILE_POLL_INTERVAL_SECONDS) -@mcp.tool() -def load_experiment(file_path: str) -> dict: - """Load an Igor Pro experiment file (.pxp) into the running instance, replacing - whatever experiment is currently open -- equivalent to Igor's File -> Open - Experiment menu command. - - Confirmed from Automation Server.ihf: LoadExperiment(flags, loadType, - symbolicPathName, filePath) exists ONLY as a COM Automation method, not as part - of Igor's own procedure/macro language -- unlike execute_igor_command's Execute2 - path, this cannot be run as a command string at all. Confirmed by checking Igor - Reference.ihf (Igor's full operations/functions reference): neither - "LoadExperiment" nor "OpenFile" appear there anywhere; both exist exclusively in - Automation Server.ihf. So this bridge calls the COM method directly instead of - going through _execute2, the same way get_wave calls DataFolder/Wave directly. - - Uses loadType=ipLoadTypeOpen (2): "Does a normal experiment open, like Igor's - File->Open Experiment menu command." Per the docs, this does **not** ask to save - changes to whatever experiment is currently open first: "LoadExperiment does not - ask if you want to save changes to the previous current experiment. If you do - want to save changes, call the SaveExperiment method before calling the - LoadExperiment method." Call execute_igor_command('SaveExperiment') first if the - currently-open experiment's changes matter. - - Disables Igor's Debugger for the duration of the call and restores it - afterward, the same way execute_igor_command_unattended does -- an experiment's - recreation procedures and startup hooks (e.g. MIES's IgorStartOrNewHook) run as - part of loading it, and this call bypasses _execute2 entirely so it would not - otherwise get that protection. - - Loading a different experiment can change everything about the live environment - (included procedure files, XOPs, data folders, Debugger settings persist but - everything else may not) -- call get_environment_summary() afterward to see the - new state. - - Raises if file_path does not point to an existing file. - """ - normalized = os.path.abspath(file_path) - if not os.path.isfile(normalized): - raise RuntimeError(f"'{normalized}' does not exist or is not a file.") + dismiss_result = _attempt_dismiss_compile_error_dialog() + return { + "compiled": False, + "poll_attempts": attempts, + "auto_dismiss_attempted": dismiss_result, + "note": ( + f"Still not compiled after polling for {_COMPILE_POLL_TIMEOUT_SECONDS:.0f}s. " + "This is more likely a genuine compile error than a timing artifact -- " + "check Igor's history/procedure window directly (or call " + "read_session_history() if launched with /UNATTENDED, see the " + "'::: error: ...' line reported there), or check for a " + "stuck compile-error dialog (see 'auto_dismiss_attempted' above)." + ), + } - saved = _read_debugger_options() - _apply_debugger_options( - { - "enable": False, - "debug_on_error": False, - "debug_on_abort": False, - "nvar_svar_wave_checking": False, - } - ) - try: - def work(): - igor = _get_igor() - igor.LoadExperiment(0, IP_LOAD_TYPE_OPEN, "", normalized) +# --- Debugger control -------------------------------------------------------------- +# +# Unchanged in spirit from the COM-based version -- see that version's extensive +# comment block (still true) for why the Debugger MUST be disabled for any +# unattended/automated session: there is no scriptable way to resume, step, or +# dismiss the Debugger window once something pauses it, and a paused call hangs +# forever. DebuggerOptions is NOT subject to the Execute-only restriction (confirmed +# live), so ZBR_GetDebuggerState/ZBR_SetDebuggerEnabled/ZBR_RestoreDebuggerSettings +# are all single, synchronous CallFunction calls -- no submit/poll needed. - _run_with_reconnect(work) - finally: - _apply_debugger_options(saved) +_saved_debugger_settings = None - return {"loaded_file": normalized} +def _decode_debugger_state(multi) -> dict: + enable, debug_on_error, debug_on_abort, nvar_checking = multi + return { + "enable": bool(enable), + "debug_on_error": bool(debug_on_error), + "debug_on_abort": bool(debug_on_abort), + "nvar_svar_wave_checking": bool(nvar_checking), + } -# This bridge's own version, kept in sync with manifest.json's "version" field on every -# release -- not read from manifest.json at runtime because the on-disk layout after -# Claude Desktop installs a .mcpb extension is not guaranteed to keep server.py and -# manifest.json at a fixed relative path to each other; a hardcoded constant avoids that -# assumption entirely. Added specifically because a prior session had no way to confirm -# from inside a conversation which .mcpb build was actually loaded/active in Claude -# Desktop, which made it impossible to verify whether a given fix (e.g. the reload/compile -# timing relaxation) was actually in effect during a test -- see SESSION_NOTES.md. -_BRIDGE_VERSION = "1.27.0" +@mcp.tool() +def get_debugger_state() -> dict: + """Read Igor Pro's current Debugger settings (enable, debugOnError, debugOnAbort, + NVAR_SVAR_WAVE_Checking) without changing them, and save a snapshot inside this + bridge process for restore_debugger_settings to restore later. -def _installed_package_version(distribution_name: str) -> str | None: - """Return the installed version of a distribution (e.g. "mcp", "pywin32"), or None - if it isn't installed/resolvable. Best-effort only -- wrapped in - get_bridge_version() to help diagnose *which* Python environment this process is - actually running in, not to be relied on for anything else. + **Call this before starting any unattended/automated session**, immediately + before calling set_debugger_enabled(False). """ - try: - return importlib.metadata.version(distribution_name) - except importlib.metadata.PackageNotFoundError: - return None + global _saved_debugger_settings + state = _decode_debugger_state(call_function("ZBR#ZBR_GetDebuggerState")) + _saved_debugger_settings = dict(state) + return state @mcp.tool() -def get_bridge_version() -> dict: - """Return the version of this Igor Pro Bridge build that is actually running right - now, in this Claude Desktop session, plus which Python interpreter and package - versions it's actually running with. +def set_debugger_enabled( + enabled: bool, + debug_on_error: bool = None, + debug_on_abort: bool = None, + nvar_svar_wave_checking: bool = None, +) -> dict: + """Turn Igor Pro's Debugger on or off (and optionally its debugOnError/ + debugOnAbort/NVAR_SVAR_WAVE_Checking sub-settings). - Call this whenever it matters to confirm which build is active -- e.g. before - relying on a specific fix or behavior change from a recent version, or when - reporting results from a test that depends on a particular fix being in effect. - There is no other way to determine this from inside a conversation: installing a - newer .mcpb requires restarting Claude Desktop, and nothing else surfaces which - version ended up actually loaded afterward. - - The "python_executable" field is also the authoritative answer to a separate, - easy-to-get-wrong question: *which* Python environment Claude Desktop actually - launched this process with. Claude Desktop's manifest.json only specifies the bare - command "python", resolved via whatever PATH Claude Desktop's own process - environment has at launch time (regardless of whether that process happens to be - elevated) -- which is not guaranteed to match the Python an interactive console - session resolves (e.g. a PowerShell profile - activating a conda environment, or a per-user Microsoft Store "app execution - alias" stub that behaves differently once elevated). install.ps1 makes its own - best-effort guess at install time; after installing and restarting Claude Desktop, - call this tool to confirm "python_executable" actually matches what install.ps1 - installed into -- if it doesn't, re-run install.ps1 with an explicit -PythonPath - pointing at the path reported here. - """ - return { - "version": _BRIDGE_VERSION, - "python_executable": sys.executable, - "python_version": sys.version.split()[0], - "mcp_package_version": _installed_package_version("mcp"), - "pywin32_build": _installed_package_version("pywin32"), - } + **For any unattended/automated session, the debugger MUST be disabled: call + set_debugger_enabled(False) before starting.** See get_debugger_state's docstring + and the module-level Debugger-control comment above for why. + enabled=False clears all four settings regardless of the other arguments -- this + is Igor's own documented DebuggerOptions behavior, not a limitation of this + function -- so the sub-flags are only applied when enabled=True. Any sub-flag left + as None (the default) falls back to Igor's CURRENT setting for that flag rather + than being forced off. -@mcp.tool() -def check_bridge_health() -> dict: - """Check whether the Igor Pro bridge is actually able to reach Igor Pro right now, - and report exactly which requirement is unmet if not. - - Call this first whenever a command fails or behaves unexpectedly. This session's - own debugging hit three distinct failure modes that all needed different fixes: - (1) this Python process and Igor Pro running at mismatched privilege levels (one - elevated, one not), (2) no Igor Pro COM object registered at all (Igor not - running), and (3) a registered-but-dead COM object (Igor crashed/was - force-closed, leaving a stale registration that reconnecting alone can't fix -- - Igor itself needs relaunching). This check distinguishes all three rather than - surfacing one generic failure. - - Note on (1): elevation itself is not the requirement -- empirically confirmed - (both this bridge process and Igor Pro running non-elevated, as an ordinary - user, via a standalone win32com.client.GetActiveObject test) that COM attaches - fine as long as client and server share the same privilege level. This check - therefore always attempts the real COM call rather than pre-emptively failing - based on this process's own elevation state -- a mismatch, if present, shows up - as the COM/RPC-transport error below. - - Returns a dict with at least a "status" key ("OK" or "FAIL") and, on FAIL, a - "problem" key with a specific, actionable description. + Recommended pattern around an unattended session: + get_debugger_state() # read + save the current settings + set_debugger_enabled(False) # disable for the unattended run + ... run the unattended session ... + restore_debugger_settings() # put the saved settings back """ - report = {"python_process_elevated": _is_current_process_elevated()} - - try: - _get_igor() - except RuntimeError as e: - report["status"] = "FAIL" - report["problem"] = ( - f"No running Igor Pro instance found via COM ({e}). Make sure Igor Pro " - "9.00 or later is open, and that it and this Python process are running " - "at the same privilege level (both elevated, or both not) -- a mismatch " - "is the most common cause of this failure, not elevation itself." + current = _decode_debugger_state(call_function("ZBR#ZBR_GetDebuggerState")) + if not enabled: + call_function("ZBR#ZBR_SetDebuggerEnabled", [0]) + else: + call_function( + "ZBR#ZBR_RestoreDebuggerSettings", + [ + 1, + int(current["debug_on_error"] if debug_on_error is None else debug_on_error), + int(current["debug_on_abort"] if debug_on_abort is None else debug_on_abort), + int( + current["nvar_svar_wave_checking"] + if nvar_svar_wave_checking is None + else nvar_svar_wave_checking + ), + ], ) - return report - - def try_call(): - igor = _get_igor() - return igor.Execute2(0, 0, 'fprintf 0, "%s", IgorInfo(1)') - - try: - errorCode, errorMsg, history, results = try_call() - report["reconnect_was_needed"] = False - except pywintypes.com_error: - report["reconnect_was_needed"] = True - try: - _get_igor(force_reconnect=True) - errorCode, errorMsg, history, results = try_call() - except pywintypes.com_error as e2: - report["status"] = "FAIL" - report["problem"] = ( - f"Found a registered Igor Pro COM object, but calls to it fail with a " - f"COM/RPC-transport error even after reconnecting ({e2}). This usually " - "means a stale/dead COM registration (Igor Pro crashed or was " - "force-closed previously -- check Task Manager for Igor64.exe, there " - "should be exactly one, fully close it, and relaunch Igor Pro fresh), " - "or a privilege-level mismatch between this process and Igor Pro (both " - "must be elevated, or both not -- elevation itself is not required)." - ) - return report - - if errorCode != 0: - report["status"] = "FAIL" - report["problem"] = f"Igor-level command failed (code {errorCode}): {errorMsg}" - return report - - report["status"] = "OK" - report["igor_info"] = results - return report - - -# Confirmed against a live Igor Pro instance during development: this is exactly the -# method used by IsProcGlobalCompiled() in -# Packages/igortest/procedures/igortest-test-compilation.ipf. FunctionInfo() for a -# deliberately non-existing function returns an empty string when procedure code is -# compiled, and a non-empty string (observed: "Procedures Not Compiled") when it is -# not. The expression is inlined directly into the fprintf call (no intermediate -# variable) deliberately: an earlier version assigned to a local first, but Igor's -# command line persists local variables across separate command-line invocations, so -# a *second* call declaring the same variable name again failed with "the name -# already exists as a variable" -- confirmed empirically. Inlining the expression -# sidesteps that entirely, since there is no variable to persist or collide with. -_PROCEDURES_COMPILED_CHECK_CMD = ( - 'fprintf 0, "%s", FunctionInfo("ProcGlobal#NON_EXISTING_FUNCTION")' -) + return _decode_debugger_state(call_function("ZBR#ZBR_GetDebuggerState")) @mcp.tool() -def check_compilation_state() -> dict: - """Check whether Igor Pro's procedure code is currently compiled or uncompiled. +def restore_debugger_settings() -> dict: + """Restore Igor Pro's Debugger settings to whatever get_debugger_state last + captured. - Functions from procedure code can only be called while compiled. Igor Pro enters - the uncompiled state when procedure code is edited inside Igor (only possible - while nothing is running), or when nothing is running and an included procedure - file changed on disk. Use reload_and_compile_procedures to get back to compiled - after editing a .ipf file on disk. + **Call this when an unattended/automated session ends.** + + Raises if get_debugger_state was never called in this bridge process. """ - errorCode, errorMsg, history, results = _execute2(_PROCEDURES_COMPILED_CHECK_CMD) - if errorCode != 0: + if _saved_debugger_settings is None: raise RuntimeError( - f"Could not check compilation state (error code {errorCode}): {errorMsg}" + "No saved Debugger settings to restore -- call get_debugger_state() " + "before starting the unattended session so there is something to " + "restore afterward." ) - return {"compiled": results == "", "raw_function_info": results} - - -_COMPILE_POLL_INTERVAL_SECONDS = 0.5 -_COMPILE_POLL_TIMEOUT_SECONDS = 5.0 -# Pause between issuing "RELOAD CHANGED PROCS " and "COMPILEPROCEDURES ", and after -# issuing "COMPILEPROCEDURES ", both queued via Execute/P (see reload_and_compile_procedures). -# Added to relax the timing between these two operation-queue commands after Igor Pro -# crashes were observed around reload/compile activity this session (see SESSION_NOTES.md) -- -# not a confirmed root-cause fix, just a precaution to reduce how tightly these are packed. -_RELOAD_TO_COMPILE_PAUSE_SECONDS = 2.0 -_POST_COMPILE_PAUSE_SECONDS = 1.0 -# Number of consecutive "compiled" reads required before trusting the FunctionInfo-based -# fallback signal -- see the false-positive race explained in -# reload_and_compile_procedures's docstring. Not needed for the AfterCompiledHook-based -# counter signal, which is race-free by construction (see _read_claude_helper_compile_counter). -_COMPILE_CONFIRM_CHECKS = 2 - -# MIES_ClaudeHelper.ipf's AfterCompiledHook (gated behind #ifdef IGOR_PRO_BRIDGE -- see -# SESSION_NOTES.md) increments root:gClaudeHelperCompileCounter every time Igor calls it, -# which only happens once ALL procedure windows have genuinely compiled successfully -# (confirmed from Igor Pro Folder/Igor Help Files/Advanced Topics.ihf). Unlike the -# FunctionInfo-based poll below, there is no staleness/race concern reading this: the -# counter only ever changes at the exact moment Igor itself confirms a successful -# compile, so any observed increase over a baseline is unconditionally trustworthy, no -# repeated-confirmation dance required. NumVarOrDefault's own -1 default is used as the -# "unavailable" sentinel (a real counter value can never be negative), which handles two -# unavailability cases identically: IGOR_PRO_BRIDGE not defined for this experiment (the -# hook doesn't exist at all), or MIES_ClaudeHelper.ipf not included in the first place -- -# this bridge has to keep working either way, so the counter is only ever an optional -# extra confirmation, never a requirement. -_CLAUDE_HELPER_COMPILE_COUNTER_CMD = ( - 'fprintf 0, "%g", NumVarOrDefault("root:gClaudeHelperCompileCounter", -1)' -) - - -def _read_claude_helper_compile_counter(): - """Read root:gClaudeHelperCompileCounter, or None if it's unavailable for any reason - (COM/Igor-level error, or the sentinel -1 meaning the variable doesn't exist -- see - the constant's comment above for why both are treated as simply "unavailable", never - fatal).""" - errorCode, errorMsg, history, results = _execute2( - _CLAUDE_HELPER_COMPILE_COUNTER_CMD + s = _saved_debugger_settings + call_function( + "ZBR#ZBR_RestoreDebuggerSettings", + [ + int(s["enable"]), + int(s["debug_on_error"]), + int(s["debug_on_abort"]), + int(s["nvar_svar_wave_checking"]), + ], ) - if errorCode != 0: - return None - try: - value = float(results) - except (TypeError, ValueError): - return None - return None if value < 0 else value - - -_COMPILE_ERROR_DIALOG_NOTE = ( - "One confirmed cause if this is unexpected (e.g. you just fixed a known syntax " - "error and expected this to succeed): a compile-error dialog left open in Igor " - "from a PREVIOUS failed attempt blocks Igor's operation queue from ever draining " - "-- confirmed from Igor Pro Folder/Igor Help Files/Advanced Topics.ihf, " - "'Operation Queue' section: 'Igor services the operation queue when no " - "procedures are running and the command line is empty.' A modal dialog means " - "Igor is never idle, so RELOAD CHANGED PROCS/COMPILEPROCEDURES queued by a later " - "call sit there without ever actually running, even though this bridge's own COM " - "calls keep responding normally throughout (confirmed empirically: this hang " - "does not show up as a hung tool call, only as 'compiled' staying stuck at False " - "no matter how many times this is retried). " - "reload_and_compile_procedures already attempts an automatic fix for exactly this " - "case (posting an Escape key press directly to Igor's dialog window, via " - "dismiss_compile_error_dialog's underlying logic, without needing OS focus/" - "foreground) before returning this note -- see the 'auto_dismiss_attempted' " - "field for what that attempt found and did. " - "ACTION FOR WHATEVER IS CALLING THIS TOOL: if the automatic attempt did not " - "resolve it (or was not attempted, e.g. because no matching dialog window was " - "found), do not just log this and retry silently -- explicitly ask " - "the human operator right now whether a compile-error dialog is showing in Igor " - "Pro, and if so, to close it, before retrying. Explicitly prompting the human is " - "what actually keeps an unattended/agent-driven workflow moving when the " - "automatic attempt isn't enough." -) + return _decode_debugger_state(call_function("ZBR#ZBR_GetDebuggerState")) -# --- Compile-error dialog dismissal (posted Escape key message) --------------------- -# -# Added after a user-proposed mitigation for the compile-error-dialog problem -# documented above: there is still no documented COM operation to detect or dismiss -# that dialog, but Escape closes it, and a simulated key press can be delivered to -# it directly. -# -# **Confirmed live against real Igor Pro instances -- both Igor Pro 10.03 and Igor -# Pro 9.06 (this is no longer a guess for either)**: the original assumption that -# this dialog is an ordinary "#32770" Win32 dialog was WRONG -- Igor Pro's UI (both -# major versions tested) is Qt-based, and the compile-error dialog is a Qt window -# with a version-hash-looking class name (observed on 10.03: "Qt693QWindowIcon"; -# not re-checked on 9.06 since title matching alone was already sufficient there). -# Since that class name likely varies across Igor/Qt builds and isn't a stable -# thing to match on, this instead matches on the dialog's window TITLE, which was -# directly observed to be exactly "Function Compilation Error" on BOTH Igor Pro -# 10.03 and 9.06 -- a stable, Igor-chosen string, not a toolkit implementation -# detail, and apparently stable across at least these two major versions. -# -# An earlier version also OR'd in a blanket "#32770" (the standard native Windows -# dialog class) check, on the theory that it was "harmless" and would cover some -# other genuinely native Igor-raised dialog. A Copilot PR review correctly flagged -# this as a real risk instead: since this is called automatically from -# reload_and_compile_procedures, matching ANY "#32770" window regardless of title -# could Escape-dismiss an unrelated native dialog (e.g. a save-changes -# confirmation), causing data loss or unexpected state changes -- and it was never -# actually needed, since the real compile-error dialog isn't "#32770" on either -# version tested. Removed; title matching alone is both sufficient and safer. -# -# PostMessage(hwnd, WM_KEYDOWN/WM_KEYUP, VK_ESCAPE, ...) is used rather than a -# hardware-level input simulation so this never needs to steal OS focus/ -# foreground from whatever the user is doing. **Confirmed live against a real -# stuck "Function Compilation Error" dialog on BOTH Igor Pro 10.03 and Igor Pro -# 9.06: a POSTED (not real hardware) WM_KEYDOWN/WM_KEYUP for VK_ESCAPE -# successfully closed it in both cases** -- Qt's Windows platform plugin -# intercepts native window messages in its own WndProc regardless of a message's -# origin, so it reacted the same way a real key press would, with no -# foreground/focus change needed. (If a future Igor/Qt version doesn't react the -# same way, the fallback would be a hardware-level simulation -- -# SetForegroundWindow + keybd_event/SendInput -- targeted at this same window, at -# the cost of stealing focus.) -# -# Targeting no longer relies on the OS foreground window at all (the very first, -# now-superseded approach): it enumerates all top-level windows and keeps visible -# ones belonging to an Igor Pro process (exe name starting with "igor") whose title -# matches a known stuck-dialog title (see _KNOWN_STUCK_DIALOG_TITLES). If it never -# matches, dismissal safely reports "not found" (see "igor_windows_seen" in that -# result for exactly what windows exist, to extend this list further if a new -# stuck-dialog title shows up). +# --- Environment summary ----------------------------------------------------------- # -# Trade-off, confirmed to be acceptable by the user who proposed this mitigation: -# this recovers the ability to continue working, but does NOT recover the actual -# compile-error message -- Escape just closes the dialog, it doesn't read it. If the -# exact error text matters, check Igor's procedure window/history directly (or ask a -# human to read the dialog) before this or reload_and_compile_procedures's automatic -# call to it dismisses it. - -_IGOR_PROCESS_NAME_PREFIX = "igor" -# Known titles of Igor Pro popups that block the operation queue and are safe to -# dismiss with Escape. Confirmed live against both Igor Pro 10.03 and Igor Pro -# 9.06: the compile-error dialog is titled exactly "Function Compilation Error" -# and is a Qt window, NOT a native "#32770" dialog -- so title matching is the -# only signal used, and it appears stable across major versions. -_KNOWN_STUCK_DIALOG_TITLES = ("Function Compilation Error",) -_POSTED_KEY_GAP_SECONDS = 0.05 -_POSTED_KEY_SETTLE_SECONDS = 0.2 - +# Composed client-side from the small, generic ZBR_IgorInfo/ZBR_WinList/ +# ZBR_ProcedureText/ZBR_DataFolderDir wrappers in ZMQ_BridgeHelpers.ipf -- exactly +# mirroring how the COM-based version worked (it also just ran fprintf-wrapped +# built-in calls and parsed/structured the raw string results in Python). See that +# file's own comments for the confirmed IgorInfo() index meanings and the +# ProcedureText("", 0, "Procedure") argument-order gotcha. -def _is_stuck_dialog_window(class_name: str, title: str) -> bool: - """True if a window's title matches one of the known stuck-dialog cases this - bridge knows how to dismiss (see _KNOWN_STUCK_DIALOG_TITLES). - - Deliberately title-only. An earlier version also treated ANY window with the - generic native Windows dialog class ("#32770") as safe to dismiss regardless of - title -- flagged by a Copilot PR review as a real risk: since - dismiss_compile_error_dialog is called automatically from - reload_and_compile_procedures, that blanket rule could have Escape-dismissed an - unrelated native dialog (e.g. a save-changes confirmation), causing data loss or - unexpected state changes. It also never bought anything in practice: the actual - compile-error dialog confirmed live on both Igor Pro 10.03 and 9.06 is a Qt - window, not a "#32770" dialog at all, so the class-only branch could only ever - match something else. class_name is accepted as a parameter for signature - stability / potential future use, but is currently unused. - """ - return any(known.lower() in title.lower() for known in _KNOWN_STUCK_DIALOG_TITLES) +def _categorize_procedure_file(name: str) -> str: + """Bucket an included procedure file name into a coarse category, purely to make a + ~250-entry file list skimmable in a summary. Buckets reflect this specific repo's + naming conventions (MIES_*, UTF_* unit tests, igortest-* test framework, IPNWB_*), + not a general Igor Pro convention.""" + if name.startswith("igortest"): + return "igortest_framework" + if name.startswith("UTF_"): + return "unit_tests" + if name.startswith("IPNWB"): + return "ipnwb" + if name.startswith("MIES_"): + return "mies_production" + return "other" -def _get_process_exe_name(pid: int): - """Best-effort lookup of the executable file name (e.g. "Igor64.exe") owning - `pid`, or None if it can't be determined. Returns just the base file name, not - the full path, so callers can do a simple case-insensitive prefix check.""" - ACCESS = win32con.PROCESS_QUERY_INFORMATION | win32con.PROCESS_VM_READ - hProcess = None - try: - hProcess = win32api.OpenProcess(ACCESS, False, pid) - path = win32process.GetModuleFileNameEx(hProcess, 0) - return os.path.basename(path) - except Exception: - return None - finally: - if hProcess is not None: - win32api.CloseHandle(hProcess) +@mcp.tool() +def get_environment_summary() -> dict: + """Summarize the current Igor Pro instance's live environment: Igor version, the + loaded experiment, loaded external operations (XOPs), which procedure files are + actually included right now, the contents of the always-present "Procedure" + window, and the top-level global data folder layout. -def _find_igor_dialog_window(): - """Find a visible top-level window that looks like a known stuck Igor Pro - dialog (see _is_stuck_dialog_window) and is owned by an Igor Pro process, - without regard to OS foreground/focus state. - - Returns (hwnd, title, exe_name) for the first match found, or None if no such - window exists right now. EnumWindows's callback is never made to return False - (pywin32 raises a spurious error if it does -- the underlying Win32 call reports - that as a failure even though it just means "the callback asked to stop early"), - so this always enumerates every top-level window and collects all matches, then - returns the first one -- windows are typically (though not strictly guaranteed) - reported in top-to-bottom Z-order, so in the common case of a single dialog this - is simply that dialog. + Returns a dict with: + - igor_version_info / os_info: raw IgorInfo(0) / IgorInfo(3) strings + - experiment_file_name / experiment_file_kind: e.g. "Basic.pxp" / "Packed" + - loaded_xops: list of loaded external operations + - procedure_window_text: raw contents of the special "Procedure" window -- + inspect this for experiment-specific #include/#define directives + - included_procedure_file_count / included_procedure_files_by_category / + included_procedure_files: currently included .ipf files + - data_folders / top_level_waves: top-level layout under root: + - debugger_settings: current Debugger state (see set_debugger_enabled's + docstring for why this matters before any unattended session) """ - matches = [] - - def _callback(hwnd, _extra): - if win32gui.IsWindowVisible(hwnd): - title = win32gui.GetWindowText(hwnd) - class_name = win32gui.GetClassName(hwnd) - if _is_stuck_dialog_window(class_name, title): - _, pid = win32process.GetWindowThreadProcessId(hwnd) - exe_name = _get_process_exe_name(pid) - if exe_name and exe_name.lower().startswith(_IGOR_PROCESS_NAME_PREFIX): - matches.append((hwnd, title, exe_name)) - return True - - win32gui.EnumWindows(_callback, None) - return matches[0] if matches else None - - -def _list_igor_top_level_windows() -> list: - """Diagnostic helper: list EVERY visible top-level window owned by an Igor Pro - process, regardless of class -- title, class name, and exe name for each. - - Used only when _find_igor_dialog_window() finds no match, to surface what's - actually there instead of just reporting "not found" with no further - information -- this is exactly how the compile-error dialog's real title - ("Function Compilation Error") and class ("Qt693QWindowIcon", a Qt window, NOT - a native "#32770" dialog) were identified live, without needing a separate - one-off diagnostic tool. Useful again if some other stuck dialog shows up with - a title not yet in _KNOWN_STUCK_DIALOG_TITLES. - """ - windows = [] - - def _callback(hwnd, _extra): - if win32gui.IsWindowVisible(hwnd): - _, pid = win32process.GetWindowThreadProcessId(hwnd) - exe_name = _get_process_exe_name(pid) - if exe_name and exe_name.lower().startswith(_IGOR_PROCESS_NAME_PREFIX): - windows.append( - { - "title": win32gui.GetWindowText(hwnd), - "class_name": win32gui.GetClassName(hwnd), - "process": exe_name, - } - ) - return True - - win32gui.EnumWindows(_callback, None) - return windows - - -def _attempt_dismiss_compile_error_dialog() -> dict: - """Post a simulated Escape key press directly to an Igor Pro dialog window (if - one can be found), without requiring it to be focused or in the OS foreground. - Returns a dict describing what was found and whether anything was actually sent - -- see the module-level comment above this function for the reasoning, the - unverified assumptions, and the trade-offs. - """ - found = _find_igor_dialog_window() - if found is None: - return { - "attempted": False, - "reason": ( - "No visible window with a title containing one of " - f"{_KNOWN_STUCK_DIALOG_TITLES}, owned by an Igor Pro process, was " - "found. Either there is no stuck dialog right now, or it's a kind " - "not seen before -- see 'igor_windows_seen' below for every " - "visible window Igor currently owns, to identify it." - ), - "igor_windows_seen": _list_igor_top_level_windows(), - } - - hwnd, window_title, exe_name = found - - try: - win32api.PostMessage(hwnd, win32con.WM_KEYDOWN, win32con.VK_ESCAPE, 0) - time.sleep(_POSTED_KEY_GAP_SECONDS) - win32api.PostMessage(hwnd, win32con.WM_KEYUP, win32con.VK_ESCAPE, 0) - time.sleep(_POSTED_KEY_SETTLE_SECONDS) - except Exception as e: - return { - "attempted": False, - "reason": f"Posting the simulated Escape key press failed: {e}", - "dialog_window_title": window_title, - "dialog_window_process": exe_name, - } - - return { - "attempted": True, - "dialog_window_title": window_title, - "dialog_window_process": exe_name, - "note": ( - "Posted a simulated Escape key press directly to this dialog window " - "(no OS foreground/focus change was made or needed) -- confirmed live " - 'to close Igor\'s "Function Compilation Error" Qt dialog the same way ' - "a real key press would. This does NOT recover the actual " - "compile-error message -- it only closes whatever modal dialog was " - "showing. Follow up with check_compilation_state() or " - "reload_and_compile_procedures() to see whether this actually " - "un-stuck anything." - ), - } - - -@mcp.tool() -def dismiss_compile_error_dialog() -> dict: - """Attempt to close a stuck Igor Pro modal compile-error dialog by posting a - simulated Escape key press directly to it, WITHOUT recovering the actual error - message and WITHOUT requiring or changing OS focus/foreground state. - - Use this manually when you suspect Igor Pro has a compile-error dialog open (e.g. - reload_and_compile_procedures kept reporting "not compiled" even after fixing a - known syntax error) and want to try clearing it yourself, separately from - reload_and_compile_procedures's own automatic attempt at the same thing (see its - docstring -- it already calls this same logic once before giving up and asking a - human). - - Mechanism: enumerates top-level windows for a visible one, owned by a process - whose exe name starts with "igor" (e.g. Igor64.exe), whose title matches a - known stuck-dialog title. **Confirmed live against both Igor Pro 10.03 and - Igor Pro 9.06: the compile-error dialog is titled exactly "Function - Compilation Error" and is a Qt window (class observed as "Qt693QWindowIcon" on - 10.03), NOT a native "#32770" dialog** -- so title matching is what actually - finds it, on both major versions tested. (An earlier version also matched any - generic native "#32770" dialog regardless of title; removed after a Copilot PR - review correctly flagged it as a real risk -- since this is called - automatically from reload_and_compile_procedures, it could have Escape-dismissed - an unrelated native dialog, e.g. a save-changes confirmation, and it was never - actually needed since the real dialog isn't "#32770" anyway.) Once found, this - posts WM_KEYDOWN/WM_KEYUP for VK_ESCAPE directly to that window via PostMessage, - without requiring it to be focused or in the foreground. - - **Confirmed live on both Igor Pro 10.03 and Igor Pro 9.06: a POSTED (not real - hardware) Escape key event is enough to make Qt's Windows platform layer - close this dialog the same way a real key press would** -- verified against a - real stuck "Function Compilation Error" dialog on each version, with no - foreground/focus change needed. If no matching window is found at all (e.g. a - different, not-yet-seen Igor popup), this reports "attempted": false (safe - failure) along with "igor_windows_seen": every visible top-level window - currently owned by an Igor Pro process (title/class/process), so a new stuck - dialog's real title/class can be identified and added to - _KNOWN_STUCK_DIALOG_TITLES instead of guessing. - - This works despite Igor Pro's elevated status because this bridge's own process - is also required to run elevated (see the module docstring) -- Windows blocks - simulated input from a lower-privilege process reaching a higher-privilege - window (UIPI), but does not block it between two equally elevated processes. - - **Trade-off: this does not tell you what the error was.** It only clears - whatever dialog is blocking Igor's operation queue so work can continue. If the - actual error text matters, check the .ipf file directly or ask a human to read - the dialog before calling this. - """ - return _attempt_dismiss_compile_error_dialog() - - -_COMPILE_POLL_TIMEOUT_AFTER_DISMISS_SECONDS = 3.0 - - -def _poll_for_compile_confirmation(baseline_counter, timeout_seconds: float) -> dict: - """Poll for up to timeout_seconds for confirmation that Igor Pro's procedure code - compiled successfully, checking both signals described in - reload_and_compile_procedures's docstring. Returns one of: - - - {"compiled": True, "poll_attempts": N, "confirmed_via": "..."} - - {"compiled": False, "compiled_state_known": False, "poll_attempts": N, - "last_error_code": ..., "last_error_msg": ...} -- the compiled-state check - itself kept failing throughout the poll. - - {"compiled": False, "poll_attempts": N, "raw_function_info": ...} -- the - compiled-state check succeeded but never confirmed a compile within the - timeout. - - Factored out of reload_and_compile_procedures so it can be called a second time, - with a shorter timeout, after an automatic compile-error-dialog dismissal - attempt, without duplicating the polling logic. - """ - deadline = time.monotonic() + timeout_seconds - lastErrorCode = None - lastErrorMsg = None - lastResults = None - attempts = 0 - consecutive_compiled = 0 - - while True: - attempts += 1 - - current_counter = _read_claude_helper_compile_counter() - if ( - baseline_counter is not None - and current_counter is not None - and current_counter > baseline_counter - ): - return { - "compiled": True, - "poll_attempts": attempts, - "confirmed_via": "AfterCompiledHook counter (root:gClaudeHelperCompileCounter)", - } - - compiledErrorCode, compiledErrorMsg, _, compiledResults = _execute2( - _PROCEDURES_COMPILED_CHECK_CMD - ) - if compiledErrorCode == 0: - lastErrorCode = None - lastResults = compiledResults - if compiledResults == "": - consecutive_compiled += 1 - if consecutive_compiled >= _COMPILE_CONFIRM_CHECKS: - return { - "compiled": True, - "poll_attempts": attempts, - "confirmed_via": ( - "FunctionInfo poll (AfterCompiledHook counter unavailable " - "or unchanged)" - ), - } - else: - consecutive_compiled = 0 - else: - lastErrorCode, lastErrorMsg = compiledErrorCode, compiledErrorMsg - consecutive_compiled = 0 - - if time.monotonic() >= deadline: - break - time.sleep(_COMPILE_POLL_INTERVAL_SECONDS) - - if lastErrorCode is not None: - return { - "compiled": False, - "compiled_state_known": False, - "poll_attempts": attempts, - "last_error_code": lastErrorCode, - "last_error_msg": lastErrorMsg, - } - - return { - "compiled": False, - "poll_attempts": attempts, - "raw_function_info": lastResults, - } - - -@mcp.tool() -def reload_and_compile_procedures() -> dict: - """Force Igor Pro to reload procedure code from the .ipf files on disk and attempt - a fresh compilation, then report whether it ended up compiled. - - Use this after editing a .ipf file directly on disk -- the correct way to change - MIES/Igor procedure code -- to make Igor pick up the change. Only call this while - Igor Pro is not currently running other procedure code; reloading/compiling while - code is running is not supported. - - **Caution, observed twice during this bridge's development against a real Igor - Pro 10.03 instance**: Igor Pro became unreachable via COM (crashed or was - closed) shortly after a reload/compile attempt, in two separate incidents -- - once with broken procedure code present, once immediately after fixing it. No - root cause has been confirmed (no Windows crash logs were accessible from - here), and it's not established whether this bridge's own actions are involved - at all versus a pre-existing Igor Pro stability issue independent of it. If a - tool call after this one starts failing with a COM/RPC error, check - check_bridge_health() and be prepared for Igor Pro to need relaunching. - - Mirrors the exact method used by CompileAndRestart() in igortest-tracing.ipf: - - Execute/P "RELOAD CHANGED PROCS " - Execute/P "COMPILEPROCEDURES " - - Both commands go through Igor's operation queue, not immediate execution -- - confirmed from Igor Pro Folder/Igor Help Files/Advanced Topics.ihf, "Operation - Queue" section (COMPILEPROCEDURES and RELOAD CHANGED PROCS are documented there; - neither has its own entry in the main Igor Reference): "Igor services the - operation queue when no procedures are running and the command line is empty. If - the operation queue is not empty, Igor then executes the oldest command in the - queue." /P only appends to that queue -- it does NOT guarantee either command has - actually run by the time this function starts checking, only that Igor will get - to it once genuinely idle. - - Because of that, a single immediate compiled-state check was observed (against a - live Igor Pro instance) to occasionally report "compiled: true" on the very - first read even though the queue had not drained yet -- a false positive, reading - stale pre-reload state rather than the real post-compile result -- as well as the - opposite false negative (briefly still "not compiled" right after a compile that - actually succeeded). To guard against both, this checks two independent signals on - every poll (every 0.2s, up to 5s total): - - 1. root:gClaudeHelperCompileCounter, incremented by MIES_ClaudeHelper.ipf's - AfterCompiledHook (see _read_claude_helper_compile_counter) -- authoritative and - race-free when available (requires #define IGOR_PRO_BRIDGE in the experiment's - Procedure window), since it only changes at the exact moment Igor itself confirms - a successful compile. Any increase over the baseline read before issuing RELOAD/ - COMPILE is trusted immediately, no repeated confirmation needed. - 2. The original FunctionInfo-based check_compilation_state poll, kept as a fallback - for when the counter is unavailable (IGOR_PRO_BRIDGE not defined, or - MIES_ClaudeHelper.ipf not included at all) -- still requires - _COMPILE_CONFIRM_CHECKS consecutive "compiled" reads in a row before trusting it, - since this signal alone doesn't rule out the staleness race described above. - - If neither signal confirms compilation after the full 5s, that's a much stronger - signal of a genuine compile error -- check Igor's history/procedure window - directly. See _COMPILE_ERROR_DIALOG_NOTE for one confirmed, concrete cause: a - compile-error dialog left open from an earlier failed attempt blocks the - operation queue from ever draining, so this will keep reporting "not compiled" - even after the underlying .ipf file is genuinely fixed, until a person closes - that dialog by hand. This happened for real during development. - - Before giving up, if compilation isn't confirmed within the initial timeout, this - automatically makes ONE attempt to dismiss a possible stuck compile-error dialog - by posting an Escape key press directly to it (see dismiss_compile_error_dialog), - then polls again briefly. This does not require or change OS focus/foreground - state; it only fires if a matching Igor Pro dialog window can actually be found - -- see dismiss_compile_error_dialog's docstring for the full mechanism and its - trade-off (it recovers the ability to continue, not the error message). The - returned dict's "auto_dismiss_attempted" field always reports what that attempt - found/did, even when it wasn't needed or no matching window was found. - - **If the returned dict has "prompt_user_to_check_for_dialog": True, whatever is - calling this tool should explicitly ask the human operator to check Igor Pro's - screen for a stuck compile-error dialog and close it, before retrying** -- not - just read the accompanying "note" text and move on. This only happens after the - automatic dismissal attempt above has already been tried and didn't resolve it - (or wasn't possible, e.g. no matching dialog window was found). - Confirmed directly during development: silently retrying or only logging the - note left the workflow stuck; explicitly prompting the human at this point is - what actually un-stuck it. - """ - baseline_counter = _read_claude_helper_compile_counter() - - errorCode, errorMsg, history, results = _execute2( - 'Execute/P "RELOAD CHANGED PROCS "' - ) - if errorCode != 0: - raise RuntimeError( - f"RELOAD CHANGED PROCS failed (error code {errorCode}): {errorMsg}" - ) - - time.sleep(_RELOAD_TO_COMPILE_PAUSE_SECONDS) - - errorCode, errorMsg, history, results = _execute2('Execute/P "COMPILEPROCEDURES "') - if errorCode != 0: - raise RuntimeError( - f"COMPILEPROCEDURES failed (error code {errorCode}): {errorMsg}" - ) - - time.sleep(_POST_COMPILE_PAUSE_SECONDS) - - poll_result = _poll_for_compile_confirmation( - baseline_counter, _COMPILE_POLL_TIMEOUT_SECONDS - ) - if poll_result["compiled"]: - return {"reload_triggered": True, "compile_triggered": True, **poll_result} - - dismiss_result = _attempt_dismiss_compile_error_dialog() - - if dismiss_result.get("attempted"): - poll_result = _poll_for_compile_confirmation( - baseline_counter, _COMPILE_POLL_TIMEOUT_AFTER_DISMISS_SECONDS - ) - if poll_result["compiled"]: - return { - "reload_triggered": True, - "compile_triggered": True, - **poll_result, - "auto_dismiss_attempted": dismiss_result, - "note": ( - "Compilation only succeeded after automatically simulating an " - "Escape key press to close what was very likely a stuck " - "compile-error dialog. The dialog's exact error message was NOT " - "recovered -- if this keeps happening, check the .ipf file's " - "syntax directly, or ask a human to read the dialog text before " - "it gets dismissed next time." - ), - } - - if "compiled_state_known" in poll_result: - note = ( - f"Reload/compile commands ran, but checking the resulting state kept " - f"failing (last error code {poll_result.get('last_error_code')}): " - f"{poll_result.get('last_error_msg')}. " + _COMPILE_ERROR_DIALOG_NOTE - ) - else: - note = ( - f"Still not compiled after polling for {_COMPILE_POLL_TIMEOUT_SECONDS:.0f}s" - + ( - f" plus a further {_COMPILE_POLL_TIMEOUT_AFTER_DISMISS_SECONDS:.0f}s " - "after an automatic Escape-key dismissal attempt" - if dismiss_result.get("attempted") - else "" - ) - + f" (requiring {_COMPILE_CONFIRM_CHECKS} consecutive confirmations). This is " - "more likely a genuine compile error in the procedure code than a timing " - "artifact -- check Igor's history/procedure window directly. " - + _COMPILE_ERROR_DIALOG_NOTE - ) - - return { - "reload_triggered": True, - "compile_triggered": True, - **poll_result, - "auto_dismiss_attempted": dismiss_result, - "prompt_user_to_check_for_dialog": True, - "note": note, - } - - -# --- Defining IGOR_PRO_BRIDGE without manual experiment setup ---------------------- -# -# Some procedure files wrap bridge-support helper functions in -# "#ifdef IGOR_PRO_BRIDGE / ... / #endif" so those helpers don't get silently -# compiled into an ordinary end-user build -- this repo's own -# Packages/MIES/MIES_ClaudeHelper.ipf is one example (see that file's own header -# comment), but nothing about this convention or this tool is specific to MIES: ANY -# Igor Pro experiment/procedure tree can adopt the same "#ifdef IGOR_PRO_BRIDGE" -# pattern for its own bridge-support code. The catch is the same regardless of whose -# code is gated: a freshly opened Igor Pro environment this bridge has never touched -# before (a bare "Untitled" experiment, or any experiment/branch whose Procedure -# window was never hand-edited for this) will not have IGOR_PRO_BRIDGE defined, so -# that gated code stays uncompiled until someone adds "#define IGOR_PRO_BRIDGE" to -# the experiment's Procedure window by hand and recompiles. -# -# Confirmed from Igor Pro Folder/Igor Help Files/Programming.ihf, "Conditional -# Compilation" topic: an ordinary "#define symbol" inside a procedure file is scoped -# to that file (or, for the main Procedure window specifically, to every -# non-independent-module file) -- but "SetIgorOption poundDefine=symb" instead adds -# symb to a separate *global* symbol list, "available in all procedure windows -# (including independent modules)". Queried via "SetIgorOption poundDefine=symb?" -# (sets V_flag to 1/0), reversed via "SetIgorOption poundUndefine=symb". "A symbol -# defined in a global list is not undefined by a #undef in a procedure window." -# -# Also confirmed there and cross-checked in Advanced Topics.ihf: this change is -# temporary -- it lasts only until Igor Pro quits (not saved into the experiment, -# must be redone every fresh Igor session) -- and itself triggers a recompile: the -# BeforeUncompiledHook table lists "SetIgorOption poundDefine" as changeCode 6 and -# "SetIgorOption poundUndefine" as changeCode 7, each described as "causes a -# recompile". Per Igor Reference.ihf's own SetIgorOption entry: "SetIgorOption is -# not compilable. To use it in a user-defined function, you need to use Execute" -- -# a non-issue here, since this bridge always sends it as an interpreted command-line -# statement via Execute2, never from inside compiled code. -# -# This tool deliberately has NO built-in knowledge of MIES or any other specific -# codebase -- it only manages the IGOR_PRO_BRIDGE symbol itself (defining it, -# recompiling, reporting the result), so it's equally useful for any Igor Pro -# experiment that adopts this convention for its own bridge-support code. An -# optional caller-supplied marker_function argument lets a caller who *does* know -# about a specific gated function (e.g. this repo's own "CH_ListXOPExports") get an -# extra confirmation that it actually became available, without that name being -# hardcoded into the bridge itself. -_IGOR_PRO_BRIDGE_DEFINE = "IGOR_PRO_BRIDGE" -_IGOR_PRO_BRIDGE_DEFINE_QUERY_CMD = ( - f'SetIgorOption poundDefine={_IGOR_PRO_BRIDGE_DEFINE}?; fprintf 0, "%d", V_flag' -) -# Function names in Igor are restricted to letters/digits/underscore (and can't start -# with a digit); this is just a defensive check against a caller-supplied string -# breaking out of the quoted FunctionInfo(...) call built below, not a claim about -# every valid Igor identifier rule. -_SAFE_IGOR_IDENTIFIER_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$") - - -def _is_igor_pro_bridge_defined() -> bool: - """Query Igor's global #define symbol list (see the block comment above) for - IGOR_PRO_BRIDGE.""" - errorCode, errorMsg, history, results = _execute2(_IGOR_PRO_BRIDGE_DEFINE_QUERY_CMD) - if errorCode != 0: - raise RuntimeError( - f"Could not query {_IGOR_PRO_BRIDGE_DEFINE} define state (error code " - f"{errorCode}): {errorMsg or '(no error message)'}" - ) - return results == "1" - - -def _function_resolves(function_name: str) -> bool: - """True if FunctionInfo(function_name) is non-empty, i.e. Igor currently has a - compiled function/operation by that name -- no assumption about which procedure - file or codebase defines it.""" - if not _SAFE_IGOR_IDENTIFIER_RE.match(function_name): - raise ValueError( - f"Not a plausible Igor function name: {function_name!r}" - ) - errorCode, errorMsg, history, results = _execute2( - f'fprintf 0, "%s", FunctionInfo("{function_name}")' - ) - if errorCode != 0: - raise RuntimeError( - f"Could not check FunctionInfo({function_name!r}) (error code " - f"{errorCode}): {errorMsg or '(no error message)'}" - ) - return results != "" - - -@mcp.tool() -def ensure_igor_pro_bridge_defined(marker_function: str = "") -> dict: - """Make sure the IGOR_PRO_BRIDGE conditional-compilation symbol is defined in the - current Igor Pro instance -- defining it and forcing a recompile if it wasn't - already, instead of requiring a human to hand-edit the experiment's Procedure - window first. See the block comment above this tool in server.py for the full - SetIgorOption/Conditional-Compilation background, confirmed directly from Igor - Pro Folder/Igor Help Files/Programming.ihf and Advanced Topics.ihf. - - This tool has NO built-in knowledge of MIES or any other specific codebase -- it - only manages the IGOR_PRO_BRIDGE symbol itself, so it's equally useful for any - Igor Pro experiment that adopts the "#ifdef IGOR_PRO_BRIDGE" convention for its - own bridge-support procedure code, not just this repo's own - MIES_ClaudeHelper.ipf. - - Call this proactively whenever a fresh/unfamiliar Igor Pro environment is being - used with this bridge for the first time in a session, or whenever some - IGOR_PRO_BRIDGE-gated behavior you rely on (e.g. - reload_and_compile_procedures's AfterCompiledHook-counter signal, if the - procedure file providing it is gated this way) looks unavailable and you want to - try fixing that rather than just accepting a weaker fallback. - - Args: - marker_function: optional. If you know of a *specific* function that should - become available once IGOR_PRO_BRIDGE is defined and compiled in (e.g. - "CH_ListXOPExports" for this repo's own MIES_ClaudeHelper.ipf), pass its - name here to get an extra before/after FunctionInfo(...) confirmation - layered on top of the define/recompile result. Leave blank to just - manage the define/recompile with no such check -- this is the fully - generic mode, appropriate when you don't know (or don't need to know) - about any specific gated function. - - Steps taken: - 1. If marker_function is given, checks FunctionInfo(marker_function) now, before - doing anything else (recorded as "marker_function_available_before"). - 2. Checks IGOR_PRO_BRIDGE's current state in the global #define list - (SetIgorOption poundDefine=IGOR_PRO_BRIDGE?). If already defined, returns - immediately with "igor_pro_bridge_defined": True and no recompile triggered -- - if marker_function was given and still doesn't resolve, that means whatever - procedure file defines it simply isn't #include-d by whatever is currently - loaded (e.g. a bare "Untitled" experiment) -- NOT something this tool can fix - by redefining IGOR_PRO_BRIDGE, so it's reported via - "marker_function_available_before"/"_after" rather than retried. - 3. If IGOR_PRO_BRIDGE is genuinely undefined, runs - 'SetIgorOption poundDefine=IGOR_PRO_BRIDGE' then 'COMPILEPROCEDURES ' (both - via Execute/P, the same operation-queue mechanism reload_and_compile_procedures - uses -- RELOAD CHANGED PROCS is deliberately skipped here since no on-disk - .ipf file changed, only Igor's in-memory global symbol list), then polls for - compile confirmation exactly the way reload_and_compile_procedures does, - reusing _poll_for_compile_confirmation (see that function's docstring for why - two independent signals are checked). - 4. If marker_function was given, re-checks FunctionInfo(marker_function) one - final time ("marker_function_available_after") and reports the outcome. - - Only ever ADDS the define, never calls poundUndefine -- there is currently no - known reason for this bridge to want IGOR_PRO_BRIDGE turned back off within a - session, and doing so would itself force yet another recompile for no benefit. - """ - marker_before = _function_resolves(marker_function) if marker_function else None - - was_defined = _is_igor_pro_bridge_defined() - - if was_defined: - result = {"igor_pro_bridge_defined": True, "define_set": False} - if marker_function: - result["marker_function"] = marker_function - result["marker_function_available_before"] = marker_before - result["marker_function_available_after"] = marker_before - if not marker_before: - result["note"] = ( - f"{_IGOR_PRO_BRIDGE_DEFINE} is already defined globally, but " - f'FunctionInfo("{marker_function}") still does not resolve. ' - "This means whatever procedure file defines that function is " - "not #include-d by whatever is currently loaded (e.g. a bare " - f"'Untitled' experiment) -- defining {_IGOR_PRO_BRIDGE_DEFINE} " - "again cannot fix that. Load an experiment/procedure file that " - "actually includes it instead (see load_experiment)." - ) - return result - - baseline_counter = _read_claude_helper_compile_counter() - - errorCode, errorMsg, history, results = _execute2( - f'Execute/P "SetIgorOption poundDefine={_IGOR_PRO_BRIDGE_DEFINE}"' - ) - if errorCode != 0: - raise RuntimeError( - f"SetIgorOption poundDefine={_IGOR_PRO_BRIDGE_DEFINE} failed (error code " - f"{errorCode}): {errorMsg}" - ) - - time.sleep(_RELOAD_TO_COMPILE_PAUSE_SECONDS) - - errorCode, errorMsg, history, results = _execute2('Execute/P "COMPILEPROCEDURES "') - if errorCode != 0: - raise RuntimeError( - f"COMPILEPROCEDURES failed (error code {errorCode}): {errorMsg}" - ) - - time.sleep(_POST_COMPILE_PAUSE_SECONDS) - - poll_result = _poll_for_compile_confirmation( - baseline_counter, _COMPILE_POLL_TIMEOUT_SECONDS - ) - - result = { - "igor_pro_bridge_defined_before": False, - "define_set": True, - **poll_result, - } - if marker_function: - result["marker_function"] = marker_function - result["marker_function_available_before"] = marker_before - result["marker_function_available_after"] = _function_resolves(marker_function) - return result + igor_version_info = call_function("ZBR#ZBR_IgorInfo", [0]) + os_info = call_function("ZBR#ZBR_IgorInfo", [3]) + loaded_xops_raw = call_function("ZBR#ZBR_IgorInfo", [10]) + experiment_file_kind = call_function("ZBR#ZBR_IgorInfo", [11]) + experiment_file_name = call_function("ZBR#ZBR_IgorInfo", [12]) + included_raw = call_function("ZBR#ZBR_WinList", ["*", "WIN:128"]) + data_folders_raw = call_function("ZBR#ZBR_DataFolderDir", [3]) + procedure_window_text = call_function("ZBR#ZBR_ProcedureText", ["", 0, "Procedure"]) + debugger_settings = _decode_debugger_state(call_function("ZBR#ZBR_GetDebuggerState")) + included_procedure_files = [ + name for name in included_raw.split(";") if name and name != "Procedure" + ] + loaded_xops = [x for x in loaded_xops_raw.split(";") if x] -# --- Debugger control --------------------------------------------------------------- -# -# Confirmed against a live Igor Pro instance during development, and against Igor -# Reference.ihf / Debugging.ihf directly (not guessed): -# -# - DebuggerOptions [enable=en, debugOnAbort=doa, debugOnError=doe, -# NVAR_SVAR_WAVE_Checking=nvwc] is the only operation that changes debugger settings. -# All parameters are optional; calling it with none just (re)sets its V_enable / -# V_debugOnError / V_debugOnAbort / V_NVAR_SVAR_WAVE_Checking output variables to the -# current state without changing anything -- confirmed verbatim from the docs: "All -# parameters are optional. If none are specified, no action is taken, but the output -# variables are still set." Multiple keyword arguments are comma-separated, confirmed -# from a real doc example: "DebuggerOptions enable=1, debugOnError=1". -# - "If the debugger is disabled then the other settings are cleared even if other -# settings are on" (verbatim from the docs) -- so enable=0 always clears everything. -# - **Why this matters for unattended/automated use, confirmed empirically this -# session**: there is no scriptable/COM way to resume, step, or dismiss the Debugger -# window once something pauses it (no such operation exists in Igor Reference.ihf, -# and the Debugger panel itself doesn't even show up as a window in -# WinList("*", ";", "WIN:65535")). If a breakpoint, a runtime error (debugOnError), -# a user abort (debugOnAbort), or a stale NVAR/SVAR/WAVE reference -# (NVAR_SVAR_WAVE_Checking) trips the debugger during an automated run, the specific -# COM call that triggered it hangs forever -- Execute2 is synchronous, and only a -# human clicking "Go" in the Debugger window can unblock it. (Other new COM calls -# still get answered while paused, since Igor's command line stays reentrant -- but -# the original call, and anything waiting on it, is stuck for good.) So: the debugger -# must be disabled before any unattended/automated session. -# - **This is not hypothetical -- it happened during development of this bridge**: a -# plain execute_igor_command call ran a test function while the Debugger was still -# enabled from earlier interactive use, Igor paused with the Debugger window open, -# and the call hung until a person closed the window by hand. That's exactly why -# execute_igor_command_unattended exists below: it disables the Debugger, runs the -# command, and restores the Debugger afterward automatically (in a try/finally, so -# it restores even if the command errors), rather than depending on whoever/whatever -# is calling this bridge to remember the manual get_debugger_state() / -# set_debugger_enabled(False) / restore_debugger_settings() dance every time. Use -# execute_igor_command_unattended by default for anything that might call -# user-defined procedure code unattended; reach for plain execute_igor_command only -# when a Debugger pause is deliberately wanted (e.g. interactively testing a -# breakpoint). - -# The trailing KillVariables/Z is not optional cleanup -- it's load-bearing. Igor's -# DebuggerOptions operation creates V_enable/V_debugOnError/V_debugOnAbort/ -# V_NVAR_SVAR_WAVE_Checking as output variables in whatever data folder happens to be -# current *every single time it's invoked*, regardless of which arguments (if any) were -# passed. Confirmed via a live A/B test: running an identical test suite via -# execute_igor_command_unattended (which calls this query, and _apply_debugger_options -# below, on every call) left those four variables behind in root:, which made the next -# hardware test case's CHECK_EMPTY_FOLDER() teardown check fail spuriously -- while the -# same test suite run via plain execute_igor_command (no DebuggerOptions call involved) -# left root: untouched. The values are captured into `results` via fprintf on the same -# line, before the KillVariables/Z runs, so nothing is lost by cleaning up immediately. -_DEBUGGER_STATE_CHECK_CMD = ( - 'DebuggerOptions; fprintf 0, "enable=%d,debugOnError=%d,debugOnAbort=%d,' - 'NVAR_SVAR_WAVE_Checking=%d", V_enable, V_debugOnError, V_debugOnAbort, ' - "V_NVAR_SVAR_WAVE_Checking; " - "KillVariables/Z V_enable, V_debugOnError, V_debugOnAbort, V_NVAR_SVAR_WAVE_Checking" -) - -# Snapshot captured by get_debugger_state(), consumed by restore_debugger_settings(). -# Process-lifetime state is fine here: one bridge process serves one Claude Desktop -# session, and this is meant to bracket exactly one unattended run within that. -_saved_debugger_settings = None + folders_part, waves_part = "", "" + for part in data_folders_raw.split("\r"): + part = part.strip() + if part.startswith("FOLDERS:"): + folders_part = part[len("FOLDERS:") :].rstrip(";") + elif part.startswith("WAVES:"): + waves_part = part[len("WAVES:") :].rstrip(";") + data_folders = [f for f in folders_part.split(",") if f] + top_level_waves = [w for w in waves_part.split(",") if w] + category_counts: dict = {} + for name in included_procedure_files: + category = _categorize_procedure_file(name) + category_counts[category] = category_counts.get(category, 0) + 1 -def _read_debugger_options() -> dict: - """Read the four DebuggerOptions settings without changing them. Returns - {"enable": bool, "debug_on_error": bool, "debug_on_abort": bool, - "nvar_svar_wave_checking": bool}.""" - errorCode, errorMsg, history, results = _execute2(_DEBUGGER_STATE_CHECK_CMD) - if errorCode != 0: - raise RuntimeError( - f"Could not read Debugger settings (error code {errorCode}): " - f"{errorMsg or '(no error message)'}" - ) - values = {} - for pair in results.split(","): - key, _, value = pair.partition("=") - values[key] = value return { - "enable": values.get("enable") == "1", - "debug_on_error": values.get("debugOnError") == "1", - "debug_on_abort": values.get("debugOnAbort") == "1", - "nvar_svar_wave_checking": values.get("NVAR_SVAR_WAVE_Checking") == "1", + "igor_version_info": igor_version_info, + "os_info": os_info, + "experiment_file_name": experiment_file_name, + "experiment_file_kind": experiment_file_kind, + "loaded_xops": loaded_xops, + "procedure_window_text": procedure_window_text, + "included_procedure_file_count": len(included_procedure_files), + "included_procedure_files_by_category": category_counts, + "included_procedure_files": included_procedure_files, + "data_folders": data_folders, + "top_level_waves": top_level_waves, + "debugger_settings": debugger_settings, } -def _apply_debugger_options(state: dict): - """Issue a DebuggerOptions command that sets all four settings to `state` - (enable/debug_on_error/debug_on_abort/nvar_svar_wave_checking), used by both - set_debugger_enabled and restore_debugger_settings so they can't drift apart.""" - parts = [f"enable={1 if state['enable'] else 0}"] - if state["enable"]: - parts.append(f"debugOnError={1 if state['debug_on_error'] else 0}") - parts.append(f"debugOnAbort={1 if state['debug_on_abort'] else 0}") - parts.append( - f"NVAR_SVAR_WAVE_Checking={1 if state['nvar_svar_wave_checking'] else 0}" - ) - cmd = "DebuggerOptions " + ", ".join(parts) - # See the comment above _DEBUGGER_STATE_CHECK_CMD: DebuggerOptions always creates - # these four globals in the current data folder as a side effect of being called at - # all. Clean them up immediately so every caller of this helper (execute_igor_ - # command_unattended, load_experiment, set_debugger_enabled, restore_debugger_ - # settings) never leaves them behind as stray root: globals. - cmd += ( - "; KillVariables/Z V_enable, V_debugOnError, V_debugOnAbort, " - "V_NVAR_SVAR_WAVE_Checking" - ) - - errorCode, errorMsg, history, results = _execute2(cmd) - if errorCode != 0: - raise RuntimeError( - f"Could not set Debugger settings (error code {errorCode}): " - f"{errorMsg or '(no error message)'}\nCommand was: {cmd}" - ) - - -@mcp.tool() -def get_debugger_state() -> dict: - """Read Igor Pro's current Debugger settings (enable, debugOnError, debugOnAbort, - NVAR_SVAR_WAVE_Checking) without changing them, and save a snapshot inside this - bridge process for restore_debugger_settings to restore later. - - **Call this before starting any unattended/automated session**, immediately before - calling set_debugger_enabled(False) to actually turn the debugger off. See - set_debugger_enabled's docstring for why the debugger must be off for unattended - execution -- a pause it causes cannot be resumed or dismissed remotely. - """ - global _saved_debugger_settings - state = _read_debugger_options() - _saved_debugger_settings = dict(state) - return state - - -@mcp.tool() -def set_debugger_enabled( - enabled: bool, - debug_on_error: bool = None, - debug_on_abort: bool = None, - nvar_svar_wave_checking: bool = None, -) -> dict: - """Turn Igor Pro's Debugger on or off (and optionally its debugOnError/ - debugOnAbort/NVAR_SVAR_WAVE_Checking sub-settings), via DebuggerOptions. - - **For any unattended/automated session -- running tests, scripted builds, anything - without a person watching -- the debugger MUST be disabled: call - set_debugger_enabled(False) before starting.** Confirmed empirically this session: - there is no scriptable/COM way to resume, step, or dismiss the Debugger window once - something pauses it (no such operation is documented in Igor Reference.ihf, and the - Debugger panel doesn't even appear as a window in WinList). If a breakpoint, a - runtime error (debugOnError), a user abort (debugOnAbort), or a stale NVAR/SVAR/WAVE - reference (NVAR_SVAR_WAVE_Checking) trips the debugger mid-run, the specific COM - call that triggered it hangs forever -- Execute2 is synchronous, and only a human - clicking "Go" in the Debugger window can unblock it. Other new COM calls still get - answered while paused (Igor's command line stays reentrant), but the original call, - and anything waiting on it, is stuck for good. - - enabled=False clears all four settings regardless of the other arguments -- this is - Igor's own documented behavior ("If the debugger is disabled then the other - settings are cleared even if other settings are on"), not a limitation of this - function -- so debug_on_error/debug_on_abort/nvar_svar_wave_checking are only - applied when enabled=True. - - debug_on_error/debug_on_abort/nvar_svar_wave_checking are truly optional: any left - as None (the default) fall back to Igor's CURRENT setting for that specific - sub-flag (read via _read_debugger_options) rather than being forced off. (Fixed - from an earlier version of this function, caught by code review: bool(None) is - False, so leaving a sub-flag unspecified used to silently clear it to off, even - though the docstring described these as optional -- i.e. "leave unchanged", not - "turn off".) Pass explicit True/False for any you want to actually change. - - Recommended pattern around an unattended session: - get_debugger_state() # read + save the current settings - set_debugger_enabled(False) # disable for the unattended run - ... run the unattended session ... - restore_debugger_settings() # put the saved settings back - """ - current = _read_debugger_options() - _apply_debugger_options( - { - "enable": enabled, - "debug_on_error": ( - current["debug_on_error"] if debug_on_error is None else debug_on_error - ), - "debug_on_abort": ( - current["debug_on_abort"] if debug_on_abort is None else debug_on_abort - ), - "nvar_svar_wave_checking": ( - current["nvar_svar_wave_checking"] - if nvar_svar_wave_checking is None - else nvar_svar_wave_checking - ), - } - ) - return _read_debugger_options() - - -@mcp.tool() -def restore_debugger_settings() -> dict: - """Restore Igor Pro's Debugger settings to whatever get_debugger_state last - captured. - - **Call this when an unattended/automated session ends**, to put the debugger back - the way it was before set_debugger_enabled(False) turned it off for the run. - - Raises if get_debugger_state was never called in this bridge process (nothing has - been saved to restore). - """ - if _saved_debugger_settings is None: - raise RuntimeError( - "No saved Debugger settings to restore -- call get_debugger_state() " - "before starting the unattended session so there is something to restore " - "afterward." - ) - _apply_debugger_options(_saved_debugger_settings) - return _read_debugger_options() - - -@mcp.tool() -def execute_igor_command_unattended(command: str) -> dict: - """Run `command` exactly like execute_igor_command, but automatically disable - Igor's Debugger before running it and restore it again afterward -- even if - `command` raises an Igor-level error. Uses its own local snapshot rather than the - get_debugger_state/restore_debugger_settings pair, so it's self-contained and - won't clash with a separate manual bracket around a longer session. - - **This is the tool to reach for whenever `command` might call user-defined - procedure code (e.g. any MIES or test function) and nothing is watching that could - close a Debugger popup by hand.** It exists because of a concrete failure hit - during development of this bridge: a plain execute_igor_command call ran a test - function while the Debugger was still enabled from earlier interactive use; Igor - Pro paused with the Debugger window open, and the call hung until a person closed - it manually -- there is no scriptable way to resume or dismiss a Debugger pause - (see set_debugger_enabled's docstring for the full explanation of why). Wrapping - the call so the Debugger is guaranteed off first removes that failure mode - entirely, instead of relying on remembering to call set_debugger_enabled(False) - beforehand every time. - - Only reach for plain execute_igor_command when you deliberately want the Debugger - available -- e.g. interactively testing a breakpoint, as done earlier in this - bridge's own development. - - For a longer unattended session made of many calls, prefer bracketing the whole - session with get_debugger_state() / set_debugger_enabled(False) once at the start - and restore_debugger_settings() once at the end, rather than paying the extra - disable/restore COM round-trip on every single command via this tool. - - Returns a dict with "results" (fprintf output) and "history" (anything - `command` sent to Igor's history area during this call, e.g. `print` output or - the command echo itself) -- see execute_igor_command's docstring for exactly - what "history" contains and why it's the reliable way to verify a `print` - actually happened. - - **On failure:** a nonzero error code means at least one unhandled runtime error - occurred somewhere in `command` -- it does NOT mean execution stopped there, and - it does NOT mean it was the only problem (see the runtime error model notes - above _format_execute2_error, right before execute_igor_command). The raised - error includes any partial `results` and `history` captured, since that's often - the only way to tell how far execution actually got. - """ - saved = _read_debugger_options() - _apply_debugger_options( - { - "enable": False, - "debug_on_error": False, - "debug_on_abort": False, - "nvar_svar_wave_checking": False, - } - ) - try: - errorCode, errorMsg, history, results = _execute2(command) - finally: - _apply_debugger_options(saved) - - if errorCode != 0: - raise RuntimeError( - _format_execute2_error(command, errorCode, errorMsg, results, history) - ) - return {"results": results, "history": history} - - -# --- Reading .ihf help files as formatted notebooks --------------------------------- -# -# Confirmed live this session against Igor Pro 9 Nightly. An .ihf file is itself an -# Igor formatted-text notebook, and Igor pre-registers every one in the Help Files -# folder as "open as a help file" via an ordinary (often hidden) help window -- -# WinList("*", ";", "WIN:512") is the correct bit for these (confirmed against -# WinList's own documented bit table; an earlier guess of WIN:1024 was wrong and -# matches no window type at all). A help-file view and a plain-notebook view of the -# same file are mutually exclusive: OpenNotebook/R on a file whose help window -# (hidden or not) is currently open fails with error 251 ("already open but as a help -# file"). CloseHelp/ALL releases every currently-open help file so OpenNotebook/R can -# succeed; OpenHelp/V=.../INT=0 re-opens a specific file afterward to restore it. +# --- Reading .ihf help files --------------------------------------------------------- # -# Reading the exported HTML (SaveNotebook/S=5) rather than the plain-text selection -# (Notebook .../GetSelection) matters because WaveMetrics' own help-authoring -# convention assigns a semantic paragraph style class to nearly every paragraph -- -# e.g. "Topic" for a heading, "Code1" for a line of example code, "Steps" for a -# bullet item -- confirmed live against several real .ihf files. This is a direct, -# reliable signal for a paragraph's content role that a flat plain-text read can't -# provide. - - -def _fprintf_query(expr: str) -> str: - """Run `fprintf 0, "%s", ` and return the resulting string, raising on - failure. Deliberately avoids ever declaring an intermediate `String` variable - for this: an Execute2 command runs as top-level interpreted code, so `String x = - ...` creates a process-lifetime global -- confirmed this session to collide with - "error 25: the name already exists as a variable" the second time the same - command runs in one Igor session. A bare fprintf has no such state to collide - with.""" - cmd = f'fprintf 0, "%s", {expr}' - errorCode, errorMsg, history, results = _execute2(cmd) - if errorCode != 0: - raise RuntimeError( - _format_execute2_error(cmd, errorCode, errorMsg, results, history) - ) - return results - - -def _winlist(match: str, options: str) -> list: - return [ - name - for name in _fprintf_query(f'WinList("{match}", ";", "{options}")').split(";") - if name - ] - - -def _igor_quote_path(path: str) -> str: - """Double every backslash so `path` is safe inside an Igor command string - literal -- Igor treats a single backslash as an escape character (Path - Separators, Advanced Topics.ihf).""" - return path.replace("\\", "\\\\") - - -def _resolve_help_file_path(bare_name: str): - """Resolve a bare help-file name (WinList's help-window bit never includes a - path -- "Procedure windows and help windows don't have names. WinList returns - the window title instead", confirmed this session) back to a full path, by - checking the two folders Igor Pro itself loads help files from: the global - `Igor Help Files` folder and the user-specific `Igor Help Files` folder. Both roots come from Igor's own - SpecialDirPath function rather than any hardcoded/guessed path, so this works - regardless of the specific Igor Pro version/install location. Returns None if - not found in either -- e.g. a third-party XOP's help file installed somewhere - else entirely.""" - for special_dir in ("Igor Application", "Igor Pro User Files"): - try: - base = _fprintf_query(f'SpecialDirPath("{special_dir}", 0, 1, 0)') - except RuntimeError: - continue - if not base: - continue - candidate = os.path.join(base, "Igor Help Files", bare_name) - if os.path.isfile(candidate): - return candidate - return None +# The actual CloseHelp/OpenNotebook/SaveNotebook/KillWindow/OpenHelp sequence now runs +# synchronously, Igor-side, in ZBR_ReadHelpFile (ZMQ_BridgeHelpers.ipf) -- none of +# those operations are subject to the Execute-only restriction, so this needs no +# submit/poll. The exported HTML is written to a temp file (built here, same as the +# COM-based version did) and read directly off disk afterward rather than serialized +# back through the CallFunction reply -- both processes run on the same machine, so +# this sidesteps any question about reply-size limits for a potentially large export. class _NotebookHTMLParser(html.parser.HTMLParser): @@ -1978,322 +1029,353 @@ def handle_data(self, data): @mcp.tool() -def read_help_file(file_path: str) -> dict: +def read_help_file(file_path: str, timeout_ms: int = 30000) -> dict: """Read an Igor Pro help file (.ihf) as structured, formatted text -- e.g. to look up an operation's exact flags/behavior straight from Igor's own docs -- without leaving any lasting change to Igor's help-window state. - Better than an OS-level file read for two reasons. First, .ihf files are - themselves Igor formatted-text notebooks, and Igor pre-registers every one in - the Help Files folder as an open help window (visible or hidden); this tool - handles the required CloseHelp/ALL -> OpenNotebook/R -> ... -> OpenHelp restore - dance so the caller doesn't have to. Second, and more importantly: the returned - "paragraphs" list preserves the paragraph style name WaveMetrics' own help - authoring convention assigns to each paragraph (e.g. "Topic" for a section - heading, "Code1" for a line of example code, "Steps" for a bullet item) -- - genuine content-block structure, not just flat prose. - - file_path must be a full path to an existing .ihf file (e.g. one found by - listing the Help Files folder -- see get_environment_summary's "loaded_xops" - field plus the global/user Help Files folders under Igor's own installation for - XOP-supplied help files, which don't all exist -- some XOPs ship none at all, - and their functions/operations then require external documentation instead). - - Full sequence, entirely reversible even if a step fails partway through: - 1. Snapshot every currently open help file (visible or hidden, via WinList's - WIN:512 bit) and every currently open plain-notebook window. - 2. CloseHelp/ALL (required: an .ihf file can't be opened as a notebook while - Igor considers it already open as a help file). - 3. OpenNotebook/R file_path, then diff WinList's notebook list against the - step-1 snapshot to find the name Igor assigned the new window (e.g. - "Notebook0") -- OpenNotebook doesn't return this directly. - 4. SaveNotebook/O/S=5/H=... export to a local temporary HTML file, parsed - here into the returned "paragraphs" list, then delete the temp file. - 5. KillWindow/Z the temporary notebook. - 6. Restore every help file captured in step 1 via OpenHelp/V=.../INT=0, - re-resolving each bare file name back to a full path via - SpecialDirPath("Igor Application"/"Igor Pro User Files", ...) + "Igor Help - Files" (Igor's global vs. user-specific include folders). - - Steps 5-6 run in a `finally` block, so a failure in step 3 or 4 still restores - whatever help state existed before this call. Returns a dict with: + file_path must be a full path to an existing .ihf file (e.g. one found via + get_environment_summary's "loaded_xops" field plus the global/user Help Files + folders under Igor's own installation for XOP-supplied help files). + + timeout_ms defaults to 30 seconds rather than this bridge's usual 5-second + default -- confirmed live that exporting a genuinely large help file (e.g. the + entire "Igor Reference.ihf" manual) as HTML can take longer than 5 seconds, which + would otherwise time out this call even though Igor-side the export eventually + succeeds anyway (harmlessly logging a "Host unreachable" ZeroMQ-XOP error to + history when it tries to reply to a client that already gave up -- see + check_bridge_health if you see that in read_session_history's output after a + timeout here). Pass a larger value still for very large help files if 30s isn't + enough. + + Returns a dict with: - "paragraphs": [{"style": "Topic", "text": "Debugging"}, ...] -- "style" is - "" for a paragraph with no explicit class. - - "restore_failures": bare file names from step 1 that could not be resolved - back to a full path (e.g. a help file supplied from somewhere other than - the two standard Help Files folders) -- these were NOT reopened, unlike - every other file captured in the snapshot. - - Raises if file_path does not exist, or if OpenNotebook/SaveNotebook fail (e.g. - file_path is not actually a notebook-compatible file). + WaveMetrics' own paragraph-class convention (e.g. "Topic" for a heading, + "Code1" for example code, "Steps" for a bullet item), "" if unset. + - "restore_failures": bare file names that could not be resolved back to a full + path and so were NOT reopened as help windows (e.g. a help file supplied from + somewhere other than the two standard Help Files folders). + + Raises if file_path does not exist, or if the underlying OpenNotebook/SaveNotebook + sequence fails Igor-side (e.g. file_path is not actually a notebook-compatible + file) -- help-window restoration is still attempted even then. """ normalized = os.path.abspath(file_path) if not os.path.isfile(normalized): raise RuntimeError(f"'{normalized}' does not exist or is not a file.") - quoted_path = _igor_quote_path(normalized) - - # Step 1: snapshot before touching anything. - help_all = _winlist("*", "WIN:512") - help_visible = set(_winlist("*", "WIN:512,VISIBLE:1")) - notebooks_before = set(_winlist("*", "WIN:16")) tmp_fd, tmp_html_path = tempfile.mkstemp(suffix=".html", prefix="igor_help_") os.close(tmp_fd) os.remove(tmp_html_path) # SaveNotebook must create it fresh; only the name is reused - quoted_tmp_html = _igor_quote_path(tmp_html_path) - notebook_name = None try: - # Step 2. - errorCode, errorMsg, history, results = _execute2("CloseHelp/ALL") - if errorCode != 0: - raise RuntimeError( - _format_execute2_error( - "CloseHelp/ALL", errorCode, errorMsg, results, history - ) - ) + status = call_function( + "ZBR#ZBR_ReadHelpFile", [normalized, tmp_html_path], timeout_ms=timeout_ms + ) + parts = status.split("|") + outcome = parts[0] if parts else "" + restore_failures = [f for f in (parts[-1] if parts else "").split(";") if f] - # Step 3. - open_cmd = f'OpenNotebook/R "{quoted_path}"' - errorCode, errorMsg, history, results = _execute2(open_cmd) - if errorCode != 0: + if outcome != "OK": + message = parts[1] if len(parts) > 1 else "(no message)" raise RuntimeError( - _format_execute2_error(open_cmd, errorCode, errorMsg, results, history) + f"Could not read help file {normalized!r}: {message} " + f"(restore_failures={restore_failures!r})" ) - new_names = [n for n in _winlist("*", "WIN:16") if n not in notebooks_before] - if not new_names: - raise RuntimeError( - "OpenNotebook/R succeeded but no new notebook window was found via " - f"WinList -- before: {sorted(notebooks_before)!r}" - ) - notebook_name = new_names[0] - # Step 4. - export_cmd = ( - f'SaveNotebook/O/S=5/H={{"UTF-8", 3, 7, 0, 0.9, 32}} {notebook_name} ' - f'as "{quoted_tmp_html}"' - ) - errorCode, errorMsg, history, results = _execute2(export_cmd) - if errorCode != 0: + if not os.path.isfile(tmp_html_path): raise RuntimeError( - _format_execute2_error( - export_cmd, errorCode, errorMsg, results, history - ) + f"ZBR_ReadHelpFile reported success but {tmp_html_path!r} was not " + "created." ) + with open(tmp_html_path, "r", encoding="utf-8") as f: html_text = f.read() parser = _NotebookHTMLParser() parser.feed(html_text) finally: - # Step 5 (best-effort -- must not skip step 6). - if notebook_name: - try: - _execute2(f"KillWindow/Z {notebook_name}") - except Exception: - pass try: if os.path.isfile(tmp_html_path): os.remove(tmp_html_path) except Exception: pass - # Step 6. - restore_failures = [] - for name in help_all: - resolved = _resolve_help_file_path(name) - if resolved is None: - restore_failures.append(name) - continue - visible_flag = 1 if name in help_visible else 0 - restore_cmd = ( - f'OpenHelp/V={visible_flag}/INT=0/Z=1 "{_igor_quote_path(resolved)}"' - ) - try: - _execute2(restore_cmd) - except Exception: - restore_failures.append(name) - return {"paragraphs": parser.paragraphs, "restore_failures": restore_failures} -# --- Environment summary ----------------------------------------------------------- -# -# Confirmed against a live Igor Pro instance during development (Igor Pro 10.03, build -# 30115). These are ordinary Igor built-in functions -- not part of the COM Automation -# Server API itself -- run the same way as any other command, via _execute2/fprintf: -# -# - IgorInfo(n) for n in 0-18 (n outside that range raises an Igor-level error, e.g. -# "expected value between 0 and 18"). The indices used below were identified -# empirically by probing all valid values against a live instance and matching each -# returned string to its evident meaning -- there is no single confirmed index for -# "the experiment's name", for example, so this was found by inspection, not assumed: -# IgorInfo(0) -- system report string (IGORVERS/BUILD/COMMIT/memory/screen info) -# IgorInfo(3) -- OS name/version/locale string -# IgorInfo(10) -- semicolon-separated list of loaded XOPs -# IgorInfo(11) -- experiment file kind (e.g. "Packed") -# IgorInfo(12) -- experiment file name (e.g. "Basic.pxp") -# - WinList("*", ";", "WIN:128") -- lists currently included procedure windows/files. -# The "128" bit was confirmed empirically (tested directly against a live instance, -# not looked up in documentation) to mean "procedure windows"; it reliably returns a -# complete, sensible-looking list of every included .ipf plus the special "Procedure" -# window (see below). -# - ProcedureText(macroOrFunctionNameStr, flags, winTitleStr) -- retrieves procedure -# text. IMPORTANT, confirmed the hard way this session: to get the *entire contents* -# of a named procedure window, the window name goes in winTitleStr (the third -# argument), with macroOrFunctionNameStr left as "" -- i.e. -# ProcedureText("", 0, "Procedure"), NOT ProcedureText("Procedure", 0, ""). The first -# argument instead names one specific macro/function *within* a window; passing a -# window name there matches nothing and silently returns "" rather than raising an -# error, which produced an incorrect "the Procedure window is empty" result during -# development until the user caught and corrected it. -# - The always-present "Procedure" window matters because Igor experiments (.pxp) can -# carry additional #include/#define directives there beyond what's in any on-disk -# .ipf file in the repo -- e.g. this project's experiments were found to #include -# ":UTF_Basic" and #define AUTOMATED_TESTING directly in that window. So the live -# in-memory environment is experiment-dependent, not fully determined by the repo -# file system alone. -# - DataFolderDir(3) -- returns "FOLDERS:name1,name2,...;WAVES:name1,name2,...;" -# (bitmask 3 = folders + waves) for the current data folder; confirmed empirically -# against root: to list top-level data folders and top-level waves. - -_ENV_SUMMARY_COMMANDS = { - "igor_version_info": 'fprintf 0, "%s", IgorInfo(0)', - "os_info": 'fprintf 0, "%s", IgorInfo(3)', - "loaded_xops_raw": 'fprintf 0, "%s", IgorInfo(10)', - "experiment_file_kind": 'fprintf 0, "%s", IgorInfo(11)', - "experiment_file_name": 'fprintf 0, "%s", IgorInfo(12)', - "included_procedure_windows_raw": 'fprintf 0, "%s", WinList("*", ";", "WIN:128")', - "data_folders_raw": 'fprintf 0, "%s", DataFolderDir(3)', - "procedure_window_text": 'fprintf 0, "%s", ProcedureText("", 0, "Procedure")', -} +# --- Bridge identity / health -------------------------------------------------------- +# Kept as a hardcoded constant (not read from manifest.json at runtime) for the same +# reason as before: the on-disk layout after Claude Desktop installs a .mcpb isn't +# guaranteed to keep server.py and manifest.json at a fixed relative path, and this +# needs to be confirmable from inside a conversation independent of that. +_BRIDGE_VERSION = "2.2.3" -def _categorize_procedure_file(name: str) -> str: - """Bucket an included procedure file name into a coarse category, purely to make a - ~250-entry file list skimmable in a summary. Buckets reflect this specific repo's - naming conventions (MIES_*, UTF_* unit tests, igortest-* test framework, IPNWB_*), - not a general Igor Pro convention.""" - if name.startswith("igortest"): - return "igortest_framework" - if name.startswith("UTF_"): - return "unit_tests" - if name.startswith("IPNWB"): - return "ipnwb" - if name.startswith("MIES_"): - return "mies_production" - return "other" + +def _installed_package_version(distribution_name: str) -> str | None: + try: + return importlib.metadata.version(distribution_name) + except importlib.metadata.PackageNotFoundError: + return None @mcp.tool() -def get_environment_summary() -> dict: - """Summarize the current Igor Pro instance's live environment: Igor version, the - loaded experiment, loaded external operations (XOPs), which procedure files are - actually included right now, the contents of the always-present "Procedure" window - (which can carry experiment-specific #include/#define directives not present in any - on-disk .ipf file), and the top-level global data folder layout. +def get_bridge_version() -> dict: + """Return the version of this Igor Pro Bridge build that is actually running right + now, plus which Python interpreter and package versions it's running with. + + Call this whenever it matters to confirm which build is active -- e.g. before + relying on a specific fix, or when reporting results from a test that depends on + a particular fix being in effect. Installing a newer .mcpb requires restarting + Claude Desktop; this is the only way to confirm afterward which version actually + loaded. + """ + return { + "version": _BRIDGE_VERSION, + "python_executable": sys.executable, + "python_version": sys.version.split()[0], + "mcp_package_version": _installed_package_version("mcp"), + "pyzmq_version": _installed_package_version("pyzmq"), + } - This queries the live instance directly rather than assuming the repo's file system - determines what's loaded -- which experiment (.pxp) is open changes all of this. - Returns a dict with: - - igor_version_info: raw IgorInfo(0) string (version/build/commit/memory/screen) - - os_info: raw IgorInfo(3) string (OS name/version/locale) - - experiment_file_name / experiment_file_kind: e.g. "Basic.pxp" / "Packed" - - loaded_xops: list of loaded external operations (e.g. NIDAQmx64, itcXOP2-64) - - procedure_window_text: raw contents of the special "Procedure" window -- - inspect this for experiment-specific #include/#define directives - - included_procedure_file_count: total number of currently included .ipf files - (excluding the "Procedure" window entry itself) - - included_procedure_files_by_category: counts per category (see - _categorize_procedure_file) - - included_procedure_files: the full list of currently included .ipf file names - - data_folders: top-level data folder names under root: - - top_level_waves: top-level wave names directly under root: (usually empty -- - MIES keeps its data organized into subfolders) - - debugger_settings: current enable/debugOnError/debugOnAbort/ - NVAR_SVAR_WAVE_Checking state (see _read_debugger_options). If "enable" is - True here, any unattended/automated session must call - get_debugger_state() + set_debugger_enabled(False) first -- see - set_debugger_enabled's docstring for why. +@mcp.tool() +def check_bridge_health() -> dict: + """Check whether this bridge can actually reach Igor Pro's ZeroMQ server right + now, and report what's known if not. + + Call this first whenever a command fails or behaves unexpectedly. + + Unlike the old COM-based version, this can no longer cleanly distinguish "Igor + Pro isn't running" from "Igor Pro is running but ZMQ_BridgeHelpers.ipf isn't + included/compiled/bound" from "wrong port/firewall" -- a ZeroMQ REQ socket that + gets no reply at all looks the same in all three cases (see the module docstring's + "Behavior changes" section). If this reports FAIL, check by hand: is Igor64.exe + actually running (Task Manager)? Is Packages/MIES/ZMQ_BridgeHelpers.ipf + #include-d and does check_compilation_state-equivalent info suggest it's + compiled? Try `netstat -a -b` (needs admin) to see whether anything is actually + listening on IGOR_ZMQ_ENDPOINT's port. + + Returns a dict with a "status" key ("OK" or "FAIL") and, on FAIL, a "problem" key. """ - raw = {} - for key, cmd in _ENV_SUMMARY_COMMANDS.items(): - errorCode, errorMsg, history, results = _execute2(cmd) - if errorCode != 0: - raise RuntimeError( - f"Could not retrieve '{key}' (error code {errorCode}): " - f"{errorMsg or '(no error message)'}\nCommand was: {cmd}" - ) - raw[key] = results + try: + info = call_function("ZBR#ZBR_Ping") + except IgorZmqUnreachable as e: + return { + "status": "FAIL", + "problem": ( + f"No reply from Igor Pro's ZeroMQ server ({e}). This can mean Igor " + "Pro isn't running, ZMQ_BridgeHelpers.ipf isn't #include-d/compiled " + "in the current experiment (see that file's own header comment for " + "the one-time setup step), or its ZeroMQ socket isn't bound to " + f"{IGOR_ZMQ_ENDPOINT} for some other reason." + ), + } + except IgorZmqError as e: + return { + "status": "FAIL", + "problem": f"Igor Pro's ZeroMQ server replied but reported an error: {e}", + } - included_procedure_files = [ - name for name in raw["included_procedure_windows_raw"].split(";") if name - ] - included_procedure_files = [ - name for name in included_procedure_files if name != "Procedure" - ] + return {"status": "OK", "igor_info": info} - loaded_xops = [x for x in raw["loaded_xops_raw"].split(";") if x] - folders_part, waves_part = "", "" - for part in raw["data_folders_raw"].split("\r"): - part = part.strip() - if part.startswith("FOLDERS:"): - folders_part = part[len("FOLDERS:") :].rstrip(";") - elif part.startswith("WAVES:"): - waves_part = part[len("WAVES:") :].rstrip(";") - data_folders = [f for f in folders_part.split(",") if f] - top_level_waves = [w for w in waves_part.split(",") if w] +# --- Compile-error dialog dismissal (posted Escape key message) --------------------- +# +# Unchanged from the COM-based version: this is pure OS-level window handling +# (win32gui/win32api), entirely independent of which transport talks to Igor Pro's +# procedure code. See the original version's extensive comment history (still +# accurate) for how the dialog's title/class were identified live, why title-only +# matching is used (a Copilot PR review flagged blanket "#32770" matching as unsafe), +# and why PostMessage (not a real hardware key event) is used. - category_counts: dict = {} - for name in included_procedure_files: - category = _categorize_procedure_file(name) - category_counts[category] = category_counts.get(category, 0) + 1 +_IGOR_PROCESS_NAME_PREFIX = "igor" +_KNOWN_STUCK_DIALOG_TITLES = ("Function Compilation Error",) +_POSTED_KEY_GAP_SECONDS = 0.05 +_POSTED_KEY_SETTLE_SECONDS = 0.2 + + +def _is_stuck_dialog_window(class_name: str, title: str) -> bool: + return any(known.lower() in title.lower() for known in _KNOWN_STUCK_DIALOG_TITLES) + + +def _get_process_exe_name(pid: int): + ACCESS = win32con.PROCESS_QUERY_INFORMATION | win32con.PROCESS_VM_READ + hProcess = None + try: + hProcess = win32api.OpenProcess(ACCESS, False, pid) + path = win32process.GetModuleFileNameEx(hProcess, 0) + return os.path.basename(path) + except Exception: + return None + finally: + if hProcess is not None: + win32api.CloseHandle(hProcess) + + +def _list_pids_for_exe_basename(basename_lower: str) -> list: + """Return the PIDs of every currently running process whose own module file name + matches basename_lower exactly (case-insensitively), e.g. "igor64.exe". + + Used by load_experiment to wait for an Igor Pro process to fully exit -- **not** + the same thing as it going quiet over ZeroMQ. Confirmed live (user report, this + session): ZeroMQ stops answering well before the underlying Igor64.exe process + actually terminates, and launching a replacement `Igor64.exe /UNATTENDED ` + command line while the old process is still alive -- even if it's already mid-quit + -- does NOT spawn a new process at all. Windows/Igor's single-instance-per-user + behavior instead either (a) redirects the launch request into the still-live old + instance, which can pop an unhandled "save changes?" dialog if it has unsaved + edits, or (b) if the old instance is already mid-shutdown, silently drops the + request altogether -- which looks exactly like the relaunch had no effect at all, + with no error reported anywhere. Waiting for the actual OS process list to be + clear of the target exe name, rather than trusting ZeroMQ silence, avoids both. + """ + matches = [] + for pid in win32process.EnumProcesses(): + if pid == 0: + continue + exe_name = _get_process_exe_name(pid) + if exe_name and exe_name.lower() == basename_lower: + matches.append(pid) + return matches + + +def _find_igor_dialog_window(): + matches = [] + + def _callback(hwnd, _extra): + if win32gui.IsWindowVisible(hwnd): + title = win32gui.GetWindowText(hwnd) + class_name = win32gui.GetClassName(hwnd) + if _is_stuck_dialog_window(class_name, title): + _, pid = win32process.GetWindowThreadProcessId(hwnd) + exe_name = _get_process_exe_name(pid) + if exe_name and exe_name.lower().startswith(_IGOR_PROCESS_NAME_PREFIX): + matches.append((hwnd, title, exe_name)) + return True + + win32gui.EnumWindows(_callback, None) + return matches[0] if matches else None + + +def _list_igor_top_level_windows() -> list: + windows = [] + + def _callback(hwnd, _extra): + if win32gui.IsWindowVisible(hwnd): + _, pid = win32process.GetWindowThreadProcessId(hwnd) + exe_name = _get_process_exe_name(pid) + if exe_name and exe_name.lower().startswith(_IGOR_PROCESS_NAME_PREFIX): + windows.append( + { + "title": win32gui.GetWindowText(hwnd), + "class_name": win32gui.GetClassName(hwnd), + "process": exe_name, + } + ) + return True + + win32gui.EnumWindows(_callback, None) + return windows + + +def _attempt_dismiss_compile_error_dialog() -> dict: + found = _find_igor_dialog_window() + if found is None: + return { + "attempted": False, + "reason": ( + "No visible window with a title containing one of " + f"{_KNOWN_STUCK_DIALOG_TITLES}, owned by an Igor Pro process, was " + "found. Either there is no stuck dialog right now, or it's a kind " + "not seen before -- see 'igor_windows_seen' below." + ), + "igor_windows_seen": _list_igor_top_level_windows(), + } + + hwnd, window_title, exe_name = found + + try: + win32api.PostMessage(hwnd, win32con.WM_KEYDOWN, win32con.VK_ESCAPE, 0) + time.sleep(_POSTED_KEY_GAP_SECONDS) + win32api.PostMessage(hwnd, win32con.WM_KEYUP, win32con.VK_ESCAPE, 0) + time.sleep(_POSTED_KEY_SETTLE_SECONDS) + except Exception as e: + return { + "attempted": False, + "reason": f"Posting the simulated Escape key press failed: {e}", + "dialog_window_title": window_title, + "dialog_window_process": exe_name, + } return { - "igor_version_info": raw["igor_version_info"], - "os_info": raw["os_info"], - "experiment_file_name": raw["experiment_file_name"], - "experiment_file_kind": raw["experiment_file_kind"], - "loaded_xops": loaded_xops, - "procedure_window_text": raw["procedure_window_text"], - "included_procedure_file_count": len(included_procedure_files), - "included_procedure_files_by_category": category_counts, - "included_procedure_files": included_procedure_files, - "data_folders": data_folders, - "top_level_waves": top_level_waves, - "debugger_settings": _read_debugger_options(), + "attempted": True, + "dialog_window_title": window_title, + "dialog_window_process": exe_name, + "note": ( + "Posted a simulated Escape key press directly to this dialog window " + "(no OS foreground/focus change was made or needed). This does NOT " + "recover the actual compile-error message -- it only closes whatever " + "modal dialog was showing. Follow up with check_compilation_state() or " + "reload_and_compile_procedures() to see whether this actually un-stuck " + "anything." + ), } +@mcp.tool() +def dismiss_compile_error_dialog() -> dict: + """Attempt to close a stuck Igor Pro modal compile-error dialog by posting a + simulated Escape key press directly to it, WITHOUT recovering the actual error + message and WITHOUT requiring or changing OS focus/foreground state. + + Use this manually when you suspect Igor Pro has a compile-error dialog open (e.g. + reload_and_compile_procedures kept reporting "not compiled" even after fixing a + known syntax error). Note: launching Igor Pro with /UNATTENDED (see + launch_igor_pro_unattended) suppresses this dialog entirely in favor of a plain + history line, so this should rarely be needed for an instance launched that way. + + Mechanism: enumerates top-level windows for a visible one, owned by a process + whose exe name starts with "igor", whose title matches a known stuck-dialog title + ("Function Compilation Error", confirmed live on both Igor Pro 10.03 and 9.06, + both a Qt window rather than a native "#32770" dialog). Posts WM_KEYDOWN/WM_KEYUP + for VK_ESCAPE via PostMessage, without requiring focus or foreground. + + If no matching window is found, reports "attempted": False along with + "igor_windows_seen": every visible top-level window currently owned by an Igor + Pro process, so a new stuck dialog's real title/class can be identified. + + **Trade-off: this does not tell you what the error was.** It only clears whatever + dialog is blocking Igor's operation queue so work can continue. + """ + return _attempt_dismiss_compile_error_dialog() + + +# --- Launching / relaunching Igor Pro ------------------------------------------------- +# +# Still pure Python/OS-level (starting a whole new Igor Pro *process* has to be done +# from outside any already-running instance -- no transport can do this from the +# inside). **No more elevation branching at all** -- see the module docstring: ZeroMQ +# has no privilege-matching requirement, so Igor Pro is always launched as a plain +# child process of this bridge process, at whatever privilege level that already is. + +_configured_igor_exe_path = None + + def _build_igor_launch_env(): - """Return an environment dict for subprocess.Popen when launching Igor Pro as a - direct child process, patching in COMSPEC if this process's own environment is - missing it. - - **Confirmed live this session, against a real launch_igor_pro_unattended call**: - Igor Pro's MIES procedures run a startup hook (IgorStartOrNewHook -> - GetMiesVersion -> CreateMiesVersionNoCache -> ExecuteGitForMIESVersion, in - MIES_GlobalStringAndVariableAccess.ipf) that shells out to git via - `ExecuteScriptText` to regenerate version.txt, using `GetCmdPath()` - (`GetEnvironmentVariable("COMSPEC")`, in MIES_Utilities_File.ipf) to find - cmd.exe. A child process launched via subprocess.Popen with no explicit env - inherits THIS Python process's own environment -- and querying the live - instance directly (`GetEnvironmentVariable("COMSPEC")`) showed it came back - empty, even though PATH itself was intact (including a working git - installation). Windows normally sets COMSPEC automatically for every - interactive login session, so a normal double-click/Start Menu launch of Igor - Pro never hits this; it only showed up because this bridge process's own - environment (inherited from whatever launched Claude Desktop) apparently never - had COMSPEC set. With COMSPEC empty, MIES's git-shell-out command becomes - malformed, ExecuteScriptText fails, and + """Return an environment dict for subprocess.Popen when launching Igor Pro, + patching in COMSPEC if this process's own environment is missing it. + + Confirmed necessary during this bridge's development: MIES's own startup hook + (IgorStartOrNewHook -> ... -> ExecuteGitForMIESVersion, MIES_GlobalStringAndVariable + Access.ipf) shells out to git via ExecuteScriptText using GetCmdPath()/COMSPEC to + find cmd.exe. A child process launched via subprocess.Popen with no explicit env + inherits this process's own environment -- which may not have COMSPEC set (a + normal interactive login session always does; this bridge process's own + environment, inherited from whatever launched Claude Desktop, might not). Without + it, MIES's git-shell-out becomes malformed and `ASSERT(!V_flag, "We have git installed but could not regenerate version.txt")` - (MIES_GlobalStringAndVariableAccess.ipf, ExecuteGitForMIESVersion) trips on every - fresh launch via this bridge. See SESSION_NOTES.md for the full diagnosis. - - Only patches COMSPEC specifically (the one confirmed-missing variable), rather - than rebuilding the whole environment from scratch -- everything else (PATH, - etc.) was already intact when this was diagnosed. + trips on every launch via this path. """ env = os.environ.copy() if not env.get("COMSPEC"): @@ -2305,23 +1387,17 @@ def _build_igor_launch_env(): @mcp.tool() def configure_igor_launch(exe_path: str) -> dict: """Record the full path to the Igor Pro executable (e.g. "...\\IgorBinaries_x64\\ - Igor64.exe") to use for launch_igor_pro_unattended, for the rest of this bridge - process's session. - - **Whatever agent is calling this tool should ask the user for this path (and - confirm they understand the elevation requirement below) once, at the start of - a session that might need to launch Igor Pro -- do not guess or default to a - typical installation path.** This repo alone has been tested against Igor Pro - installed in more than one differently-named folder (an Igor Pro 9 install and a - separate Igor Pro 10 install), so there is no single reliable default. This - setting is intentionally session-scoped, matching this bridge's other - session-scoped state (e.g. the history capture refnum) -- it resets if this - bridge process itself restarts (e.g. Claude Desktop fully restarts), so ask - again in a new session rather than assuming a previous answer still applies. + Igor64.exe") to use for launch_igor_pro_unattended/load_experiment, for the rest + of this bridge process's session. + + **Whatever agent is calling this tool should ask the user for this path once, at + the start of a session that might need to launch Igor Pro** -- do not guess or + default to a typical installation path; this repo alone has been tested against + Igor Pro installed in more than one differently-named folder. This setting is + session-scoped: it resets if this bridge process itself restarts. Raises if exe_path does not point to an existing file. Does not otherwise - validate that the file is actually Igor Pro (beyond a soft filename check) -- - launch_igor_pro_unattended will simply fail informatively if it isn't. + validate that the file is actually Igor Pro (beyond a soft filename check). """ global _configured_igor_exe_path @@ -2339,57 +1415,11 @@ def configure_igor_launch(exe_path: str) -> dict: note = ( "This file name does not look like a typical Igor Pro executable " "(expected something like 'Igor64.exe'). Proceeding anyway in case the " - "user has a renamed executable, but this is worth double-checking with " - "them if launch_igor_pro_unattended behaves unexpectedly." + "user has a renamed executable." ) _configured_igor_exe_path = normalized - elevated = _is_current_process_elevated() - - # _is_current_process_elevated() can return True, False, OR None (undetermined - # -- see its own docstring). A plain `if elevated` treats None the same as - # False, which would misreport "NOT currently elevated" as a confirmed fact - # when it's actually unknown -- flagged by a Copilot PR review as genuinely - # misleading guidance. Branched three ways explicitly instead. - if elevated is True: - elevation_plan = ( - "This bridge process is already elevated: launch_igor_pro_unattended " - "will start Igor Pro as a direct child process, which inherits this " - "process's elevation automatically -- no UAC prompt expected." - ) - elif elevated is False: - elevation_plan = ( - "This bridge process is NOT currently elevated: " - "launch_igor_pro_unattended will request elevation via Windows' UAC " - "('Run as administrator') when launching Igor Pro, which requires the " - "user to approve a consent dialog themselves. Even after that succeeds, " - "THIS Python process will still not be elevated, so COM calls will keep " - "failing due to the resulting privilege-level mismatch (see " - "check_bridge_health) until Claude Desktop itself is relaunched at a " - "matching level (elevated, to match the now-elevated Igor Pro) -- make " - "sure the user understands this before relying on " - "launch_igor_pro_unattended to get a fully working bridge. " - "Alternatively, Igor Pro can simply be launched non-elevated by hand " - "instead, which needs no elevation match at all." - ) - else: - elevation_plan = ( - "Could not determine whether this bridge process is currently " - "elevated. launch_igor_pro_unattended treats this the same as " - "'not elevated' as a conservative default (requesting UAC elevation " - "via ShellExecute's 'runas' verb rather than risking a silently " - "unelevated direct launch) -- if COM calls fail afterward, check " - "check_bridge_health and make sure Claude Desktop and Igor Pro are " - "running at the same privilege level (both elevated, or both not; " - "elevation itself is not required)." - ) - - return { - "configured_exe_path": normalized, - "python_process_elevated": elevated, - "note": note, - "elevation_plan": elevation_plan, - } + return {"configured_exe_path": normalized, "note": note} _POST_LAUNCH_POLL_INTERVAL_SECONDS = 1.0 @@ -2401,58 +1431,34 @@ def launch_igor_pro_unattended(wait_for_ready_seconds: float = 30.0) -> dict: flag. Per Igor Pro Folder/Igor Help Files/Advanced Topics.ihf ("Calling Igor from - Scripts"), /UNATTENDED "suppresses certain interactions that are inconvenient - for unattended operations" -- documented examples are the About Autosave dialog - and (Igor Pro 10+) the license activation dialog. Empirically confirmed this - session against a live Igor Pro 9.06 instance, but NOT documented anywhere in - Igor's help files: /UNATTENDED also suppresses the modal "Function Compilation - Error" dialog that normally appears on a bad procedure compile, and instead - reports the error as a plain line in Igor's history area (format - "::: error: ", readable via read_session_history) -- - see SESSION_NOTES.md for the full finding. This means a bridge session started - this way should never need dismiss_compile_error_dialog at all. - - **Requires configure_igor_launch(exe_path) to have been called first in this - same bridge session** -- there is no default or guessed path. Raises immediately - with an actionable message if it hasn't been. See configure_igor_launch's - docstring for why the calling agent should ask the user for this rather than - assume it. - - Refuses to launch (returns "launched": False rather than raising) if an Igor Pro - instance is already reachable via COM right now: launching the executable again - with only /UNATTENDED (no /I, /X, /SN, or file-path argument) is documented to - start a genuinely new instance rather than reusing an existing one (Advanced - Topics.ihf, "Calling Igor from Scripts", Details section: an existing instance - is only reused if you include /X, /SN, or a path to a file), which would leave - two Igor64.exe processes running -- contradicting this bridge's existing - guidance elsewhere (check_bridge_health) that there should be exactly one. - - Elevation handling: if this Python process is itself already elevated, Igor Pro - launches as a direct child process, which inherits that elevation automatically - -- no prompt, no separate step. If this process is NOT elevated, launches via - ShellExecute's "runas" verb instead, which triggers a normal Windows UAC consent - dialog the user must approve -- but even after that succeeds, THIS process will - still not be elevated, so COM calls will keep failing (the classic - privilege-level-mismatch failure mode -- see check_bridge_health; elevation - itself is not the requirement, matching levels is) until Claude Desktop itself - is relaunched at a matching level (elevated, to match the now-elevated Igor - Pro). configure_igor_launch's own return value already surfaces which of these - two paths will be taken -- check that first. - - The direct-child-process path also patches COMSPEC into the child's environment - if this Python process's own environment is missing it (see - _build_igor_launch_env) -- confirmed necessary this session: without it, MIES's - own startup code (IgorStartOrNewHook -> ... -> ExecuteGitForMIESVersion, which - shells out to git via ExecuteScriptText using GetCmdPath()/COMSPEC to find - cmd.exe) hits an assertion ("We have git installed but could not regenerate - version.txt") on every launch via this path, even though a normal - double-click/Start Menu launch never hits it (a real interactive login session - always has COMSPEC set). - - After launching, polls for the new instance to become reachable via COM (every - ~1s) up to wait_for_ready_seconds, since Igor Pro can take several seconds to - finish initializing its Automation Server. Returns whether it became ready and - how many polling attempts that took. + Scripts"), /UNATTENDED "suppresses certain interactions that are inconvenient for + unattended operations" -- documented examples are the About Autosave dialog and + (Igor Pro 10+) the license activation dialog. Also confirmed empirically (not + documented anywhere in Igor's help files): /UNATTENDED also suppresses the modal + "Function Compilation Error" dialog on a bad procedure compile, reporting the + error as a plain history line instead (format "::: error: + ", readable via read_session_history). + + **Requires configure_igor_launch(exe_path) to have been called first.** + + Refuses to launch (returns "launched": False) if something already answers + ZBR#ZBR_Ping right now -- launching the executable again with only /UNATTENDED + starts a genuinely new instance rather than reusing an existing one (Advanced + Topics.ihf), which would leave two Igor64.exe processes running. + + **Always launches as a plain child process (subprocess.Popen), at whatever + privilege level this bridge process itself is running at -- no elevation request, + no UAC prompt, ever.** This is a deliberate change from the COM-based version: + ZeroMQ has no privilege-matching requirement at all (unlike COM, which needed this + process and Igor Pro to run at the SAME privilege level), so there is nothing to + branch on anymore. + + Patches COMSPEC into the child's environment if missing (see + _build_igor_launch_env) -- needed for MIES's own git-based startup hook. + + After launching, polls for ZBR#ZBR_Ping to start answering (every ~1s) up to + wait_for_ready_seconds. Returns whether it became ready and how many polling + attempts that took. """ if not _configured_igor_exe_path: raise RuntimeError( @@ -2463,86 +1469,164 @@ def launch_igor_pro_unattended(wait_for_ready_seconds: float = 30.0) -> dict: "this tool." ) - try: - _get_igor(force_reconnect=True) - already_running = True - except RuntimeError: - already_running = False - - if already_running: + if _reachable(timeout_ms=1000): return { "launched": False, "reason": ( - "An Igor Pro instance is already running and reachable via COM. " - "Refusing to launch a second one -- close the existing instance " - "first if a genuinely fresh one (e.g. relaunched with /UNATTENDED) " - "is actually wanted." + "Something already answered ZBR#ZBR_Ping over ZeroMQ. Refusing to " + "launch a second Igor Pro instance -- close the existing one first " + "if a genuinely fresh one is actually wanted." ), } - elevated = _is_current_process_elevated() - # _is_current_process_elevated() can return True, False, OR None (undetermined -- - # see its own docstring). A plain `if elevated`/`elevated else ...` treats None the - # same as False -- behaviorally fine here since undetermined should conservatively - # fall back to the not-elevated (UAC-prompting) path anyway, but written as an - # explicit `is True` check for clarity, matching the same fix already applied to - # configure_igor_launch's elevation_plan text above. - launch_method = ( - "direct_child_process" if elevated is True else "shell_execute_runas" - ) - try: - if elevated is True: - subprocess.Popen( - [_configured_igor_exe_path, "/UNATTENDED"], - env=_build_igor_launch_env(), - ) - else: - win32api.ShellExecute( - 0, - "runas", - _configured_igor_exe_path, - "/UNATTENDED", - None, - win32con.SW_SHOWNORMAL, - ) + subprocess.Popen( + [_configured_igor_exe_path, "/UNATTENDED"], + env=_build_igor_launch_env(), + ) except Exception as e: - return { - "launched": False, - "reason": f"Failed to start the process ({e}).", - "launch_method": launch_method, - } + return {"launched": False, "reason": f"Failed to start the process ({e})."} deadline = time.monotonic() + wait_for_ready_seconds attempts = 0 while time.monotonic() < deadline: attempts += 1 - try: - _get_igor(force_reconnect=True) - return { - "launched": True, - "launch_method": launch_method, - "com_ready": True, - "poll_attempts": attempts, - } - except RuntimeError: - time.sleep(_POST_LAUNCH_POLL_INTERVAL_SECONDS) + if _reachable(timeout_ms=1000): + return {"launched": True, "zmq_ready": True, "poll_attempts": attempts} + time.sleep(_POST_LAUNCH_POLL_INTERVAL_SECONDS) return { "launched": True, - "launch_method": launch_method, - "com_ready": False, + "zmq_ready": False, + "poll_attempts": attempts, + "note": ( + f"The process was started, but nothing answered ZBR#ZBR_Ping within " + f"{wait_for_ready_seconds:.0f}s. Igor Pro may still be initializing " + "(slower on first launch or a cold machine), or ZMQ_BridgeHelpers.ipf may " + "not be #include-d/compiled in whatever experiment this instance opened " + "by default -- try check_bridge_health() again after waiting longer." + ), + } + + +@mcp.tool() +def load_experiment( + file_path: str, + wait_for_ready_seconds: float = 30.0, + process_exit_timeout_seconds: float = 30.0, +) -> dict: + """Load an Igor Pro experiment file (.pxp), replacing whatever is currently open. + + **Behavior change from the COM-based version, read carefully**: COM's + IApplication.LoadExperiment hot-swapped the experiment inside the SAME running + Igor Pro process. That method is COM-only (confirmed: neither "LoadExperiment" + nor "OpenFile" appear anywhere in Igor Reference.ihf, only in Automation + Server.ihf) -- there is no procedure-language equivalent, so this transport + cannot replicate it. Instead, this tool: + + 1. Asks the currently-running instance to quit (`Quit/N`, submitted via + ZBR_SubmitCommand -- deferred, so this step itself still gets a normal reply + before Igor actually exits). + 2. Waits for the underlying Igor64.exe **OS process** to fully disappear from the + process list -- see below for why this can't just check ZeroMQ reachability. + 3. Relaunches the configured Igor Pro executable (see configure_igor_launch) with + `/UNATTENDED` plus the target file path as a launch argument -- passing a file + path on launch is documented (Advanced Topics.ihf, "Calling Igor from + Scripts") to open that specific file. + 4. Polls for the new instance to start answering ZBR#ZBR_Ping, same as + launch_igor_pro_unattended. + + **Step 2 is not optional, and checking ZeroMQ reachability alone is not enough -- + confirmed live (user report) after an early version of this tool relied on exactly + that and silently failed.** ZeroMQ goes quiet well before the Igor64.exe process + actually terminates (Igor can take several seconds to fully exit after `Quit/N` + runs). If the replacement `Igor64.exe /UNATTENDED ` command line is launched + while the old process is still alive -- even mid-shutdown -- Windows/Igor's + single-instance-per-user behavior does not spawn a new process at all: it either + (a) redirects the launch into the still-live old instance, which pops an unhandled + "save changes?" dialog if it happens to have unsaved edits, or (b) if the old + instance is already mid-quit, silently drops the request altogether. Case (b) is + especially deceptive: nothing errors, no new process appears, and the net effect + looks exactly like the relaunch had no effect whatsoever. This tool instead polls + the actual OS process list (matching the configured executable's own file name, + e.g. "Igor64.exe") until no such process remains, up to + process_exit_timeout_seconds, before ever invoking the relaunch command line. If + that timeout elapses with the process still present, this raises rather than + proceeding -- proceeding anyway would just reproduce the same silent-failure risk + this check exists to prevent. A stuck "save changes?" dialog on the OLD instance + (e.g. if something modified the experiment after your last save) is the most + likely cause; check for one by hand if this happens. + + This is a genuine process restart, not an in-place swap. **Any unsaved changes in + the currently-open experiment are lost** -- call + execute_igor_command('SaveExperiment') first if that matters (matching the old + version's own behavior: LoadExperiment never auto-saved either, but there was at + least still a "hot" instance to save from afterward if you forgot; now there + isn't). + + Requires configure_igor_launch(exe_path) to have been called first. + + Raises if file_path does not point to an existing file, or if the old Igor Pro + process does not fully exit within process_exit_timeout_seconds. + """ + if not _configured_igor_exe_path: + raise RuntimeError( + "No Igor Pro executable path configured yet -- call " + "configure_igor_launch(exe_path) first." + ) + + normalized = os.path.abspath(file_path) + if not os.path.isfile(normalized): + raise RuntimeError(f"'{normalized}' does not exist or is not a file.") + + igor_basename_lower = os.path.basename(_configured_igor_exe_path).lower() + + try: + call_function("ZBR#ZBR_SubmitCommand", ["Quit/N"]) + except (IgorZmqError, IgorZmqUnreachable): + pass # nothing was running/reachable to begin with -- nothing to quit + + quit_deadline = time.monotonic() + process_exit_timeout_seconds + remaining_pids = _list_pids_for_exe_basename(igor_basename_lower) + while remaining_pids and time.monotonic() < quit_deadline: + time.sleep(0.5) + remaining_pids = _list_pids_for_exe_basename(igor_basename_lower) + + if remaining_pids: + raise RuntimeError( + f"'{igor_basename_lower}' did not fully exit within " + f"{process_exit_timeout_seconds:.0f}s of sending Quit/N (PIDs still " + f"running: {remaining_pids}). Relaunching now would risk silently doing " + "nothing (see this tool's own docstring) -- check whether the existing " + "Igor Pro instance is stuck on an unhandled dialog (e.g. 'save changes?' " + "if something modified the experiment since your last save) and resolve " + "it by hand, or retry with a longer process_exit_timeout_seconds." + ) + + try: + subprocess.Popen( + [_configured_igor_exe_path, "/UNATTENDED", normalized], + env=_build_igor_launch_env(), + ) + except Exception as e: + raise RuntimeError(f"Failed to relaunch Igor Pro with {normalized!r}: {e}") from e + + deadline = time.monotonic() + wait_for_ready_seconds + attempts = 0 + while time.monotonic() < deadline: + attempts += 1 + if _reachable(timeout_ms=1000): + return {"loaded_file": normalized, "zmq_ready": True, "poll_attempts": attempts} + time.sleep(_POST_LAUNCH_POLL_INTERVAL_SECONDS) + + return { + "loaded_file": normalized, + "zmq_ready": False, "poll_attempts": attempts, "note": ( - f"The process was started, but no COM connection became reachable " - f"within {wait_for_ready_seconds:.0f}s. Igor Pro may still be " - "initializing (slower on first launch or a cold machine) -- try " - "check_bridge_health() again after waiting longer. If launch_method is " - "'shell_execute_runas', also consider that this Python process itself " - "is not elevated while Igor Pro (just launched via the UAC prompt) now " - "is, which will prevent a COM connection indefinitely regardless of how " - "long you wait, until Claude Desktop is relaunched at a matching " - "(elevated) level." + f"Process relaunched with {normalized!r}, but nothing answered " + f"ZBR#ZBR_Ping within {wait_for_ready_seconds:.0f}s. Try " + "check_bridge_health() again after waiting longer." ), } From 2d50182a00950f633a23ca494cd812a139c72452 Mon Sep 17 00:00:00 2001 From: Michael Huth Date: Wed, 5 Aug 2026 23:44:18 +0200 Subject: [PATCH 10/12] MCP: v2.3.0 Mitigate crashes when recompiling code --- Packages/doc/igor-pro-bridge.rst | 56 ++++++--- tools/igor-mcp-bridge/ZMQ_BridgeHelpers.ipf | 108 +++++++++++++++--- .../igor-pro-bridge-2.2.3.mcpb | Bin 33896 -> 0 bytes .../igor-pro-bridge-2.3.0.mcpb | Bin 0 -> 35142 bytes tools/igor-mcp-bridge/manifest.json | 4 +- tools/igor-mcp-bridge/server.py | 29 +++-- 6 files changed, 151 insertions(+), 46 deletions(-) delete mode 100644 tools/igor-mcp-bridge/igor-pro-bridge-2.2.3.mcpb create mode 100644 tools/igor-mcp-bridge/igor-pro-bridge-2.3.0.mcpb diff --git a/Packages/doc/igor-pro-bridge.rst b/Packages/doc/igor-pro-bridge.rst index ab990dfaf5..26e2aec52d 100644 --- a/Packages/doc/igor-pro-bridge.rst +++ b/Packages/doc/igor-pro-bridge.rst @@ -405,12 +405,31 @@ Available tools errors (the bridge briefly unable to reach Igor mid-recompile) are treated as "not ready yet" rather than fatal. If compilation still isn't confirmed after the poll times out, this automatically makes one attempt to dismiss a possible stuck - compile-error dialog (see ``dismiss_compile_error_dialog``). **Caution**: on more - than one occasion during this bridge's development (both under the COM transport - and, circumstantially, still suspected under ZeroMQ), Igor Pro became unreachable - shortly after a reload/compile attempt -- root cause unconfirmed; treat any failure - on a subsequent call as a signal to check ``check_bridge_health()`` and be prepared - to relaunch Igor Pro. See ``SESSION_NOTES.md`` for the ongoing investigation. + compile-error dialog (see ``dismiss_compile_error_dialog``). **Caution, carried + over from the COM-based version**: Igor Pro has been observed becoming + unreachable shortly after a reload/compile attempt on more than one occasion + during this bridge's development (crashed or was closed). As of **v2.3.0**, + crash-dump analysis (two ``.dmp`` files, parsed with Python's ``minidump`` + package) traced this to a genuine ``EXCEPTION_ACCESS_VIOLATION`` deep inside + ``Igor64.exe`` itself, not this bridge's own code, and a likely mechanism was + identified by comparing against this repo's own ``igortest-tracing.ipf`` + (whose ``CompileAndRestart()``/``AfterCompiledHook()`` pattern never crashes): + this bridge's ZeroMQ-XOP handler runs as a background thread that keeps + dispatching incoming ``CallFunction`` requests regardless of what Igor's main + thread is doing, so a request arriving while ``COMPILEPROCEDURES`` is + mid-rebuild of Igor's own internal function/symbol tables is a plausible + cross-thread race. v2.3.0 mitigates this by stopping the ZeroMQ handler + (``zeromq_handler_stop()``, via ``ZBR_StopHandlerBeforeRecompile``) before + ``RELOAD CHANGED PROCS``/``COMPILEPROCEDURES`` run, and restarting it only + after compilation finishes (the existing ``AfterCompiledHook`` -> + ``ZBR_EnsureZeroMQBound()`` call, unchanged). This is a well-reasoned + mitigation, not a proven fix -- ``Igor64.exe`` ships no public symbols, so the + exact fault can't be confirmed from here, and the crash was already + rare/nondeterministic. Confirmed live afterward, including three concurrent + ``reload_and_compile_procedures()`` calls as a stress test with no crash. If a + tool call after this one starts failing anyway, check + ``check_bridge_health()`` and be prepared for Igor Pro to need relaunching. + See ``SESSION_NOTES.md`` for the full investigation. ``dismiss_compile_error_dialog()`` Attempts to close a stuck Igor Pro dialog by posting a simulated Escape key press @@ -816,16 +835,21 @@ Known limitations session running on the same Windows machine as Igor Pro, not from a cloud/sandboxed session. - **Observed on more than one occasion during this bridge's development (both under - the v1.x COM transport, and circumstantially suspected under v2.0.0's ZeroMQ - transport), root cause unconfirmed**: Igor Pro became unreachable (crashed or was - closed) shortly after a ``reload_and_compile_procedures`` call. It isn't established - whether this is related to the bridge's own actions (either transport) or a - pre-existing Igor Pro stability issue independent of both -- fresh Igor Pro launches - followed by ordinary (non-reload/compile) tool calls have never reproduced it in this - bridge's development, which is circumstantial evidence pointing away from the new - ZeroMQ code specifically, but is not conclusive. Treat any unreachability after a - compile attempt as a signal to check ``check_bridge_health()`` and be prepared to - relaunch Igor Pro. See ``SESSION_NOTES.md`` for the full, ongoing investigation. + the v1.x COM transport and v2.0.0's ZeroMQ transport)**: Igor Pro became unreachable + (crashed or was closed) shortly after a ``reload_and_compile_procedures`` call. As + of **v2.3.0**, this was traced via crash-dump analysis to a genuine + ``EXCEPTION_ACCESS_VIOLATION`` inside ``Igor64.exe`` itself, with a likely + mechanism identified (confirmed correct by the repo owner's comparison against + ``igortest-tracing.ipf``'s crash-free ``CompileAndRestart()``/``AfterCompiledHook()`` + pattern): the ZeroMQ-XOP's background message-handler thread dispatching a + ``CallFunction`` request while Igor's main thread is mid-``COMPILEPROCEDURES``, + racing on Igor's own internal function/symbol tables. v2.3.0 mitigates this by + stopping the handler before reload/compile and restarting it only after + compilation finishes -- see the ``reload_and_compile_procedures()`` reference entry + above for the full mechanism and the caveat that this is a mitigation, not a + proven fix. Treat any unreachability after a compile attempt as a signal to check + ``check_bridge_health()`` and be prepared to relaunch Igor Pro. See + ``SESSION_NOTES.md`` for the full investigation. .. _igor_pro_bridge_v1_history: diff --git a/tools/igor-mcp-bridge/ZMQ_BridgeHelpers.ipf b/tools/igor-mcp-bridge/ZMQ_BridgeHelpers.ipf index ef23494bf6..7340565207 100644 --- a/tools/igor-mcp-bridge/ZMQ_BridgeHelpers.ipf +++ b/tools/igor-mcp-bridge/ZMQ_BridgeHelpers.ipf @@ -1,7 +1,7 @@ -#pragma TextEncoding = "UTF-8" -#pragma rtGlobals = 3 +#pragma TextEncoding = "UTF-8" +#pragma rtGlobals = 3 #pragma IndependentModule = ZBR -#pragma version = 1.00 +#pragma version = 1.00 // ZMQ_BridgeHelpers.ipf -- Igor Pro-side utility functions backing the Igor Pro Bridge // (tools/igor-mcp-bridge/) from v2.0.0 onward, which now talks to Igor Pro over the @@ -127,9 +127,9 @@ static Function ZBR_EnsureStorage() DFREF dfr = root:Packages:ZBR if(!WaveExists(dfr:done)) - Make/N=0/O dfr:done // 0 = pending, 1 = done + Make/N=0/O dfr:done // 0 = pending, 1 = done Make/N=0/O/T dfr:resultText - Make/N=0/O dfr:historyStart + Make/N=0/O dfr:historyStart endif End @@ -184,7 +184,7 @@ End /// trigger the Debugger. static Function ZBR_EnsureCaptureStarted() - string dummy + string dummy variable err NewDataFolder/O root:Packages @@ -192,14 +192,14 @@ static Function ZBR_EnsureCaptureStarted() NVAR/Z refnum = root:Packages:ZBR:captureRefNum if(!NVAR_Exists(refnum)) - Variable/G root:Packages:ZBR:captureRefNum = CaptureHistoryStart() + variable/G root:Packages:ZBR:captureRefNum = CaptureHistoryStart() else NVAR refnumRW = root:Packages:ZBR:captureRefNum try - dummy = CaptureHistory(refnumRW, 0);AbortOnRTE + dummy = CaptureHistory(refnumRW, 0); AbortOnRTE catch - err = GetRTError(1) // clear the trapped error; discard the specific code, we always recover the same way + err = GetRTError(1) // clear the trapped error; discard the specific code, we always recover the same way refnumRW = CaptureHistoryStart() endtry endif @@ -240,7 +240,7 @@ static Function/S ZBR_AllocateToken() variable n ZBR_EnsureStorage() - DFREF dfr = root:Packages:ZBR + DFREF dfr = root:Packages:ZBR WAVE done = dfr:done WAVE/T resultText = dfr:resultText WAVE historyStart = dfr:historyStart @@ -299,10 +299,10 @@ Function/S ZBR_SubmitCommandUnattended(string cmd) string token, finishCall, restore DebuggerOptions - Variable/G root:Packages:ZBR:savedDebugEnable = V_enable - Variable/G root:Packages:ZBR:savedDebugOnError = V_debugOnError - Variable/G root:Packages:ZBR:savedDebugOnAbort = V_debugOnAbort - Variable/G root:Packages:ZBR:savedDebugNvarCheck = V_NVAR_SVAR_WAVE_Checking + variable/G root:Packages:ZBR:savedDebugEnable = V_enable + variable/G root:Packages:ZBR:savedDebugOnError = V_debugOnError + variable/G root:Packages:ZBR:savedDebugOnAbort = V_debugOnAbort + variable/G root:Packages:ZBR:savedDebugNvarCheck = V_NVAR_SVAR_WAVE_Checking KillVariables/Z V_enable, V_debugOnError, V_debugOnAbort, V_NVAR_SVAR_WAVE_Checking restore = "DebuggerOptions enable=root:Packages:ZBR:savedDebugEnable, " @@ -364,9 +364,9 @@ End Function ZBR_FinishToken(variable idx) variable err - string errMsg + string errMsg - DFREF dfr = root:Packages:ZBR + DFREF dfr = root:Packages:ZBR WAVE done = dfr:done WAVE/T resultText = dfr:resultText WAVE historyStart = dfr:historyStart @@ -397,7 +397,7 @@ Function [variable isDone, string result] ZBR_PollCommand(string token) variable idx - DFREF dfr = root:Packages:ZBR + DFREF dfr = root:Packages:ZBR WAVE done = dfr:done WAVE/T resultText = dfr:resultText @@ -473,14 +473,70 @@ End /// error reported anywhere). Poll ZBR_IsCompiled() instead -- already a direct, /// synchronous, standalone check that doesn't depend on anything surviving the /// recompile -- to find out when this has taken effect. +/// +/// **User-identified crash hypothesis, now addressed here**: this bridge has hit +/// multiple unexplained `EXCEPTION_ACCESS_VIOLATION` crashes deep inside Igor64.exe +/// itself (confirmed via crash-dump analysis, see SESSION_NOTES.md) coinciding with +/// COMPILEPROCEDURES, with no root cause ever identified. `igortest-tracing.ipf`'s own +/// `CompileAndRestart()` runs the exact same RELOAD-CHANGED-PROCS/COMPILEPROCEDURES +/// pair reliably, with no crashes ever observed -- the key structural difference +/// (per direct user review) is that nothing else can call into Igor between +/// `CompileAndRestart()` running and Igor itself firing `AfterCompiledHook`, whereas +/// this bridge's ZeroMQ-XOP runs "a threaded message handler" (its own help file's +/// wording, ZeroMQ.ihf) that keeps dispatching incoming CallFunction requests in the +/// background regardless of what Igor's main thread is doing. If a new CallFunction +/// request -- including this bridge's own compile-status polling, or any other tool +/// call that happens to be in flight -- gets dispatched while Igor's main thread is +/// mid-COMPILEPROCEDURES (tearing down and rebuilding its own internal +/// compiled-function/symbol tables), that's a genuine cross-thread race on those very +/// tables, and a bad-pointer read deep inside Igor64.exe (exactly what both crash dumps +/// showed) is a very plausible symptom. +/// +/// Fix: stop the ZeroMQ handler (`zeromq_handler_stop()`, queued via +/// ZBR_StopHandlerBeforeRecompile so it runs only after THIS call's own reply has +/// already gone out -- see that function's docstring) before RELOAD CHANGED +/// PROCS/COMPILEPROCEDURES ever run, so nothing can be dispatched into Igor while +/// it's mid-recompile. AfterCompiledHook's existing (unchanged, synchronous) +/// ZBR_EnsureZeroMQBound() call restarts the handler once compilation has actually +/// finished. This is a mitigation based on a well-reasoned but not 100%-certain +/// mechanism (Igor64.exe ships no public symbols, so the exact fault can't be proven +/// from here) -- see SESSION_NOTES.md for the full reasoning and its honest +/// limitations. Function ZBR_SubmitReloadAndCompile() + Execute/P/Q/Z "ZBR#ZBR_StopHandlerBeforeRecompile()" Execute/P/Q/Z "RELOAD CHANGED PROCS " Execute/P/Q/Z "COMPILEPROCEDURES " return 0 End +/// Stops this module's ZeroMQ message handler thread (zeromq_handler_stop()) -- queued +/// as its own independent Execute/P entry by ZBR_SubmitReloadAndCompile, deliberately +/// BEFORE RELOAD CHANGED PROCS/COMPILEPROCEDURES, so no new CallFunction request can be +/// dispatched into Igor while it's mid-recompile. See ZBR_SubmitReloadAndCompile's own +/// docstring for the full crash-hypothesis reasoning this targets. +/// +/// Deliberately NOT called directly/synchronously from inside ZBR_SubmitReloadAndCompile +/// itself -- that function's own invocation is, in the end, just another CallFunction +/// request being served by the very same handler this stops. Queuing the stop via +/// Execute/P instead guarantees it only actually runs after Igor has returned to idle, +/// i.e. after THIS call's own reply has already been sent back over ZeroMQ -- avoiding +/// any risk of the handler being stopped out from under its own in-flight response. +/// +/// Does not call zeromq_stop() (which would tear down every ZeroMQ bind/connection for +/// the whole Igor Pro instance, not just this module's own -- see ZBR_EnsureZeroMQBound's +/// docstring for why that's avoided elsewhere too) -- zeromq_handler_stop() is the +/// narrower, paired stop for zeromq_handler_start(), per ZeroMQ.ihf. +Function ZBR_StopHandlerBeforeRecompile() + + variable err + + zeromq_handler_stop(); err = GetRTError(1) + + return 0 +End + // --- Debugger control ---------------------------------------------------------------- /// Direct (non-deferred) call -- confirmed live that DebuggerOptions, unlike @@ -733,7 +789,7 @@ Function/S ZBR_ReadHelpFile(string filePath, string tmpHtmlPath) endif restoreFailures = "" - n = ItemsInList(helpAll) + n = ItemsInList(helpAll) for(i = 0; i < n; i += 1) name = StringFromList(i, helpAll) resolvedPath = ZBR_ResolveHelpFilePath(name) @@ -779,6 +835,14 @@ End /// later line would risk popping the Debugger window here, which -- same as every other /// popup this session has hit -- has no scriptable dismissal and would hang unattended /// operation. +/// +/// Called synchronously and directly from AfterCompiledHook (not deferred) -- confirmed +/// via user review that this part of the design is fine as-is. See +/// ZBR_StopHandlerBeforeRecompile's docstring instead for the actual fix targeting the +/// crashes this bridge has hit coinciding with COMPILEPROCEDURES: the real hazard is on +/// the OTHER side of the recompile window (a live ZeroMQ handler thread able to dispatch +/// a new CallFunction request WHILE Igor is mid-recompile), not anything happening here +/// after compilation has already finished successfully. static Function ZBR_EnsureZeroMQBound() variable err @@ -805,7 +869,13 @@ static Function AfterCompiledHook() // Make this module's ZeroMQ server listen again immediately after every compile -- // the whole point of running this from AfterCompiledHook rather than requiring a - // separate manual step each time the code changes. + // separate manual step each time the code changes. Called directly/synchronously + // (not deferred) -- confirmed via user review that this is fine: by the time + // AfterCompiledHook runs, compilation has already finished successfully, so there + // is nothing left to race against here. See ZBR_StopHandlerBeforeRecompile's + // docstring for the actual fix targeting this bridge's COMPILEPROCEDURES-adjacent + // crashes -- the real hazard is upstream of this point (a live handler able to + // dispatch a new CallFunction request while Igor is still mid-recompile). ZBR_EnsureZeroMQBound() // Bare Variable/G (no initializer) is safe to call unconditionally: per Igor diff --git a/tools/igor-mcp-bridge/igor-pro-bridge-2.2.3.mcpb b/tools/igor-mcp-bridge/igor-pro-bridge-2.2.3.mcpb deleted file mode 100644 index 9f92980c8f6ac16c2f2e5f25506e21d328de7d0c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33896 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 E0AvGivH$=8 diff --git a/tools/igor-mcp-bridge/igor-pro-bridge-2.3.0.mcpb b/tools/igor-mcp-bridge/igor-pro-bridge-2.3.0.mcpb new file mode 100644 index 0000000000000000000000000000000000000000..4cea084888acfba4694188610cf02e7a13685ff3 GIT binary patch literal 35142 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%0DMiIc-NS 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 K0J?|(0000{` command line while the old process is still alive (even mid-shutdown) doesn't spawn a new process at all; Windows/Igor's single-instance-per-user behavior instead either redirects the load into the still-live old instance (risking an unhandled \"save changes?\" dialog) or silently drops the request if that instance is already mid-quit. Fixed by polling the actual OS process list for the configured executable's own process to fully exit before ever launching the replacement, raising a clear error instead of proceeding if it doesn't exit in time. (3) `read_help_file`: added a `timeout_ms` parameter (default raised from this bridge's usual 5s to 30s) -- confirmed live that exporting a genuinely large help file (e.g. the full \"Igor Reference.ihf\") can take longer than 5 seconds, which previously timed out the call even though the export eventually succeeded Igor-side anyway.\n\nv2.0.0: BREAKING transport change. Replaces the COM Automation Server transport (win32com.client, ProgID `IgorPro.Application`) with the ZeroMQ-XOP's `CallFunction` JSON protocol over a plain localhost TCP socket (tcp://127.0.0.1:5680), talking to a small set of Igor-side helper functions in `Packages/MIES/ZMQ_BridgeHelpers.ipf` (the `ZBR` independent module).\n\nWhy: COM required this bridge process and Igor Pro to run at the SAME Windows privilege level (both elevated, or both not) -- an easy-to-miss mismatch (e.g. Claude Desktop reopened normally after Igor Pro was left running elevated from before). ZeroMQ is a plain TCP socket with no such requirement at all -- elevation no longer matters in any way. `launch_igor_pro_unattended` no longer has an elevation-branching code path: it always launches Igor Pro as a plain, non-elevated child process, at whatever privilege level this bridge process itself runs at.\n\n**New setup requirement**: unlike the COM transport (which worked against a stock Igor Pro installation with zero custom procedure code), this transport requires `Packages/MIES/ZMQ_BridgeHelpers.ipf` to be `#include`-d and compiled into whichever Igor Pro experiment this bridge talks to -- there is no bootstrap path over ZeroMQ itself. This repo's own `Packages/MIES_Include.ipf` already does this permanently. Any OTHER Igor Pro experiment needs `ZMQ_BridgeHelpers.ipf` copied onto its own procedure search path with a matching `#include` added by hand, then a recompile -- see `Packages/doc/igor-pro-bridge.rst` (\"Installation\") for the exact one-time steps.\n\n**Capability improvements**: `get_wave` now supports any wave dimensionality (up to 4D), real and complex numeric waves, text waves, and wave-reference waves -- the entire wave comes back from one round trip, natively serialized (the old COM version was limited to 1D real-valued waves, read one point at a time).\n\n**Behavior changes to be aware of**: `execute_igor_command`/`execute_igor_command_unattended` can no longer separate fprintf-only output from the full echoed history the way COM's `Execute2` could -- both \"results\" and \"history\" now hold the same captured text; prefix a command with `Silent 1;` to suppress the echo. `load_experiment` no longer hot-swaps the open experiment in place (that was a COM-only method with no procedure-language equivalent) -- it now quits the running instance and relaunches Igor Pro with the target file path as a launch argument, a real process restart; unsaved changes in the previously-open experiment are lost. `ensure_igor_pro_bridge_defined` is retired -- it existed to manage a `#ifdef IGOR_PRO_BRIDGE` gating convention that ZBR's always-on independent-module architecture doesn't use.\n\nSee `Packages/doc/igor-pro-bridge.rst` and `SESSION_NOTES.md` in the MIES repository for the full protocol research, design decisions, and confirmed-live test results behind this rewrite.\n\nTools:\n- `execute_igor_command` / `execute_igor_command_unattended`: run a command string on Igor's command line (submitted via `ZBR_SubmitCommand`/`ZBR_SubmitCommandUnattended`, polled via `ZBR_PollCommand`); include a `print` call (NOT `fprintf 0, ...` -- confirmed not captured, see v2.0.1 changelog above) to get data back. Both return `{results, history}`. The `_unattended` variant automatically disables/restores Igor's Debugger around the call.\n- `read_session_history`: read back everything sent to Igor's history area since this bridge's capture started.\n- `get_wave`: read any Igor wave's full data and metadata back (any dimensionality, numeric/complex/text/wave-reference).\n- `load_experiment`: quit the running instance and relaunch Igor Pro with a .pxp file path as a launch argument.\n- `check_bridge_health`: diagnose whether this bridge can reach Igor Pro's ZeroMQ server right now.\n- `check_compilation_state` / `reload_and_compile_procedures`: check and refresh Igor's compiled state after editing a `.ipf` file on disk.\n- `dismiss_compile_error_dialog`: close a stuck \"Function Compilation Error\" dialog via a posted Escape key, without needing OS focus.\n- `get_debugger_state` / `set_debugger_enabled` / `restore_debugger_settings`: read, change, and restore Igor's Debugger settings.\n- `get_environment_summary`: summarize the live instance (version, loaded experiment, XOPs, included procedure files, data folders, Debugger settings).\n- `read_help_file`: read an Igor Pro help file (.ihf) as structured, formatted text.\n- `get_bridge_version`: report the running bridge version and Python/package versions.\n- `configure_igor_launch` / `launch_igor_pro_unattended`: record the Igor Pro executable path once per session, then launch it with the `/UNATTENDED` flag and wait for it to become reachable over ZeroMQ.\n\n**Requirements:**\n- Igor Pro 9.00 or later, running on Windows, with the ZeroMQ-XOP installed and loaded.\n- `Packages/MIES/ZMQ_BridgeHelpers.ipf` (or a copy of it) `#include`-d and compiled into the target experiment -- see `Packages/doc/igor-pro-bridge.rst` (\"Installation\") for the one-time setup steps for experiments other than this repo's own.\n- Python (accessible as `python` on PATH), with the pinned packages in `requirements.txt` installed into that same environment. Run `install.ps1` to do this correctly and also complete pywin32's required post-install step (still needed for window-handling/process-launch helpers, even though this bridge no longer uses COM) -- see that script's own help (`Get-Help ./install.ps1 -Full`).\n- No elevation/privilege-matching requirement of any kind -- this is the change v2.0.0 makes.\n\nPrior COM-based history (v1.x): see `Packages/doc/igor-pro-bridge.rst` for the full v1.x changelog, retained there for reference.", + "long_description": "v2.3.0: investigated the long-standing, previously-unexplained crash pattern where Igor Pro itself (not this bridge) would sometimes become unreachable shortly after `reload_and_compile_procedures`. Two Windows crash dumps captured during this bridge's development were analyzed with Python's `minidump` package (Igor's own crash reporter provides no stack trace), confirming both were genuine `EXCEPTION_ACCESS_VIOLATION`s deep inside `Igor64.exe` itself, not in this bridge's code or any XOP. A likely mechanism was then identified, based directly on a comparison the repo owner suggested against this repo's own `igortest-tracing.ipf`, whose `CompileAndRestart()`/`AfterCompiledHook()` pattern reloads and compiles procedures reliably with no crash: unlike that reference pattern, this bridge's ZeroMQ-XOP handler runs as a background thread (documented in `HelpFiles/ZeroMQ.ihf` as \"a threaded message handler\") that keeps dispatching incoming `CallFunction` requests regardless of what Igor's main thread is doing -- so a request arriving while `COMPILEPROCEDURES` is mid-rebuild of Igor's own internal function/symbol tables is a plausible cross-thread race, distinct from anything happening inside `AfterCompiledHook` itself (which runs safely afterward, exactly like `igortest`'s hook). Fixed by adding `ZBR_StopHandlerBeforeRecompile()` (calls `zeromq_handler_stop()`) and queuing it as the FIRST deferred `Execute/P` entry in `ZBR_SubmitReloadAndCompile()`, ahead of `RELOAD CHANGED PROCS`/`COMPILEPROCEDURES`; the handler is restarted afterward via the existing, unchanged `AfterCompiledHook` -> `ZBR_EnsureZeroMQBound()` synchronous call. Confirmed live: recompiled successfully afterward, then ran three CONCURRENT `reload_and_compile_procedures()` calls as a stress test -- all three returned `compiled: true` cleanly with no crash, and `check_bridge_health()`/`execute_igor_command()` both worked correctly afterward. Honest caveat: this is a well-reasoned mitigation, not a proven fix -- `Igor64.exe` ships no public symbols, so the exact fault can't be confirmed from here, and the crash was already rare/nondeterministic before this fix. No Python-side (`server.py`) logic changes were needed beyond an updated docstring; the fix is entirely within `ZMQ_BridgeHelpers.ipf`.\n\nv2.2.3: fixed another uncaught-runtime-error-pops-a-dialog bug, this time in `ZBR_FinishToken`, found while cleaning up leftover test-token rows from this bridge's own `root:Packages:ZBR` storage waves. `ZBR_AllocateToken` captures a token's row index (`idx`) at submission time, but `ZBR_FinishToken` -- the callback that actually writes the result and flips the done flag -- runs later, in its own separate deferred `Execute/P` entry (per the v2.2.0 fix). If the `done`/`resultText`/`historyStart` waves are resized smaller in between (e.g. maintenance code clearing out old tokens, exactly as happened live during this session's own cleanup), `idx` can end up pointing past the end of the now-shorter waves. `ZBR_FinishToken` had no bounds check, so writing to `resultText[idx]` in that state threw an uncaught \"Index out of range for wave...\" runtime error -- which, like the v2.2.1 bug, pops a real modal dialog and blocks Igor's entire main thread until a human dismisses it. Fixed by bounds-checking `idx` against `DimSize(done, 0)` before writing anything; if the row no longer exists, the callback now just silently does nothing (there is nothing useful left to recover), and `ZBR_PollCommand`'s own existing bounds check already reports a clean `\"ERROR: unknown token\"` response to the caller instead of a hang. Confirmed live: reproduced the exact original crash (resizing the storage waves to 0 rows while a submitted command's own token was still in flight, which had just thrown the dialog moments earlier during this session's testing), then repeated it after the fix -- this time it returned `\"ERROR: unknown token ...\"` cleanly with no dialog and no hang, and `check_bridge_health()` stayed `OK` throughout. No Python-side changes were needed.\n\nv2.2.2: minor robustness refinement to the v2.2.1 fix, contributed directly by the repo owner after installing it. `ZBR_EnsureCaptureStarted`'s stale-refnum probe (`CaptureHistory(refnumRW, 0)`) and its `AbortOnRTE` were originally written on separate lines; moved onto the SAME line. Reason: Igor's Debug on Error check happens at the END of each line, not each statement -- if the two were on separate lines, a user with Debug on Error enabled (unusual, but this bridge doesn't force the Debugger off during a raw `execute_igor_command`/`submit_igor_command` call the way the `_unattended` variants do) would get a Debugger popup right when the stale refnum's runtime error occurred, before `AbortOnRTE` ever got a chance to convert it into a catchable abort -- defeating the whole point of the v2.2.1 fix in that one specific configuration. Confirmed live both before accepting this change (by temporarily enabling `debug_on_error` via `set_debugger_enabled` and repeating the deliberately-corrupted-refnum test) and after: with the two statements on one line, the corrupted-refnum scenario completes cleanly with no Debugger popup and no hang even with Debug on Error active, and the refnum still self-heals correctly. No other behavior change.\n\nv2.2.1: fixed a bug where `load_experiment` (or a user manually reopening a saved .pxp) could leave the bridge completely unreachable, requiring a human to dismiss a modal Igor error dialog. Root cause: this bridge's history-capture mechanism stores its `CaptureHistoryStart()` reference number in a plain `Variable/G` global (`root:Packages:ZBR:captureRefNum`), which Igor persists into a saved experiment like any other global -- but that refnum is only meaningful within the OS process that created it. Reloading a saved experiment brings the OLD numeric value back even though the process is brand new; the prior code only checked that the global *existed*, not whether its value was still a live capture, so it trusted the stale refnum. Using it then threw a genuine Igor runtime error (\"there is no open file with this reference number\") from a plain top-level `Execute/P` entry -- not caught anywhere -- which popped a real modal dialog and blocked Igor's entire main thread (and therefore every ZeroMQ reply) until a human dismissed it. Confirmed live by saving and reloading an experiment via `load_experiment` and then calling `execute_igor_command`, which reproduced the dialog exactly. Fixed in `ZBR_EnsureCaptureStarted` (`ZMQ_BridgeHelpers.ipf`): the stored refnum is now validated with a `try`/`catch`/`endtry`+`AbortOnRTE` block before being trusted, and silently replaced with a fresh `CaptureHistoryStart()` call if it's stale -- confirmed live afterward, both by deliberately corrupting the refnum to a bogus value and by repeating the exact save-then-`load_experiment`-then-`execute_igor_command` sequence that originally triggered the dialog; neither reproduced the dialog and both recovered silently. (Two syntax mistakes were made and caught along the way while implementing this: a runtime error inside a `try` block does not by itself jump to `catch` without an explicit `AbortOnRTE` immediately after the risky call, and a bare `return` with no value is invalid inside a plain, implicit-`Variable`-returning `Function` -- both confirmed from Igor's own bundled help, \"Flow Control for Aborts\" and \"The Return Statement\" in `Programming.ihf`.) No Python-side (`server.py`) changes were needed for this fix -- it is entirely within the Igor-side `ZMQ_BridgeHelpers.ipf` helper file.\n\nv2.2.0: fixed a serious reliability bug in the v2.1.0 submit/poll primitives, found via live testing of the exact scenario they exist for (a command whose runtime is unknown and could be very long). `ZBR_SubmitCommand`/`ZBR_SubmitCommandUnattended` (Igor-side, in `ZMQ_BridgeHelpers.ipf`) previously joined the submitted command and the internal finish-callback into ONE compound `Execute/P` string (`cmd + \"; \" + finishCall`). Confirmed live: if `cmd` failed to parse, OR hit a genuine Igor-level runtime error partway through (not just a Debugger pause), the ENTIRE joined string aborted, so the appended finish-callback never ran -- leaving `poll_igor_command` reporting `\"done\": false` forever, indistinguishable from a job still genuinely in progress. This directly undermines the reliability guarantee these tools exist to provide. Root-caused via two live tests: (1) queuing an invalid command and a valid one as two SEPARATE `Execute/P` entries showed the second still ran fine despite the first failing, proving Igor's deferred-operation queue processes independently-queued entries regardless of a prior one's failure; (2) the same joined-string command that had a genuine runtime error (not just an invalid command name) also silently swallowed its own appended print statement, confirming the bug applies to runtime errors too, not just parse errors. Fixed by queuing `cmd` and the finish-callback (and, for the `_unattended` variant, a Debugger-disable step and a Debugger-restore step, with the saved settings carried across via persistent globals) as fully independent `Execute/P` entries instead of one joined string -- verified live afterward with an invalid command, a genuine runtime-erroring command, and a normal command, all three now correctly reaching `\"done\": true` instead of hanging. **Known residual limitation, also confirmed live:** there is currently no generic way to tell \"the command ran and legitimately printed nothing\" apart from \"the command errored out with no output\" -- both now come back as `\"done\": true` with an empty/short result and no error indication (an attempted fix using `GetRTError()` in the finish-callback was tested and found not to work: each `Execute/P` entry is dispatched as its own independent top-level execution, so Igor clears any pending runtime-error state before the next queued entry runs). If a submitted command's success needs to be verifiable, have it `print` an explicit sentinel/result value itself.\n\nv2.1.0: added first-class support for commands with an unknown or very long runtime (hours to weeks). `execute_igor_command`/`execute_igor_command_unattended` block the whole MCP tool call while polling, which cannot work for a genuinely long calculation -- the MCP transport itself has been observed to time out a single tool call well under a minute, regardless of the requested `timeout_seconds`. Three new tools expose the existing submit/poll primitives directly: `submit_igor_command`/`submit_igor_command_unattended` queue a command and return a token immediately with no waiting at all, and `poll_igor_command(token)` performs one cheap, instant check for completion, callable any number of times spaced arbitrarily far apart. This is reliable no matter how long the job runs or how many times this bridge process or Claude Desktop itself restarts in between, because all of the actual state (a done flag and the captured text) lives entirely in Igor Pro's own data waves (`root:Packages:ZBR`), not in this bridge's Python process -- the only thing that actually ends the job is Igor Pro itself quitting, crashing, or restarting. As with the existing `_unattended` tools, use `submit_igor_command_unattended` for anything long-running: a Debugger pause partway through leaves `poll_igor_command` reporting \"not done\" forever, indistinguishable from the command still genuinely running.\n\nv2.0.1: three bug fixes found during live v2.0.0 testing. (1) `execute_igor_command`/`execute_igor_command_unattended`: the documented pattern of using `fprintf 0, ...` to get data back does not actually work over this transport -- confirmed live that `CaptureHistory` (which this bridge's capture mechanism relies on) captures `print` output but never captures `fprintf`-to-history output at all (refnum 0, -1, or -2 all silently produce nothing, even though the command itself runs without error). Use `print` instead; all docs/docstrings updated accordingly. (2) `load_experiment`: fixed a silent-failure bug where relaunching Igor Pro with a new experiment file could do nothing at all, with no error reported. Root cause (diagnosed live): the tool was waiting for Igor Pro to stop answering over ZeroMQ as its signal that the old process had quit, but ZeroMQ goes quiet well before the underlying Igor64.exe process actually terminates -- launching the replacement `Igor64.exe /UNATTENDED ` command line while the old process is still alive (even mid-shutdown) doesn't spawn a new process at all; Windows/Igor's single-instance-per-user behavior instead either redirects the load into the still-live old instance (risking an unhandled \"save changes?\" dialog) or silently drops the request if that instance is already mid-quit. Fixed by polling the actual OS process list for the configured executable's own process to fully exit before ever launching the replacement, raising a clear error instead of proceeding if it doesn't exit in time. (3) `read_help_file`: added a `timeout_ms` parameter (default raised from this bridge's usual 5s to 30s) -- confirmed live that exporting a genuinely large help file (e.g. the full \"Igor Reference.ihf\") can take longer than 5 seconds, which previously timed out the call even though the export eventually succeeded Igor-side anyway.\n\nv2.0.0: BREAKING transport change. Replaces the COM Automation Server transport (win32com.client, ProgID `IgorPro.Application`) with the ZeroMQ-XOP's `CallFunction` JSON protocol over a plain localhost TCP socket (tcp://127.0.0.1:5680), talking to a small set of Igor-side helper functions in `Packages/MIES/ZMQ_BridgeHelpers.ipf` (the `ZBR` independent module).\n\nWhy: COM required this bridge process and Igor Pro to run at the SAME Windows privilege level (both elevated, or both not) -- an easy-to-miss mismatch (e.g. Claude Desktop reopened normally after Igor Pro was left running elevated from before). ZeroMQ is a plain TCP socket with no such requirement at all -- elevation no longer matters in any way. `launch_igor_pro_unattended` no longer has an elevation-branching code path: it always launches Igor Pro as a plain, non-elevated child process, at whatever privilege level this bridge process itself runs at.\n\n**New setup requirement**: unlike the COM transport (which worked against a stock Igor Pro installation with zero custom procedure code), this transport requires `Packages/MIES/ZMQ_BridgeHelpers.ipf` to be `#include`-d and compiled into whichever Igor Pro experiment this bridge talks to -- there is no bootstrap path over ZeroMQ itself. This repo's own `Packages/MIES_Include.ipf` already does this permanently. Any OTHER Igor Pro experiment needs `ZMQ_BridgeHelpers.ipf` copied onto its own procedure search path with a matching `#include` added by hand, then a recompile -- see `Packages/doc/igor-pro-bridge.rst` (\"Installation\") for the exact one-time steps.\n\n**Capability improvements**: `get_wave` now supports any wave dimensionality (up to 4D), real and complex numeric waves, text waves, and wave-reference waves -- the entire wave comes back from one round trip, natively serialized (the old COM version was limited to 1D real-valued waves, read one point at a time).\n\n**Behavior changes to be aware of**: `execute_igor_command`/`execute_igor_command_unattended` can no longer separate fprintf-only output from the full echoed history the way COM's `Execute2` could -- both \"results\" and \"history\" now hold the same captured text; prefix a command with `Silent 1;` to suppress the echo. `load_experiment` no longer hot-swaps the open experiment in place (that was a COM-only method with no procedure-language equivalent) -- it now quits the running instance and relaunches Igor Pro with the target file path as a launch argument, a real process restart; unsaved changes in the previously-open experiment are lost. `ensure_igor_pro_bridge_defined` is retired -- it existed to manage a `#ifdef IGOR_PRO_BRIDGE` gating convention that ZBR's always-on independent-module architecture doesn't use.\n\nSee `Packages/doc/igor-pro-bridge.rst` and `SESSION_NOTES.md` in the MIES repository for the full protocol research, design decisions, and confirmed-live test results behind this rewrite.\n\nTools:\n- `execute_igor_command` / `execute_igor_command_unattended`: run a command string on Igor's command line (submitted via `ZBR_SubmitCommand`/`ZBR_SubmitCommandUnattended`, polled via `ZBR_PollCommand`); include a `print` call (NOT `fprintf 0, ...` -- confirmed not captured, see v2.0.1 changelog above) to get data back. Both return `{results, history}`. The `_unattended` variant automatically disables/restores Igor's Debugger around the call.\n- `read_session_history`: read back everything sent to Igor's history area since this bridge's capture started.\n- `get_wave`: read any Igor wave's full data and metadata back (any dimensionality, numeric/complex/text/wave-reference).\n- `load_experiment`: quit the running instance and relaunch Igor Pro with a .pxp file path as a launch argument.\n- `check_bridge_health`: diagnose whether this bridge can reach Igor Pro's ZeroMQ server right now.\n- `check_compilation_state` / `reload_and_compile_procedures`: check and refresh Igor's compiled state after editing a `.ipf` file on disk.\n- `dismiss_compile_error_dialog`: close a stuck \"Function Compilation Error\" dialog via a posted Escape key, without needing OS focus.\n- `get_debugger_state` / `set_debugger_enabled` / `restore_debugger_settings`: read, change, and restore Igor's Debugger settings.\n- `get_environment_summary`: summarize the live instance (version, loaded experiment, XOPs, included procedure files, data folders, Debugger settings).\n- `read_help_file`: read an Igor Pro help file (.ihf) as structured, formatted text.\n- `get_bridge_version`: report the running bridge version and Python/package versions.\n- `configure_igor_launch` / `launch_igor_pro_unattended`: record the Igor Pro executable path once per session, then launch it with the `/UNATTENDED` flag and wait for it to become reachable over ZeroMQ.\n\n**Requirements:**\n- Igor Pro 9.00 or later, running on Windows, with the ZeroMQ-XOP installed and loaded.\n- `Packages/MIES/ZMQ_BridgeHelpers.ipf` (or a copy of it) `#include`-d and compiled into the target experiment -- see `Packages/doc/igor-pro-bridge.rst` (\"Installation\") for the one-time setup steps for experiments other than this repo's own.\n- Python (accessible as `python` on PATH), with the pinned packages in `requirements.txt` installed into that same environment. Run `install.ps1` to do this correctly and also complete pywin32's required post-install step (still needed for window-handling/process-launch helpers, even though this bridge no longer uses COM) -- see that script's own help (`Get-Help ./install.ps1 -Full`).\n- No elevation/privilege-matching requirement of any kind -- this is the change v2.0.0 makes.\n\nPrior COM-based history (v1.x): see `Packages/doc/igor-pro-bridge.rst` for the full v1.x changelog, retained there for reference.", "author": { "name": "Michael Huth" }, diff --git a/tools/igor-mcp-bridge/server.py b/tools/igor-mcp-bridge/server.py index 327b65c8b0..b7df7e769d 100644 --- a/tools/igor-mcp-bridge/server.py +++ b/tools/igor-mcp-bridge/server.py @@ -704,16 +704,27 @@ def reload_and_compile_procedures() -> dict: Use this after editing a .ipf file directly on disk. Only call this while Igor Pro is not currently running other procedure code. - **Caution, carried over from the COM-based version and confirmed to still apply**: - Igor Pro has been observed becoming unreachable shortly after a reload/compile - attempt on more than one occasion during this bridge's development (crashed or - was closed), root cause unconfirmed. If a tool call after this one starts failing, + **Caution, carried over from the COM-based version**: Igor Pro has been observed + becoming unreachable shortly after a reload/compile attempt on more than one + occasion during this bridge's development (crashed or was closed). As of v2.3.0, + crash-dump analysis traced this to a genuine EXCEPTION_ACCESS_VIOLATION deep inside + Igor64.exe itself (not this bridge's own code), and a likely mechanism was + identified: this bridge's ZeroMQ handler runs as a background thread that keeps + dispatching incoming CallFunction requests regardless of what Igor's main thread is + doing, so a request arriving while COMPILEPROCEDURES is mid-rebuild of Igor's own + internal function/symbol tables is a plausible cross-thread race. v2.3.0 mitigates + this by stopping the ZeroMQ handler before RELOAD CHANGED PROCS/COMPILEPROCEDURES + run and restarting it only after compilation finishes (see + ZBR_StopHandlerBeforeRecompile/ZBR_SubmitReloadAndCompile in ZMQ_BridgeHelpers.ipf). + This is a well-reasoned mitigation, not a proven fix -- Igor64.exe ships no public + symbols, so the exact fault can't be confirmed from here, and the crash was already + rare/nondeterministic. If a tool call after this one starts failing anyway, check_bridge_health() and be prepared for Igor Pro to need relaunching. - Mechanism: calls ZBR_SubmitReloadAndCompile(), which queues - `Execute/P "RELOAD CHANGED PROCS "` then `Execute/P "COMPILEPROCEDURES "` - Igor-side (both commands need their queue, and their own mandatory trailing - space -- see that function's docstring in ZMQ_BridgeHelpers.ipf), then polls for + Mechanism: calls ZBR_SubmitReloadAndCompile(), which queues (as three independent + Execute/P entries) a call to stop the ZeroMQ handler, then + `Execute/P "RELOAD CHANGED PROCS "`, then `Execute/P "COMPILEPROCEDURES "` + Igor-side (see that function's docstring in ZMQ_BridgeHelpers.ipf), then polls for completion using two independent signals, same as the COM-based version did: 1. root:gClaudeHelperCompileCounter (ZBR_ReadCompileCounter), bumped by @@ -1109,7 +1120,7 @@ def read_help_file(file_path: str, timeout_ms: int = 30000) -> dict: # reason as before: the on-disk layout after Claude Desktop installs a .mcpb isn't # guaranteed to keep server.py and manifest.json at a fixed relative path, and this # needs to be confirmable from inside a conversation independent of that. -_BRIDGE_VERSION = "2.2.3" +_BRIDGE_VERSION = "2.3.0" def _installed_package_version(distribution_name: str) -> str | None: From 9db2f0b81388dd195eba2b887cd80998c12343c6 Mon Sep 17 00:00:00 2001 From: Michael Huth Date: Thu, 6 Aug 2026 03:06:51 +0200 Subject: [PATCH 11/12] MCP: v2.3.1 fix issue that Igor Bridge could get unreachable - v2.3.0 had an issue that with non compiling code in ProcGlobal the bridge could get unreachable. --- Packages/MIES/MIES_ClaudeHelper.ipf | 48 +----- Packages/doc/igor-pro-bridge.rst | 67 +++++++- tools/igor-mcp-bridge/ZMQ_BridgeHelpers.ipf | 155 +++++++++++++++++- .../igor-pro-bridge-2.3.0.mcpb | Bin 35142 -> 0 bytes .../igor-pro-bridge-2.3.1.mcpb | Bin 0 -> 37267 bytes tools/igor-mcp-bridge/manifest.json | 4 +- tools/igor-mcp-bridge/server.py | 87 ++++++++-- 7 files changed, 286 insertions(+), 75 deletions(-) delete mode 100644 tools/igor-mcp-bridge/igor-pro-bridge-2.3.0.mcpb create mode 100644 tools/igor-mcp-bridge/igor-pro-bridge-2.3.1.mcpb diff --git a/Packages/MIES/MIES_ClaudeHelper.ipf b/Packages/MIES/MIES_ClaudeHelper.ipf index 40e34c483f..c8c46b5a50 100644 --- a/Packages/MIES/MIES_ClaudeHelper.ipf +++ b/Packages/MIES/MIES_ClaudeHelper.ipf @@ -48,52 +48,6 @@ static Constant CH_CSTRING_SEARCH_INITIAL_CHUNK = 256 // initial static Constant CH_CSTRING_SEARCH_MAX_CHUNK = 8192 // give up (return -1) once search size exceeds this many bytes static Constant CH_CMPSTR_CASE_SENSITIVE = 1 // CmpStr's caseSensitive flag (1 = case-sensitive, per Igor Reference) -/// AfterCompiledHook() is a predefined Igor hook: Igor calls it after ALL procedure -/// windows have compiled successfully (confirmed from Igor Pro Folder/Igor Help -/// Files/Advanced Topics.ihf). It is declared static so it coexists with any other -/// file's own static AfterCompiledHook() (e.g. the one in MIES_Include.ipf used only -/// for the too-old-Igor warning panel) without colliding. -/// -/// It records a monotonically increasing counter in root:gClaudeHelperCompileCounter -/// each time it fires. This gives the Igor Pro Bridge a compile confirmation -/// driven by Igor itself, rather than only inferred by polling FunctionInfo() for a -/// non-existing function -- which can read stale state before Igor's operation queue -/// (RELOAD CHANGED PROCS / COMPILEPROCEDURES) has actually drained. There is no -/// equivalent Igor hook for a *failed* compile, so this only helps confirm success, -/// not detect failure. - -static Function AfterCompiledHook() - - variable modifiedBefore - - // Creating/incrementing a global marks the experiment as modified, same as any - // other data change. Captured/restored here so this hook never flips an - // otherwise-unmodified experiment to modified, matching the existing convention - // in MIES_IgorHooks.ipf's own AfterCompiledHook -- flagged by a Copilot PR - // review as a real risk otherwise: an experiment spuriously marked modified can - // trigger a "Save changes?" prompt later, which is exactly the kind of dialog - // this bridge (built around unattended operation) cannot dismiss remotely. - ExperimentModified - modifiedBefore = V_flag - - // Bare Variable/G (no initializer) is safe to call unconditionally: per Igor - // Reference.ihf, /G "overwrites any existing variable" but "the variable is - // initialized when it is created if you supply the initial value" -- i.e. the - // overwrite-to-a-value only happens when an initializer is given. Without one, - // this creates the global at 0 the first time and leaves an existing value - // alone on every call after that, so no NVAR_Exists guard is needed. - variable/G root:gClaudeHelperCompileCounter - NVAR gClaudeHelperCompileCounter = root:gClaudeHelperCompileCounter - - gClaudeHelperCompileCounter += 1 - - if(!modifiedBefore) - ExperimentModified 0 - endif - - return 0 -End - /// CH_ListXOPExports(xopPath) reads the operations and functions a compiled .xop /// file (a Windows DLL) adds to Igor, straight from the file's own bytes -- no /// vendor documentation or source needed, and no live Igor Pro instance needs to @@ -228,7 +182,7 @@ static Function CH_PERVAToFileOffset(variable refNum, variable sectionTableOffse secVirtSize = CH_PEReadU32(refNum, secPos + CH_PE_SECTION_VIRTUAL_SIZE_OFFSET) secVA = CH_PEReadU32(refNum, secPos + CH_PE_SECTION_VIRTUAL_ADDRESS_OFFSET) secRawPtr = CH_PEReadU32(refNum, secPos + CH_PE_SECTION_RAW_DATA_PTR_OFFSET) - if(rva >= secVA && rva < secVA + secVirtSize) + if(rva >= secVA && rva < (secVA + secVirtSize)) return secRawPtr + (rva - secVA) endif endfor diff --git a/Packages/doc/igor-pro-bridge.rst b/Packages/doc/igor-pro-bridge.rst index 26e2aec52d..147d86277b 100644 --- a/Packages/doc/igor-pro-bridge.rst +++ b/Packages/doc/igor-pro-bridge.rst @@ -392,7 +392,12 @@ Available tools ``Packages/igortest/procedures/igortest-test-compilation.ipf``). Since ``ZBR`` compiles as its own independent module, a ``true`` result here specifically reflects ProcGlobal's compile state -- reaching ``ZBR`` at all over ZeroMQ already implies - ``ZBR`` itself is compiled. + ``ZBR`` itself is compiled. **Fixed in v2.3.1**: the ``FunctionInfo()`` call was + previously unqualified, so it resolved against ``ZBR``'s own (always-compiled) + independent-module namespace instead of ProcGlobal's, meaning this could report + ``true`` even with a genuine ProcGlobal compile error. Fixed by qualifying it as + ``FunctionInfo("ProcGlobal#...")``, matching ``igortest``'s own reference + implementation. ``reload_and_compile_procedures()`` Forces Igor to reload changed ``.ipf`` files from disk (``RELOAD CHANGED PROCS``) and @@ -431,6 +436,46 @@ Available tools ``check_bridge_health()`` and be prepared for Igor Pro to need relaunching. See ``SESSION_NOTES.md`` for the full investigation. + **v2.3.1 fix -- the handler could stay stopped forever on a failed compile.** + v2.3.0's restart path was ``AfterCompiledHook`` alone, and Igor only calls that + hook after a *successful* compile. If the edited ``.ipf`` failed to compile, the + hook never fired, and the handler -- already stopped by + ``ZBR_StopHandlerBeforeRecompile`` -- stayed stopped permanently, killing the + bridge with no recovery short of relaunching Igor Pro. Found live by the repo + owner while intentionally testing a bad edit. Fixed by + ``ZBR_ArmRecompileWatchdog``/``ZBR_RecompileWatchdogTick`` (see + :ref:`igor_pro_bridge_zbr_helpers`): a named ``CtrlNamedBackground`` task, armed + immediately before the handler is stopped, that unconditionally restarts/rebinds + the handler regardless of whether the compile succeeds or fails, then + self-disarms the first time either it or ``AfterCompiledHook`` runs. Because the + background-task scheduler and the deferred operation queue (``Execute/P``) are + two independent Igor subsystems, live ``stopmstimer(-2)`` instrumentation (three + timestamped printouts: queue start, watchdog tick, ``AfterCompiledHook``) + confirmed they are **not** strictly ordered -- with only ``period=30`` set, the + watchdog ticked ~62ms after being armed, well before a real compile finished + (~414ms). The task is therefore registered with an explicit ``start=60`` (an + ~1-second floor before its first possible tick, independent of the ``period=30`` + interval governing later ticks), comfortably longer than any compile observed + this session. The property this protects is that the watchdog must not fire + before the operation queue has finished draining -- not that it must fire after + ``AfterCompiledHook`` specifically, which is meaningless on the failure path + since that hook never runs then; once the queue has drained, the relative order + of the two on the success path no longer matters, since both converge on the + same idempotent ``ZBR_EnsureZeroMQBound()`` call. This is a generous empirical + margin, not a mathematically airtight guarantee against an arbitrarily slow + future compile. As of v2.3.1, the bridge itself should stay reachable even after + a failed compile -- fix the ``.ipf`` and call ``reload_and_compile_procedures`` + again rather than needing to relaunch Igor Pro. + + A separate, unrelated issue also surfaced during this work: a pre-existing MIES + background *thread* (not task) left running during ``COMPILEPROCEDURES`` could + raise a blocking "Function Execution Module is still active" dialog, freezing + Igor's entire operation queue (direct ``CallFunction`` calls like + ``check_bridge_health`` kept working throughout, since they don't route through + the queue). Mitigated by a new ``BeforeUncompiledHook`` in + ``ZMQ_BridgeHelpers.ipf`` that calls ``ThreadGroupRelease(-2)`` to release + running thread groups before Igor uncompiles, added by the repo owner. + ``dismiss_compile_error_dialog()`` Attempts to close a stuck Igor Pro dialog by posting a simulated Escape key press directly to it (via ``PostMessage``), targeting a visible window owned by an Igor @@ -747,10 +792,20 @@ race-free confirmation signal, read back via ``ZBR_ReadCompileCounter()``. ``reload_and_compile_procedures`` reads a baseline before issuing ``RELOAD CHANGED PROCS``/``COMPILEPROCEDURES`` and treats any increase as immediate, trustworthy success, falling back to the ``ZBR_IsCompiled()``-based poll when the -counter is unavailable. There is no equivalent hook for a *failed* compile. Declared +counter is unavailable. There is no equivalent hook for a *failed* compile -- that +gap is exactly why v2.3.1 added the ``ZBR_ArmRecompileWatchdog``/ +``ZBR_RecompileWatchdogTick`` background-task restart path described in the +``reload_and_compile_procedures()`` reference entry above, since ``AfterCompiledHook`` +by itself cannot recover the ZeroMQ handler on the failure path. Declared ``static`` so it coexists with any other file's own static ``AfterCompiledHook`` without colliding. +A companion ``BeforeUncompiledHook`` (also added in v2.3.1) calls +``ThreadGroupRelease(-2)`` before Igor uncompiles, to release any running Igor +thread groups and avoid a blocking "Function Execution Module is still active" +dialog that a pre-existing MIES background thread could otherwise raise during +``COMPILEPROCEDURES``. + Auto-binding the ZeroMQ server socket on every compile ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -850,6 +905,14 @@ Known limitations proven fix. Treat any unreachability after a compile attempt as a signal to check ``check_bridge_health()`` and be prepared to relaunch Igor Pro. See ``SESSION_NOTES.md`` for the full investigation. +- **v2.3.0's restart path had its own concept flaw, fixed in v2.3.1**: it relied + solely on ``AfterCompiledHook``, which Igor never calls if the compile itself + fails -- so a bad edit left the ZeroMQ handler stopped permanently, with no + recovery short of relaunching Igor Pro. Fixed by an unconditional background-task + watchdog (armed with an explicit ``start=60`` floor, after live timing data showed + it could otherwise fire before the operation queue finished draining) -- see the + ``reload_and_compile_procedures()`` reference entry above for the full mechanism. + As of v2.3.1 the bridge should stay reachable even after a failed compile. .. _igor_pro_bridge_v1_history: diff --git a/tools/igor-mcp-bridge/ZMQ_BridgeHelpers.ipf b/tools/igor-mcp-bridge/ZMQ_BridgeHelpers.ipf index 7340565207..67fa5c19d7 100644 --- a/tools/igor-mcp-bridge/ZMQ_BridgeHelpers.ipf +++ b/tools/igor-mcp-bridge/ZMQ_BridgeHelpers.ipf @@ -1,3 +1,4 @@ +#pragma rtFunctionErrors = 1 #pragma TextEncoding = "UTF-8" #pragma rtGlobals = 3 #pragma IndependentModule = ZBR @@ -433,9 +434,30 @@ End /// FunctionInfo() for a deliberately non-existent function returns "" when procedures /// are compiled, and a non-empty string ("Procedures Not Compiled") otherwise. No /// Execute needed -- FunctionInfo is a plain built-in function. +/// +/// **Bug fixed (identified live by the repo owner while testing the v2.3.1 recompile +/// watchdog): the function name passed to FunctionInfo() MUST be qualified with the +/// "ProcGlobal#" prefix, exactly as igortest-test-compilation.ipf's own +/// IsProcGlobalCompiled() already does (`FunctionInfo("ProcGlobal#NON_EXISTING_FUNCTION")`) +/// -- an earlier version of this function omitted it. Per Igor's own FunctionInfo +/// documentation (Igor Reference.ihf): an unqualified functionNameStr resolves relative +/// to the CALLING function's own module context, and explicitly names "ProcGlobal" as a +/// valid independent-module qualifier for asking about a DIFFERENT module's namespace +/// (their own example: "Procedure [ProcGlobal]" to reach the main procedure window from +/// inside an independent module). Since ZBR_IsCompiled() itself is compiled INTO the ZBR +/// independent module, the unqualified name resolved against ZBR's OWN (always-fine) +/// compile state instead of ProcGlobal's -- meaning this function was silently reporting +/// "compiled" no matter what ProcGlobal's real state was, confirmed live: with a +/// deliberately broken ProcGlobal function in place, this returned true (via +/// check_compilation_state) while a plain `print FunctionInfo("ProcGlobal#...")` +/// dispatched through the ordinary top-level command queue correctly reported +/// "Procedures Not Compiled". reload_and_compile_procedures() happened to still report +/// the correct answer during that same test only because its PRIMARY signal (the +/// AfterCompiledHook-driven compile counter) never advanced -- this function was a +/// silently-broken fallback the whole time it existed. Function ZBR_IsCompiled() - return strlen(FunctionInfo("ZBR_DefinitelyNotARealFunctionName_8f3a1c")) == 0 + return strlen(FunctionInfo("ProcGlobal#ZBR_DefinitelyNotARealFunctionName_8f3a1c")) == 0 End /// Read root:gClaudeHelperCompileCounter (bumped by AfterCompiledHook below every time @@ -496,12 +518,17 @@ End /// ZBR_StopHandlerBeforeRecompile so it runs only after THIS call's own reply has /// already gone out -- see that function's docstring) before RELOAD CHANGED /// PROCS/COMPILEPROCEDURES ever run, so nothing can be dispatched into Igor while -/// it's mid-recompile. AfterCompiledHook's existing (unchanged, synchronous) -/// ZBR_EnsureZeroMQBound() call restarts the handler once compilation has actually -/// finished. This is a mitigation based on a well-reasoned but not 100%-certain -/// mechanism (Igor64.exe ships no public symbols, so the exact fault can't be proven -/// from here) -- see SESSION_NOTES.md for the full reasoning and its honest +/// it's mid-recompile. This is a mitigation based on a well-reasoned but not +/// 100%-certain mechanism (Igor64.exe ships no public symbols, so the exact fault can't +/// be proven from here) -- see SESSION_NOTES.md for the full reasoning and its honest /// limitations. +/// +/// **v2.3.1**: the handler is restarted via TWO independent paths, not just +/// AfterCompiledHook -- see ZBR_ArmRecompileWatchdog/ZBR_RecompileWatchdogTick's +/// docstrings for why AfterCompiledHook alone was a real, live-confirmed bug (it never +/// runs at all if the compile attempt fails, leaving the handler stopped forever) and +/// for the actual, empirically-verified fix (a `start=60`-armed named background task +/// that restarts the handler unconditionally, whether the compile succeeded or failed). Function ZBR_SubmitReloadAndCompile() Execute/P/Q/Z "ZBR#ZBR_StopHandlerBeforeRecompile()" @@ -528,15 +555,119 @@ End /// the whole Igor Pro instance, not just this module's own -- see ZBR_EnsureZeroMQBound's /// docstring for why that's avoided elsewhere too) -- zeromq_handler_stop() is the /// narrower, paired stop for zeromq_handler_start(), per ZeroMQ.ihf. +/// +/// **v2.3.1: also arms ZBR_ArmRecompileWatchdog here** -- see that function's docstring +/// for the concept flaw in the original v2.3.0 design this fixes (AfterCompiledHook, +/// which used to be the ONLY thing that restarted the handler, never fires at all if the +/// upcoming compile attempt fails). Function ZBR_StopHandlerBeforeRecompile() variable err zeromq_handler_stop(); err = GetRTError(1) + ZBR_ArmRecompileWatchdog() + + return 0 +End + +/// Name of the one-shot named background task armed by ZBR_ArmRecompileWatchdog. Named +/// (not the legacy unnamed CtrlBackground task) per Igor's own recommendation ("New code +/// should use named background tasks") and so it can't collide with any unrelated +/// background task some other part of this experiment (e.g. MIES's own) might be running. +static StrConstant ZBR_RECOMPILE_WATCHDOG_TASK = "ZBR_RecompileWatchdog" + +/// **v2.3.1 fix for a concept flaw in v2.3.0, identified live by the repo owner**: v2.3.0 +/// stopped the ZeroMQ handler before RELOAD CHANGED PROCS/COMPILEPROCEDURES and relied +/// entirely on AfterCompiledHook (via its existing, unchanged ZBR_EnsureZeroMQBound() +/// call) to restart it afterward. But per Igor's own Advanced Topics.ihf, "AfterCompiledHook +/// is a user-defined function that Igor calls after the procedure windows have all been +/// compiled successfully" -- if the triggering reload/compile attempt instead FAILS (e.g. +/// a syntax error in whatever .ipf was just edited), AfterCompiledHook never runs at all, +/// so the handler this bridge deliberately stopped moments earlier would stay stopped +/// forever, killing the entire bridge until a human manually fixes the compile error and +/// recompiles via Igor's own GUI. Confirmed live: exactly this scenario (a deliberately +/// broken ProcGlobal compile) left the bridge permanently unreachable under the +/// unpatched v2.3.0 code. +/// +/// A plain Execute/P entry queued to run right after COMPILEPROCEDURES was considered and +/// rejected: ZBR_SubmitReloadAndCompile's own docstring already documents (from earlier, +/// unrelated live testing) that anything queued behind COMPILEPROCEDURES in Igor's +/// deferred *operation queue* gets silently discarded/invalidated by the recompile, so +/// that mechanism can't be trusted here either, regardless of success or failure. +/// +/// A NAMED BACKGROUND TASK is a different Igor subsystem entirely (driven by Igor's own +/// idle-time task scheduler, not the deferred operation queue), and -- per Igor's own +/// Background Tasks help (Advanced Topics.ihf): "If you need your background task to +/// continue running even if you edit other procedures in Igor, you need to make your +/// project an independent module" -- a background task whose target function lives in an +/// independent module (like ZBR_RecompileWatchdogTick here, in this same ZBR module) +/// keeps running/ticking even while ProcGlobal is uncompiled or failed to compile. +/// +/// **Correction, found live via direct timer instrumentation (repo owner added +/// stopmstimer(-2) printouts at the start of the queue, in this function, and in +/// AfterCompiledHook, then had Claude recompute the deltas)**: an earlier version of this +/// docstring claimed background tasks "can't tick at all while Igor's main thread is +/// actually busy compiling," and concluded from that there was "no risk of this watchdog +/// firing while COMPILEPROCEDURES is still actually running." That conclusion was +/// DISPROVEN by the timer data: with a plain `start` (no explicit startTicks) and +/// period=30, the watchdog tick fired only ~62ms after the operation queue began +/// draining, while the actual compile (confirmed via AfterCompiledHook's own timestamp) +/// didn't finish until ~414ms in -- i.e. the watchdog CAN and did fire before +/// COMPILEPROCEDURES had actually finished. Background tasks and the deferred operation +/// queue are apparently NOT strictly ordered with respect to each other the way this +/// module's earlier reasoning assumed. +/// +/// **Actual fix: the `start=60` argument below** (distinct from a bare `start`) sets an +/// explicit ~1-second floor (60 ticks, ~1/60s each) before the watchdog's EARLIEST +/// possible first tick, decoupled from `period` (which only governs the interval between +/// ticks after the first one). The correctness property this needs is not "fires after +/// AfterCompiledHook" -- AfterCompiledHook never runs at all on a failed compile, so +/// there's nothing to compare against on that path -- it's "does not fire before the +/// operation queue (ZBR_StopHandlerBeforeRecompile's own queuing, then RELOAD CHANGED +/// PROCS, then COMPILEPROCEDURES) has actually finished draining." The repo owner's +/// judgment call: the brief setup overhead before RELOAD CHANGED PROCS begins, plus any +/// realistic compile of this codebase, comfortably finishes within that 1-second floor +/// (every recompile observed this session, including deliberately large ones, finished in +/// well under a second) -- not a mathematically airtight guarantee against an arbitrarily +/// slow future compile, but a practical, generous margin over anything actually observed. +/// +/// Called from ZBR_StopHandlerBeforeRecompile, i.e. right before RELOAD CHANGED +/// PROCS/COMPILEPROCEDURES run. `start` on an already-running named background task is +/// tolerated (err cleared) rather than propagated, since overlapping/concurrent +/// reload-and-compile attempts (already live-tested elsewhere, see SESSION_NOTES.md) would +/// otherwise each try to arm the same task name. +static Function ZBR_ArmRecompileWatchdog() + + variable err + + CtrlNamedBackground $ZBR_RECOMPILE_WATCHDOG_TASK, period=30, proc=ZBR_RecompileWatchdogTick, start=60 + err = GetRTError(1) return 0 End +/// Fires once, at least ~1 second after ZBR_ArmRecompileWatchdog armed it (see that +/// function's docstring for why the `start=60` floor -- not "background tasks can't run +/// during a compile" -- is what actually keeps this safely behind the triggering RELOAD +/// CHANGED PROCS/COMPILEPROCEDURES attempt finishing, whether it succeeded or failed). +/// Unconditionally restarts this module's ZeroMQ server/handler via +/// ZBR_EnsureZeroMQBound() -- harmless even if AfterCompiledHook already did the exact +/// same thing moments earlier on a successful compile, since that function already +/// tolerates being called when already bound/started (see its own docstring) -- then +/// stops itself (returning 1, which per Igor's own documented convention for background +/// task functions is how a task tells Igor to stop calling it again). +/// +/// Public (non-static): background-task `proc=` targets, like Execute/P-dispatched +/// callbacks elsewhere in this module (e.g. ZBR_FinishToken), need to be resolvable from +/// outside this function's own immediate caller. +Function ZBR_RecompileWatchdogTick(STRUCT WMBackgroundStruct &s) + + ZBR_EnsureZeroMQBound() + CtrlNamedBackground $ZBR_RECOMPILE_WATCHDOG_TASK, stop + + return 1 +End + // --- Debugger control ---------------------------------------------------------------- /// Direct (non-deferred) call -- confirmed live that DebuggerOptions, unlike @@ -544,7 +675,10 @@ End Function [variable enable, variable debugOnError, variable debugOnAbort, variable nvarChecking] ZBR_GetDebuggerState() DebuggerOptions - variable e = V_enable, doe = V_debugOnError, doa = V_debugOnAbort, nv = V_NVAR_SVAR_WAVE_Checking + variable e = V_enable + variable doe = V_debugOnError + variable doa = V_debugOnAbort + variable nv = V_NVAR_SVAR_WAVE_Checking KillVariables/Z V_enable, V_debugOnError, V_debugOnAbort, V_NVAR_SVAR_WAVE_Checking return [e, doe, doa, nv] @@ -853,6 +987,13 @@ static Function ZBR_EnsureZeroMQBound() return 0 End +static Function BeforeUncompiledHook(variable changeCode, string procedureWindowTitleStr, string textChangeStr) + + variable err + + err = ThreadGroupRelease(-2) +End + static Function AfterCompiledHook() variable modifiedBefore diff --git a/tools/igor-mcp-bridge/igor-pro-bridge-2.3.0.mcpb b/tools/igor-mcp-bridge/igor-pro-bridge-2.3.0.mcpb deleted file mode 100644 index 4cea084888acfba4694188610cf02e7a13685ff3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 35142 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%0DMiIc-NS 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 z00000000000002M0hK5K0CRF `ZBR_EnsureZeroMQBound()` synchronous call. Confirmed live: recompiled successfully afterward, then ran three CONCURRENT `reload_and_compile_procedures()` calls as a stress test -- all three returned `compiled: true` cleanly with no crash, and `check_bridge_health()`/`execute_igor_command()` both worked correctly afterward. Honest caveat: this is a well-reasoned mitigation, not a proven fix -- `Igor64.exe` ships no public symbols, so the exact fault can't be confirmed from here, and the crash was already rare/nondeterministic before this fix. No Python-side (`server.py`) logic changes were needed beyond an updated docstring; the fix is entirely within `ZMQ_BridgeHelpers.ipf`.\n\nv2.2.3: fixed another uncaught-runtime-error-pops-a-dialog bug, this time in `ZBR_FinishToken`, found while cleaning up leftover test-token rows from this bridge's own `root:Packages:ZBR` storage waves. `ZBR_AllocateToken` captures a token's row index (`idx`) at submission time, but `ZBR_FinishToken` -- the callback that actually writes the result and flips the done flag -- runs later, in its own separate deferred `Execute/P` entry (per the v2.2.0 fix). If the `done`/`resultText`/`historyStart` waves are resized smaller in between (e.g. maintenance code clearing out old tokens, exactly as happened live during this session's own cleanup), `idx` can end up pointing past the end of the now-shorter waves. `ZBR_FinishToken` had no bounds check, so writing to `resultText[idx]` in that state threw an uncaught \"Index out of range for wave...\" runtime error -- which, like the v2.2.1 bug, pops a real modal dialog and blocks Igor's entire main thread until a human dismisses it. Fixed by bounds-checking `idx` against `DimSize(done, 0)` before writing anything; if the row no longer exists, the callback now just silently does nothing (there is nothing useful left to recover), and `ZBR_PollCommand`'s own existing bounds check already reports a clean `\"ERROR: unknown token\"` response to the caller instead of a hang. Confirmed live: reproduced the exact original crash (resizing the storage waves to 0 rows while a submitted command's own token was still in flight, which had just thrown the dialog moments earlier during this session's testing), then repeated it after the fix -- this time it returned `\"ERROR: unknown token ...\"` cleanly with no dialog and no hang, and `check_bridge_health()` stayed `OK` throughout. No Python-side changes were needed.\n\nv2.2.2: minor robustness refinement to the v2.2.1 fix, contributed directly by the repo owner after installing it. `ZBR_EnsureCaptureStarted`'s stale-refnum probe (`CaptureHistory(refnumRW, 0)`) and its `AbortOnRTE` were originally written on separate lines; moved onto the SAME line. Reason: Igor's Debug on Error check happens at the END of each line, not each statement -- if the two were on separate lines, a user with Debug on Error enabled (unusual, but this bridge doesn't force the Debugger off during a raw `execute_igor_command`/`submit_igor_command` call the way the `_unattended` variants do) would get a Debugger popup right when the stale refnum's runtime error occurred, before `AbortOnRTE` ever got a chance to convert it into a catchable abort -- defeating the whole point of the v2.2.1 fix in that one specific configuration. Confirmed live both before accepting this change (by temporarily enabling `debug_on_error` via `set_debugger_enabled` and repeating the deliberately-corrupted-refnum test) and after: with the two statements on one line, the corrupted-refnum scenario completes cleanly with no Debugger popup and no hang even with Debug on Error active, and the refnum still self-heals correctly. No other behavior change.\n\nv2.2.1: fixed a bug where `load_experiment` (or a user manually reopening a saved .pxp) could leave the bridge completely unreachable, requiring a human to dismiss a modal Igor error dialog. Root cause: this bridge's history-capture mechanism stores its `CaptureHistoryStart()` reference number in a plain `Variable/G` global (`root:Packages:ZBR:captureRefNum`), which Igor persists into a saved experiment like any other global -- but that refnum is only meaningful within the OS process that created it. Reloading a saved experiment brings the OLD numeric value back even though the process is brand new; the prior code only checked that the global *existed*, not whether its value was still a live capture, so it trusted the stale refnum. Using it then threw a genuine Igor runtime error (\"there is no open file with this reference number\") from a plain top-level `Execute/P` entry -- not caught anywhere -- which popped a real modal dialog and blocked Igor's entire main thread (and therefore every ZeroMQ reply) until a human dismissed it. Confirmed live by saving and reloading an experiment via `load_experiment` and then calling `execute_igor_command`, which reproduced the dialog exactly. Fixed in `ZBR_EnsureCaptureStarted` (`ZMQ_BridgeHelpers.ipf`): the stored refnum is now validated with a `try`/`catch`/`endtry`+`AbortOnRTE` block before being trusted, and silently replaced with a fresh `CaptureHistoryStart()` call if it's stale -- confirmed live afterward, both by deliberately corrupting the refnum to a bogus value and by repeating the exact save-then-`load_experiment`-then-`execute_igor_command` sequence that originally triggered the dialog; neither reproduced the dialog and both recovered silently. (Two syntax mistakes were made and caught along the way while implementing this: a runtime error inside a `try` block does not by itself jump to `catch` without an explicit `AbortOnRTE` immediately after the risky call, and a bare `return` with no value is invalid inside a plain, implicit-`Variable`-returning `Function` -- both confirmed from Igor's own bundled help, \"Flow Control for Aborts\" and \"The Return Statement\" in `Programming.ihf`.) No Python-side (`server.py`) changes were needed for this fix -- it is entirely within the Igor-side `ZMQ_BridgeHelpers.ipf` helper file.\n\nv2.2.0: fixed a serious reliability bug in the v2.1.0 submit/poll primitives, found via live testing of the exact scenario they exist for (a command whose runtime is unknown and could be very long). `ZBR_SubmitCommand`/`ZBR_SubmitCommandUnattended` (Igor-side, in `ZMQ_BridgeHelpers.ipf`) previously joined the submitted command and the internal finish-callback into ONE compound `Execute/P` string (`cmd + \"; \" + finishCall`). Confirmed live: if `cmd` failed to parse, OR hit a genuine Igor-level runtime error partway through (not just a Debugger pause), the ENTIRE joined string aborted, so the appended finish-callback never ran -- leaving `poll_igor_command` reporting `\"done\": false` forever, indistinguishable from a job still genuinely in progress. This directly undermines the reliability guarantee these tools exist to provide. Root-caused via two live tests: (1) queuing an invalid command and a valid one as two SEPARATE `Execute/P` entries showed the second still ran fine despite the first failing, proving Igor's deferred-operation queue processes independently-queued entries regardless of a prior one's failure; (2) the same joined-string command that had a genuine runtime error (not just an invalid command name) also silently swallowed its own appended print statement, confirming the bug applies to runtime errors too, not just parse errors. Fixed by queuing `cmd` and the finish-callback (and, for the `_unattended` variant, a Debugger-disable step and a Debugger-restore step, with the saved settings carried across via persistent globals) as fully independent `Execute/P` entries instead of one joined string -- verified live afterward with an invalid command, a genuine runtime-erroring command, and a normal command, all three now correctly reaching `\"done\": true` instead of hanging. **Known residual limitation, also confirmed live:** there is currently no generic way to tell \"the command ran and legitimately printed nothing\" apart from \"the command errored out with no output\" -- both now come back as `\"done\": true` with an empty/short result and no error indication (an attempted fix using `GetRTError()` in the finish-callback was tested and found not to work: each `Execute/P` entry is dispatched as its own independent top-level execution, so Igor clears any pending runtime-error state before the next queued entry runs). If a submitted command's success needs to be verifiable, have it `print` an explicit sentinel/result value itself.\n\nv2.1.0: added first-class support for commands with an unknown or very long runtime (hours to weeks). `execute_igor_command`/`execute_igor_command_unattended` block the whole MCP tool call while polling, which cannot work for a genuinely long calculation -- the MCP transport itself has been observed to time out a single tool call well under a minute, regardless of the requested `timeout_seconds`. Three new tools expose the existing submit/poll primitives directly: `submit_igor_command`/`submit_igor_command_unattended` queue a command and return a token immediately with no waiting at all, and `poll_igor_command(token)` performs one cheap, instant check for completion, callable any number of times spaced arbitrarily far apart. This is reliable no matter how long the job runs or how many times this bridge process or Claude Desktop itself restarts in between, because all of the actual state (a done flag and the captured text) lives entirely in Igor Pro's own data waves (`root:Packages:ZBR`), not in this bridge's Python process -- the only thing that actually ends the job is Igor Pro itself quitting, crashing, or restarting. As with the existing `_unattended` tools, use `submit_igor_command_unattended` for anything long-running: a Debugger pause partway through leaves `poll_igor_command` reporting \"not done\" forever, indistinguishable from the command still genuinely running.\n\nv2.0.1: three bug fixes found during live v2.0.0 testing. (1) `execute_igor_command`/`execute_igor_command_unattended`: the documented pattern of using `fprintf 0, ...` to get data back does not actually work over this transport -- confirmed live that `CaptureHistory` (which this bridge's capture mechanism relies on) captures `print` output but never captures `fprintf`-to-history output at all (refnum 0, -1, or -2 all silently produce nothing, even though the command itself runs without error). Use `print` instead; all docs/docstrings updated accordingly. (2) `load_experiment`: fixed a silent-failure bug where relaunching Igor Pro with a new experiment file could do nothing at all, with no error reported. Root cause (diagnosed live): the tool was waiting for Igor Pro to stop answering over ZeroMQ as its signal that the old process had quit, but ZeroMQ goes quiet well before the underlying Igor64.exe process actually terminates -- launching the replacement `Igor64.exe /UNATTENDED ` command line while the old process is still alive (even mid-shutdown) doesn't spawn a new process at all; Windows/Igor's single-instance-per-user behavior instead either redirects the load into the still-live old instance (risking an unhandled \"save changes?\" dialog) or silently drops the request if that instance is already mid-quit. Fixed by polling the actual OS process list for the configured executable's own process to fully exit before ever launching the replacement, raising a clear error instead of proceeding if it doesn't exit in time. (3) `read_help_file`: added a `timeout_ms` parameter (default raised from this bridge's usual 5s to 30s) -- confirmed live that exporting a genuinely large help file (e.g. the full \"Igor Reference.ihf\") can take longer than 5 seconds, which previously timed out the call even though the export eventually succeeded Igor-side anyway.\n\nv2.0.0: BREAKING transport change. Replaces the COM Automation Server transport (win32com.client, ProgID `IgorPro.Application`) with the ZeroMQ-XOP's `CallFunction` JSON protocol over a plain localhost TCP socket (tcp://127.0.0.1:5680), talking to a small set of Igor-side helper functions in `Packages/MIES/ZMQ_BridgeHelpers.ipf` (the `ZBR` independent module).\n\nWhy: COM required this bridge process and Igor Pro to run at the SAME Windows privilege level (both elevated, or both not) -- an easy-to-miss mismatch (e.g. Claude Desktop reopened normally after Igor Pro was left running elevated from before). ZeroMQ is a plain TCP socket with no such requirement at all -- elevation no longer matters in any way. `launch_igor_pro_unattended` no longer has an elevation-branching code path: it always launches Igor Pro as a plain, non-elevated child process, at whatever privilege level this bridge process itself runs at.\n\n**New setup requirement**: unlike the COM transport (which worked against a stock Igor Pro installation with zero custom procedure code), this transport requires `Packages/MIES/ZMQ_BridgeHelpers.ipf` to be `#include`-d and compiled into whichever Igor Pro experiment this bridge talks to -- there is no bootstrap path over ZeroMQ itself. This repo's own `Packages/MIES_Include.ipf` already does this permanently. Any OTHER Igor Pro experiment needs `ZMQ_BridgeHelpers.ipf` copied onto its own procedure search path with a matching `#include` added by hand, then a recompile -- see `Packages/doc/igor-pro-bridge.rst` (\"Installation\") for the exact one-time steps.\n\n**Capability improvements**: `get_wave` now supports any wave dimensionality (up to 4D), real and complex numeric waves, text waves, and wave-reference waves -- the entire wave comes back from one round trip, natively serialized (the old COM version was limited to 1D real-valued waves, read one point at a time).\n\n**Behavior changes to be aware of**: `execute_igor_command`/`execute_igor_command_unattended` can no longer separate fprintf-only output from the full echoed history the way COM's `Execute2` could -- both \"results\" and \"history\" now hold the same captured text; prefix a command with `Silent 1;` to suppress the echo. `load_experiment` no longer hot-swaps the open experiment in place (that was a COM-only method with no procedure-language equivalent) -- it now quits the running instance and relaunches Igor Pro with the target file path as a launch argument, a real process restart; unsaved changes in the previously-open experiment are lost. `ensure_igor_pro_bridge_defined` is retired -- it existed to manage a `#ifdef IGOR_PRO_BRIDGE` gating convention that ZBR's always-on independent-module architecture doesn't use.\n\nSee `Packages/doc/igor-pro-bridge.rst` and `SESSION_NOTES.md` in the MIES repository for the full protocol research, design decisions, and confirmed-live test results behind this rewrite.\n\nTools:\n- `execute_igor_command` / `execute_igor_command_unattended`: run a command string on Igor's command line (submitted via `ZBR_SubmitCommand`/`ZBR_SubmitCommandUnattended`, polled via `ZBR_PollCommand`); include a `print` call (NOT `fprintf 0, ...` -- confirmed not captured, see v2.0.1 changelog above) to get data back. Both return `{results, history}`. The `_unattended` variant automatically disables/restores Igor's Debugger around the call.\n- `read_session_history`: read back everything sent to Igor's history area since this bridge's capture started.\n- `get_wave`: read any Igor wave's full data and metadata back (any dimensionality, numeric/complex/text/wave-reference).\n- `load_experiment`: quit the running instance and relaunch Igor Pro with a .pxp file path as a launch argument.\n- `check_bridge_health`: diagnose whether this bridge can reach Igor Pro's ZeroMQ server right now.\n- `check_compilation_state` / `reload_and_compile_procedures`: check and refresh Igor's compiled state after editing a `.ipf` file on disk.\n- `dismiss_compile_error_dialog`: close a stuck \"Function Compilation Error\" dialog via a posted Escape key, without needing OS focus.\n- `get_debugger_state` / `set_debugger_enabled` / `restore_debugger_settings`: read, change, and restore Igor's Debugger settings.\n- `get_environment_summary`: summarize the live instance (version, loaded experiment, XOPs, included procedure files, data folders, Debugger settings).\n- `read_help_file`: read an Igor Pro help file (.ihf) as structured, formatted text.\n- `get_bridge_version`: report the running bridge version and Python/package versions.\n- `configure_igor_launch` / `launch_igor_pro_unattended`: record the Igor Pro executable path once per session, then launch it with the `/UNATTENDED` flag and wait for it to become reachable over ZeroMQ.\n\n**Requirements:**\n- Igor Pro 9.00 or later, running on Windows, with the ZeroMQ-XOP installed and loaded.\n- `Packages/MIES/ZMQ_BridgeHelpers.ipf` (or a copy of it) `#include`-d and compiled into the target experiment -- see `Packages/doc/igor-pro-bridge.rst` (\"Installation\") for the one-time setup steps for experiments other than this repo's own.\n- Python (accessible as `python` on PATH), with the pinned packages in `requirements.txt` installed into that same environment. Run `install.ps1` to do this correctly and also complete pywin32's required post-install step (still needed for window-handling/process-launch helpers, even though this bridge no longer uses COM) -- see that script's own help (`Get-Help ./install.ps1 -Full`).\n- No elevation/privilege-matching requirement of any kind -- this is the change v2.0.0 makes.\n\nPrior COM-based history (v1.x): see `Packages/doc/igor-pro-bridge.rst` for the full v1.x changelog, retained there for reference.", + "long_description": "v2.3.1: fixed a concept flaw in v2.3.0's crash mitigation, found live by the repo owner: v2.3.0 stops the ZeroMQ handler before `RELOAD CHANGED PROCS`/`COMPILEPROCEDURES` run and relies on `AfterCompiledHook` to restart it afterward -- but Igor only calls `AfterCompiledHook` after a *successful* compile. If the edited `.ipf` failed to compile, the hook never fired, the handler stayed stopped forever, and the bridge was permanently dead with no recovery short of restarting Igor Pro. Fixed by adding `ZBR_ArmRecompileWatchdog`/`ZBR_RecompileWatchdogTick` (`ZMQ_BridgeHelpers.ipf`): a named background task, armed immediately before the handler is stopped, that unconditionally restarts/rebinds the handler regardless of whether the compile succeeds or fails, then self-disarms. Because Igor's background-task scheduler and its deferred operation queue (`Execute/P`) are two independent subsystems, the repo owner raised a sharp concern: could the watchdog fire before `RELOAD CHANGED PROCS`/`COMPILEPROCEDURES` even finish, reintroducing the exact cross-thread race v2.3.0 exists to prevent? Live timing instrumentation (three `stopmstimer(-2)` printouts: queue start, watchdog tick, `AfterCompiledHook`) confirmed the concern was valid -- with only `period=30` set, the watchdog could and did tick (~62ms after arming) well before a real compile finished (~414ms, per `AfterCompiledHook`'s own timestamp). Fixed by registering the task with an explicit `start=60` (an ~1-second floor before its first possible tick, decoupled from the `period=30` interval between later ticks), comfortably longer than any compile observed this session. The correctness property this protects is that the watchdog must not fire before the operation queue has finished draining -- not that it must fire after `AfterCompiledHook` specifically, which is meaningless on the failure path since that hook never runs then; once the queue has drained, the relative order of the watchdog and `AfterCompiledHook` on the success path no longer matters, since both converge on the same idempotent `ZBR_EnsureZeroMQBound()` restart. This is a generous empirical margin based on observed compile times, not a mathematically airtight guarantee against an arbitrarily slow future compile. A second, independent bug was found and diagnosed live by the repo owner while testing the above: `ZBR_IsCompiled()`'s `FunctionInfo()` call was unqualified, so it resolved against ZBR's own independent-module namespace (always fine) instead of ProcGlobal's -- meaning `check_compilation_state()` could silently report `true` even while ProcGlobal itself had a genuine compile error. Fixed by qualifying the call as `FunctionInfo(\"ProcGlobal#...\")`, confirmed against Igor's own docs and this repo's `igortest-test-compilation.ipf` reference implementation. A third, unrelated issue surfaced during live testing: a pre-existing MIES background thread (not task) left running during `COMPILEPROCEDURES` could raise a blocking \"Function Execution Module is still active\" dialog, freezing Igor's entire operation queue (though direct `CallFunction` calls like `check_bridge_health` kept working throughout, since they don't route through the queue). Mitigated by a new `BeforeUncompiledHook` in `ZMQ_BridgeHelpers.ipf` that calls `ThreadGroupRelease(-2)` to release running thread groups before Igor uncompiles, added by the repo owner and confirmed to prevent recurrence in subsequent testing. No Python-side (`server.py`) logic changes were needed beyond updated docstrings describing these fixes; all of the actual fixes are within `ZMQ_BridgeHelpers.ipf`.\n\nv2.3.0: investigated the long-standing, previously-unexplained crash pattern where Igor Pro itself (not this bridge) would sometimes become unreachable shortly after `reload_and_compile_procedures`. Two Windows crash dumps captured during this bridge's development were analyzed with Python's `minidump` package (Igor's own crash reporter provides no stack trace), confirming both were genuine `EXCEPTION_ACCESS_VIOLATION`s deep inside `Igor64.exe` itself, not in this bridge's code or any XOP. A likely mechanism was then identified, based directly on a comparison the repo owner suggested against this repo's own `igortest-tracing.ipf`, whose `CompileAndRestart()`/`AfterCompiledHook()` pattern reloads and compiles procedures reliably with no crash: unlike that reference pattern, this bridge's ZeroMQ-XOP handler runs as a background thread (documented in `HelpFiles/ZeroMQ.ihf` as \"a threaded message handler\") that keeps dispatching incoming `CallFunction` requests regardless of what Igor's main thread is doing -- so a request arriving while `COMPILEPROCEDURES` is mid-rebuild of Igor's own internal function/symbol tables is a plausible cross-thread race, distinct from anything happening inside `AfterCompiledHook` itself (which runs safely afterward, exactly like `igortest`'s hook). Fixed by adding `ZBR_StopHandlerBeforeRecompile()` (calls `zeromq_handler_stop()`) and queuing it as the FIRST deferred `Execute/P` entry in `ZBR_SubmitReloadAndCompile()`, ahead of `RELOAD CHANGED PROCS`/`COMPILEPROCEDURES`; the handler is restarted afterward via the existing, unchanged `AfterCompiledHook` -> `ZBR_EnsureZeroMQBound()` synchronous call. Confirmed live: recompiled successfully afterward, then ran three CONCURRENT `reload_and_compile_procedures()` calls as a stress test -- all three returned `compiled: true` cleanly with no crash, and `check_bridge_health()`/`execute_igor_command()` both worked correctly afterward. Honest caveat: this is a well-reasoned mitigation, not a proven fix -- `Igor64.exe` ships no public symbols, so the exact fault can't be confirmed from here, and the crash was already rare/nondeterministic before this fix. No Python-side (`server.py`) logic changes were needed beyond an updated docstring; the fix is entirely within `ZMQ_BridgeHelpers.ipf`.\n\nv2.2.3: fixed another uncaught-runtime-error-pops-a-dialog bug, this time in `ZBR_FinishToken`, found while cleaning up leftover test-token rows from this bridge's own `root:Packages:ZBR` storage waves. `ZBR_AllocateToken` captures a token's row index (`idx`) at submission time, but `ZBR_FinishToken` -- the callback that actually writes the result and flips the done flag -- runs later, in its own separate deferred `Execute/P` entry (per the v2.2.0 fix). If the `done`/`resultText`/`historyStart` waves are resized smaller in between (e.g. maintenance code clearing out old tokens, exactly as happened live during this session's own cleanup), `idx` can end up pointing past the end of the now-shorter waves. `ZBR_FinishToken` had no bounds check, so writing to `resultText[idx]` in that state threw an uncaught \"Index out of range for wave...\" runtime error -- which, like the v2.2.1 bug, pops a real modal dialog and blocks Igor's entire main thread until a human dismisses it. Fixed by bounds-checking `idx` against `DimSize(done, 0)` before writing anything; if the row no longer exists, the callback now just silently does nothing (there is nothing useful left to recover), and `ZBR_PollCommand`'s own existing bounds check already reports a clean `\"ERROR: unknown token\"` response to the caller instead of a hang. Confirmed live: reproduced the exact original crash (resizing the storage waves to 0 rows while a submitted command's own token was still in flight, which had just thrown the dialog moments earlier during this session's testing), then repeated it after the fix -- this time it returned `\"ERROR: unknown token ...\"` cleanly with no dialog and no hang, and `check_bridge_health()` stayed `OK` throughout. No Python-side changes were needed.\n\nv2.2.2: minor robustness refinement to the v2.2.1 fix, contributed directly by the repo owner after installing it. `ZBR_EnsureCaptureStarted`'s stale-refnum probe (`CaptureHistory(refnumRW, 0)`) and its `AbortOnRTE` were originally written on separate lines; moved onto the SAME line. Reason: Igor's Debug on Error check happens at the END of each line, not each statement -- if the two were on separate lines, a user with Debug on Error enabled (unusual, but this bridge doesn't force the Debugger off during a raw `execute_igor_command`/`submit_igor_command` call the way the `_unattended` variants do) would get a Debugger popup right when the stale refnum's runtime error occurred, before `AbortOnRTE` ever got a chance to convert it into a catchable abort -- defeating the whole point of the v2.2.1 fix in that one specific configuration. Confirmed live both before accepting this change (by temporarily enabling `debug_on_error` via `set_debugger_enabled` and repeating the deliberately-corrupted-refnum test) and after: with the two statements on one line, the corrupted-refnum scenario completes cleanly with no Debugger popup and no hang even with Debug on Error active, and the refnum still self-heals correctly. No other behavior change.\n\nv2.2.1: fixed a bug where `load_experiment` (or a user manually reopening a saved .pxp) could leave the bridge completely unreachable, requiring a human to dismiss a modal Igor error dialog. Root cause: this bridge's history-capture mechanism stores its `CaptureHistoryStart()` reference number in a plain `Variable/G` global (`root:Packages:ZBR:captureRefNum`), which Igor persists into a saved experiment like any other global -- but that refnum is only meaningful within the OS process that created it. Reloading a saved experiment brings the OLD numeric value back even though the process is brand new; the prior code only checked that the global *existed*, not whether its value was still a live capture, so it trusted the stale refnum. Using it then threw a genuine Igor runtime error (\"there is no open file with this reference number\") from a plain top-level `Execute/P` entry -- not caught anywhere -- which popped a real modal dialog and blocked Igor's entire main thread (and therefore every ZeroMQ reply) until a human dismissed it. Confirmed live by saving and reloading an experiment via `load_experiment` and then calling `execute_igor_command`, which reproduced the dialog exactly. Fixed in `ZBR_EnsureCaptureStarted` (`ZMQ_BridgeHelpers.ipf`): the stored refnum is now validated with a `try`/`catch`/`endtry`+`AbortOnRTE` block before being trusted, and silently replaced with a fresh `CaptureHistoryStart()` call if it's stale -- confirmed live afterward, both by deliberately corrupting the refnum to a bogus value and by repeating the exact save-then-`load_experiment`-then-`execute_igor_command` sequence that originally triggered the dialog; neither reproduced the dialog and both recovered silently. (Two syntax mistakes were made and caught along the way while implementing this: a runtime error inside a `try` block does not by itself jump to `catch` without an explicit `AbortOnRTE` immediately after the risky call, and a bare `return` with no value is invalid inside a plain, implicit-`Variable`-returning `Function` -- both confirmed from Igor's own bundled help, \"Flow Control for Aborts\" and \"The Return Statement\" in `Programming.ihf`.) No Python-side (`server.py`) changes were needed for this fix -- it is entirely within the Igor-side `ZMQ_BridgeHelpers.ipf` helper file.\n\nv2.2.0: fixed a serious reliability bug in the v2.1.0 submit/poll primitives, found via live testing of the exact scenario they exist for (a command whose runtime is unknown and could be very long). `ZBR_SubmitCommand`/`ZBR_SubmitCommandUnattended` (Igor-side, in `ZMQ_BridgeHelpers.ipf`) previously joined the submitted command and the internal finish-callback into ONE compound `Execute/P` string (`cmd + \"; \" + finishCall`). Confirmed live: if `cmd` failed to parse, OR hit a genuine Igor-level runtime error partway through (not just a Debugger pause), the ENTIRE joined string aborted, so the appended finish-callback never ran -- leaving `poll_igor_command` reporting `\"done\": false` forever, indistinguishable from a job still genuinely in progress. This directly undermines the reliability guarantee these tools exist to provide. Root-caused via two live tests: (1) queuing an invalid command and a valid one as two SEPARATE `Execute/P` entries showed the second still ran fine despite the first failing, proving Igor's deferred-operation queue processes independently-queued entries regardless of a prior one's failure; (2) the same joined-string command that had a genuine runtime error (not just an invalid command name) also silently swallowed its own appended print statement, confirming the bug applies to runtime errors too, not just parse errors. Fixed by queuing `cmd` and the finish-callback (and, for the `_unattended` variant, a Debugger-disable step and a Debugger-restore step, with the saved settings carried across via persistent globals) as fully independent `Execute/P` entries instead of one joined string -- verified live afterward with an invalid command, a genuine runtime-erroring command, and a normal command, all three now correctly reaching `\"done\": true` instead of hanging. **Known residual limitation, also confirmed live:** there is currently no generic way to tell \"the command ran and legitimately printed nothing\" apart from \"the command errored out with no output\" -- both now come back as `\"done\": true` with an empty/short result and no error indication (an attempted fix using `GetRTError()` in the finish-callback was tested and found not to work: each `Execute/P` entry is dispatched as its own independent top-level execution, so Igor clears any pending runtime-error state before the next queued entry runs). If a submitted command's success needs to be verifiable, have it `print` an explicit sentinel/result value itself.\n\nv2.1.0: added first-class support for commands with an unknown or very long runtime (hours to weeks). `execute_igor_command`/`execute_igor_command_unattended` block the whole MCP tool call while polling, which cannot work for a genuinely long calculation -- the MCP transport itself has been observed to time out a single tool call well under a minute, regardless of the requested `timeout_seconds`. Three new tools expose the existing submit/poll primitives directly: `submit_igor_command`/`submit_igor_command_unattended` queue a command and return a token immediately with no waiting at all, and `poll_igor_command(token)` performs one cheap, instant check for completion, callable any number of times spaced arbitrarily far apart. This is reliable no matter how long the job runs or how many times this bridge process or Claude Desktop itself restarts in between, because all of the actual state (a done flag and the captured text) lives entirely in Igor Pro's own data waves (`root:Packages:ZBR`), not in this bridge's Python process -- the only thing that actually ends the job is Igor Pro itself quitting, crashing, or restarting. As with the existing `_unattended` tools, use `submit_igor_command_unattended` for anything long-running: a Debugger pause partway through leaves `poll_igor_command` reporting \"not done\" forever, indistinguishable from the command still genuinely running.\n\nv2.0.1: three bug fixes found during live v2.0.0 testing. (1) `execute_igor_command`/`execute_igor_command_unattended`: the documented pattern of using `fprintf 0, ...` to get data back does not actually work over this transport -- confirmed live that `CaptureHistory` (which this bridge's capture mechanism relies on) captures `print` output but never captures `fprintf`-to-history output at all (refnum 0, -1, or -2 all silently produce nothing, even though the command itself runs without error). Use `print` instead; all docs/docstrings updated accordingly. (2) `load_experiment`: fixed a silent-failure bug where relaunching Igor Pro with a new experiment file could do nothing at all, with no error reported. Root cause (diagnosed live): the tool was waiting for Igor Pro to stop answering over ZeroMQ as its signal that the old process had quit, but ZeroMQ goes quiet well before the underlying Igor64.exe process actually terminates -- launching the replacement `Igor64.exe /UNATTENDED ` command line while the old process is still alive (even mid-shutdown) doesn't spawn a new process at all; Windows/Igor's single-instance-per-user behavior instead either redirects the load into the still-live old instance (risking an unhandled \"save changes?\" dialog) or silently drops the request if that instance is already mid-quit. Fixed by polling the actual OS process list for the configured executable's own process to fully exit before ever launching the replacement, raising a clear error instead of proceeding if it doesn't exit in time. (3) `read_help_file`: added a `timeout_ms` parameter (default raised from this bridge's usual 5s to 30s) -- confirmed live that exporting a genuinely large help file (e.g. the full \"Igor Reference.ihf\") can take longer than 5 seconds, which previously timed out the call even though the export eventually succeeded Igor-side anyway.\n\nv2.0.0: BREAKING transport change. Replaces the COM Automation Server transport (win32com.client, ProgID `IgorPro.Application`) with the ZeroMQ-XOP's `CallFunction` JSON protocol over a plain localhost TCP socket (tcp://127.0.0.1:5680), talking to a small set of Igor-side helper functions in `Packages/MIES/ZMQ_BridgeHelpers.ipf` (the `ZBR` independent module).\n\nWhy: COM required this bridge process and Igor Pro to run at the SAME Windows privilege level (both elevated, or both not) -- an easy-to-miss mismatch (e.g. Claude Desktop reopened normally after Igor Pro was left running elevated from before). ZeroMQ is a plain TCP socket with no such requirement at all -- elevation no longer matters in any way. `launch_igor_pro_unattended` no longer has an elevation-branching code path: it always launches Igor Pro as a plain, non-elevated child process, at whatever privilege level this bridge process itself runs at.\n\n**New setup requirement**: unlike the COM transport (which worked against a stock Igor Pro installation with zero custom procedure code), this transport requires `Packages/MIES/ZMQ_BridgeHelpers.ipf` to be `#include`-d and compiled into whichever Igor Pro experiment this bridge talks to -- there is no bootstrap path over ZeroMQ itself. This repo's own `Packages/MIES_Include.ipf` already does this permanently. Any OTHER Igor Pro experiment needs `ZMQ_BridgeHelpers.ipf` copied onto its own procedure search path with a matching `#include` added by hand, then a recompile -- see `Packages/doc/igor-pro-bridge.rst` (\"Installation\") for the exact one-time steps.\n\n**Capability improvements**: `get_wave` now supports any wave dimensionality (up to 4D), real and complex numeric waves, text waves, and wave-reference waves -- the entire wave comes back from one round trip, natively serialized (the old COM version was limited to 1D real-valued waves, read one point at a time).\n\n**Behavior changes to be aware of**: `execute_igor_command`/`execute_igor_command_unattended` can no longer separate fprintf-only output from the full echoed history the way COM's `Execute2` could -- both \"results\" and \"history\" now hold the same captured text; prefix a command with `Silent 1;` to suppress the echo. `load_experiment` no longer hot-swaps the open experiment in place (that was a COM-only method with no procedure-language equivalent) -- it now quits the running instance and relaunches Igor Pro with the target file path as a launch argument, a real process restart; unsaved changes in the previously-open experiment are lost. `ensure_igor_pro_bridge_defined` is retired -- it existed to manage a `#ifdef IGOR_PRO_BRIDGE` gating convention that ZBR's always-on independent-module architecture doesn't use.\n\nSee `Packages/doc/igor-pro-bridge.rst` and `SESSION_NOTES.md` in the MIES repository for the full protocol research, design decisions, and confirmed-live test results behind this rewrite.\n\nTools:\n- `execute_igor_command` / `execute_igor_command_unattended`: run a command string on Igor's command line (submitted via `ZBR_SubmitCommand`/`ZBR_SubmitCommandUnattended`, polled via `ZBR_PollCommand`); include a `print` call (NOT `fprintf 0, ...` -- confirmed not captured, see v2.0.1 changelog above) to get data back. Both return `{results, history}`. The `_unattended` variant automatically disables/restores Igor's Debugger around the call.\n- `read_session_history`: read back everything sent to Igor's history area since this bridge's capture started.\n- `get_wave`: read any Igor wave's full data and metadata back (any dimensionality, numeric/complex/text/wave-reference).\n- `load_experiment`: quit the running instance and relaunch Igor Pro with a .pxp file path as a launch argument.\n- `check_bridge_health`: diagnose whether this bridge can reach Igor Pro's ZeroMQ server right now.\n- `check_compilation_state` / `reload_and_compile_procedures`: check and refresh Igor's compiled state after editing a `.ipf` file on disk.\n- `dismiss_compile_error_dialog`: close a stuck \"Function Compilation Error\" dialog via a posted Escape key, without needing OS focus.\n- `get_debugger_state` / `set_debugger_enabled` / `restore_debugger_settings`: read, change, and restore Igor's Debugger settings.\n- `get_environment_summary`: summarize the live instance (version, loaded experiment, XOPs, included procedure files, data folders, Debugger settings).\n- `read_help_file`: read an Igor Pro help file (.ihf) as structured, formatted text.\n- `get_bridge_version`: report the running bridge version and Python/package versions.\n- `configure_igor_launch` / `launch_igor_pro_unattended`: record the Igor Pro executable path once per session, then launch it with the `/UNATTENDED` flag and wait for it to become reachable over ZeroMQ.\n\n**Requirements:**\n- Igor Pro 9.00 or later, running on Windows, with the ZeroMQ-XOP installed and loaded.\n- `Packages/MIES/ZMQ_BridgeHelpers.ipf` (or a copy of it) `#include`-d and compiled into the target experiment -- see `Packages/doc/igor-pro-bridge.rst` (\"Installation\") for the one-time setup steps for experiments other than this repo's own.\n- Python (accessible as `python` on PATH), with the pinned packages in `requirements.txt` installed into that same environment. Run `install.ps1` to do this correctly and also complete pywin32's required post-install step (still needed for window-handling/process-launch helpers, even though this bridge no longer uses COM) -- see that script's own help (`Get-Help ./install.ps1 -Full`).\n- No elevation/privilege-matching requirement of any kind -- this is the change v2.0.0 makes.\n\nPrior COM-based history (v1.x): see `Packages/doc/igor-pro-bridge.rst` for the full v1.x changelog, retained there for reference.", "author": { "name": "Michael Huth" }, diff --git a/tools/igor-mcp-bridge/server.py b/tools/igor-mcp-bridge/server.py index b7df7e769d..1cdc89925e 100644 --- a/tools/igor-mcp-bridge/server.py +++ b/tools/igor-mcp-bridge/server.py @@ -432,7 +432,9 @@ def _submit_and_poll(submit_function: str, command: str, timeout_seconds: float) @mcp.tool() -def execute_igor_command(command: str, timeout_seconds: float = _SUBMIT_POLL_TIMEOUT_SECONDS) -> dict: +def execute_igor_command( + command: str, timeout_seconds: float = _SUBMIT_POLL_TIMEOUT_SECONDS +) -> dict: """Execute a single Igor Pro command string in the running Igor instance. **If `command`'s runtime is unknown or could be long (more than roughly a @@ -481,7 +483,9 @@ def execute_igor_command(command: str, timeout_seconds: float = _SUBMIT_POLL_TIM @mcp.tool() -def execute_igor_command_unattended(command: str, timeout_seconds: float = _SUBMIT_POLL_TIMEOUT_SECONDS) -> dict: +def execute_igor_command_unattended( + command: str, timeout_seconds: float = _SUBMIT_POLL_TIMEOUT_SECONDS +) -> dict: """Run `command` exactly like execute_igor_command, but automatically disable Igor's Debugger before running it and restore it again afterward -- even if `command` raises an Igor-level error. @@ -714,15 +718,44 @@ def reload_and_compile_procedures() -> dict: doing, so a request arriving while COMPILEPROCEDURES is mid-rebuild of Igor's own internal function/symbol tables is a plausible cross-thread race. v2.3.0 mitigates this by stopping the ZeroMQ handler before RELOAD CHANGED PROCS/COMPILEPROCEDURES - run and restarting it only after compilation finishes (see - ZBR_StopHandlerBeforeRecompile/ZBR_SubmitReloadAndCompile in ZMQ_BridgeHelpers.ipf). - This is a well-reasoned mitigation, not a proven fix -- Igor64.exe ships no public - symbols, so the exact fault can't be confirmed from here, and the crash was already - rare/nondeterministic. If a tool call after this one starts failing anyway, - check_bridge_health() and be prepared for Igor Pro to need relaunching. + run and restarting it only after compilation finishes. This is a well-reasoned + mitigation, not a proven fix -- Igor64.exe ships no public symbols, so the exact + fault can't be confirmed from here, and the crash was already rare/nondeterministic. + If a tool call after this one starts failing anyway, check_bridge_health() and be + prepared for Igor Pro to need relaunching. + + **v2.3.1 fix -- handler could stay stopped forever on a failed compile**: v2.3.0's + restart path was AfterCompiledHook alone, which Igor only calls after a + *successful* compile. If the edited .ipf had a syntax error, COMPILEPROCEDURES + failed, AfterCompiledHook never fired, and the ZeroMQ handler -- already stopped by + ZBR_StopHandlerBeforeRecompile -- stayed stopped permanently, killing the bridge + with no recovery path short of restarting Igor. Fixed by ZBR_ArmRecompileWatchdog/ + ZBR_RecompileWatchdogTick in ZMQ_BridgeHelpers.ipf: a named background task, armed + right before the handler is stopped, that unconditionally rebinds/restarts the + handler regardless of whether the compile succeeds or fails. It is registered with + `start=60` (an explicit ~1-second floor before its first possible tick, independent + of its `period=30` interval) so it cannot fire before RELOAD CHANGED + PROCS/COMPILEPROCEDURES have finished draining Igor's operation queue -- confirmed + necessary by live timing instrumentation (stopmstimer(-2)) showing the background + task could otherwise tick within ~62ms of being armed, well before a real compile + (~414ms observed) finishes. AfterCompiledHook still restarts the handler + immediately on the success path; the watchdog is what covers the failure path, and + self-disarms (CtrlNamedBackground .. stop) the first time either one runs. + + **v2.3.1 fix -- ZBR_IsCompiled() checked the wrong module**: its FunctionInfo() call + was unqualified, so it resolved against ZBR's own (independent-module) namespace + -- which compiles separately from ProcGlobal -- instead of ProcGlobal's. This meant + it could report "compiled" even while ProcGlobal itself had a compile error. Fixed + by qualifying the lookup as `FunctionInfo("ProcGlobal#...")`. + + **v2.3.1 fix -- "Function Execution Module is still active" dialog**: an unrelated + pre-existing MIES background thread (not task) left running during + COMPILEPROCEDURES could raise this modal dialog and freeze the entire operation + queue. Mitigated by a new BeforeUncompiledHook in ZMQ_BridgeHelpers.ipf that calls + ThreadGroupRelease(-2) to release any running thread groups before Igor uncompiles. Mechanism: calls ZBR_SubmitReloadAndCompile(), which queues (as three independent - Execute/P entries) a call to stop the ZeroMQ handler, then + Execute/P entries) a call to stop the ZeroMQ handler and arm the watchdog, then `Execute/P "RELOAD CHANGED PROCS "`, then `Execute/P "COMPILEPROCEDURES "` Igor-side (see that function's docstring in ZMQ_BridgeHelpers.ipf), then polls for completion using two independent signals, same as the COM-based version did: @@ -741,7 +774,9 @@ def reload_and_compile_procedures() -> dict: a plain "::: error: " line there (readable via read_session_history), rather than a modal dialog. If NOT launched /UNATTENDED, a stuck "Function Compilation Error" dialog is also possible -- see - dismiss_compile_error_dialog. + dismiss_compile_error_dialog. As of v2.3.1, the bridge itself should still be + reachable in this case (see the watchdog fix above) -- fix the .ipf and call this + tool again rather than needing to relaunch Igor Pro. """ baseline_counter = _read_compile_counter() @@ -866,8 +901,16 @@ def set_debugger_enabled( "ZBR#ZBR_RestoreDebuggerSettings", [ 1, - int(current["debug_on_error"] if debug_on_error is None else debug_on_error), - int(current["debug_on_abort"] if debug_on_abort is None else debug_on_abort), + int( + current["debug_on_error"] + if debug_on_error is None + else debug_on_error + ), + int( + current["debug_on_abort"] + if debug_on_abort is None + else debug_on_abort + ), int( current["nvar_svar_wave_checking"] if nvar_svar_wave_checking is None @@ -959,7 +1002,9 @@ def get_environment_summary() -> dict: included_raw = call_function("ZBR#ZBR_WinList", ["*", "WIN:128"]) data_folders_raw = call_function("ZBR#ZBR_DataFolderDir", [3]) procedure_window_text = call_function("ZBR#ZBR_ProcedureText", ["", 0, "Procedure"]) - debugger_settings = _decode_debugger_state(call_function("ZBR#ZBR_GetDebuggerState")) + debugger_settings = _decode_debugger_state( + call_function("ZBR#ZBR_GetDebuggerState") + ) included_procedure_files = [ name for name in included_raw.split(";") if name and name != "Procedure" @@ -1077,7 +1122,9 @@ def read_help_file(file_path: str, timeout_ms: int = 30000) -> dict: tmp_fd, tmp_html_path = tempfile.mkstemp(suffix=".html", prefix="igor_help_") os.close(tmp_fd) - os.remove(tmp_html_path) # SaveNotebook must create it fresh; only the name is reused + os.remove( + tmp_html_path + ) # SaveNotebook must create it fresh; only the name is reused try: status = call_function( @@ -1120,7 +1167,7 @@ def read_help_file(file_path: str, timeout_ms: int = 30000) -> dict: # reason as before: the on-disk layout after Claude Desktop installs a .mcpb isn't # guaranteed to keep server.py and manifest.json at a fixed relative path, and this # needs to be confirmable from inside a conversation independent of that. -_BRIDGE_VERSION = "2.3.0" +_BRIDGE_VERSION = "2.3.1" def _installed_package_version(distribution_name: str) -> str | None: @@ -1620,14 +1667,20 @@ def load_experiment( env=_build_igor_launch_env(), ) except Exception as e: - raise RuntimeError(f"Failed to relaunch Igor Pro with {normalized!r}: {e}") from e + raise RuntimeError( + f"Failed to relaunch Igor Pro with {normalized!r}: {e}" + ) from e deadline = time.monotonic() + wait_for_ready_seconds attempts = 0 while time.monotonic() < deadline: attempts += 1 if _reachable(timeout_ms=1000): - return {"loaded_file": normalized, "zmq_ready": True, "poll_attempts": attempts} + return { + "loaded_file": normalized, + "zmq_ready": True, + "poll_attempts": attempts, + } time.sleep(_POST_LAUNCH_POLL_INTERVAL_SECONDS) return { From 4f645d79cca838ca02778db85b8181d614b13b53 Mon Sep 17 00:00:00 2001 From: Michael Huth Date: Thu, 6 Aug 2026 17:42:15 +0200 Subject: [PATCH 12/12] MCP: Add v2.3.2 support multiple igor pro instances - each igor instance requires a zeromq bind on a different port the default port is 5680. The configure_igor_launch MCP command supports setting a custom port. The port is set through an environment variable named IGOR_PRO_BRIDGE_PORT in the environment where Igor Pro is started. When started through the AI agent the environment variable is automatically set. - the agent can then switch between different igor pro instances identified by the port (or give it alias names if you like) - cleaned up the immense amount of code comments in the ipf file - use IgorStartOrNew hook to bind the zeromq server instead of the AfterCompileHook --- Packages/doc/igor-pro-bridge.rst | 31 +- tools/igor-mcp-bridge/ZMQ_BridgeHelpers.ipf | 743 ++++-------------- .../igor-pro-bridge-2.3.1.mcpb | Bin 37267 -> 0 bytes .../igor-pro-bridge-2.3.2.mcpb | Bin 0 -> 39909 bytes tools/igor-mcp-bridge/manifest.json | 6 +- tools/igor-mcp-bridge/pyproject.toml | 10 + tools/igor-mcp-bridge/server.py | 110 ++- 7 files changed, 286 insertions(+), 614 deletions(-) delete mode 100644 tools/igor-mcp-bridge/igor-pro-bridge-2.3.1.mcpb create mode 100644 tools/igor-mcp-bridge/igor-pro-bridge-2.3.2.mcpb create mode 100644 tools/igor-mcp-bridge/pyproject.toml diff --git a/Packages/doc/igor-pro-bridge.rst b/Packages/doc/igor-pro-bridge.rst index 147d86277b..cd77e40449 100644 --- a/Packages/doc/igor-pro-bridge.rst +++ b/Packages/doc/igor-pro-bridge.rst @@ -365,6 +365,13 @@ Available tools all three cases; the ``"problem"`` field lists all three as things to check by hand. Run this first whenever something doesn't work. + **As of v2.3.2**, this (like every ZeroMQ-talking function) connects to whichever + endpoint is currently configured via ``configure_igor_launch(port=...)`` -- if a + custom port was set but the target Igor Pro instance wasn't actually launched with + that same port in effect, this reports unreachable even though Igor Pro itself may + be running fine on its default port. The ``"problem"`` message on failure includes + the exact endpoint that was tried. + ``get_bridge_version()`` Returns the version of this Igor Pro Bridge build that is actually running in the current Claude Desktop session, plus which Python interpreter/packages it's actually @@ -524,7 +531,13 @@ Available tools (``OpenNotebook/R`` fails with error 251 otherwise) -- this tool handles the required ``CloseHelp/ALL`` -> ``OpenNotebook/R`` -> ``SaveNotebook`` export -> ``KillWindow/Z`` -> ``OpenHelp`` restore dance, entirely in a ``finally`` block so a mid-sequence - failure still restores whatever help state existed beforehand. Second, and more + failure still restores whatever help state existed beforehand. **Fixed in v2.3.2**: + the case where the expected new notebook window isn't found used to raise a plain + ``Abort ""``, which displays a real alert dialog the moment it executes -- + before the surrounding ``try``/``catch`` ever gets a chance to intervene, unlike an + ordinary runtime error. That would have hung unattended use exactly like every other + undismissable popup this bridge works around elsewhere. Fixed by setting the error + status directly instead of calling ``Abort`` at all in that branch. Second, and more importantly: the returned ``"paragraphs"`` list (``[{"style": "Topic", "text": "Debugging"}, ...]``) preserves the paragraph style name WaveMetrics' own help authoring convention assigns to nearly every paragraph (e.g. ``"Topic"`` for a section @@ -537,7 +550,7 @@ Available tools folders (both resolved via Igor's own ``SpecialDirPath`` function) before assuming a given XOP has one. -``configure_igor_launch(exe_path)`` +``configure_igor_launch(exe_path, port=None)`` Records the full path to the Igor Pro executable to use for ``launch_igor_pro_unattended``/``load_experiment``, for the rest of this bridge process's session. There is no default or guessed path -- whatever agent is driving @@ -546,6 +559,20 @@ Available tools alone has been tested against separately-named Igor Pro 9 and Igor Pro 10 installs). Session-scoped: resets if the bridge process itself restarts. + **``port``, added in v2.3.2** -- preparation for eventually talking to more than one + Igor Pro instance, not full simultaneous multi-instance support yet (this bridge + still only tracks one currently-configured endpoint at a time). Passing an integer + 1-65535 sets ``IGOR_PRO_BRIDGE_PORT`` in this bridge process's own environment; + ``launch_igor_pro_unattended``/``load_experiment`` inherit it when they start Igor + Pro, and the launched instance's ``ZBR_EnsureZeroMQBound`` (``ZMQ_BridgeHelpers.ipf``) + reads it back to decide which port to bind instead of its own default (5680). Every + ZeroMQ-talking function in this bridge also immediately starts targeting that same + port for its own connections (see ``check_bridge_health()`` above), not just the + next launch. **Omitting ``port`` (or passing ``None``) clears a previously-configured + custom port** -- it removes the environment variable entirely rather than leaving it + as-is, so calling this again to change just ``exe_path`` will also clear ``port`` + unless it's repeated on that same call. + ``launch_igor_pro_unattended(wait_for_ready_seconds=30.0)`` Launches the configured executable with the ``/UNATTENDED`` command-line flag (see :ref:`igor_pro_bridge_unattended_flag` below) and polls for it to become reachable diff --git a/tools/igor-mcp-bridge/ZMQ_BridgeHelpers.ipf b/tools/igor-mcp-bridge/ZMQ_BridgeHelpers.ipf index 67fa5c19d7..358524bb72 100644 --- a/tools/igor-mcp-bridge/ZMQ_BridgeHelpers.ipf +++ b/tools/igor-mcp-bridge/ZMQ_BridgeHelpers.ipf @@ -4,123 +4,34 @@ #pragma IndependentModule = ZBR #pragma version = 1.00 -// ZMQ_BridgeHelpers.ipf -- Igor Pro-side utility functions backing the Igor Pro Bridge -// (tools/igor-mcp-bridge/) from v2.0.0 onward, which now talks to Igor Pro over the -// ZeroMQ-XOP's CallFunction JSON protocol instead of COM Execute2/IWave/IDataFolder. +// ZMQ_BridgeHelpers.ipf -- Igor Pro-side helper functions for the Igor Pro Bridge +// (tools/igor-mcp-bridge/), which talks to Igor over the ZeroMQ-XOP's CallFunction JSON +// protocol. #include-d from Packages/MIES_Include.ipf; see igor-pro-bridge.rst for setup +// in other experiments, and SESSION_NOTES.md for full design rationale/history. // -// **No longer a throwaway prototype**: this file is now a real, permanent dependency of -// the bridge -- see SESSION_NOTES.md for the evaluation that led here and for the -// COM-vs-ZeroMQ trade-offs. #include-d from Packages/MIES_Include.ipf in this repo; any -// OTHER Igor Pro experiment that wants to use this bridge needs this file copied -// somewhere on its own procedure search path with a matching #include added by hand -- -// there is deliberately no auto-load/zero-setup mechanism (see igor-pro-bridge.rst for -// the one-time setup steps). -// -// Why an independent module (#pragma IndependentModule=ZBR): -// Per Igor's own "Advanced Topics.ihf" help (Independent Modules section): "An -// independent module is a set of procedure files that are compiled separately from all -// other procedures. Because it is compiled separately, an independent module can run -// when other procedures are in an uncompiled state because the user is editing them or -// because an error occurred in the last compile." Placing bridge-support code here means -// it stays callable via ZeroMQ's CallFunction even if the rest of the experiment (e.g. -// MIES) currently has a compile error -- unlike today's MIES_ClaudeHelper.ipf, which is -// an ordinary (non-independent) file and therefore goes down along with the rest of -// MIES's compile state. (Nothing stops MIES_ClaudeHelper.ipf from being restructured the -// same way independently of any transport change -- this benefit isn't unique to -// ZeroMQ.) -// -// The central design problem this file has to work around: Igor's `Execute` operation -// (used to run arbitrary command text, replicating COM's Execute2) cannot be called -// unqueued from inside a Function -- only `Execute/P` (deferred: queued to run only -// *after* the calling function returns to Igor's main loop) is legal there. This means a -// single ZeroMQ CallFunction round trip cannot synchronously "run this command and hand -// back what it printed" the way COM's Execute2 can, because the command hasn't actually -// run yet by the time the function returns and the reply is sent. -// -// The pattern used throughout below is submit-then-poll instead of a single blocking -// call: -// 1. ZBR_SubmitCommand(cmd) queues `cmd` and a call back into this module -// (ZBR_FinishToken) as TWO SEPARATE Execute/P entries (not one joined string), then -// returns a token immediately. This split matters: a single joined -// "cmd + ; + finishCall" string was tried first and found, via live testing, to be -// broken -- if `cmd` fails to parse OR hits a genuine runtime error partway through, -// Igor aborts the REST of that same top-level command string, so the appended -// finish-callback would silently never run, leaving ZBR_PollCommand reporting -// done=0 forever (indistinguishable from a job still genuinely running). Queuing -// `cmd` and the finish-callback as independent Execute/P entries avoids this: each -// runs (or fails) on its own, so the finish-callback always fires regardless of what -// happened to `cmd`. ZBR_SubmitCommandUnattended follows the same principle with its -// extra Debugger-disable/restore steps. -// 2. ZBR_PollCommand(token), called via a LATER, separate CallFunction request, reports -// whether it's done yet and returns whatever was printed while `cmd` ran (prefixed -// with "ERROR: ..." if `cmd` left a pending runtime error -- see ZBR_FinishToken). -// This mirrors (and reuses the same underlying mechanism as) how the COM bridge already -// has to defer COMPILEPROCEDURES/RELOAD CHANGED PROCS and SetIgorOption poundDefine via -// Execute/P -- that part is not new or specific to ZeroMQ, it's inherent to Igor's -// compile-safety model. -// -// Limitation of independent modules relevant here (same help topic, "Limitations of -// Independent Modules", #3): "Functions in an independent module can not call functions -// in other modules except through the Execute operation." This is exactly why the -// generic Execute/P-based command submission above is the right general-purpose escape -// hatch here, matching the pattern already proven live this session with the user's own -// ZMQ_TEST#SimpleExecute test function. Direct WAVE/DFREF references are NOT -// module-scoped, so ZBR_GetWaveGeneric below needs no Execute at all. -// -// What is deliberately NOT covered here, because it isn't something Igor procedure code -// can do at all -- these must stay implemented on the CLIENT side (e.g. in Python), -// regardless of which transport (COM or ZeroMQ) carries the request: -// - dismiss_compile_error_dialog: posts a raw Win32 WM_KEYDOWN/WM_KEYUP message to an -// arbitrary OS window handle. No Igor operation does this. -// - configure_igor_launch / launch_igor_pro_unattended: starting a whole new Igor Pro -// *process* has to be done from outside any already-running Igor Pro instance. -// - load_experiment: IApplication.LoadExperiment is COM-only (confirmed: neither -// "LoadExperiment" nor "OpenFile" appear anywhere in Igor Reference.ihf, only in -// Automation Server.ihf) -- there is no procedure-language way to hot-swap the open -// experiment from inside a running instance. The bridge now instead relaunches the -// Igor Pro *process* with the target file path as a launch argument (see -// launch_igor_pro_unattended's docstring) -- a real process restart, not an -// in-place swap, but needs no COM at all. -// - get_bridge_version's python_executable/mcp_package_version/pywin32_build fields: -// these describe the Python process, not Igor Pro. ZBR_Ping below is the Igor-side -// analogue -- confirms this module is loaded and reachable. -// -// read_help_file (CloseHelp/OpenNotebook/SaveNotebook/parse/restore, see -// ZBR_ReadHelpFile below) IS covered here, synchronously: none of those operations are -// subject to the Execute-only-from-top-level restriction that COMPILEPROCEDURES/RELOAD -// CHANGED PROCS need -- that restriction is specific to recompiling procedures while -// procedure code is running, not a blanket rule about every window/notebook operation. -// -// Verification status: the original submit/poll, wave-access, compilation-state, -// debugger-control, and ZeroMQ-bind functions were all written into a live Igor Pro 9 -// Nightly test session (via the v1.27 COM bridge, temporarily included from -// Packages/MIES/, reverted afterward) and exercised directly; a subset was also -// exercised for real over ZeroMQ via tools/zeromq-xop-test/call_igor_function_via_zmq.py. -// The introspection wrappers and ZBR_ReadHelpFile added for the v2.0.0 rewrite are new -// and have only been syntax/compile-checked so far -- see SESSION_NOTES.md for exactly -// what has and hasn't been live-verified. +// Compiles as its own independent module (#pragma IndependentModule=ZBR) so it stays +// reachable via CallFunction even if the rest of the experiment has a compile error. +// Execute cannot run unqueued from inside a Function, so most operations here use a +// submit (Execute/P) + poll pattern instead of one blocking call -- see +// ZBR_SubmitCommand/ZBR_PollCommand. // --- Constants ------------------------------------------------------------------------- -/// Reported by ZBR_Ping -- kept as a named constant (rather than inline in the sprintf -/// call) per this repo's standing convention against unexplained literals, and so it -/// only needs updating in one place if this module's version ever changes independently -/// of the #pragma version above. +/// Reported by ZBR_Ping. static StrConstant ZBR_VERSION_STR = "1.00" -/// Local endpoint this module's ZeroMQ ROUTER (server) socket (re-)binds to on every -/// compile -- see ZBR_EnsureZeroMQBound/AfterCompiledHook below. Deliberately not -/// MIES_Constants.ipf's own ZEROMQ_BIND_REP_PORT (5670) -- this prototype needs its own -/// port so it can't collide with MIES's real ZeroMQ subsystem -/// (MIES_MiesUtilities_ZeroMQ.ipf's StartZeroMQSockets) if that's ever active in the -/// same experiment. Matches the port already used throughout this session's own manual -/// testing (tools/zeromq-xop-test/call_igor_function_via_zmq.py's default endpoint). -static StrConstant ZBR_ZEROMQ_ENDPOINT = "tcp://127.0.0.1:5680" +/// ZeroMQ ROUTER (server) socket endpoint -- distinct from MIES's own ZeroMQ port +/// (MIES_MiesUtilities_ZeroMQ.ipf) to avoid collisions. +static StrConstant ZBR_ZEROMQ_ENDPOINT = "tcp://127.0.0.1" +static Constant ZBR_ZEROMQ_DEFAULT_PORT = 5680 +static StrConstant ZBR_ZEROMQ_ENV_PORT = "IGOR_PRO_BRIDGE_PORT" + +static StrConstant ZBR_RECOMPILE_WATCHDOG_TASK = "ZBR_RecompileWatchdog" // --- Storage ------------------------------------------------------------------------- -/// Parallel-array storage for in-flight ZBR_SubmitCommand() calls, indexed by row. -/// A row's index (as a string) is the "token" handed back to the caller. +/// Parallel-array storage for in-flight ZBR_SubmitCommand() calls, indexed by row. A +/// row's index (as a string) is the token handed back to the caller. static Function ZBR_EnsureStorage() NewDataFolder/O root:Packages @@ -134,55 +45,9 @@ static Function ZBR_EnsureStorage() endif End -/// One-time CaptureHistoryStart() so ZBR_SubmitCommand/ZBR_FinishToken can diff Igor's -/// history area to recover what a deferred command printed. Mirrors the same mechanism -/// (and the same "start once, read incrementally" usage pattern) the COM bridge already -/// uses for read_session_history. -/// -/// CaptureHistory's real signature -- confirmed from Igor Reference.ihf, since a first -/// draft of this file wrongly assumed a single-argument CaptureHistory(stopCapturing) -/// and failed to compile -- is CaptureHistory(refnum, stopCapturing): refnum must be the -/// value CaptureHistoryStart() returned, not omitted. -/// -/// **Confirmed live bug, now fixed**: `root:Packages:ZBR:captureRefNum` is a plain -/// `Variable/G`, which Igor persists into a saved experiment like any other global -- -/// but the refnum it holds is only meaningful within the OS process that called -/// CaptureHistoryStart() to create it. Reloading a saved experiment (via this bridge's -/// own load_experiment, or the user manually reopening a .pxp) brings the OLD numeric -/// value back even though the process is brand new, so the mere *existence* check this -/// function used to do (`NVAR_Exists(refnum)`) was not enough -- it happily trusted a -/// stale refnum from a now-dead process. Using it then throws a genuine Igor runtime -/// error ("there is no open file with this reference number"), which -- since this can -/// be reached from a plain top-level Execute/P entry, not just from inside a Try/Catch -/// higher up -- pops a real modal error dialog and blocks Igor's whole main thread -/// (and therefore every ZeroMQ reply) until a human dismisses it. Confirmed live by -/// saving+reloading an experiment via load_experiment and then calling -/// execute_igor_command, which triggered exactly this dialog. -/// -/// Fix: don't just check existence, actually try using the stored refnum, wrapped in a -/// try-catch-endtry block (see "Flow Control for Aborts" in Igor's own Programming.ihf -/// help) with an explicit AbortOnRTE right after the risky call -- a runtime error -/// inside a try block does NOT by itself jump to catch, only AbortOnRTE converts it -/// into an abort that does (confirmed from Igor's own help; an earlier version of this -/// fix omitted AbortOnRTE and also used a bare `return` with no value inside a plain, -/// implicit-Variable-returning Function, which is invalid and failed to compile -- -/// see Igor's help, "The Return Statement": "The type of the returned value must -/// agree with the type declared in the function declaration"). try-catch-endtry -/// suppresses the error dialog for anything aborted inside the try block (that's its -/// whole documented purpose), so this also prevents the dialog described above from -/// appearing at all going forward. If the stored refnum turns out stale, silently -/// start a fresh capture and overwrite the stored global instead of ever surfacing -/// this to the user. -/// -/// **User refinement**: CaptureHistory(...) and AbortOnRTE are deliberately kept on -/// the SAME line, not split across two lines the way this was first written. Igor's -/// Debug on Error check happens at the END of each line, not each statement -- if the -/// probe call and AbortOnRTE were on separate lines, Debug on Error (if the user -/// happens to have it enabled) would trigger a Debugger popup right when the stale -/// refnum's runtime error occurs, before AbortOnRTE ever gets a chance to convert it -/// into a catchable abort. Keeping both on one line means the end-of-line check only -/// happens after AbortOnRTE has already run, so there's nothing left pending to -/// trigger the Debugger. +/// Ensures a valid CaptureHistoryStart() refnum is active, recovering if the stored one +/// is stale (e.g. after reloading a saved experiment). See SESSION_NOTES.md for why the +/// probe call and AbortOnRTE must stay on the same line. static Function ZBR_EnsureCaptureStarted() string dummy @@ -200,7 +65,7 @@ static Function ZBR_EnsureCaptureStarted() try dummy = CaptureHistory(refnumRW, 0); AbortOnRTE catch - err = GetRTError(1) // clear the trapped error; discard the specific code, we always recover the same way + err = GetRTError(1) refnumRW = CaptureHistoryStart() endtry endif @@ -232,10 +97,8 @@ End // --- Generic command execution (submit/poll) ------------------------------------------ -/// Allocate a new token/storage row for an in-flight submission -- shared by -/// ZBR_SubmitCommand and ZBR_SubmitReloadAndCompile (the latter needs its own submit -/// function since its two commands must be queued separately, not joined into one -/// compound Execute/P string -- see its docstring below). +/// Allocates a new token/storage row, shared by ZBR_SubmitCommand and +/// ZBR_SubmitReloadAndCompile. static Function/S ZBR_AllocateToken() variable n @@ -255,21 +118,9 @@ static Function/S ZBR_AllocateToken() return num2istr(n) End -/// Queue `cmd` for deferred execution and return a token to poll for its result via -/// ZBR_PollCommand(). Does NOT run `cmd` synchronously -- see the module docstring above -/// for why that's not possible from inside a Function at all, independent module or not. -/// -/// IMPORTANT: `cmd` and the finish-callback are queued as TWO SEPARATE Execute/P entries, -/// not joined into one string with ";". This is deliberate and was learned the hard way: -/// if `cmd` fails to parse, OR hits a genuine runtime error partway through, Igor aborts -/// the REST of that same top-level command string -- so a joined "cmd; finishCall" string -/// would silently drop the finish-callback whenever cmd errors, leaving ZBR_PollCommand -/// reporting done=0 forever with no way to distinguish that from a job still genuinely -/// running. Queuing them as independent Execute/P entries avoids this: each one runs (or -/// fails) on its own, regardless of what happened to the entry before it. This was verified -/// live: two separately-queued Execute/P entries (one invalid, one valid) both ran their -/// own outcome independently, whereas joining them with ";" let a failure in the first -/// swallow the second. +/// Queues `cmd` for deferred execution and returns a token to poll via ZBR_PollCommand(). +/// `cmd` and the finish-callback are queued as separate Execute/P entries so the +/// callback still runs even if `cmd` fails to parse or errors -- see SESSION_NOTES.md. Function/S ZBR_SubmitCommand(string cmd) string token, finishCall @@ -282,19 +133,9 @@ Function/S ZBR_SubmitCommand(string cmd) return token End -/// Same as ZBR_SubmitCommand, but disables Igor's Debugger for the duration of `cmd` and -/// restores its exact prior settings afterward -- mirrors execute_igor_command_unattended's -/// reason for existing (a Debugger pause has no scriptable resume and would otherwise hang -/// forever). -/// -/// The disable step, `cmd`, the restore step, and the finish-callback are FOUR SEPARATE -/// Execute/P entries (not one joined string), for the same reason described in -/// ZBR_SubmitCommand's docstring: if `cmd` errors, anything appended after it in the same -/// string would never run. Here that would mean the Debugger stays disabled forever after -/// any erroring `cmd`, in addition to the finish-callback never firing. Because a plain -/// local variable does not survive the boundary between separate top-level Execute/P -/// entries, the saved Debugger settings are stashed in persistent globals under -/// root:Packages:ZBR instead, and the restore entry reads them back from there. +/// Same as ZBR_SubmitCommand, but disables Igor's Debugger for `cmd`'s duration and +/// restores it afterward via persistent globals (plain locals don't survive the +/// boundary between separate Execute/P entries). Function/S ZBR_SubmitCommandUnattended(string cmd) string token, finishCall, restore @@ -325,43 +166,9 @@ Function/S ZBR_SubmitCommandUnattended(string cmd) return token End -/// Callback queued by ZBR_SubmitCommand/ZBR_SubmitCommandUnattended as its own, separate -/// deferred entry -- runs after `cmd` (and, for the unattended path, after the Debugger -/// restore entry) regardless of whether those entries succeeded, errored, or failed to -/// parse, so by the time a later ZBR_PollCommand() call sees done[idx] == 1, resultText[idx] -/// is guaranteed fully populated. Public (non-static) because it's invoked via a qualified -/// name (ZBR#ZBR_FinishToken) from a queued Execute/P string. -/// -/// Also checks GetRTError(1), on the theory that a runtime error left pending by `cmd` -/// could be surfaced here as an explicit "ERROR: ..." prefix. **Confirmed live NOT to -/// work**: GetRTError(1) reads 0 here even immediately after a `cmd` that genuinely -/// errored (tested with both an unparseable command and a genuine runtime error -- -/// WaveStats on a non-existent wave). Root cause: each Execute/P entry is dispatched as -/// its own independent top-level execution, the same as if a person had typed it at the -/// command line and pressed enter separately -- Igor resolves and clears any runtime-error -/// state as part of returning that entry to idle, before the queue advances to the next -/// entry, so nothing is left pending for a later, separate entry (like this callback) to -/// read. Also confirmed: Igor does not append anything about the error to the -/// CaptureHistory-tracked history stream either, so ZBR_HistorySince(historyStart[idx]) -/// alone won't reveal it. Net effect: **there is currently no reliable, generic way for a -/// caller to distinguish "cmd ran and legitimately printed nothing" from "cmd errored out -/// partway through with no output"** -- both look identical (done=true, empty result). The -/// check below is kept as a harmless no-op/best-effort in case some other error path does -/// leave state behind, but callers should not rely on it. -/// -/// **Bounds-checks idx before writing, confirmed live necessary**: `idx` is captured by -/// ZBR_AllocateToken at submission time, but this callback runs later, in its own -/// separate deferred Execute/P entry -- if the `done`/`resultText`/`historyStart` waves -/// are ever resized smaller in between (e.g. maintenance code clearing out old/orphaned -/// tokens, as happened live during this bridge's own development), `idx` can end up -/// pointing past the end of the (now-shorter) waves. Writing to an out-of-range wave -/// index throws an uncaught Igor runtime error ("Index out of range for wave..."), which -/// -- exactly like the CaptureHistory bug this module already works around -- pops a -/// real modal dialog and blocks Igor's entire main thread until a human dismisses it. -/// If idx no longer refers to a real row, there is nothing useful left to do (that -/// token's storage is simply gone), so just skip the write silently rather than crash; -/// ZBR_PollCommand already reports "ERROR: unknown token" for exactly this case via its -/// own DimSize check, so the caller still gets a clear, non-hanging answer. +/// Deferred callback that finalizes a submitted command's result row. Bounds-checks idx +/// before writing since storage may have been resized since submission. See +/// SESSION_NOTES.md for why GetRTError(1) here can't reliably detect that `cmd` errored. Function ZBR_FinishToken(variable idx) variable err @@ -385,15 +192,9 @@ Function ZBR_FinishToken(variable idx) endif End -/// Poll a token from ZBR_SubmitCommand/ZBR_SubmitCommandUnattended. isDone is 0 while -/// still pending (result is then always ""); once isDone is 1, result holds everything -/// printed to history while the command ran. This is now guaranteed to eventually reach -/// isDone==1 even if the submitted command failed to parse or hit a genuine runtime error -/// partway through (see ZBR_SubmitCommand's docstring) -- but note there is currently no -/// generic way to tell that case apart from "ran fine and simply printed nothing": both -/// come back as an empty result (see ZBR_FinishToken's docstring for why the obvious -/// GetRTError(1)-based approach to detecting this doesn't work). If a command's success -/// needs to be verifiable, have it `print` an explicit sentinel value/message itself. +/// Polls a token from ZBR_SubmitCommand/ZBR_SubmitCommandUnattended. isDone=0 while +/// pending; once isDone=1, result holds everything printed while the command ran (see +/// SESSION_NOTES.md for the "ran fine vs. errored silently" ambiguity). Function [variable isDone, string result] ZBR_PollCommand(string token) variable idx @@ -416,11 +217,7 @@ End // --- Wave access ----------------------------------------------------------------------- -/// Return a wave by its full data-folder path (e.g. "root:MyFolder:mywave"). WAVE/DFREF -/// references are not module-scoped, so this needs no Execute at all -- and unlike the -/// COM bridge's current per-point GetNumericWavePointValue loop, the ZeroMQ-XOP -/// serializes the ENTIRE wave (dimensions, units, note, complex/text/wave-ref support) -/// from this one call, per its documented wave serialization format. +/// Returns a wave by its full data-folder path. Function/WAVE ZBR_GetWaveGeneric(string wavePath) WAVE/Z w = $wavePath @@ -429,106 +226,26 @@ End // --- Compilation state ------------------------------------------------------------- -/// Same trick already used by check_compilation_state (and by -/// Packages/igortest/procedures/igortest-test-compilation.ipf's IsProcGlobalCompiled()): -/// FunctionInfo() for a deliberately non-existent function returns "" when procedures -/// are compiled, and a non-empty string ("Procedures Not Compiled") otherwise. No -/// Execute needed -- FunctionInfo is a plain built-in function. -/// -/// **Bug fixed (identified live by the repo owner while testing the v2.3.1 recompile -/// watchdog): the function name passed to FunctionInfo() MUST be qualified with the -/// "ProcGlobal#" prefix, exactly as igortest-test-compilation.ipf's own -/// IsProcGlobalCompiled() already does (`FunctionInfo("ProcGlobal#NON_EXISTING_FUNCTION")`) -/// -- an earlier version of this function omitted it. Per Igor's own FunctionInfo -/// documentation (Igor Reference.ihf): an unqualified functionNameStr resolves relative -/// to the CALLING function's own module context, and explicitly names "ProcGlobal" as a -/// valid independent-module qualifier for asking about a DIFFERENT module's namespace -/// (their own example: "Procedure [ProcGlobal]" to reach the main procedure window from -/// inside an independent module). Since ZBR_IsCompiled() itself is compiled INTO the ZBR -/// independent module, the unqualified name resolved against ZBR's OWN (always-fine) -/// compile state instead of ProcGlobal's -- meaning this function was silently reporting -/// "compiled" no matter what ProcGlobal's real state was, confirmed live: with a -/// deliberately broken ProcGlobal function in place, this returned true (via -/// check_compilation_state) while a plain `print FunctionInfo("ProcGlobal#...")` -/// dispatched through the ordinary top-level command queue correctly reported -/// "Procedures Not Compiled". reload_and_compile_procedures() happened to still report -/// the correct answer during that same test only because its PRIMARY signal (the -/// AfterCompiledHook-driven compile counter) never advanced -- this function was a -/// silently-broken fallback the whole time it existed. +/// True if ProcGlobal is compiled. Must qualify the FunctionInfo() probe with +/// "ProcGlobal#" -- an unqualified name resolves against this (always-compiled) +/// independent module instead. See SESSION_NOTES.md. Function ZBR_IsCompiled() return strlen(FunctionInfo("ProcGlobal#ZBR_DefinitelyNotARealFunctionName_8f3a1c")) == 0 End -/// Read root:gClaudeHelperCompileCounter (bumped by AfterCompiledHook below every time -/// Igor confirms a successful compile -- see that function) without creating it if -/// missing. Returns -1 (a real counter value can never be negative) if the global -/// doesn't exist yet, e.g. before this module's own first compile -- mirrors the COM -/// bridge's _read_claude_helper_compile_counter/_CLAUDE_HELPER_COMPILE_COUNTER_CMD -/// exactly, just as a direct typed call instead of an fprintf-wrapped command string. -/// Race-free by construction: unlike ZBR_IsCompiled's FunctionInfo poll, this only ever -/// changes at the exact moment Igor itself confirms a successful compile, so any -/// observed increase over a baseline read before triggering a reload/compile is -/// trustworthy immediately, no repeated-confirmation dance needed. +/// Reads root:gClaudeHelperCompileCounter (bumped by AfterCompiledHook on every +/// successful compile) without creating it. Returns -1 if not yet created. Function ZBR_ReadCompileCounter() return NumVarOrDefault("root:gClaudeHelperCompileCounter", -1) End -/// RELOAD CHANGED PROCS / COMPILEPROCEDURES are themselves restricted the same way -/// Execute is -- not a new restriction introduced by this module; the COM bridge -/// already has to defer these exact same operations via Execute/P (see that bridge's -/// reload_and_compile_procedures). -/// -/// **Correction (user-supplied): the two commands must be issued as separate -/// Execute/P calls, not joined into one compound string via ";" the way -/// ZBR_SubmitCommand does for arbitrary commands -- and each needs its own mandatory -/// trailing space ("RELOAD CHANGED PROCS ", "COMPILEPROCEDURES ").** -/// -/// Deliberately does NOT use the ZBR_SubmitCommand/ZBR_PollCommand token+callback -/// mechanism, despite that being the obvious first attempt (and what an earlier -/// version of this function did) -- confirmed live that a finish-callback queued via -/// Execute/P *after* COMPILEPROCEDURES never actually runs: recompiling the whole -/// procedure set appears to discard/invalidate whatever was still pending behind it -/// in Igor's operation queue, rather than letting it complete afterward -/// (ZBR_FinishToken's target row stayed permanently un-done in a live test, with no -/// error reported anywhere). Poll ZBR_IsCompiled() instead -- already a direct, -/// synchronous, standalone check that doesn't depend on anything surviving the -/// recompile -- to find out when this has taken effect. -/// -/// **User-identified crash hypothesis, now addressed here**: this bridge has hit -/// multiple unexplained `EXCEPTION_ACCESS_VIOLATION` crashes deep inside Igor64.exe -/// itself (confirmed via crash-dump analysis, see SESSION_NOTES.md) coinciding with -/// COMPILEPROCEDURES, with no root cause ever identified. `igortest-tracing.ipf`'s own -/// `CompileAndRestart()` runs the exact same RELOAD-CHANGED-PROCS/COMPILEPROCEDURES -/// pair reliably, with no crashes ever observed -- the key structural difference -/// (per direct user review) is that nothing else can call into Igor between -/// `CompileAndRestart()` running and Igor itself firing `AfterCompiledHook`, whereas -/// this bridge's ZeroMQ-XOP runs "a threaded message handler" (its own help file's -/// wording, ZeroMQ.ihf) that keeps dispatching incoming CallFunction requests in the -/// background regardless of what Igor's main thread is doing. If a new CallFunction -/// request -- including this bridge's own compile-status polling, or any other tool -/// call that happens to be in flight -- gets dispatched while Igor's main thread is -/// mid-COMPILEPROCEDURES (tearing down and rebuilding its own internal -/// compiled-function/symbol tables), that's a genuine cross-thread race on those very -/// tables, and a bad-pointer read deep inside Igor64.exe (exactly what both crash dumps -/// showed) is a very plausible symptom. -/// -/// Fix: stop the ZeroMQ handler (`zeromq_handler_stop()`, queued via -/// ZBR_StopHandlerBeforeRecompile so it runs only after THIS call's own reply has -/// already gone out -- see that function's docstring) before RELOAD CHANGED -/// PROCS/COMPILEPROCEDURES ever run, so nothing can be dispatched into Igor while -/// it's mid-recompile. This is a mitigation based on a well-reasoned but not -/// 100%-certain mechanism (Igor64.exe ships no public symbols, so the exact fault can't -/// be proven from here) -- see SESSION_NOTES.md for the full reasoning and its honest -/// limitations. -/// -/// **v2.3.1**: the handler is restarted via TWO independent paths, not just -/// AfterCompiledHook -- see ZBR_ArmRecompileWatchdog/ZBR_RecompileWatchdogTick's -/// docstrings for why AfterCompiledHook alone was a real, live-confirmed bug (it never -/// runs at all if the compile attempt fails, leaving the handler stopped forever) and -/// for the actual, empirically-verified fix (a `start=60`-armed named background task -/// that restarts the handler unconditionally, whether the compile succeeded or failed). +/// Queues ZBR_StopHandlerBeforeRecompile, RELOAD CHANGED PROCS, and COMPILEPROCEDURES as +/// three separate Execute/P entries (each needs its own trailing space). Poll +/// ZBR_IsCompiled()/ZBR_ReadCompileCounter() rather than a finish-callback -- anything +/// queued behind COMPILEPROCEDURES is discarded by the recompile. See SESSION_NOTES.md +/// for the crash mitigation this exists for. Function ZBR_SubmitReloadAndCompile() Execute/P/Q/Z "ZBR#ZBR_StopHandlerBeforeRecompile()" @@ -538,28 +255,8 @@ Function ZBR_SubmitReloadAndCompile() return 0 End -/// Stops this module's ZeroMQ message handler thread (zeromq_handler_stop()) -- queued -/// as its own independent Execute/P entry by ZBR_SubmitReloadAndCompile, deliberately -/// BEFORE RELOAD CHANGED PROCS/COMPILEPROCEDURES, so no new CallFunction request can be -/// dispatched into Igor while it's mid-recompile. See ZBR_SubmitReloadAndCompile's own -/// docstring for the full crash-hypothesis reasoning this targets. -/// -/// Deliberately NOT called directly/synchronously from inside ZBR_SubmitReloadAndCompile -/// itself -- that function's own invocation is, in the end, just another CallFunction -/// request being served by the very same handler this stops. Queuing the stop via -/// Execute/P instead guarantees it only actually runs after Igor has returned to idle, -/// i.e. after THIS call's own reply has already been sent back over ZeroMQ -- avoiding -/// any risk of the handler being stopped out from under its own in-flight response. -/// -/// Does not call zeromq_stop() (which would tear down every ZeroMQ bind/connection for -/// the whole Igor Pro instance, not just this module's own -- see ZBR_EnsureZeroMQBound's -/// docstring for why that's avoided elsewhere too) -- zeromq_handler_stop() is the -/// narrower, paired stop for zeromq_handler_start(), per ZeroMQ.ihf. -/// -/// **v2.3.1: also arms ZBR_ArmRecompileWatchdog here** -- see that function's docstring -/// for the concept flaw in the original v2.3.0 design this fixes (AfterCompiledHook, -/// which used to be the ONLY thing that restarted the handler, never fires at all if the -/// upcoming compile attempt fails). +/// Stops the ZeroMQ handler and arms the recompile watchdog before RELOAD CHANGED +/// PROCS/COMPILEPROCEDURES run. Function ZBR_StopHandlerBeforeRecompile() variable err @@ -570,72 +267,22 @@ Function ZBR_StopHandlerBeforeRecompile() return 0 End -/// Name of the one-shot named background task armed by ZBR_ArmRecompileWatchdog. Named -/// (not the legacy unnamed CtrlBackground task) per Igor's own recommendation ("New code -/// should use named background tasks") and so it can't collide with any unrelated -/// background task some other part of this experiment (e.g. MIES's own) might be running. -static StrConstant ZBR_RECOMPILE_WATCHDOG_TASK = "ZBR_RecompileWatchdog" +/// Restarts the ZeroMQ handler only -- does not rebind the socket. See +/// ZBR_EnsureZeroMQBound's docstring for why binding now lives elsewhere. +Function ZBR_StartHandlerAfterRecompile() + + variable err -/// **v2.3.1 fix for a concept flaw in v2.3.0, identified live by the repo owner**: v2.3.0 -/// stopped the ZeroMQ handler before RELOAD CHANGED PROCS/COMPILEPROCEDURES and relied -/// entirely on AfterCompiledHook (via its existing, unchanged ZBR_EnsureZeroMQBound() -/// call) to restart it afterward. But per Igor's own Advanced Topics.ihf, "AfterCompiledHook -/// is a user-defined function that Igor calls after the procedure windows have all been -/// compiled successfully" -- if the triggering reload/compile attempt instead FAILS (e.g. -/// a syntax error in whatever .ipf was just edited), AfterCompiledHook never runs at all, -/// so the handler this bridge deliberately stopped moments earlier would stay stopped -/// forever, killing the entire bridge until a human manually fixes the compile error and -/// recompiles via Igor's own GUI. Confirmed live: exactly this scenario (a deliberately -/// broken ProcGlobal compile) left the bridge permanently unreachable under the -/// unpatched v2.3.0 code. -/// -/// A plain Execute/P entry queued to run right after COMPILEPROCEDURES was considered and -/// rejected: ZBR_SubmitReloadAndCompile's own docstring already documents (from earlier, -/// unrelated live testing) that anything queued behind COMPILEPROCEDURES in Igor's -/// deferred *operation queue* gets silently discarded/invalidated by the recompile, so -/// that mechanism can't be trusted here either, regardless of success or failure. -/// -/// A NAMED BACKGROUND TASK is a different Igor subsystem entirely (driven by Igor's own -/// idle-time task scheduler, not the deferred operation queue), and -- per Igor's own -/// Background Tasks help (Advanced Topics.ihf): "If you need your background task to -/// continue running even if you edit other procedures in Igor, you need to make your -/// project an independent module" -- a background task whose target function lives in an -/// independent module (like ZBR_RecompileWatchdogTick here, in this same ZBR module) -/// keeps running/ticking even while ProcGlobal is uncompiled or failed to compile. -/// -/// **Correction, found live via direct timer instrumentation (repo owner added -/// stopmstimer(-2) printouts at the start of the queue, in this function, and in -/// AfterCompiledHook, then had Claude recompute the deltas)**: an earlier version of this -/// docstring claimed background tasks "can't tick at all while Igor's main thread is -/// actually busy compiling," and concluded from that there was "no risk of this watchdog -/// firing while COMPILEPROCEDURES is still actually running." That conclusion was -/// DISPROVEN by the timer data: with a plain `start` (no explicit startTicks) and -/// period=30, the watchdog tick fired only ~62ms after the operation queue began -/// draining, while the actual compile (confirmed via AfterCompiledHook's own timestamp) -/// didn't finish until ~414ms in -- i.e. the watchdog CAN and did fire before -/// COMPILEPROCEDURES had actually finished. Background tasks and the deferred operation -/// queue are apparently NOT strictly ordered with respect to each other the way this -/// module's earlier reasoning assumed. -/// -/// **Actual fix: the `start=60` argument below** (distinct from a bare `start`) sets an -/// explicit ~1-second floor (60 ticks, ~1/60s each) before the watchdog's EARLIEST -/// possible first tick, decoupled from `period` (which only governs the interval between -/// ticks after the first one). The correctness property this needs is not "fires after -/// AfterCompiledHook" -- AfterCompiledHook never runs at all on a failed compile, so -/// there's nothing to compare against on that path -- it's "does not fire before the -/// operation queue (ZBR_StopHandlerBeforeRecompile's own queuing, then RELOAD CHANGED -/// PROCS, then COMPILEPROCEDURES) has actually finished draining." The repo owner's -/// judgment call: the brief setup overhead before RELOAD CHANGED PROCS begins, plus any -/// realistic compile of this codebase, comfortably finishes within that 1-second floor -/// (every recompile observed this session, including deliberately large ones, finished in -/// well under a second) -- not a mathematically airtight guarantee against an arbitrarily -/// slow future compile, but a practical, generous margin over anything actually observed. -/// -/// Called from ZBR_StopHandlerBeforeRecompile, i.e. right before RELOAD CHANGED -/// PROCS/COMPILEPROCEDURES run. `start` on an already-running named background task is -/// tolerated (err cleared) rather than propagated, since overlapping/concurrent -/// reload-and-compile attempts (already live-tested elsewhere, see SESSION_NOTES.md) would -/// otherwise each try to arm the same task name. + zeromq_handler_start(); err = GetRTError(1) + + return 0 +End + +/// Arms a named background task that unconditionally restarts the ZeroMQ handler after a +/// reload/compile attempt, whether it succeeded or failed (AfterCompiledHook alone can't +/// cover the failure case). `start=60` sets an explicit ~1s floor before the first +/// possible tick, since background tasks and the deferred operation queue are not +/// strictly ordered -- see SESSION_NOTES.md for the timing data behind this value. static Function ZBR_ArmRecompileWatchdog() variable err @@ -646,23 +293,12 @@ static Function ZBR_ArmRecompileWatchdog() return 0 End -/// Fires once, at least ~1 second after ZBR_ArmRecompileWatchdog armed it (see that -/// function's docstring for why the `start=60` floor -- not "background tasks can't run -/// during a compile" -- is what actually keeps this safely behind the triggering RELOAD -/// CHANGED PROCS/COMPILEPROCEDURES attempt finishing, whether it succeeded or failed). -/// Unconditionally restarts this module's ZeroMQ server/handler via -/// ZBR_EnsureZeroMQBound() -- harmless even if AfterCompiledHook already did the exact -/// same thing moments earlier on a successful compile, since that function already -/// tolerates being called when already bound/started (see its own docstring) -- then -/// stops itself (returning 1, which per Igor's own documented convention for background -/// task functions is how a task tells Igor to stop calling it again). -/// -/// Public (non-static): background-task `proc=` targets, like Execute/P-dispatched -/// callbacks elsewhere in this module (e.g. ZBR_FinishToken), need to be resolvable from +/// Restarts the ZeroMQ handler via ZBR_StartHandlerAfterRecompile() and self-disarms. +/// Public (non-static): CtrlNamedBackground's proc= target must be resolvable from /// outside this function's own immediate caller. Function ZBR_RecompileWatchdogTick(STRUCT WMBackgroundStruct &s) - ZBR_EnsureZeroMQBound() + ZBR_StartHandlerAfterRecompile() CtrlNamedBackground $ZBR_RECOMPILE_WATCHDOG_TASK, stop return 1 @@ -670,8 +306,6 @@ End // --- Debugger control ---------------------------------------------------------------- -/// Direct (non-deferred) call -- confirmed live that DebuggerOptions, unlike -/// COMPILEPROCEDURES, is NOT restricted to top-level/Execute-only use. Function [variable enable, variable debugOnError, variable debugOnAbort, variable nvarChecking] ZBR_GetDebuggerState() DebuggerOptions @@ -702,52 +336,38 @@ End // --- Direct built-in introspection wrappers ------------------------------------------- // -// Each of these is exactly one synchronous CallFunction round trip: a thin wrapper -// around a single read-only Igor built-in function with no side effects and no -// Execute-restriction, so none of them need the submit/poll pattern above. Deliberately -// generic (parameters passed straight through) rather than one bespoke wrapper per -// COM-bridge tool -- structuring/parsing the returned raw strings into a proper dict -// happens client-side (Python), exactly mirroring how the COM bridge already worked (it -// also just ran fprintf-wrapped built-in calls and parsed the raw string results in -// Python) -- see get_environment_summary in server.py for where these get assembled. - -/// IgorInfo(n) -- e.g. n=0 for the version/build/memory/screen report string, n=3 for -/// OS info, n=10 for the semicolon-separated loaded-XOPs list, n=11/12 for the current -/// experiment's file kind/name. See Igor Reference.ihf for the full index table. +// Thin, generic passthroughs to read-only Igor built-ins -- structuring/parsing the +// returned raw strings into a proper dict happens client-side (Python); see +// get_environment_summary in server.py for where these get assembled. + +/// IgorInfo(n) passthrough -- see Igor Reference.ihf for the index table. Function/S ZBR_IgorInfo(variable n) return IgorInfo(n) End -/// WinList(matchStr, ";", options) -- e.g. ZBR_WinList("*", "WIN:128") for included -/// procedure windows/files, ZBR_WinList("*", "WIN:512") for help windows. +/// WinList(matchStr, ";", options) passthrough. Function/S ZBR_WinList(string matchStr, string options) return WinList(matchStr, ";", options) End -/// ProcedureText(funcName, flags, winTitle) -- pass funcName="" and winTitle=a specific -/// window name (e.g. "Procedure") to retrieve that whole window's contents, per the -/// hard-won finding recorded in the COM bridge's own get_environment_summary comment: -/// the window name goes in the THIRD argument, not the first -- passing it as the first -/// argument instead silently returns "" rather than raising an error. +/// ProcedureText(funcName, flags, winTitle) passthrough. Pass funcName="" and winTitle=a +/// window name to retrieve that window's whole contents -- winTitle is the third +/// argument, not the first (passing it first silently returns ""). Function/S ZBR_ProcedureText(string funcName, variable flags, string winTitle) return ProcedureText(funcName, flags, winTitle) End -/// DataFolderDir(bits) for the CURRENT data folder -- callers wanting a specific folder -/// should set it first via ZBR_SubmitCommand("SetDataFolder ...") since a DFREF argument -/// isn't threaded through here; bits=3 (folders + waves) is what get_environment_summary -/// uses. +/// DataFolderDir(bits) for the current data folder. Callers wanting a specific folder +/// should set it first via ZBR_SubmitCommand("SetDataFolder ..."). Function/S ZBR_DataFolderDir(variable bits) return DataFolderDir(bits) End -/// FunctionInfo(name) -- generic compiled-function-exists probe. ZBR_IsCompiled() above -/// is really just this called with a deliberately-bogus name; exposed generically here -/// too so a caller can check any specific marker function by name. +/// FunctionInfo(name) passthrough. ZBR_IsCompiled() is this called with a bogus name. Function/S ZBR_FunctionInfo(string name) return FunctionInfo(name) @@ -755,11 +375,8 @@ End // --- Environment introspection ------------------------------------------------------- -/// Minimal identity/diagnostic summary -- NOT what get_environment_summary uses (that -/// tool composes its full picture client-side from the granular ZBR_IgorInfo/ZBR_WinList/ -/// etc. wrappers above instead, for the same reason those exist as separate functions: -/// keeping each Igor-side wrapper trivial and generic, with all the actual structuring -/// done in Python). Kept as a quick one-call smoke check alongside ZBR_Ping. +/// Minimal identity/diagnostic summary; get_environment_summary composes its full +/// picture client-side from the granular wrappers above instead. Function/S ZBR_GetEnvironmentSummary() string summary @@ -768,11 +385,8 @@ Function/S ZBR_GetEnvironmentSummary() return summary End -/// Read back everything sent to history since the capture started -- analogue of -/// read_session_history. stop=1 stops the capture (matching that tool's stop=True) -- -/// per CaptureHistory's own docs, a stopped refnum errors if reused, so this kills the -/// stored refnum too; the next call transparently starts a fresh capture, same as the -/// COM bridge's own read_session_history behavior. +/// Reads back everything sent to history since the capture started. stop=1 also kills +/// the stored refnum; the next call starts a fresh capture. Function/S ZBR_ReadSessionHistory(variable stop) string text @@ -789,10 +403,7 @@ End // --- Health / identity ----------------------------------------------------------------- -/// Minimal health-check/identity analogue of get_bridge_version -- confirms this module -/// specifically (not just "some Igor Pro instance") is loaded and reachable, and gives a -/// per-instance-distinguishing value (same idea as this session's earlier -/// GetInstanceInfo, generalized). +/// Confirms this module specifically is loaded and reachable. Function/S ZBR_Ping() string info @@ -803,17 +414,11 @@ End // --- Help file reading ------------------------------------------------------------------ // -// Synchronous equivalent of the COM bridge's read_help_file. CloseHelp/OpenNotebook/ -// SaveNotebook/KillWindow/OpenHelp are ordinary window/notebook operations -- NOT subject -// to the Execute-only-from-top-level restriction that COMPILEPROCEDURES/RELOAD CHANGED -// PROCS need (see the module docstring: that restriction is about recompiling procedures -// while procedure code is running, not a blanket rule about every operation) -- so this -// entire sequence runs as one direct, synchronous CallFunction round trip, no -// submit/poll needed. - -/// Return the first entry in `afterList` (semicolon-delimited) that is not present in -/// `beforeList` -- used to identify which new window WinList assigned to a just-opened -/// notebook (OpenNotebook/R doesn't return this directly). Returns "" if none found. +// Synchronous equivalent of the COM bridge's read_help_file -- none of these operations +// are subject to the Execute-only-from-top-level restriction COMPILEPROCEDURES needs, so +// this runs as one direct CallFunction round trip, no submit/poll needed. + +/// Returns the first entry in `afterList` not present in `beforeList`, or "" if none. static Function/S ZBR_FirstNewListEntry(string afterList, string beforeList) variable i, n @@ -830,11 +435,8 @@ static Function/S ZBR_FirstNewListEntry(string afterList, string beforeList) return "" End -/// Resolve a bare help-file name (WinList's WIN:512 bit never includes a path -- "Procedure -/// windows and help windows don't have names. WinList returns the window title instead") -/// back to a full path, checking the two folders Igor Pro itself loads help files from. -/// Mirrors the COM bridge's _resolve_help_file_path exactly, just in compiled Igor -/// instead of Python + os.path. Returns "" if not found in either location. +/// Resolves a bare help-file name (as returned by WinList's WIN:512 bit) to a full path, +/// or "" if not found in Igor's standard Help Files folders. static Function/S ZBR_ResolveHelpFilePath(string bareName) string specialDirs = "Igor Application;Igor Pro User Files" @@ -857,41 +459,15 @@ static Function/S ZBR_ResolveHelpFilePath(string bareName) return "" End -/// Read filePath (an .ihf help file, itself an Igor formatted-text notebook) and export -/// it as HTML to tmpHtmlPath (caller-supplied -- built by the Python side via -/// tempfile.mkstemp, same as the COM bridge already did), for the caller to parse -/// afterward. tmpHtmlPath is read directly off disk by the caller rather than being -/// serialized back through this reply: both processes run on the same machine, so a -/// local file handoff sidesteps any question about how large a CallFunction reply can -/// carry for a potentially big HTML export (the ZeroMQ-XOP's own default -/// ZMQ_MAXMSGSIZE=1024-byte limit applies to the Router's *incoming* request size, but -/// this avoids relying on any assumption about outgoing reply size limits too). -/// -/// Full sequence, matching the COM bridge's read_help_file exactly, just executed -/// synchronously in compiled Igor code instead of via a client-side finally block: -/// 1. Snapshot every currently open help file (visible or hidden, WIN:512) and every -/// currently open plain-notebook window (WIN:16). -/// 2. CloseHelp/ALL (required: an .ihf can't be opened as a notebook while Igor -/// considers it already open as a help file). -/// 3. OpenNotebook/R filePath, then diff WinList's notebook list against the step-1 -/// snapshot to find the name Igor assigned the new window. -/// 4. SaveNotebook/O/S=5/H=... export to tmpHtmlPath. -/// 5. KillWindow/Z the temporary notebook. -/// 6. Restore every help file captured in step 1 via OpenHelp/V=.../INT=0. -/// Steps 5-6 always run (via try/catch rather than a true finally, since Igor procedure -/// code has no finally block) even if step 3 or 4 failed, so a failure partway through -/// still restores whatever help state existed before this call. -/// -/// Returns a "|"-joined status string: "OK|" on success, -/// or "ERROR||" if OpenNotebook/SaveNotebook -/// itself failed. restoreFailures lists bare file names from step 1 that could not be -/// resolved back to a full path (e.g. a help file supplied from somewhere other than the -/// two standard Help Files folders) -- these were NOT reopened. +/// Exports filePath (an .ihf help file) as HTML to tmpHtmlPath, restoring whatever help +/// windows were open beforehand. Returns "OK|" or +/// "ERROR||". See SESSION_NOTES.md for the full sequence and +/// the Abort-dialog pitfall this avoids. Function/S ZBR_ReadHelpFile(string filePath, string tmpHtmlPath) string helpAll, helpVisible, notebooksBefore, newName, restoreFailures string name, resolvedPath, statusStr - variable i, n, visibleFlag, err + variable i, numHelpWin, visibleFlag, err helpAll = WinList("*", ";", "WIN:512") helpVisible = WinList("*", ";", "WIN:512,VISIBLE:1") @@ -900,19 +476,17 @@ Function/S ZBR_ReadHelpFile(string filePath, string tmpHtmlPath) statusStr = "OK" try - CloseHelp/ALL - AbortOnRTE + CloseHelp/ALL; AbortOnRTE - OpenNotebook/R filePath - AbortOnRTE + OpenNotebook/R filePath; AbortOnRTE newName = ZBR_FirstNewListEntry(WinList("*", ";", "WIN:16"), notebooksBefore) if(strlen(newName) == 0) - Abort "OpenNotebook/R succeeded but no new notebook window was found" + // Not Abort "" -- pops a dialog before catch runs. See SESSION_NOTES.md. + statusStr = "ERROR|OpenNotebook/R succeeded but no new notebook window was found" + else + SaveNotebook/O/S=5/H={"UTF-8", 3, 7, 0, 0.9, 32} $newName as tmpHtmlPath; AbortOnRTE endif - - SaveNotebook/O/S=5/H={"UTF-8", 3, 7, 0, 0.9, 32} $newName as tmpHtmlPath - AbortOnRTE catch err = GetRTError(1) statusStr = "ERROR|" + GetErrMessage(err) @@ -923,8 +497,8 @@ Function/S ZBR_ReadHelpFile(string filePath, string tmpHtmlPath) endif restoreFailures = "" - n = ItemsInList(helpAll) - for(i = 0; i < n; i += 1) + numHelpWin = ItemsInList(helpAll) + for(i = 0; i < numHelpWin; i += 1) name = StringFromList(i, helpAll) resolvedPath = ZBR_ResolveHelpFilePath(name) if(strlen(resolvedPath) == 0) @@ -932,8 +506,7 @@ Function/S ZBR_ReadHelpFile(string filePath, string tmpHtmlPath) continue endif visibleFlag = (WhichListItem(name, helpVisible) != -1) ? 1 : 0 - OpenHelp/V=(visibleFlag)/INT=0/Z=1 resolvedPath - err = GetRTError(1) + OpenHelp/V=(visibleFlag)/INT=0/Z=1 resolvedPath; err = GetRTError(1) if(err) restoreFailures = AddListItem(name, restoreFailures, ";", Inf) endif @@ -944,49 +517,41 @@ End // --- ZeroMQ server bind ---------------------------------------------------------------- -/// (Re-)bind this module's ZeroMQ ROUTER (server) socket and (re-)start the XOP's -/// background message handler, so CallFunction requests (e.g. "ZBR#ZBR_Ping") are served -/// automatically from here on -- no separate manual zeromq_server_bind/ -/// zeromq_handler_start call needed after a recompile, unlike this session's earlier -/// manual testing. -/// -/// **Correction (user-supplied): deliberately does NOT call zeromq_stop() first**, -/// unlike the three-call idiom shown in Igor Pro Folder/Igor Help Files/ZeroMQ.ihf's own -/// introductory example (its ServerSide() function). zeromq_stop() stops *every* ZeroMQ -/// bind/connection/handler for the whole Igor Pro instance, not just this module's own -/// -- calling it unconditionally on every compile would corrupt/tear down any other -/// already-established ZeroMQ binds (e.g. MIES's own real subsystem, -/// MIES_MiesUtilities_ZeroMQ.ipf's StartZeroMQSockets, currently short-circuited via an -/// uncommitted `return 2` in this repo's working tree but not necessarily always so). -/// Calling zeromq_server_bind directly, without stopping first, is safe to repeat on -/// every compile: if this module's own socket is already bound from an earlier compile, -/// the call simply errors ("Address in use"), caught below rather than propagated. -/// -/// `; err = GetRTError(1)` immediately after each XOP call clears any resulting runtime -/// error right there on the same line -- necessary because Igor's Debugger (when -/// "Debug on Error" is enabled) only checks for a pending RTE state at the *end of a -/// line*, so leaving either of these calls' potential error unacknowledged until some -/// later line would risk popping the Debugger window here, which -- same as every other -/// popup this session has hit -- has no scriptable dismissal and would hang unattended -/// operation. -/// -/// Called synchronously and directly from AfterCompiledHook (not deferred) -- confirmed -/// via user review that this part of the design is fine as-is. See -/// ZBR_StopHandlerBeforeRecompile's docstring instead for the actual fix targeting the -/// crashes this bridge has hit coinciding with COMPILEPROCEDURES: the real hazard is on -/// the OTHER side of the recompile window (a live ZeroMQ handler thread able to dispatch -/// a new CallFunction request WHILE Igor is mid-recompile), not anything happening here -/// after compilation has already finished successfully. +/// (Re-)binds this module's ZeroMQ ROUTER socket and starts its handler. Does not call +/// zeromq_stop() first, so it won't tear down any other ZeroMQ binds in the same +/// experiment (e.g. MIES's own). Safe to call repeatedly -- an already-bound error is +/// caught, not propagated. Called only from IgorStartOrNewHook; recompiles instead just +/// stop/restart the handler via ZBR_StopHandlerBeforeRecompile/ +/// ZBR_StartHandlerAfterRecompile, not a full rebind. See SESSION_NOTES.md. static Function ZBR_EnsureZeroMQBound() - variable err + variable err, port + string bindURL, envPort + + envPort = GetEnvironmentVariable(ZBR_ZEROMQ_ENV_PORT) + if(!strlen(envPort)) + port = ZBR_ZEROMQ_DEFAULT_PORT + else + port = str2num(envPort); err = GetRTError(1) + if(numType(port) == 2) + printf "Could not parse port number from %s: %s\rUsing default port %d\r", ZBR_ZEROMQ_ENV_PORT, envPort, ZBR_ZEROMQ_DEFAULT_PORT + port = ZBR_ZEROMQ_DEFAULT_PORT + endif + endif - zeromq_server_bind(ZBR_ZEROMQ_ENDPOINT); err = GetRTError(1) + sprintf bindURL, "%s:%d", ZBR_ZEROMQ_ENDPOINT, port + zeromq_server_bind(bindURL); err = GetRTError(1) + if(!err) + printf "Igor Pro Bridge MCP bound through ZMQ at port %d\r", port + endif zeromq_handler_start(); err = GetRTError(1) return 0 End +/// Releases running thread groups before Igor uncompiles, preventing a blocking +/// "Function Execution Module is still active" dialog from a stray MIES background +/// thread during COMPILEPROCEDURES. See SESSION_NOTES.md. static Function BeforeUncompiledHook(variable changeCode, string procedureWindowTitleStr, string textChangeStr) variable err @@ -994,37 +559,33 @@ static Function BeforeUncompiledHook(variable changeCode, string procedureWindow err = ThreadGroupRelease(-2) End -static Function AfterCompiledHook() +/// Fires on Igor launch and on creating a new experiment. Binds/starts the ZeroMQ +/// handler here (once per process) rather than in AfterCompiledHook. +static Function IgorStartOrNewHook(string igorApplicationNameStr) variable modifiedBefore - // Creating/incrementing a global marks the experiment as modified, same as any - // other data change. Captured/restored here so this hook never flips an - // otherwise-unmodified experiment to modified, matching the existing convention - // in MIES_IgorHooks.ipf's own AfterCompiledHook -- flagged by a Copilot PR - // review as a real risk otherwise: an experiment spuriously marked modified can - // trigger a "Save changes?" prompt later, which is exactly the kind of dialog - // this bridge (built around unattended operation) cannot dismiss remotely. ExperimentModified modifiedBefore = V_flag - // Make this module's ZeroMQ server listen again immediately after every compile -- - // the whole point of running this from AfterCompiledHook rather than requiring a - // separate manual step each time the code changes. Called directly/synchronously - // (not deferred) -- confirmed via user review that this is fine: by the time - // AfterCompiledHook runs, compilation has already finished successfully, so there - // is nothing left to race against here. See ZBR_StopHandlerBeforeRecompile's - // docstring for the actual fix targeting this bridge's COMPILEPROCEDURES-adjacent - // crashes -- the real hazard is upstream of this point (a live handler able to - // dispatch a new CallFunction request while Igor is still mid-recompile). ZBR_EnsureZeroMQBound() - // Bare Variable/G (no initializer) is safe to call unconditionally: per Igor - // Reference.ihf, /G "overwrites any existing variable" but "the variable is - // initialized when it is created if you supply the initial value" -- i.e. the - // overwrite-to-a-value only happens when an initializer is given. Without one, - // this creates the global at 0 the first time and leaves an existing value - // alone on every call after that, so no NVAR_Exists guard is needed. + if(!modifiedBefore) + ExperimentModified 0 + endif + + return 0 +End + +/// Bumps the compile-confirmation counter on every successful compile. Does not touch +/// the ZeroMQ socket/handler -- see IgorStartOrNewHook. +static Function AfterCompiledHook() + + variable modifiedBefore + + ExperimentModified + modifiedBefore = V_flag + variable/G root:gClaudeHelperCompileCounter NVAR gClaudeHelperCompileCounter = root:gClaudeHelperCompileCounter diff --git a/tools/igor-mcp-bridge/igor-pro-bridge-2.3.1.mcpb b/tools/igor-mcp-bridge/igor-pro-bridge-2.3.1.mcpb deleted file mode 100644 index 0991d372f70943a109d8134402d19b9f4abbbc05..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37267 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

Y#8(v$lSWSwvWg%>k?kw)ceIa@6w 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 literal 0 HcmV?d00001 diff --git a/tools/igor-mcp-bridge/manifest.json b/tools/igor-mcp-bridge/manifest.json index 98bc3b8739..5149679e81 100644 --- a/tools/igor-mcp-bridge/manifest.json +++ b/tools/igor-mcp-bridge/manifest.json @@ -2,9 +2,9 @@ "manifest_version": "0.4", "name": "igor-pro-bridge", "display_name": "Igor Pro Bridge", - "version": "2.3.1", + "version": "2.3.2", "description": "Control a running Igor Pro instance via the ZeroMQ-XOP's CallFunction interface: run commands, read wave data, manage compilation, load experiments, and launch Igor Pro itself, directly from Claude.", - "long_description": "v2.3.1: fixed a concept flaw in v2.3.0's crash mitigation, found live by the repo owner: v2.3.0 stops the ZeroMQ handler before `RELOAD CHANGED PROCS`/`COMPILEPROCEDURES` run and relies on `AfterCompiledHook` to restart it afterward -- but Igor only calls `AfterCompiledHook` after a *successful* compile. If the edited `.ipf` failed to compile, the hook never fired, the handler stayed stopped forever, and the bridge was permanently dead with no recovery short of restarting Igor Pro. Fixed by adding `ZBR_ArmRecompileWatchdog`/`ZBR_RecompileWatchdogTick` (`ZMQ_BridgeHelpers.ipf`): a named background task, armed immediately before the handler is stopped, that unconditionally restarts/rebinds the handler regardless of whether the compile succeeds or fails, then self-disarms. Because Igor's background-task scheduler and its deferred operation queue (`Execute/P`) are two independent subsystems, the repo owner raised a sharp concern: could the watchdog fire before `RELOAD CHANGED PROCS`/`COMPILEPROCEDURES` even finish, reintroducing the exact cross-thread race v2.3.0 exists to prevent? Live timing instrumentation (three `stopmstimer(-2)` printouts: queue start, watchdog tick, `AfterCompiledHook`) confirmed the concern was valid -- with only `period=30` set, the watchdog could and did tick (~62ms after arming) well before a real compile finished (~414ms, per `AfterCompiledHook`'s own timestamp). Fixed by registering the task with an explicit `start=60` (an ~1-second floor before its first possible tick, decoupled from the `period=30` interval between later ticks), comfortably longer than any compile observed this session. The correctness property this protects is that the watchdog must not fire before the operation queue has finished draining -- not that it must fire after `AfterCompiledHook` specifically, which is meaningless on the failure path since that hook never runs then; once the queue has drained, the relative order of the watchdog and `AfterCompiledHook` on the success path no longer matters, since both converge on the same idempotent `ZBR_EnsureZeroMQBound()` restart. This is a generous empirical margin based on observed compile times, not a mathematically airtight guarantee against an arbitrarily slow future compile. A second, independent bug was found and diagnosed live by the repo owner while testing the above: `ZBR_IsCompiled()`'s `FunctionInfo()` call was unqualified, so it resolved against ZBR's own independent-module namespace (always fine) instead of ProcGlobal's -- meaning `check_compilation_state()` could silently report `true` even while ProcGlobal itself had a genuine compile error. Fixed by qualifying the call as `FunctionInfo(\"ProcGlobal#...\")`, confirmed against Igor's own docs and this repo's `igortest-test-compilation.ipf` reference implementation. A third, unrelated issue surfaced during live testing: a pre-existing MIES background thread (not task) left running during `COMPILEPROCEDURES` could raise a blocking \"Function Execution Module is still active\" dialog, freezing Igor's entire operation queue (though direct `CallFunction` calls like `check_bridge_health` kept working throughout, since they don't route through the queue). Mitigated by a new `BeforeUncompiledHook` in `ZMQ_BridgeHelpers.ipf` that calls `ThreadGroupRelease(-2)` to release running thread groups before Igor uncompiles, added by the repo owner and confirmed to prevent recurrence in subsequent testing. No Python-side (`server.py`) logic changes were needed beyond updated docstrings describing these fixes; all of the actual fixes are within `ZMQ_BridgeHelpers.ipf`.\n\nv2.3.0: investigated the long-standing, previously-unexplained crash pattern where Igor Pro itself (not this bridge) would sometimes become unreachable shortly after `reload_and_compile_procedures`. Two Windows crash dumps captured during this bridge's development were analyzed with Python's `minidump` package (Igor's own crash reporter provides no stack trace), confirming both were genuine `EXCEPTION_ACCESS_VIOLATION`s deep inside `Igor64.exe` itself, not in this bridge's code or any XOP. A likely mechanism was then identified, based directly on a comparison the repo owner suggested against this repo's own `igortest-tracing.ipf`, whose `CompileAndRestart()`/`AfterCompiledHook()` pattern reloads and compiles procedures reliably with no crash: unlike that reference pattern, this bridge's ZeroMQ-XOP handler runs as a background thread (documented in `HelpFiles/ZeroMQ.ihf` as \"a threaded message handler\") that keeps dispatching incoming `CallFunction` requests regardless of what Igor's main thread is doing -- so a request arriving while `COMPILEPROCEDURES` is mid-rebuild of Igor's own internal function/symbol tables is a plausible cross-thread race, distinct from anything happening inside `AfterCompiledHook` itself (which runs safely afterward, exactly like `igortest`'s hook). Fixed by adding `ZBR_StopHandlerBeforeRecompile()` (calls `zeromq_handler_stop()`) and queuing it as the FIRST deferred `Execute/P` entry in `ZBR_SubmitReloadAndCompile()`, ahead of `RELOAD CHANGED PROCS`/`COMPILEPROCEDURES`; the handler is restarted afterward via the existing, unchanged `AfterCompiledHook` -> `ZBR_EnsureZeroMQBound()` synchronous call. Confirmed live: recompiled successfully afterward, then ran three CONCURRENT `reload_and_compile_procedures()` calls as a stress test -- all three returned `compiled: true` cleanly with no crash, and `check_bridge_health()`/`execute_igor_command()` both worked correctly afterward. Honest caveat: this is a well-reasoned mitigation, not a proven fix -- `Igor64.exe` ships no public symbols, so the exact fault can't be confirmed from here, and the crash was already rare/nondeterministic before this fix. No Python-side (`server.py`) logic changes were needed beyond an updated docstring; the fix is entirely within `ZMQ_BridgeHelpers.ipf`.\n\nv2.2.3: fixed another uncaught-runtime-error-pops-a-dialog bug, this time in `ZBR_FinishToken`, found while cleaning up leftover test-token rows from this bridge's own `root:Packages:ZBR` storage waves. `ZBR_AllocateToken` captures a token's row index (`idx`) at submission time, but `ZBR_FinishToken` -- the callback that actually writes the result and flips the done flag -- runs later, in its own separate deferred `Execute/P` entry (per the v2.2.0 fix). If the `done`/`resultText`/`historyStart` waves are resized smaller in between (e.g. maintenance code clearing out old tokens, exactly as happened live during this session's own cleanup), `idx` can end up pointing past the end of the now-shorter waves. `ZBR_FinishToken` had no bounds check, so writing to `resultText[idx]` in that state threw an uncaught \"Index out of range for wave...\" runtime error -- which, like the v2.2.1 bug, pops a real modal dialog and blocks Igor's entire main thread until a human dismisses it. Fixed by bounds-checking `idx` against `DimSize(done, 0)` before writing anything; if the row no longer exists, the callback now just silently does nothing (there is nothing useful left to recover), and `ZBR_PollCommand`'s own existing bounds check already reports a clean `\"ERROR: unknown token\"` response to the caller instead of a hang. Confirmed live: reproduced the exact original crash (resizing the storage waves to 0 rows while a submitted command's own token was still in flight, which had just thrown the dialog moments earlier during this session's testing), then repeated it after the fix -- this time it returned `\"ERROR: unknown token ...\"` cleanly with no dialog and no hang, and `check_bridge_health()` stayed `OK` throughout. No Python-side changes were needed.\n\nv2.2.2: minor robustness refinement to the v2.2.1 fix, contributed directly by the repo owner after installing it. `ZBR_EnsureCaptureStarted`'s stale-refnum probe (`CaptureHistory(refnumRW, 0)`) and its `AbortOnRTE` were originally written on separate lines; moved onto the SAME line. Reason: Igor's Debug on Error check happens at the END of each line, not each statement -- if the two were on separate lines, a user with Debug on Error enabled (unusual, but this bridge doesn't force the Debugger off during a raw `execute_igor_command`/`submit_igor_command` call the way the `_unattended` variants do) would get a Debugger popup right when the stale refnum's runtime error occurred, before `AbortOnRTE` ever got a chance to convert it into a catchable abort -- defeating the whole point of the v2.2.1 fix in that one specific configuration. Confirmed live both before accepting this change (by temporarily enabling `debug_on_error` via `set_debugger_enabled` and repeating the deliberately-corrupted-refnum test) and after: with the two statements on one line, the corrupted-refnum scenario completes cleanly with no Debugger popup and no hang even with Debug on Error active, and the refnum still self-heals correctly. No other behavior change.\n\nv2.2.1: fixed a bug where `load_experiment` (or a user manually reopening a saved .pxp) could leave the bridge completely unreachable, requiring a human to dismiss a modal Igor error dialog. Root cause: this bridge's history-capture mechanism stores its `CaptureHistoryStart()` reference number in a plain `Variable/G` global (`root:Packages:ZBR:captureRefNum`), which Igor persists into a saved experiment like any other global -- but that refnum is only meaningful within the OS process that created it. Reloading a saved experiment brings the OLD numeric value back even though the process is brand new; the prior code only checked that the global *existed*, not whether its value was still a live capture, so it trusted the stale refnum. Using it then threw a genuine Igor runtime error (\"there is no open file with this reference number\") from a plain top-level `Execute/P` entry -- not caught anywhere -- which popped a real modal dialog and blocked Igor's entire main thread (and therefore every ZeroMQ reply) until a human dismissed it. Confirmed live by saving and reloading an experiment via `load_experiment` and then calling `execute_igor_command`, which reproduced the dialog exactly. Fixed in `ZBR_EnsureCaptureStarted` (`ZMQ_BridgeHelpers.ipf`): the stored refnum is now validated with a `try`/`catch`/`endtry`+`AbortOnRTE` block before being trusted, and silently replaced with a fresh `CaptureHistoryStart()` call if it's stale -- confirmed live afterward, both by deliberately corrupting the refnum to a bogus value and by repeating the exact save-then-`load_experiment`-then-`execute_igor_command` sequence that originally triggered the dialog; neither reproduced the dialog and both recovered silently. (Two syntax mistakes were made and caught along the way while implementing this: a runtime error inside a `try` block does not by itself jump to `catch` without an explicit `AbortOnRTE` immediately after the risky call, and a bare `return` with no value is invalid inside a plain, implicit-`Variable`-returning `Function` -- both confirmed from Igor's own bundled help, \"Flow Control for Aborts\" and \"The Return Statement\" in `Programming.ihf`.) No Python-side (`server.py`) changes were needed for this fix -- it is entirely within the Igor-side `ZMQ_BridgeHelpers.ipf` helper file.\n\nv2.2.0: fixed a serious reliability bug in the v2.1.0 submit/poll primitives, found via live testing of the exact scenario they exist for (a command whose runtime is unknown and could be very long). `ZBR_SubmitCommand`/`ZBR_SubmitCommandUnattended` (Igor-side, in `ZMQ_BridgeHelpers.ipf`) previously joined the submitted command and the internal finish-callback into ONE compound `Execute/P` string (`cmd + \"; \" + finishCall`). Confirmed live: if `cmd` failed to parse, OR hit a genuine Igor-level runtime error partway through (not just a Debugger pause), the ENTIRE joined string aborted, so the appended finish-callback never ran -- leaving `poll_igor_command` reporting `\"done\": false` forever, indistinguishable from a job still genuinely in progress. This directly undermines the reliability guarantee these tools exist to provide. Root-caused via two live tests: (1) queuing an invalid command and a valid one as two SEPARATE `Execute/P` entries showed the second still ran fine despite the first failing, proving Igor's deferred-operation queue processes independently-queued entries regardless of a prior one's failure; (2) the same joined-string command that had a genuine runtime error (not just an invalid command name) also silently swallowed its own appended print statement, confirming the bug applies to runtime errors too, not just parse errors. Fixed by queuing `cmd` and the finish-callback (and, for the `_unattended` variant, a Debugger-disable step and a Debugger-restore step, with the saved settings carried across via persistent globals) as fully independent `Execute/P` entries instead of one joined string -- verified live afterward with an invalid command, a genuine runtime-erroring command, and a normal command, all three now correctly reaching `\"done\": true` instead of hanging. **Known residual limitation, also confirmed live:** there is currently no generic way to tell \"the command ran and legitimately printed nothing\" apart from \"the command errored out with no output\" -- both now come back as `\"done\": true` with an empty/short result and no error indication (an attempted fix using `GetRTError()` in the finish-callback was tested and found not to work: each `Execute/P` entry is dispatched as its own independent top-level execution, so Igor clears any pending runtime-error state before the next queued entry runs). If a submitted command's success needs to be verifiable, have it `print` an explicit sentinel/result value itself.\n\nv2.1.0: added first-class support for commands with an unknown or very long runtime (hours to weeks). `execute_igor_command`/`execute_igor_command_unattended` block the whole MCP tool call while polling, which cannot work for a genuinely long calculation -- the MCP transport itself has been observed to time out a single tool call well under a minute, regardless of the requested `timeout_seconds`. Three new tools expose the existing submit/poll primitives directly: `submit_igor_command`/`submit_igor_command_unattended` queue a command and return a token immediately with no waiting at all, and `poll_igor_command(token)` performs one cheap, instant check for completion, callable any number of times spaced arbitrarily far apart. This is reliable no matter how long the job runs or how many times this bridge process or Claude Desktop itself restarts in between, because all of the actual state (a done flag and the captured text) lives entirely in Igor Pro's own data waves (`root:Packages:ZBR`), not in this bridge's Python process -- the only thing that actually ends the job is Igor Pro itself quitting, crashing, or restarting. As with the existing `_unattended` tools, use `submit_igor_command_unattended` for anything long-running: a Debugger pause partway through leaves `poll_igor_command` reporting \"not done\" forever, indistinguishable from the command still genuinely running.\n\nv2.0.1: three bug fixes found during live v2.0.0 testing. (1) `execute_igor_command`/`execute_igor_command_unattended`: the documented pattern of using `fprintf 0, ...` to get data back does not actually work over this transport -- confirmed live that `CaptureHistory` (which this bridge's capture mechanism relies on) captures `print` output but never captures `fprintf`-to-history output at all (refnum 0, -1, or -2 all silently produce nothing, even though the command itself runs without error). Use `print` instead; all docs/docstrings updated accordingly. (2) `load_experiment`: fixed a silent-failure bug where relaunching Igor Pro with a new experiment file could do nothing at all, with no error reported. Root cause (diagnosed live): the tool was waiting for Igor Pro to stop answering over ZeroMQ as its signal that the old process had quit, but ZeroMQ goes quiet well before the underlying Igor64.exe process actually terminates -- launching the replacement `Igor64.exe /UNATTENDED ` command line while the old process is still alive (even mid-shutdown) doesn't spawn a new process at all; Windows/Igor's single-instance-per-user behavior instead either redirects the load into the still-live old instance (risking an unhandled \"save changes?\" dialog) or silently drops the request if that instance is already mid-quit. Fixed by polling the actual OS process list for the configured executable's own process to fully exit before ever launching the replacement, raising a clear error instead of proceeding if it doesn't exit in time. (3) `read_help_file`: added a `timeout_ms` parameter (default raised from this bridge's usual 5s to 30s) -- confirmed live that exporting a genuinely large help file (e.g. the full \"Igor Reference.ihf\") can take longer than 5 seconds, which previously timed out the call even though the export eventually succeeded Igor-side anyway.\n\nv2.0.0: BREAKING transport change. Replaces the COM Automation Server transport (win32com.client, ProgID `IgorPro.Application`) with the ZeroMQ-XOP's `CallFunction` JSON protocol over a plain localhost TCP socket (tcp://127.0.0.1:5680), talking to a small set of Igor-side helper functions in `Packages/MIES/ZMQ_BridgeHelpers.ipf` (the `ZBR` independent module).\n\nWhy: COM required this bridge process and Igor Pro to run at the SAME Windows privilege level (both elevated, or both not) -- an easy-to-miss mismatch (e.g. Claude Desktop reopened normally after Igor Pro was left running elevated from before). ZeroMQ is a plain TCP socket with no such requirement at all -- elevation no longer matters in any way. `launch_igor_pro_unattended` no longer has an elevation-branching code path: it always launches Igor Pro as a plain, non-elevated child process, at whatever privilege level this bridge process itself runs at.\n\n**New setup requirement**: unlike the COM transport (which worked against a stock Igor Pro installation with zero custom procedure code), this transport requires `Packages/MIES/ZMQ_BridgeHelpers.ipf` to be `#include`-d and compiled into whichever Igor Pro experiment this bridge talks to -- there is no bootstrap path over ZeroMQ itself. This repo's own `Packages/MIES_Include.ipf` already does this permanently. Any OTHER Igor Pro experiment needs `ZMQ_BridgeHelpers.ipf` copied onto its own procedure search path with a matching `#include` added by hand, then a recompile -- see `Packages/doc/igor-pro-bridge.rst` (\"Installation\") for the exact one-time steps.\n\n**Capability improvements**: `get_wave` now supports any wave dimensionality (up to 4D), real and complex numeric waves, text waves, and wave-reference waves -- the entire wave comes back from one round trip, natively serialized (the old COM version was limited to 1D real-valued waves, read one point at a time).\n\n**Behavior changes to be aware of**: `execute_igor_command`/`execute_igor_command_unattended` can no longer separate fprintf-only output from the full echoed history the way COM's `Execute2` could -- both \"results\" and \"history\" now hold the same captured text; prefix a command with `Silent 1;` to suppress the echo. `load_experiment` no longer hot-swaps the open experiment in place (that was a COM-only method with no procedure-language equivalent) -- it now quits the running instance and relaunches Igor Pro with the target file path as a launch argument, a real process restart; unsaved changes in the previously-open experiment are lost. `ensure_igor_pro_bridge_defined` is retired -- it existed to manage a `#ifdef IGOR_PRO_BRIDGE` gating convention that ZBR's always-on independent-module architecture doesn't use.\n\nSee `Packages/doc/igor-pro-bridge.rst` and `SESSION_NOTES.md` in the MIES repository for the full protocol research, design decisions, and confirmed-live test results behind this rewrite.\n\nTools:\n- `execute_igor_command` / `execute_igor_command_unattended`: run a command string on Igor's command line (submitted via `ZBR_SubmitCommand`/`ZBR_SubmitCommandUnattended`, polled via `ZBR_PollCommand`); include a `print` call (NOT `fprintf 0, ...` -- confirmed not captured, see v2.0.1 changelog above) to get data back. Both return `{results, history}`. The `_unattended` variant automatically disables/restores Igor's Debugger around the call.\n- `read_session_history`: read back everything sent to Igor's history area since this bridge's capture started.\n- `get_wave`: read any Igor wave's full data and metadata back (any dimensionality, numeric/complex/text/wave-reference).\n- `load_experiment`: quit the running instance and relaunch Igor Pro with a .pxp file path as a launch argument.\n- `check_bridge_health`: diagnose whether this bridge can reach Igor Pro's ZeroMQ server right now.\n- `check_compilation_state` / `reload_and_compile_procedures`: check and refresh Igor's compiled state after editing a `.ipf` file on disk.\n- `dismiss_compile_error_dialog`: close a stuck \"Function Compilation Error\" dialog via a posted Escape key, without needing OS focus.\n- `get_debugger_state` / `set_debugger_enabled` / `restore_debugger_settings`: read, change, and restore Igor's Debugger settings.\n- `get_environment_summary`: summarize the live instance (version, loaded experiment, XOPs, included procedure files, data folders, Debugger settings).\n- `read_help_file`: read an Igor Pro help file (.ihf) as structured, formatted text.\n- `get_bridge_version`: report the running bridge version and Python/package versions.\n- `configure_igor_launch` / `launch_igor_pro_unattended`: record the Igor Pro executable path once per session, then launch it with the `/UNATTENDED` flag and wait for it to become reachable over ZeroMQ.\n\n**Requirements:**\n- Igor Pro 9.00 or later, running on Windows, with the ZeroMQ-XOP installed and loaded.\n- `Packages/MIES/ZMQ_BridgeHelpers.ipf` (or a copy of it) `#include`-d and compiled into the target experiment -- see `Packages/doc/igor-pro-bridge.rst` (\"Installation\") for the one-time setup steps for experiments other than this repo's own.\n- Python (accessible as `python` on PATH), with the pinned packages in `requirements.txt` installed into that same environment. Run `install.ps1` to do this correctly and also complete pywin32's required post-install step (still needed for window-handling/process-launch helpers, even though this bridge no longer uses COM) -- see that script's own help (`Get-Help ./install.ps1 -Full`).\n- No elevation/privilege-matching requirement of any kind -- this is the change v2.0.0 makes.\n\nPrior COM-based history (v1.x): see `Packages/doc/igor-pro-bridge.rst` for the full v1.x changelog, retained there for reference.", + "long_description": "v2.3.2: preparation for eventually talking to more than one Igor Pro instance, plus a comment-reduction pass and one more unattended-use dialog fix. On the Igor side (`ZMQ_BridgeHelpers.ipf`), `ZBR_EnsureZeroMQBound` now reads an `IGOR_PRO_BRIDGE_PORT` environment variable (`ZBR_ZEROMQ_ENV_PORT`) at bind time and binds to that port instead of its own default (5680, `ZBR_ZEROMQ_DEFAULT_PORT`) when it's set. The ZeroMQ bind/handler-start was also restructured: binding (`zeromq_server_bind`) now happens once, in a new `IgorStartOrNewHook` (fires on Igor launch and on creating a new experiment), rather than on every compile via `AfterCompiledHook` as in v2.3.0/v2.3.1 -- `AfterCompiledHook` now only bumps the compile-confirmation counter, and the recompile watchdog/`ZBR_StopHandlerBeforeRecompile` path only stops/restarts the message *handler* (`ZBR_StartHandlerAfterRecompile`, `zeromq_handler_stop`/`zeromq_handler_start`) rather than attempting a fresh bind on every recompile, since the bind is a property of the ZeroMQ-XOP itself and doesn't need repeating just because Igor recompiled a procedure file. On the Python side (`server.py`), `configure_igor_launch` gained an optional `port` parameter: setting it writes `IGOR_PRO_BRIDGE_PORT` into this bridge process's own environment (inherited by the launched Igor Pro child process, which reads it via the above), and every ZeroMQ-talking function (`call_function`, `check_bridge_health`, and everything built on them) now resolves its endpoint through one `_igor_zmq_endpoint()` helper instead of a fixed constant, so setting or clearing the port immediately retargets all of them together, not just the next launch. Omitting `port` (or passing `None`) explicitly clears a previously-configured custom port, removing the environment variable rather than merely not re-setting it -- calling `configure_igor_launch` to update just the exe path without repeating `port=` will also clear any previously-set port, documented prominently since Python can't distinguish an omitted argument from an explicit `None`. This bridge still only tracks one currently-configured endpoint at a time; talking to two Igor Pro instances simultaneously isn't supported yet. Also fixed in `ZBR_ReadHelpFile`: an `Abort \"\"` used when the expected new notebook window wasn't found displayed a real alert dialog at the moment `Abort` executed, BEFORE the enclosing `try`/`catch` ever got a chance to intervene -- wrapping it in `try`/`catch` did not suppress the popup the way it does for an ordinary runtime error, which would have hung unattended use. Fixed by setting the error status string directly and skipping `SaveNotebook` in that branch instead of calling `Abort` at all. Finally, `ZMQ_BridgeHelpers.ipf`'s in-code comments (which had grown to extensive historical narratives per function) were cut down to short one/two-line docstrings; the full design rationale and bug histories they contained were moved to a new reference section in `SESSION_NOTES.md` instead, so the reasoning is preserved without cluttering the source file. No behavior change from the comment reduction itself.\n\nv2.3.1: fixed a concept flaw in v2.3.0's crash mitigation, found live by the repo owner: v2.3.0 stops the ZeroMQ handler before `RELOAD CHANGED PROCS`/`COMPILEPROCEDURES` run and relies on `AfterCompiledHook` to restart it afterward -- but Igor only calls `AfterCompiledHook` after a *successful* compile. If the edited `.ipf` failed to compile, the hook never fired, the handler stayed stopped forever, and the bridge was permanently dead with no recovery short of restarting Igor Pro. Fixed by adding `ZBR_ArmRecompileWatchdog`/`ZBR_RecompileWatchdogTick` (`ZMQ_BridgeHelpers.ipf`): a named background task, armed immediately before the handler is stopped, that unconditionally restarts/rebinds the handler regardless of whether the compile succeeds or fails, then self-disarms. Because Igor's background-task scheduler and its deferred operation queue (`Execute/P`) are two independent subsystems, the repo owner raised a sharp concern: could the watchdog fire before `RELOAD CHANGED PROCS`/`COMPILEPROCEDURES` even finish, reintroducing the exact cross-thread race v2.3.0 exists to prevent? Live timing instrumentation (three `stopmstimer(-2)` printouts: queue start, watchdog tick, `AfterCompiledHook`) confirmed the concern was valid -- with only `period=30` set, the watchdog could and did tick (~62ms after arming) well before a real compile finished (~414ms, per `AfterCompiledHook`'s own timestamp). Fixed by registering the task with an explicit `start=60` (an ~1-second floor before its first possible tick, decoupled from the `period=30` interval between later ticks), comfortably longer than any compile observed this session. The correctness property this protects is that the watchdog must not fire before the operation queue has finished draining -- not that it must fire after `AfterCompiledHook` specifically, which is meaningless on the failure path since that hook never runs then; once the queue has drained, the relative order of the watchdog and `AfterCompiledHook` on the success path no longer matters, since both converge on the same idempotent `ZBR_EnsureZeroMQBound()` restart. This is a generous empirical margin based on observed compile times, not a mathematically airtight guarantee against an arbitrarily slow future compile. A second, independent bug was found and diagnosed live by the repo owner while testing the above: `ZBR_IsCompiled()`'s `FunctionInfo()` call was unqualified, so it resolved against ZBR's own independent-module namespace (always fine) instead of ProcGlobal's -- meaning `check_compilation_state()` could silently report `true` even while ProcGlobal itself had a genuine compile error. Fixed by qualifying the call as `FunctionInfo(\"ProcGlobal#...\")`, confirmed against Igor's own docs and this repo's `igortest-test-compilation.ipf` reference implementation. A third, unrelated issue surfaced during live testing: a pre-existing MIES background thread (not task) left running during `COMPILEPROCEDURES` could raise a blocking \"Function Execution Module is still active\" dialog, freezing Igor's entire operation queue (though direct `CallFunction` calls like `check_bridge_health` kept working throughout, since they don't route through the queue). Mitigated by a new `BeforeUncompiledHook` in `ZMQ_BridgeHelpers.ipf` that calls `ThreadGroupRelease(-2)` to release running thread groups before Igor uncompiles, added by the repo owner and confirmed to prevent recurrence in subsequent testing. No Python-side (`server.py`) logic changes were needed beyond updated docstrings describing these fixes; all of the actual fixes are within `ZMQ_BridgeHelpers.ipf`.\n\nv2.3.0: investigated the long-standing, previously-unexplained crash pattern where Igor Pro itself (not this bridge) would sometimes become unreachable shortly after `reload_and_compile_procedures`. Two Windows crash dumps captured during this bridge's development were analyzed with Python's `minidump` package (Igor's own crash reporter provides no stack trace), confirming both were genuine `EXCEPTION_ACCESS_VIOLATION`s deep inside `Igor64.exe` itself, not in this bridge's code or any XOP. A likely mechanism was then identified, based directly on a comparison the repo owner suggested against this repo's own `igortest-tracing.ipf`, whose `CompileAndRestart()`/`AfterCompiledHook()` pattern reloads and compiles procedures reliably with no crash: unlike that reference pattern, this bridge's ZeroMQ-XOP handler runs as a background thread (documented in `HelpFiles/ZeroMQ.ihf` as \"a threaded message handler\") that keeps dispatching incoming `CallFunction` requests regardless of what Igor's main thread is doing -- so a request arriving while `COMPILEPROCEDURES` is mid-rebuild of Igor's own internal function/symbol tables is a plausible cross-thread race, distinct from anything happening inside `AfterCompiledHook` itself (which runs safely afterward, exactly like `igortest`'s hook). Fixed by adding `ZBR_StopHandlerBeforeRecompile()` (calls `zeromq_handler_stop()`) and queuing it as the FIRST deferred `Execute/P` entry in `ZBR_SubmitReloadAndCompile()`, ahead of `RELOAD CHANGED PROCS`/`COMPILEPROCEDURES`; the handler is restarted afterward via the existing, unchanged `AfterCompiledHook` -> `ZBR_EnsureZeroMQBound()` synchronous call. Confirmed live: recompiled successfully afterward, then ran three CONCURRENT `reload_and_compile_procedures()` calls as a stress test -- all three returned `compiled: true` cleanly with no crash, and `check_bridge_health()`/`execute_igor_command()` both worked correctly afterward. Honest caveat: this is a well-reasoned mitigation, not a proven fix -- `Igor64.exe` ships no public symbols, so the exact fault can't be confirmed from here, and the crash was already rare/nondeterministic before this fix. No Python-side (`server.py`) logic changes were needed beyond an updated docstring; the fix is entirely within `ZMQ_BridgeHelpers.ipf`.\n\nv2.2.3: fixed another uncaught-runtime-error-pops-a-dialog bug, this time in `ZBR_FinishToken`, found while cleaning up leftover test-token rows from this bridge's own `root:Packages:ZBR` storage waves. `ZBR_AllocateToken` captures a token's row index (`idx`) at submission time, but `ZBR_FinishToken` -- the callback that actually writes the result and flips the done flag -- runs later, in its own separate deferred `Execute/P` entry (per the v2.2.0 fix). If the `done`/`resultText`/`historyStart` waves are resized smaller in between (e.g. maintenance code clearing out old tokens, exactly as happened live during this session's own cleanup), `idx` can end up pointing past the end of the now-shorter waves. `ZBR_FinishToken` had no bounds check, so writing to `resultText[idx]` in that state threw an uncaught \"Index out of range for wave...\" runtime error -- which, like the v2.2.1 bug, pops a real modal dialog and blocks Igor's entire main thread until a human dismisses it. Fixed by bounds-checking `idx` against `DimSize(done, 0)` before writing anything; if the row no longer exists, the callback now just silently does nothing (there is nothing useful left to recover), and `ZBR_PollCommand`'s own existing bounds check already reports a clean `\"ERROR: unknown token\"` response to the caller instead of a hang. Confirmed live: reproduced the exact original crash (resizing the storage waves to 0 rows while a submitted command's own token was still in flight, which had just thrown the dialog moments earlier during this session's testing), then repeated it after the fix -- this time it returned `\"ERROR: unknown token ...\"` cleanly with no dialog and no hang, and `check_bridge_health()` stayed `OK` throughout. No Python-side changes were needed.\n\nv2.2.2: minor robustness refinement to the v2.2.1 fix, contributed directly by the repo owner after installing it. `ZBR_EnsureCaptureStarted`'s stale-refnum probe (`CaptureHistory(refnumRW, 0)`) and its `AbortOnRTE` were originally written on separate lines; moved onto the SAME line. Reason: Igor's Debug on Error check happens at the END of each line, not each statement -- if the two were on separate lines, a user with Debug on Error enabled (unusual, but this bridge doesn't force the Debugger off during a raw `execute_igor_command`/`submit_igor_command` call the way the `_unattended` variants do) would get a Debugger popup right when the stale refnum's runtime error occurred, before `AbortOnRTE` ever got a chance to convert it into a catchable abort -- defeating the whole point of the v2.2.1 fix in that one specific configuration. Confirmed live both before accepting this change (by temporarily enabling `debug_on_error` via `set_debugger_enabled` and repeating the deliberately-corrupted-refnum test) and after: with the two statements on one line, the corrupted-refnum scenario completes cleanly with no Debugger popup and no hang even with Debug on Error active, and the refnum still self-heals correctly. No other behavior change.\n\nv2.2.1: fixed a bug where `load_experiment` (or a user manually reopening a saved .pxp) could leave the bridge completely unreachable, requiring a human to dismiss a modal Igor error dialog. Root cause: this bridge's history-capture mechanism stores its `CaptureHistoryStart()` reference number in a plain `Variable/G` global (`root:Packages:ZBR:captureRefNum`), which Igor persists into a saved experiment like any other global -- but that refnum is only meaningful within the OS process that created it. Reloading a saved experiment brings the OLD numeric value back even though the process is brand new; the prior code only checked that the global *existed*, not whether its value was still a live capture, so it trusted the stale refnum. Using it then threw a genuine Igor runtime error (\"there is no open file with this reference number\") from a plain top-level `Execute/P` entry -- not caught anywhere -- which popped a real modal dialog and blocked Igor's entire main thread (and therefore every ZeroMQ reply) until a human dismissed it. Confirmed live by saving and reloading an experiment via `load_experiment` and then calling `execute_igor_command`, which reproduced the dialog exactly. Fixed in `ZBR_EnsureCaptureStarted` (`ZMQ_BridgeHelpers.ipf`): the stored refnum is now validated with a `try`/`catch`/`endtry`+`AbortOnRTE` block before being trusted, and silently replaced with a fresh `CaptureHistoryStart()` call if it's stale -- confirmed live afterward, both by deliberately corrupting the refnum to a bogus value and by repeating the exact save-then-`load_experiment`-then-`execute_igor_command` sequence that originally triggered the dialog; neither reproduced the dialog and both recovered silently. (Two syntax mistakes were made and caught along the way while implementing this: a runtime error inside a `try` block does not by itself jump to `catch` without an explicit `AbortOnRTE` immediately after the risky call, and a bare `return` with no value is invalid inside a plain, implicit-`Variable`-returning `Function` -- both confirmed from Igor's own bundled help, \"Flow Control for Aborts\" and \"The Return Statement\" in `Programming.ihf`.) No Python-side (`server.py`) changes were needed for this fix -- it is entirely within the Igor-side `ZMQ_BridgeHelpers.ipf` helper file.\n\nv2.2.0: fixed a serious reliability bug in the v2.1.0 submit/poll primitives, found via live testing of the exact scenario they exist for (a command whose runtime is unknown and could be very long). `ZBR_SubmitCommand`/`ZBR_SubmitCommandUnattended` (Igor-side, in `ZMQ_BridgeHelpers.ipf`) previously joined the submitted command and the internal finish-callback into ONE compound `Execute/P` string (`cmd + \"; \" + finishCall`). Confirmed live: if `cmd` failed to parse, OR hit a genuine Igor-level runtime error partway through (not just a Debugger pause), the ENTIRE joined string aborted, so the appended finish-callback never ran -- leaving `poll_igor_command` reporting `\"done\": false` forever, indistinguishable from a job still genuinely in progress. This directly undermines the reliability guarantee these tools exist to provide. Root-caused via two live tests: (1) queuing an invalid command and a valid one as two SEPARATE `Execute/P` entries showed the second still ran fine despite the first failing, proving Igor's deferred-operation queue processes independently-queued entries regardless of a prior one's failure; (2) the same joined-string command that had a genuine runtime error (not just an invalid command name) also silently swallowed its own appended print statement, confirming the bug applies to runtime errors too, not just parse errors. Fixed by queuing `cmd` and the finish-callback (and, for the `_unattended` variant, a Debugger-disable step and a Debugger-restore step, with the saved settings carried across via persistent globals) as fully independent `Execute/P` entries instead of one joined string -- verified live afterward with an invalid command, a genuine runtime-erroring command, and a normal command, all three now correctly reaching `\"done\": true` instead of hanging. **Known residual limitation, also confirmed live:** there is currently no generic way to tell \"the command ran and legitimately printed nothing\" apart from \"the command errored out with no output\" -- both now come back as `\"done\": true` with an empty/short result and no error indication (an attempted fix using `GetRTError()` in the finish-callback was tested and found not to work: each `Execute/P` entry is dispatched as its own independent top-level execution, so Igor clears any pending runtime-error state before the next queued entry runs). If a submitted command's success needs to be verifiable, have it `print` an explicit sentinel/result value itself.\n\nv2.1.0: added first-class support for commands with an unknown or very long runtime (hours to weeks). `execute_igor_command`/`execute_igor_command_unattended` block the whole MCP tool call while polling, which cannot work for a genuinely long calculation -- the MCP transport itself has been observed to time out a single tool call well under a minute, regardless of the requested `timeout_seconds`. Three new tools expose the existing submit/poll primitives directly: `submit_igor_command`/`submit_igor_command_unattended` queue a command and return a token immediately with no waiting at all, and `poll_igor_command(token)` performs one cheap, instant check for completion, callable any number of times spaced arbitrarily far apart. This is reliable no matter how long the job runs or how many times this bridge process or Claude Desktop itself restarts in between, because all of the actual state (a done flag and the captured text) lives entirely in Igor Pro's own data waves (`root:Packages:ZBR`), not in this bridge's Python process -- the only thing that actually ends the job is Igor Pro itself quitting, crashing, or restarting. As with the existing `_unattended` tools, use `submit_igor_command_unattended` for anything long-running: a Debugger pause partway through leaves `poll_igor_command` reporting \"not done\" forever, indistinguishable from the command still genuinely running.\n\nv2.0.1: three bug fixes found during live v2.0.0 testing. (1) `execute_igor_command`/`execute_igor_command_unattended`: the documented pattern of using `fprintf 0, ...` to get data back does not actually work over this transport -- confirmed live that `CaptureHistory` (which this bridge's capture mechanism relies on) captures `print` output but never captures `fprintf`-to-history output at all (refnum 0, -1, or -2 all silently produce nothing, even though the command itself runs without error). Use `print` instead; all docs/docstrings updated accordingly. (2) `load_experiment`: fixed a silent-failure bug where relaunching Igor Pro with a new experiment file could do nothing at all, with no error reported. Root cause (diagnosed live): the tool was waiting for Igor Pro to stop answering over ZeroMQ as its signal that the old process had quit, but ZeroMQ goes quiet well before the underlying Igor64.exe process actually terminates -- launching the replacement `Igor64.exe /UNATTENDED ` command line while the old process is still alive (even mid-shutdown) doesn't spawn a new process at all; Windows/Igor's single-instance-per-user behavior instead either redirects the load into the still-live old instance (risking an unhandled \"save changes?\" dialog) or silently drops the request if that instance is already mid-quit. Fixed by polling the actual OS process list for the configured executable's own process to fully exit before ever launching the replacement, raising a clear error instead of proceeding if it doesn't exit in time. (3) `read_help_file`: added a `timeout_ms` parameter (default raised from this bridge's usual 5s to 30s) -- confirmed live that exporting a genuinely large help file (e.g. the full \"Igor Reference.ihf\") can take longer than 5 seconds, which previously timed out the call even though the export eventually succeeded Igor-side anyway.\n\nv2.0.0: BREAKING transport change. Replaces the COM Automation Server transport (win32com.client, ProgID `IgorPro.Application`) with the ZeroMQ-XOP's `CallFunction` JSON protocol over a plain localhost TCP socket (tcp://127.0.0.1:5680), talking to a small set of Igor-side helper functions in `Packages/MIES/ZMQ_BridgeHelpers.ipf` (the `ZBR` independent module).\n\nWhy: COM required this bridge process and Igor Pro to run at the SAME Windows privilege level (both elevated, or both not) -- an easy-to-miss mismatch (e.g. Claude Desktop reopened normally after Igor Pro was left running elevated from before). ZeroMQ is a plain TCP socket with no such requirement at all -- elevation no longer matters in any way. `launch_igor_pro_unattended` no longer has an elevation-branching code path: it always launches Igor Pro as a plain, non-elevated child process, at whatever privilege level this bridge process itself runs at.\n\n**New setup requirement**: unlike the COM transport (which worked against a stock Igor Pro installation with zero custom procedure code), this transport requires `Packages/MIES/ZMQ_BridgeHelpers.ipf` to be `#include`-d and compiled into whichever Igor Pro experiment this bridge talks to -- there is no bootstrap path over ZeroMQ itself. This repo's own `Packages/MIES_Include.ipf` already does this permanently. Any OTHER Igor Pro experiment needs `ZMQ_BridgeHelpers.ipf` copied onto its own procedure search path with a matching `#include` added by hand, then a recompile -- see `Packages/doc/igor-pro-bridge.rst` (\"Installation\") for the exact one-time steps.\n\n**Capability improvements**: `get_wave` now supports any wave dimensionality (up to 4D), real and complex numeric waves, text waves, and wave-reference waves -- the entire wave comes back from one round trip, natively serialized (the old COM version was limited to 1D real-valued waves, read one point at a time).\n\n**Behavior changes to be aware of**: `execute_igor_command`/`execute_igor_command_unattended` can no longer separate fprintf-only output from the full echoed history the way COM's `Execute2` could -- both \"results\" and \"history\" now hold the same captured text; prefix a command with `Silent 1;` to suppress the echo. `load_experiment` no longer hot-swaps the open experiment in place (that was a COM-only method with no procedure-language equivalent) -- it now quits the running instance and relaunches Igor Pro with the target file path as a launch argument, a real process restart; unsaved changes in the previously-open experiment are lost. `ensure_igor_pro_bridge_defined` is retired -- it existed to manage a `#ifdef IGOR_PRO_BRIDGE` gating convention that ZBR's always-on independent-module architecture doesn't use.\n\nSee `Packages/doc/igor-pro-bridge.rst` and `SESSION_NOTES.md` in the MIES repository for the full protocol research, design decisions, and confirmed-live test results behind this rewrite.\n\nTools:\n- `execute_igor_command` / `execute_igor_command_unattended`: run a command string on Igor's command line (submitted via `ZBR_SubmitCommand`/`ZBR_SubmitCommandUnattended`, polled via `ZBR_PollCommand`); include a `print` call (NOT `fprintf 0, ...` -- confirmed not captured, see v2.0.1 changelog above) to get data back. Both return `{results, history}`. The `_unattended` variant automatically disables/restores Igor's Debugger around the call.\n- `read_session_history`: read back everything sent to Igor's history area since this bridge's capture started.\n- `get_wave`: read any Igor wave's full data and metadata back (any dimensionality, numeric/complex/text/wave-reference).\n- `load_experiment`: quit the running instance and relaunch Igor Pro with a .pxp file path as a launch argument.\n- `check_bridge_health`: diagnose whether this bridge can reach Igor Pro's ZeroMQ server right now.\n- `check_compilation_state` / `reload_and_compile_procedures`: check and refresh Igor's compiled state after editing a `.ipf` file on disk.\n- `dismiss_compile_error_dialog`: close a stuck \"Function Compilation Error\" dialog via a posted Escape key, without needing OS focus.\n- `get_debugger_state` / `set_debugger_enabled` / `restore_debugger_settings`: read, change, and restore Igor's Debugger settings.\n- `get_environment_summary`: summarize the live instance (version, loaded experiment, XOPs, included procedure files, data folders, Debugger settings).\n- `read_help_file`: read an Igor Pro help file (.ihf) as structured, formatted text.\n- `get_bridge_version`: report the running bridge version and Python/package versions.\n- `configure_igor_launch` / `launch_igor_pro_unattended`: record the Igor Pro executable path once per session, then launch it with the `/UNATTENDED` flag and wait for it to become reachable over ZeroMQ.\n\n**Requirements:**\n- Igor Pro 9.00 or later, running on Windows, with the ZeroMQ-XOP installed and loaded.\n- `Packages/MIES/ZMQ_BridgeHelpers.ipf` (or a copy of it) `#include`-d and compiled into the target experiment -- see `Packages/doc/igor-pro-bridge.rst` (\"Installation\") for the one-time setup steps for experiments other than this repo's own.\n- Python (accessible as `python` on PATH), with the pinned packages in `requirements.txt` installed into that same environment. Run `install.ps1` to do this correctly and also complete pywin32's required post-install step (still needed for window-handling/process-launch helpers, even though this bridge no longer uses COM) -- see that script's own help (`Get-Help ./install.ps1 -Full`).\n- No elevation/privilege-matching requirement of any kind -- this is the change v2.0.0 makes.\n\nPrior COM-based history (v1.x): see `Packages/doc/igor-pro-bridge.rst` for the full v1.x changelog, retained there for reference.", "author": { "name": "Michael Huth" }, @@ -103,7 +103,7 @@ }, { "name": "configure_igor_launch", - "description": "Record the full path to the Igor Pro executable to use for launch_igor_pro_unattended/load_experiment, for this bridge session." + "description": "Record the full path to the Igor Pro executable to use for launch_igor_pro_unattended/load_experiment, for this bridge session. Optionally set (or clear) a custom ZeroMQ port for the next launched instance to bind to and for this bridge itself to connect on." }, { "name": "launch_igor_pro_unattended", diff --git a/tools/igor-mcp-bridge/pyproject.toml b/tools/igor-mcp-bridge/pyproject.toml new file mode 100644 index 0000000000..f07891855c --- /dev/null +++ b/tools/igor-mcp-bridge/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "igor-pro-bridge" +version = "2.3.2" +description = "MCP bridge for controlling Igor Pro via its ZeroMQ-XOP CallFunction interface" +requires-python = ">=3.10" +dependencies = [ + "mcp>=1.29.0,<2", + "pyzmq==27.1.0", + "pywin32==312; sys_platform == 'win32'", +] diff --git a/tools/igor-mcp-bridge/server.py b/tools/igor-mcp-bridge/server.py index 1cdc89925e..df77d0e437 100644 --- a/tools/igor-mcp-bridge/server.py +++ b/tools/igor-mcp-bridge/server.py @@ -39,10 +39,13 @@ Protocol summary (confirmed empirically this session against a live Igor Pro 9.06 instance, and against https://github.com/AllenInstitute/ZeroMQ-XOP's own README): -- Endpoint: `tcp://127.0.0.1:5680` (`IGOR_ZMQ_ENDPOINT` below) -- matches - `ZBR_ZEROMQ_ENDPOINT` in ZMQ_BridgeHelpers.ipf, deliberately NOT MIES's own - `ZEROMQ_BIND_REP_PORT` (5670) so this can coexist with MIES's own (currently - short-circuited) ZeroMQ subsystem in the same experiment. +- Endpoint: `tcp://127.0.0.1:5680` by default (`_igor_zmq_endpoint()` below) -- + matches `ZBR_ZEROMQ_ENDPOINT`/`ZBR_ZEROMQ_DEFAULT_PORT` in ZMQ_BridgeHelpers.ipf, + deliberately NOT MIES's own `ZEROMQ_BIND_REP_PORT` (5670) so this can coexist with + MIES's own (currently short-circuited) ZeroMQ subsystem in the same experiment. + configure_igor_launch(port=...) can override the port this bridge itself connects + to (every ZMQ-talking function goes through `_igor_zmq_endpoint()`, not a fixed + constant, so they all pick up a configured custom port together). - One JSON `CallFunction` request per ZeroMQ REQ-socket round trip: send `{"version": 1, "messageID": ..., "CallFunction": {"name": ..., "params": [...]}}`, receive `{"errorCode": {"value": ..., "msg": ...}, "result": ...}`. A NEW REQ socket @@ -174,6 +177,7 @@ import tempfile import time import uuid +from typing import Optional if sys.platform != "win32": raise RuntimeError( @@ -198,12 +202,18 @@ # --- ZeroMQ transport ---------------------------------------------------------------- -# Matches ZBR_ZEROMQ_ENDPOINT in Packages/MIES/ZMQ_BridgeHelpers.ipf. -IGOR_ZMQ_ENDPOINT = "tcp://127.0.0.1:5680" +# Matches ZBR_ZEROMQ_ENDPOINT/ZBR_ZEROMQ_DEFAULT_PORT in +# Packages/MIES/ZMQ_BridgeHelpers.ipf. +IGOR_ZMQ_HOST = "tcp://127.0.0.1" +IGOR_ZMQ_DEFAULT_PORT = 5680 _ZMQ_DEFAULT_RECV_TIMEOUT_MS = 5000 _ZMQ_SEND_TIMEOUT_MS = 2000 _ZMQ_LINGER_MS = 0 +# Set by configure_igor_launch(port=...) -- see that tool's docstring. None means +# "use IGOR_ZMQ_DEFAULT_PORT". +_configured_igor_port = None + _zmq_context = None @@ -214,6 +224,19 @@ def _get_zmq_context(): return _zmq_context +def _igor_zmq_endpoint(): + """The ZeroMQ endpoint this bridge currently connects to. Every function that + talks to Igor Pro over ZeroMQ must go through this (not a fixed string) so they + all consistently follow whatever custom port configure_igor_launch(port=...) set, + instead of some functions silently still targeting the default port.""" + port = ( + _configured_igor_port + if _configured_igor_port is not None + else IGOR_ZMQ_DEFAULT_PORT + ) + return f"{IGOR_ZMQ_HOST}:{port}" + + class IgorZmqError(RuntimeError): """Igor Pro replied, but reported errorCode.value != 0 for the CallFunction call.""" @@ -221,8 +244,9 @@ class IgorZmqError(RuntimeError): class IgorZmqUnreachable(RuntimeError): """No reply was received at all within the timeout. Could mean: Igor Pro isn't running, ZMQ_BridgeHelpers.ipf isn't #include-d/compiled in the running instance, - its ZeroMQ server socket isn't bound to IGOR_ZMQ_ENDPOINT, or the reply is simply - slow (e.g. a long-running command; pass a longer timeout_ms).""" + its ZeroMQ server socket isn't bound to this bridge's currently configured + endpoint (see _igor_zmq_endpoint), or the reply is simply slow (e.g. a + long-running command; pass a longer timeout_ms).""" def _decode_number(value): @@ -352,11 +376,12 @@ def call_function(name, params=None, timeout_ms=_ZMQ_DEFAULT_RECV_TIMEOUT_MS): } payload = json.dumps(request) + endpoint = _igor_zmq_endpoint() sock = _get_zmq_context().socket(zmq.REQ) sock.setsockopt(zmq.RCVTIMEO, timeout_ms) sock.setsockopt(zmq.SNDTIMEO, _ZMQ_SEND_TIMEOUT_MS) sock.setsockopt(zmq.LINGER, _ZMQ_LINGER_MS) - sock.connect(IGOR_ZMQ_ENDPOINT) + sock.connect(endpoint) try: sock.send_string(payload) reply_raw = sock.recv_string() @@ -365,7 +390,7 @@ def call_function(name, params=None, timeout_ms=_ZMQ_DEFAULT_RECV_TIMEOUT_MS): f"No reply from Igor Pro within {timeout_ms}ms while calling {name!r}. " f"Make sure Igor Pro is running, ZMQ_BridgeHelpers.ipf is #include-d and " f"compiled in the current experiment, and its ZeroMQ server socket is " - f"bound to {IGOR_ZMQ_ENDPOINT!r} (see check_bridge_health)." + f"bound to {endpoint!r} (see check_bridge_health)." ) from None finally: sock.close() @@ -1167,7 +1192,7 @@ def read_help_file(file_path: str, timeout_ms: int = 30000) -> dict: # reason as before: the on-disk layout after Claude Desktop installs a .mcpb isn't # guaranteed to keep server.py and manifest.json at a fixed relative path, and this # needs to be confirmable from inside a conversation independent of that. -_BRIDGE_VERSION = "2.3.1" +_BRIDGE_VERSION = "2.3.2" def _installed_package_version(distribution_name: str) -> str | None: @@ -1212,7 +1237,10 @@ def check_bridge_health() -> dict: actually running (Task Manager)? Is Packages/MIES/ZMQ_BridgeHelpers.ipf #include-d and does check_compilation_state-equivalent info suggest it's compiled? Try `netstat -a -b` (needs admin) to see whether anything is actually - listening on IGOR_ZMQ_ENDPOINT's port. + listening on this bridge's currently configured port (see _igor_zmq_endpoint; + reported in the "problem" message below on FAIL) -- if a custom port was set via + configure_igor_launch(port=...), make sure the target Igor Pro instance was + actually launched with that same port in effect. Returns a dict with a "status" key ("OK" or "FAIL") and, on FAIL, a "problem" key. """ @@ -1226,7 +1254,7 @@ def check_bridge_health() -> dict: "Pro isn't running, ZMQ_BridgeHelpers.ipf isn't #include-d/compiled " "in the current experiment (see that file's own header comment for " "the one-time setup step), or its ZeroMQ socket isn't bound to " - f"{IGOR_ZMQ_ENDPOINT} for some other reason." + f"{_igor_zmq_endpoint()} for some other reason." ), } except IgorZmqError as e: @@ -1417,6 +1445,13 @@ def dismiss_compile_error_dialog() -> dict: # has no privilege-matching requirement, so Igor Pro is always launched as a plain # child process of this bridge process, at whatever privilege level that already is. +# Matches ZBR_ZEROMQ_ENV_PORT in Packages/MIES/ZMQ_BridgeHelpers.ipf: when set, a +# launched Igor Pro instance's ZBR_EnsureZeroMQBound binds its ZeroMQ socket to this +# port instead of its own default (5680). Preparation for talking to more than one +# Igor Pro instance -- see configure_igor_launch. _configured_igor_port itself lives +# up in the "ZeroMQ transport" section since _igor_zmq_endpoint() also reads it. +_IGOR_PRO_BRIDGE_PORT_ENV_VAR = "IGOR_PRO_BRIDGE_PORT" + _configured_igor_exe_path = None @@ -1434,6 +1469,10 @@ def _build_igor_launch_env(): it, MIES's git-shell-out becomes malformed and `ASSERT(!V_flag, "We have git installed but could not regenerate version.txt")` trips on every launch via this path. + + Also carries through _IGOR_PRO_BRIDGE_PORT_ENV_VAR if configure_igor_launch set + (or cleared) it on this process's own os.environ -- no extra handling needed here + since this starts from a plain copy of that. """ env = os.environ.copy() if not env.get("COMSPEC"): @@ -1443,7 +1482,7 @@ def _build_igor_launch_env(): @mcp.tool() -def configure_igor_launch(exe_path: str) -> dict: +def configure_igor_launch(exe_path: str, port: Optional[int] = None) -> dict: """Record the full path to the Igor Pro executable (e.g. "...\\IgorBinaries_x64\\ Igor64.exe") to use for launch_igor_pro_unattended/load_experiment, for the rest of this bridge process's session. @@ -1454,10 +1493,31 @@ def configure_igor_launch(exe_path: str) -> dict: Igor Pro installed in more than one differently-named folder. This setting is session-scoped: it resets if this bridge process itself restarts. - Raises if exe_path does not point to an existing file. Does not otherwise - validate that the file is actually Igor Pro (beyond a soft filename check). + port, if given, is a custom ZeroMQ port for the NEXT launched instance to bind + to, instead of its own default (5680) -- preparation for eventually talking to + more than one Igor Pro instance at once. Setting this writes + IGOR_PRO_BRIDGE_PORT=str(port) into this bridge process's own environment; + launch_igor_pro_unattended/load_experiment inherit that when they start Igor Pro + (see _build_igor_launch_env), and the launched instance's own + ZBR_EnsureZeroMQBound (ZMQ_BridgeHelpers.ipf) reads it back to decide which port + to bind. **Omitting port (or passing None) clears any previously-configured + custom port** -- it removes IGOR_PRO_BRIDGE_PORT from this process's environment + entirely, so the next launch uses Igor's own default port again. This is not + "leave unchanged": repeat the same port value on every call while a custom port + should stay in effect. + + Also updates which port THIS bridge itself connects to for every subsequent tool + call (see _igor_zmq_endpoint) -- every ZMQ-talking function goes through that one + helper, so setting/clearing port here immediately retargets all of them, not just + the next launch. This bridge still only tracks ONE currently-configured + endpoint at a time, though -- talking to two Igor Pro instances simultaneously + (rather than switching which single one this points at) isn't supported yet. + + Raises if exe_path does not point to an existing file, or if port is given but + is not a valid TCP port number (1-65535). Does not otherwise validate that + exe_path is actually Igor Pro (beyond a soft filename check). """ - global _configured_igor_exe_path + global _configured_igor_exe_path, _configured_igor_port normalized = os.path.abspath(exe_path) if not os.path.isfile(normalized): @@ -1468,6 +1528,14 @@ def configure_igor_launch(exe_path: str) -> dict: " -- the exact folder name varies by Igor Pro version) and try again." ) + if port is not None and ( + not isinstance(port, int) or isinstance(port, bool) or not (1 <= port <= 65535) + ): + raise RuntimeError( + f"'{port}' is not a valid TCP port -- expected an integer between 1 and " + "65535, or omit/None to clear a previously-configured custom port." + ) + note = None if "igor" not in os.path.basename(normalized).lower(): note = ( @@ -1476,8 +1544,14 @@ def configure_igor_launch(exe_path: str) -> dict: "user has a renamed executable." ) + if port is not None: + os.environ[_IGOR_PRO_BRIDGE_PORT_ENV_VAR] = str(port) + else: + os.environ.pop(_IGOR_PRO_BRIDGE_PORT_ENV_VAR, None) + _configured_igor_exe_path = normalized - return {"configured_exe_path": normalized, "note": note} + _configured_igor_port = port + return {"configured_exe_path": normalized, "configured_port": port, "note": note} _POST_LAUNCH_POLL_INTERVAL_SECONDS = 1.0