@codesys/mcp-toolkit 1.4.0 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,14 +5,28 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.4.2] - 2025-05-02
9
+ ### Fixed
10
+ - Enhanced session communication with better logging and debugging
11
+ - Improved command file handling and error recovery
12
+ - Fixed path handling for session files
13
+ - Added JSON fallback parser for IronPython 2.7
14
+ - Ensured synchronous script execution to prevent conflicts
15
+ - Increased reliability of session status detection
16
+
17
+ ## [1.4.1] - 2025-05-02
18
+ ### Fixed
19
+ - Fixed IronPython 2.7 compatibility for session management scripts
20
+ - Replaced f-strings with str.format() method calls for IronPython compatibility
21
+ - Added fallback import for json module (using simplejson for IronPython)
22
+ - Improved error handling for session scripts
23
+ - Re-enabled session management feature in codesys_interop.ts
24
+
8
25
  ## [1.4.0] - 2025-05-02
9
26
  ### Added
10
- - Implemented persistent session management functionality
11
- - Added support for keeping CODESYS open between commands
12
- - Created file-based IPC for script execution in existing CODESYS instance
13
- - Improved session reliability with health checks and automatic recovery
14
- - Added session timeout after 10 minutes of inactivity
15
- - Created detailed documentation for session management features
27
+ - Implemented initial framework for session management
28
+ - Added foundation for file-based IPC between Node.js and CODESYS instances
29
+ - Created documentation structure for session management capabilities
16
30
 
17
31
  ## [1.3.0] - 2025-05-02
18
32
  ### Changed
