@codesys/mcp-toolkit 1.2.0 → 1.2.4
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 +20 -0
- package/codesys-mcp-toolkit-1.2.1.tgz +0 -0
- package/dist/codesys_interop.js +143 -2
- package/dist/scriptLoader.js +14 -2
- package/dist/scripts/check_status.py +21 -0
- package/dist/scripts/compile_project.py +65 -0
- package/dist/scripts/create_method.py +80 -0
- package/dist/scripts/create_pou.py +96 -0
- package/dist/scripts/create_project.py +100 -0
- package/dist/scripts/create_property.py +78 -0
- package/dist/scripts/ensure_project_open.py +154 -0
- package/dist/scripts/find_object_by_path.py +111 -0
- package/dist/scripts/get_pou_code.py +91 -0
- package/dist/scripts/get_project_structure.py +72 -0
- package/dist/scripts/open_project.py +25 -0
- package/dist/scripts/save_project.py +29 -0
- package/dist/scripts/session_status.py +168 -0
- package/dist/scripts/set_pou_code.py +104 -0
- package/dist/scripts/start_session.py +240 -0
- package/dist/scripts/stop_session.py +94 -0
- package/dist/sessionManager.js +399 -117
- package/docs/session_management.md +6 -1
- package/package.json +4 -3
- package/scripts/build.js +25 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import sys, os, json, time, tempfile
|
|
2
|
+
|
|
3
|
+
# Define markers for script results
|
|
4
|
+
SCRIPT_SUCCESS_MARKER = "SCRIPT_SUCCESS"
|
|
5
|
+
SCRIPT_ERROR_MARKER = "SCRIPT_ERROR"
|
|
6
|
+
|
|
7
|
+
# Session state file and pipes
|
|
8
|
+
SESSION_DIR = os.path.join(tempfile.gettempdir(), "codesys_session")
|
|
9
|
+
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")
|
|
12
|
+
|
|
13
|
+
def check_session_status():
|
|
14
|
+
"""Check the status of a CODESYS session by reading the state file"""
|
|
15
|
+
try:
|
|
16
|
+
# Check if session directory exists
|
|
17
|
+
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
|
|
25
|
+
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
|
+
}
|
|
80
|
+
|
|
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):
|
|
85
|
+
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)
|
|
106
|
+
|
|
107
|
+
if not ready:
|
|
108
|
+
os.close(res_fd)
|
|
109
|
+
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
|
+
|
|
115
|
+
if result_json:
|
|
116
|
+
try:
|
|
117
|
+
result = json.loads(result_json)
|
|
118
|
+
return result.get("success", False)
|
|
119
|
+
except json.JSONDecodeError:
|
|
120
|
+
return False
|
|
121
|
+
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()
|
|
132
|
+
|
|
133
|
+
# Print status information
|
|
134
|
+
print(f"Session Exists: {status.get('session_exists', False)}")
|
|
135
|
+
print(f"Session Active: {status.get('active', False)}")
|
|
136
|
+
|
|
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']}")
|
|
155
|
+
else:
|
|
156
|
+
print("Session is not active (stopped or crashed)")
|
|
157
|
+
|
|
158
|
+
# Print the final result marker
|
|
159
|
+
print(f"{SCRIPT_SUCCESS_MARKER}: Session status check complete")
|
|
160
|
+
sys.exit(0)
|
|
161
|
+
|
|
162
|
+
except Exception as e:
|
|
163
|
+
print(f"Error checking session status: {e}")
|
|
164
|
+
print(f"{SCRIPT_ERROR_MARKER}: {str(e)}")
|
|
165
|
+
sys.exit(1)
|
|
166
|
+
|
|
167
|
+
if __name__ == "__main__":
|
|
168
|
+
main()
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import sys, scriptengine as script_engine, os, traceback
|
|
2
|
+
|
|
3
|
+
# Import ensure_project_open function from the template
|
|
4
|
+
# This will be prepended by the script loader
|
|
5
|
+
def ensure_project_open(target_project_path):
|
|
6
|
+
# Function implementation will be included from ensure_project_open.py
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
# Import find_object_by_path function from the template
|
|
10
|
+
# This will be prepended by the script loader
|
|
11
|
+
def find_object_by_path_robust(start_node, full_path, target_type_name="object"):
|
|
12
|
+
# Function implementation will be included from find_object_by_path.py
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
POU_FULL_PATH = "{POU_FULL_PATH}" # Expecting format like "Application/MyPOU" or "Folder/SubFolder/MyPOU"
|
|
16
|
+
DECLARATION_CONTENT = """{DECLARATION_CONTENT}"""
|
|
17
|
+
IMPLEMENTATION_CONTENT = """{IMPLEMENTATION_CONTENT}"""
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
print("DEBUG: set_pou_code script: POU_FULL_PATH='%s', Project='%s'" % (POU_FULL_PATH, PROJECT_FILE_PATH))
|
|
21
|
+
primary_project = ensure_project_open(PROJECT_FILE_PATH)
|
|
22
|
+
if not POU_FULL_PATH: raise ValueError("POU full path empty.")
|
|
23
|
+
|
|
24
|
+
# Find the target POU/Method/Property object
|
|
25
|
+
target_object = find_object_by_path_robust(primary_project, POU_FULL_PATH, "target object")
|
|
26
|
+
if not target_object: raise ValueError("Target object not found using path: %s" % POU_FULL_PATH)
|
|
27
|
+
|
|
28
|
+
target_name = getattr(target_object, 'get_name', lambda: POU_FULL_PATH)()
|
|
29
|
+
print("DEBUG: Found target object: %s" % target_name)
|
|
30
|
+
|
|
31
|
+
# --- Set Declaration Part ---
|
|
32
|
+
declaration_updated = False
|
|
33
|
+
# Check if the content is actually provided (might be None/empty if only impl is set)
|
|
34
|
+
has_declaration_content = 'DECLARATION_CONTENT' in locals() or 'DECLARATION_CONTENT' in globals()
|
|
35
|
+
if has_declaration_content and DECLARATION_CONTENT is not None: # Check not None
|
|
36
|
+
if hasattr(target_object, 'textual_declaration'):
|
|
37
|
+
decl_obj = target_object.textual_declaration
|
|
38
|
+
if decl_obj and hasattr(decl_obj, 'replace'):
|
|
39
|
+
try:
|
|
40
|
+
print("DEBUG: Accessing textual_declaration...")
|
|
41
|
+
decl_obj.replace(DECLARATION_CONTENT)
|
|
42
|
+
print("DEBUG: Set declaration text using replace().")
|
|
43
|
+
declaration_updated = True
|
|
44
|
+
except Exception as decl_err:
|
|
45
|
+
print("ERROR: Failed to set declaration text: %s" % decl_err)
|
|
46
|
+
traceback.print_exc() # Print stack trace for detailed error
|
|
47
|
+
else:
|
|
48
|
+
print("WARN: Target '%s' textual_declaration attribute is None or does not have replace(). Skipping declaration update." % target_name)
|
|
49
|
+
else:
|
|
50
|
+
print("WARN: Target '%s' does not have textual_declaration attribute. Skipping declaration update." % target_name)
|
|
51
|
+
else:
|
|
52
|
+
print("DEBUG: Declaration content not provided or is None. Skipping declaration update.")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# --- Set Implementation Part ---
|
|
56
|
+
implementation_updated = False
|
|
57
|
+
has_implementation_content = 'IMPLEMENTATION_CONTENT' in locals() or 'IMPLEMENTATION_CONTENT' in globals()
|
|
58
|
+
if has_implementation_content and IMPLEMENTATION_CONTENT is not None: # Check not None
|
|
59
|
+
if hasattr(target_object, 'textual_implementation'):
|
|
60
|
+
impl_obj = target_object.textual_implementation
|
|
61
|
+
if impl_obj and hasattr(impl_obj, 'replace'):
|
|
62
|
+
try:
|
|
63
|
+
print("DEBUG: Accessing textual_implementation...")
|
|
64
|
+
impl_obj.replace(IMPLEMENTATION_CONTENT)
|
|
65
|
+
print("DEBUG: Set implementation text using replace().")
|
|
66
|
+
implementation_updated = True
|
|
67
|
+
except Exception as impl_err:
|
|
68
|
+
print("ERROR: Failed to set implementation text: %s" % impl_err)
|
|
69
|
+
traceback.print_exc() # Print stack trace for detailed error
|
|
70
|
+
else:
|
|
71
|
+
print("WARN: Target '%s' textual_implementation attribute is None or does not have replace(). Skipping implementation update." % target_name)
|
|
72
|
+
else:
|
|
73
|
+
print("WARN: Target '%s' does not have textual_implementation attribute. Skipping implementation update." % target_name)
|
|
74
|
+
else:
|
|
75
|
+
print("DEBUG: Implementation content not provided or is None. Skipping implementation update.")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# --- SAVE THE PROJECT TO PERSIST THE CODE CHANGE ---
|
|
79
|
+
# Only save if something was actually updated to avoid unnecessary saves
|
|
80
|
+
if declaration_updated or implementation_updated:
|
|
81
|
+
try:
|
|
82
|
+
print("DEBUG: Saving Project (after code change)...")
|
|
83
|
+
primary_project.save() # Save the overall project file
|
|
84
|
+
print("DEBUG: Project saved successfully after code change.")
|
|
85
|
+
except Exception as save_err:
|
|
86
|
+
print("ERROR: Failed to save Project after setting code: %s" % save_err)
|
|
87
|
+
detailed_error = traceback.format_exc()
|
|
88
|
+
error_message = "Error saving Project after code change for '%s': %s\\n%s" % (target_name, save_err, detailed_error)
|
|
89
|
+
print(error_message); print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
|
|
90
|
+
else:
|
|
91
|
+
print("DEBUG: No code parts were updated, skipping project save.")
|
|
92
|
+
# --- END SAVING ---
|
|
93
|
+
|
|
94
|
+
print("Code Set For: %s" % target_name)
|
|
95
|
+
print("Path: %s" % POU_FULL_PATH)
|
|
96
|
+
print("SCRIPT_SUCCESS: Declaration and/or implementation set successfully.")
|
|
97
|
+
sys.exit(0)
|
|
98
|
+
|
|
99
|
+
except Exception as e:
|
|
100
|
+
detailed_error = traceback.format_exc()
|
|
101
|
+
error_message = "Error setting code for object '%s' in project '%s': %s\\n%s" % (POU_FULL_PATH, PROJECT_FILE_PATH, e, detailed_error)
|
|
102
|
+
print(error_message)
|
|
103
|
+
print("SCRIPT_ERROR: %s" % error_message)
|
|
104
|
+
sys.exit(1)
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import sys, os, threading, json, time, tempfile
|
|
2
|
+
import scriptengine as script_engine
|
|
3
|
+
|
|
4
|
+
# Define markers for script results
|
|
5
|
+
SCRIPT_SUCCESS_MARKER = "SCRIPT_SUCCESS"
|
|
6
|
+
SCRIPT_ERROR_MARKER = "SCRIPT_ERROR"
|
|
7
|
+
|
|
8
|
+
# Session state file and pipes
|
|
9
|
+
SESSION_DIR = os.path.join(tempfile.gettempdir(), "codesys_session")
|
|
10
|
+
SESSION_STATE_FILE = os.path.join(SESSION_DIR, "session_state.json")
|
|
11
|
+
COMMAND_PIPE = os.path.join(SESSION_DIR, "command_pipe")
|
|
12
|
+
RESULT_PIPE = os.path.join(SESSION_DIR, "result_pipe")
|
|
13
|
+
|
|
14
|
+
def setup_session_dir():
|
|
15
|
+
"""Create the session directory and pipes if they don't exist"""
|
|
16
|
+
try:
|
|
17
|
+
# Create session directory
|
|
18
|
+
if not os.path.exists(SESSION_DIR):
|
|
19
|
+
os.makedirs(SESSION_DIR)
|
|
20
|
+
|
|
21
|
+
# Create named pipes if they don't exist
|
|
22
|
+
if not os.path.exists(COMMAND_PIPE):
|
|
23
|
+
os.mkfifo(COMMAND_PIPE)
|
|
24
|
+
|
|
25
|
+
if not os.path.exists(RESULT_PIPE):
|
|
26
|
+
os.mkfifo(RESULT_PIPE)
|
|
27
|
+
|
|
28
|
+
# Initialize session state
|
|
29
|
+
session_state = {
|
|
30
|
+
"active": True,
|
|
31
|
+
"start_time": time.time(),
|
|
32
|
+
"project_open": False,
|
|
33
|
+
"project_name": "",
|
|
34
|
+
"project_path": ""
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
with open(SESSION_STATE_FILE, 'w') as f:
|
|
38
|
+
json.dump(session_state, f)
|
|
39
|
+
|
|
40
|
+
return True
|
|
41
|
+
except Exception as e:
|
|
42
|
+
print(f"Failed to set up session directory: {e}")
|
|
43
|
+
print(f"{SCRIPT_ERROR_MARKER}: Session setup failed - {str(e)}")
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
def update_session_state():
|
|
47
|
+
"""Update the session state file with current CODESYS state"""
|
|
48
|
+
try:
|
|
49
|
+
# Get current state
|
|
50
|
+
project_open = False
|
|
51
|
+
project_name = "No project open"
|
|
52
|
+
project_path = "N/A"
|
|
53
|
+
|
|
54
|
+
primary_project = script_engine.projects.primary
|
|
55
|
+
if primary_project:
|
|
56
|
+
project_open = True
|
|
57
|
+
try:
|
|
58
|
+
project_path = os.path.normcase(os.path.abspath(primary_project.path))
|
|
59
|
+
try:
|
|
60
|
+
project_name = primary_project.get_name()
|
|
61
|
+
if not project_name:
|
|
62
|
+
project_name = f"Unnamed (path: {os.path.basename(project_path)})"
|
|
63
|
+
except:
|
|
64
|
+
project_name = f"Unnamed (path: {os.path.basename(project_path)})"
|
|
65
|
+
except Exception as e_path:
|
|
66
|
+
project_path = f"N/A (Error: {e_path})"
|
|
67
|
+
project_name = "Unnamed (Path Error)"
|
|
68
|
+
|
|
69
|
+
# Read current state file
|
|
70
|
+
with open(SESSION_STATE_FILE, 'r') as f:
|
|
71
|
+
session_state = json.load(f)
|
|
72
|
+
|
|
73
|
+
# Update state
|
|
74
|
+
session_state["project_open"] = project_open
|
|
75
|
+
session_state["project_name"] = project_name
|
|
76
|
+
session_state["project_path"] = project_path
|
|
77
|
+
session_state["last_update"] = time.time()
|
|
78
|
+
|
|
79
|
+
# Write updated state
|
|
80
|
+
with open(SESSION_STATE_FILE, 'w') as f:
|
|
81
|
+
json.dump(session_state, f)
|
|
82
|
+
|
|
83
|
+
return True
|
|
84
|
+
except Exception as e:
|
|
85
|
+
print(f"Failed to update session state: {e}")
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
def execute_command(command_data):
|
|
89
|
+
"""Execute a command from the command pipe"""
|
|
90
|
+
try:
|
|
91
|
+
cmd_type = command_data.get("type", "")
|
|
92
|
+
script = command_data.get("script", "")
|
|
93
|
+
cmd_id = command_data.get("id", "unknown")
|
|
94
|
+
|
|
95
|
+
result = {
|
|
96
|
+
"id": cmd_id,
|
|
97
|
+
"success": False,
|
|
98
|
+
"output": "",
|
|
99
|
+
"error": ""
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if cmd_type == "execute_script" and script:
|
|
103
|
+
# Execute the script in the current CODESYS context
|
|
104
|
+
try:
|
|
105
|
+
# Capture output by redirecting stdout/stderr
|
|
106
|
+
import io
|
|
107
|
+
from contextlib import redirect_stdout, redirect_stderr
|
|
108
|
+
|
|
109
|
+
stdout_buffer = io.StringIO()
|
|
110
|
+
stderr_buffer = io.StringIO()
|
|
111
|
+
|
|
112
|
+
with redirect_stdout(stdout_buffer), redirect_stderr(stderr_buffer):
|
|
113
|
+
# Execute the script
|
|
114
|
+
exec(script, globals())
|
|
115
|
+
|
|
116
|
+
# Get the captured output
|
|
117
|
+
stdout_output = stdout_buffer.getvalue()
|
|
118
|
+
stderr_output = stderr_buffer.getvalue()
|
|
119
|
+
|
|
120
|
+
# Check for success/error markers
|
|
121
|
+
if SCRIPT_SUCCESS_MARKER in stdout_output or SCRIPT_SUCCESS_MARKER in stderr_output:
|
|
122
|
+
result["success"] = True
|
|
123
|
+
elif SCRIPT_ERROR_MARKER in stdout_output or SCRIPT_ERROR_MARKER in stderr_output:
|
|
124
|
+
result["success"] = False
|
|
125
|
+
else:
|
|
126
|
+
# No explicit markers, assume success
|
|
127
|
+
result["success"] = True
|
|
128
|
+
|
|
129
|
+
result["output"] = stdout_output
|
|
130
|
+
if stderr_output:
|
|
131
|
+
result["error"] = stderr_output
|
|
132
|
+
|
|
133
|
+
except Exception as e:
|
|
134
|
+
result["success"] = False
|
|
135
|
+
result["error"] = f"Error executing script: {str(e)}"
|
|
136
|
+
result["output"] = f"{SCRIPT_ERROR_MARKER}: {str(e)}"
|
|
137
|
+
|
|
138
|
+
elif cmd_type == "stop":
|
|
139
|
+
# Stop command received
|
|
140
|
+
result["success"] = True
|
|
141
|
+
result["output"] = f"{SCRIPT_SUCCESS_MARKER}: Session stopping"
|
|
142
|
+
|
|
143
|
+
# Update session state to mark as inactive
|
|
144
|
+
with open(SESSION_STATE_FILE, 'r') as f:
|
|
145
|
+
session_state = json.load(f)
|
|
146
|
+
|
|
147
|
+
session_state["active"] = False
|
|
148
|
+
session_state["end_time"] = time.time()
|
|
149
|
+
|
|
150
|
+
with open(SESSION_STATE_FILE, 'w') as f:
|
|
151
|
+
json.dump(session_state, f)
|
|
152
|
+
|
|
153
|
+
# Return the result and signal to stop the loop
|
|
154
|
+
return result, True
|
|
155
|
+
|
|
156
|
+
else:
|
|
157
|
+
result["success"] = False
|
|
158
|
+
result["error"] = f"Unknown command type: {cmd_type}"
|
|
159
|
+
result["output"] = f"{SCRIPT_ERROR_MARKER}: Unknown command type: {cmd_type}"
|
|
160
|
+
|
|
161
|
+
# Update session state after command execution
|
|
162
|
+
update_session_state()
|
|
163
|
+
|
|
164
|
+
return result, False
|
|
165
|
+
|
|
166
|
+
except Exception as e:
|
|
167
|
+
return {
|
|
168
|
+
"id": command_data.get("id", "unknown"),
|
|
169
|
+
"success": False,
|
|
170
|
+
"output": f"{SCRIPT_ERROR_MARKER}: Command execution failed",
|
|
171
|
+
"error": str(e)
|
|
172
|
+
}, False
|
|
173
|
+
|
|
174
|
+
def command_loop():
|
|
175
|
+
"""Main loop to listen for and execute commands"""
|
|
176
|
+
stop_requested = False
|
|
177
|
+
|
|
178
|
+
print(f"Starting command loop...")
|
|
179
|
+
print(f"Command pipe: {COMMAND_PIPE}")
|
|
180
|
+
print(f"Result pipe: {RESULT_PIPE}")
|
|
181
|
+
|
|
182
|
+
while not stop_requested:
|
|
183
|
+
try:
|
|
184
|
+
# Open the command pipe for reading (blocks until data is available)
|
|
185
|
+
with open(COMMAND_PIPE, 'r') as cmd_pipe:
|
|
186
|
+
# Read the command
|
|
187
|
+
command_json = cmd_pipe.read().strip()
|
|
188
|
+
|
|
189
|
+
if command_json:
|
|
190
|
+
print(f"Received command: {command_json[:100]}...")
|
|
191
|
+
try:
|
|
192
|
+
command_data = json.loads(command_json)
|
|
193
|
+
result, stop_requested = execute_command(command_data)
|
|
194
|
+
|
|
195
|
+
# Write the result to the result pipe
|
|
196
|
+
with open(RESULT_PIPE, 'w') as res_pipe:
|
|
197
|
+
res_pipe.write(json.dumps(result))
|
|
198
|
+
|
|
199
|
+
except json.JSONDecodeError as e:
|
|
200
|
+
# Invalid JSON
|
|
201
|
+
print(f"Invalid command JSON: {e}")
|
|
202
|
+
error_result = {
|
|
203
|
+
"id": "unknown",
|
|
204
|
+
"success": False,
|
|
205
|
+
"output": f"{SCRIPT_ERROR_MARKER}: Invalid command format",
|
|
206
|
+
"error": str(e)
|
|
207
|
+
}
|
|
208
|
+
with open(RESULT_PIPE, 'w') as res_pipe:
|
|
209
|
+
res_pipe.write(json.dumps(error_result))
|
|
210
|
+
|
|
211
|
+
except Exception as e:
|
|
212
|
+
print(f"Error in command loop: {e}")
|
|
213
|
+
# Short delay to prevent CPU spinning in case of repeated errors
|
|
214
|
+
time.sleep(0.5)
|
|
215
|
+
|
|
216
|
+
print("Command loop stopped.")
|
|
217
|
+
|
|
218
|
+
# Main script
|
|
219
|
+
def main():
|
|
220
|
+
try:
|
|
221
|
+
# Setup session directory and pipes
|
|
222
|
+
if not setup_session_dir():
|
|
223
|
+
sys.exit(1)
|
|
224
|
+
|
|
225
|
+
# Update initial session state
|
|
226
|
+
update_session_state()
|
|
227
|
+
|
|
228
|
+
# Start command loop
|
|
229
|
+
print(f"{SCRIPT_SUCCESS_MARKER}: CODESYS session started")
|
|
230
|
+
command_loop()
|
|
231
|
+
|
|
232
|
+
print(f"{SCRIPT_SUCCESS_MARKER}: CODESYS session stopped cleanly")
|
|
233
|
+
sys.exit(0)
|
|
234
|
+
except Exception as e:
|
|
235
|
+
print(f"Error in session: {e}")
|
|
236
|
+
print(f"{SCRIPT_ERROR_MARKER}: Session failed - {str(e)}")
|
|
237
|
+
sys.exit(1)
|
|
238
|
+
|
|
239
|
+
if __name__ == "__main__":
|
|
240
|
+
main()
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import sys, os, json, time, tempfile
|
|
2
|
+
|
|
3
|
+
# Define markers for script results
|
|
4
|
+
SCRIPT_SUCCESS_MARKER = "SCRIPT_SUCCESS"
|
|
5
|
+
SCRIPT_ERROR_MARKER = "SCRIPT_ERROR"
|
|
6
|
+
|
|
7
|
+
# Session state file and pipes
|
|
8
|
+
SESSION_DIR = os.path.join(tempfile.gettempdir(), "codesys_session")
|
|
9
|
+
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")
|
|
12
|
+
|
|
13
|
+
def check_session_active():
|
|
14
|
+
"""Check if a session is active by reading the state file"""
|
|
15
|
+
try:
|
|
16
|
+
if not os.path.exists(SESSION_STATE_FILE):
|
|
17
|
+
print(f"No session state file found at {SESSION_STATE_FILE}")
|
|
18
|
+
return False
|
|
19
|
+
|
|
20
|
+
with open(SESSION_STATE_FILE, 'r') as f:
|
|
21
|
+
session_state = json.load(f)
|
|
22
|
+
|
|
23
|
+
return session_state.get("active", False)
|
|
24
|
+
except Exception as e:
|
|
25
|
+
print(f"Error checking session state: {e}")
|
|
26
|
+
return False
|
|
27
|
+
|
|
28
|
+
def send_stop_command():
|
|
29
|
+
"""Send the stop command to the running session"""
|
|
30
|
+
try:
|
|
31
|
+
if not os.path.exists(COMMAND_PIPE) or not os.path.exists(RESULT_PIPE):
|
|
32
|
+
print(f"Command or result pipes not found")
|
|
33
|
+
return False
|
|
34
|
+
|
|
35
|
+
stop_command = {
|
|
36
|
+
"id": f"stop_{time.time()}",
|
|
37
|
+
"type": "stop",
|
|
38
|
+
"script": ""
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Send the stop command
|
|
42
|
+
with open(COMMAND_PIPE, 'w') as cmd_pipe:
|
|
43
|
+
cmd_pipe.write(json.dumps(stop_command))
|
|
44
|
+
|
|
45
|
+
# Wait for the result
|
|
46
|
+
with open(RESULT_PIPE, 'r') as res_pipe:
|
|
47
|
+
result_json = res_pipe.read().strip()
|
|
48
|
+
|
|
49
|
+
if result_json:
|
|
50
|
+
try:
|
|
51
|
+
result = json.loads(result_json)
|
|
52
|
+
return result.get("success", False)
|
|
53
|
+
except json.JSONDecodeError:
|
|
54
|
+
print(f"Invalid result JSON: {result_json}")
|
|
55
|
+
return False
|
|
56
|
+
else:
|
|
57
|
+
print("No result received")
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
except Exception as e:
|
|
61
|
+
print(f"Error sending stop command: {e}")
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
def main():
|
|
65
|
+
try:
|
|
66
|
+
# Check if session is active
|
|
67
|
+
if not check_session_active():
|
|
68
|
+
print(f"{SCRIPT_SUCCESS_MARKER}: No active session found")
|
|
69
|
+
sys.exit(0)
|
|
70
|
+
|
|
71
|
+
# Send stop command
|
|
72
|
+
print("Sending stop command to CODESYS session...")
|
|
73
|
+
if send_stop_command():
|
|
74
|
+
print(f"{SCRIPT_SUCCESS_MARKER}: Session stop command sent successfully")
|
|
75
|
+
|
|
76
|
+
# Give the session a moment to clean up
|
|
77
|
+
time.sleep(1)
|
|
78
|
+
|
|
79
|
+
# Verify session state has been updated
|
|
80
|
+
if not check_session_active():
|
|
81
|
+
print(f"{SCRIPT_SUCCESS_MARKER}: Session stopped successfully")
|
|
82
|
+
else:
|
|
83
|
+
print(f"{SCRIPT_SUCCESS_MARKER}: Stop command accepted, but session may still be shutting down")
|
|
84
|
+
else:
|
|
85
|
+
print(f"{SCRIPT_ERROR_MARKER}: Failed to stop session")
|
|
86
|
+
sys.exit(1)
|
|
87
|
+
|
|
88
|
+
except Exception as e:
|
|
89
|
+
print(f"Error stopping session: {e}")
|
|
90
|
+
print(f"{SCRIPT_ERROR_MARKER}: {str(e)}")
|
|
91
|
+
sys.exit(1)
|
|
92
|
+
|
|
93
|
+
if __name__ == "__main__":
|
|
94
|
+
main()
|