@codesys/mcp-toolkit 1.5.8 → 2.0.0-alpha.0

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.
Files changed (110) hide show
  1. package/CHANGELOG.md +153 -189
  2. package/LICENSE +21 -21
  3. package/README.md +224 -107
  4. package/dist/actions.d.ts +28 -0
  5. package/dist/actions.js +98 -0
  6. package/dist/actions.js.map +1 -0
  7. package/dist/bridge/bridge.d.ts +105 -0
  8. package/dist/bridge/bridge.js +400 -0
  9. package/dist/bridge/bridge.js.map +1 -0
  10. package/dist/cli.d.ts +2 -0
  11. package/dist/cli.js +196 -0
  12. package/dist/cli.js.map +1 -0
  13. package/dist/codesys/cache.d.ts +48 -0
  14. package/dist/codesys/cache.js +82 -0
  15. package/dist/codesys/cache.js.map +1 -0
  16. package/dist/codesys/changes.d.ts +57 -0
  17. package/dist/codesys/changes.js +133 -0
  18. package/dist/codesys/changes.js.map +1 -0
  19. package/dist/codesys/decl.d.ts +45 -0
  20. package/dist/codesys/decl.js +251 -0
  21. package/dist/codesys/decl.js.map +1 -0
  22. package/dist/codesys/devices.d.ts +34 -0
  23. package/dist/codesys/devices.js +82 -0
  24. package/dist/codesys/devices.js.map +1 -0
  25. package/dist/codesys/diff.d.ts +13 -0
  26. package/dist/codesys/diff.js +61 -0
  27. package/dist/codesys/diff.js.map +1 -0
  28. package/dist/codesys/install.d.ts +40 -0
  29. package/dist/codesys/install.js +144 -0
  30. package/dist/codesys/install.js.map +1 -0
  31. package/dist/codesys/library-browse.d.ts +19 -0
  32. package/dist/codesys/library-browse.js +57 -0
  33. package/dist/codesys/library-browse.js.map +1 -0
  34. package/dist/codesys/outline.d.ts +32 -0
  35. package/dist/codesys/outline.js +219 -0
  36. package/dist/codesys/outline.js.map +1 -0
  37. package/dist/codesys/simulation.d.ts +34 -0
  38. package/dist/codesys/simulation.js +57 -0
  39. package/dist/codesys/simulation.js.map +1 -0
  40. package/dist/codesys/st.d.ts +36 -0
  41. package/dist/codesys/st.js +69 -0
  42. package/dist/codesys/st.js.map +1 -0
  43. package/dist/daemon.d.ts +46 -0
  44. package/dist/daemon.js +173 -0
  45. package/dist/daemon.js.map +1 -0
  46. package/dist/driver.d.ts +25 -0
  47. package/dist/driver.js +33 -0
  48. package/dist/driver.js.map +1 -0
  49. package/dist/panel/sim-panel.html +357 -0
  50. package/dist/panel/viewer.html +126 -0
  51. package/dist/panel-types.d.ts +20 -0
  52. package/dist/panel-types.js +3 -0
  53. package/dist/panel-types.js.map +1 -0
  54. package/dist/panel.d.ts +27 -0
  55. package/dist/panel.js +208 -0
  56. package/dist/panel.js.map +1 -0
  57. package/dist/server.d.ts +5 -0
  58. package/dist/server.js +33 -974
  59. package/dist/server.js.map +1 -0
  60. package/dist/tools.d.ts +103 -0
  61. package/dist/tools.js +793 -0
  62. package/dist/tools.js.map +1 -0
  63. package/dist/viewer.d.ts +14 -0
  64. package/dist/viewer.js +125 -0
  65. package/dist/viewer.js.map +1 -0
  66. package/package.json +37 -21
  67. package/watcher/mcp_compat.py +147 -0
  68. package/watcher/mcp_ops.py +1178 -0
  69. package/watcher/mcp_sim.py +246 -0
  70. package/watcher/watcher.py +255 -0
  71. package/.claude/settings.local.json +0 -29
  72. package/CLAUDE.md +0 -26
  73. package/CONTRIBUTING.md +0 -72
  74. package/README.md.enhanced +0 -155
  75. package/codesys-mcp-toolkit-1.2.0.tgz +0 -0
  76. package/codesys-mcp-toolkit-1.2.1.tgz +0 -0
  77. package/codesys-mcp-toolkit-1.5.7.tgz +0 -0
  78. package/codesys-mcp-toolkit-1.5.8.tgz +0 -0
  79. package/dist/bin.js +0 -75
  80. package/dist/check_session_paths.js +0 -149
  81. package/dist/codesys_interop.js +0 -435
  82. package/dist/direct_session_command.js +0 -293
  83. package/dist/scriptLoader.js +0 -165
  84. package/dist/scripts/check_status.py +0 -21
  85. package/dist/scripts/compile_project.py +0 -65
  86. package/dist/scripts/create_method.py +0 -80
  87. package/dist/scripts/create_pou.py +0 -102
  88. package/dist/scripts/create_project.py +0 -116
  89. package/dist/scripts/create_property.py +0 -78
  90. package/dist/scripts/ensure_project_open.py +0 -154
  91. package/dist/scripts/find_object_by_path.py +0 -111
  92. package/dist/scripts/get_pou_code.py +0 -91
  93. package/dist/scripts/get_project_structure.py +0 -72
  94. package/dist/scripts/open_project.py +0 -25
  95. package/dist/scripts/save_project.py +0 -29
  96. package/dist/scripts/session_status.py +0 -281
  97. package/dist/scripts/set_pou_code.py +0 -109
  98. package/dist/scripts/start_session.py +0 -809
  99. package/dist/scripts/stop_session.py +0 -324
  100. package/dist/sessionManager.js +0 -798
  101. package/docs/configuration.md +0 -141
  102. package/docs/index.md +0 -26
  103. package/docs/installation.md +0 -84
  104. package/docs/session_management.md +0 -110
  105. package/examples/README.md +0 -78
  106. package/examples/sample_config.json +0 -32
  107. package/replacements_session_status.txt +0 -16
  108. package/scriptDoc.txt +0 -8679
  109. package/scripts/build.js +0 -25
  110. package/scripts/fix_fstrings.py +0 -76
