@codesys/mcp-toolkit 1.3.0 → 1.4.1
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 +19 -0
- package/dist/codesys_interop.js +1 -1
- package/dist/scripts/session_status.py +92 -28
- package/dist/scripts/start_session.py +194 -50
- package/dist/scripts/stop_session.py +72 -32
- package/docs/session_management.md +75 -54
- package/package.json +1 -1
- package/replacements_session_status.txt +16 -0
- package/scripts/fix_fstrings.py +76 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,25 @@ 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.1] - 2025-05-02
|
|
9
|
+
### Fixed
|
|
10
|
+
- Fixed IronPython 2.7 compatibility for session management scripts
|
|
11
|
+
- Replaced f-strings with str.format() method calls for IronPython compatibility
|
|
12
|
+
- Added fallback import for json module (using simplejson for IronPython)
|
|
13
|
+
- Improved error handling for session scripts
|
|
14
|
+
- Re-enabled session management feature in codesys_interop.ts
|
|
15
|
+
|
|
16
|
+
## [1.4.0] - 2025-05-02
|
|
17
|
+
### Added
|
|
18
|
+
- Implemented initial framework for session management
|
|
19
|
+
- Added foundation for file-based IPC between Node.js and CODESYS instances
|
|
20
|
+
- Created documentation structure for session management capabilities
|
|
21
|
+
|
|
22
|
+
## [1.3.0] - 2025-05-02
|
|
23
|
+
### Changed
|
|
24
|
+
- Refactored codebase for improved reliability and maintainability
|
|
25
|
+
- Enhanced error handling in Python script templates
|
|
26
|
+
|
|
8
27
|
## [1.2.4] - 2025-05-02
|
|
9
28
|
### Fixed
|
|
10
29
|
- Fully disabled session management to prevent operations appearing to succeed but not actually running
|
package/dist/codesys_interop.js
CHANGED
|
@@ -92,7 +92,7 @@ function executeCodesysScript(scriptContent, codesysExePath, codesysProfileName,
|
|
|
92
92
|
throw new Error(`CODESYS executable not found at provided path: ${codesysExePath}`);
|
|
93
93
|
// --- End Pre-checks ---
|
|
94
94
|
// Enable or disable session management
|
|
95
|
-
const SESSION_FEATURE_DISABLED = false;
|
|
95
|
+
const SESSION_FEATURE_DISABLED = false; // Session feature enabled
|
|
96
96
|
// --- Check if we should use the persistent session ---
|
|
97
97
|
if (!SESSION_FEATURE_DISABLED && sessionManager) {
|
|
98
98
|
const sessionState = sessionManager.getState();
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
import sys, os,
|
|
1
|
+
import sys, os, time, traceback
|
|
2
2
|
import tempfile
|
|
3
3
|
|
|
4
|
+
# Import JSON handling for IronPython 2.7 compatibility
|
|
5
|
+
try:
|
|
6
|
+
import json
|
|
7
|
+
except ImportError:
|
|
8
|
+
# For IronPython 2.7, we need to use simplejson instead
|
|
9
|
+
import simplejson as json
|
|
10
|
+
|
|
4
11
|
# Define markers for script results
|
|
5
12
|
SCRIPT_SUCCESS_MARKER = "SCRIPT_SUCCESS"
|
|
6
13
|
SCRIPT_ERROR_MARKER = "SCRIPT_ERROR"
|
|
@@ -17,27 +24,33 @@ def check_session_status():
|
|
|
17
24
|
try:
|
|
18
25
|
# Check if session directory exists
|
|
19
26
|
if not os.path.exists(SESSION_DIR):
|
|
20
|
-
print(
|
|
27
|
+
print("{0}: No session active. Session directory not found.".format(SCRIPT_SUCCESS_MARKER))
|
|
21
28
|
return False
|
|
22
29
|
|
|
23
30
|
# Check if session state file exists
|
|
24
31
|
if not os.path.exists(SESSION_STATE_FILE):
|
|
25
|
-
print(
|
|
32
|
+
print("{0}: No session active. Session state file not found.".format(SCRIPT_SUCCESS_MARKER))
|
|
26
33
|
return False
|
|
27
34
|
|
|
28
35
|
# Read session state
|
|
29
|
-
|
|
30
|
-
|
|
36
|
+
try:
|
|
37
|
+
with open(SESSION_STATE_FILE, 'r') as f:
|
|
31
38
|
session_state = json.load(f)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
39
|
+
except json.JSONDecodeError:
|
|
40
|
+
print("{0}: Invalid session state file".format(SCRIPT_ERROR_MARKER))
|
|
41
|
+
return False
|
|
35
42
|
|
|
36
43
|
# Check if session is active
|
|
37
44
|
active = session_state.get('active', False)
|
|
38
45
|
status = session_state.get('status', 'unknown')
|
|
39
46
|
|
|
47
|
+
# If the status indicates the session is not active, return immediately
|
|
48
|
+
if not active or status in ['stopped', 'error', 'stopping', 'timeout']:
|
|
49
|
+
print("{0}: Session is not active (Status: {1})".format(SCRIPT_SUCCESS_MARKER, status))
|
|
50
|
+
return False
|
|
51
|
+
|
|
40
52
|
# Check session lock file to see if it's been updated recently
|
|
53
|
+
lock_is_stale = False
|
|
41
54
|
if os.path.exists(SESSION_LOCK_FILE):
|
|
42
55
|
try:
|
|
43
56
|
with open(SESSION_LOCK_FILE, 'r') as f:
|
|
@@ -47,26 +60,33 @@ def check_session_status():
|
|
|
47
60
|
|
|
48
61
|
# If lock file hasn't been updated in 30 seconds, session might be dead
|
|
49
62
|
if time_diff > 30:
|
|
50
|
-
print(
|
|
51
|
-
|
|
63
|
+
print("WARNING: Session lock file is stale by {0:.1f} seconds".format(time_diff))
|
|
64
|
+
lock_is_stale = True
|
|
52
65
|
except Exception as e:
|
|
53
|
-
print(
|
|
66
|
+
print("Error reading lock file: {0}".format(e))
|
|
67
|
+
lock_is_stale = True
|
|
68
|
+
else:
|
|
69
|
+
print("WARNING: Session lock file not found")
|
|
70
|
+
lock_is_stale = True
|
|
54
71
|
|
|
55
|
-
#
|
|
56
|
-
|
|
72
|
+
# If the lock file is stale but other checks suggest the session is active,
|
|
73
|
+
# we'll perform a ping test to be sure
|
|
74
|
+
ping_success = False
|
|
75
|
+
if status in ["running", "busy"]:
|
|
57
76
|
# Try to ping the session
|
|
58
77
|
try:
|
|
59
78
|
# Check if command file exists
|
|
60
79
|
if os.path.exists(COMMAND_FILE):
|
|
80
|
+
print("Sending ping command to verify session...")
|
|
81
|
+
|
|
61
82
|
# Create ping command
|
|
62
83
|
ping_command = {
|
|
63
|
-
"id":
|
|
64
|
-
"type": "
|
|
65
|
-
"script": "print('SCRIPT_SUCCESS: Ping successful')",
|
|
84
|
+
"id": "ping_{0}".format(int(time.time())),
|
|
85
|
+
"type": "ping", # Use specific ping command type
|
|
66
86
|
"timestamp": time.time()
|
|
67
87
|
}
|
|
68
88
|
|
|
69
|
-
# Write command to file
|
|
89
|
+
# Write command to file with pretty-printing
|
|
70
90
|
command_data = {
|
|
71
91
|
"status": "pending",
|
|
72
92
|
"command": ping_command,
|
|
@@ -74,12 +94,11 @@ def check_session_status():
|
|
|
74
94
|
}
|
|
75
95
|
|
|
76
96
|
with open(COMMAND_FILE, 'w') as f:
|
|
77
|
-
json.dump(command_data, f)
|
|
97
|
+
json.dump(command_data, f, indent=2)
|
|
78
98
|
|
|
79
99
|
# Wait for result (with short timeout)
|
|
80
100
|
timeout = 5 # seconds
|
|
81
101
|
start_time = time.time()
|
|
82
|
-
ping_success = False
|
|
83
102
|
|
|
84
103
|
while not ping_success and (time.time() - start_time) < timeout:
|
|
85
104
|
# Check if result file exists
|
|
@@ -95,33 +114,78 @@ def check_session_status():
|
|
|
95
114
|
result = result_data.get("result")
|
|
96
115
|
if result.get('id') == ping_command['id'] and result.get('success', False):
|
|
97
116
|
ping_success = True
|
|
117
|
+
print("Ping successful, session is responsive")
|
|
98
118
|
break
|
|
99
119
|
except Exception as e:
|
|
100
|
-
print(
|
|
120
|
+
print("Error reading result: {0}".format(e))
|
|
101
121
|
time.sleep(0.5)
|
|
102
122
|
|
|
103
123
|
if not ping_success:
|
|
104
|
-
print(
|
|
105
|
-
|
|
124
|
+
print("WARNING: Session did not respond to ping within {0} seconds".format(timeout))
|
|
125
|
+
|
|
126
|
+
# Reset command file since ping failed
|
|
127
|
+
try:
|
|
128
|
+
command_data = {
|
|
129
|
+
"status": "ready",
|
|
130
|
+
"command": None,
|
|
131
|
+
"timestamp": time.time()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
with open(COMMAND_FILE, 'w') as f:
|
|
135
|
+
json.dump(command_data, f, indent=2)
|
|
136
|
+
|
|
137
|
+
print("Reset command file after failed ping")
|
|
138
|
+
except Exception as e:
|
|
139
|
+
print("Error resetting command file: {0}".format(e))
|
|
140
|
+
else:
|
|
141
|
+
print("WARNING: Command file not found, cannot ping session")
|
|
106
142
|
except Exception as e:
|
|
107
|
-
print(
|
|
143
|
+
print("Error during ping test: {0}".format(e))
|
|
144
|
+
traceback.print_exc()
|
|
145
|
+
|
|
146
|
+
# Make final determination based on all checks
|
|
147
|
+
session_active = active and (not lock_is_stale or ping_success) and status in ["running", "busy"]
|
|
108
148
|
|
|
109
149
|
# Build status message
|
|
110
150
|
project_info = ""
|
|
111
151
|
if session_state.get('project_open', False):
|
|
112
152
|
project_name = session_state.get('project_name', 'Unknown')
|
|
113
153
|
project_path = session_state.get('project_path', 'Unknown')
|
|
114
|
-
project_info =
|
|
154
|
+
project_info = ", Project: {0} ({1})".format(project_name, project_path)
|
|
115
155
|
|
|
116
|
-
|
|
117
|
-
|
|
156
|
+
# Include session info
|
|
157
|
+
session_id = session_state.get('session_id', 'Unknown')
|
|
158
|
+
start_time = session_state.get('start_time', 0)
|
|
159
|
+
uptime = time.time() - start_time if start_time else 0
|
|
160
|
+
uptime_str = "{0:.0f} seconds".format(uptime) if uptime < 60 else "{0:.1f} minutes".format(uptime/60)
|
|
161
|
+
|
|
162
|
+
if session_active:
|
|
163
|
+
print("{0}: Session is active (ID: {1}, Status: {2}, Uptime: {3}{4})".format(SCRIPT_SUCCESS_MARKER, session_id, status, uptime_str, project_info))
|
|
118
164
|
return True
|
|
119
165
|
else:
|
|
120
|
-
|
|
166
|
+
# If we get here, session may be in a state where files exist but process is dead
|
|
167
|
+
if active and status in ["running", "busy"] and lock_is_stale and not ping_success:
|
|
168
|
+
print("{0}: Session appears to be zombie (ID: {1}, Status: {2} but unresponsive)".format(SCRIPT_SUCCESS_MARKER, session_id, status))
|
|
169
|
+
# Update session state to error so it will be considered inactive
|
|
170
|
+
try:
|
|
171
|
+
session_state['status'] = 'error'
|
|
172
|
+
session_state['active'] = False
|
|
173
|
+
session_state['error_reason'] = 'Unresponsive to ping'
|
|
174
|
+
|
|
175
|
+
with open(SESSION_STATE_FILE, 'w') as f:
|
|
176
|
+
json.dump(session_state, f, indent=2)
|
|
177
|
+
|
|
178
|
+
print("Updated session state to error due to unresponsiveness")
|
|
179
|
+
except Exception as e:
|
|
180
|
+
print("Error updating session state: {0}".format(e))
|
|
181
|
+
else:
|
|
182
|
+
print("{0}: Session is not active (Status: {1})".format(SCRIPT_SUCCESS_MARKER, status))
|
|
183
|
+
|
|
121
184
|
return False
|
|
122
185
|
|
|
123
186
|
except Exception as e:
|
|
124
|
-
print(
|
|
187
|
+
print("{0}: Error checking session status - {1}".format(SCRIPT_ERROR_MARKER, str(e)))
|
|
188
|
+
traceback.print_exc()
|
|
125
189
|
return False
|
|
126
190
|
|
|
127
191
|
# Main script
|
|
@@ -1,5 +1,19 @@
|
|
|
1
|
-
import sys, os,
|
|
2
|
-
|
|
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
|
+
import simplejson as json
|
|
9
|
+
|
|
10
|
+
# Import scriptengine
|
|
11
|
+
try:
|
|
12
|
+
import scriptengine as script_engine
|
|
13
|
+
except ImportError:
|
|
14
|
+
# Log the error and continue - we'll handle this gracefully
|
|
15
|
+
print("Error importing scriptengine. This script must be run within CODESYS environment.")
|
|
16
|
+
script_engine = None
|
|
3
17
|
|
|
4
18
|
# Define markers for script results
|
|
5
19
|
SCRIPT_SUCCESS_MARKER = "SCRIPT_SUCCESS"
|
|
@@ -11,47 +25,75 @@ SESSION_STATE_FILE = os.path.join(SESSION_DIR, "session_state.json")
|
|
|
11
25
|
COMMAND_FILE = os.path.join(SESSION_DIR, "command.json")
|
|
12
26
|
RESULT_FILE = os.path.join(SESSION_DIR, "result.json")
|
|
13
27
|
SESSION_LOCK_FILE = os.path.join(SESSION_DIR, "session.lock")
|
|
14
|
-
POLL_INTERVAL = 0.
|
|
28
|
+
POLL_INTERVAL = 0.1 # seconds (faster polling for better responsiveness)
|
|
29
|
+
MAX_INACTIVITY_TIME = 600 # seconds (10 minutes of inactivity before auto-shutdown)
|
|
15
30
|
|
|
16
31
|
def setup_session_dir():
|
|
17
32
|
"""Create the session directory and initial files"""
|
|
18
33
|
try:
|
|
34
|
+
# Clean up any existing session first
|
|
35
|
+
if os.path.exists(SESSION_DIR):
|
|
36
|
+
try:
|
|
37
|
+
# Try to read existing session state
|
|
38
|
+
if os.path.exists(SESSION_STATE_FILE):
|
|
39
|
+
try:
|
|
40
|
+
with open(SESSION_STATE_FILE, 'r') as f:
|
|
41
|
+
old_state = json.load(f)
|
|
42
|
+
print("Cleaning up previous session (ID: {0})".format(old_state.get('session_id', 'unknown')))
|
|
43
|
+
except:
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
# Remove all files in the directory
|
|
47
|
+
for filename in os.listdir(SESSION_DIR):
|
|
48
|
+
filepath = os.path.join(SESSION_DIR, filename)
|
|
49
|
+
try:
|
|
50
|
+
if os.path.isfile(filepath):
|
|
51
|
+
os.unlink(filepath)
|
|
52
|
+
except Exception as e:
|
|
53
|
+
print("Error removing file {0}: {1}".format(filepath, e))
|
|
54
|
+
except Exception as e:
|
|
55
|
+
print("Error cleaning up previous session: {0}".format(e))
|
|
56
|
+
|
|
19
57
|
# Create session directory
|
|
20
58
|
if not os.path.exists(SESSION_DIR):
|
|
21
59
|
os.makedirs(SESSION_DIR)
|
|
22
60
|
|
|
23
61
|
# Initialize session state
|
|
62
|
+
session_id = str(time.time()) + "_" + str(os.getpid())
|
|
24
63
|
session_state = {
|
|
25
64
|
"active": True,
|
|
26
|
-
"session_id":
|
|
65
|
+
"session_id": session_id,
|
|
27
66
|
"start_time": time.time(),
|
|
67
|
+
"last_activity": time.time(),
|
|
28
68
|
"status": "running",
|
|
29
69
|
"project_open": False,
|
|
30
70
|
"project_name": "",
|
|
31
|
-
"project_path": ""
|
|
71
|
+
"project_path": "",
|
|
72
|
+
"version": "1.0"
|
|
32
73
|
}
|
|
33
74
|
|
|
34
75
|
# Write session state
|
|
35
76
|
with open(SESSION_STATE_FILE, 'w') as f:
|
|
36
|
-
json.dump(session_state, f)
|
|
77
|
+
json.dump(session_state, f, indent=2)
|
|
37
78
|
|
|
38
79
|
# Create empty command file
|
|
39
80
|
with open(COMMAND_FILE, 'w') as f:
|
|
40
|
-
json.dump({"status": "ready", "command": None, "timestamp": time.time()}, f)
|
|
81
|
+
json.dump({"status": "ready", "command": None, "timestamp": time.time()}, f, indent=2)
|
|
41
82
|
|
|
42
83
|
# Create empty result file
|
|
43
84
|
with open(RESULT_FILE, 'w') as f:
|
|
44
|
-
json.dump({"status": "none", "result": None, "timestamp": time.time()}, f)
|
|
85
|
+
json.dump({"status": "none", "result": None, "timestamp": time.time()}, f, indent=2)
|
|
45
86
|
|
|
46
87
|
# Create lock file
|
|
47
88
|
with open(SESSION_LOCK_FILE, 'w') as f:
|
|
48
89
|
f.write(str(time.time()))
|
|
49
90
|
|
|
50
|
-
print(
|
|
91
|
+
print("Session directory setup at: {0}".format(SESSION_DIR))
|
|
92
|
+
print("Session ID: {0}".format(session_id))
|
|
51
93
|
return True
|
|
52
94
|
except Exception as e:
|
|
53
|
-
print(
|
|
54
|
-
print(
|
|
95
|
+
print("Failed to set up session directory: {0}".format(e))
|
|
96
|
+
print("{0}: Session setup failed - {1}".format(SCRIPT_ERROR_MARKER, str(e)))
|
|
55
97
|
return False
|
|
56
98
|
|
|
57
99
|
def update_session_state(status="running"):
|
|
@@ -62,24 +104,44 @@ def update_session_state(status="running"):
|
|
|
62
104
|
project_name = "No project open"
|
|
63
105
|
project_path = "N/A"
|
|
64
106
|
|
|
65
|
-
|
|
66
|
-
if
|
|
67
|
-
|
|
107
|
+
# Check if script_engine was properly imported
|
|
108
|
+
if script_engine is None:
|
|
109
|
+
print("Warning: scriptengine not available, cannot get project info")
|
|
110
|
+
else:
|
|
111
|
+
# Try to get primary project
|
|
68
112
|
try:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
113
|
+
primary_project = script_engine.projects.primary
|
|
114
|
+
if primary_project:
|
|
115
|
+
project_open = True
|
|
116
|
+
try:
|
|
117
|
+
project_path = os.path.normcase(os.path.abspath(primary_project.path))
|
|
118
|
+
try:
|
|
119
|
+
project_name = primary_project.get_name()
|
|
120
|
+
if not project_name:
|
|
121
|
+
project_name = "Unnamed (path: {0})".format(os.path.basename(project_path))
|
|
122
|
+
except:
|
|
123
|
+
project_name = "Unnamed (path: {0})".format(os.path.basename(project_path))
|
|
124
|
+
except Exception as e_path:
|
|
125
|
+
project_path = "N/A (Error: {0})".format(str(e_path))
|
|
126
|
+
project_name = "Unnamed (Path Error)"
|
|
127
|
+
except Exception as e:
|
|
128
|
+
print("Error accessing script_engine.projects: {0}".format(str(e)))
|
|
129
|
+
traceback.print_exc()
|
|
79
130
|
|
|
80
131
|
# Read current state file
|
|
81
|
-
|
|
82
|
-
|
|
132
|
+
try:
|
|
133
|
+
with open(SESSION_STATE_FILE, 'r') as f:
|
|
134
|
+
session_state = json.load(f)
|
|
135
|
+
except (FileNotFoundError, json.JSONDecodeError):
|
|
136
|
+
# State file doesn't exist or is invalid, create a new one
|
|
137
|
+
session_id = str(time.time()) + "_" + str(os.getpid())
|
|
138
|
+
session_state = {
|
|
139
|
+
"active": True,
|
|
140
|
+
"session_id": session_id,
|
|
141
|
+
"start_time": time.time(),
|
|
142
|
+
"status": status,
|
|
143
|
+
"version": "1.0"
|
|
144
|
+
}
|
|
83
145
|
|
|
84
146
|
# Update state
|
|
85
147
|
session_state["project_open"] = project_open
|
|
@@ -90,11 +152,12 @@ def update_session_state(status="running"):
|
|
|
90
152
|
|
|
91
153
|
# Write updated state
|
|
92
154
|
with open(SESSION_STATE_FILE, 'w') as f:
|
|
93
|
-
json.dump(session_state, f)
|
|
155
|
+
json.dump(session_state, f, indent=2)
|
|
94
156
|
|
|
95
157
|
return True
|
|
96
158
|
except Exception as e:
|
|
97
|
-
print(
|
|
159
|
+
print("Failed to update session state: {0}".format(e))
|
|
160
|
+
traceback.print_exc()
|
|
98
161
|
return False
|
|
99
162
|
|
|
100
163
|
def execute_command(command_data):
|
|
@@ -114,6 +177,10 @@ def execute_command(command_data):
|
|
|
114
177
|
if cmd_type == "execute_script" and script:
|
|
115
178
|
# Execute the script in the current CODESYS context
|
|
116
179
|
try:
|
|
180
|
+
# Create a temporary globals dictionary with the current globals
|
|
181
|
+
# This allows us to isolate the script execution somewhat
|
|
182
|
+
local_globals = globals().copy()
|
|
183
|
+
|
|
117
184
|
# Capture output by redirecting stdout/stderr
|
|
118
185
|
import io
|
|
119
186
|
from contextlib import redirect_stdout, redirect_stderr
|
|
@@ -123,7 +190,7 @@ def execute_command(command_data):
|
|
|
123
190
|
|
|
124
191
|
with redirect_stdout(stdout_buffer), redirect_stderr(stderr_buffer):
|
|
125
192
|
# Execute the script
|
|
126
|
-
exec(script,
|
|
193
|
+
exec(script, local_globals)
|
|
127
194
|
|
|
128
195
|
# Get the captured output
|
|
129
196
|
stdout_output = stdout_buffer.getvalue()
|
|
@@ -143,14 +210,15 @@ def execute_command(command_data):
|
|
|
143
210
|
result["error"] = stderr_output
|
|
144
211
|
|
|
145
212
|
except Exception as e:
|
|
213
|
+
traceback.print_exc()
|
|
146
214
|
result["success"] = False
|
|
147
|
-
result["error"] =
|
|
148
|
-
result["output"] =
|
|
215
|
+
result["error"] = "Error executing script: {0}\n{1}".format(str(e), traceback.format_exc())
|
|
216
|
+
result["output"] = "{0}: {1}".format(SCRIPT_ERROR_MARKER, str(e))
|
|
149
217
|
|
|
150
218
|
elif cmd_type == "stop":
|
|
151
219
|
# Stop command received
|
|
152
220
|
result["success"] = True
|
|
153
|
-
result["output"] =
|
|
221
|
+
result["output"] = "{0}: Session stopping".format(SCRIPT_SUCCESS_MARKER)
|
|
154
222
|
|
|
155
223
|
# Update session state to mark as stopping
|
|
156
224
|
update_session_state("stopping")
|
|
@@ -158,10 +226,15 @@ def execute_command(command_data):
|
|
|
158
226
|
# Return the result and signal to stop the loop
|
|
159
227
|
return result, True
|
|
160
228
|
|
|
229
|
+
elif cmd_type == "ping":
|
|
230
|
+
# Simple ping command to check if session is alive
|
|
231
|
+
result["success"] = True
|
|
232
|
+
result["output"] = "{0}: Session is active".format(SCRIPT_SUCCESS_MARKER)
|
|
233
|
+
|
|
161
234
|
else:
|
|
162
235
|
result["success"] = False
|
|
163
|
-
result["error"] =
|
|
164
|
-
result["output"] =
|
|
236
|
+
result["error"] = "Unknown command type: {0}".format(cmd_type)
|
|
237
|
+
result["output"] = "{0}: Unknown command type: {1}".format(SCRIPT_ERROR_MARKER, cmd_type)
|
|
165
238
|
|
|
166
239
|
# Update session state after command execution
|
|
167
240
|
update_session_state()
|
|
@@ -169,19 +242,31 @@ def execute_command(command_data):
|
|
|
169
242
|
return result, False
|
|
170
243
|
|
|
171
244
|
except Exception as e:
|
|
245
|
+
traceback.print_exc()
|
|
172
246
|
return {
|
|
173
247
|
"id": command_data.get("id", "unknown"),
|
|
174
248
|
"success": False,
|
|
175
|
-
"output":
|
|
176
|
-
"error": str(e)
|
|
249
|
+
"output": "{0}: Command execution failed".format(SCRIPT_ERROR_MARKER),
|
|
250
|
+
"error": "{0}\n{1}".format(str(e), traceback.format_exc())
|
|
177
251
|
}, False
|
|
178
252
|
|
|
179
253
|
def check_command():
|
|
180
254
|
"""Check if there is a new command to execute"""
|
|
181
255
|
try:
|
|
256
|
+
# If command file doesn't exist, return immediately
|
|
257
|
+
if not os.path.exists(COMMAND_FILE):
|
|
258
|
+
return False
|
|
259
|
+
|
|
182
260
|
# Read the command file
|
|
183
|
-
|
|
184
|
-
|
|
261
|
+
try:
|
|
262
|
+
with open(COMMAND_FILE, 'r') as f:
|
|
263
|
+
command_data = json.load(f)
|
|
264
|
+
except json.JSONDecodeError:
|
|
265
|
+
# If command file is corrupted, reset it
|
|
266
|
+
command_data = {"status": "ready", "command": None, "timestamp": time.time()}
|
|
267
|
+
with open(COMMAND_FILE, 'w') as f:
|
|
268
|
+
json.dump(command_data, f, indent=2)
|
|
269
|
+
return False
|
|
185
270
|
|
|
186
271
|
# Check if there's a new command
|
|
187
272
|
if command_data.get("status") == "pending" and command_data.get("command"):
|
|
@@ -191,7 +276,10 @@ def check_command():
|
|
|
191
276
|
# Mark as processing
|
|
192
277
|
command_data["status"] = "processing"
|
|
193
278
|
with open(COMMAND_FILE, 'w') as f:
|
|
194
|
-
json.dump(command_data, f)
|
|
279
|
+
json.dump(command_data, f, indent=2)
|
|
280
|
+
|
|
281
|
+
# Update session state to busy
|
|
282
|
+
update_session_state("busy")
|
|
195
283
|
|
|
196
284
|
# Execute the command
|
|
197
285
|
result, stop_requested = execute_command(command)
|
|
@@ -204,7 +292,7 @@ def check_command():
|
|
|
204
292
|
}
|
|
205
293
|
|
|
206
294
|
with open(RESULT_FILE, 'w') as f:
|
|
207
|
-
json.dump(result_data, f)
|
|
295
|
+
json.dump(result_data, f, indent=2)
|
|
208
296
|
|
|
209
297
|
# Reset command file
|
|
210
298
|
command_data = {
|
|
@@ -214,22 +302,64 @@ def check_command():
|
|
|
214
302
|
}
|
|
215
303
|
|
|
216
304
|
with open(COMMAND_FILE, 'w') as f:
|
|
217
|
-
json.dump(command_data, f)
|
|
305
|
+
json.dump(command_data, f, indent=2)
|
|
306
|
+
|
|
307
|
+
# Update session state to running again
|
|
308
|
+
if not stop_requested:
|
|
309
|
+
update_session_state("running")
|
|
310
|
+
|
|
311
|
+
# Update last activity time
|
|
312
|
+
update_last_activity()
|
|
218
313
|
|
|
219
314
|
return stop_requested
|
|
220
315
|
|
|
221
316
|
return False
|
|
222
317
|
except Exception as e:
|
|
223
|
-
print(
|
|
318
|
+
print("Error checking command: {0}".format(e))
|
|
319
|
+
traceback.print_exc()
|
|
320
|
+
return False
|
|
321
|
+
|
|
322
|
+
def update_last_activity():
|
|
323
|
+
"""Update the last activity timestamp in the session state"""
|
|
324
|
+
try:
|
|
325
|
+
if os.path.exists(SESSION_STATE_FILE):
|
|
326
|
+
with open(SESSION_STATE_FILE, 'r') as f:
|
|
327
|
+
state = json.load(f)
|
|
328
|
+
|
|
329
|
+
state["last_activity"] = time.time()
|
|
330
|
+
|
|
331
|
+
with open(SESSION_STATE_FILE, 'w') as f:
|
|
332
|
+
json.dump(state, f, indent=2)
|
|
333
|
+
except Exception as e:
|
|
334
|
+
print("Error updating last activity: {0}".format(e))
|
|
335
|
+
|
|
336
|
+
def check_inactivity_timeout():
|
|
337
|
+
"""Check if session has been inactive for too long"""
|
|
338
|
+
try:
|
|
339
|
+
if os.path.exists(SESSION_STATE_FILE):
|
|
340
|
+
with open(SESSION_STATE_FILE, 'r') as f:
|
|
341
|
+
state = json.load(f)
|
|
342
|
+
|
|
343
|
+
last_activity = state.get("last_activity", state.get("start_time", time.time()))
|
|
344
|
+
current_time = time.time()
|
|
345
|
+
|
|
346
|
+
if (current_time - last_activity) > MAX_INACTIVITY_TIME:
|
|
347
|
+
print("Session inactive for {0:.1f} seconds, shutting down".format(current_time - last_activity))
|
|
348
|
+
return True
|
|
349
|
+
|
|
350
|
+
return False
|
|
351
|
+
except Exception as e:
|
|
352
|
+
print("Error checking inactivity timeout: {0}".format(e))
|
|
224
353
|
return False
|
|
225
354
|
|
|
226
355
|
def command_loop():
|
|
227
356
|
"""Main loop to check for and execute commands"""
|
|
228
357
|
stop_requested = False
|
|
358
|
+
last_state_update = time.time()
|
|
229
359
|
|
|
230
|
-
print(
|
|
231
|
-
print(
|
|
232
|
-
print(
|
|
360
|
+
print("Starting command loop...")
|
|
361
|
+
print("Command file: {0}".format(COMMAND_FILE))
|
|
362
|
+
print("Result file: {0}".format(RESULT_FILE))
|
|
233
363
|
|
|
234
364
|
while not stop_requested:
|
|
235
365
|
try:
|
|
@@ -240,11 +370,24 @@ def command_loop():
|
|
|
240
370
|
# Check for new commands
|
|
241
371
|
stop_requested = check_command()
|
|
242
372
|
|
|
373
|
+
# Check for inactivity timeout
|
|
374
|
+
if check_inactivity_timeout():
|
|
375
|
+
print("Session timeout due to inactivity")
|
|
376
|
+
update_session_state("timeout")
|
|
377
|
+
stop_requested = True
|
|
378
|
+
|
|
379
|
+
# Periodically update session state (every 30 seconds)
|
|
380
|
+
current_time = time.time()
|
|
381
|
+
if (current_time - last_state_update) > 30:
|
|
382
|
+
update_session_state()
|
|
383
|
+
last_state_update = current_time
|
|
384
|
+
|
|
243
385
|
# Sleep for a bit
|
|
244
386
|
time.sleep(POLL_INTERVAL)
|
|
245
387
|
|
|
246
388
|
except Exception as e:
|
|
247
|
-
print(
|
|
389
|
+
print("Error in command loop: {0}".format(e))
|
|
390
|
+
traceback.print_exc()
|
|
248
391
|
# Sleep for a bit
|
|
249
392
|
time.sleep(POLL_INTERVAL)
|
|
250
393
|
|
|
@@ -261,17 +404,18 @@ def main():
|
|
|
261
404
|
update_session_state()
|
|
262
405
|
|
|
263
406
|
# Start command loop
|
|
264
|
-
print(
|
|
407
|
+
print("{0}: CODESYS session started".format(SCRIPT_SUCCESS_MARKER))
|
|
265
408
|
command_loop()
|
|
266
409
|
|
|
267
410
|
# Update final state
|
|
268
411
|
update_session_state("stopped")
|
|
269
412
|
|
|
270
|
-
print(
|
|
413
|
+
print("{0}: CODESYS session stopped cleanly".format(SCRIPT_SUCCESS_MARKER))
|
|
271
414
|
sys.exit(0)
|
|
272
415
|
except Exception as e:
|
|
273
|
-
print(
|
|
274
|
-
print(
|
|
416
|
+
print("Error in session: {0}".format(e))
|
|
417
|
+
print("{0}: Session failed - {1}".format(SCRIPT_ERROR_MARKER, str(e)))
|
|
418
|
+
traceback.print_exc()
|
|
275
419
|
sys.exit(1)
|
|
276
420
|
|
|
277
421
|
if __name__ == "__main__":
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
import sys, os,
|
|
1
|
+
import sys, os, time, traceback
|
|
2
2
|
import tempfile
|
|
3
3
|
|
|
4
|
+
# Import JSON handling for IronPython 2.7 compatibility
|
|
5
|
+
try:
|
|
6
|
+
import json
|
|
7
|
+
except ImportError:
|
|
8
|
+
# For IronPython 2.7, we need to use simplejson instead
|
|
9
|
+
import simplejson as json
|
|
10
|
+
|
|
4
11
|
# Define markers for script results
|
|
5
12
|
SCRIPT_SUCCESS_MARKER = "SCRIPT_SUCCESS"
|
|
6
13
|
SCRIPT_ERROR_MARKER = "SCRIPT_ERROR"
|
|
@@ -17,40 +24,53 @@ def stop_session():
|
|
|
17
24
|
try:
|
|
18
25
|
# Check if session directory exists
|
|
19
26
|
if not os.path.exists(SESSION_DIR):
|
|
20
|
-
print(
|
|
27
|
+
print("{0}: No session directory found at {1}".format(SCRIPT_ERROR_MARKER, SESSION_DIR))
|
|
21
28
|
return False
|
|
22
29
|
|
|
23
30
|
# Check if session state file exists
|
|
24
31
|
if not os.path.exists(SESSION_STATE_FILE):
|
|
25
|
-
print(
|
|
32
|
+
print("{0}: No session state file found at {1}".format(SCRIPT_ERROR_MARKER, SESSION_STATE_FILE))
|
|
26
33
|
return False
|
|
27
34
|
|
|
28
35
|
# Read session state
|
|
29
|
-
|
|
30
|
-
|
|
36
|
+
try:
|
|
37
|
+
with open(SESSION_STATE_FILE, 'r') as f:
|
|
31
38
|
session_state = json.load(f)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
39
|
+
except json.JSONDecodeError:
|
|
40
|
+
print("{0}: Invalid session state file".format(SCRIPT_ERROR_MARKER))
|
|
41
|
+
return False
|
|
35
42
|
|
|
36
43
|
# Check if session is active
|
|
37
|
-
if not session_state.get('active', False):
|
|
38
|
-
print(
|
|
44
|
+
if not session_state.get('active', False) or session_state.get('status') == 'stopped':
|
|
45
|
+
print("{0}: Session is already stopped".format(SCRIPT_SUCCESS_MARKER))
|
|
39
46
|
return True
|
|
40
47
|
|
|
41
48
|
# Check if command file exists
|
|
42
49
|
if not os.path.exists(COMMAND_FILE):
|
|
43
|
-
print(
|
|
50
|
+
print("{0}: Command file not found at {1}".format(SCRIPT_ERROR_MARKER, COMMAND_FILE))
|
|
44
51
|
return False
|
|
45
52
|
|
|
53
|
+
# Try to read the lock file to check if session is still alive
|
|
54
|
+
lock_time = 0
|
|
55
|
+
try:
|
|
56
|
+
if os.path.exists(SESSION_LOCK_FILE):
|
|
57
|
+
with open(SESSION_LOCK_FILE, 'r') as f:
|
|
58
|
+
lock_time = float(f.read().strip())
|
|
59
|
+
|
|
60
|
+
current_time = time.time()
|
|
61
|
+
if (current_time - lock_time) > 30:
|
|
62
|
+
print("WARNING: Session lock file is stale ({0:.1f} seconds old). Session may have crashed.".format(current_time - lock_time))
|
|
63
|
+
except Exception as e:
|
|
64
|
+
print("Warning: Error reading lock file: {0}".format(e))
|
|
65
|
+
|
|
46
66
|
# Create stop command
|
|
47
67
|
stop_command = {
|
|
48
|
-
"id":
|
|
68
|
+
"id": "stop_{0}".format(int(time.time())),
|
|
49
69
|
"type": "stop",
|
|
50
70
|
"timestamp": time.time()
|
|
51
71
|
}
|
|
52
72
|
|
|
53
|
-
# Write command to file
|
|
73
|
+
# Write command to file with pretty-printing
|
|
54
74
|
command_data = {
|
|
55
75
|
"status": "pending",
|
|
56
76
|
"command": stop_command,
|
|
@@ -58,12 +78,12 @@ def stop_session():
|
|
|
58
78
|
}
|
|
59
79
|
|
|
60
80
|
with open(COMMAND_FILE, 'w') as f:
|
|
61
|
-
json.dump(command_data, f)
|
|
81
|
+
json.dump(command_data, f, indent=2)
|
|
62
82
|
|
|
63
|
-
print(
|
|
83
|
+
print("Stop command sent to session")
|
|
64
84
|
|
|
65
85
|
# Wait for result (with timeout)
|
|
66
|
-
timeout =
|
|
86
|
+
timeout = 15 # seconds (increased timeout for more reliability)
|
|
67
87
|
start_time = time.time()
|
|
68
88
|
result_received = False
|
|
69
89
|
|
|
@@ -81,32 +101,52 @@ def stop_session():
|
|
|
81
101
|
result = result_data.get("result")
|
|
82
102
|
if result.get('id') == stop_command['id']:
|
|
83
103
|
result_received = True
|
|
84
|
-
print(
|
|
104
|
+
print("Session stop result: {0}".format(result.get('output', 'No output')))
|
|
85
105
|
break
|
|
86
106
|
except Exception as e:
|
|
87
|
-
print(
|
|
107
|
+
print("Error reading result: {0}".format(e))
|
|
88
108
|
time.sleep(0.5)
|
|
89
109
|
|
|
90
|
-
# Update session state to indicate it's stopped
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
110
|
+
# Update session state to indicate it's stopped regardless of result
|
|
111
|
+
try:
|
|
112
|
+
with open(SESSION_STATE_FILE, 'r') as f:
|
|
113
|
+
session_state = json.load(f)
|
|
114
|
+
|
|
115
|
+
session_state['active'] = False
|
|
116
|
+
session_state['status'] = "stopped"
|
|
117
|
+
session_state['end_time'] = time.time()
|
|
97
118
|
|
|
98
|
-
|
|
99
|
-
|
|
119
|
+
with open(SESSION_STATE_FILE, 'w') as f:
|
|
120
|
+
json.dump(session_state, f, indent=2)
|
|
121
|
+
except Exception as e:
|
|
122
|
+
print("Warning: Failed to update session state file: {0}".format(e))
|
|
100
123
|
|
|
101
124
|
if result_received:
|
|
102
|
-
print(
|
|
125
|
+
print("{0}: Session stopped successfully".format(SCRIPT_SUCCESS_MARKER))
|
|
126
|
+
return True
|
|
103
127
|
else:
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
128
|
+
# If we didn't get a response, the session may be hung
|
|
129
|
+
# Force a cleanup by removing session files
|
|
130
|
+
print("Warning: Session did not respond to stop command. Forcing cleanup.")
|
|
131
|
+
try:
|
|
132
|
+
# Try to remove all session files to ensure a clean state for next session
|
|
133
|
+
for filename in os.listdir(SESSION_DIR):
|
|
134
|
+
filepath = os.path.join(SESSION_DIR, filename)
|
|
135
|
+
try:
|
|
136
|
+
if os.path.isfile(filepath):
|
|
137
|
+
os.unlink(filepath)
|
|
138
|
+
except Exception as e:
|
|
139
|
+
print("Error removing file {0}: {1}".format(filepath, e))
|
|
140
|
+
|
|
141
|
+
print("{0}: Session files cleaned up forcefully".format(SCRIPT_SUCCESS_MARKER))
|
|
142
|
+
return True
|
|
143
|
+
except Exception as e:
|
|
144
|
+
print("{0}: Failed to force cleanup - {1}".format(SCRIPT_ERROR_MARKER, str(e)))
|
|
145
|
+
return False
|
|
107
146
|
|
|
108
147
|
except Exception as e:
|
|
109
|
-
print(
|
|
148
|
+
print("{0}: Error stopping session - {1}".format(SCRIPT_ERROR_MARKER, str(e)))
|
|
149
|
+
traceback.print_exc()
|
|
110
150
|
return False
|
|
111
151
|
|
|
112
152
|
# 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
|
package/package.json
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
f"{SCRIPT_ERROR_MARKER}: Invalid session state file" | "{0}: Invalid session state file".format(SCRIPT_ERROR_MARKER)
|
|
2
|
+
f"{SCRIPT_SUCCESS_MARKER}: Session is not active (Status: {status})" | "{0}: Session is not active (Status: {1})".format(SCRIPT_SUCCESS_MARKER, status)
|
|
3
|
+
f"WARNING: Session lock file is stale by {time_diff:.1f} seconds" | "WARNING: Session lock file is stale by {0:.1f} seconds".format(time_diff)
|
|
4
|
+
f"Error reading lock file: {e}" | "Error reading result: {0}".format(e)
|
|
5
|
+
f"ping_{int(time.time())}" | "ping_{0}".format(int(time.time()))
|
|
6
|
+
f"WARNING: Session did not respond to ping within {timeout} seconds" | "WARNING: Session did not respond to ping within {0} seconds".format(timeout)
|
|
7
|
+
f"Error resetting command file: {e}" | "Error resetting command file: {0}".format(e)
|
|
8
|
+
f"Error during ping test: {e}" | "Error during ping test: {0}".format(e)
|
|
9
|
+
f", Project: {project_name} ({project_path})" | ", Project: {0} ({1})".format(project_name, project_path)
|
|
10
|
+
f"{uptime:.0f} seconds" | "{0:.0f} seconds".format(uptime)
|
|
11
|
+
f"{uptime/60:.1f} minutes" | "{0:.1f} minutes".format(uptime/60)
|
|
12
|
+
f"{SCRIPT_SUCCESS_MARKER}: Session is active (ID: {session_id}, Status: {status}, Uptime: {uptime_str}{project_info})" | "{0}: Session is active (ID: {1}, Status: {2}, Uptime: {3}{4})".format(SCRIPT_SUCCESS_MARKER, session_id, status, uptime_str, project_info)
|
|
13
|
+
f"{SCRIPT_SUCCESS_MARKER}: Session appears to be zombie (ID: {session_id}, Status: {status} but unresponsive)" | "{0}: Session appears to be zombie (ID: {1}, Status: {2} but unresponsive)".format(SCRIPT_SUCCESS_MARKER, session_id, status)
|
|
14
|
+
f"Error updating session state: {e}" | "Error updating session state: {0}".format(e)
|
|
15
|
+
f"{SCRIPT_SUCCESS_MARKER}: Session is not active (Status: {status})" | "{0}: Session is not active (Status: {1})".format(SCRIPT_SUCCESS_MARKER, status)
|
|
16
|
+
f"{SCRIPT_ERROR_MARKER}: Error checking session status - {str(e)}" | "{0}: Error checking session status - {1}".format(SCRIPT_ERROR_MARKER, str(e))
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Script to fix f-strings in Python files to make them compatible with IronPython 2.7.
|
|
4
|
+
Converts f-strings to str.format() calls.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
def process_file(filepath):
|
|
12
|
+
print(f"Processing {filepath}")
|
|
13
|
+
with open(filepath, 'r', encoding='utf-8') as f:
|
|
14
|
+
content = f.read()
|
|
15
|
+
|
|
16
|
+
# Find and replace f-strings
|
|
17
|
+
# This is a simplified approach - complex nested expressions might need manual review
|
|
18
|
+
pattern = r'f"([^"]*\{[^}]+\}[^"]*)"'
|
|
19
|
+
|
|
20
|
+
# Count potential matches
|
|
21
|
+
matches = re.findall(pattern, content)
|
|
22
|
+
print(f"Found {len(matches)} potential f-strings")
|
|
23
|
+
|
|
24
|
+
# Function to convert each match
|
|
25
|
+
def replace_fstring(match):
|
|
26
|
+
template_str = match.group(1)
|
|
27
|
+
# Replace {var} with {0}, {1}, etc.
|
|
28
|
+
var_pattern = r'\{([^}:]+)(?::([^}]+))?\}'
|
|
29
|
+
var_matches = re.finditer(var_pattern, template_str)
|
|
30
|
+
|
|
31
|
+
format_args = []
|
|
32
|
+
i = 0
|
|
33
|
+
|
|
34
|
+
new_template = template_str
|
|
35
|
+
for m in var_matches:
|
|
36
|
+
var_name = m.group(1)
|
|
37
|
+
format_spec = m.group(2)
|
|
38
|
+
|
|
39
|
+
if format_spec:
|
|
40
|
+
repl = "{" + str(i) + ":" + format_spec + "}"
|
|
41
|
+
else:
|
|
42
|
+
repl = "{" + str(i) + "}"
|
|
43
|
+
|
|
44
|
+
# Store the argument
|
|
45
|
+
format_args.append(var_name)
|
|
46
|
+
|
|
47
|
+
# Replace in the template
|
|
48
|
+
new_template = new_template.replace(m.group(0), repl, 1)
|
|
49
|
+
i += 1
|
|
50
|
+
|
|
51
|
+
# Build the complete format expression
|
|
52
|
+
args_str = ", ".join(format_args)
|
|
53
|
+
return f'"{new_template}".format({args_str})'
|
|
54
|
+
|
|
55
|
+
# Apply the replacements
|
|
56
|
+
new_content = re.sub(pattern, replace_fstring, content)
|
|
57
|
+
|
|
58
|
+
# Write the changes back
|
|
59
|
+
with open(filepath, 'w', encoding='utf-8') as f:
|
|
60
|
+
f.write(new_content)
|
|
61
|
+
|
|
62
|
+
print(f"Completed processing {filepath}")
|
|
63
|
+
|
|
64
|
+
def main():
|
|
65
|
+
if len(sys.argv) < 2:
|
|
66
|
+
print("Usage: python fix_fstrings.py <file_paths...>")
|
|
67
|
+
sys.exit(1)
|
|
68
|
+
|
|
69
|
+
for file_path in sys.argv[1:]:
|
|
70
|
+
if os.path.exists(file_path):
|
|
71
|
+
process_file(file_path)
|
|
72
|
+
else:
|
|
73
|
+
print(f"Error: File {file_path} not found")
|
|
74
|
+
|
|
75
|
+
if __name__ == "__main__":
|
|
76
|
+
main()
|