From 418f678409c066601219a3006b7845d07e42c6d0 Mon Sep 17 00:00:00 2001 From: Karstein Phobic Nyvold Kvistad Date: Sun, 26 Apr 2026 23:05:42 +0200 Subject: [PATCH] fix(compile_project, get_compile_messages): coerce Python long to int before json.dumps On IronPython 2.7 (the interpreter CODESYS embeds), some compiler diagnostic fields surface as 'long' values. json.dumps() cannot serialize long and raises TypeError, so both tools return an internal error instead of the diagnostic data the user asked for. Walk the diagnostic dicts before serialization and coerce any long values to int. Same fix in both scripts since they share the diagnostic shape. --- src/scripts/compile_project.py | 149 +++++++++++++++------------- src/scripts/get_compile_messages.py | 138 +++++++++++++------------- 2 files changed, 147 insertions(+), 140 deletions(-) diff --git a/src/scripts/compile_project.py b/src/scripts/compile_project.py index 3c9da2d..735a220 100644 --- a/src/scripts/compile_project.py +++ b/src/scripts/compile_project.py @@ -1,5 +1,72 @@ import sys, scriptengine as script_engine, os, traceback, json + +def _coerce_int(v): + """IronPython 2.7's json.dumps cannot serialize System.Int64-backed + `long` values, which is what CODESYS's compile-message objects expose + as line_number / position. Coerce to native int. Returns None on + failure so the message still serializes (just with line=null) instead + of taking down the whole emit.""" + if v is None: + return None + try: + return int(v) + except (TypeError, ValueError, OverflowError): + return None + + +def _coerce_str(v): + """Some message fields come back as System.Uri / System.IO.FileInfo / + similar CLR objects whose default __str__ json.dumps refuses. Force a + str() so the value is always serializable.""" + if v is None: + return None + try: + return str(v) + except Exception: + return None + + +def _build_message_entry(msg): + """Extract a JSON-serializable dict from a single compile-message object. + Centralised so both compile_project and get_compile_messages share the + same shape and the same coercion logic.""" + entry = {} + if hasattr(msg, 'severity'): + try: + sev = str(msg.severity).lower() + except Exception: + sev = 'unknown' + if 'error' in sev: + entry['severity'] = 'error' + elif 'warning' in sev: + entry['severity'] = 'warning' + elif 'info' in sev: + entry['severity'] = 'info' + else: + entry['severity'] = sev + else: + entry['severity'] = 'unknown' + text = None + for attr in ('text', 'message'): + if hasattr(msg, attr): + text = _coerce_str(getattr(msg, attr)) + if text is not None: + break + if text is None: + text = _coerce_str(msg) + entry['text'] = text + if hasattr(msg, 'object_name'): + entry['object'] = _coerce_str(msg.object_name) + elif hasattr(msg, 'source'): + entry['object'] = _coerce_str(msg.source) + if hasattr(msg, 'line_number'): + entry['line'] = _coerce_int(msg.line_number) + elif hasattr(msg, 'position'): + entry['line'] = _coerce_int(msg.position) + return entry + + try: print("DEBUG: compile_project script: Project='%s'" % PROJECT_FILE_PATH) primary_project = ensure_project_open(PROJECT_FILE_PATH) @@ -56,29 +123,7 @@ try: if msg_objects is not None: messages_found = True for msg in msg_objects: - entry = {} - if hasattr(msg, 'severity'): - sev = str(msg.severity).lower() - if 'error' in sev: - entry['severity'] = 'error' - elif 'warning' in sev: - entry['severity'] = 'warning' - elif 'info' in sev: - entry['severity'] = 'info' - else: - entry['severity'] = sev - else: - entry['severity'] = 'unknown' - entry['text'] = getattr(msg, 'text', getattr(msg, 'message', str(msg))) - if hasattr(msg, 'object_name'): - entry['object'] = msg.object_name - elif hasattr(msg, 'source'): - entry['object'] = str(msg.source) - if hasattr(msg, 'line_number'): - entry['line'] = msg.line_number - elif hasattr(msg, 'position'): - entry['line'] = msg.position - messages.append(entry) + messages.append(_build_message_entry(msg)) print("DEBUG: Got %d messages from app.get_message_objects()" % len(messages)) except Exception as e: print("DEBUG: app.get_message_objects() failed: %s" % e) @@ -92,29 +137,7 @@ try: if msg_objects is not None: messages_found = True for msg in msg_objects: - entry = {} - if hasattr(msg, 'severity'): - sev = str(msg.severity).lower() - if 'error' in sev: - entry['severity'] = 'error' - elif 'warning' in sev: - entry['severity'] = 'warning' - elif 'info' in sev: - entry['severity'] = 'info' - else: - entry['severity'] = sev - else: - entry['severity'] = 'unknown' - entry['text'] = getattr(msg, 'text', getattr(msg, 'message', str(msg))) - if hasattr(msg, 'object_name'): - entry['object'] = msg.object_name - elif hasattr(msg, 'source'): - entry['object'] = str(msg.source) - if hasattr(msg, 'line_number'): - entry['line'] = msg.line_number - elif hasattr(msg, 'position'): - entry['line'] = msg.position - messages.append(entry) + messages.append(_build_message_entry(msg)) print("DEBUG: Got %d messages from system.get_message_objects()" % len(messages)) except Exception as e: print("DEBUG: system.get_message_objects() failed: %s" % e) @@ -128,34 +151,20 @@ try: if msg_objects is not None: messages_found = True for msg in msg_objects: - entry = {} - if hasattr(msg, 'severity'): - sev = str(msg.severity).lower() - if 'error' in sev: - entry['severity'] = 'error' - elif 'warning' in sev: - entry['severity'] = 'warning' - elif 'info' in sev: - entry['severity'] = 'info' - else: - entry['severity'] = sev - else: - entry['severity'] = 'unknown' - entry['text'] = getattr(msg, 'text', getattr(msg, 'message', str(msg))) - if hasattr(msg, 'object_name'): - entry['object'] = msg.object_name - elif hasattr(msg, 'source'): - entry['object'] = str(msg.source) - if hasattr(msg, 'line_number'): - entry['line'] = msg.line_number - elif hasattr(msg, 'position'): - entry['line'] = msg.position - messages.append(entry) + messages.append(_build_message_entry(msg)) print("DEBUG: Got %d messages from system.get_messages()" % len(messages)) except Exception as e: print("DEBUG: system.get_messages() failed: %s" % e) - messages_json = json.dumps(messages) + # Defensive json.dumps: if a stray field still slips past the coercion + # helpers, retry with a default=str fallback so a single odd type + # doesn't kill the whole emit. The default param converts unknown + # objects via str() instead of raising TypeError. + try: + messages_json = json.dumps(messages) + except TypeError as je: + print("WARN: json.dumps raised %s -- retrying with default=str fallback" % je) + messages_json = json.dumps(messages, default=lambda o: str(o)) print("### COMPILE_MESSAGES_START ###") print(messages_json) print("### COMPILE_MESSAGES_END ###") diff --git a/src/scripts/get_compile_messages.py b/src/scripts/get_compile_messages.py index 69fd67e..119d098 100644 --- a/src/scripts/get_compile_messages.py +++ b/src/scripts/get_compile_messages.py @@ -1,5 +1,65 @@ import sys, scriptengine as script_engine, os, traceback, json + +def _coerce_int(v): + """IronPython 2.7's json.dumps cannot serialize System.Int64-backed + `long` values, which is what CODESYS's compile-message objects expose + as line_number / position. Coerce to native int.""" + if v is None: + return None + try: + return int(v) + except (TypeError, ValueError, OverflowError): + return None + + +def _coerce_str(v): + """Force str() on CLR-typed fields (System.Uri etc.).""" + if v is None: + return None + try: + return str(v) + except Exception: + return None + + +def _build_message_entry(msg): + entry = {} + if hasattr(msg, 'severity'): + try: + sev = str(msg.severity).lower() + except Exception: + sev = 'unknown' + if 'error' in sev: + entry['severity'] = 'error' + elif 'warning' in sev: + entry['severity'] = 'warning' + elif 'info' in sev: + entry['severity'] = 'info' + else: + entry['severity'] = sev + else: + entry['severity'] = 'unknown' + text = None + for attr in ('text', 'message'): + if hasattr(msg, attr): + text = _coerce_str(getattr(msg, attr)) + if text is not None: + break + if text is None: + text = _coerce_str(msg) + entry['text'] = text + if hasattr(msg, 'object_name'): + entry['object'] = _coerce_str(msg.object_name) + elif hasattr(msg, 'source'): + entry['object'] = _coerce_str(msg.source) + if hasattr(msg, 'line_number'): + entry['line'] = _coerce_int(msg.line_number) + elif hasattr(msg, 'position'): + entry['line'] = _coerce_int(msg.position) + return entry + + try: print("DEBUG: get_compile_messages script: Project='%s'" % PROJECT_FILE_PATH) primary_project = ensure_project_open(PROJECT_FILE_PATH) @@ -41,29 +101,7 @@ try: if msg_objects is not None: messages_found = True for msg in msg_objects: - entry = {} - if hasattr(msg, 'severity'): - sev = str(msg.severity).lower() - if 'error' in sev: - entry['severity'] = 'error' - elif 'warning' in sev: - entry['severity'] = 'warning' - elif 'info' in sev: - entry['severity'] = 'info' - else: - entry['severity'] = sev - else: - entry['severity'] = 'unknown' - entry['text'] = getattr(msg, 'text', getattr(msg, 'message', str(msg))) - if hasattr(msg, 'object_name'): - entry['object'] = msg.object_name - elif hasattr(msg, 'source'): - entry['object'] = str(msg.source) - if hasattr(msg, 'line_number'): - entry['line'] = msg.line_number - elif hasattr(msg, 'position'): - entry['line'] = msg.position - messages.append(entry) + messages.append(_build_message_entry(msg)) print("DEBUG: Got %d messages from app.get_message_objects()" % len(messages)) except Exception as e: print("DEBUG: app.get_message_objects() failed: %s" % e) @@ -77,29 +115,7 @@ try: if msg_objects is not None: messages_found = True for msg in msg_objects: - entry = {} - if hasattr(msg, 'severity'): - sev = str(msg.severity).lower() - if 'error' in sev: - entry['severity'] = 'error' - elif 'warning' in sev: - entry['severity'] = 'warning' - elif 'info' in sev: - entry['severity'] = 'info' - else: - entry['severity'] = sev - else: - entry['severity'] = 'unknown' - entry['text'] = getattr(msg, 'text', getattr(msg, 'message', str(msg))) - if hasattr(msg, 'object_name'): - entry['object'] = msg.object_name - elif hasattr(msg, 'source'): - entry['object'] = str(msg.source) - if hasattr(msg, 'line_number'): - entry['line'] = msg.line_number - elif hasattr(msg, 'position'): - entry['line'] = msg.position - messages.append(entry) + messages.append(_build_message_entry(msg)) print("DEBUG: Got %d messages from system.get_message_objects()" % len(messages)) except Exception as e: print("DEBUG: system.get_message_objects() failed: %s" % e) @@ -113,34 +129,16 @@ try: if msg_objects is not None: messages_found = True for msg in msg_objects: - entry = {} - if hasattr(msg, 'severity'): - sev = str(msg.severity).lower() - if 'error' in sev: - entry['severity'] = 'error' - elif 'warning' in sev: - entry['severity'] = 'warning' - elif 'info' in sev: - entry['severity'] = 'info' - else: - entry['severity'] = sev - else: - entry['severity'] = 'unknown' - entry['text'] = getattr(msg, 'text', getattr(msg, 'message', str(msg))) - if hasattr(msg, 'object_name'): - entry['object'] = msg.object_name - elif hasattr(msg, 'source'): - entry['object'] = str(msg.source) - if hasattr(msg, 'line_number'): - entry['line'] = msg.line_number - elif hasattr(msg, 'position'): - entry['line'] = msg.position - messages.append(entry) + messages.append(_build_message_entry(msg)) print("DEBUG: Got %d messages from system.get_messages()" % len(messages)) except Exception as e: print("DEBUG: system.get_messages() failed: %s" % e) - messages_json = json.dumps(messages) + try: + messages_json = json.dumps(messages) + except TypeError as je: + print("WARN: json.dumps raised %s -- retrying with default=str fallback" % je) + messages_json = json.dumps(messages, default=lambda o: str(o)) print("### COMPILE_MESSAGES_START ###") print(messages_json) print("### COMPILE_MESSAGES_END ###")