@codesys/mcp-toolkit 1.3.0 → 1.4.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/.claude/settings.local.json +3 -1
- package/CHANGELOG.md +14 -0
- package/dist/scripts/session_status.py +77 -20
- package/dist/scripts/start_session.py +139 -18
- package/dist/scripts/stop_session.py +55 -22
- package/docs/session_management.md +75 -54
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.4.0] - 2025-05-02
|
|
9
|
+
### Added
|
|
10
|
+
- Implemented persistent session management functionality
|
|
11
|
+
- Added support for keeping CODESYS open between commands
|
|
12
|
+
- Created file-based IPC for script execution in existing CODESYS instance
|
|
13
|
+
- Improved session reliability with health checks and automatic recovery
|
|
14
|
+
- Added session timeout after 10 minutes of inactivity
|
|
15
|
+
- Created detailed documentation for session management features
|
|
16
|
+
|
|
17
|
+
## [1.3.0] - 2025-05-02
|
|
18
|
+
### Changed
|
|
19
|
+
- Refactored codebase for improved reliability and maintainability
|
|
20
|
+
- Enhanced error handling in Python script templates
|
|
21
|
+
|
|
8
22
|
## [1.2.4] - 2025-05-02
|
|
9
23
|
### Fixed
|
|
10
24
|
- Fully disabled session management to prevent operations appearing to succeed but not actually running
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import sys, os, json, time
|
|
1
|
+
import sys, os, json, time, traceback
|
|
2
2
|
import tempfile
|
|
3
3
|
|
|
4
4
|
# Define markers for script results
|
|
@@ -26,18 +26,24 @@ def check_session_status():
|
|
|
26
26
|
return False
|
|
27
27
|
|
|
28
28
|
# Read session state
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
try:
|
|
30
|
+
with open(SESSION_STATE_FILE, 'r') as f:
|
|
31
31
|
session_state = json.load(f)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
except json.JSONDecodeError:
|
|
33
|
+
print(f"{SCRIPT_ERROR_MARKER}: Invalid session state file")
|
|
34
|
+
return False
|
|
35
35
|
|
|
36
36
|
# Check if session is active
|
|
37
37
|
active = session_state.get('active', False)
|
|
38
38
|
status = session_state.get('status', 'unknown')
|
|
39
39
|
|
|
40
|
+
# If the status indicates the session is not active, return immediately
|
|
41
|
+
if not active or status in ['stopped', 'error', 'stopping', 'timeout']:
|
|
42
|
+
print(f"{SCRIPT_SUCCESS_MARKER}: Session is not active (Status: {status})")
|
|
43
|
+
return False
|
|
44
|
+
|
|
40
45
|
# Check session lock file to see if it's been updated recently
|
|
46
|
+
lock_is_stale = False
|
|
41
47
|
if os.path.exists(SESSION_LOCK_FILE):
|
|
42
48
|
try:
|
|
43
49
|
with open(SESSION_LOCK_FILE, 'r') as f:
|
|
@@ -47,26 +53,33 @@ def check_session_status():
|
|
|
47
53
|
|
|
48
54
|
# If lock file hasn't been updated in 30 seconds, session might be dead
|
|
49
55
|
if time_diff > 30:
|
|
50
|
-
print(f"
|
|
51
|
-
|
|
56
|
+
print(f"WARNING: Session lock file is stale by {time_diff:.1f} seconds")
|
|
57
|
+
lock_is_stale = True
|
|
52
58
|
except Exception as e:
|
|
53
59
|
print(f"Error reading lock file: {e}")
|
|
60
|
+
lock_is_stale = True
|
|
61
|
+
else:
|
|
62
|
+
print("WARNING: Session lock file not found")
|
|
63
|
+
lock_is_stale = True
|
|
54
64
|
|
|
55
|
-
#
|
|
56
|
-
|
|
65
|
+
# If the lock file is stale but other checks suggest the session is active,
|
|
66
|
+
# we'll perform a ping test to be sure
|
|
67
|
+
ping_success = False
|
|
68
|
+
if status in ["running", "busy"]:
|
|
57
69
|
# Try to ping the session
|
|
58
70
|
try:
|
|
59
71
|
# Check if command file exists
|
|
60
72
|
if os.path.exists(COMMAND_FILE):
|
|
73
|
+
print("Sending ping command to verify session...")
|
|
74
|
+
|
|
61
75
|
# Create ping command
|
|
62
76
|
ping_command = {
|
|
63
77
|
"id": f"ping_{int(time.time())}",
|
|
64
|
-
"type": "
|
|
65
|
-
"script": "print('SCRIPT_SUCCESS: Ping successful')",
|
|
78
|
+
"type": "ping", # Use specific ping command type
|
|
66
79
|
"timestamp": time.time()
|
|
67
80
|
}
|
|
68
81
|
|
|
69
|
-
# Write command to file
|
|
82
|
+
# Write command to file with pretty-printing
|
|
70
83
|
command_data = {
|
|
71
84
|
"status": "pending",
|
|
72
85
|
"command": ping_command,
|
|
@@ -74,12 +87,11 @@ def check_session_status():
|
|
|
74
87
|
}
|
|
75
88
|
|
|
76
89
|
with open(COMMAND_FILE, 'w') as f:
|
|
77
|
-
json.dump(command_data, f)
|
|
90
|
+
json.dump(command_data, f, indent=2)
|
|
78
91
|
|
|
79
92
|
# Wait for result (with short timeout)
|
|
80
93
|
timeout = 5 # seconds
|
|
81
94
|
start_time = time.time()
|
|
82
|
-
ping_success = False
|
|
83
95
|
|
|
84
96
|
while not ping_success and (time.time() - start_time) < timeout:
|
|
85
97
|
# Check if result file exists
|
|
@@ -95,16 +107,37 @@ def check_session_status():
|
|
|
95
107
|
result = result_data.get("result")
|
|
96
108
|
if result.get('id') == ping_command['id'] and result.get('success', False):
|
|
97
109
|
ping_success = True
|
|
110
|
+
print("Ping successful, session is responsive")
|
|
98
111
|
break
|
|
99
112
|
except Exception as e:
|
|
100
113
|
print(f"Error reading result: {e}")
|
|
101
114
|
time.sleep(0.5)
|
|
102
115
|
|
|
103
116
|
if not ping_success:
|
|
104
|
-
print(f"
|
|
105
|
-
|
|
117
|
+
print(f"WARNING: Session did not respond to ping within {timeout} seconds")
|
|
118
|
+
|
|
119
|
+
# Reset command file since ping failed
|
|
120
|
+
try:
|
|
121
|
+
command_data = {
|
|
122
|
+
"status": "ready",
|
|
123
|
+
"command": None,
|
|
124
|
+
"timestamp": time.time()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
with open(COMMAND_FILE, 'w') as f:
|
|
128
|
+
json.dump(command_data, f, indent=2)
|
|
129
|
+
|
|
130
|
+
print("Reset command file after failed ping")
|
|
131
|
+
except Exception as e:
|
|
132
|
+
print(f"Error resetting command file: {e}")
|
|
133
|
+
else:
|
|
134
|
+
print("WARNING: Command file not found, cannot ping session")
|
|
106
135
|
except Exception as e:
|
|
107
136
|
print(f"Error during ping test: {e}")
|
|
137
|
+
traceback.print_exc()
|
|
138
|
+
|
|
139
|
+
# Make final determination based on all checks
|
|
140
|
+
session_active = active and (not lock_is_stale or ping_success) and status in ["running", "busy"]
|
|
108
141
|
|
|
109
142
|
# Build status message
|
|
110
143
|
project_info = ""
|
|
@@ -113,15 +146,39 @@ def check_session_status():
|
|
|
113
146
|
project_path = session_state.get('project_path', 'Unknown')
|
|
114
147
|
project_info = f", Project: {project_name} ({project_path})"
|
|
115
148
|
|
|
116
|
-
|
|
117
|
-
|
|
149
|
+
# Include session info
|
|
150
|
+
session_id = session_state.get('session_id', 'Unknown')
|
|
151
|
+
start_time = session_state.get('start_time', 0)
|
|
152
|
+
uptime = time.time() - start_time if start_time else 0
|
|
153
|
+
uptime_str = f"{uptime:.0f} seconds" if uptime < 60 else f"{uptime/60:.1f} minutes"
|
|
154
|
+
|
|
155
|
+
if session_active:
|
|
156
|
+
print(f"{SCRIPT_SUCCESS_MARKER}: Session is active (ID: {session_id}, Status: {status}, Uptime: {uptime_str}{project_info})")
|
|
118
157
|
return True
|
|
119
158
|
else:
|
|
120
|
-
|
|
159
|
+
# If we get here, session may be in a state where files exist but process is dead
|
|
160
|
+
if active and status in ["running", "busy"] and lock_is_stale and not ping_success:
|
|
161
|
+
print(f"{SCRIPT_SUCCESS_MARKER}: Session appears to be zombie (ID: {session_id}, Status: {status} but unresponsive)")
|
|
162
|
+
# Update session state to error so it will be considered inactive
|
|
163
|
+
try:
|
|
164
|
+
session_state['status'] = 'error'
|
|
165
|
+
session_state['active'] = False
|
|
166
|
+
session_state['error_reason'] = 'Unresponsive to ping'
|
|
167
|
+
|
|
168
|
+
with open(SESSION_STATE_FILE, 'w') as f:
|
|
169
|
+
json.dump(session_state, f, indent=2)
|
|
170
|
+
|
|
171
|
+
print("Updated session state to error due to unresponsiveness")
|
|
172
|
+
except Exception as e:
|
|
173
|
+
print(f"Error updating session state: {e}")
|
|
174
|
+
else:
|
|
175
|
+
print(f"{SCRIPT_SUCCESS_MARKER}: Session is not active (Status: {status})")
|
|
176
|
+
|
|
121
177
|
return False
|
|
122
178
|
|
|
123
179
|
except Exception as e:
|
|
124
180
|
print(f"{SCRIPT_ERROR_MARKER}: Error checking session status - {str(e)}")
|
|
181
|
+
traceback.print_exc()
|
|
125
182
|
return False
|
|
126
183
|
|
|
127
184
|
# Main script
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import sys, os, json, time, tempfile
|
|
1
|
+
import sys, os, json, time, tempfile, traceback
|
|
2
2
|
import scriptengine as script_engine
|
|
3
3
|
|
|
4
4
|
# Define markers for script results
|
|
@@ -11,43 +11,71 @@ SESSION_STATE_FILE = os.path.join(SESSION_DIR, "session_state.json")
|
|
|
11
11
|
COMMAND_FILE = os.path.join(SESSION_DIR, "command.json")
|
|
12
12
|
RESULT_FILE = os.path.join(SESSION_DIR, "result.json")
|
|
13
13
|
SESSION_LOCK_FILE = os.path.join(SESSION_DIR, "session.lock")
|
|
14
|
-
POLL_INTERVAL = 0.
|
|
14
|
+
POLL_INTERVAL = 0.1 # seconds (faster polling for better responsiveness)
|
|
15
|
+
MAX_INACTIVITY_TIME = 600 # seconds (10 minutes of inactivity before auto-shutdown)
|
|
15
16
|
|
|
16
17
|
def setup_session_dir():
|
|
17
18
|
"""Create the session directory and initial files"""
|
|
18
19
|
try:
|
|
20
|
+
# Clean up any existing session first
|
|
21
|
+
if os.path.exists(SESSION_DIR):
|
|
22
|
+
try:
|
|
23
|
+
# Try to read existing session state
|
|
24
|
+
if os.path.exists(SESSION_STATE_FILE):
|
|
25
|
+
try:
|
|
26
|
+
with open(SESSION_STATE_FILE, 'r') as f:
|
|
27
|
+
old_state = json.load(f)
|
|
28
|
+
print(f"Cleaning up previous session (ID: {old_state.get('session_id', 'unknown')})")
|
|
29
|
+
except:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
# Remove all files in the directory
|
|
33
|
+
for filename in os.listdir(SESSION_DIR):
|
|
34
|
+
filepath = os.path.join(SESSION_DIR, filename)
|
|
35
|
+
try:
|
|
36
|
+
if os.path.isfile(filepath):
|
|
37
|
+
os.unlink(filepath)
|
|
38
|
+
except Exception as e:
|
|
39
|
+
print(f"Error removing file {filepath}: {e}")
|
|
40
|
+
except Exception as e:
|
|
41
|
+
print(f"Error cleaning up previous session: {e}")
|
|
42
|
+
|
|
19
43
|
# Create session directory
|
|
20
44
|
if not os.path.exists(SESSION_DIR):
|
|
21
45
|
os.makedirs(SESSION_DIR)
|
|
22
46
|
|
|
23
47
|
# Initialize session state
|
|
48
|
+
session_id = str(time.time()) + "_" + str(os.getpid())
|
|
24
49
|
session_state = {
|
|
25
50
|
"active": True,
|
|
26
|
-
"session_id":
|
|
51
|
+
"session_id": session_id,
|
|
27
52
|
"start_time": time.time(),
|
|
53
|
+
"last_activity": time.time(),
|
|
28
54
|
"status": "running",
|
|
29
55
|
"project_open": False,
|
|
30
56
|
"project_name": "",
|
|
31
|
-
"project_path": ""
|
|
57
|
+
"project_path": "",
|
|
58
|
+
"version": "1.0"
|
|
32
59
|
}
|
|
33
60
|
|
|
34
61
|
# Write session state
|
|
35
62
|
with open(SESSION_STATE_FILE, 'w') as f:
|
|
36
|
-
json.dump(session_state, f)
|
|
63
|
+
json.dump(session_state, f, indent=2)
|
|
37
64
|
|
|
38
65
|
# Create empty command file
|
|
39
66
|
with open(COMMAND_FILE, 'w') as f:
|
|
40
|
-
json.dump({"status": "ready", "command": None, "timestamp": time.time()}, f)
|
|
67
|
+
json.dump({"status": "ready", "command": None, "timestamp": time.time()}, f, indent=2)
|
|
41
68
|
|
|
42
69
|
# Create empty result file
|
|
43
70
|
with open(RESULT_FILE, 'w') as f:
|
|
44
|
-
json.dump({"status": "none", "result": None, "timestamp": time.time()}, f)
|
|
71
|
+
json.dump({"status": "none", "result": None, "timestamp": time.time()}, f, indent=2)
|
|
45
72
|
|
|
46
73
|
# Create lock file
|
|
47
74
|
with open(SESSION_LOCK_FILE, 'w') as f:
|
|
48
75
|
f.write(str(time.time()))
|
|
49
76
|
|
|
50
77
|
print(f"Session directory setup at: {SESSION_DIR}")
|
|
78
|
+
print(f"Session ID: {session_id}")
|
|
51
79
|
return True
|
|
52
80
|
except Exception as e:
|
|
53
81
|
print(f"Failed to set up session directory: {e}")
|
|
@@ -78,8 +106,19 @@ def update_session_state(status="running"):
|
|
|
78
106
|
project_name = "Unnamed (Path Error)"
|
|
79
107
|
|
|
80
108
|
# Read current state file
|
|
81
|
-
|
|
82
|
-
|
|
109
|
+
try:
|
|
110
|
+
with open(SESSION_STATE_FILE, 'r') as f:
|
|
111
|
+
session_state = json.load(f)
|
|
112
|
+
except (FileNotFoundError, json.JSONDecodeError):
|
|
113
|
+
# State file doesn't exist or is invalid, create a new one
|
|
114
|
+
session_id = str(time.time()) + "_" + str(os.getpid())
|
|
115
|
+
session_state = {
|
|
116
|
+
"active": True,
|
|
117
|
+
"session_id": session_id,
|
|
118
|
+
"start_time": time.time(),
|
|
119
|
+
"status": status,
|
|
120
|
+
"version": "1.0"
|
|
121
|
+
}
|
|
83
122
|
|
|
84
123
|
# Update state
|
|
85
124
|
session_state["project_open"] = project_open
|
|
@@ -90,11 +129,12 @@ def update_session_state(status="running"):
|
|
|
90
129
|
|
|
91
130
|
# Write updated state
|
|
92
131
|
with open(SESSION_STATE_FILE, 'w') as f:
|
|
93
|
-
json.dump(session_state, f)
|
|
132
|
+
json.dump(session_state, f, indent=2)
|
|
94
133
|
|
|
95
134
|
return True
|
|
96
135
|
except Exception as e:
|
|
97
136
|
print(f"Failed to update session state: {e}")
|
|
137
|
+
traceback.print_exc()
|
|
98
138
|
return False
|
|
99
139
|
|
|
100
140
|
def execute_command(command_data):
|
|
@@ -114,6 +154,10 @@ def execute_command(command_data):
|
|
|
114
154
|
if cmd_type == "execute_script" and script:
|
|
115
155
|
# Execute the script in the current CODESYS context
|
|
116
156
|
try:
|
|
157
|
+
# Create a temporary globals dictionary with the current globals
|
|
158
|
+
# This allows us to isolate the script execution somewhat
|
|
159
|
+
local_globals = globals().copy()
|
|
160
|
+
|
|
117
161
|
# Capture output by redirecting stdout/stderr
|
|
118
162
|
import io
|
|
119
163
|
from contextlib import redirect_stdout, redirect_stderr
|
|
@@ -123,7 +167,7 @@ def execute_command(command_data):
|
|
|
123
167
|
|
|
124
168
|
with redirect_stdout(stdout_buffer), redirect_stderr(stderr_buffer):
|
|
125
169
|
# Execute the script
|
|
126
|
-
exec(script,
|
|
170
|
+
exec(script, local_globals)
|
|
127
171
|
|
|
128
172
|
# Get the captured output
|
|
129
173
|
stdout_output = stdout_buffer.getvalue()
|
|
@@ -143,8 +187,9 @@ def execute_command(command_data):
|
|
|
143
187
|
result["error"] = stderr_output
|
|
144
188
|
|
|
145
189
|
except Exception as e:
|
|
190
|
+
traceback.print_exc()
|
|
146
191
|
result["success"] = False
|
|
147
|
-
result["error"] = f"Error executing script: {str(e)}"
|
|
192
|
+
result["error"] = f"Error executing script: {str(e)}\n{traceback.format_exc()}"
|
|
148
193
|
result["output"] = f"{SCRIPT_ERROR_MARKER}: {str(e)}"
|
|
149
194
|
|
|
150
195
|
elif cmd_type == "stop":
|
|
@@ -158,6 +203,11 @@ def execute_command(command_data):
|
|
|
158
203
|
# Return the result and signal to stop the loop
|
|
159
204
|
return result, True
|
|
160
205
|
|
|
206
|
+
elif cmd_type == "ping":
|
|
207
|
+
# Simple ping command to check if session is alive
|
|
208
|
+
result["success"] = True
|
|
209
|
+
result["output"] = f"{SCRIPT_SUCCESS_MARKER}: Session is active"
|
|
210
|
+
|
|
161
211
|
else:
|
|
162
212
|
result["success"] = False
|
|
163
213
|
result["error"] = f"Unknown command type: {cmd_type}"
|
|
@@ -169,19 +219,31 @@ def execute_command(command_data):
|
|
|
169
219
|
return result, False
|
|
170
220
|
|
|
171
221
|
except Exception as e:
|
|
222
|
+
traceback.print_exc()
|
|
172
223
|
return {
|
|
173
224
|
"id": command_data.get("id", "unknown"),
|
|
174
225
|
"success": False,
|
|
175
226
|
"output": f"{SCRIPT_ERROR_MARKER}: Command execution failed",
|
|
176
|
-
"error": str(e)
|
|
227
|
+
"error": f"{str(e)}\n{traceback.format_exc()}"
|
|
177
228
|
}, False
|
|
178
229
|
|
|
179
230
|
def check_command():
|
|
180
231
|
"""Check if there is a new command to execute"""
|
|
181
232
|
try:
|
|
233
|
+
# If command file doesn't exist, return immediately
|
|
234
|
+
if not os.path.exists(COMMAND_FILE):
|
|
235
|
+
return False
|
|
236
|
+
|
|
182
237
|
# Read the command file
|
|
183
|
-
|
|
184
|
-
|
|
238
|
+
try:
|
|
239
|
+
with open(COMMAND_FILE, 'r') as f:
|
|
240
|
+
command_data = json.load(f)
|
|
241
|
+
except json.JSONDecodeError:
|
|
242
|
+
# If command file is corrupted, reset it
|
|
243
|
+
command_data = {"status": "ready", "command": None, "timestamp": time.time()}
|
|
244
|
+
with open(COMMAND_FILE, 'w') as f:
|
|
245
|
+
json.dump(command_data, f, indent=2)
|
|
246
|
+
return False
|
|
185
247
|
|
|
186
248
|
# Check if there's a new command
|
|
187
249
|
if command_data.get("status") == "pending" and command_data.get("command"):
|
|
@@ -191,7 +253,10 @@ def check_command():
|
|
|
191
253
|
# Mark as processing
|
|
192
254
|
command_data["status"] = "processing"
|
|
193
255
|
with open(COMMAND_FILE, 'w') as f:
|
|
194
|
-
json.dump(command_data, f)
|
|
256
|
+
json.dump(command_data, f, indent=2)
|
|
257
|
+
|
|
258
|
+
# Update session state to busy
|
|
259
|
+
update_session_state("busy")
|
|
195
260
|
|
|
196
261
|
# Execute the command
|
|
197
262
|
result, stop_requested = execute_command(command)
|
|
@@ -204,7 +269,7 @@ def check_command():
|
|
|
204
269
|
}
|
|
205
270
|
|
|
206
271
|
with open(RESULT_FILE, 'w') as f:
|
|
207
|
-
json.dump(result_data, f)
|
|
272
|
+
json.dump(result_data, f, indent=2)
|
|
208
273
|
|
|
209
274
|
# Reset command file
|
|
210
275
|
command_data = {
|
|
@@ -214,18 +279,60 @@ def check_command():
|
|
|
214
279
|
}
|
|
215
280
|
|
|
216
281
|
with open(COMMAND_FILE, 'w') as f:
|
|
217
|
-
json.dump(command_data, f)
|
|
282
|
+
json.dump(command_data, f, indent=2)
|
|
283
|
+
|
|
284
|
+
# Update session state to running again
|
|
285
|
+
if not stop_requested:
|
|
286
|
+
update_session_state("running")
|
|
287
|
+
|
|
288
|
+
# Update last activity time
|
|
289
|
+
update_last_activity()
|
|
218
290
|
|
|
219
291
|
return stop_requested
|
|
220
292
|
|
|
221
293
|
return False
|
|
222
294
|
except Exception as e:
|
|
223
295
|
print(f"Error checking command: {e}")
|
|
296
|
+
traceback.print_exc()
|
|
297
|
+
return False
|
|
298
|
+
|
|
299
|
+
def update_last_activity():
|
|
300
|
+
"""Update the last activity timestamp in the session state"""
|
|
301
|
+
try:
|
|
302
|
+
if os.path.exists(SESSION_STATE_FILE):
|
|
303
|
+
with open(SESSION_STATE_FILE, 'r') as f:
|
|
304
|
+
state = json.load(f)
|
|
305
|
+
|
|
306
|
+
state["last_activity"] = time.time()
|
|
307
|
+
|
|
308
|
+
with open(SESSION_STATE_FILE, 'w') as f:
|
|
309
|
+
json.dump(state, f, indent=2)
|
|
310
|
+
except Exception as e:
|
|
311
|
+
print(f"Error updating last activity: {e}")
|
|
312
|
+
|
|
313
|
+
def check_inactivity_timeout():
|
|
314
|
+
"""Check if session has been inactive for too long"""
|
|
315
|
+
try:
|
|
316
|
+
if os.path.exists(SESSION_STATE_FILE):
|
|
317
|
+
with open(SESSION_STATE_FILE, 'r') as f:
|
|
318
|
+
state = json.load(f)
|
|
319
|
+
|
|
320
|
+
last_activity = state.get("last_activity", state.get("start_time", time.time()))
|
|
321
|
+
current_time = time.time()
|
|
322
|
+
|
|
323
|
+
if (current_time - last_activity) > MAX_INACTIVITY_TIME:
|
|
324
|
+
print(f"Session inactive for {current_time - last_activity:.1f} seconds, shutting down")
|
|
325
|
+
return True
|
|
326
|
+
|
|
327
|
+
return False
|
|
328
|
+
except Exception as e:
|
|
329
|
+
print(f"Error checking inactivity timeout: {e}")
|
|
224
330
|
return False
|
|
225
331
|
|
|
226
332
|
def command_loop():
|
|
227
333
|
"""Main loop to check for and execute commands"""
|
|
228
334
|
stop_requested = False
|
|
335
|
+
last_state_update = time.time()
|
|
229
336
|
|
|
230
337
|
print(f"Starting command loop...")
|
|
231
338
|
print(f"Command file: {COMMAND_FILE}")
|
|
@@ -240,11 +347,24 @@ def command_loop():
|
|
|
240
347
|
# Check for new commands
|
|
241
348
|
stop_requested = check_command()
|
|
242
349
|
|
|
350
|
+
# Check for inactivity timeout
|
|
351
|
+
if check_inactivity_timeout():
|
|
352
|
+
print("Session timeout due to inactivity")
|
|
353
|
+
update_session_state("timeout")
|
|
354
|
+
stop_requested = True
|
|
355
|
+
|
|
356
|
+
# Periodically update session state (every 30 seconds)
|
|
357
|
+
current_time = time.time()
|
|
358
|
+
if (current_time - last_state_update) > 30:
|
|
359
|
+
update_session_state()
|
|
360
|
+
last_state_update = current_time
|
|
361
|
+
|
|
243
362
|
# Sleep for a bit
|
|
244
363
|
time.sleep(POLL_INTERVAL)
|
|
245
364
|
|
|
246
365
|
except Exception as e:
|
|
247
366
|
print(f"Error in command loop: {e}")
|
|
367
|
+
traceback.print_exc()
|
|
248
368
|
# Sleep for a bit
|
|
249
369
|
time.sleep(POLL_INTERVAL)
|
|
250
370
|
|
|
@@ -272,6 +392,7 @@ def main():
|
|
|
272
392
|
except Exception as e:
|
|
273
393
|
print(f"Error in session: {e}")
|
|
274
394
|
print(f"{SCRIPT_ERROR_MARKER}: Session failed - {str(e)}")
|
|
395
|
+
traceback.print_exc()
|
|
275
396
|
sys.exit(1)
|
|
276
397
|
|
|
277
398
|
if __name__ == "__main__":
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import sys, os, json, time
|
|
1
|
+
import sys, os, json, time, traceback
|
|
2
2
|
import tempfile
|
|
3
3
|
|
|
4
4
|
# Define markers for script results
|
|
@@ -26,15 +26,15 @@ def stop_session():
|
|
|
26
26
|
return False
|
|
27
27
|
|
|
28
28
|
# Read session state
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
try:
|
|
30
|
+
with open(SESSION_STATE_FILE, 'r') as f:
|
|
31
31
|
session_state = json.load(f)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
except json.JSONDecodeError:
|
|
33
|
+
print(f"{SCRIPT_ERROR_MARKER}: Invalid session state file")
|
|
34
|
+
return False
|
|
35
35
|
|
|
36
36
|
# Check if session is active
|
|
37
|
-
if not session_state.get('active', False):
|
|
37
|
+
if not session_state.get('active', False) or session_state.get('status') == 'stopped':
|
|
38
38
|
print(f"{SCRIPT_SUCCESS_MARKER}: Session is already stopped")
|
|
39
39
|
return True
|
|
40
40
|
|
|
@@ -43,6 +43,19 @@ def stop_session():
|
|
|
43
43
|
print(f"{SCRIPT_ERROR_MARKER}: Command file not found at {COMMAND_FILE}")
|
|
44
44
|
return False
|
|
45
45
|
|
|
46
|
+
# Try to read the lock file to check if session is still alive
|
|
47
|
+
lock_time = 0
|
|
48
|
+
try:
|
|
49
|
+
if os.path.exists(SESSION_LOCK_FILE):
|
|
50
|
+
with open(SESSION_LOCK_FILE, 'r') as f:
|
|
51
|
+
lock_time = float(f.read().strip())
|
|
52
|
+
|
|
53
|
+
current_time = time.time()
|
|
54
|
+
if (current_time - lock_time) > 30:
|
|
55
|
+
print(f"WARNING: Session lock file is stale ({current_time - lock_time:.1f} seconds old). Session may have crashed.")
|
|
56
|
+
except Exception as e:
|
|
57
|
+
print(f"Warning: Error reading lock file: {e}")
|
|
58
|
+
|
|
46
59
|
# Create stop command
|
|
47
60
|
stop_command = {
|
|
48
61
|
"id": f"stop_{int(time.time())}",
|
|
@@ -50,7 +63,7 @@ def stop_session():
|
|
|
50
63
|
"timestamp": time.time()
|
|
51
64
|
}
|
|
52
65
|
|
|
53
|
-
# Write command to file
|
|
66
|
+
# Write command to file with pretty-printing
|
|
54
67
|
command_data = {
|
|
55
68
|
"status": "pending",
|
|
56
69
|
"command": stop_command,
|
|
@@ -58,12 +71,12 @@ def stop_session():
|
|
|
58
71
|
}
|
|
59
72
|
|
|
60
73
|
with open(COMMAND_FILE, 'w') as f:
|
|
61
|
-
json.dump(command_data, f)
|
|
74
|
+
json.dump(command_data, f, indent=2)
|
|
62
75
|
|
|
63
76
|
print(f"Stop command sent to session")
|
|
64
77
|
|
|
65
78
|
# Wait for result (with timeout)
|
|
66
|
-
timeout =
|
|
79
|
+
timeout = 15 # seconds (increased timeout for more reliability)
|
|
67
80
|
start_time = time.time()
|
|
68
81
|
result_received = False
|
|
69
82
|
|
|
@@ -87,26 +100,46 @@ def stop_session():
|
|
|
87
100
|
print(f"Error reading result: {e}")
|
|
88
101
|
time.sleep(0.5)
|
|
89
102
|
|
|
90
|
-
# Update session state to indicate it's stopped
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
103
|
+
# Update session state to indicate it's stopped regardless of result
|
|
104
|
+
try:
|
|
105
|
+
with open(SESSION_STATE_FILE, 'r') as f:
|
|
106
|
+
session_state = json.load(f)
|
|
107
|
+
|
|
108
|
+
session_state['active'] = False
|
|
109
|
+
session_state['status'] = "stopped"
|
|
110
|
+
session_state['end_time'] = time.time()
|
|
97
111
|
|
|
98
|
-
|
|
99
|
-
|
|
112
|
+
with open(SESSION_STATE_FILE, 'w') as f:
|
|
113
|
+
json.dump(session_state, f, indent=2)
|
|
114
|
+
except Exception as e:
|
|
115
|
+
print(f"Warning: Failed to update session state file: {e}")
|
|
100
116
|
|
|
101
117
|
if result_received:
|
|
102
118
|
print(f"{SCRIPT_SUCCESS_MARKER}: Session stopped successfully")
|
|
119
|
+
return True
|
|
103
120
|
else:
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
121
|
+
# If we didn't get a response, the session may be hung
|
|
122
|
+
# Force a cleanup by removing session files
|
|
123
|
+
print(f"Warning: Session did not respond to stop command. Forcing cleanup.")
|
|
124
|
+
try:
|
|
125
|
+
# Try to remove all session files to ensure a clean state for next session
|
|
126
|
+
for filename in os.listdir(SESSION_DIR):
|
|
127
|
+
filepath = os.path.join(SESSION_DIR, filename)
|
|
128
|
+
try:
|
|
129
|
+
if os.path.isfile(filepath):
|
|
130
|
+
os.unlink(filepath)
|
|
131
|
+
except Exception as e:
|
|
132
|
+
print(f"Error removing file {filepath}: {e}")
|
|
133
|
+
|
|
134
|
+
print(f"{SCRIPT_SUCCESS_MARKER}: Session files cleaned up forcefully")
|
|
135
|
+
return True
|
|
136
|
+
except Exception as e:
|
|
137
|
+
print(f"{SCRIPT_ERROR_MARKER}: Failed to force cleanup - {str(e)}")
|
|
138
|
+
return False
|
|
107
139
|
|
|
108
140
|
except Exception as e:
|
|
109
141
|
print(f"{SCRIPT_ERROR_MARKER}: Error stopping session - {str(e)}")
|
|
142
|
+
traceback.print_exc()
|
|
110
143
|
return False
|
|
111
144
|
|
|
112
145
|
# Main script
|
|
@@ -1,89 +1,110 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
> **IMPORTANT:** Session management is currently disabled in version 1.2.4.
|
|
4
|
-
> The `start_session` and `stop_session` commands exist but are non-functional.
|
|
5
|
-
> All operations will run in standard mode (starting and stopping CODESYS for each operation).
|
|
6
|
-
> This feature will be fully implemented in a future release.
|
|
1
|
+
# Session Management in CODESYS MCP Toolkit
|
|
7
2
|
|
|
8
3
|
## Overview
|
|
9
4
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
## Why Session Management?
|
|
5
|
+
Starting from version 1.4.0, the CODESYS MCP Toolkit supports persistent sessions. This feature significantly improves performance by allowing a single instance of CODESYS to stay open while multiple commands are executed, then closing it when explicitly requested or after a period of inactivity.
|
|
13
6
|
|
|
14
|
-
|
|
7
|
+
## Benefits
|
|
15
8
|
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
9
|
+
- **Faster execution**: Eliminates the overhead of starting and stopping CODESYS for each command
|
|
10
|
+
- **Shared context**: Commands executed in the same session share the same CODESYS runtime context
|
|
11
|
+
- **Resource efficiency**: Reduces CPU and memory usage by avoiding repeated process creation
|
|
12
|
+
- **Automatic timeout**: Sessions automatically shut down after a period of inactivity (10 minutes by default)
|
|
19
13
|
|
|
20
|
-
|
|
21
|
-
1. Start a CODESYS session once
|
|
22
|
-
2. Execute multiple operations without restarting CODESYS
|
|
23
|
-
3. Close the session when you're finished
|
|
14
|
+
## Session Management Commands
|
|
24
15
|
|
|
25
|
-
|
|
16
|
+
The toolkit provides three specific commands for managing sessions:
|
|
26
17
|
|
|
27
|
-
|
|
18
|
+
### 1. `start_session`
|
|
28
19
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
### Starting a Session
|
|
20
|
+
Starts a new persistent CODESYS session. If a session is already running, this command reports that fact without starting another.
|
|
32
21
|
|
|
22
|
+
**Example**:
|
|
33
23
|
```
|
|
34
|
-
|
|
24
|
+
/mcp codesys_local start_session
|
|
35
25
|
```
|
|
36
26
|
|
|
37
|
-
###
|
|
27
|
+
### 2. `session_status`
|
|
38
28
|
|
|
39
|
-
|
|
29
|
+
Checks if a session is currently active and returns information about it, including:
|
|
30
|
+
- Current state (running, busy, stopped, etc.)
|
|
31
|
+
- How long the session has been running
|
|
32
|
+
- Which project is open in the session (if any)
|
|
40
33
|
|
|
34
|
+
**Example**:
|
|
41
35
|
```
|
|
42
|
-
|
|
36
|
+
/mcp codesys_local session_status
|
|
43
37
|
```
|
|
44
38
|
|
|
45
|
-
|
|
46
|
-
Using the CODESYS Local server, add a POU named MainProgram of type Program with ST language
|
|
47
|
-
```
|
|
39
|
+
### 3. `stop_session`
|
|
48
40
|
|
|
49
|
-
|
|
41
|
+
Stops an active session and cleans up session resources. If no session is running, this command reports that fact without taking any action.
|
|
50
42
|
|
|
43
|
+
**Example**:
|
|
51
44
|
```
|
|
52
|
-
|
|
45
|
+
/mcp codesys_local stop_session
|
|
53
46
|
```
|
|
54
47
|
|
|
55
|
-
|
|
48
|
+
## How to Use Session Management
|
|
56
49
|
|
|
57
|
-
|
|
50
|
+
### Recommended Workflow
|
|
58
51
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
52
|
+
1. Start a session at the beginning of your work:
|
|
53
|
+
```
|
|
54
|
+
/mcp codesys_local start_session
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
2. Execute all your commands normally - they will automatically use the active session:
|
|
58
|
+
```
|
|
59
|
+
/mcp codesys_local open_project --filePath "C:/path/to/project.project"
|
|
60
|
+
/mcp codesys_local create_pou --projectFilePath "C:/path/to/project.project" --name "MyPOU" --type "FunctionBlock" --language "ST" --parentPath "Application"
|
|
61
|
+
```
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
3. When you're done working, stop the session:
|
|
64
|
+
```
|
|
65
|
+
/mcp codesys_local stop_session
|
|
66
|
+
```
|
|
64
67
|
|
|
65
|
-
|
|
68
|
+
If you forget to stop the session, it will automatically time out after 10 minutes of inactivity.
|
|
66
69
|
|
|
67
|
-
|
|
68
|
-
2. Execute all required operations (create project, create POUs, edit code, etc.)
|
|
69
|
-
3. Stop the session: `Using the CODESYS Local server, run the stop_session command to close the CODESYS session.`
|
|
70
|
+
### Session Behavior
|
|
70
71
|
|
|
71
|
-
|
|
72
|
+
- All commands automatically use an active session if one exists
|
|
73
|
+
- If no session exists when a command is run, it will execute without a session (traditional mode)
|
|
74
|
+
- You can explicitly start a session before running commands to improve performance
|
|
75
|
+
- Sessions maintain state between commands, including open projects
|
|
72
76
|
|
|
73
|
-
|
|
74
|
-
- The session status can be in one of the following states: `IDLE`, `STARTING`, `RUNNING`, `BUSY`, `STOPPING`, `STOPPED`, or `ERROR`
|
|
75
|
-
- If a session operation fails, the system will automatically fall back to the standard one-time execution mode
|
|
77
|
+
## Troubleshooting
|
|
76
78
|
|
|
77
|
-
|
|
79
|
+
### Common Issues
|
|
78
80
|
|
|
79
|
-
|
|
80
|
-
- Only one session can be active at a time per MCP server instance
|
|
81
|
-
- Some operations that modify the CODESYS environment itself may require restarting the session
|
|
81
|
+
#### Session Won't Start
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
If you can't start a session, check:
|
|
84
|
+
- Make sure CODESYS is properly installed and accessible at the path specified in your configuration
|
|
85
|
+
- Verify that the CODESYS profile exists and is valid
|
|
86
|
+
- Check if there are any error messages in the client's output
|
|
87
|
+
|
|
88
|
+
#### Session Appears Unresponsive
|
|
89
|
+
|
|
90
|
+
If a session seems stuck:
|
|
91
|
+
1. Check its status: `/mcp codesys_local session_status`
|
|
92
|
+
2. If it reports as active but not responding, try stopping it: `/mcp codesys_local stop_session`
|
|
93
|
+
3. If it still doesn't respond, the toolkit includes a forced cleanup mechanism that will activate automatically when you try to stop a non-responsive session
|
|
94
|
+
|
|
95
|
+
#### Session Files Remain After Stopping
|
|
96
|
+
|
|
97
|
+
Session state is stored in a temporary directory (`[temp]/codesys_session/`). These files should be cleaned up automatically when a session stops, but in rare cases (like if the process is killed abruptly), they might remain. The toolkit will clean up old files when starting a new session.
|
|
98
|
+
|
|
99
|
+
## Technical Details
|
|
100
|
+
|
|
101
|
+
The session management system works by:
|
|
84
102
|
|
|
85
|
-
|
|
103
|
+
1. Starting a CODESYS instance with a special Python script that enters a command loop
|
|
104
|
+
2. Maintaining session state in files within a temporary directory
|
|
105
|
+
3. Communicating with the running session via file-based IPC (Inter-Process Communication)
|
|
106
|
+
4. Executing scripts within the context of the running CODESYS instance
|
|
107
|
+
5. Automatically detecting and handling session health issues
|
|
86
108
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
3. If problems persist, fall back to using standard operations without explicit session management
|
|
109
|
+
Session files are stored in the system's temporary directory in a subfolder called `codesys_session`.
|
|
110
|
+
EOF < /dev/null
|