@codesys/mcp-toolkit 1.2.4 → 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 +162 -141
- package/dist/scripts/start_session.py +222 -63
- package/dist/scripts/stop_session.py +128 -72
- package/dist/sessionManager.js +54 -49
- 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,168 +1,189 @@
|
|
|
1
|
-
import sys, os, json, time,
|
|
1
|
+
import sys, os, json, time, traceback
|
|
2
|
+
import tempfile
|
|
2
3
|
|
|
3
4
|
# Define markers for script results
|
|
4
5
|
SCRIPT_SUCCESS_MARKER = "SCRIPT_SUCCESS"
|
|
5
6
|
SCRIPT_ERROR_MARKER = "SCRIPT_ERROR"
|
|
6
7
|
|
|
7
|
-
# Session state
|
|
8
|
+
# Session state and command files
|
|
8
9
|
SESSION_DIR = os.path.join(tempfile.gettempdir(), "codesys_session")
|
|
9
10
|
SESSION_STATE_FILE = os.path.join(SESSION_DIR, "session_state.json")
|
|
10
|
-
|
|
11
|
-
|
|
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")
|
|
12
14
|
|
|
13
15
|
def check_session_status():
|
|
14
|
-
"""Check the status of a CODESYS session
|
|
16
|
+
"""Check the status of a CODESYS session"""
|
|
15
17
|
try:
|
|
16
18
|
# Check if session directory exists
|
|
17
19
|
if not os.path.exists(SESSION_DIR):
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
# Check if state file exists
|
|
20
|
+
print(f"{SCRIPT_SUCCESS_MARKER}: No session active. Session directory not found.")
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
# Check if session state file exists
|
|
25
24
|
if not os.path.exists(SESSION_STATE_FILE):
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
"active": False,
|
|
29
|
-
"message": "Session directory exists but no state file found"
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
# Read the state file
|
|
33
|
-
with open(SESSION_STATE_FILE, 'r') as f:
|
|
34
|
-
session_state = json.load(f)
|
|
35
|
-
|
|
36
|
-
# Check if pipes exist
|
|
37
|
-
pipes_exist = os.path.exists(COMMAND_PIPE) and os.path.exists(RESULT_PIPE)
|
|
38
|
-
|
|
39
|
-
# Get session duration if available
|
|
40
|
-
start_time = session_state.get("start_time", 0)
|
|
41
|
-
if start_time > 0:
|
|
42
|
-
current_time = time.time()
|
|
43
|
-
duration_secs = int(current_time - start_time)
|
|
44
|
-
hours, remainder = divmod(duration_secs, 3600)
|
|
45
|
-
minutes, seconds = divmod(remainder, 60)
|
|
46
|
-
duration = f"{hours:02d}:{minutes:02d}:{seconds:02d}"
|
|
47
|
-
else:
|
|
48
|
-
duration = "unknown"
|
|
49
|
-
|
|
50
|
-
# Last update time
|
|
51
|
-
last_update = session_state.get("last_update", 0)
|
|
52
|
-
last_update_str = "never"
|
|
53
|
-
if last_update > 0:
|
|
54
|
-
seconds_since_update = int(time.time() - last_update)
|
|
55
|
-
if seconds_since_update < 60:
|
|
56
|
-
last_update_str = f"{seconds_since_update} seconds ago"
|
|
57
|
-
elif seconds_since_update < 3600:
|
|
58
|
-
last_update_str = f"{seconds_since_update // 60} minutes ago"
|
|
59
|
-
else:
|
|
60
|
-
last_update_str = f"{seconds_since_update // 3600} hours ago"
|
|
61
|
-
|
|
62
|
-
return {
|
|
63
|
-
"session_exists": True,
|
|
64
|
-
"active": session_state.get("active", False),
|
|
65
|
-
"project_open": session_state.get("project_open", False),
|
|
66
|
-
"project_name": session_state.get("project_name", "unknown"),
|
|
67
|
-
"project_path": session_state.get("project_path", "unknown"),
|
|
68
|
-
"duration": duration,
|
|
69
|
-
"pipes_exist": pipes_exist,
|
|
70
|
-
"last_update": last_update_str,
|
|
71
|
-
"message": "Session information retrieved successfully"
|
|
72
|
-
}
|
|
73
|
-
except Exception as e:
|
|
74
|
-
return {
|
|
75
|
-
"session_exists": True,
|
|
76
|
-
"active": False,
|
|
77
|
-
"error": str(e),
|
|
78
|
-
"message": f"Error reading session state: {e}"
|
|
79
|
-
}
|
|
25
|
+
print(f"{SCRIPT_SUCCESS_MARKER}: No session active. Session state file not found.")
|
|
26
|
+
return False
|
|
80
27
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
28
|
+
# Read session state
|
|
29
|
+
try:
|
|
30
|
+
with open(SESSION_STATE_FILE, 'r') as f:
|
|
31
|
+
session_state = json.load(f)
|
|
32
|
+
except json.JSONDecodeError:
|
|
33
|
+
print(f"{SCRIPT_ERROR_MARKER}: Invalid session state file")
|
|
85
34
|
return False
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
"script": "print('SCRIPT_SUCCESS: Ping successful')"
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
# Set a timeout for the ping operation
|
|
94
|
-
timeout = 5 # seconds
|
|
95
|
-
|
|
96
|
-
# Use non-blocking I/O to handle timeouts
|
|
97
|
-
import select
|
|
98
|
-
|
|
99
|
-
# Send the ping command
|
|
100
|
-
with open(COMMAND_PIPE, 'w') as cmd_pipe:
|
|
101
|
-
cmd_pipe.write(json.dumps(ping_command))
|
|
102
|
-
|
|
103
|
-
# Wait for the result pipe to be readable with timeout
|
|
104
|
-
res_fd = os.open(RESULT_PIPE, os.O_RDONLY | os.O_NONBLOCK)
|
|
105
|
-
ready, _, _ = select.select([res_fd], [], [], timeout)
|
|
35
|
+
|
|
36
|
+
# Check if session is active
|
|
37
|
+
active = session_state.get('active', False)
|
|
38
|
+
status = session_state.get('status', 'unknown')
|
|
106
39
|
|
|
107
|
-
|
|
108
|
-
|
|
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})")
|
|
109
43
|
return False
|
|
110
|
-
|
|
111
|
-
# Read the result
|
|
112
|
-
result_json = os.read(res_fd, 4096).decode('utf-8').strip()
|
|
113
|
-
os.close(res_fd)
|
|
114
44
|
|
|
115
|
-
if
|
|
45
|
+
# Check session lock file to see if it's been updated recently
|
|
46
|
+
lock_is_stale = False
|
|
47
|
+
if os.path.exists(SESSION_LOCK_FILE):
|
|
116
48
|
try:
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
49
|
+
with open(SESSION_LOCK_FILE, 'r') as f:
|
|
50
|
+
lock_time = float(f.read().strip())
|
|
51
|
+
current_time = time.time()
|
|
52
|
+
time_diff = current_time - lock_time
|
|
53
|
+
|
|
54
|
+
# If lock file hasn't been updated in 30 seconds, session might be dead
|
|
55
|
+
if time_diff > 30:
|
|
56
|
+
print(f"WARNING: Session lock file is stale by {time_diff:.1f} seconds")
|
|
57
|
+
lock_is_stale = True
|
|
58
|
+
except Exception as e:
|
|
59
|
+
print(f"Error reading lock file: {e}")
|
|
60
|
+
lock_is_stale = True
|
|
121
61
|
else:
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
62
|
+
print("WARNING: Session lock file not found")
|
|
63
|
+
lock_is_stale = True
|
|
64
|
+
|
|
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"]:
|
|
69
|
+
# Try to ping the session
|
|
70
|
+
try:
|
|
71
|
+
# Check if command file exists
|
|
72
|
+
if os.path.exists(COMMAND_FILE):
|
|
73
|
+
print("Sending ping command to verify session...")
|
|
74
|
+
|
|
75
|
+
# Create ping command
|
|
76
|
+
ping_command = {
|
|
77
|
+
"id": f"ping_{int(time.time())}",
|
|
78
|
+
"type": "ping", # Use specific ping command type
|
|
79
|
+
"timestamp": time.time()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# Write command to file with pretty-printing
|
|
83
|
+
command_data = {
|
|
84
|
+
"status": "pending",
|
|
85
|
+
"command": ping_command,
|
|
86
|
+
"timestamp": time.time()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
with open(COMMAND_FILE, 'w') as f:
|
|
90
|
+
json.dump(command_data, f, indent=2)
|
|
91
|
+
|
|
92
|
+
# Wait for result (with short timeout)
|
|
93
|
+
timeout = 5 # seconds
|
|
94
|
+
start_time = time.time()
|
|
95
|
+
|
|
96
|
+
while not ping_success and (time.time() - start_time) < timeout:
|
|
97
|
+
# Check if result file exists
|
|
98
|
+
if not os.path.exists(RESULT_FILE):
|
|
99
|
+
time.sleep(0.5)
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
with open(RESULT_FILE, 'r') as f:
|
|
104
|
+
result_data = json.load(f)
|
|
105
|
+
|
|
106
|
+
if result_data.get("status") == "complete" and result_data.get("result"):
|
|
107
|
+
result = result_data.get("result")
|
|
108
|
+
if result.get('id') == ping_command['id'] and result.get('success', False):
|
|
109
|
+
ping_success = True
|
|
110
|
+
print("Ping successful, session is responsive")
|
|
111
|
+
break
|
|
112
|
+
except Exception as e:
|
|
113
|
+
print(f"Error reading result: {e}")
|
|
114
|
+
time.sleep(0.5)
|
|
115
|
+
|
|
116
|
+
if not ping_success:
|
|
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")
|
|
135
|
+
except Exception as e:
|
|
136
|
+
print(f"Error during ping test: {e}")
|
|
137
|
+
traceback.print_exc()
|
|
132
138
|
|
|
133
|
-
#
|
|
134
|
-
|
|
135
|
-
print(f"Session Active: {status.get('active', False)}")
|
|
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"]
|
|
136
141
|
|
|
137
|
-
#
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
142
|
+
# Build status message
|
|
143
|
+
project_info = ""
|
|
144
|
+
if session_state.get('project_open', False):
|
|
145
|
+
project_name = session_state.get('project_name', 'Unknown')
|
|
146
|
+
project_path = session_state.get('project_path', 'Unknown')
|
|
147
|
+
project_info = f", Project: {project_name} ({project_path})"
|
|
148
|
+
|
|
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})")
|
|
157
|
+
return True
|
|
158
|
+
else:
|
|
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}")
|
|
155
174
|
else:
|
|
156
|
-
print("Session is not active (
|
|
175
|
+
print(f"{SCRIPT_SUCCESS_MARKER}: Session is not active (Status: {status})")
|
|
157
176
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
sys.exit(0)
|
|
161
|
-
|
|
177
|
+
return False
|
|
178
|
+
|
|
162
179
|
except Exception as e:
|
|
163
|
-
print(f"Error checking session status
|
|
164
|
-
|
|
165
|
-
|
|
180
|
+
print(f"{SCRIPT_ERROR_MARKER}: Error checking session status - {str(e)}")
|
|
181
|
+
traceback.print_exc()
|
|
182
|
+
return False
|
|
166
183
|
|
|
184
|
+
# Main script
|
|
167
185
|
if __name__ == "__main__":
|
|
168
|
-
|
|
186
|
+
if check_session_status():
|
|
187
|
+
sys.exit(0)
|
|
188
|
+
else:
|
|
189
|
+
sys.exit(1)
|