@codesys/mcp-toolkit 1.4.0 → 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -6
- package/dist/codesys_interop.js +1 -1
- package/dist/scripts/session_status.py +84 -19
- package/dist/scripts/start_session.py +274 -93
- package/dist/scripts/stop_session.py +89 -19
- package/dist/sessionManager.js +64 -9
- package/package.json +1 -1
- package/replacements_session_status.txt +16 -0
- package/scripts/fix_fstrings.py +76 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,14 +5,28 @@ 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.2] - 2025-05-02
|
|
9
|
+
### Fixed
|
|
10
|
+
- Enhanced session communication with better logging and debugging
|
|
11
|
+
- Improved command file handling and error recovery
|
|
12
|
+
- Fixed path handling for session files
|
|
13
|
+
- Added JSON fallback parser for IronPython 2.7
|
|
14
|
+
- Ensured synchronous script execution to prevent conflicts
|
|
15
|
+
- Increased reliability of session status detection
|
|
16
|
+
|
|
17
|
+
## [1.4.1] - 2025-05-02
|
|
18
|
+
### Fixed
|
|
19
|
+
- Fixed IronPython 2.7 compatibility for session management scripts
|
|
20
|
+
- Replaced f-strings with str.format() method calls for IronPython compatibility
|
|
21
|
+
- Added fallback import for json module (using simplejson for IronPython)
|
|
22
|
+
- Improved error handling for session scripts
|
|
23
|
+
- Re-enabled session management feature in codesys_interop.ts
|
|
24
|
+
|
|
8
25
|
## [1.4.0] - 2025-05-02
|
|
9
26
|
### Added
|
|
10
|
-
- Implemented
|
|
11
|
-
- Added
|
|
12
|
-
- Created
|
|
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
|
|
27
|
+
- Implemented initial framework for session management
|
|
28
|
+
- Added foundation for file-based IPC between Node.js and CODESYS instances
|
|
29
|
+
- Created documentation structure for session management capabilities
|
|
16
30
|
|
|
17
31
|
## [1.3.0] - 2025-05-02
|
|
18
32
|
### Changed
|
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,62 @@
|
|
|
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
|
+
try:
|
|
10
|
+
import simplejson as json
|
|
11
|
+
except ImportError:
|
|
12
|
+
print("Neither json nor simplejson could be imported. Check IronPython installation.")
|
|
13
|
+
# Provide a minimal fallback implementation for basic needs
|
|
14
|
+
class DummyJson:
|
|
15
|
+
@staticmethod
|
|
16
|
+
def load(file_obj):
|
|
17
|
+
content = file_obj.read()
|
|
18
|
+
# Very basic parsing for our limited needs
|
|
19
|
+
if content.strip().startswith("{") and content.strip().endswith("}"):
|
|
20
|
+
result = {}
|
|
21
|
+
parts = content.strip()[1:-1].split(",")
|
|
22
|
+
for part in parts:
|
|
23
|
+
if ":" in part:
|
|
24
|
+
k, v = part.split(":", 1)
|
|
25
|
+
k = k.strip().strip('"\'')
|
|
26
|
+
v = v.strip().strip('"\'')
|
|
27
|
+
result[k] = v
|
|
28
|
+
return result
|
|
29
|
+
return {}
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def loads(s):
|
|
33
|
+
if s.strip().startswith("{") and s.strip().endswith("}"):
|
|
34
|
+
result = {}
|
|
35
|
+
parts = s.strip()[1:-1].split(",")
|
|
36
|
+
for part in parts:
|
|
37
|
+
if ":" in part:
|
|
38
|
+
k, v = part.split(":", 1)
|
|
39
|
+
k = k.strip().strip('"\'')
|
|
40
|
+
v = v.strip().strip('"\'')
|
|
41
|
+
result[k] = v
|
|
42
|
+
return result
|
|
43
|
+
return {}
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def dump(obj, file_obj, indent=None):
|
|
47
|
+
if isinstance(obj, dict):
|
|
48
|
+
pairs = []
|
|
49
|
+
for k, v in obj.items():
|
|
50
|
+
if isinstance(v, str):
|
|
51
|
+
pairs.append('"{0}": "{1}"'.format(k, v))
|
|
52
|
+
else:
|
|
53
|
+
pairs.append('"{0}": {1}'.format(k, v))
|
|
54
|
+
file_obj.write("{" + ", ".join(pairs) + "}")
|
|
55
|
+
else:
|
|
56
|
+
file_obj.write(str(obj))
|
|
57
|
+
|
|
58
|
+
json = DummyJson()
|
|
59
|
+
|
|
4
60
|
# Define markers for script results
|
|
5
61
|
SCRIPT_SUCCESS_MARKER = "SCRIPT_SUCCESS"
|
|
6
62
|
SCRIPT_ERROR_MARKER = "SCRIPT_ERROR"
|
|
@@ -12,17 +68,26 @@ COMMAND_FILE = os.path.join(SESSION_DIR, "command.json")
|
|
|
12
68
|
RESULT_FILE = os.path.join(SESSION_DIR, "result.json")
|
|
13
69
|
SESSION_LOCK_FILE = os.path.join(SESSION_DIR, "session.lock")
|
|
14
70
|
|
|
71
|
+
# Print key information at start
|
|
72
|
+
print("Session status check script starting")
|
|
73
|
+
print("tempdir: {0}".format(tempfile.gettempdir()))
|
|
74
|
+
print("SESSION_DIR: {0}".format(SESSION_DIR))
|
|
75
|
+
print("SESSION_STATE_FILE: {0}".format(SESSION_STATE_FILE))
|
|
76
|
+
print("COMMAND_FILE: {0}".format(COMMAND_FILE))
|
|
77
|
+
print("RESULT_FILE: {0}".format(RESULT_FILE))
|
|
78
|
+
print("SESSION_LOCK_FILE: {0}".format(SESSION_LOCK_FILE))
|
|
79
|
+
|
|
15
80
|
def check_session_status():
|
|
16
81
|
"""Check the status of a CODESYS session"""
|
|
17
82
|
try:
|
|
18
83
|
# Check if session directory exists
|
|
19
84
|
if not os.path.exists(SESSION_DIR):
|
|
20
|
-
print(
|
|
85
|
+
print("{0}: No session active. Session directory not found.".format(SCRIPT_SUCCESS_MARKER))
|
|
21
86
|
return False
|
|
22
87
|
|
|
23
88
|
# Check if session state file exists
|
|
24
89
|
if not os.path.exists(SESSION_STATE_FILE):
|
|
25
|
-
print(
|
|
90
|
+
print("{0}: No session active. Session state file not found.".format(SCRIPT_SUCCESS_MARKER))
|
|
26
91
|
return False
|
|
27
92
|
|
|
28
93
|
# Read session state
|
|
@@ -30,7 +95,7 @@ def check_session_status():
|
|
|
30
95
|
with open(SESSION_STATE_FILE, 'r') as f:
|
|
31
96
|
session_state = json.load(f)
|
|
32
97
|
except json.JSONDecodeError:
|
|
33
|
-
print(
|
|
98
|
+
print("{0}: Invalid session state file".format(SCRIPT_ERROR_MARKER))
|
|
34
99
|
return False
|
|
35
100
|
|
|
36
101
|
# Check if session is active
|
|
@@ -39,7 +104,7 @@ def check_session_status():
|
|
|
39
104
|
|
|
40
105
|
# If the status indicates the session is not active, return immediately
|
|
41
106
|
if not active or status in ['stopped', 'error', 'stopping', 'timeout']:
|
|
42
|
-
print(
|
|
107
|
+
print("{0}: Session is not active (Status: {1})".format(SCRIPT_SUCCESS_MARKER, status))
|
|
43
108
|
return False
|
|
44
109
|
|
|
45
110
|
# Check session lock file to see if it's been updated recently
|
|
@@ -53,10 +118,10 @@ def check_session_status():
|
|
|
53
118
|
|
|
54
119
|
# If lock file hasn't been updated in 30 seconds, session might be dead
|
|
55
120
|
if time_diff > 30:
|
|
56
|
-
print(
|
|
121
|
+
print("WARNING: Session lock file is stale by {0:.1f} seconds".format(time_diff))
|
|
57
122
|
lock_is_stale = True
|
|
58
123
|
except Exception as e:
|
|
59
|
-
print(
|
|
124
|
+
print("Error reading lock file: {0}".format(e))
|
|
60
125
|
lock_is_stale = True
|
|
61
126
|
else:
|
|
62
127
|
print("WARNING: Session lock file not found")
|
|
@@ -74,7 +139,7 @@ def check_session_status():
|
|
|
74
139
|
|
|
75
140
|
# Create ping command
|
|
76
141
|
ping_command = {
|
|
77
|
-
"id":
|
|
142
|
+
"id": "ping_{0}".format(int(time.time())),
|
|
78
143
|
"type": "ping", # Use specific ping command type
|
|
79
144
|
"timestamp": time.time()
|
|
80
145
|
}
|
|
@@ -110,11 +175,11 @@ def check_session_status():
|
|
|
110
175
|
print("Ping successful, session is responsive")
|
|
111
176
|
break
|
|
112
177
|
except Exception as e:
|
|
113
|
-
print(
|
|
178
|
+
print("Error reading result: {0}".format(e))
|
|
114
179
|
time.sleep(0.5)
|
|
115
180
|
|
|
116
181
|
if not ping_success:
|
|
117
|
-
print(
|
|
182
|
+
print("WARNING: Session did not respond to ping within {0} seconds".format(timeout))
|
|
118
183
|
|
|
119
184
|
# Reset command file since ping failed
|
|
120
185
|
try:
|
|
@@ -129,11 +194,11 @@ def check_session_status():
|
|
|
129
194
|
|
|
130
195
|
print("Reset command file after failed ping")
|
|
131
196
|
except Exception as e:
|
|
132
|
-
print(
|
|
197
|
+
print("Error resetting command file: {0}".format(e))
|
|
133
198
|
else:
|
|
134
199
|
print("WARNING: Command file not found, cannot ping session")
|
|
135
200
|
except Exception as e:
|
|
136
|
-
print(
|
|
201
|
+
print("Error during ping test: {0}".format(e))
|
|
137
202
|
traceback.print_exc()
|
|
138
203
|
|
|
139
204
|
# Make final determination based on all checks
|
|
@@ -144,21 +209,21 @@ def check_session_status():
|
|
|
144
209
|
if session_state.get('project_open', False):
|
|
145
210
|
project_name = session_state.get('project_name', 'Unknown')
|
|
146
211
|
project_path = session_state.get('project_path', 'Unknown')
|
|
147
|
-
project_info =
|
|
212
|
+
project_info = ", Project: {0} ({1})".format(project_name, project_path)
|
|
148
213
|
|
|
149
214
|
# Include session info
|
|
150
215
|
session_id = session_state.get('session_id', 'Unknown')
|
|
151
216
|
start_time = session_state.get('start_time', 0)
|
|
152
217
|
uptime = time.time() - start_time if start_time else 0
|
|
153
|
-
uptime_str =
|
|
218
|
+
uptime_str = "{0:.0f} seconds".format(uptime) if uptime < 60 else "{0:.1f} minutes".format(uptime/60)
|
|
154
219
|
|
|
155
220
|
if session_active:
|
|
156
|
-
print(
|
|
221
|
+
print("{0}: Session is active (ID: {1}, Status: {2}, Uptime: {3}{4})".format(SCRIPT_SUCCESS_MARKER, session_id, status, uptime_str, project_info))
|
|
157
222
|
return True
|
|
158
223
|
else:
|
|
159
224
|
# If we get here, session may be in a state where files exist but process is dead
|
|
160
225
|
if active and status in ["running", "busy"] and lock_is_stale and not ping_success:
|
|
161
|
-
print(
|
|
226
|
+
print("{0}: Session appears to be zombie (ID: {1}, Status: {2} but unresponsive)".format(SCRIPT_SUCCESS_MARKER, session_id, status))
|
|
162
227
|
# Update session state to error so it will be considered inactive
|
|
163
228
|
try:
|
|
164
229
|
session_state['status'] = 'error'
|
|
@@ -170,14 +235,14 @@ def check_session_status():
|
|
|
170
235
|
|
|
171
236
|
print("Updated session state to error due to unresponsiveness")
|
|
172
237
|
except Exception as e:
|
|
173
|
-
print(
|
|
238
|
+
print("Error updating session state: {0}".format(e))
|
|
174
239
|
else:
|
|
175
|
-
print(
|
|
240
|
+
print("{0}: Session is not active (Status: {1})".format(SCRIPT_SUCCESS_MARKER, status))
|
|
176
241
|
|
|
177
242
|
return False
|
|
178
243
|
|
|
179
244
|
except Exception as e:
|
|
180
|
-
print(
|
|
245
|
+
print("{0}: Error checking session status - {1}".format(SCRIPT_ERROR_MARKER, str(e)))
|
|
181
246
|
traceback.print_exc()
|
|
182
247
|
return False
|
|
183
248
|
|
|
@@ -1,5 +1,74 @@
|
|
|
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
|
+
try:
|
|
9
|
+
import simplejson as json
|
|
10
|
+
except ImportError:
|
|
11
|
+
print("Neither json nor simplejson could be imported. Check IronPython installation.")
|
|
12
|
+
# Provide a minimal fallback implementation for basic needs
|
|
13
|
+
class DummyJson:
|
|
14
|
+
@staticmethod
|
|
15
|
+
def load(file_obj):
|
|
16
|
+
content = file_obj.read()
|
|
17
|
+
# Very basic parsing for our limited needs
|
|
18
|
+
if content.strip().startswith("{") and content.strip().endswith("}"):
|
|
19
|
+
result = {}
|
|
20
|
+
parts = content.strip()[1:-1].split(",")
|
|
21
|
+
for part in parts:
|
|
22
|
+
if ":" in part:
|
|
23
|
+
k, v = part.split(":", 1)
|
|
24
|
+
k = k.strip().strip('"\'')
|
|
25
|
+
v = v.strip().strip('"\'')
|
|
26
|
+
result[k] = v
|
|
27
|
+
return result
|
|
28
|
+
return {}
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def loads(s):
|
|
32
|
+
if s.strip().startswith("{") and s.strip().endswith("}"):
|
|
33
|
+
result = {}
|
|
34
|
+
parts = s.strip()[1:-1].split(",")
|
|
35
|
+
for part in parts:
|
|
36
|
+
if ":" in part:
|
|
37
|
+
k, v = part.split(":", 1)
|
|
38
|
+
k = k.strip().strip('"\'')
|
|
39
|
+
v = v.strip().strip('"\'')
|
|
40
|
+
result[k] = v
|
|
41
|
+
return result
|
|
42
|
+
return {}
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
def dump(obj, file_obj, indent=None):
|
|
46
|
+
if isinstance(obj, dict):
|
|
47
|
+
pairs = []
|
|
48
|
+
for k, v in obj.items():
|
|
49
|
+
if isinstance(v, str):
|
|
50
|
+
pairs.append('"{0}": "{1}"'.format(k, v))
|
|
51
|
+
else:
|
|
52
|
+
pairs.append('"{0}": {1}'.format(k, v))
|
|
53
|
+
file_obj.write("{" + ", ".join(pairs) + "}")
|
|
54
|
+
else:
|
|
55
|
+
file_obj.write(str(obj))
|
|
56
|
+
|
|
57
|
+
json = DummyJson()
|
|
58
|
+
|
|
59
|
+
# Import scriptengine
|
|
60
|
+
try:
|
|
61
|
+
import scriptengine as script_engine
|
|
62
|
+
print("Successfully imported scriptengine module")
|
|
63
|
+
except ImportError:
|
|
64
|
+
# Log the error and continue - we'll handle this gracefully
|
|
65
|
+
print("Error importing scriptengine. This script must be run within CODESYS environment.")
|
|
66
|
+
script_engine = None
|
|
67
|
+
|
|
68
|
+
# Print Python and platform info
|
|
69
|
+
print("Python version: {0}.{1}.{2}".format(sys.version_info[0], sys.version_info[1], sys.version_info[2]))
|
|
70
|
+
print("Platform: {0}".format(sys.platform))
|
|
71
|
+
print("Executable: {0}".format(sys.executable))
|
|
3
72
|
|
|
4
73
|
# Define markers for script results
|
|
5
74
|
SCRIPT_SUCCESS_MARKER = "SCRIPT_SUCCESS"
|
|
@@ -25,7 +94,7 @@ def setup_session_dir():
|
|
|
25
94
|
try:
|
|
26
95
|
with open(SESSION_STATE_FILE, 'r') as f:
|
|
27
96
|
old_state = json.load(f)
|
|
28
|
-
print(
|
|
97
|
+
print("Cleaning up previous session (ID: {0})".format(old_state.get('session_id', 'unknown')))
|
|
29
98
|
except:
|
|
30
99
|
pass
|
|
31
100
|
|
|
@@ -36,9 +105,9 @@ def setup_session_dir():
|
|
|
36
105
|
if os.path.isfile(filepath):
|
|
37
106
|
os.unlink(filepath)
|
|
38
107
|
except Exception as e:
|
|
39
|
-
print(
|
|
108
|
+
print("Error removing file {0}: {1}".format(filepath, e))
|
|
40
109
|
except Exception as e:
|
|
41
|
-
print(
|
|
110
|
+
print("Error cleaning up previous session: {0}".format(e))
|
|
42
111
|
|
|
43
112
|
# Create session directory
|
|
44
113
|
if not os.path.exists(SESSION_DIR):
|
|
@@ -74,12 +143,12 @@ def setup_session_dir():
|
|
|
74
143
|
with open(SESSION_LOCK_FILE, 'w') as f:
|
|
75
144
|
f.write(str(time.time()))
|
|
76
145
|
|
|
77
|
-
print(
|
|
78
|
-
print(
|
|
146
|
+
print("Session directory setup at: {0}".format(SESSION_DIR))
|
|
147
|
+
print("Session ID: {0}".format(session_id))
|
|
79
148
|
return True
|
|
80
149
|
except Exception as e:
|
|
81
|
-
print(
|
|
82
|
-
print(
|
|
150
|
+
print("Failed to set up session directory: {0}".format(e))
|
|
151
|
+
print("{0}: Session setup failed - {1}".format(SCRIPT_ERROR_MARKER, str(e)))
|
|
83
152
|
return False
|
|
84
153
|
|
|
85
154
|
def update_session_state(status="running"):
|
|
@@ -90,20 +159,29 @@ def update_session_state(status="running"):
|
|
|
90
159
|
project_name = "No project open"
|
|
91
160
|
project_path = "N/A"
|
|
92
161
|
|
|
93
|
-
|
|
94
|
-
if
|
|
95
|
-
|
|
162
|
+
# Check if script_engine was properly imported
|
|
163
|
+
if script_engine is None:
|
|
164
|
+
print("Warning: scriptengine not available, cannot get project info")
|
|
165
|
+
else:
|
|
166
|
+
# Try to get primary project
|
|
96
167
|
try:
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
168
|
+
primary_project = script_engine.projects.primary
|
|
169
|
+
if primary_project:
|
|
170
|
+
project_open = True
|
|
171
|
+
try:
|
|
172
|
+
project_path = os.path.normcase(os.path.abspath(primary_project.path))
|
|
173
|
+
try:
|
|
174
|
+
project_name = primary_project.get_name()
|
|
175
|
+
if not project_name:
|
|
176
|
+
project_name = "Unnamed (path: {0})".format(os.path.basename(project_path))
|
|
177
|
+
except:
|
|
178
|
+
project_name = "Unnamed (path: {0})".format(os.path.basename(project_path))
|
|
179
|
+
except Exception as e_path:
|
|
180
|
+
project_path = "N/A (Error: {0})".format(str(e_path))
|
|
181
|
+
project_name = "Unnamed (Path Error)"
|
|
182
|
+
except Exception as e:
|
|
183
|
+
print("Error accessing script_engine.projects: {0}".format(str(e)))
|
|
184
|
+
traceback.print_exc()
|
|
107
185
|
|
|
108
186
|
# Read current state file
|
|
109
187
|
try:
|
|
@@ -133,7 +211,7 @@ def update_session_state(status="running"):
|
|
|
133
211
|
|
|
134
212
|
return True
|
|
135
213
|
except Exception as e:
|
|
136
|
-
print(
|
|
214
|
+
print("Failed to update session state: {0}".format(e))
|
|
137
215
|
traceback.print_exc()
|
|
138
216
|
return False
|
|
139
217
|
|
|
@@ -189,13 +267,13 @@ def execute_command(command_data):
|
|
|
189
267
|
except Exception as e:
|
|
190
268
|
traceback.print_exc()
|
|
191
269
|
result["success"] = False
|
|
192
|
-
result["error"] =
|
|
193
|
-
result["output"] =
|
|
270
|
+
result["error"] = "Error executing script: {0}\n{1}".format(str(e), traceback.format_exc())
|
|
271
|
+
result["output"] = "{0}: {1}".format(SCRIPT_ERROR_MARKER, str(e))
|
|
194
272
|
|
|
195
273
|
elif cmd_type == "stop":
|
|
196
274
|
# Stop command received
|
|
197
275
|
result["success"] = True
|
|
198
|
-
result["output"] =
|
|
276
|
+
result["output"] = "{0}: Session stopping".format(SCRIPT_SUCCESS_MARKER)
|
|
199
277
|
|
|
200
278
|
# Update session state to mark as stopping
|
|
201
279
|
update_session_state("stopping")
|
|
@@ -206,12 +284,12 @@ def execute_command(command_data):
|
|
|
206
284
|
elif cmd_type == "ping":
|
|
207
285
|
# Simple ping command to check if session is alive
|
|
208
286
|
result["success"] = True
|
|
209
|
-
result["output"] =
|
|
287
|
+
result["output"] = "{0}: Session is active".format(SCRIPT_SUCCESS_MARKER)
|
|
210
288
|
|
|
211
289
|
else:
|
|
212
290
|
result["success"] = False
|
|
213
|
-
result["error"] =
|
|
214
|
-
result["output"] =
|
|
291
|
+
result["error"] = "Unknown command type: {0}".format(cmd_type)
|
|
292
|
+
result["output"] = "{0}: Unknown command type: {1}".format(SCRIPT_ERROR_MARKER, cmd_type)
|
|
215
293
|
|
|
216
294
|
# Update session state after command execution
|
|
217
295
|
update_session_state()
|
|
@@ -223,8 +301,8 @@ def execute_command(command_data):
|
|
|
223
301
|
return {
|
|
224
302
|
"id": command_data.get("id", "unknown"),
|
|
225
303
|
"success": False,
|
|
226
|
-
"output":
|
|
227
|
-
"error":
|
|
304
|
+
"output": "{0}: Command execution failed".format(SCRIPT_ERROR_MARKER),
|
|
305
|
+
"error": "{0}\n{1}".format(str(e), traceback.format_exc())
|
|
228
306
|
}, False
|
|
229
307
|
|
|
230
308
|
def check_command():
|
|
@@ -232,67 +310,128 @@ def check_command():
|
|
|
232
310
|
try:
|
|
233
311
|
# If command file doesn't exist, return immediately
|
|
234
312
|
if not os.path.exists(COMMAND_FILE):
|
|
313
|
+
print("Command file does not exist at {0}".format(COMMAND_FILE))
|
|
235
314
|
return False
|
|
236
315
|
|
|
237
316
|
# Read the command file
|
|
238
317
|
try:
|
|
318
|
+
print("Reading command file: {0}".format(COMMAND_FILE))
|
|
239
319
|
with open(COMMAND_FILE, 'r') as f:
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
320
|
+
file_content = f.read()
|
|
321
|
+
print("Raw command file content: {0}".format(file_content[:100] if len(file_content) > 100 else file_content))
|
|
322
|
+
|
|
323
|
+
# Check if file is empty
|
|
324
|
+
if not file_content.strip():
|
|
325
|
+
print("Command file is empty, resetting")
|
|
326
|
+
command_data = {"status": "ready", "command": None, "timestamp": time.time()}
|
|
327
|
+
with open(COMMAND_FILE, 'w') as fw:
|
|
328
|
+
json.dump(command_data, fw, indent=2)
|
|
329
|
+
return False
|
|
330
|
+
|
|
331
|
+
# Try to parse JSON
|
|
332
|
+
try:
|
|
333
|
+
command_data = json.loads(file_content)
|
|
334
|
+
except Exception as json_err:
|
|
335
|
+
print("Error parsing command file JSON: {0}".format(json_err))
|
|
336
|
+
# If command file is corrupted, reset it
|
|
337
|
+
command_data = {"status": "ready", "command": None, "timestamp": time.time()}
|
|
338
|
+
with open(COMMAND_FILE, 'w') as fw:
|
|
339
|
+
json.dump(command_data, fw, indent=2)
|
|
340
|
+
return False
|
|
341
|
+
except Exception as read_err:
|
|
342
|
+
print("Error reading command file: {0}".format(read_err))
|
|
343
|
+
# Reset the command file
|
|
243
344
|
command_data = {"status": "ready", "command": None, "timestamp": time.time()}
|
|
244
|
-
|
|
245
|
-
|
|
345
|
+
try:
|
|
346
|
+
with open(COMMAND_FILE, 'w') as fw:
|
|
347
|
+
json.dump(command_data, fw, indent=2)
|
|
348
|
+
except Exception as write_err:
|
|
349
|
+
print("Error writing reset command file: {0}".format(write_err))
|
|
246
350
|
return False
|
|
247
351
|
|
|
248
352
|
# Check if there's a new command
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
with open(COMMAND_FILE, 'w') as f:
|
|
256
|
-
json.dump(command_data, f, indent=2)
|
|
257
|
-
|
|
258
|
-
# Update session state to busy
|
|
259
|
-
update_session_state("busy")
|
|
260
|
-
|
|
261
|
-
# Execute the command
|
|
262
|
-
result, stop_requested = execute_command(command)
|
|
263
|
-
|
|
264
|
-
# Write the result
|
|
265
|
-
result_data = {
|
|
266
|
-
"status": "complete",
|
|
267
|
-
"result": result,
|
|
268
|
-
"timestamp": time.time()
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
with open(RESULT_FILE, 'w') as f:
|
|
272
|
-
json.dump(result_data, f, indent=2)
|
|
273
|
-
|
|
274
|
-
# Reset command file
|
|
275
|
-
command_data = {
|
|
276
|
-
"status": "ready",
|
|
277
|
-
"command": None,
|
|
278
|
-
"timestamp": time.time()
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
with open(COMMAND_FILE, 'w') as 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")
|
|
353
|
+
status = command_data.get("status")
|
|
354
|
+
command = command_data.get("command")
|
|
355
|
+
|
|
356
|
+
print("Command status: {0}".format(status))
|
|
357
|
+
if command:
|
|
358
|
+
print("Command found: {0}".format(command.get("type", "unknown")))
|
|
287
359
|
|
|
288
|
-
|
|
289
|
-
|
|
360
|
+
if status == "pending" and command:
|
|
361
|
+
print("Processing pending command...")
|
|
290
362
|
|
|
291
|
-
|
|
363
|
+
try:
|
|
364
|
+
# Mark as processing
|
|
365
|
+
command_data["status"] = "processing"
|
|
366
|
+
with open(COMMAND_FILE, 'w') as f:
|
|
367
|
+
json.dump(command_data, f, indent=2)
|
|
368
|
+
print("Updated command status to 'processing'")
|
|
369
|
+
|
|
370
|
+
# Update session state to busy
|
|
371
|
+
update_session_state("busy")
|
|
372
|
+
|
|
373
|
+
# Execute the command
|
|
374
|
+
print("Executing command: {0}".format(command.get("type", "unknown")))
|
|
375
|
+
result, stop_requested = execute_command(command)
|
|
376
|
+
print("Command execution complete, stop_requested: {0}".format(stop_requested))
|
|
377
|
+
|
|
378
|
+
# Write the result
|
|
379
|
+
result_data = {
|
|
380
|
+
"status": "complete",
|
|
381
|
+
"result": result,
|
|
382
|
+
"timestamp": time.time()
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
print("Writing result to {0}".format(RESULT_FILE))
|
|
386
|
+
with open(RESULT_FILE, 'w') as f:
|
|
387
|
+
json.dump(result_data, f, indent=2)
|
|
388
|
+
|
|
389
|
+
# Reset command file
|
|
390
|
+
command_data = {
|
|
391
|
+
"status": "ready",
|
|
392
|
+
"command": None,
|
|
393
|
+
"timestamp": time.time()
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
print("Resetting command file")
|
|
397
|
+
with open(COMMAND_FILE, 'w') as f:
|
|
398
|
+
json.dump(command_data, f, indent=2)
|
|
399
|
+
|
|
400
|
+
# Update session state to running again
|
|
401
|
+
if not stop_requested:
|
|
402
|
+
update_session_state("running")
|
|
403
|
+
|
|
404
|
+
# Update last activity time
|
|
405
|
+
update_last_activity()
|
|
406
|
+
|
|
407
|
+
return stop_requested
|
|
408
|
+
except Exception as cmd_err:
|
|
409
|
+
print("Error processing command: {0}".format(cmd_err))
|
|
410
|
+
traceback.print_exc()
|
|
411
|
+
|
|
412
|
+
# Try to reset the command file
|
|
413
|
+
try:
|
|
414
|
+
command_data = {
|
|
415
|
+
"status": "ready",
|
|
416
|
+
"command": None,
|
|
417
|
+
"timestamp": time.time()
|
|
418
|
+
}
|
|
419
|
+
with open(COMMAND_FILE, 'w') as f:
|
|
420
|
+
json.dump(command_data, f, indent=2)
|
|
421
|
+
except Exception as reset_err:
|
|
422
|
+
print("Error resetting command file: {0}".format(reset_err))
|
|
423
|
+
|
|
424
|
+
# Try to restore session state
|
|
425
|
+
try:
|
|
426
|
+
update_session_state("running")
|
|
427
|
+
except Exception as state_err:
|
|
428
|
+
print("Error updating session state: {0}".format(state_err))
|
|
429
|
+
|
|
430
|
+
return False
|
|
292
431
|
|
|
293
432
|
return False
|
|
294
433
|
except Exception as e:
|
|
295
|
-
print(
|
|
434
|
+
print("Error in check_command: {0}".format(e))
|
|
296
435
|
traceback.print_exc()
|
|
297
436
|
return False
|
|
298
437
|
|
|
@@ -308,7 +447,7 @@ def update_last_activity():
|
|
|
308
447
|
with open(SESSION_STATE_FILE, 'w') as f:
|
|
309
448
|
json.dump(state, f, indent=2)
|
|
310
449
|
except Exception as e:
|
|
311
|
-
print(
|
|
450
|
+
print("Error updating last activity: {0}".format(e))
|
|
312
451
|
|
|
313
452
|
def check_inactivity_timeout():
|
|
314
453
|
"""Check if session has been inactive for too long"""
|
|
@@ -321,31 +460,73 @@ def check_inactivity_timeout():
|
|
|
321
460
|
current_time = time.time()
|
|
322
461
|
|
|
323
462
|
if (current_time - last_activity) > MAX_INACTIVITY_TIME:
|
|
324
|
-
print(
|
|
463
|
+
print("Session inactive for {0:.1f} seconds, shutting down".format(current_time - last_activity))
|
|
325
464
|
return True
|
|
326
465
|
|
|
327
466
|
return False
|
|
328
467
|
except Exception as e:
|
|
329
|
-
print(
|
|
468
|
+
print("Error checking inactivity timeout: {0}".format(e))
|
|
330
469
|
return False
|
|
331
470
|
|
|
332
471
|
def command_loop():
|
|
333
472
|
"""Main loop to check for and execute commands"""
|
|
334
473
|
stop_requested = False
|
|
335
474
|
last_state_update = time.time()
|
|
475
|
+
loop_count = 0
|
|
476
|
+
last_heartbeat = time.time()
|
|
336
477
|
|
|
337
|
-
print(
|
|
338
|
-
print(
|
|
339
|
-
print(
|
|
478
|
+
print("Starting command loop...")
|
|
479
|
+
print("Command file: {0}".format(COMMAND_FILE))
|
|
480
|
+
print("Result file: {0}".format(RESULT_FILE))
|
|
481
|
+
print("Session lock file: {0}".format(SESSION_LOCK_FILE))
|
|
482
|
+
print("Session state file: {0}".format(SESSION_STATE_FILE))
|
|
483
|
+
print("Poll interval: {0} seconds".format(POLL_INTERVAL))
|
|
484
|
+
print("Max inactivity time: {0} seconds".format(MAX_INACTIVITY_TIME))
|
|
485
|
+
|
|
486
|
+
# Set up initial session state files
|
|
487
|
+
try:
|
|
488
|
+
# Create initial command file if it doesn't exist
|
|
489
|
+
if not os.path.exists(COMMAND_FILE):
|
|
490
|
+
with open(COMMAND_FILE, 'w') as f:
|
|
491
|
+
json.dump({
|
|
492
|
+
"status": "ready",
|
|
493
|
+
"command": None,
|
|
494
|
+
"timestamp": time.time()
|
|
495
|
+
}, f, indent=2)
|
|
496
|
+
print("Created initial command file")
|
|
497
|
+
|
|
498
|
+
# Create initial result file if it doesn't exist
|
|
499
|
+
if not os.path.exists(RESULT_FILE):
|
|
500
|
+
with open(RESULT_FILE, 'w') as f:
|
|
501
|
+
json.dump({
|
|
502
|
+
"status": "none",
|
|
503
|
+
"result": None,
|
|
504
|
+
"timestamp": time.time()
|
|
505
|
+
}, f, indent=2)
|
|
506
|
+
print("Created initial result file")
|
|
507
|
+
except Exception as setup_err:
|
|
508
|
+
print("Error setting up initial files: {0}".format(setup_err))
|
|
509
|
+
traceback.print_exc()
|
|
340
510
|
|
|
341
511
|
while not stop_requested:
|
|
342
512
|
try:
|
|
513
|
+
loop_count += 1
|
|
514
|
+
current_time = time.time()
|
|
515
|
+
|
|
343
516
|
# Update session lock file to indicate we're alive
|
|
344
517
|
with open(SESSION_LOCK_FILE, 'w') as f:
|
|
345
|
-
f.write(str(
|
|
518
|
+
f.write(str(current_time))
|
|
519
|
+
|
|
520
|
+
# Print heartbeat message every 60 seconds
|
|
521
|
+
if current_time - last_heartbeat > 60:
|
|
522
|
+
print("Command loop heartbeat - still alive (iteration {0})".format(loop_count))
|
|
523
|
+
last_heartbeat = current_time
|
|
346
524
|
|
|
347
525
|
# Check for new commands
|
|
526
|
+
print("Checking for commands...")
|
|
348
527
|
stop_requested = check_command()
|
|
528
|
+
if stop_requested:
|
|
529
|
+
print("Stop was requested by a command")
|
|
349
530
|
|
|
350
531
|
# Check for inactivity timeout
|
|
351
532
|
if check_inactivity_timeout():
|
|
@@ -354,8 +535,8 @@ def command_loop():
|
|
|
354
535
|
stop_requested = True
|
|
355
536
|
|
|
356
537
|
# Periodically update session state (every 30 seconds)
|
|
357
|
-
current_time = time.time()
|
|
358
538
|
if (current_time - last_state_update) > 30:
|
|
539
|
+
print("Periodic session state update")
|
|
359
540
|
update_session_state()
|
|
360
541
|
last_state_update = current_time
|
|
361
542
|
|
|
@@ -363,12 +544,12 @@ def command_loop():
|
|
|
363
544
|
time.sleep(POLL_INTERVAL)
|
|
364
545
|
|
|
365
546
|
except Exception as e:
|
|
366
|
-
print(
|
|
547
|
+
print("Error in command loop: {0}".format(e))
|
|
367
548
|
traceback.print_exc()
|
|
368
|
-
# Sleep for a bit
|
|
549
|
+
# Sleep for a bit before retrying
|
|
369
550
|
time.sleep(POLL_INTERVAL)
|
|
370
551
|
|
|
371
|
-
print("Command loop stopped.
|
|
552
|
+
print("Command loop stopped after {0} iterations".format(loop_count))
|
|
372
553
|
|
|
373
554
|
# Main script
|
|
374
555
|
def main():
|
|
@@ -381,17 +562,17 @@ def main():
|
|
|
381
562
|
update_session_state()
|
|
382
563
|
|
|
383
564
|
# Start command loop
|
|
384
|
-
print(
|
|
565
|
+
print("{0}: CODESYS session started".format(SCRIPT_SUCCESS_MARKER))
|
|
385
566
|
command_loop()
|
|
386
567
|
|
|
387
568
|
# Update final state
|
|
388
569
|
update_session_state("stopped")
|
|
389
570
|
|
|
390
|
-
print(
|
|
571
|
+
print("{0}: CODESYS session stopped cleanly".format(SCRIPT_SUCCESS_MARKER))
|
|
391
572
|
sys.exit(0)
|
|
392
573
|
except Exception as e:
|
|
393
|
-
print(
|
|
394
|
-
print(
|
|
574
|
+
print("Error in session: {0}".format(e))
|
|
575
|
+
print("{0}: Session failed - {1}".format(SCRIPT_ERROR_MARKER, str(e)))
|
|
395
576
|
traceback.print_exc()
|
|
396
577
|
sys.exit(1)
|
|
397
578
|
|
|
@@ -1,6 +1,67 @@
|
|
|
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
|
+
try:
|
|
10
|
+
import simplejson as json
|
|
11
|
+
except ImportError:
|
|
12
|
+
print("Neither json nor simplejson could be imported. Check IronPython installation.")
|
|
13
|
+
# Provide a minimal fallback implementation for basic needs
|
|
14
|
+
class DummyJson:
|
|
15
|
+
@staticmethod
|
|
16
|
+
def load(file_obj):
|
|
17
|
+
content = file_obj.read()
|
|
18
|
+
# Very basic parsing for our limited needs
|
|
19
|
+
if content.strip().startswith("{") and content.strip().endswith("}"):
|
|
20
|
+
result = {}
|
|
21
|
+
parts = content.strip()[1:-1].split(",")
|
|
22
|
+
for part in parts:
|
|
23
|
+
if ":" in part:
|
|
24
|
+
k, v = part.split(":", 1)
|
|
25
|
+
k = k.strip().strip('"\'')
|
|
26
|
+
v = v.strip().strip('"\'')
|
|
27
|
+
result[k] = v
|
|
28
|
+
return result
|
|
29
|
+
return {}
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def loads(s):
|
|
33
|
+
if s.strip().startswith("{") and s.strip().endswith("}"):
|
|
34
|
+
result = {}
|
|
35
|
+
parts = s.strip()[1:-1].split(",")
|
|
36
|
+
for part in parts:
|
|
37
|
+
if ":" in part:
|
|
38
|
+
k, v = part.split(":", 1)
|
|
39
|
+
k = k.strip().strip('"\'')
|
|
40
|
+
v = v.strip().strip('"\'')
|
|
41
|
+
result[k] = v
|
|
42
|
+
return result
|
|
43
|
+
return {}
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def dump(obj, file_obj, indent=None):
|
|
47
|
+
if isinstance(obj, dict):
|
|
48
|
+
pairs = []
|
|
49
|
+
for k, v in obj.items():
|
|
50
|
+
if isinstance(v, str):
|
|
51
|
+
pairs.append('"{0}": "{1}"'.format(k, v))
|
|
52
|
+
else:
|
|
53
|
+
pairs.append('"{0}": {1}'.format(k, v))
|
|
54
|
+
file_obj.write("{" + ", ".join(pairs) + "}")
|
|
55
|
+
else:
|
|
56
|
+
file_obj.write(str(obj))
|
|
57
|
+
|
|
58
|
+
json = DummyJson()
|
|
59
|
+
|
|
60
|
+
# Print Python and platform info
|
|
61
|
+
print("Python version: {0}.{1}.{2}".format(sys.version_info[0], sys.version_info[1], sys.version_info[2]))
|
|
62
|
+
print("Platform: {0}".format(sys.platform))
|
|
63
|
+
print("Executable: {0}".format(sys.executable))
|
|
64
|
+
|
|
4
65
|
# Define markers for script results
|
|
5
66
|
SCRIPT_SUCCESS_MARKER = "SCRIPT_SUCCESS"
|
|
6
67
|
SCRIPT_ERROR_MARKER = "SCRIPT_ERROR"
|
|
@@ -12,17 +73,26 @@ COMMAND_FILE = os.path.join(SESSION_DIR, "command.json")
|
|
|
12
73
|
RESULT_FILE = os.path.join(SESSION_DIR, "result.json")
|
|
13
74
|
SESSION_LOCK_FILE = os.path.join(SESSION_DIR, "session.lock")
|
|
14
75
|
|
|
76
|
+
# Print key information at start
|
|
77
|
+
print("Session stop script starting")
|
|
78
|
+
print("tempdir: {0}".format(tempfile.gettempdir()))
|
|
79
|
+
print("SESSION_DIR: {0}".format(SESSION_DIR))
|
|
80
|
+
print("SESSION_STATE_FILE: {0}".format(SESSION_STATE_FILE))
|
|
81
|
+
print("COMMAND_FILE: {0}".format(COMMAND_FILE))
|
|
82
|
+
print("RESULT_FILE: {0}".format(RESULT_FILE))
|
|
83
|
+
print("SESSION_LOCK_FILE: {0}".format(SESSION_LOCK_FILE))
|
|
84
|
+
|
|
15
85
|
def stop_session():
|
|
16
86
|
"""Stop a running CODESYS session"""
|
|
17
87
|
try:
|
|
18
88
|
# Check if session directory exists
|
|
19
89
|
if not os.path.exists(SESSION_DIR):
|
|
20
|
-
print(
|
|
90
|
+
print("{0}: No session directory found at {1}".format(SCRIPT_ERROR_MARKER, SESSION_DIR))
|
|
21
91
|
return False
|
|
22
92
|
|
|
23
93
|
# Check if session state file exists
|
|
24
94
|
if not os.path.exists(SESSION_STATE_FILE):
|
|
25
|
-
print(
|
|
95
|
+
print("{0}: No session state file found at {1}".format(SCRIPT_ERROR_MARKER, SESSION_STATE_FILE))
|
|
26
96
|
return False
|
|
27
97
|
|
|
28
98
|
# Read session state
|
|
@@ -30,17 +100,17 @@ def stop_session():
|
|
|
30
100
|
with open(SESSION_STATE_FILE, 'r') as f:
|
|
31
101
|
session_state = json.load(f)
|
|
32
102
|
except json.JSONDecodeError:
|
|
33
|
-
print(
|
|
103
|
+
print("{0}: Invalid session state file".format(SCRIPT_ERROR_MARKER))
|
|
34
104
|
return False
|
|
35
105
|
|
|
36
106
|
# Check if session is active
|
|
37
107
|
if not session_state.get('active', False) or session_state.get('status') == 'stopped':
|
|
38
|
-
print(
|
|
108
|
+
print("{0}: Session is already stopped".format(SCRIPT_SUCCESS_MARKER))
|
|
39
109
|
return True
|
|
40
110
|
|
|
41
111
|
# Check if command file exists
|
|
42
112
|
if not os.path.exists(COMMAND_FILE):
|
|
43
|
-
print(
|
|
113
|
+
print("{0}: Command file not found at {1}".format(SCRIPT_ERROR_MARKER, COMMAND_FILE))
|
|
44
114
|
return False
|
|
45
115
|
|
|
46
116
|
# Try to read the lock file to check if session is still alive
|
|
@@ -52,13 +122,13 @@ def stop_session():
|
|
|
52
122
|
|
|
53
123
|
current_time = time.time()
|
|
54
124
|
if (current_time - lock_time) > 30:
|
|
55
|
-
print(
|
|
125
|
+
print("WARNING: Session lock file is stale ({0:.1f} seconds old). Session may have crashed.".format(current_time - lock_time))
|
|
56
126
|
except Exception as e:
|
|
57
|
-
print(
|
|
127
|
+
print("Warning: Error reading lock file: {0}".format(e))
|
|
58
128
|
|
|
59
129
|
# Create stop command
|
|
60
130
|
stop_command = {
|
|
61
|
-
"id":
|
|
131
|
+
"id": "stop_{0}".format(int(time.time())),
|
|
62
132
|
"type": "stop",
|
|
63
133
|
"timestamp": time.time()
|
|
64
134
|
}
|
|
@@ -73,7 +143,7 @@ def stop_session():
|
|
|
73
143
|
with open(COMMAND_FILE, 'w') as f:
|
|
74
144
|
json.dump(command_data, f, indent=2)
|
|
75
145
|
|
|
76
|
-
print(
|
|
146
|
+
print("Stop command sent to session")
|
|
77
147
|
|
|
78
148
|
# Wait for result (with timeout)
|
|
79
149
|
timeout = 15 # seconds (increased timeout for more reliability)
|
|
@@ -94,10 +164,10 @@ def stop_session():
|
|
|
94
164
|
result = result_data.get("result")
|
|
95
165
|
if result.get('id') == stop_command['id']:
|
|
96
166
|
result_received = True
|
|
97
|
-
print(
|
|
167
|
+
print("Session stop result: {0}".format(result.get('output', 'No output')))
|
|
98
168
|
break
|
|
99
169
|
except Exception as e:
|
|
100
|
-
print(
|
|
170
|
+
print("Error reading result: {0}".format(e))
|
|
101
171
|
time.sleep(0.5)
|
|
102
172
|
|
|
103
173
|
# Update session state to indicate it's stopped regardless of result
|
|
@@ -112,15 +182,15 @@ def stop_session():
|
|
|
112
182
|
with open(SESSION_STATE_FILE, 'w') as f:
|
|
113
183
|
json.dump(session_state, f, indent=2)
|
|
114
184
|
except Exception as e:
|
|
115
|
-
print(
|
|
185
|
+
print("Warning: Failed to update session state file: {0}".format(e))
|
|
116
186
|
|
|
117
187
|
if result_received:
|
|
118
|
-
print(
|
|
188
|
+
print("{0}: Session stopped successfully".format(SCRIPT_SUCCESS_MARKER))
|
|
119
189
|
return True
|
|
120
190
|
else:
|
|
121
191
|
# If we didn't get a response, the session may be hung
|
|
122
192
|
# Force a cleanup by removing session files
|
|
123
|
-
print(
|
|
193
|
+
print("Warning: Session did not respond to stop command. Forcing cleanup.")
|
|
124
194
|
try:
|
|
125
195
|
# Try to remove all session files to ensure a clean state for next session
|
|
126
196
|
for filename in os.listdir(SESSION_DIR):
|
|
@@ -129,16 +199,16 @@ def stop_session():
|
|
|
129
199
|
if os.path.isfile(filepath):
|
|
130
200
|
os.unlink(filepath)
|
|
131
201
|
except Exception as e:
|
|
132
|
-
print(
|
|
202
|
+
print("Error removing file {0}: {1}".format(filepath, e))
|
|
133
203
|
|
|
134
|
-
print(
|
|
204
|
+
print("{0}: Session files cleaned up forcefully".format(SCRIPT_SUCCESS_MARKER))
|
|
135
205
|
return True
|
|
136
206
|
except Exception as e:
|
|
137
|
-
print(
|
|
207
|
+
print("{0}: Failed to force cleanup - {1}".format(SCRIPT_ERROR_MARKER, str(e)))
|
|
138
208
|
return False
|
|
139
209
|
|
|
140
210
|
except Exception as e:
|
|
141
|
-
print(
|
|
211
|
+
print("{0}: Error stopping session - {1}".format(SCRIPT_ERROR_MARKER, str(e)))
|
|
142
212
|
traceback.print_exc()
|
|
143
213
|
return False
|
|
144
214
|
|
package/dist/sessionManager.js
CHANGED
|
@@ -122,7 +122,8 @@ class CodesysSessionManager extends events_1.EventEmitter {
|
|
|
122
122
|
this.idleTimeoutMs = idleTimeoutMs;
|
|
123
123
|
this.sessionId = (0, uuid_1.v4)();
|
|
124
124
|
// Set up session directory and files
|
|
125
|
-
|
|
125
|
+
// IMPORTANT: Don't use a subdirectory - Python scripts expect files directly in SESSION_DIR
|
|
126
|
+
this.sessionDir = SESSION_DIR;
|
|
126
127
|
this.commandFilePath = path.join(this.sessionDir, COMMAND_FILE);
|
|
127
128
|
this.resultFilePath = path.join(this.sessionDir, RESULT_FILE);
|
|
128
129
|
this.stateFilePath = path.join(this.sessionDir, STATE_FILE);
|
|
@@ -442,16 +443,27 @@ class CodesysSessionManager extends events_1.EventEmitter {
|
|
|
442
443
|
}, 2000); // Give it 2 seconds to terminate gracefully
|
|
443
444
|
});
|
|
444
445
|
}
|
|
445
|
-
// Clean up session directory
|
|
446
|
+
// Clean up session files (not entire directory)
|
|
446
447
|
try {
|
|
447
448
|
if (fs.existsSync(this.sessionDir)) {
|
|
448
|
-
console.error(`SESSION_MANAGER: Cleaning up session
|
|
449
|
-
//
|
|
450
|
-
|
|
449
|
+
console.error(`SESSION_MANAGER: Cleaning up session files in: ${this.sessionDir}`);
|
|
450
|
+
// Remove only the session-related files, not the entire directory
|
|
451
|
+
const filesToRemove = [
|
|
452
|
+
this.commandFilePath,
|
|
453
|
+
this.resultFilePath,
|
|
454
|
+
this.stateFilePath,
|
|
455
|
+
this.lockFilePath
|
|
456
|
+
];
|
|
457
|
+
for (const filePath of filesToRemove) {
|
|
458
|
+
if (fs.existsSync(filePath)) {
|
|
459
|
+
fs.unlinkSync(filePath);
|
|
460
|
+
console.error(`SESSION_MANAGER: Removed session file: ${filePath}`);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
451
463
|
}
|
|
452
464
|
}
|
|
453
465
|
catch (error) {
|
|
454
|
-
console.error(`SESSION_MANAGER: Error cleaning up session
|
|
466
|
+
console.error(`SESSION_MANAGER: Error cleaning up session files: ${error}`);
|
|
455
467
|
}
|
|
456
468
|
this.codesysProcess = null;
|
|
457
469
|
this.sessionState = exports.SessionState.STOPPED;
|
|
@@ -524,8 +536,6 @@ class CodesysSessionManager extends events_1.EventEmitter {
|
|
|
524
536
|
}
|
|
525
537
|
// Reset idle timeout since we're about to do work
|
|
526
538
|
this.resetIdleTimeout();
|
|
527
|
-
// Mark the session as busy
|
|
528
|
-
this.sessionState = exports.SessionState.BUSY;
|
|
529
539
|
// Check if the command file exists
|
|
530
540
|
if (!fs.existsSync(this.commandFilePath)) {
|
|
531
541
|
this.sessionState = exports.SessionState.ERROR;
|
|
@@ -536,6 +546,50 @@ class CodesysSessionManager extends events_1.EventEmitter {
|
|
|
536
546
|
this.sessionState = exports.SessionState.ERROR;
|
|
537
547
|
throw new Error(`Result file not found at ${this.resultFilePath}`);
|
|
538
548
|
}
|
|
549
|
+
// IMPORTANT: Ensure only one script is executed at a time by checking if other scripts are pending
|
|
550
|
+
if (this.pendingScripts.size > 0) {
|
|
551
|
+
console.error(`SESSION_MANAGER: Waiting for ${this.pendingScripts.size} pending scripts to complete before executing new script`);
|
|
552
|
+
// Wait for all pending scripts to complete
|
|
553
|
+
yield new Promise((resolve) => {
|
|
554
|
+
const checkInterval = setInterval(() => {
|
|
555
|
+
if (this.pendingScripts.size === 0) {
|
|
556
|
+
clearInterval(checkInterval);
|
|
557
|
+
resolve();
|
|
558
|
+
}
|
|
559
|
+
}, 100);
|
|
560
|
+
});
|
|
561
|
+
console.error('SESSION_MANAGER: All pending scripts completed, proceeding with new script execution');
|
|
562
|
+
}
|
|
563
|
+
// Mark the session as busy
|
|
564
|
+
this.sessionState = exports.SessionState.BUSY;
|
|
565
|
+
// Read current command file to ensure we're not overwriting an in-progress command
|
|
566
|
+
try {
|
|
567
|
+
const currentCommandData = JSON.parse(fs.readFileSync(this.commandFilePath, 'utf8'));
|
|
568
|
+
if (currentCommandData.status === 'pending' || currentCommandData.status === 'processing') {
|
|
569
|
+
console.error(`SESSION_MANAGER: Found pending command in file, waiting for it to complete`);
|
|
570
|
+
// Wait for the command to complete (status to be 'ready')
|
|
571
|
+
yield new Promise((resolve) => {
|
|
572
|
+
const checkInterval = setInterval(() => {
|
|
573
|
+
try {
|
|
574
|
+
const checkData = JSON.parse(fs.readFileSync(this.commandFilePath, 'utf8'));
|
|
575
|
+
if (checkData.status === 'ready') {
|
|
576
|
+
clearInterval(checkInterval);
|
|
577
|
+
resolve();
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
catch (err) {
|
|
581
|
+
// If we can't read the file, just continue
|
|
582
|
+
console.error(`SESSION_MANAGER: Error reading command file during wait: ${err}`);
|
|
583
|
+
}
|
|
584
|
+
}, 100);
|
|
585
|
+
});
|
|
586
|
+
console.error('SESSION_MANAGER: Previous command completed, proceeding with new command');
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
catch (err) {
|
|
590
|
+
console.error(`SESSION_MANAGER: Error reading current command file: ${err}`);
|
|
591
|
+
// If we can't read the file, we'll try to proceed anyway
|
|
592
|
+
}
|
|
539
593
|
// Return a promise that will be resolved when script execution completes
|
|
540
594
|
return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
|
|
541
595
|
try {
|
|
@@ -584,7 +638,8 @@ class CodesysSessionManager extends events_1.EventEmitter {
|
|
|
584
638
|
timestamp: Date.now()
|
|
585
639
|
};
|
|
586
640
|
// Send the command by writing to the command file
|
|
587
|
-
|
|
641
|
+
console.error(`SESSION_MANAGER: Writing command to file ${this.commandFilePath}`);
|
|
642
|
+
fs.writeFileSync(this.commandFilePath, JSON.stringify(commandData, null, 2));
|
|
588
643
|
console.error(`SESSION_MANAGER: Command written to file`);
|
|
589
644
|
// Start reading the result file
|
|
590
645
|
this.readResultFile(scriptId);
|
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()
|