@codesys/mcp-toolkit 1.2.1 → 1.2.4
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 +168 -0
- package/dist/scripts/start_session.py +240 -0
- package/dist/scripts/stop_session.py +94 -0
- package/dist/sessionManager.js +399 -117
- package/docs/session_management.md +6 -1
- package/package.json +1 -1
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import sys, os, threading, 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 file and pipes
|
|
9
|
+
SESSION_DIR = os.path.join(tempfile.gettempdir(), "codesys_session")
|
|
10
|
+
SESSION_STATE_FILE = os.path.join(SESSION_DIR, "session_state.json")
|
|
11
|
+
COMMAND_PIPE = os.path.join(SESSION_DIR, "command_pipe")
|
|
12
|
+
RESULT_PIPE = os.path.join(SESSION_DIR, "result_pipe")
|
|
13
|
+
|
|
14
|
+
def setup_session_dir():
|
|
15
|
+
"""Create the session directory and pipes if they don't exist"""
|
|
16
|
+
try:
|
|
17
|
+
# Create session directory
|
|
18
|
+
if not os.path.exists(SESSION_DIR):
|
|
19
|
+
os.makedirs(SESSION_DIR)
|
|
20
|
+
|
|
21
|
+
# Create named pipes if they don't exist
|
|
22
|
+
if not os.path.exists(COMMAND_PIPE):
|
|
23
|
+
os.mkfifo(COMMAND_PIPE)
|
|
24
|
+
|
|
25
|
+
if not os.path.exists(RESULT_PIPE):
|
|
26
|
+
os.mkfifo(RESULT_PIPE)
|
|
27
|
+
|
|
28
|
+
# Initialize session state
|
|
29
|
+
session_state = {
|
|
30
|
+
"active": True,
|
|
31
|
+
"start_time": time.time(),
|
|
32
|
+
"project_open": False,
|
|
33
|
+
"project_name": "",
|
|
34
|
+
"project_path": ""
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
with open(SESSION_STATE_FILE, 'w') as f:
|
|
38
|
+
json.dump(session_state, f)
|
|
39
|
+
|
|
40
|
+
return True
|
|
41
|
+
except Exception as e:
|
|
42
|
+
print(f"Failed to set up session directory: {e}")
|
|
43
|
+
print(f"{SCRIPT_ERROR_MARKER}: Session setup failed - {str(e)}")
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
def update_session_state():
|
|
47
|
+
"""Update the session state file with current CODESYS state"""
|
|
48
|
+
try:
|
|
49
|
+
# Get current state
|
|
50
|
+
project_open = False
|
|
51
|
+
project_name = "No project open"
|
|
52
|
+
project_path = "N/A"
|
|
53
|
+
|
|
54
|
+
primary_project = script_engine.projects.primary
|
|
55
|
+
if primary_project:
|
|
56
|
+
project_open = True
|
|
57
|
+
try:
|
|
58
|
+
project_path = os.path.normcase(os.path.abspath(primary_project.path))
|
|
59
|
+
try:
|
|
60
|
+
project_name = primary_project.get_name()
|
|
61
|
+
if not project_name:
|
|
62
|
+
project_name = f"Unnamed (path: {os.path.basename(project_path)})"
|
|
63
|
+
except:
|
|
64
|
+
project_name = f"Unnamed (path: {os.path.basename(project_path)})"
|
|
65
|
+
except Exception as e_path:
|
|
66
|
+
project_path = f"N/A (Error: {e_path})"
|
|
67
|
+
project_name = "Unnamed (Path Error)"
|
|
68
|
+
|
|
69
|
+
# Read current state file
|
|
70
|
+
with open(SESSION_STATE_FILE, 'r') as f:
|
|
71
|
+
session_state = json.load(f)
|
|
72
|
+
|
|
73
|
+
# Update state
|
|
74
|
+
session_state["project_open"] = project_open
|
|
75
|
+
session_state["project_name"] = project_name
|
|
76
|
+
session_state["project_path"] = project_path
|
|
77
|
+
session_state["last_update"] = time.time()
|
|
78
|
+
|
|
79
|
+
# Write updated state
|
|
80
|
+
with open(SESSION_STATE_FILE, 'w') as f:
|
|
81
|
+
json.dump(session_state, f)
|
|
82
|
+
|
|
83
|
+
return True
|
|
84
|
+
except Exception as e:
|
|
85
|
+
print(f"Failed to update session state: {e}")
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
def execute_command(command_data):
|
|
89
|
+
"""Execute a command from the command pipe"""
|
|
90
|
+
try:
|
|
91
|
+
cmd_type = command_data.get("type", "")
|
|
92
|
+
script = command_data.get("script", "")
|
|
93
|
+
cmd_id = command_data.get("id", "unknown")
|
|
94
|
+
|
|
95
|
+
result = {
|
|
96
|
+
"id": cmd_id,
|
|
97
|
+
"success": False,
|
|
98
|
+
"output": "",
|
|
99
|
+
"error": ""
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if cmd_type == "execute_script" and script:
|
|
103
|
+
# Execute the script in the current CODESYS context
|
|
104
|
+
try:
|
|
105
|
+
# Capture output by redirecting stdout/stderr
|
|
106
|
+
import io
|
|
107
|
+
from contextlib import redirect_stdout, redirect_stderr
|
|
108
|
+
|
|
109
|
+
stdout_buffer = io.StringIO()
|
|
110
|
+
stderr_buffer = io.StringIO()
|
|
111
|
+
|
|
112
|
+
with redirect_stdout(stdout_buffer), redirect_stderr(stderr_buffer):
|
|
113
|
+
# Execute the script
|
|
114
|
+
exec(script, globals())
|
|
115
|
+
|
|
116
|
+
# Get the captured output
|
|
117
|
+
stdout_output = stdout_buffer.getvalue()
|
|
118
|
+
stderr_output = stderr_buffer.getvalue()
|
|
119
|
+
|
|
120
|
+
# Check for success/error markers
|
|
121
|
+
if SCRIPT_SUCCESS_MARKER in stdout_output or SCRIPT_SUCCESS_MARKER in stderr_output:
|
|
122
|
+
result["success"] = True
|
|
123
|
+
elif SCRIPT_ERROR_MARKER in stdout_output or SCRIPT_ERROR_MARKER in stderr_output:
|
|
124
|
+
result["success"] = False
|
|
125
|
+
else:
|
|
126
|
+
# No explicit markers, assume success
|
|
127
|
+
result["success"] = True
|
|
128
|
+
|
|
129
|
+
result["output"] = stdout_output
|
|
130
|
+
if stderr_output:
|
|
131
|
+
result["error"] = stderr_output
|
|
132
|
+
|
|
133
|
+
except Exception as e:
|
|
134
|
+
result["success"] = False
|
|
135
|
+
result["error"] = f"Error executing script: {str(e)}"
|
|
136
|
+
result["output"] = f"{SCRIPT_ERROR_MARKER}: {str(e)}"
|
|
137
|
+
|
|
138
|
+
elif cmd_type == "stop":
|
|
139
|
+
# Stop command received
|
|
140
|
+
result["success"] = True
|
|
141
|
+
result["output"] = f"{SCRIPT_SUCCESS_MARKER}: Session stopping"
|
|
142
|
+
|
|
143
|
+
# Update session state to mark as inactive
|
|
144
|
+
with open(SESSION_STATE_FILE, 'r') as f:
|
|
145
|
+
session_state = json.load(f)
|
|
146
|
+
|
|
147
|
+
session_state["active"] = False
|
|
148
|
+
session_state["end_time"] = time.time()
|
|
149
|
+
|
|
150
|
+
with open(SESSION_STATE_FILE, 'w') as f:
|
|
151
|
+
json.dump(session_state, f)
|
|
152
|
+
|
|
153
|
+
# Return the result and signal to stop the loop
|
|
154
|
+
return result, True
|
|
155
|
+
|
|
156
|
+
else:
|
|
157
|
+
result["success"] = False
|
|
158
|
+
result["error"] = f"Unknown command type: {cmd_type}"
|
|
159
|
+
result["output"] = f"{SCRIPT_ERROR_MARKER}: Unknown command type: {cmd_type}"
|
|
160
|
+
|
|
161
|
+
# Update session state after command execution
|
|
162
|
+
update_session_state()
|
|
163
|
+
|
|
164
|
+
return result, False
|
|
165
|
+
|
|
166
|
+
except Exception as e:
|
|
167
|
+
return {
|
|
168
|
+
"id": command_data.get("id", "unknown"),
|
|
169
|
+
"success": False,
|
|
170
|
+
"output": f"{SCRIPT_ERROR_MARKER}: Command execution failed",
|
|
171
|
+
"error": str(e)
|
|
172
|
+
}, False
|
|
173
|
+
|
|
174
|
+
def command_loop():
|
|
175
|
+
"""Main loop to listen for and execute commands"""
|
|
176
|
+
stop_requested = False
|
|
177
|
+
|
|
178
|
+
print(f"Starting command loop...")
|
|
179
|
+
print(f"Command pipe: {COMMAND_PIPE}")
|
|
180
|
+
print(f"Result pipe: {RESULT_PIPE}")
|
|
181
|
+
|
|
182
|
+
while not stop_requested:
|
|
183
|
+
try:
|
|
184
|
+
# Open the command pipe for reading (blocks until data is available)
|
|
185
|
+
with open(COMMAND_PIPE, 'r') as cmd_pipe:
|
|
186
|
+
# Read the command
|
|
187
|
+
command_json = cmd_pipe.read().strip()
|
|
188
|
+
|
|
189
|
+
if command_json:
|
|
190
|
+
print(f"Received command: {command_json[:100]}...")
|
|
191
|
+
try:
|
|
192
|
+
command_data = json.loads(command_json)
|
|
193
|
+
result, stop_requested = execute_command(command_data)
|
|
194
|
+
|
|
195
|
+
# Write the result to the result pipe
|
|
196
|
+
with open(RESULT_PIPE, 'w') as res_pipe:
|
|
197
|
+
res_pipe.write(json.dumps(result))
|
|
198
|
+
|
|
199
|
+
except json.JSONDecodeError as e:
|
|
200
|
+
# Invalid JSON
|
|
201
|
+
print(f"Invalid command JSON: {e}")
|
|
202
|
+
error_result = {
|
|
203
|
+
"id": "unknown",
|
|
204
|
+
"success": False,
|
|
205
|
+
"output": f"{SCRIPT_ERROR_MARKER}: Invalid command format",
|
|
206
|
+
"error": str(e)
|
|
207
|
+
}
|
|
208
|
+
with open(RESULT_PIPE, 'w') as res_pipe:
|
|
209
|
+
res_pipe.write(json.dumps(error_result))
|
|
210
|
+
|
|
211
|
+
except Exception as e:
|
|
212
|
+
print(f"Error in command loop: {e}")
|
|
213
|
+
# Short delay to prevent CPU spinning in case of repeated errors
|
|
214
|
+
time.sleep(0.5)
|
|
215
|
+
|
|
216
|
+
print("Command loop stopped.")
|
|
217
|
+
|
|
218
|
+
# Main script
|
|
219
|
+
def main():
|
|
220
|
+
try:
|
|
221
|
+
# Setup session directory and pipes
|
|
222
|
+
if not setup_session_dir():
|
|
223
|
+
sys.exit(1)
|
|
224
|
+
|
|
225
|
+
# Update initial session state
|
|
226
|
+
update_session_state()
|
|
227
|
+
|
|
228
|
+
# Start command loop
|
|
229
|
+
print(f"{SCRIPT_SUCCESS_MARKER}: CODESYS session started")
|
|
230
|
+
command_loop()
|
|
231
|
+
|
|
232
|
+
print(f"{SCRIPT_SUCCESS_MARKER}: CODESYS session stopped cleanly")
|
|
233
|
+
sys.exit(0)
|
|
234
|
+
except Exception as e:
|
|
235
|
+
print(f"Error in session: {e}")
|
|
236
|
+
print(f"{SCRIPT_ERROR_MARKER}: Session failed - {str(e)}")
|
|
237
|
+
sys.exit(1)
|
|
238
|
+
|
|
239
|
+
if __name__ == "__main__":
|
|
240
|
+
main()
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import sys, os, json, time, tempfile
|
|
2
|
+
|
|
3
|
+
# Define markers for script results
|
|
4
|
+
SCRIPT_SUCCESS_MARKER = "SCRIPT_SUCCESS"
|
|
5
|
+
SCRIPT_ERROR_MARKER = "SCRIPT_ERROR"
|
|
6
|
+
|
|
7
|
+
# Session state file and pipes
|
|
8
|
+
SESSION_DIR = os.path.join(tempfile.gettempdir(), "codesys_session")
|
|
9
|
+
SESSION_STATE_FILE = os.path.join(SESSION_DIR, "session_state.json")
|
|
10
|
+
COMMAND_PIPE = os.path.join(SESSION_DIR, "command_pipe")
|
|
11
|
+
RESULT_PIPE = os.path.join(SESSION_DIR, "result_pipe")
|
|
12
|
+
|
|
13
|
+
def check_session_active():
|
|
14
|
+
"""Check if a session is active by reading the state file"""
|
|
15
|
+
try:
|
|
16
|
+
if not os.path.exists(SESSION_STATE_FILE):
|
|
17
|
+
print(f"No session state file found at {SESSION_STATE_FILE}")
|
|
18
|
+
return False
|
|
19
|
+
|
|
20
|
+
with open(SESSION_STATE_FILE, 'r') as f:
|
|
21
|
+
session_state = json.load(f)
|
|
22
|
+
|
|
23
|
+
return session_state.get("active", False)
|
|
24
|
+
except Exception as e:
|
|
25
|
+
print(f"Error checking session state: {e}")
|
|
26
|
+
return False
|
|
27
|
+
|
|
28
|
+
def send_stop_command():
|
|
29
|
+
"""Send the stop command to the running session"""
|
|
30
|
+
try:
|
|
31
|
+
if not os.path.exists(COMMAND_PIPE) or not os.path.exists(RESULT_PIPE):
|
|
32
|
+
print(f"Command or result pipes not found")
|
|
33
|
+
return False
|
|
34
|
+
|
|
35
|
+
stop_command = {
|
|
36
|
+
"id": f"stop_{time.time()}",
|
|
37
|
+
"type": "stop",
|
|
38
|
+
"script": ""
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Send the stop command
|
|
42
|
+
with open(COMMAND_PIPE, 'w') as cmd_pipe:
|
|
43
|
+
cmd_pipe.write(json.dumps(stop_command))
|
|
44
|
+
|
|
45
|
+
# Wait for the result
|
|
46
|
+
with open(RESULT_PIPE, 'r') as res_pipe:
|
|
47
|
+
result_json = res_pipe.read().strip()
|
|
48
|
+
|
|
49
|
+
if result_json:
|
|
50
|
+
try:
|
|
51
|
+
result = json.loads(result_json)
|
|
52
|
+
return result.get("success", False)
|
|
53
|
+
except json.JSONDecodeError:
|
|
54
|
+
print(f"Invalid result JSON: {result_json}")
|
|
55
|
+
return False
|
|
56
|
+
else:
|
|
57
|
+
print("No result received")
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
except Exception as e:
|
|
61
|
+
print(f"Error sending stop command: {e}")
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
def main():
|
|
65
|
+
try:
|
|
66
|
+
# Check if session is active
|
|
67
|
+
if not check_session_active():
|
|
68
|
+
print(f"{SCRIPT_SUCCESS_MARKER}: No active session found")
|
|
69
|
+
sys.exit(0)
|
|
70
|
+
|
|
71
|
+
# Send stop command
|
|
72
|
+
print("Sending stop command to CODESYS session...")
|
|
73
|
+
if send_stop_command():
|
|
74
|
+
print(f"{SCRIPT_SUCCESS_MARKER}: Session stop command sent successfully")
|
|
75
|
+
|
|
76
|
+
# Give the session a moment to clean up
|
|
77
|
+
time.sleep(1)
|
|
78
|
+
|
|
79
|
+
# Verify session state has been updated
|
|
80
|
+
if not check_session_active():
|
|
81
|
+
print(f"{SCRIPT_SUCCESS_MARKER}: Session stopped successfully")
|
|
82
|
+
else:
|
|
83
|
+
print(f"{SCRIPT_SUCCESS_MARKER}: Stop command accepted, but session may still be shutting down")
|
|
84
|
+
else:
|
|
85
|
+
print(f"{SCRIPT_ERROR_MARKER}: Failed to stop session")
|
|
86
|
+
sys.exit(1)
|
|
87
|
+
|
|
88
|
+
except Exception as e:
|
|
89
|
+
print(f"Error stopping session: {e}")
|
|
90
|
+
print(f"{SCRIPT_ERROR_MARKER}: {str(e)}")
|
|
91
|
+
sys.exit(1)
|
|
92
|
+
|
|
93
|
+
if __name__ == "__main__":
|
|
94
|
+
main()
|