@codesys/mcp-toolkit 2.0.1 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,147 +1,175 @@
1
- # -*- coding: utf-8 -*-
2
- # Python 2.7 (IronPython, inside CODESYS) / Python 3 (tests) compatibility.
3
- # Keep this file ASCII-only.
4
- from __future__ import print_function
5
-
6
- import codecs
7
- import json
8
- import os
9
- import sys
10
- import time
11
-
12
- PY2 = sys.version_info[0] == 2
13
-
14
- if PY2:
15
- text_type = unicode # noqa: F821
16
- string_types = (str, unicode) # noqa: F821
17
- else:
18
- text_type = str
19
- string_types = (str,)
20
-
21
-
22
- def to_text(value):
23
- """Best-effort conversion of anything (including .NET strings/enums) to text."""
24
- if value is None:
25
- return None
26
- if isinstance(value, text_type):
27
- return value
28
- if PY2 and isinstance(value, str):
29
- for enc in ("utf-8", "mbcs", "latin-1"):
30
- try:
31
- return value.decode(enc)
32
- except Exception:
33
- continue
34
- try:
35
- return text_type(value)
36
- except Exception:
37
- return repr(value)
38
-
39
-
40
- def read_json(path):
41
- f = codecs.open(path, "r", "utf-8-sig")
42
- try:
43
- return json.loads(f.read())
44
- finally:
45
- f.close()
46
-
47
-
48
- def _replace(src, dst):
49
- if hasattr(os, "replace"):
50
- os.replace(src, dst)
51
- return
52
- try:
53
- import System.IO # IronPython
54
- if System.IO.File.Exists(dst):
55
- System.IO.File.Replace(src, dst, None)
56
- else:
57
- System.IO.File.Move(src, dst)
58
- return
59
- except ImportError:
60
- pass
61
- if os.path.exists(dst):
62
- os.remove(dst)
63
- os.rename(src, dst)
64
-
65
-
66
- FS_RETRY_S = 3.0
67
-
68
-
69
- def retry_fs(fn, *args):
70
- """Run a file operation, retrying while Windows reports the file as in use.
71
-
72
- On Windows, replacing or removing a file fails with "access denied" (WinError 5,
73
- or IOException in .NET) while another process holds it open: the server polling
74
- heartbeat.json, an antivirus scan, the search indexer. That lasts milliseconds,
75
- so retry for up to FS_RETRY_S before giving up and raising the last error.
76
- """
77
- deadline = time.time() + FS_RETRY_S
78
- delay = 0.005
79
- while True:
80
- try:
81
- return fn(*args)
82
- except Exception:
83
- if time.time() + delay > deadline:
84
- raise
85
- time.sleep(delay)
86
- delay = min(delay * 2, 0.1)
87
-
88
-
89
- def write_json_atomic(path, obj):
90
- tmp = path + ".tmp"
91
- data = json.dumps(obj, ensure_ascii=False, indent=None, separators=(",", ":"))
92
- f = retry_fs(codecs.open, tmp, "w", "utf-8")
93
- try:
94
- f.write(to_text(data))
95
- finally:
96
- f.close()
97
- retry_fs(_replace, tmp, path)
98
-
99
-
100
- def remove_file(path):
101
- retry_fs(os.remove, path)
102
-
103
-
104
- def current_pid():
105
- try:
106
- import System.Diagnostics # IronPython
107
- return int(System.Diagnostics.Process.GetCurrentProcess().Id)
108
- except ImportError:
109
- return os.getpid()
110
-
111
-
112
- def pid_alive(pid):
113
- if not pid:
114
- return True
115
- try:
116
- import System.Diagnostics # IronPython
117
- try:
118
- p = System.Diagnostics.Process.GetProcessById(int(pid))
119
- return not p.HasExited
120
- except Exception:
121
- return False
122
- except ImportError:
123
- pass
124
- if os.name == "nt":
125
- import ctypes
126
- PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
127
- STILL_ACTIVE = 259
128
- k32 = ctypes.windll.kernel32
129
- h = k32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, int(pid))
130
- if not h:
131
- return False
132
- try:
133
- code = ctypes.c_ulong()
134
- if not k32.GetExitCodeProcess(h, ctypes.byref(code)):
135
- return False
136
- return code.value == STILL_ACTIVE
137
- finally:
138
- k32.CloseHandle(h)
139
- try:
140
- os.kill(int(pid), 0)
141
- return True
142
- except OSError:
143
- return False
144
-
145
-
146
- def now():
147
- return time.time()
1
+ # -*- coding: utf-8 -*-
2
+ # Python 2.7 (IronPython, inside CODESYS) / Python 3 (tests) compatibility.
3
+ # Keep this file ASCII-only.
4
+ from __future__ import print_function
5
+
6
+ import codecs
7
+ import json
8
+ import os
9
+ import sys
10
+ import time
11
+
12
+ PY2 = sys.version_info[0] == 2
13
+
14
+ if PY2:
15
+ text_type = unicode # noqa: F821
16
+ string_types = (str, unicode) # noqa: F821
17
+ else:
18
+ text_type = str
19
+ string_types = (str,)
20
+
21
+
22
+ def to_text(value):
23
+ """Best-effort conversion of anything (including .NET strings/enums) to text."""
24
+ if value is None:
25
+ return None
26
+ if isinstance(value, text_type):
27
+ return value
28
+ if PY2 and isinstance(value, str):
29
+ for enc in ("utf-8", "mbcs", "latin-1"):
30
+ try:
31
+ return value.decode(enc)
32
+ except Exception:
33
+ continue
34
+ try:
35
+ return text_type(value)
36
+ except Exception:
37
+ return repr(value)
38
+
39
+
40
+ def read_json(path):
41
+ f = codecs.open(path, "r", "utf-8-sig")
42
+ try:
43
+ return json.loads(f.read())
44
+ finally:
45
+ f.close()
46
+
47
+
48
+ def _replace(src, dst):
49
+ if hasattr(os, "replace"):
50
+ os.replace(src, dst)
51
+ return
52
+ try:
53
+ import System.IO # IronPython
54
+ if System.IO.File.Exists(dst):
55
+ System.IO.File.Replace(src, dst, None)
56
+ else:
57
+ System.IO.File.Move(src, dst)
58
+ return
59
+ except ImportError:
60
+ pass
61
+ if os.path.exists(dst):
62
+ os.remove(dst)
63
+ os.rename(src, dst)
64
+
65
+
66
+ FS_RETRY_S = 3.0
67
+
68
+
69
+ def retry_fs(fn, *args):
70
+ """Run a file operation, retrying while Windows reports the file as in use.
71
+
72
+ On Windows, replacing or removing a file fails with "access denied" (WinError 5,
73
+ or IOException in .NET) while another process holds it open: the server polling
74
+ heartbeat.json, an antivirus scan, the search indexer. That lasts milliseconds,
75
+ so retry for up to FS_RETRY_S before giving up and raising the last error.
76
+ """
77
+ deadline = time.time() + FS_RETRY_S
78
+ delay = 0.005
79
+ while True:
80
+ try:
81
+ return fn(*args)
82
+ except Exception:
83
+ if time.time() + delay > deadline:
84
+ raise
85
+ time.sleep(delay)
86
+ delay = min(delay * 2, 0.1)
87
+
88
+
89
+ _TMP_SEQ = [0]
90
+
91
+
92
+ def _write_text_net(path, data):
93
+ """Write with .NET (IronPython only): the handle is closed when this returns."""
94
+ import System.IO
95
+ import System.Text
96
+ System.IO.File.WriteAllText(path, data, System.Text.UTF8Encoding(False))
97
+
98
+
99
+ def write_json_atomic(path, obj):
100
+ data = to_text(json.dumps(obj, ensure_ascii=False, indent=None, separators=(",", ":")))
101
+ # A fresh temp name each time: inside a Machine Expert window the process has been seen
102
+ # holding on to a temp file (no delete sharing), so renaming that one name failed for good.
103
+ _TMP_SEQ[0] += 1
104
+ tmp = "%s.%d.tmp" % (path, _TMP_SEQ[0])
105
+ try:
106
+ try:
107
+ _write_text_net(tmp, data)
108
+ except ImportError:
109
+ f = retry_fs(codecs.open, tmp, "w", "utf-8")
110
+ try:
111
+ f.write(data)
112
+ finally:
113
+ f.close()
114
+ retry_fs(_replace, tmp, path)
115
+ except Exception as first:
116
+ # Last resort: write the target itself (not atomic; readers retry on a bad read).
117
+ try:
118
+ os.remove(tmp)
119
+ except Exception:
120
+ pass
121
+ try:
122
+ import System.IO # noqa: F401 (IronPython only)
123
+ except ImportError:
124
+ raise first
125
+ _write_text_net(path, data)
126
+
127
+
128
+ def remove_file(path):
129
+ retry_fs(os.remove, path)
130
+
131
+
132
+ def current_pid():
133
+ try:
134
+ import System.Diagnostics # IronPython
135
+ return int(System.Diagnostics.Process.GetCurrentProcess().Id)
136
+ except ImportError:
137
+ return os.getpid()
138
+
139
+
140
+ def pid_alive(pid):
141
+ if not pid:
142
+ return True
143
+ try:
144
+ import System.Diagnostics # IronPython
145
+ try:
146
+ p = System.Diagnostics.Process.GetProcessById(int(pid))
147
+ return not p.HasExited
148
+ except Exception:
149
+ return False
150
+ except ImportError:
151
+ pass
152
+ if os.name == "nt":
153
+ import ctypes
154
+ PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
155
+ STILL_ACTIVE = 259
156
+ k32 = ctypes.windll.kernel32
157
+ h = k32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, int(pid))
158
+ if not h:
159
+ return False
160
+ try:
161
+ code = ctypes.c_ulong()
162
+ if not k32.GetExitCodeProcess(h, ctypes.byref(code)):
163
+ return False
164
+ return code.value == STILL_ACTIVE
165
+ finally:
166
+ k32.CloseHandle(h)
167
+ try:
168
+ os.kill(int(pid), 0)
169
+ return True
170
+ except OSError:
171
+ return False
172
+
173
+
174
+ def now():
175
+ return time.time()
@@ -838,6 +838,11 @@ def op_devices_list(args):
838
838
  nm = name_of(ch)
