@codesys/mcp-toolkit 1.2.1 → 1.3.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 +16 -0
- package/dist/codesys_interop.js +143 -2
- package/dist/scriptLoader.js +14 -2
- package/dist/scripts/create_project.py +51 -5
- package/dist/scripts/session_status.py +132 -0
- package/dist/scripts/start_session.py +278 -0
- package/dist/scripts/stop_session.py +117 -0
- package/dist/sessionManager.js +404 -117
- package/docs/session_management.md +6 -1
- package/package.json +1 -1
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import sys, os, json, time, tempfile
|
|
2
|
+
import scriptengine as script_engine
|
|
3
|
+
|
|
4
|
+
# Define markers for script results
|
|
5
|
+
SCRIPT_SUCCESS_MARKER = "SCRIPT_SUCCESS"
|
|
6
|
+
SCRIPT_ERROR_MARKER = "SCRIPT_ERROR"
|
|
7
|
+
|
|
8
|
+
# Session state and command files
|
|
9
|
+
SESSION_DIR = os.path.join(tempfile.gettempdir(), "codesys_session")
|
|
10
|
+
SESSION_STATE_FILE = os.path.join(SESSION_DIR, "session_state.json")
|
|
11
|
+
COMMAND_FILE = os.path.join(SESSION_DIR, "command.json")
|
|
12
|
+
RESULT_FILE = os.path.join(SESSION_DIR, "result.json")
|
|
13
|
+
SESSION_LOCK_FILE = os.path.join(SESSION_DIR, "session.lock")
|
|
14
|
+
POLL_INTERVAL = 0.5 # seconds
|
|
15
|
+
|
|
16
|
+
def setup_session_dir():
|
|
17
|
+
"""Create the session directory and initial files"""
|
|
18
|
+
try:
|
|
19
|
+
# Create session directory
|
|
20
|
+
if not os.path.exists(SESSION_DIR):
|
|
21
|
+
os.makedirs(SESSION_DIR)
|
|
22
|
+
|
|
23
|
+
# Initialize session state
|
|
24
|
+
session_state = {
|
|
25
|
+
"active": True,
|
|
26
|
+
"session_id": str(time.time()),
|
|
27
|
+
"start_time": time.time(),
|
|
28
|
+
"status": "running",
|
|
29
|
+
"project_open": False,
|
|
30
|
+
"project_name": "",
|
|
31
|
+
"project_path": ""
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# Write session state
|
|
35
|
+
with open(SESSION_STATE_FILE, 'w') as f:
|
|
36
|
+
json.dump(session_state, f)
|
|
37
|
+
|
|
38
|
+
# Create empty command file
|
|
39
|
+
with open(COMMAND_FILE, 'w') as f:
|
|
40
|
+
json.dump({"status": "ready", "command": None, "timestamp": time.time()}, f)
|
|
41
|
+
|
|
42
|
+
# Create empty result file
|
|
43
|
+
with open(RESULT_FILE, 'w') as f:
|
|
44
|
+
json.dump({"status": "none", "result": None, "timestamp": time.time()}, f)
|
|
45
|
+
|
|
46
|
+
# Create lock file
|
|
47
|
+
with open(SESSION_LOCK_FILE, 'w') as f:
|
|
48
|
+
f.write(str(time.time()))
|
|
49
|
+
|
|
50
|
+
print(f"Session directory setup at: {SESSION_DIR}")
|
|
51
|
+
return True
|
|
52
|
+
except Exception as e:
|
|
53
|
+
print(f"Failed to set up session directory: {e}")
|
|
54
|
+
print(f"{SCRIPT_ERROR_MARKER}: Session setup failed - {str(e)}")
|
|
55
|
+
return False
|
|
56
|
+
|
|
57
|
+
def update_session_state(status="running"):
|
|
58
|
+
"""Update the session state file with current CODESYS state"""
|
|
59
|
+
try:
|
|
60
|
+
# Get current state
|
|
61
|
+
project_open = False
|
|
62
|
+
project_name = "No project open"
|
|
63
|
+
project_path = "N/A"
|
|
64
|
+
|
|
65
|
+
primary_project = script_engine.projects.primary
|
|
66
|
+
if primary_project:
|
|
67
|
+
project_open = True
|
|
68
|
+
try:
|
|
69
|
+
project_path = os.path.normcase(os.path.abspath(primary_project.path))
|
|
70
|
+
try:
|
|
71
|
+
project_name = primary_project.get_name()
|
|
72
|
+
if not project_name:
|
|
73
|
+
project_name = f"Unnamed (path: {os.path.basename(project_path)})"
|
|
74
|
+
except:
|
|
75
|
+
project_name = f"Unnamed (path: {os.path.basename(project_path)})"
|
|
76
|
+
except Exception as e_path:
|
|
77
|
+
project_path = f"N/A (Error: {e_path})"
|
|
78
|
+
project_name = "Unnamed (Path Error)"
|
|
79
|
+
|
|
80
|
+
# Read current state file
|
|
81
|
+
with open(SESSION_STATE_FILE, 'r') as f:
|
|
82
|
+
session_state = json.load(f)
|
|
83
|
+
|
|
84
|
+
# Update state
|
|
85
|
+
session_state["project_open"] = project_open
|
|
86
|
+
session_state["project_name"] = project_name
|
|
87
|
+
session_state["project_path"] = project_path
|
|
88
|
+
session_state["last_update"] = time.time()
|
|
89
|
+
session_state["status"] = status
|
|
90
|
+
|
|
91
|
+
# Write updated state
|
|
92
|
+
with open(SESSION_STATE_FILE, 'w') as f:
|
|
93
|
+
json.dump(session_state, f)
|
|
94
|
+
|
|
95
|
+
return True
|
|
96
|
+
except Exception as e:
|
|
97
|
+
print(f"Failed to update session state: {e}")
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
def execute_command(command_data):
|
|
101
|
+
"""Execute a command"""
|
|
102
|
+
try:
|
|
103
|
+
cmd_type = command_data.get("type", "execute_script")
|
|
104
|
+
script = command_data.get("script", "")
|
|
105
|
+
cmd_id = command_data.get("id", "unknown")
|
|
106
|
+
|
|
107
|
+
result = {
|
|
108
|
+
"id": cmd_id,
|
|
109
|
+
"success": False,
|
|
110
|
+
"output": "",
|
|
111
|
+
"error": ""
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if cmd_type == "execute_script" and script:
|
|
115
|
+
# Execute the script in the current CODESYS context
|
|
116
|
+
try:
|
|
117
|
+
# Capture output by redirecting stdout/stderr
|
|
118
|
+
import io
|
|
119
|
+
from contextlib import redirect_stdout, redirect_stderr
|
|
120
|
+
|
|
121
|
+
stdout_buffer = io.StringIO()
|
|
122
|
+
stderr_buffer = io.StringIO()
|
|
123
|
+
|
|
124
|
+
with redirect_stdout(stdout_buffer), redirect_stderr(stderr_buffer):
|
|
125
|
+
# Execute the script
|
|
126
|
+
exec(script, globals())
|
|
127
|
+
|
|
128
|
+
# Get the captured output
|
|
129
|
+
stdout_output = stdout_buffer.getvalue()
|
|
130
|
+
stderr_output = stderr_buffer.getvalue()
|
|
131
|
+
|
|
132
|
+
# Check for success/error markers
|
|
133
|
+
if SCRIPT_SUCCESS_MARKER in stdout_output or SCRIPT_SUCCESS_MARKER in stderr_output:
|
|
134
|
+
result["success"] = True
|
|
135
|
+
elif SCRIPT_ERROR_MARKER in stdout_output or SCRIPT_ERROR_MARKER in stderr_output:
|
|
136
|
+
result["success"] = False
|
|
137
|
+
else:
|
|
138
|
+
# No explicit markers, assume success
|
|
139
|
+
result["success"] = True
|
|
140
|
+
|
|
141
|
+
result["output"] = stdout_output
|
|
142
|
+
if stderr_output:
|
|
143
|
+
result["error"] = stderr_output
|
|
144
|
+
|
|
145
|
+
except Exception as e:
|
|
146
|
+
result["success"] = False
|
|
147
|
+
result["error"] = f"Error executing script: {str(e)}"
|
|
148
|
+
result["output"] = f"{SCRIPT_ERROR_MARKER}: {str(e)}"
|
|
149
|
+
|
|
150
|
+
elif cmd_type == "stop":
|
|
151
|
+
# Stop command received
|
|
152
|
+
result["success"] = True
|
|
153
|
+
result["output"] = f"{SCRIPT_SUCCESS_MARKER}: Session stopping"
|
|
154
|
+
|
|
155
|
+
# Update session state to mark as stopping
|
|
156
|
+
update_session_state("stopping")
|
|
157
|
+
|
|
158
|
+
# Return the result and signal to stop the loop
|
|
159
|
+
return result, True
|
|
160
|
+
|
|
161
|
+
else:
|
|
162
|
+
result["success"] = False
|
|
163
|
+
result["error"] = f"Unknown command type: {cmd_type}"
|
|
164
|
+
result["output"] = f"{SCRIPT_ERROR_MARKER}: Unknown command type: {cmd_type}"
|
|
165
|
+
|
|
166
|
+
# Update session state after command execution
|
|
167
|
+
update_session_state()
|
|
168
|
+
|
|
169
|
+
return result, False
|
|
170
|
+
|
|
171
|
+
except Exception as e:
|
|
172
|
+
return {
|
|
173
|
+
"id": command_data.get("id", "unknown"),
|
|
174
|
+
"success": False,
|
|
175
|
+
"output": f"{SCRIPT_ERROR_MARKER}: Command execution failed",
|
|
176
|
+
"error": str(e)
|
|
177
|
+
}, False
|
|
178
|
+
|
|
179
|
+
def check_command():
|
|
180
|
+
"""Check if there is a new command to execute"""
|
|
181
|
+
try:
|
|
182
|
+
# Read the command file
|
|
183
|
+
with open(COMMAND_FILE, 'r') as f:
|
|
184
|
+
command_data = json.load(f)
|
|
185
|
+
|
|
186
|
+
# Check if there's a new command
|
|
187
|
+
if command_data.get("status") == "pending" and command_data.get("command"):
|
|
188
|
+
# Extract the command
|
|
189
|
+
command = command_data.get("command")
|
|
190
|
+
|
|
191
|
+
# Mark as processing
|
|
192
|
+
command_data["status"] = "processing"
|
|
193
|
+
with open(COMMAND_FILE, 'w') as f:
|
|
194
|
+
json.dump(command_data, f)
|
|
195
|
+
|
|
196
|
+
# Execute the command
|
|
197
|
+
result, stop_requested = execute_command(command)
|
|
198
|
+
|
|
199
|
+
# Write the result
|
|
200
|
+
result_data = {
|
|
201
|
+
"status": "complete",
|
|
202
|
+
"result": result,
|
|
203
|
+
"timestamp": time.time()
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
with open(RESULT_FILE, 'w') as f:
|
|
207
|
+
json.dump(result_data, f)
|
|
208
|
+
|
|
209
|
+
# Reset command file
|
|
210
|
+
command_data = {
|
|
211
|
+
"status": "ready",
|
|
212
|
+
"command": None,
|
|
213
|
+
"timestamp": time.time()
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
with open(COMMAND_FILE, 'w') as f:
|
|
217
|
+
json.dump(command_data, f)
|
|
218
|
+
|
|
219
|
+
return stop_requested
|
|
220
|
+
|
|
221
|
+
return False
|
|
222
|
+
except Exception as e:
|
|
223
|
+
print(f"Error checking command: {e}")
|
|
224
|
+
return False
|
|
225
|
+
|
|
226
|
+
def command_loop():
|
|
227
|
+
"""Main loop to check for and execute commands"""
|
|
228
|
+
stop_requested = False
|
|
229
|
+
|
|
230
|
+
print(f"Starting command loop...")
|
|
231
|
+
print(f"Command file: {COMMAND_FILE}")
|
|
232
|
+
print(f"Result file: {RESULT_FILE}")
|
|
233
|
+
|
|
234
|
+
while not stop_requested:
|
|
235
|
+
try:
|
|
236
|
+
# Update session lock file to indicate we're alive
|
|
237
|
+
with open(SESSION_LOCK_FILE, 'w') as f:
|
|
238
|
+
f.write(str(time.time()))
|
|
239
|
+
|
|
240
|
+
# Check for new commands
|
|
241
|
+
stop_requested = check_command()
|
|
242
|
+
|
|
243
|
+
# Sleep for a bit
|
|
244
|
+
time.sleep(POLL_INTERVAL)
|
|
245
|
+
|
|
246
|
+
except Exception as e:
|
|
247
|
+
print(f"Error in command loop: {e}")
|
|
248
|
+
# Sleep for a bit
|
|
249
|
+
time.sleep(POLL_INTERVAL)
|
|
250
|
+
|
|
251
|
+
print("Command loop stopped.")
|
|
252
|
+
|
|
253
|
+
# Main script
|
|
254
|
+
def main():
|
|
255
|
+
try:
|
|
256
|
+
# Setup session directory and files
|
|
257
|
+
if not setup_session_dir():
|
|
258
|
+
sys.exit(1)
|
|
259
|
+
|
|
260
|
+
# Update initial session state
|
|
261
|
+
update_session_state()
|
|
262
|
+
|
|
263
|
+
# Start command loop
|
|
264
|
+
print(f"{SCRIPT_SUCCESS_MARKER}: CODESYS session started")
|
|
265
|
+
command_loop()
|
|
266
|
+
|
|
267
|
+
# Update final state
|
|
268
|
+
update_session_state("stopped")
|
|
269
|
+
|
|
270
|
+
print(f"{SCRIPT_SUCCESS_MARKER}: CODESYS session stopped cleanly")
|
|
271
|
+
sys.exit(0)
|
|
272
|
+
except Exception as e:
|
|
273
|
+
print(f"Error in session: {e}")
|
|
274
|
+
print(f"{SCRIPT_ERROR_MARKER}: Session failed - {str(e)}")
|
|
275
|
+
sys.exit(1)
|
|
276
|
+
|
|
277
|
+
if __name__ == "__main__":
|
|
278
|
+
main()
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import sys, os, json, time
|
|
2
|
+
import tempfile
|
|
3
|
+
|
|
4
|
+
# Define markers for script results
|
|
5
|
+
SCRIPT_SUCCESS_MARKER = "SCRIPT_SUCCESS"
|
|
6
|
+
SCRIPT_ERROR_MARKER = "SCRIPT_ERROR"
|
|
7
|
+
|
|
8
|
+
# Session state and command files
|
|
9
|
+
SESSION_DIR = os.path.join(tempfile.gettempdir(), "codesys_session")
|
|
10
|
+
SESSION_STATE_FILE = os.path.join(SESSION_DIR, "session_state.json")
|
|
11
|
+
COMMAND_FILE = os.path.join(SESSION_DIR, "command.json")
|
|
12
|
+
RESULT_FILE = os.path.join(SESSION_DIR, "result.json")
|
|
13
|
+
SESSION_LOCK_FILE = os.path.join(SESSION_DIR, "session.lock")
|
|
14
|
+
|
|
15
|
+
def stop_session():
|
|
16
|
+
"""Stop a running CODESYS session"""
|
|
17
|
+
try:
|
|
18
|
+
# Check if session directory exists
|
|
19
|
+
if not os.path.exists(SESSION_DIR):
|
|
20
|
+
print(f"{SCRIPT_ERROR_MARKER}: No session directory found at {SESSION_DIR}")
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
# Check if session state file exists
|
|
24
|
+
if not os.path.exists(SESSION_STATE_FILE):
|
|
25
|
+
print(f"{SCRIPT_ERROR_MARKER}: No session state file found at {SESSION_STATE_FILE}")
|
|
26
|
+
return False
|
|
27
|
+
|
|
28
|
+
# Read session state
|
|
29
|
+
with open(SESSION_STATE_FILE, 'r') as f:
|
|
30
|
+
try:
|
|
31
|
+
session_state = json.load(f)
|
|
32
|
+
except json.JSONDecodeError:
|
|
33
|
+
print(f"{SCRIPT_ERROR_MARKER}: Invalid session state file")
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
# Check if session is active
|
|
37
|
+
if not session_state.get('active', False):
|
|
38
|
+
print(f"{SCRIPT_SUCCESS_MARKER}: Session is already stopped")
|
|
39
|
+
return True
|
|
40
|
+
|
|
41
|
+
# Check if command file exists
|
|
42
|
+
if not os.path.exists(COMMAND_FILE):
|
|
43
|
+
print(f"{SCRIPT_ERROR_MARKER}: Command file not found at {COMMAND_FILE}")
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
# Create stop command
|
|
47
|
+
stop_command = {
|
|
48
|
+
"id": f"stop_{int(time.time())}",
|
|
49
|
+
"type": "stop",
|
|
50
|
+
"timestamp": time.time()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# Write command to file
|
|
54
|
+
command_data = {
|
|
55
|
+
"status": "pending",
|
|
56
|
+
"command": stop_command,
|
|
57
|
+
"timestamp": time.time()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
with open(COMMAND_FILE, 'w') as f:
|
|
61
|
+
json.dump(command_data, f)
|
|
62
|
+
|
|
63
|
+
print(f"Stop command sent to session")
|
|
64
|
+
|
|
65
|
+
# Wait for result (with timeout)
|
|
66
|
+
timeout = 10 # seconds
|
|
67
|
+
start_time = time.time()
|
|
68
|
+
result_received = False
|
|
69
|
+
|
|
70
|
+
while not result_received and (time.time() - start_time) < timeout:
|
|
71
|
+
# Check if result file exists
|
|
72
|
+
if not os.path.exists(RESULT_FILE):
|
|
73
|
+
time.sleep(0.5)
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
with open(RESULT_FILE, 'r') as f:
|
|
78
|
+
result_data = json.load(f)
|
|
79
|
+
|
|
80
|
+
if result_data.get("status") == "complete" and result_data.get("result"):
|
|
81
|
+
result = result_data.get("result")
|
|
82
|
+
if result.get('id') == stop_command['id']:
|
|
83
|
+
result_received = True
|
|
84
|
+
print(f"Session stop result: {result.get('output', 'No output')}")
|
|
85
|
+
break
|
|
86
|
+
except Exception as e:
|
|
87
|
+
print(f"Error reading result: {e}")
|
|
88
|
+
time.sleep(0.5)
|
|
89
|
+
|
|
90
|
+
# Update session state to indicate it's stopped
|
|
91
|
+
with open(SESSION_STATE_FILE, 'r') as f:
|
|
92
|
+
session_state = json.load(f)
|
|
93
|
+
|
|
94
|
+
session_state['active'] = False
|
|
95
|
+
session_state['status'] = "stopped"
|
|
96
|
+
session_state['end_time'] = time.time()
|
|
97
|
+
|
|
98
|
+
with open(SESSION_STATE_FILE, 'w') as f:
|
|
99
|
+
json.dump(session_state, f)
|
|
100
|
+
|
|
101
|
+
if result_received:
|
|
102
|
+
print(f"{SCRIPT_SUCCESS_MARKER}: Session stopped successfully")
|
|
103
|
+
else:
|
|
104
|
+
print(f"{SCRIPT_ERROR_MARKER}: Timeout waiting for session to stop")
|
|
105
|
+
|
|
106
|
+
return result_received
|
|
107
|
+
|
|
108
|
+
except Exception as e:
|
|
109
|
+
print(f"{SCRIPT_ERROR_MARKER}: Error stopping session - {str(e)}")
|
|
110
|
+
return False
|
|
111
|
+
|
|
112
|
+
# Main script
|
|
113
|
+
if __name__ == "__main__":
|
|
114
|
+
if stop_session():
|
|
115
|
+
sys.exit(0)
|
|
116
|
+
else:
|
|
117
|
+
sys.exit(1)
|