@codesys/mcp-toolkit 1.5.8 → 2.0.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.
- package/CHANGELOG.md +153 -189
- package/LICENSE +21 -21
- package/README.md +269 -152
- package/dist/actions.d.ts +28 -0
- package/dist/actions.js +98 -0
- package/dist/actions.js.map +1 -0
- package/dist/bridge/bridge.d.ts +105 -0
- package/dist/bridge/bridge.js +400 -0
- package/dist/bridge/bridge.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +196 -0
- package/dist/cli.js.map +1 -0
- package/dist/codesys/cache.d.ts +48 -0
- package/dist/codesys/cache.js +82 -0
- package/dist/codesys/cache.js.map +1 -0
- package/dist/codesys/changes.d.ts +57 -0
- package/dist/codesys/changes.js +133 -0
- package/dist/codesys/changes.js.map +1 -0
- package/dist/codesys/decl.d.ts +45 -0
- package/dist/codesys/decl.js +251 -0
- package/dist/codesys/decl.js.map +1 -0
- package/dist/codesys/devices.d.ts +34 -0
- package/dist/codesys/devices.js +82 -0
- package/dist/codesys/devices.js.map +1 -0
- package/dist/codesys/diff.d.ts +13 -0
- package/dist/codesys/diff.js +61 -0
- package/dist/codesys/diff.js.map +1 -0
- package/dist/codesys/install.d.ts +40 -0
- package/dist/codesys/install.js +144 -0
- package/dist/codesys/install.js.map +1 -0
- package/dist/codesys/library-browse.d.ts +19 -0
- package/dist/codesys/library-browse.js +57 -0
- package/dist/codesys/library-browse.js.map +1 -0
- package/dist/codesys/outline.d.ts +32 -0
- package/dist/codesys/outline.js +219 -0
- package/dist/codesys/outline.js.map +1 -0
- package/dist/codesys/simulation.d.ts +34 -0
- package/dist/codesys/simulation.js +57 -0
- package/dist/codesys/simulation.js.map +1 -0
- package/dist/codesys/st.d.ts +36 -0
- package/dist/codesys/st.js +69 -0
- package/dist/codesys/st.js.map +1 -0
- package/dist/daemon.d.ts +46 -0
- package/dist/daemon.js +173 -0
- package/dist/daemon.js.map +1 -0
- package/dist/driver.d.ts +25 -0
- package/dist/driver.js +33 -0
- package/dist/driver.js.map +1 -0
- package/dist/panel/sim-panel.html +357 -0
- package/dist/panel/viewer.html +126 -0
- package/dist/panel-types.d.ts +20 -0
- package/dist/panel-types.js +3 -0
- package/dist/panel-types.js.map +1 -0
- package/dist/panel.d.ts +27 -0
- package/dist/panel.js +208 -0
- package/dist/panel.js.map +1 -0
- package/dist/server.d.ts +5 -0
- package/dist/server.js +33 -974
- package/dist/server.js.map +1 -0
- package/dist/tools.d.ts +103 -0
- package/dist/tools.js +793 -0
- package/dist/tools.js.map +1 -0
- package/dist/viewer.d.ts +14 -0
- package/dist/viewer.js +125 -0
- package/dist/viewer.js.map +1 -0
- package/package.json +66 -51
- package/watcher/mcp_compat.py +147 -0
- package/watcher/mcp_ops.py +1178 -0
- package/watcher/mcp_sim.py +246 -0
- package/watcher/watcher.py +255 -0
- package/.claude/settings.local.json +0 -29
- package/CLAUDE.md +0 -26
- package/CONTRIBUTING.md +0 -72
- package/README.md.enhanced +0 -155
- package/codesys-mcp-toolkit-1.2.0.tgz +0 -0
- package/codesys-mcp-toolkit-1.2.1.tgz +0 -0
- package/codesys-mcp-toolkit-1.5.7.tgz +0 -0
- package/codesys-mcp-toolkit-1.5.8.tgz +0 -0
- package/dist/bin.js +0 -75
- package/dist/check_session_paths.js +0 -149
- package/dist/codesys_interop.js +0 -435
- package/dist/direct_session_command.js +0 -293
- package/dist/scriptLoader.js +0 -165
- package/dist/scripts/check_status.py +0 -21
- package/dist/scripts/compile_project.py +0 -65
- package/dist/scripts/create_method.py +0 -80
- package/dist/scripts/create_pou.py +0 -102
- package/dist/scripts/create_project.py +0 -116
- package/dist/scripts/create_property.py +0 -78
- package/dist/scripts/ensure_project_open.py +0 -154
- package/dist/scripts/find_object_by_path.py +0 -111
- package/dist/scripts/get_pou_code.py +0 -91
- package/dist/scripts/get_project_structure.py +0 -72
- package/dist/scripts/open_project.py +0 -25
- package/dist/scripts/save_project.py +0 -29
- package/dist/scripts/session_status.py +0 -281
- package/dist/scripts/set_pou_code.py +0 -109
- package/dist/scripts/start_session.py +0 -809
- package/dist/scripts/stop_session.py +0 -324
- package/dist/sessionManager.js +0 -798
- package/docs/configuration.md +0 -141
- package/docs/index.md +0 -26
- package/docs/installation.md +0 -84
- package/docs/session_management.md +0 -110
- package/examples/README.md +0 -78
- package/examples/sample_config.json +0 -32
- package/replacements_session_status.txt +0 -16
- package/scriptDoc.txt +0 -8679
- package/scripts/build.js +0 -25
- 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()
|