@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.
@@ -18,7 +18,9 @@
18
18
  "Bash(npm publish:*)",
19
19
  "Bash(npm publish:*)",
20
20
  "Bash(ls:*)",
21
- "Bash(ls:*)"
21
+ "Bash(ls:*)",
22
+ "Bash(mkdir:*)",
23
+ "Bash(mkdir:*)"
22
24
  ],
23
25
  "deny": []
24
26
  }
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, tempfile
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 file and pipes
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
- COMMAND_PIPE = os.path.join(SESSION_DIR, "command_pipe")
11
- RESULT_PIPE = os.path.join(SESSION_DIR, "result_pipe")
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 by reading the state file"""
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
- return {
19
- "session_exists": False,
20
- "active": False,
21
- "message": "No session directory found"
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
- return {
27
- "session_exists": True,
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
- def ping_session():
82
- """Attempt to ping the session with a simple command to verify it's responsive"""
83
- try:
84
- if not os.path.exists(COMMAND_PIPE) or not os.path.exists(RESULT_PIPE):
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
- ping_command = {
88
- "id": f"ping_{time.time()}",
89
- "type": "execute_script",
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
- if not ready:
108
- os.close(res_fd)
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 result_json:
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
- result = json.loads(result_json)
118
- return result.get("success", False)
119
- except json.JSONDecodeError:
120
- return False
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
- return False
123
-
124
- except Exception as e:
125
- print(f"Error pinging session: {e}")
126
- return False
127
-
128
- def main():
129
- try:
130
- # Get session status
131
- status = check_session_status()
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
- # Print status information
134
- print(f"Session Exists: {status.get('session_exists', False)}")
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
- # If session exists, print more details
138
- if status.get("session_exists", False):
139
- if status.get("active", False):
140
- # Try to ping the session
141
- is_responsive = ping_session()
142
- print(f"Session Responsive: {is_responsive}")
143
-
144
- # Print project information if available
145
- if "project_open" in status:
146
- print(f"Project Open: {status['project_open']}")
147
- if "project_name" in status:
148
- print(f"Project Name: {status['project_name']}")
149
- if "project_path" in status:
150
- print(f"Project Path: {status['project_path']}")
151
- if "duration" in status:
152
- print(f"Session Duration: {status['duration']}")
153
- if "last_update" in status:
154
- print(f"Last Activity: {status['last_update']}")
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 (stopped or crashed)")
175
+ print(f"{SCRIPT_SUCCESS_MARKER}: Session is not active (Status: {status})")
157
176
 
158
- # Print the final result marker
159
- print(f"{SCRIPT_SUCCESS_MARKER}: Session status check complete")
160
- sys.exit(0)
161
-
177
+ return False
178
+
162
179
  except Exception as e:
163
- print(f"Error checking session status: {e}")
164
- print(f"{SCRIPT_ERROR_MARKER}: {str(e)}")
165
- sys.exit(1)
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
- main()
186
+ if check_session_status():
187
+ sys.exit(0)
188
+ else:
189
+ sys.exit(1)