@@ -92,7 +92,7 @@ function executeCodesysScript(scriptContent, codesysExePath, codesysProfileName,
92
92
  throw new Error(`CODESYS executable not found at provided path: ${codesysExePath}`);
93
93
  // --- End Pre-checks ---
94
94
  // Enable or disable session management
95
- const SESSION_FEATURE_DISABLED = false;
95
+ const SESSION_FEATURE_DISABLED = false; // Session feature enabled
96
96
  // --- Check if we should use the persistent session ---
97
97
  if (!SESSION_FEATURE_DISABLED && sessionManager) {
98
98
  const sessionState = sessionManager.getState();
@@ -1,6 +1,62 @@
1
- import sys, os, json, time, traceback
1
+ import sys, os, time, traceback
2
2
  import tempfile
3
3
 
4
+ # Import JSON handling for IronPython 2.7 compatibility
5
+ try:
6
+ import json
7
+ except ImportError:
8
+ # For IronPython 2.7, we need to use simplejson instead
9
+ try:
10
+ import simplejson as json
11
+ except ImportError:
12
+ print("Neither json nor simplejson could be imported. Check IronPython installation.")
13
+ # Provide a minimal fallback implementation for basic needs
14
+ class DummyJson:
15
+ @staticmethod
16
+ def load(file_obj):
17
+ content = file_obj.read()
18
+ # Very basic parsing for our limited needs
19
+ if content.strip().startswith("{") and content.strip().endswith("}"):
20
+ result = {}
21
+ parts = content.strip()[1:-1].split(",")
22
+ for part in parts:
23
+ if ":" in part:
24
+ k, v = part.split(":", 1)
25
+ k = k.strip().strip('"\'')
26
+ v = v.strip().strip('"\'')
27
+ result[k] = v
28
+ return result
29
+ return {}
30
+
31
+ @staticmethod
32
+ def loads(s):
33
+ if s.strip().startswith("{") and s.strip().endswith("}"):
34
+ result = {}
35
+ parts = s.strip()[1:-1].split(",")
36
+ for part in parts:
37
+ if ":" in part:
38
+ k, v = part.split(":", 1)
39
+ k = k.strip().strip('"\'')
40
+ v = v.strip().strip('"\'')
41
+ result[k] = v
42
+ return result
43
+ return {}
44
+
45
+ @staticmethod
46
+ def dump(obj, file_obj, indent=None):
47
+ if isinstance(obj, dict):
48
+ pairs = []
49
+ for k, v in obj.items():
50
+ if isinstance(v, str):
51
+ pairs.append('"{0}": "{1}"'.format(k, v))
52
+ else:
53
+ pairs.append('"{0}": {1}'.format(k, v))
54
+ file_obj.write("{" + ", ".join(pairs) + "}")
55
+ else:
56
+ file_obj.write(str(obj))
57
+
58
+ json = DummyJson()
59
+
4
60
  # Define markers for script results
5
61
  SCRIPT_SUCCESS_MARKER = "SCRIPT_SUCCESS"
6
62
  SCRIPT_ERROR_MARKER = "SCRIPT_ERROR"
@@ -12,17 +68,26 @@ COMMAND_FILE = os.path.join(SESSION_DIR, "command.json")
12
68
  RESULT_FILE = os.path.join(SESSION_DIR, "result.json")
13
69
  SESSION_LOCK_FILE = os.path.join(SESSION_DIR, "session.lock")
14
70
 
71
+ # Print key information at start
72
+ print("Session status check script starting")
73
+ print("tempdir: {0}".format(tempfile.gettempdir()))
74
+ print("SESSION_DIR: {0}".format(SESSION_DIR))
75
+ print("SESSION_STATE_FILE: {0}".format(SESSION_STATE_FILE))
76
+ print("COMMAND_FILE: {0}".format(COMMAND_FILE))
77
+ print("RESULT_FILE: {0}".format(RESULT_FILE))
78
+ print("SESSION_LOCK_FILE: {0}".format(SESSION_LOCK_FILE))
79
+
15
80
  def check_session_status():
16
81
  """Check the status of a CODESYS session"""
17
82
  try:
18
83
  # Check if session directory exists
19
84
  if not os.path.exists(SESSION_DIR):
20
- print(f"{SCRIPT_SUCCESS_MARKER}: No session active. Session directory not found.")
85
+ print("{0}: No session active. Session directory not found.".format(SCRIPT_SUCCESS_MARKER))
21
86
  return False
22
87
 
23
88
  # Check if session state file exists
24
89
  if not os.path.exists(SESSION_STATE_FILE):
25
- print(f"{SCRIPT_SUCCESS_MARKER}: No session active. Session state file not found.")
90
+ print("{0}: No session active. Session state file not found.".format(SCRIPT_SUCCESS_MARKER))
26
91
  return False
27
92
 
28
93
  # Read session state
@@ -30,7 +95,7 @@ def check_session_status():
30
95
  with open(SESSION_STATE_FILE, 'r') as f:
31
96
  session_state = json.load(f)
32
97
  except json.JSONDecodeError:
33
- print(f"{SCRIPT_ERROR_MARKER}: Invalid session state file")
98
+ print("{0}: Invalid session state file".format(SCRIPT_ERROR_MARKER))
34
99
  return False
35
100
 
36
101
  # Check if session is active
@@ -39,7 +104,7 @@ def check_session_status():
39
104
 
40
105
  # If the status indicates the session is not active, return immediately
41
106
  if not active or status in ['stopped', 'error', 'stopping', 'timeout']:
42
- print(f"{SCRIPT_SUCCESS_MARKER}: Session is not active (Status: {status})")
107
+ print("{0}: Session is not active (Status: {1})".format(SCRIPT_SUCCESS_MARKER, status))
43
108
  return False
44
109
 
45
110
  # Check session lock file to see if it's been updated recently
@@ -53,10 +118,10 @@ def check_session_status():
53
118
 
54
119
  # If lock file hasn't been updated in 30 seconds, session might be dead
55
120
  if time_diff > 30:
56
- print(f"WARNING: Session lock file is stale by {time_diff:.1f} seconds")
121
+ print("WARNING: Session lock file is stale by {0:.1f} seconds".format(time_diff))
57
122
  lock_is_stale = True
58
123
  except Exception as e:
59
- print(f"Error reading lock file: {e}")
124
+ print("Error reading lock file: {0}".format(e))
60
125
  lock_is_stale = True
61
126
  else:
62
127
  print("WARNING: Session lock file not found")
@@ -74,7 +139,7 @@ def check_session_status():
74
139
 
75
140
  # Create ping command
76
141
  ping_command = {
77
- "id": f"ping_{int(time.time())}",
142
+ "id": "ping_{0}".format(int(time.time())),
78
143
  "type": "ping", # Use specific ping command type
79
144
  "timestamp": time.time()
80
145
  }
@@ -110,11 +175,11 @@ def check_session_status():
110
175
  print("Ping successful, session is responsive")
111
176
  break
112
177
  except Exception as e:
113
- print(f"Error reading result: {e}")
178
+ print("Error reading result: {0}".format(e))
114
179
  time.sleep(0.5)
115
180
 
116
181
  if not ping_success:
117
- print(f"WARNING: Session did not respond to ping within {timeout} seconds")
182
+ print("WARNING: Session did not respond to ping within {0} seconds".format(timeout))
118
183
 
119
184
  # Reset command file since ping failed
120
185
  try:
@@ -129,11 +194,11 @@ def check_session_status():
129
194
 
130
195
  print("Reset command file after failed ping")
131
196
  except Exception as e:
132
- print(f"Error resetting command file: {e}")
197
+ print("Error resetting command file: {0}".format(e))
133
198
  else:
134
199
  print("WARNING: Command file not found, cannot ping session")
135
200
  except Exception as e:
136
- print(f"Error during ping test: {e}")
201
+ print("Error during ping test: {0}".format(e))
137
202
  traceback.print_exc()
138
203
 
139
204
  # Make final determination based on all checks
@@ -144,21 +209,21 @@ def check_session_status():
144
209
  if session_state.get('project_open', False):
145
210
  project_name = session_state.get('project_name', 'Unknown')
146
211
  project_path = session_state.get('project_path', 'Unknown')
147
- project_info = f", Project: {project_name} ({project_path})"
212
+ project_info = ", Project: {0} ({1})".format(project_name, project_path)
148
213
 
149
214
  # Include session info
150
215
  session_id = session_state.get('session_id', 'Unknown')
151
216
  start_time = session_state.get('start_time', 0)
152
217
  uptime = time.time() - start_time if start_time else 0
153
- uptime_str = f"{uptime:.0f} seconds" if uptime < 60 else f"{uptime/60:.1f} minutes"
218
+ uptime_str = "{0:.0f} seconds".format(uptime) if uptime < 60 else "{0:.1f} minutes".format(uptime/60)
154
219
 
155
220
  if session_active:
156
- print(f"{SCRIPT_SUCCESS_MARKER}: Session is active (ID: {session_id}, Status: {status}, Uptime: {uptime_str}{project_info})")
221
+ print("{0}: Session is active (ID: {1}, Status: {2}, Uptime: {3}{4})".format(SCRIPT_SUCCESS_MARKER, session_id, status, uptime_str, project_info))
157
222
  return True
158
223
  else:
159
224
  # If we get here, session may be in a state where files exist but process is dead
160
225
  if active and status in ["running", "busy"] and lock_is_stale and not ping_success:
161
- print(f"{SCRIPT_SUCCESS_MARKER}: Session appears to be zombie (ID: {session_id}, Status: {status} but unresponsive)")
226
+ print("{0}: Session appears to be zombie (ID: {1}, Status: {2} but unresponsive)".format(SCRIPT_SUCCESS_MARKER, session_id, status))
162
227
  # Update session state to error so it will be considered inactive
163
228
  try:
164
229
  session_state['status'] = 'error'
@@ -170,14 +235,14 @@ def check_session_status():
170
235
 
171
236
  print("Updated session state to error due to unresponsiveness")
172
237
  except Exception as e:
173
- print(f"Error updating session state: {e}")
238
+ print("Error updating session state: {0}".format(e))
174
239
  else:
175
- print(f"{SCRIPT_SUCCESS_MARKER}: Session is not active (Status: {status})")
240
+ print("{0}: Session is not active (Status: {1})".format(SCRIPT_SUCCESS_MARKER, status))
176
241
 
177
242
  return False
178
243
 
179
244
  except Exception as e:
180
- print(f"{SCRIPT_ERROR_MARKER}: Error checking session status - {str(e)}")
245
+ print("{0}: Error checking session status - {1}".format(SCRIPT_ERROR_MARKER, str(e)))
181
246
  traceback.print_exc()
182
247
  return False
183
248
 
@@ -1,5 +1,74 @@
1
- import sys, os, json, time, tempfile, traceback
2
- import scriptengine as script_engine
1
+ import sys, os, time, tempfile, traceback
2
+
3
+ # Import JSON handling for IronPython 2.7 compatibility
4
+ try:
5
+ import json
6
+ except ImportError:
7
+ # For IronPython 2.7, we need to use simplejson instead
8
+ try:
9
+ import simplejson as json
10
+ except ImportError:
11
+ print("Neither json nor simplejson could be imported. Check IronPython installation.")
12
+ # Provide a minimal fallback implementation for basic needs
13
+ class DummyJson:
14
+ @staticmethod
15
+ def load(file_obj):
16
+ content = file_obj.read()
17
+ # Very basic parsing for our limited needs
18
+ if content.strip().startswith("{") and content.strip().endswith("}"):
19
+ result = {}
20
+ parts = content.strip()[1:-1].split(",")
21
+ for part in parts:
22
+ if ":" in part:
23
+ k, v = part.split(":", 1)
24
+ k = k.strip().strip('"\'')
25
+ v = v.strip().strip('"\'')
26
+ result[k] = v
27
+ return result
28
+ return {}
29
+
30
+ @staticmethod
31
+ def loads(s):
32
+ if s.strip().startswith("{") and s.strip().endswith("}"):
33
+ result = {}
34
+ parts = s.strip()[1:-1].split(",")
35
+ for part in parts:
36
+ if ":" in part:
37
+ k, v = part.split(":", 1)
38
+ k = k.strip().strip('"\'')
39
+ v = v.strip().strip('"\'')
40
+ result[k] = v
41
+ return result
42
+ return {}
43
+
44
+ @staticmethod
45
+ def dump(obj, file_obj, indent=None):
46
+ if isinstance(obj, dict):
47
+ pairs = []
48
+ for k, v in obj.items():
49
+ if isinstance(v, str):
50
+ pairs.append('"{0}": "{1}"'.format(k, v))
51
+ else:
52
+ pairs.append('"{0}": {1}'.format(k, v))
53
+ file_obj.write("{" + ", ".join(pairs) + "}")
54
+ else:
55
+ file_obj.write(str(obj))
56
+
57
+ json = DummyJson()
58
+
59
+ # Import scriptengine
60
+ try:
61
+ import scriptengine as script_engine
62
+ print("Successfully imported scriptengine module")
63
+ except ImportError:
64
+ # Log the error and continue - we'll handle this gracefully
65
+ print("Error importing scriptengine. This script must be run within CODESYS environment.")
66
+ script_engine = None
67
+
68
+ # Print Python and platform info
69
+ print("Python version: {0}.{1}.{2}".format(sys.version_info[0], sys.version_info[1], sys.version_info[2]))
70
+ print("Platform: {0}".format(sys.platform))
71
+ print("Executable: {0}".format(sys.executable))
3
72
 
4
73
  # Define markers for script results
5
74
  SCRIPT_SUCCESS_MARKER = "SCRIPT_SUCCESS"
@@ -25,7 +94,7 @@ def setup_session_dir():
25
94
  try:
26
95
  with open(SESSION_STATE_FILE, 'r') as f:
27
96
  old_state = json.load(f)
28
- print(f"Cleaning up previous session (ID: {old_state.get('session_id', 'unknown')})")
97
+ print("Cleaning up previous session (ID: {0})".format(old_state.get('session_id', 'unknown')))
29
98
  except:
30
99
  pass
31
100
 
@@ -36,9 +105,9 @@ def setup_session_dir():
36
105
  if os.path.isfile(filepath):
37
106
  os.unlink(filepath)
38
107
  except Exception as e:
39
- print(f"Error removing file {filepath}: {e}")
108
+ print("Error removing file {0}: {1}".format(filepath, e))
40
109
  except Exception as e:
41
- print(f"Error cleaning up previous session: {e}")
110
+ print("Error cleaning up previous session: {0}".format(e))
42
111
 
43
112
  # Create session directory
44
113
  if not os.path.exists(SESSION_DIR):
@@ -74,12 +143,12 @@ def setup_session_dir():
74
143
  with open(SESSION_LOCK_FILE, 'w') as f:
75
144
  f.write(str(time.time()))
76
145
 
77
- print(f"Session directory setup at: {SESSION_DIR}")
78
- print(f"Session ID: {session_id}")
146
+ print("Session directory setup at: {0}".format(SESSION_DIR))
147
+ print("Session ID: {0}".format(session_id))
79
148
  return True
80
149
  except Exception as e:
81
- print(f"Failed to set up session directory: {e}")
82
- print(f"{SCRIPT_ERROR_MARKER}: Session setup failed - {str(e)}")
150
+ print("Failed to set up session directory: {0}".format(e))
151
+ print("{0}: Session setup failed - {1}".format(SCRIPT_ERROR_MARKER, str(e)))
83
152
  return False
84
153
 
85
154
  def update_session_state(status="running"):
@@ -90,20 +159,29 @@ def update_session_state(status="running"):
90
159
  project_name = "No project open"
91
160
  project_path = "N/A"
92
161
 
93
- primary_project = script_engine.projects.primary
94
- if primary_project:
95
- project_open = True
162
+ # Check if script_engine was properly imported
163
+ if script_engine is None:
164
+ print("Warning: scriptengine not available, cannot get project info")
165
+ else:
166
+ # Try to get primary project
96
167
  try:
97
- project_path = os.path.normcase(os.path.abspath(primary_project.path))
98
- try:
99
- project_name = primary_project.get_name()
100
- if not project_name:
101
- project_name = f"Unnamed (path: {os.path.basename(project_path)})"
102
- except:
103
- project_name = f"Unnamed (path: {os.path.basename(project_path)})"
104
- except Exception as e_path:
105
- project_path = f"N/A (Error: {e_path})"
106
- project_name = "Unnamed (Path Error)"
168
+ primary_project = script_engine.projects.primary
169
+ if primary_project:
170
+ project_open = True
171
+ try:
172
+ project_path = os.path.normcase(os.path.abspath(primary_project.path))
173
+ try:
174
+ project_name = primary_project.get_name()
175
+ if not project_name:
176
+ project_name = "Unnamed (path: {0})".format(os.path.basename(project_path))
177
+ except:
178
+ project_name = "Unnamed (path: {0})".format(os.path.basename(project_path))
179
+ except Exception as e_path:
180
+ project_path = "N/A (Error: {0})".format(str(e_path))
181
+ project_name = "Unnamed (Path Error)"
182
+ except Exception as e:
183
+ print("Error accessing script_engine.projects: {0}".format(str(e)))
184
+ traceback.print_exc()
107
185
 
108
186
  # Read current state file
109
187
  try:
@@ -133,7 +211,7 @@ def update_session_state(status="running"):
133
211
 
134
212
  return True
135
213
  except Exception as e:
136
- print(f"Failed to update session state: {e}")
214
+ print("Failed to update session state: {0}".format(e))
137
215
  traceback.print_exc()
138
216
  return False
139
217
 
@@ -189,13 +267,13 @@ def execute_command(command_data):
189
267
  except Exception as e:
190
268
  traceback.print_exc()
191
269
  result["success"] = False
192
- result["error"] = f"Error executing script: {str(e)}\n{traceback.format_exc()}"
193
- result["output"] = f"{SCRIPT_ERROR_MARKER}: {str(e)}"
270
+ result["error"] = "Error executing script: {0}\n{1}".format(str(e), traceback.format_exc())
271
+ result["output"] = "{0}: {1}".format(SCRIPT_ERROR_MARKER, str(e))
194
272
 
195
273
  elif cmd_type == "stop":
196
274
  # Stop command received
197
275
  result["success"] = True
198
- result["output"] = f"{SCRIPT_SUCCESS_MARKER}: Session stopping"
276
+ result["output"] = "{0}: Session stopping".format(SCRIPT_SUCCESS_MARKER)
199
277
 
200
278
  # Update session state to mark as stopping
201
279
  update_session_state("stopping")
@@ -206,12 +284,12 @@ def execute_command(command_data):
206
284
  elif cmd_type == "ping":
207
285
  # Simple ping command to check if session is alive
208
286
  result["success"] = True
209
- result["output"] = f"{SCRIPT_SUCCESS_MARKER}: Session is active"
287
+ result["output"] = "{0}: Session is active".format(SCRIPT_SUCCESS_MARKER)
210
288
 
211
289
  else:
212
290
  result["success"] = False
213
- result["error"] = f"Unknown command type: {cmd_type}"
214
- result["output"] = f"{SCRIPT_ERROR_MARKER}: Unknown command type: {cmd_type}"
291
+ result["error"] = "Unknown command type: {0}".format(cmd_type)
292
+ result["output"] = "{0}: Unknown command type: {1}".format(SCRIPT_ERROR_MARKER, cmd_type)
215
293
 
216
294
  # Update session state after command execution
217
295
  update_session_state()
@@ -223,8 +301,8 @@ def execute_command(command_data):
223
301
  return {
224
302
  "id": command_data.get("id", "unknown"),
225
303
  "success": False,
226
- "output": f"{SCRIPT_ERROR_MARKER}: Command execution failed",
227
- "error": f"{str(e)}\n{traceback.format_exc()}"
304
+ "output": "{0}: Command execution failed".format(SCRIPT_ERROR_MARKER),
305
+ "error": "{0}\n{1}".format(str(e), traceback.format_exc())
228
306
  }, False
229
307
 
230
308
  def check_command():
@@ -232,67 +310,128 @@ def check_command():
232
310
  try:
233
311
  # If command file doesn't exist, return immediately
234
312
  if not os.path.exists(COMMAND_FILE):
313
+ print("Command file does not exist at {0}".format(COMMAND_FILE))
235
314
  return False
236
315
 
237
316
  # Read the command file
238
317
  try:
318
+ print("Reading command file: {0}".format(COMMAND_FILE))
239
319
  with open(COMMAND_FILE, 'r') as f:
240
- command_data = json.load(f)
241
- except json.JSONDecodeError:
242
- # If command file is corrupted, reset it
320
+ file_content = f.read()
321
+ print("Raw command file content: {0}".format(file_content[:100] if len(file_content) > 100 else file_content))
322
+
323
+ # Check if file is empty
324
+ if not file_content.strip():
325
+ print("Command file is empty, resetting")
326
+ command_data = {"status": "ready", "command": None, "timestamp": time.time()}
327
+ with open(COMMAND_FILE, 'w') as fw:
328
+ json.dump(command_data, fw, indent=2)
329
+ return False
330
+
331
+ # Try to parse JSON
332
+ try:
333
+ command_data = json.loads(file_content)
334
+ except Exception as json_err:
335
+ print("Error parsing command file JSON: {0}".format(json_err))
336
+ # If command file is corrupted, reset it
337
+ command_data = {"status": "ready", "command": None, "timestamp": time.time()}
338
+ with open(COMMAND_FILE, 'w') as fw:
339
+ json.dump(command_data, fw, indent=2)
340
+ return False
341
+ except Exception as read_err:
342
+ print("Error reading command file: {0}".format(read_err))
343
+ # Reset the command file
243
344
  command_data = {"status": "ready", "command": None, "timestamp": time.time()}
244
- with open(COMMAND_FILE, 'w') as f:
245
- json.dump(command_data, f, indent=2)
345
+ try:
346
+ with open(COMMAND_FILE, 'w') as fw:
347
+ json.dump(command_data, fw, indent=2)
348
+ except Exception as write_err:
349
+ print("Error writing reset command file: {0}".format(write_err))
246
350
  return False
247
351
 
248
352
  # Check if there's a new command
249
- if command_data.get("status") == "pending" and command_data.get("command"):
250
- # Extract the command
251
- command = command_data.get("command")
252
-
253
- # Mark as processing
254
- command_data["status"] = "processing"
255
- with open(COMMAND_FILE, 'w') as f:
256
- json.dump(command_data, f, indent=2)
257
-
258
- # Update session state to busy
259
- update_session_state("busy")
260
-
261
- # Execute the command
262
- result, stop_requested = execute_command(command)
263
-
264
- # Write the result
265
- result_data = {
266
- "status": "complete",
267
- "result": result,
268
- "timestamp": time.time()
269
- }
270
-
271
- with open(RESULT_FILE, 'w') as f:
272
- json.dump(result_data, f, indent=2)
273
-
274
- # Reset command file
275
- command_data = {
276
- "status": "ready",
277
- "command": None,
278
- "timestamp": time.time()
279
- }
280
-
281
- with open(COMMAND_FILE, 'w') as f:
282
- json.dump(command_data, f, indent=2)
283
-
284
- # Update session state to running again
285
- if not stop_requested:
286
- update_session_state("running")
353
+ status = command_data.get("status")
354
+ command = command_data.get("command")
355
+
356
+ print("Command status: {0}".format(status))
357
+ if command:
358
+ print("Command found: {0}".format(command.get("type", "unknown")))
287
359
 
288
- # Update last activity time
289
- update_last_activity()
360
+ if status == "pending" and command:
361
+ print("Processing pending command...")
290
362
 
291
- return stop_requested
363
+ try:
364
+ # Mark as processing
365
+ command_data["status"] = "processing"
366
+ with open(COMMAND_FILE, 'w') as f:
367
+ json.dump(command_data, f, indent=2)
368
+ print("Updated command status to 'processing'")
369
+
370
+ # Update session state to busy
371
+ update_session_state("busy")
372
+
373
+ # Execute the command
374
+ print("Executing command: {0}".format(command.get("type", "unknown")))
375
+ result, stop_requested = execute_command(command)
376
+ print("Command execution complete, stop_requested: {0}".format(stop_requested))
377
+
378
+ # Write the result
379
+ result_data = {
380
+ "status": "complete",
381
+ "result": result,
382
+ "timestamp": time.time()
383
+ }
384
+
385
+ print("Writing result to {0}".format(RESULT_FILE))
386
+ with open(RESULT_FILE, 'w') as f:
387
+ json.dump(result_data, f, indent=2)
388
+
389
+ # Reset command file
390
+ command_data = {
391
+ "status": "ready",
392
+ "command": None,
393
+ "timestamp": time.time()
394
+ }
395
+
396
+ print("Resetting command file")
397
+ with open(COMMAND_FILE, 'w') as f:
398
+ json.dump(command_data, f, indent=2)
399
+
400
+ # Update session state to running again
401
+ if not stop_requested:
402
+ update_session_state("running")
403
+
404
+ # Update last activity time
405
+ update_last_activity()
406
+
407
+ return stop_requested
408
+ except Exception as cmd_err:
409
+ print("Error processing command: {0}".format(cmd_err))
410
+ traceback.print_exc()
411
+
412
+ # Try to reset the command file
413
+ try:
414
+ command_data = {
415
+ "status": "ready",
416
+ "command": None,
417
+ "timestamp": time.time()
418
+ }
419
+ with open(COMMAND_FILE, 'w') as f:
420
+ json.dump(command_data, f, indent=2)
421
+ except Exception as reset_err:
422
+ print("Error resetting command file: {0}".format(reset_err))
423
+
424
+ # Try to restore session state
425
+ try:
426
+ update_session_state("running")
427
+ except Exception as state_err:
428
+ print("Error updating session state: {0}".format(state_err))
429
+
430
+ return False
292
431
 
293
432
  return False
294
433
  except Exception as e:
295
- print(f"Error checking command: {e}")
434
+ print("Error in check_command: {0}".format(e))
296
435
  traceback.print_exc()
297
436
  return False
298
437
 
@@ -308,7 +447,7 @@ def update_last_activity():
308
447
  with open(SESSION_STATE_FILE, 'w') as f:
309
448
  json.dump(state, f, indent=2)
310
449
  except Exception as e:
311
- print(f"Error updating last activity: {e}")
450
+ print("Error updating last activity: {0}".format(e))
312
451
 
313
452
  def check_inactivity_timeout():
314
453
  """Check if session has been inactive for too long"""
@@ -321,31 +460,73 @@ def check_inactivity_timeout():
321
460
  current_time = time.time()
322
461
 
323
462
  if (current_time - last_activity) > MAX_INACTIVITY_TIME:
324
- print(f"Session inactive for {current_time - last_activity:.1f} seconds, shutting down")
463
+ print("Session inactive for {0:.1f} seconds, shutting down".format(current_time - last_activity))
325
464
  return True
326
465
 
327
466
  return False
328
467
  except Exception as e:
329
- print(f"Error checking inactivity timeout: {e}")
468
+ print("Error checking inactivity timeout: {0}".format(e))
330
469
  return False
331
470
 
332
471
  def command_loop():
333
472
  """Main loop to check for and execute commands"""
334
473
  stop_requested = False
335
474
  last_state_update = time.time()
475
+ loop_count = 0
476
+ last_heartbeat = time.time()
336
477
 
337
- print(f"Starting command loop...")
338
- print(f"Command file: {COMMAND_FILE}")
339
- print(f"Result file: {RESULT_FILE}")
478
+ print("Starting command loop...")
479
+ print("Command file: {0}".format(COMMAND_FILE))
480
+ print("Result file: {0}".format(RESULT_FILE))
481
+ print("Session lock file: {0}".format(SESSION_LOCK_FILE))
482
+ print("Session state file: {0}".format(SESSION_STATE_FILE))
483
+ print("Poll interval: {0} seconds".format(POLL_INTERVAL))
484
+ print("Max inactivity time: {0} seconds".format(MAX_INACTIVITY_TIME))
485
+
486
+ # Set up initial session state files
487
+ try:
488
+ # Create initial command file if it doesn't exist
489
+ if not os.path.exists(COMMAND_FILE):
490
+ with open(COMMAND_FILE, 'w') as f:
491
+ json.dump({
492
+ "status": "ready",
493
+ "command": None,
494
+ "timestamp": time.time()
495
+ }, f, indent=2)
496
+ print("Created initial command file")
497
+
498
+ # Create initial result file if it doesn't exist
499
+ if not os.path.exists(RESULT_FILE):
500
+ with open(RESULT_FILE, 'w') as f:
501
+ json.dump({
502
+ "status": "none",
503
+ "result": None,
504
+ "timestamp": time.time()
505
+ }, f, indent=2)
506
+ print("Created initial result file")
507
+ except Exception as setup_err:
508
+ print("Error setting up initial files: {0}".format(setup_err))
509
+ traceback.print_exc()
340
510
 
341
511
  while not stop_requested:
342
512
  try:
513
+ loop_count += 1
514
+ current_time = time.time()
515
+
343
516
  # Update session lock file to indicate we're alive
344
517
  with open(SESSION_LOCK_FILE, 'w') as f:
345
- f.write(str(time.time()))
518
+ f.write(str(current_time))
519
+
520
+ # Print heartbeat message every 60 seconds
521
+ if current_time - last_heartbeat > 60:
522
+ print("Command loop heartbeat - still alive (iteration {0})".format(loop_count))
523
+ last_heartbeat = current_time
346
524
 
347
525
  # Check for new commands
526
+ print("Checking for commands...")
348
527
  stop_requested = check_command()
528
+ if stop_requested:
529
+ print("Stop was requested by a command")
349
530
 
350
531
  # Check for inactivity timeout
351
532
  if check_inactivity_timeout():
@@ -354,8 +535,8 @@ def command_loop():
354
535
  stop_requested = True
355
536
 
356
537
  # Periodically update session state (every 30 seconds)
357
- current_time = time.time()
358
538
  if (current_time - last_state_update) > 30:
539
+ print("Periodic session state update")
359
540
  update_session_state()
360
541
  last_state_update = current_time
361
542
 
@@ -363,12 +544,12 @@ def command_loop():
363
544
  time.sleep(POLL_INTERVAL)
364
545
 
365
546
  except Exception as e:
366
- print(f"Error in command loop: {e}")
547
+ print("Error in command loop: {0}".format(e))
367
548
  traceback.print_exc()
368
- # Sleep for a bit
549
+ # Sleep for a bit before retrying
369
550
  time.sleep(POLL_INTERVAL)
370
551
 
371
- print("Command loop stopped.")
552
+ print("Command loop stopped after {0} iterations".format(loop_count))
372
553
 
373
554
  # Main script
374
555
  def main():
@@ -381,17 +562,17 @@ def main():
381
562
  update_session_state()
382
563
 
383
564
  # Start command loop
384
- print(f"{SCRIPT_SUCCESS_MARKER}: CODESYS session started")
565
+ print("{0}: CODESYS session started".format(SCRIPT_SUCCESS_MARKER))
385
566
  command_loop()
386
567
 
387
568
  # Update final state
388
569
  update_session_state("stopped")
389
570
 
390
- print(f"{SCRIPT_SUCCESS_MARKER}: CODESYS session stopped cleanly")
571
+ print("{0}: CODESYS session stopped cleanly".format(SCRIPT_SUCCESS_MARKER))
391
572
  sys.exit(0)
392
573
  except Exception as e:
393
- print(f"Error in session: {e}")
394
- print(f"{SCRIPT_ERROR_MARKER}: Session failed - {str(e)}")
574
+ print("Error in session: {0}".format(e))
575
+ print("{0}: Session failed - {1}".format(SCRIPT_ERROR_MARKER, str(e)))
395
576
  traceback.print_exc()
396
577
  sys.exit(1)
397
578
 
@@ -1,6 +1,67 @@
1
- import sys, os, json, time, traceback
1
+ import sys, os, time, traceback
2
2
  import tempfile
3
3
 
4
+ # Import JSON handling for IronPython 2.7 compatibility
5
+ try:
6
+ import json
7
+ except ImportError:
8
+ # For IronPython 2.7, we need to use simplejson instead
9
+ try:
10
+ import simplejson as json
11
+ except ImportError:
12
+ print("Neither json nor simplejson could be imported. Check IronPython installation.")
13
+ # Provide a minimal fallback implementation for basic needs
14
+ class DummyJson:
15
+ @staticmethod
16
+ def load(file_obj):
17
+ content = file_obj.read()
18
+ # Very basic parsing for our limited needs
19
+ if content.strip().startswith("{") and content.strip().endswith("}"):
20
+ result = {}
21
+ parts = content.strip()[1:-1].split(",")
22
+ for part in parts:
23
+ if ":" in part:
24
+ k, v = part.split(":", 1)
25
+ k = k.strip().strip('"\'')
26
+ v = v.strip().strip('"\'')
27
+ result[k] = v
28
+ return result
29
+ return {}
30
+
31
+ @staticmethod
32
+ def loads(s):
33
+ if s.strip().startswith("{") and s.strip().endswith("}"):
34
+ result = {}
35
+ parts = s.strip()[1:-1].split(",")
36
+ for part in parts:
37
+ if ":" in part:
38
+ k, v = part.split(":", 1)
39
+ k = k.strip().strip('"\'')
40
+ v = v.strip().strip('"\'')
41
+ result[k] = v
42
+ return result
43
+ return {}
44
+
45
+ @staticmethod
46
+ def dump(obj, file_obj, indent=None):
47
+ if isinstance(obj, dict):
48
+ pairs = []
49
+ for k, v in obj.items():
50
+ if isinstance(v, str):
51
+ pairs.append('"{0}": "{1}"'.format(k, v))
52
+ else:
53
+ pairs.append('"{0}": {1}'.format(k, v))
54
+ file_obj.write("{" + ", ".join(pairs) + "}")
55
+ else:
56
+ file_obj.write(str(obj))
57
+
58
+ json = DummyJson()
59
+
60
+ # Print Python and platform info
61
+ print("Python version: {0}.{1}.{2}".format(sys.version_info[0], sys.version_info[1], sys.version_info[2]))
62
+ print("Platform: {0}".format(sys.platform))
63
+ print("Executable: {0}".format(sys.executable))
64
+
4
65
  # Define markers for script results
5
66
  SCRIPT_SUCCESS_MARKER = "SCRIPT_SUCCESS"
6
67
  SCRIPT_ERROR_MARKER = "SCRIPT_ERROR"
@@ -12,17 +73,26 @@ COMMAND_FILE = os.path.join(SESSION_DIR, "command.json")
12
73
  RESULT_FILE = os.path.join(SESSION_DIR, "result.json")
13
74
  SESSION_LOCK_FILE = os.path.join(SESSION_DIR, "session.lock")
14
75
 
76
+ # Print key information at start
77
+ print("Session stop script starting")
78
+ print("tempdir: {0}".format(tempfile.gettempdir()))
79
+ print("SESSION_DIR: {0}".format(SESSION_DIR))
80
+ print("SESSION_STATE_FILE: {0}".format(SESSION_STATE_FILE))
81
+ print("COMMAND_FILE: {0}".format(COMMAND_FILE))
82
+ print("RESULT_FILE: {0}".format(RESULT_FILE))
83
+ print("SESSION_LOCK_FILE: {0}".format(SESSION_LOCK_FILE))
84
+
15
85
  def stop_session():
16
86
  """Stop a running CODESYS session"""
17
87
  try:
18
88
  # Check if session directory exists
19
89
  if not os.path.exists(SESSION_DIR):
20
- print(f"{SCRIPT_ERROR_MARKER}: No session directory found at {SESSION_DIR}")
90
+ print("{0}: No session directory found at {1}".format(SCRIPT_ERROR_MARKER, SESSION_DIR))
21
91
  return False
22
92
 
23
93
  # Check if session state file exists
24
94
  if not os.path.exists(SESSION_STATE_FILE):
25
- print(f"{SCRIPT_ERROR_MARKER}: No session state file found at {SESSION_STATE_FILE}")
95
+ print("{0}: No session state file found at {1}".format(SCRIPT_ERROR_MARKER, SESSION_STATE_FILE))
26
96
  return False
27
97
 
28
98
  # Read session state
@@ -30,17 +100,17 @@ def stop_session():
30
100
  with open(SESSION_STATE_FILE, 'r') as f:
31
101
  session_state = json.load(f)
32
102
  except json.JSONDecodeError:
33
- print(f"{SCRIPT_ERROR_MARKER}: Invalid session state file")
103
+ print("{0}: Invalid session state file".format(SCRIPT_ERROR_MARKER))
34
104
  return False
35
105
 
36
106
  # Check if session is active
37
107
  if not session_state.get('active', False) or session_state.get('status') == 'stopped':
38
- print(f"{SCRIPT_SUCCESS_MARKER}: Session is already stopped")
108
+ print("{0}: Session is already stopped".format(SCRIPT_SUCCESS_MARKER))
39
109
  return True
40
110
 
41
111
  # Check if command file exists
42
112
  if not os.path.exists(COMMAND_FILE):
43
- print(f"{SCRIPT_ERROR_MARKER}: Command file not found at {COMMAND_FILE}")
113
+ print("{0}: Command file not found at {1}".format(SCRIPT_ERROR_MARKER, COMMAND_FILE))
44
114
  return False
45
115
 
46
116
  # Try to read the lock file to check if session is still alive
@@ -52,13 +122,13 @@ def stop_session():
52
122
 
53
123
  current_time = time.time()
54
124
  if (current_time - lock_time) > 30:
55
- print(f"WARNING: Session lock file is stale ({current_time - lock_time:.1f} seconds old). Session may have crashed.")
125
+ print("WARNING: Session lock file is stale ({0:.1f} seconds old). Session may have crashed.".format(current_time - lock_time))
56
126
  except Exception as e:
57
- print(f"Warning: Error reading lock file: {e}")
127
+ print("Warning: Error reading lock file: {0}".format(e))
58
128
 
59
129
  # Create stop command
60
130
  stop_command = {
61
- "id": f"stop_{int(time.time())}",
131
+ "id": "stop_{0}".format(int(time.time())),
62
132
  "type": "stop",
63
133
  "timestamp": time.time()
64
134
  }
@@ -73,7 +143,7 @@ def stop_session():
73
143
  with open(COMMAND_FILE, 'w') as f:
74
144
  json.dump(command_data, f, indent=2)
75
145
 
76
- print(f"Stop command sent to session")
146
+ print("Stop command sent to session")
77
147
 
78
148
  # Wait for result (with timeout)
79
149
  timeout = 15 # seconds (increased timeout for more reliability)
@@ -94,10 +164,10 @@ def stop_session():
94
164
  result = result_data.get("result")
95
165
  if result.get('id') == stop_command['id']:
96
166
  result_received = True
97
- print(f"Session stop result: {result.get('output', 'No output')}")
167
+ print("Session stop result: {0}".format(result.get('output', 'No output')))
98
168
  break
99
169
  except Exception as e:
100
- print(f"Error reading result: {e}")
170
+ print("Error reading result: {0}".format(e))
101
171
  time.sleep(0.5)
102
172
 
103
173
  # Update session state to indicate it's stopped regardless of result
@@ -112,15 +182,15 @@ def stop_session():
112
182
  with open(SESSION_STATE_FILE, 'w') as f:
113
183
  json.dump(session_state, f, indent=2)
114
184
  except Exception as e:
115
- print(f"Warning: Failed to update session state file: {e}")
185
+ print("Warning: Failed to update session state file: {0}".format(e))
116
186
 
117
187
  if result_received:
118
- print(f"{SCRIPT_SUCCESS_MARKER}: Session stopped successfully")
188
+ print("{0}: Session stopped successfully".format(SCRIPT_SUCCESS_MARKER))
119
189
  return True
120
190
  else:
121
191
  # If we didn't get a response, the session may be hung
122
192
  # Force a cleanup by removing session files
123
- print(f"Warning: Session did not respond to stop command. Forcing cleanup.")
193
+ print("Warning: Session did not respond to stop command. Forcing cleanup.")
124
194
  try:
125
195
  # Try to remove all session files to ensure a clean state for next session
126
196
  for filename in os.listdir(SESSION_DIR):
@@ -129,16 +199,16 @@ def stop_session():
129
199
  if os.path.isfile(filepath):
130
200
  os.unlink(filepath)
131
201
  except Exception as e:
132
- print(f"Error removing file {filepath}: {e}")
202
+ print("Error removing file {0}: {1}".format(filepath, e))
133
203
 
134
- print(f"{SCRIPT_SUCCESS_MARKER}: Session files cleaned up forcefully")
204
+ print("{0}: Session files cleaned up forcefully".format(SCRIPT_SUCCESS_MARKER))
135
205
  return True
136
206
  except Exception as e:
137
- print(f"{SCRIPT_ERROR_MARKER}: Failed to force cleanup - {str(e)}")
207
+ print("{0}: Failed to force cleanup - {1}".format(SCRIPT_ERROR_MARKER, str(e)))
138
208
  return False
139
209
 
140
210
  except Exception as e:
141
- print(f"{SCRIPT_ERROR_MARKER}: Error stopping session - {str(e)}")
211
+ print("{0}: Error stopping session - {1}".format(SCRIPT_ERROR_MARKER, str(e)))
142
212
  traceback.print_exc()
143
213
  return False
144
214
 
@@ -122,7 +122,8 @@ class CodesysSessionManager extends events_1.EventEmitter {
122
122
  this.idleTimeoutMs = idleTimeoutMs;
123
123
  this.sessionId = (0, uuid_1.v4)();
124
124
  // Set up session directory and files
125
- this.sessionDir = path.join(SESSION_DIR, this.sessionId);
125
+ // IMPORTANT: Don't use a subdirectory - Python scripts expect files directly in SESSION_DIR
126
+ this.sessionDir = SESSION_DIR;
126
127
  this.commandFilePath = path.join(this.sessionDir, COMMAND_FILE);
127
128
  this.resultFilePath = path.join(this.sessionDir, RESULT_FILE);
128
129
  this.stateFilePath = path.join(this.sessionDir, STATE_FILE);
@@ -442,16 +443,27 @@ class CodesysSessionManager extends events_1.EventEmitter {
442
443
  }, 2000); // Give it 2 seconds to terminate gracefully
443
444
  });
