@codesys/mcp-toolkit 1.4.1 → 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 +9 -0
- package/dist/scripts/session_status.py +59 -1
- package/dist/scripts/start_session.py +210 -52
- package/dist/scripts/stop_session.py +64 -1
- package/dist/sessionManager.js +64 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,15 @@ 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
|
+
|
|
8
17
|
## [1.4.1] - 2025-05-02
|
|
9
18
|
### Fixed
|
|
10
19
|
- Fixed IronPython 2.7 compatibility for session management scripts
|
|
@@ -6,7 +6,56 @@ try:
|
|
|
6
6
|
import json
|
|
7
7
|
except ImportError:
|
|
8
8
|
# For IronPython 2.7, we need to use simplejson instead
|
|
9
|
-
|
|
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()
|
|
10
59
|
|
|
11
60
|
# Define markers for script results
|
|
12
61
|
SCRIPT_SUCCESS_MARKER = "SCRIPT_SUCCESS"
|
|
@@ -19,6 +68,15 @@ COMMAND_FILE = os.path.join(SESSION_DIR, "command.json")
|
|
|
19
68
|
RESULT_FILE = os.path.join(SESSION_DIR, "result.json")
|
|
20
69
|
SESSION_LOCK_FILE = os.path.join(SESSION_DIR, "session.lock")
|
|
21
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
|
+
|
|
22
80
|
def check_session_status():
|
|
23
81
|
"""Check the status of a CODESYS session"""
|
|
24
82
|
try:
|
|
@@ -5,16 +5,71 @@ try:
|
|
|
5
5
|
import json
|
|
6
6
|
except ImportError:
|
|
7
7
|
# For IronPython 2.7, we need to use simplejson instead
|
|
8
|
-
|
|
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()
|
|
9
58
|
|
|
10
59
|
# Import scriptengine
|
|
11
60
|
try:
|
|
12
61
|
import scriptengine as script_engine
|
|
62
|
+
print("Successfully imported scriptengine module")
|
|
13
63
|
except ImportError:
|
|
14
64
|
# Log the error and continue - we'll handle this gracefully
|
|
15
65
|
print("Error importing scriptengine. This script must be run within CODESYS environment.")
|
|
16
66
|
script_engine = None
|
|
17
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))
|
|
72
|
+
|
|
18
73
|
# Define markers for script results
|
|
19
74
|
SCRIPT_SUCCESS_MARKER = "SCRIPT_SUCCESS"
|
|
20
75
|
SCRIPT_ERROR_MARKER = "SCRIPT_ERROR"
|
|
@@ -255,67 +310,128 @@ def check_command():
|
|
|
255
310
|
try:
|
|
256
311
|
# If command file doesn't exist, return immediately
|
|
257
312
|
if not os.path.exists(COMMAND_FILE):
|
|
313
|
+
print("Command file does not exist at {0}".format(COMMAND_FILE))
|
|
258
314
|
return False
|
|
259
315
|
|
|
260
316
|
# Read the command file
|
|
261
317
|
try:
|
|
318
|
+
print("Reading command file: {0}".format(COMMAND_FILE))
|
|
262
319
|
with open(COMMAND_FILE, 'r') as f:
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
|
266
344
|
command_data = {"status": "ready", "command": None, "timestamp": time.time()}
|
|
267
|
-
|
|
268
|
-
|
|
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))
|
|
269
350
|
return False
|
|
270
351
|
|
|
271
352
|
# Check if there's a new command
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
with open(COMMAND_FILE, 'w') as f:
|
|
279
|
-
json.dump(command_data, f, indent=2)
|
|
280
|
-
|
|
281
|
-
# Update session state to busy
|
|
282
|
-
update_session_state("busy")
|
|
283
|
-
|
|
284
|
-
# Execute the command
|
|
285
|
-
result, stop_requested = execute_command(command)
|
|
286
|
-
|
|
287
|
-
# Write the result
|
|
288
|
-
result_data = {
|
|
289
|
-
"status": "complete",
|
|
290
|
-
"result": result,
|
|
291
|
-
"timestamp": time.time()
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
with open(RESULT_FILE, 'w') as f:
|
|
295
|
-
json.dump(result_data, f, indent=2)
|
|
296
|
-
|
|
297
|
-
# Reset command file
|
|
298
|
-
command_data = {
|
|
299
|
-
"status": "ready",
|
|
300
|
-
"command": None,
|
|
301
|
-
"timestamp": time.time()
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
with open(COMMAND_FILE, 'w') as 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")
|
|
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")))
|
|
310
359
|
|
|
311
|
-
|
|
312
|
-
|
|
360
|
+
if status == "pending" and command:
|
|
361
|
+
print("Processing pending command...")
|
|
313
362
|
|
|
314
|
-
|
|
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
|
|
315
431
|
|
|
316
432
|
return False
|
|
317
433
|
except Exception as e:
|
|
318
|
-
print("Error
|
|
434
|
+
print("Error in check_command: {0}".format(e))
|
|
319
435
|
traceback.print_exc()
|
|
320
436
|
return False
|
|
321
437
|
|
|
@@ -356,19 +472,61 @@ def command_loop():
|
|
|
356
472
|
"""Main loop to check for and execute commands"""
|
|
357
473
|
stop_requested = False
|
|
358
474
|
last_state_update = time.time()
|
|
475
|
+
loop_count = 0
|
|
476
|
+
last_heartbeat = time.time()
|
|
359
477
|
|
|
360
478
|
print("Starting command loop...")
|
|
361
479
|
print("Command file: {0}".format(COMMAND_FILE))
|
|
362
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()
|
|
363
510
|
|
|
364
511
|
while not stop_requested:
|
|
365
512
|
try:
|
|
513
|
+
loop_count += 1
|
|
514
|
+
current_time = time.time()
|
|
515
|
+
|
|
366
516
|
# Update session lock file to indicate we're alive
|
|
367
517
|
with open(SESSION_LOCK_FILE, 'w') as f:
|
|
368
|
-
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
|
|
369
524
|
|
|
370
525
|
# Check for new commands
|
|
526
|
+
print("Checking for commands...")
|
|
371
527
|
stop_requested = check_command()
|
|
528
|
+
if stop_requested:
|
|
529
|
+
print("Stop was requested by a command")
|
|
372
530
|
|
|
373
531
|
# Check for inactivity timeout
|
|
374
532
|
if check_inactivity_timeout():
|
|
@@ -377,8 +535,8 @@ def command_loop():
|
|
|
377
535
|
stop_requested = True
|
|
378
536
|
|
|
379
537
|
# Periodically update session state (every 30 seconds)
|
|
380
|
-
current_time = time.time()
|
|
381
538
|
if (current_time - last_state_update) > 30:
|
|
539
|
+
print("Periodic session state update")
|
|
382
540
|
update_session_state()
|
|
383
541
|
last_state_update = current_time
|
|
384
542
|
|
|
@@ -388,10 +546,10 @@ def command_loop():
|
|
|
388
546
|
except Exception as e:
|
|
389
547
|
print("Error in command loop: {0}".format(e))
|
|
390
548
|
traceback.print_exc()
|
|
391
|
-
# Sleep for a bit
|
|
549
|
+
# Sleep for a bit before retrying
|
|
392
550
|
time.sleep(POLL_INTERVAL)
|
|
393
551
|
|
|
394
|
-
print("Command loop stopped.
|
|
552
|
+
print("Command loop stopped after {0} iterations".format(loop_count))
|
|
395
553
|
|
|
396
554
|
# Main script
|
|
397
555
|
def main():
|
|
@@ -6,7 +6,61 @@ try:
|
|
|
6
6
|
import json
|
|
7
7
|
except ImportError:
|
|
8
8
|
# For IronPython 2.7, we need to use simplejson instead
|
|
9
|
-
|
|
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))
|
|
10
64
|
|
|
11
65
|
# Define markers for script results
|
|
12
66
|
SCRIPT_SUCCESS_MARKER = "SCRIPT_SUCCESS"
|
|
@@ -19,6 +73,15 @@ COMMAND_FILE = os.path.join(SESSION_DIR, "command.json")
|
|
|
19
73
|
RESULT_FILE = os.path.join(SESSION_DIR, "result.json")
|
|
20
74
|
SESSION_LOCK_FILE = os.path.join(SESSION_DIR, "session.lock")
|
|
21
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
|
+
|
|
22
85
|
def stop_session():
|
|
23
86
|
"""Stop a running CODESYS session"""
|
|
24
87
|
try:
|
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);
|