@@ -1,809 +0,0 @@
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))
72
-
73
- # Define markers for script results
74
- SCRIPT_SUCCESS_MARKER = "SCRIPT_SUCCESS"
75
- SCRIPT_ERROR_MARKER = "SCRIPT_ERROR"
76
-
77
- # Session state and command files
78
- SESSION_DIR = os.path.join(tempfile.gettempdir(), "codesys_session")
79
- SESSION_STATE_FILE = os.path.join(SESSION_DIR, "session_state.json")
80
- COMMAND_FILE = os.path.join(SESSION_DIR, "command.json")
81
- RESULT_FILE = os.path.join(SESSION_DIR, "result.json")
82
- SESSION_LOCK_FILE = os.path.join(SESSION_DIR, "session.lock")
83
- POLL_INTERVAL = 0.1 # seconds (faster polling for better responsiveness)
84
- MAX_INACTIVITY_TIME = 600 # seconds (10 minutes of inactivity before auto-shutdown)
85
-
86
- def setup_session_dir():
87
- """Create the session directory and initial files"""
88
- try:
89
- # Clean up any existing session first
90
- if os.path.exists(SESSION_DIR):
91
- try:
92
- # Try to read existing session state
93
- if os.path.exists(SESSION_STATE_FILE):
94
- try:
95
- with open(SESSION_STATE_FILE, 'r') as f:
96
- old_state = json.load(f)
97
- print("Cleaning up previous session (ID: {0})".format(old_state.get('session_id', 'unknown')))
98
- except:
99
- pass
100
-
101
- # Remove all files in the directory
102
- for filename in os.listdir(SESSION_DIR):
103
- filepath = os.path.join(SESSION_DIR, filename)
104
- try:
105
- if os.path.isfile(filepath):
106
- os.unlink(filepath)
107
- except Exception as e:
108
- print("Error removing file {0}: {1}".format(filepath, e))
109
- except Exception as e:
110
- print("Error cleaning up previous session: {0}".format(e))
111
-
112
- # Create session directory
113
- if not os.path.exists(SESSION_DIR):
114
- os.makedirs(SESSION_DIR)
115
-
116
- # Initialize session state
117
- session_id = str(time.time()) + "_" + str(os.getpid())
118
- session_state = {
119
- "active": True,
120
- "session_id": session_id,
121
- "start_time": time.time(),
122
- "last_activity": time.time(),
123
- "status": "running",
124
- "project_open": False,
125
- "project_name": "",
126
- "project_path": "",
127
- "version": "1.0"
128
- }
129
-
130
- # Write session state
131
- with open(SESSION_STATE_FILE, 'w') as f:
132
- json.dump(session_state, f, indent=2)
133
-
134
- # Create empty command file
135
- with open(COMMAND_FILE, 'w') as f:
136
- json.dump({"status": "ready", "command": None, "timestamp": time.time()}, f, indent=2)
137
-
138
- # Create empty result file
139
- with open(RESULT_FILE, 'w') as f:
140
- json.dump({"status": "none", "result": None, "timestamp": time.time()}, f, indent=2)
141
-
142
- # Create lock file
143
- with open(SESSION_LOCK_FILE, 'w') as f:
144
- f.write(str(time.time()))
145
-
146
- print("Session directory setup at: {0}".format(SESSION_DIR))
147
- print("Session ID: {0}".format(session_id))
148
- return True
149
- except Exception as 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)))
152
- return False
153
-
154
- def update_session_state(status="running"):
155
- """Update the session state file with current CODESYS state"""
156
- try:
157
- # Get current state
158
- project_open = False
159
- project_name = "No project open"
160
- project_path = "N/A"
161
-
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
167
- try:
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()
185
-
186
- # Read current state file
187
- try:
188
- with open(SESSION_STATE_FILE, 'r') as f:
189
- session_state = json.load(f)
190
- except (FileNotFoundError, json.JSONDecodeError):
191
- # State file doesn't exist or is invalid, create a new one
192
- session_id = str(time.time()) + "_" + str(os.getpid())
193
- session_state = {
194
- "active": True,
195
- "session_id": session_id,
196
- "start_time": time.time(),
197
- "status": status,
198
- "version": "1.0"
199
- }
200
-
201
- # Update state
202
- session_state["project_open"] = project_open
203
- session_state["project_name"] = project_name
204
- session_state["project_path"] = project_path
205
- session_state["last_update"] = time.time()
206
- session_state["status"] = status
207
-
208
- # Write updated state
209
- with open(SESSION_STATE_FILE, 'w') as f:
210
- json.dump(session_state, f, indent=2)
211
-
212
- return True
213
- except Exception as e:
214
- print("Failed to update session state: {0}".format(e))
215
- traceback.print_exc()
216
- return False
217
-
218
- def execute_command(command_data):
219
- """Execute a command"""
220
- try:
221
- cmd_type = command_data.get("type", "execute_script")
222
- script = command_data.get("script", "")
223
- cmd_id = command_data.get("id", "unknown")
224
-
225
- result = {
226
- "id": cmd_id,
227
- "success": False,
228
- "output": "",
229
- "error": ""
230
- }
231
-
232
- print("Executing command type: {0} with ID: {1}".format(cmd_type, cmd_id))
233
- sys.stdout.flush()
234
-
235
- if cmd_type == "execute_script" and script:
236
- # Execute the script in the current CODESYS context
237
- try:
238
- # Log the first few lines of the script for debugging
239
- script_lines = script.split("\n")
240
- print("Script preview (first 3 lines):")
241
- for i in range(min(3, len(script_lines))):
242
- print(" {0}: {1}".format(i+1, script_lines[i]))
243
- sys.stdout.flush()
244
-
245
- print("Script length: {0} chars, {1} lines".format(len(script), len(script_lines)))
246
- sys.stdout.flush()
247
-
248
- # Check for critical operations in the script
249
- has_project_create = "copy2" in script or "shutil.copy" in script
250
- has_project_open = "projects.open" in script
251
-
252
- if has_project_create:
253
- print("WARNING: Script contains project creation operations - these may require special handling")
254
- sys.stdout.flush()
255
-
256
- if has_project_open:
257
- print("WARNING: Script contains project open operations - these may require special handling")
258
- sys.stdout.flush()
259
-
260
- # Create a temporary globals dictionary with the current globals
261
- # This allows us to isolate the script execution somewhat
262
- local_globals = globals().copy()
263
-
264
- # Capture output by temporarily replacing stdout/stderr
265
- # Use custom approach compatible with IronPython 2.7
266
- try:
267
- import StringIO as io_module # Python 2.x
268
- except ImportError:
269
- import io as io_module # Python 3.x
270
-
271
- stdout_buffer = io_module.StringIO()
272
- stderr_buffer = io_module.StringIO()
273
-
274
- # Save original stdout and stderr
275
- old_stdout = sys.stdout
276
- old_stderr = sys.stderr
277
-
278
- print("Execution starting - output will be captured")
279
- sys.stdout.flush()
280
-
281
- # Replace with our buffers
282
- sys.stdout = stdout_buffer
283
- sys.stderr = stderr_buffer
284
-
285
- # For critical operations that might deadlock, we need to be more careful
286
- if has_project_create or has_project_open:
287
- # For project creation/opening, we'll execute the script with some guards
288
- print("Executing critical script with guards...")
289
- try:
290
- # Execute the script in a more controlled manner with debugging
291
- print("DEBUG-CRITICAL: About to execute project creation/open script")
292
- # First write project ops directly to the result file to prevent hanging
293
- try:
294
- with open(RESULT_FILE, 'w') as f:
295
- result_data = {
296
- "status": "in_progress",
297
- "result": {
298
- "id": command.get("id", "unknown"),
299
- "success": False,
300
- "output": "Project creation/opening operation in progress...",
301
- },
302
- "timestamp": time.time()
303
- }
304
- json.dump(result_data, f, indent=2)
305
- print("DEBUG-CRITICAL: Created preliminary result file")
306
- except Exception as rf_err:
307
- print("DEBUG-CRITICAL: Error creating preliminary result: {0}".format(rf_err))
308
-
309
- # Now execute the script
310
- exec(script, local_globals)
311
- print("DEBUG-CRITICAL: Script executed successfully")
312
- except Exception as e:
313
- # If the script fails, capture the error
314
- print("ERROR in critical script execution: {0}".format(e))
315
- traceback.print_exc()
316
-
317
- print("Critical script execution finished")
318
- else:
319
- # For normal scripts, execute normally
320
- try:
321
- # Execute the script
322
- exec(script, local_globals)
323
- finally:
324
- # No special handling needed
325
- pass
326
-
327
- # Restore original stdout and stderr
328
- sys.stdout = old_stdout
329
- sys.stderr = old_stderr
330
-
331
- print("Execution completed - processing results")
332
- sys.stdout.flush()
333
-
334
- # Get the captured output
335
- stdout_output = stdout_buffer.getvalue()
336
- stderr_output = stderr_buffer.getvalue()
337
-
338
- # Special handling for project creation - always check if we can find success markers
339
- # in partial output even if script appears to have issues
340
- if has_project_create:
341
- # Look specifically for the file copy completion
342
- if "File copy VERIFIED - file exists at destination" in stdout_output or "DEBUG: File copy complete" in stdout_output:
343
- print("IMPORTANT: Detected successful file copy in project creation script")
344
- project_path = ""
345
-
346
- # Try to extract the project path from output
347
- project_path_match = None
348
- for line in stdout_output.split('\n'):
349
- if "DEBUG: Target Path =" in line:
350
- project_path_match = line.split('=')[1].strip()
351
- break
352
-
353
- if project_path_match:
354
- project_path = project_path_match
355
- print("IMPORTANT: Found project path: {0}".format(project_path))
356
-
357
- # Even if there was an error later (like opening), consider this a partial success
358
- if "Error creating project" in stdout_output and "currently in use" in stdout_output:
359
- print("IMPORTANT: Project file was created but couldn't be opened due to 'in use' error")
360
- # Combine success and error markers
361
- stdout_output += "\nSCRIPT_SUCCESS: Project file was created at {0} but could not be opened because it is in use by another CODESYS instance.".format(project_path)
362
- result["success"] = True
363
-
364
- # Check for success/error markers
365
- if SCRIPT_SUCCESS_MARKER in stdout_output or SCRIPT_SUCCESS_MARKER in stderr_output:
366
- result["success"] = True
367
- print("Found success marker in script output")
368
- elif SCRIPT_ERROR_MARKER in stdout_output or SCRIPT_ERROR_MARKER in stderr_output:
369
- result["success"] = False
370
- print("Found error marker in script output")
371
- else:
372
- # No explicit markers, assume success for non-critical operations
373
- # For critical operations like project creation, be more careful
374
- if has_project_create:
375
- if "File copy complete" in stdout_output:
376
- result["success"] = True
377
- print("Project creation seems successful based on file copy completion")
378
- else:
379
- result["success"] = False
380
- print("Project creation script lacks success markers and file copy confirmation")
381
- else:
382
- result["success"] = True
383
- print("No markers found in script output, assuming success")
384
-
385
- # Tally output length to check if output is reasonable
386
- print("Output length: stdout={0} chars, stderr={1} chars".format(
387
- len(stdout_output), len(stderr_output)))
388
-
389
- result["output"] = stdout_output
390
- if stderr_output:
391
- result["error"] = stderr_output
392
-
393
- sys.stdout.flush()
394
-
395
- except Exception as e:
396
- traceback.print_exc()
397
- sys.stdout.flush()
398
- result["success"] = False
399
- result["error"] = "Error executing script: {0}\n{1}".format(str(e), traceback.format_exc())
400
- result["output"] = "{0}: {1}".format(SCRIPT_ERROR_MARKER, str(e))
401
-
402
- elif cmd_type == "stop":
403
- # Stop command received
404
- result["success"] = True
405
- result["output"] = "{0}: Session stopping".format(SCRIPT_SUCCESS_MARKER)
406
-
407
- # Update session state to mark as stopping
408
- update_session_state("stopping")
409
-
410
- # Return the result and signal to stop the loop
411
- return result, True
412
-
413
- elif cmd_type == "ping":
414
- # Simple ping command to check if session is alive
415
- result["success"] = True
416
- result["output"] = "{0}: Session is active".format(SCRIPT_SUCCESS_MARKER)
417
-
418
- else:
419
- result["success"] = False
420
- result["error"] = "Unknown command type: {0}".format(cmd_type)
421
- result["output"] = "{0}: Unknown command type: {1}".format(SCRIPT_ERROR_MARKER, cmd_type)
422
-
423
- # Update session state after command execution
424
- update_session_state()
425
-
426
- return result, False
427
-
428
- except Exception as e:
429
- traceback.print_exc()
430
- return {
431
- "id": command_data.get("id", "unknown"),
432
- "success": False,
433
- "output": "{0}: Command execution failed".format(SCRIPT_ERROR_MARKER),
434
- "error": "{0}\n{1}".format(str(e), traceback.format_exc())
435
- }, False
436
-
437
- def check_command():
438
- """Check if there is a new command to execute"""
439
- try:
440
- # If command file doesn't exist, return immediately
441
- if not os.path.exists(COMMAND_FILE):
442
- print("Command file does not exist at {0}".format(COMMAND_FILE))
443
- return False
444
-
445
- # Read the command file
446
- try:
447
- print("Reading command file: {0}".format(COMMAND_FILE))
448
- with open(COMMAND_FILE, 'r') as f:
449
- file_content = f.read()
450
- print("Raw command file content: {0}".format(file_content[:100] if len(file_content) > 100 else file_content))
451
-
452
- # Check if file is empty
453
- if not file_content.strip():
454
- print("Command file is empty, resetting")
455
- command_data = {"status": "ready", "command": None, "timestamp": time.time()}
456
- with open(COMMAND_FILE, 'w') as fw:
457
- json.dump(command_data, fw, indent=2)
458
- return False
459
-
460
- # Try to parse JSON
461
- try:
462
- command_data = json.loads(file_content)
463
- except Exception as json_err:
464
- print("Error parsing command file JSON: {0}".format(json_err))
465
- # If command file is corrupted, reset it
466
- command_data = {"status": "ready", "command": None, "timestamp": time.time()}
467
- with open(COMMAND_FILE, 'w') as fw:
468
- json.dump(command_data, fw, indent=2)
469
- return False
470
- except Exception as read_err:
471
- print("Error reading command file: {0}".format(read_err))
472
- # Reset the command file
473
- command_data = {"status": "ready", "command": None, "timestamp": time.time()}
474
- try:
475
- with open(COMMAND_FILE, 'w') as fw:
476
- json.dump(command_data, fw, indent=2)
477
- except Exception as write_err:
478
- print("Error writing reset command file: {0}".format(write_err))
479
- return False
480
-
481
- # Check if there's a new command
482
- status = command_data.get("status")
483
- command = command_data.get("command")
484
-
485
- print("Command status: {0}".format(status))
486
- if command:
487
- print("Command found: {0}".format(command.get("type", "unknown")))
488
-
489
- if status == "pending" and command:
490
- print("Processing pending command...")
491
-
492
- try:
493
- # Mark as processing
494
- command_data["status"] = "processing"
495
- with open(COMMAND_FILE, 'w') as f:
496
- json.dump(command_data, f, indent=2)
497
- print("Updated command status to 'processing'")
498
-
499
- # Update session state to busy
500
- update_session_state("busy")
501
-
502
- # Execute the command
503
- print("Executing command: {0}".format(command.get("type", "unknown")))
504
- sys.stdout.flush() # Ensure output is flushed before potentially long-running operation
505
- result, stop_requested = execute_command(command)
506
- print("Command execution complete, stop_requested: {0}".format(stop_requested))
507
-
508
- # Write the result with extra safety measures
509
- result_data = {
510
- "status": "complete",
511
- "result": result,
512
- "timestamp": time.time()
513
- }
514
-
515
- print("Writing result to {0}".format(RESULT_FILE))
516
- sys.stdout.flush()
517
-
518
- # First, write to a temporary file to avoid corruption
519
- temp_result_file = RESULT_FILE + ".tmp"
520
- try:
521
- with open(temp_result_file, 'w') as f:
522
- json.dump(result_data, f, indent=2)
523
- print("Temporary result file written successfully")
524
-
525
- # If temporary write succeeded, move to the real file
526
- if os.path.exists(temp_result_file):
527
- # On Windows, we may need to remove the destination first
528
- if os.path.exists(RESULT_FILE):
529
- try:
530
- os.remove(RESULT_FILE)
531
- except Exception as rm_err:
532
- print("Warning: Could not remove existing result file: {0}".format(rm_err))
533
-
534
- # Now rename the temp file to the real file
535
- os.rename(temp_result_file, RESULT_FILE)
536
- print("Result file updated successfully")
537
- else:
538
- print("ERROR: Temporary result file not found after writing!")
539
- # Try direct write as fallback
540
- with open(RESULT_FILE, 'w') as f:
541
- json.dump(result_data, f, indent=2)
542
- except Exception as result_err:
543
- print("ERROR writing result file: {0}".format(result_err))
544
- traceback.print_exc()
545
- sys.stdout.flush()
546
-
547
- # Desperate attempt - try direct write with minimal content
548
- try:
549
- with open(RESULT_FILE, 'w') as f:
550
- f.write('{"status":"complete","result":{"id":"' +
551
- result["id"] + '","success":' +
552
- ('true' if result["success"] else 'false') +
553
- ',"output":"Result writing failed but operation completed"},"timestamp":' +
554
- str(time.time()) + '}')
555
- print("Emergency minimal result file written")
556
- except Exception as emergency_err:
557
- print("CRITICAL ERROR: Even emergency result write failed: {0}".format(emergency_err))
558
-
559
- sys.stdout.flush()
560
-
561
- # Reset command file
562
- command_data = {
563
- "status": "ready",
564
- "command": None,
565
- "timestamp": time.time()
566
- }
567
-
568
- print("Resetting command file")
569
- with open(COMMAND_FILE, 'w') as f:
570
- json.dump(command_data, f, indent=2)
571
-
572
- # Update session state to running again
573
- if not stop_requested:
574
- update_session_state("running")
575
-
576
- # Update last activity time
577
- update_last_activity()
578
-
579
- return stop_requested
580
- except Exception as cmd_err:
581
- print("Error processing command: {0}".format(cmd_err))
582
- traceback.print_exc()
583
- sys.stdout.flush() # Ensure error output is flushed
584
-
585
- # Try to reset the command file
586
- try:
587
- command_data = {
588
- "status": "ready",
589
- "command": None,
590
- "timestamp": time.time()
591
- }
592
- with open(COMMAND_FILE, 'w') as f:
593
- json.dump(command_data, f, indent=2)
594
- except Exception as reset_err:
595
- print("Error resetting command file: {0}".format(reset_err))
596
-
597
- # Try to restore session state
598
- try:
599
- update_session_state("running")
600
- except Exception as state_err:
601
- print("Error updating session state: {0}".format(state_err))
602
-
603
- return False
604
-
605
- return False
606
- except Exception as e:
607
- print("Error in check_command: {0}".format(e))
608
- traceback.print_exc()
609
- sys.stdout.flush() # Ensure error output is flushed
610
- return False
611
-
612
- def update_last_activity():
613
- """Update the last activity timestamp in the session state"""
614
- try:
615
- if os.path.exists(SESSION_STATE_FILE):
616
- with open(SESSION_STATE_FILE, 'r') as f:
617
- state = json.load(f)
618
-
619
- state["last_activity"] = time.time()
620
-
621
- with open(SESSION_STATE_FILE, 'w') as f:
622
- json.dump(state, f, indent=2)
623
- except Exception as e:
624
- print("Error updating last activity: {0}".format(e))
625
-
626
- def check_inactivity_timeout():
627
- """Check if session has been inactive for too long"""
628
- try:
629
- if os.path.exists(SESSION_STATE_FILE):
630
- with open(SESSION_STATE_FILE, 'r') as f:
631
- state = json.load(f)
632
-
633
- last_activity = state.get("last_activity", state.get("start_time", time.time()))
634
- current_time = time.time()
635
-
636
- if (current_time - last_activity) > MAX_INACTIVITY_TIME:
637
- print("Session inactive for {0:.1f} seconds, shutting down".format(current_time - last_activity))
638
- return True
639
-
640
- return False
641
- except Exception as e:
642
- print("Error checking inactivity timeout: {0}".format(e))
643
- return False
644
-
645
- def command_loop():
646
- """Main loop to check for and execute commands"""
647
- stop_requested = False
648
- last_state_update = time.time()
649
- loop_count = 0
650
- last_heartbeat = time.time()
651
- start_time = time.time()
652
- error_count = 0 # Track consecutive errors
653
-
654
- # Maximum session runtime to prevent infinite hanging (8 hours)
655
- max_session_runtime = 60 * 60 * 8
656
-
657
- print("Starting command loop...")
658
- print("Command file: {0}".format(COMMAND_FILE))
659
- print("Result file: {0}".format(RESULT_FILE))
660
- print("Session lock file: {0}".format(SESSION_LOCK_FILE))
661
- print("Session state file: {0}".format(SESSION_STATE_FILE))
662
- print("Poll interval: {0} seconds".format(POLL_INTERVAL))
663
- print("Max inactivity time: {0} seconds".format(MAX_INACTIVITY_TIME))
664
- print("Max session runtime: {0} hours".format(max_session_runtime / 3600))
665
- sys.stdout.flush() # Ensure initialization output is visible immediately
666
-
667
- # Set up initial session state files
668
- try:
669
- # Create initial command file if it doesn't exist
670
- if not os.path.exists(COMMAND_FILE):
671
- with open(COMMAND_FILE, 'w') as f:
672
- json.dump({
673
- "status": "ready",
674
- "command": None,
675
- "timestamp": time.time()
676
- }, f, indent=2)
677
- print("Created initial command file")
678
-
679
- # Create initial result file if it doesn't exist
680
- if not os.path.exists(RESULT_FILE):
681
- with open(RESULT_FILE, 'w') as f:
682
- json.dump({
683
- "status": "none",
684
- "result": None,
685
- "timestamp": time.time()
686
- }, f, indent=2)
687
- print("Created initial result file")
688
- sys.stdout.flush()
689
- except Exception as setup_err:
690
- print("Error setting up initial files: {0}".format(setup_err))
691
- traceback.print_exc()
692
- sys.stdout.flush()
693
-
694
- print("Entering main command loop...")
695
- sys.stdout.flush()
696
-
697
- while not stop_requested:
698
- try:
699
- loop_count += 1
700
- current_time = time.time()
701
-
702
- # Check for absolute maximum session runtime to prevent infinite hanging
703
- session_runtime = current_time - start_time
704
- if session_runtime > max_session_runtime:
705
- print("SAFETY: Maximum session run time of {0} hours reached".format(max_session_runtime / 3600))
706
- print("SAFETY: This is to prevent CODESYS from hanging indefinitely")
707
- print("SAFETY: Session will now terminate")
708
- update_session_state("timeout")
709
- sys.stdout.flush()
710
- stop_requested = True
711
- break
712
-
713
- # Update session lock file to indicate we're alive
714
- with open(SESSION_LOCK_FILE, 'w') as f:
715
- f.write(str(current_time))
716
-
717
- # Print heartbeat message every 60 seconds
718
- if current_time - last_heartbeat > 60:
719
- print("Command loop heartbeat - still alive (iteration {0})".format(loop_count))
720
- print("Session running for {0:.1f} minutes".format(session_runtime / 60))
721
- last_heartbeat = current_time
722
- sys.stdout.flush()
723
-
724
- # Check for new commands
725
- stop_requested = check_command()
726
- if stop_requested:
727
- print("Stop was requested by a command")
728
- sys.stdout.flush()
729
-
730
- # Check for inactivity timeout
731
- if check_inactivity_timeout():
732
- print("Session timeout due to inactivity")
733
- update_session_state("timeout")
734
- stop_requested = True
735
- sys.stdout.flush()
736
-
737
- # Periodically update session state (every 30 seconds)
738
- if (current_time - last_state_update) > 30:
739
- print("Periodic session state update")
740
- update_session_state()
741
- last_state_update = current_time
742
- sys.stdout.flush()
743
-
744
- # Reset error count on successful iteration
745
- error_count = 0
746
-
747
- # Sleep for a bit
748
- time.sleep(POLL_INTERVAL)
749
-
750
- except Exception as e:
751
- print("Error in command loop: {0}".format(e))
752
- traceback.print_exc()
753
- sys.stdout.flush()
754
-
755
- # Increment error count
756
- error_count += 1
757
-
758
- # If we're getting repeated errors, consider breaking the loop
759
- if error_count > 10:
760
- print("SAFETY: Too many consecutive errors ({0}), stopping session".format(error_count))
761
- update_session_state("error")
762
- sys.stdout.flush()
763
- stop_requested = True
764
- else:
765
- # Sleep for a bit before retrying
766
- time.sleep(POLL_INTERVAL)
767
-
768
- print("Command loop stopped after {0} iterations".format(loop_count))
769
- sys.stdout.flush()
770
-
771
- # Main script
772
- def main():
773
- try:
774
- print("Starting CODESYS automation session...")
775
- sys.stdout.flush()
776
-
777
- # Setup session directory and files
778
- print("Setting up session directory...")
779
- sys.stdout.flush()
780
- if not setup_session_dir():
781
- print("Failed to set up session directory")
782
- sys.stdout.flush()
783
- sys.exit(1)
784
-
785
- # Update initial session state
786
- print("Updating initial session state...")
787
- sys.stdout.flush()
788
- update_session_state()
789
-
790
- # Start command loop
791
- print("{0}: CODESYS session started".format(SCRIPT_SUCCESS_MARKER))
792
- sys.stdout.flush()
793
- command_loop()
794
-
795
- # Update final state
796
- update_session_state("stopped")
797
-
798
- print("{0}: CODESYS session stopped cleanly".format(SCRIPT_SUCCESS_MARKER))
799
- sys.stdout.flush()
800
- sys.exit(0)
801
- except Exception as e:
802
- print("Error in session: {0}".format(e))
803
- print("{0}: Session failed - {1}".format(SCRIPT_ERROR_MARKER, str(e)))
804
- traceback.print_exc()
805
- sys.stdout.flush()
806
- sys.exit(1)
807
-
808
- if __name__ == "__main__":
809
- main()