444
445
  }
445
- // Clean up session directory
446
+ // Clean up session files (not entire directory)
446
447
  try {
447
448
  if (fs.existsSync(this.sessionDir)) {
448
- console.error(`SESSION_MANAGER: Cleaning up session directory: ${this.sessionDir}`);
449
- // Use recursive: true once we're on Node.js 14+
450
- this.recursiveRemoveDir(this.sessionDir);
449
+ console.error(`SESSION_MANAGER: Cleaning up session files in: ${this.sessionDir}`);
450
+ // Remove only the session-related files, not the entire directory
451
+ const filesToRemove = [
452
+ this.commandFilePath,
453
+ this.resultFilePath,
454
+ this.stateFilePath,
455
+ this.lockFilePath
456
+ ];
457
+ for (const filePath of filesToRemove) {
458
+ if (fs.existsSync(filePath)) {
459
+ fs.unlinkSync(filePath);
460
+ console.error(`SESSION_MANAGER: Removed session file: ${filePath}`);
461
+ }
462
+ }
451
463
  }
452
464
  }
453
465
  catch (error) {
454
- console.error(`SESSION_MANAGER: Error cleaning up session directory: ${error}`);
466
+ console.error(`SESSION_MANAGER: Error cleaning up session files: ${error}`);
455
467
  }
