From 48fa83ccce66f11574ad095abccf89e34e3b04c7 Mon Sep 17 00:00:00 2001 From: phobicdotno Date: Wed, 29 Apr 2026 13:17:54 +0200 Subject: [PATCH] fix(create_project): correct prompt-suppression API + close project after swap (#12) Two issues caught when Karstein noted suppression wasn't actually working: 1. Wrong API. The script was setting: script_engine.system.script_prompt_handling = ScriptPromptHandling.NoFlag but ScriptPromptHandling has no 'NoFlag' member (it's 'SuppressPrompts'), AND script_prompt_handling is a read-only property -- the settable one is the obsolete-but-still-functional 'prompt_handling' (with the obsolete PromptHandling enum, where PromptHandling.NONE = 0 is documented as equivalent to ScriptPromptHandling.SuppressPrompts). The bare AttributeError was silently swallowed by try/except so prompts stayed forwarded to UI -> intermittent code-1 crashes when the storage format dialog popped. Fix: set system.prompt_handling = PromptHandling.NONE, with int-literal fallback if the enum import doesn't resolve. Log when neither works. 2. After-swap stale state. Even after a successful update() that preserves the device subtree, CODESYS keeps the OLD device's library version pins in its in-memory project model. compile_project then errors with 'Could not open library IoStandard, 3.1.3.1 (System)' 'Device description for PLCWinNT is missing' Fix: project.close() at the end of create_project (drops in-memory state) + delete the precompilecache file. The next MCP tool call's ensure_project_open reopens fresh against the swapped XML and compile is clean. Live verified on SP22 P1: - create_project WinV3Test deviceName='CODESYS Control Win V3 x64' -> succeeds, no UI dialog, project saved + closed. - compile_project (first call after creation) -> 0 errors, 0 warnings. - create_pou Application/FB_Counter + set_pou_code with real ST. - PLC_PRG references FB_Counter. - compile_project -> 0 errors, 0 warnings. Co-authored-by: Karstein Phobic Nyvold Kvistad --- src/scripts/create_project.py | 62 ++++++++++++++++++++++++++++++----- 1 file changed, 54 insertions(+), 8 deletions(-) diff --git a/src/scripts/create_project.py b/src/scripts/create_project.py index ef8d60b..87f4a51 100644 --- a/src/scripts/create_project.py +++ b/src/scripts/create_project.py @@ -93,16 +93,30 @@ def _resolve_device_by_name(name): try: # Force-suppress modal prompts (storage-format conversion, etc.) - # ScriptPromptHandling.NoFlag is the default per the stub but some IDE - # contexts get re-set to AlwaysForwardPrompts by other plugins. Setting - # it explicitly here makes sure remove() / project.add() don't hang on - # a confirmation dialog the script can't see. + # The settable property is `system.prompt_handling` (the OBSOLETE + # PromptHandling enum is what the setter accepts); the newer + # `script_prompt_handling` is read-only. PromptHandling.NONE = 0 is + # documented as equivalent to ScriptPromptHandling.SuppressPrompts. + # Without this, project.update() / project.add() hang on a modal + # the script can't see. + suppression_set = False try: - from scriptengine import ScriptPromptHandling - script_engine.system.script_prompt_handling = ScriptPromptHandling.NoFlag - print("DEBUG: script_prompt_handling = NoFlag (silent default).") + from scriptengine import PromptHandling + script_engine.system.prompt_handling = PromptHandling.NONE + suppression_set = True + print("DEBUG: system.prompt_handling = PromptHandling.NONE (suppress).") except Exception as e: - print("DEBUG: could not set script_prompt_handling: %s" % e) + print("DEBUG: could not set system.prompt_handling = PromptHandling.NONE: %s" % e) + if not suppression_set: + try: + script_engine.system.prompt_handling = 0 + suppression_set = True + print("DEBUG: system.prompt_handling = 0 (suppress, int fallback).") + except Exception as e: + print("DEBUG: int-literal prompt_handling = 0 also failed: %s" % e) + if not suppression_set: + print("WARN: prompt suppression NOT set -- update()/remove()/save() " + "may hang on a modal dialog the script can't see.") print("DEBUG: Python script create_project (copy from template):") print("DEBUG: Template Source = %s" % TEMPLATE_PROJECT_PATH) @@ -238,6 +252,38 @@ try: except Exception as save_err: print("WARN: Save after open failed: %s" % save_err) + # After a deviceName swap, CODESYS keeps stale library version pins + # in the in-memory project state -- the next compile_project produces + # errors like "Could not open library 'IoStandard, 3.1.3.1 (System)'" + # and "Device description for 'PLCWinNT' is missing" even though the + # libman view (list_project_libraries) shows the new resolutions. + # Closing the project (without reopening) flushes the in-memory state; + # the next MCP tool call will trigger ensure_project_open which loads + # the swapped XML fresh. Also nuke the precompilecache file while the + # project is closed so the next IDE session also starts clean. + if device_swapped: + try: + print("DEBUG: Closing project to flush stale device state...") + project.close() + print("DEBUG: Project closed -- next tool call will reopen fresh.") + except Exception as close_err: + print("WARN: project.close() failed (next compile may show stale " + "PLCWinNT/IoStandard 3.1.3.1 errors until project is reopened): %s" + % close_err) + try: + project_dir = os.path.dirname(PROJECT_FILE_PATH) + base = os.path.basename(PROJECT_FILE_PATH) + if base.lower().endswith('.project'): + base = base[:-len('.project')] + cache_path = os.path.join(project_dir, base + '_project.precompilecache') + if os.path.exists(cache_path): + os.remove(cache_path) + print("DEBUG: Deleted stale precompilecache: %s" % cache_path) + else: + print("DEBUG: No precompilecache to delete (path not present): %s" % cache_path) + except Exception as cache_err: + print("WARN: Could not delete precompilecache: %s" % cache_err) + print("Project Created from Template Copy at: %s" % PROJECT_FILE_PATH) if device_swapped: print("Device set to: %s" % swapped_to)