839
839
  if _safe(lambda: ch.is_device, False):
840
840
  ident = _safe(lambda: ch.get_device_identification())
841
+ # A module (a device that exists only inside its owner, such as Machine Expert's
842
+ # local I/O and embedded functions) reports its owner's type and id; its module
843
+ # id is what tells it apart.
844
+ mod = _safe(lambda: ch.get_module_identification())
845
+ module = to_text(_safe(lambda: mod.module_id)) if mod is not None else ""
841
846
  info = {
842
847
  "path": "/".join(names + [nm]),
843
848
  "name": nm,
@@ -845,6 +850,7 @@ def op_devices_list(args):
845
850
  "type": _safe(lambda: int(ident.type)),
846
851
  "id": to_text(_safe(lambda: ident.id)),
847
852
  "version": to_text(_safe(lambda: ident.version)),
853
+ "module": module or None,
848
854
  "enabled": _safe(lambda: bool(ch.is_enabled())),
849
855
  }
850
856
  if args.get("io", True):
@@ -99,6 +99,14 @@ def op_sim_start(args):
99
99
  raise OpError("no_application", "The project has no active application to simulate.")
100
100
  if SIM["online"] is not None:
101
101
  _stop()
102
+ # In a window the user has open (attach mode) the application may already be logged in,
103
+ # perhaps to a real controller. Never take over that connection.
104
+ if _safe(lambda: se.online.create_online_application(c.app).is_logged_in, False):
105
+ raise OpError(
106
+ "already_online",
107
+ "The application is already logged in, possibly to a real controller. The toolkit only goes "
108
+ "online to a simulation it starts itself: log out in the CODESYS window, then try again.",
109
+ )
102
110
  SIM["was_dirty"] = bool(_safe(lambda: c.proj.dirty, False))
103
111
  SIM["project"] = c.proj
104
112
  SIM["device"] = _device_of(c.app)