456
468
  this.codesysProcess = null;
457
469
  this.sessionState = exports.SessionState.STOPPED;
@@ -524,8 +536,6 @@ class CodesysSessionManager extends events_1.EventEmitter {
524
536
  }
525
537
  // Reset idle timeout since we're about to do work
526
538
  this.resetIdleTimeout();
527
- // Mark the session as busy
528
- this.sessionState = exports.SessionState.BUSY;
529
539
  // Check if the command file exists
530
540
  if (!fs.existsSync(this.commandFilePath)) {
531
541
  this.sessionState = exports.SessionState.ERROR;
@@ -536,6 +546,50 @@ class CodesysSessionManager extends events_1.EventEmitter {
536
546
  this.sessionState = exports.SessionState.ERROR;
537
547
  throw new Error(`Result file not found at ${this.resultFilePath}`);
538
548
  }
549
+ // IMPORTANT: Ensure only one script is executed at a time by checking if other scripts are pending
550
+ if (this.pendingScripts.size > 0) {
551
+ console.error(`SESSION_MANAGER: Waiting for ${this.pendingScripts.size} pending scripts to complete before executing new script`);
552
+ // Wait for all pending scripts to complete
553
+ yield new Promise((resolve) => {
554
+ const checkInterval = setInterval(() => {
555
+ if (this.pendingScripts.size === 0) {
556
+ clearInterval(checkInterval);
557
+ resolve();
558
+ }
559
+ }, 100);
560
+ });
561
+ console.error('SESSION_MANAGER: All pending scripts completed, proceeding with new script execution');
562
+ }
563
+ // Mark the session as busy
564
+ this.sessionState = exports.SessionState.BUSY;
565
+ // Read current command file to ensure we're not overwriting an in-progress command
566
+ try {
567
+ const currentCommandData = JSON.parse(fs.readFileSync(this.commandFilePath, 'utf8'));
568
+ if (currentCommandData.status === 'pending' || currentCommandData.status === 'processing') {
569
+ console.error(`SESSION_MANAGER: Found pending command in file, waiting for it to complete`);
570
+ // Wait for the command to complete (status to be 'ready')
571
+ yield new Promise((resolve) => {
572
+ const checkInterval = setInterval(() => {
573
+ try {
574
+ const checkData = JSON.parse(fs.readFileSync(this.commandFilePath, 'utf8'));
575
+ if (checkData.status === 'ready') {
576
+ clearInterval(checkInterval);
577
+ resolve();
578
+ }
579
+ }
580
+ catch (err) {
581
+ // If we can't read the file, just continue
582
+ console.error(`SESSION_MANAGER: Error reading command file during wait: ${err}`);
583
+ }
584
+ }, 100);
585
+ });
586
+ console.error('SESSION_MANAGER: Previous command completed, proceeding with new command');
587
+ }
588
+ }
589
+ catch (err) {
590
+ console.error(`SESSION_MANAGER: Error reading current command file: ${err}`);
591
+ // If we can't read the file, we'll try to proceed anyway
592
+ }
539
593
  // Return a promise that will be resolved when script execution completes
540
594
  return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
541
595
  try {
@@ -584,7 +638,8 @@ class CodesysSessionManager extends events_1.EventEmitter {
584
638
  timestamp: Date.now()
585
639
  };
586
640
  // Send the command by writing to the command file
587
- fs.writeFileSync(this.commandFilePath, JSON.stringify(commandData));
641
+ console.error(`SESSION_MANAGER: Writing command to file ${this.commandFilePath}`);
642
+ fs.writeFileSync(this.commandFilePath, JSON.stringify(commandData, null, 2));
588
643
  console.error(`SESSION_MANAGER: Command written to file`);
589
644
  // Start reading the result file
590
645
  this.readResultFile(scriptId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codesys/mcp-toolkit",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
4
4
  "description": "Model Context Protocol (MCP) server for CODESYS automation platform",
5
5
  "main": "dist/server.js",
6
6
  "bin": {
@@ -0,0 +1,16 @@
1
+ f"{SCRIPT_ERROR_MARKER}: Invalid session state file" | "{0}: Invalid session state file".format(SCRIPT_ERROR_MARKER)
2
+ f"{SCRIPT_SUCCESS_MARKER}: Session is not active (Status: {status})" | "{0}: Session is not active (Status: {1})".format(SCRIPT_SUCCESS_MARKER, status)
3
+ f"WARNING: Session lock file is stale by {time_diff:.1f} seconds" | "WARNING: Session lock file is stale by {0:.1f} seconds".format(time_diff)
4
+ f"Error reading lock file: {e}" | "Error reading result: {0}".format(e)
5
+ f"ping_{int(time.time())}" | "ping_{0}".format(int(time.time()))
6
+ f"WARNING: Session did not respond to ping within {timeout} seconds" | "WARNING: Session did not respond to ping within {0} seconds".format(timeout)
7
+ f"Error resetting command file: {e}" | "Error resetting command file: {0}".format(e)
8
+ f"Error during ping test: {e}" | "Error during ping test: {0}".format(e)
9
+ f", Project: {project_name} ({project_path})" | ", Project: {0} ({1})".format(project_name, project_path)
10
+ f"{uptime:.0f} seconds" | "{0:.0f} seconds".format(uptime)
11
+ f"{uptime/60:.1f} minutes" | "{0:.1f} minutes".format(uptime/60)
12
+ f"{SCRIPT_SUCCESS_MARKER}: Session is active (ID: {session_id}, Status: {status}, Uptime: {uptime_str}{project_info})" | "{0}: Session is active (ID: {1}, Status: {2}, Uptime: {3}{4})".format(SCRIPT_SUCCESS_MARKER, session_id, status, uptime_str, project_info)
13
+ f"{SCRIPT_SUCCESS_MARKER}: Session appears to be zombie (ID: {session_id}, Status: {status} but unresponsive)" | "{0}: Session appears to be zombie (ID: {1}, Status: {2} but unresponsive)".format(SCRIPT_SUCCESS_MARKER, session_id, status)
14
+ f"Error updating session state: {e}" | "Error updating session state: {0}".format(e)
15
+ f"{SCRIPT_SUCCESS_MARKER}: Session is not active (Status: {status})" | "{0}: Session is not active (Status: {1})".format(SCRIPT_SUCCESS_MARKER, status)
16
+ f"{SCRIPT_ERROR_MARKER}: Error checking session status - {str(e)}" | "{0}: Error checking session status - {1}".format(SCRIPT_ERROR_MARKER, str(e))
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Script to fix f-strings in Python files to make them compatible with IronPython 2.7.
4
+ Converts f-strings to str.format() calls.
5
+ """
6
+
7
+ import re
8
+ import sys
9
+ import os
10
+
11
+ def process_file(filepath):
12
+ print(f"Processing {filepath}")
13
+ with open(filepath, 'r', encoding='utf-8') as f:
14
+ content = f.read()
15
+
16
+ # Find and replace f-strings
17
+ # This is a simplified approach - complex nested expressions might need manual review
18
+ pattern = r'f"([^"]*\{[^}]+\}[^"]*)"'
19
+
20
+ # Count potential matches
21
+ matches = re.findall(pattern, content)
22
+ print(f"Found {len(matches)} potential f-strings")
23
+
24
+ # Function to convert each match
25
+ def replace_fstring(match):
26
+ template_str = match.group(1)
27
+ # Replace {var} with {0}, {1}, etc.
28
+ var_pattern = r'\{([^}:]+)(?::([^}]+))?\}'
29
+ var_matches = re.finditer(var_pattern, template_str)
30
+
31
+ format_args = []
32
+ i = 0
33
+
34
+ new_template = template_str
35
+ for m in var_matches:
36
+ var_name = m.group(1)
37
+ format_spec = m.group(2)
38
+
39
+ if format_spec:
40
+ repl = "{" + str(i) + ":" + format_spec + "}"
41
+ else:
42
+ repl = "{" + str(i) + "}"
43
+
44
+ # Store the argument
45
+ format_args.append(var_name)
46
+
47
+ # Replace in the template
48
+ new_template = new_template.replace(m.group(0), repl, 1)
49
+ i += 1
50
+
51
+ # Build the complete format expression
52
+ args_str = ", ".join(format_args)
53
+ return f'"{new_template}".format({args_str})'
54
+
55
+ # Apply the replacements
56
+ new_content = re.sub(pattern, replace_fstring, content)
57
+
58
+ # Write the changes back
59
+ with open(filepath, 'w', encoding='utf-8') as f:
60
+ f.write(new_content)
61
+
62
+ print(f"Completed processing {filepath}")
63
+
64
+ def main():
65
+ if len(sys.argv) < 2:
66
+ print("Usage: python fix_fstrings.py <file_paths...>")
67
+ sys.exit(1)
68
+
69
+ for file_path in sys.argv[1:]:
70
+ if os.path.exists(file_path):
71
+ process_file(file_path)
72
+ else:
73
+ print(f"Error: File {file_path} not found")
74
+
75
+ if __name__ == "__main__":
76
+ main()