devman-runtime 0.1.0__tar.gz → 0.2.0__tar.gz
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.
- {devman_runtime-0.1.0/src/devman_runtime.egg-info → devman_runtime-0.2.0}/PKG-INFO +10 -9
- {devman_runtime-0.1.0 → devman_runtime-0.2.0}/README.md +9 -8
- {devman_runtime-0.1.0 → devman_runtime-0.2.0}/pyproject.toml +1 -1
- {devman_runtime-0.1.0 → devman_runtime-0.2.0}/src/devman_runtime/__init__.py +1 -2
- {devman_runtime-0.1.0 → devman_runtime-0.2.0}/src/devman_runtime/server.py +6 -219
- {devman_runtime-0.1.0 → devman_runtime-0.2.0/src/devman_runtime.egg-info}/PKG-INFO +10 -9
- {devman_runtime-0.1.0 → devman_runtime-0.2.0}/LICENSE +0 -0
- {devman_runtime-0.1.0 → devman_runtime-0.2.0}/setup.cfg +0 -0
- {devman_runtime-0.1.0 → devman_runtime-0.2.0}/src/devman_runtime/client.py +0 -0
- {devman_runtime-0.1.0 → devman_runtime-0.2.0}/src/devman_runtime/db.py +0 -0
- {devman_runtime-0.1.0 → devman_runtime-0.2.0}/src/devman_runtime/protocol.py +0 -0
- {devman_runtime-0.1.0 → devman_runtime-0.2.0}/src/devman_runtime/telegraf.py +0 -0
- {devman_runtime-0.1.0 → devman_runtime-0.2.0}/src/devman_runtime.egg-info/SOURCES.txt +0 -0
- {devman_runtime-0.1.0 → devman_runtime-0.2.0}/src/devman_runtime.egg-info/dependency_links.txt +0 -0
- {devman_runtime-0.1.0 → devman_runtime-0.2.0}/src/devman_runtime.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devman-runtime
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Python runtime (protocol, client, server core) for devman-gen generated device-manager bridges
|
|
5
5
|
Author: Kenji Shu
|
|
6
6
|
License-Expression: MIT
|
|
@@ -30,20 +30,21 @@ a normal dependency upgrade instead of regeneration.
|
|
|
30
30
|
(`set_link_groups` / `list_link_groups`).
|
|
31
31
|
- `devman_runtime.server` — `ManagerCore` and `serve_manager`: request
|
|
32
32
|
dispatch with ownership enforcement, device lifecycle/periodic hooks,
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
client leases. Hook files receive `core` in their context, so
|
|
34
|
+
device-specific safety monitors (e.g. the CAEN trip watchdog in
|
|
35
|
+
caenhv-devman's server_hooks.py) can use the registry and leases.
|
|
35
36
|
- `devman_runtime.db` — SQLite-backed ownership and link-group registry.
|
|
36
37
|
|
|
37
38
|
## Server features
|
|
38
39
|
|
|
39
|
-
- `trip_watchdog_interval` / `--trip-watchdog-interval`: poll registered
|
|
40
|
-
link groups and power off partners of a tripped channel, even when the
|
|
41
|
-
registering client is offline.
|
|
42
40
|
- `client_lease_sec` / `--client-lease-sec`: any authenticated request
|
|
43
|
-
renews a client lease;
|
|
44
|
-
|
|
41
|
+
renews a client lease; `core.is_client_live(name)` exposes it to hooks
|
|
42
|
+
(used e.g. for janitoring stale link groups).
|
|
45
43
|
- Periodic hook (`periodic_file` / `periodic_function` +
|
|
46
44
|
`periodic_interval_sec`): run a user callback on an interval with access
|
|
47
|
-
to the device singleton.
|
|
45
|
+
to the device singleton and the core.
|
|
46
|
+
|
|
47
|
+
Device-specific policies (what a "trip" means, when to power off partners)
|
|
48
|
+
belong in per-bridge hook files, not in this package.
|
|
48
49
|
|
|
49
50
|
This package is used by, e.g., `caenhv-devman-client`.
|
|
@@ -16,20 +16,21 @@ a normal dependency upgrade instead of regeneration.
|
|
|
16
16
|
(`set_link_groups` / `list_link_groups`).
|
|
17
17
|
- `devman_runtime.server` — `ManagerCore` and `serve_manager`: request
|
|
18
18
|
dispatch with ownership enforcement, device lifecycle/periodic hooks,
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
client leases. Hook files receive `core` in their context, so
|
|
20
|
+
device-specific safety monitors (e.g. the CAEN trip watchdog in
|
|
21
|
+
caenhv-devman's server_hooks.py) can use the registry and leases.
|
|
21
22
|
- `devman_runtime.db` — SQLite-backed ownership and link-group registry.
|
|
22
23
|
|
|
23
24
|
## Server features
|
|
24
25
|
|
|
25
|
-
- `trip_watchdog_interval` / `--trip-watchdog-interval`: poll registered
|
|
26
|
-
link groups and power off partners of a tripped channel, even when the
|
|
27
|
-
registering client is offline.
|
|
28
26
|
- `client_lease_sec` / `--client-lease-sec`: any authenticated request
|
|
29
|
-
renews a client lease;
|
|
30
|
-
|
|
27
|
+
renews a client lease; `core.is_client_live(name)` exposes it to hooks
|
|
28
|
+
(used e.g. for janitoring stale link groups).
|
|
31
29
|
- Periodic hook (`periodic_file` / `periodic_function` +
|
|
32
30
|
`periodic_interval_sec`): run a user callback on an interval with access
|
|
33
|
-
to the device singleton.
|
|
31
|
+
to the device singleton and the core.
|
|
32
|
+
|
|
33
|
+
Device-specific policies (what a "trip" means, when to power off partners)
|
|
34
|
+
belong in per-bridge hook files, not in this package.
|
|
34
35
|
|
|
35
36
|
This package is used by, e.g., `caenhv-devman-client`.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "devman-runtime"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "Python runtime (protocol, client, server core) for devman-gen generated device-manager bridges"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -7,7 +7,7 @@ agnostic and is not required at runtime.
|
|
|
7
7
|
|
|
8
8
|
from .client import ManagerClient, ManagerError
|
|
9
9
|
from .db import OwnershipDB
|
|
10
|
-
from .server import ManagerCore, RuntimeFunctionSpec,
|
|
10
|
+
from .server import ManagerCore, RuntimeFunctionSpec, serve_manager
|
|
11
11
|
from .telegraf import TelegrafSender, build_line
|
|
12
12
|
|
|
13
13
|
__all__ = [
|
|
@@ -17,7 +17,6 @@ __all__ = [
|
|
|
17
17
|
"OwnershipDB",
|
|
18
18
|
"RuntimeFunctionSpec",
|
|
19
19
|
"TelegrafSender",
|
|
20
|
-
"TripWatchdog",
|
|
21
20
|
"build_line",
|
|
22
21
|
"serve_manager",
|
|
23
22
|
]
|
|
@@ -21,207 +21,6 @@ from .protocol import recv_message, send_message
|
|
|
21
21
|
|
|
22
22
|
_EXPAND_FIELD_RE = re.compile(r"\{([A-Za-z_]\w*)\[\]\}")
|
|
23
23
|
|
|
24
|
-
_CHANNEL_RESOURCE_RE = re.compile(r"^slot:(\d+):ch:(\d+)$")
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
class TripWatchdog:
|
|
28
|
-
"""Monitor registered link groups and power off partners when one trips.
|
|
29
|
-
|
|
30
|
-
Runs server-side so linked channels stay protected even when the client
|
|
31
|
-
application that registered the groups is closed. Status is read and Pw
|
|
32
|
-
is written directly on the device singleton, bypassing ownership checks:
|
|
33
|
-
powering a channel off is the inherently safe direction.
|
|
34
|
-
"""
|
|
35
|
-
|
|
36
|
-
# Status bits per CAEN convention: 0 = ON, 6 = external trip, 8 = internal trip.
|
|
37
|
-
ON_MASK = 1 << 0
|
|
38
|
-
TRIP_MASK = (1 << 6) | (1 << 8)
|
|
39
|
-
|
|
40
|
-
def __init__(
|
|
41
|
-
self,
|
|
42
|
-
device: Any,
|
|
43
|
-
db: OwnershipDB,
|
|
44
|
-
interval_sec: float,
|
|
45
|
-
is_client_live: Any | None = None,
|
|
46
|
-
device_lock: Any | None = None,
|
|
47
|
-
) -> None:
|
|
48
|
-
self._device = device
|
|
49
|
-
self._db = db
|
|
50
|
-
self._interval_sec = float(interval_sec)
|
|
51
|
-
self._is_client_live = is_client_live
|
|
52
|
-
self._device_lock = device_lock if device_lock is not None else Lock()
|
|
53
|
-
self._stop = Event()
|
|
54
|
-
self._thread: Thread | None = None
|
|
55
|
-
self._latched_groups: set[frozenset[tuple[int, int]]] = set()
|
|
56
|
-
self._warned_unsync: set[tuple[str, int]] = set()
|
|
57
|
-
|
|
58
|
-
def start(self) -> None:
|
|
59
|
-
if self._thread is not None:
|
|
60
|
-
return
|
|
61
|
-
self._thread = Thread(target=self._loop, name="devman-trip-watchdog", daemon=True)
|
|
62
|
-
self._thread.start()
|
|
63
|
-
|
|
64
|
-
def stop(self) -> None:
|
|
65
|
-
self._stop.set()
|
|
66
|
-
if self._thread is not None:
|
|
67
|
-
self._thread.join(timeout=2.0)
|
|
68
|
-
self._thread = None
|
|
69
|
-
|
|
70
|
-
def _log(self, message: str) -> None:
|
|
71
|
-
ts = datetime.now().isoformat(timespec="seconds")
|
|
72
|
-
print(f"[devman trip-watchdog {ts}] {message}", file=sys.stderr, flush=True)
|
|
73
|
-
|
|
74
|
-
@staticmethod
|
|
75
|
-
def _parse_members(resources: list[str]) -> list[tuple[int, int]]:
|
|
76
|
-
members: list[tuple[int, int]] = []
|
|
77
|
-
for resource in resources:
|
|
78
|
-
match = _CHANNEL_RESOURCE_RE.match(str(resource).strip())
|
|
79
|
-
if match:
|
|
80
|
-
members.append((int(match.group(1)), int(match.group(2))))
|
|
81
|
-
return members
|
|
82
|
-
|
|
83
|
-
def _read_status(self, slot: int, channel: int) -> int | None:
|
|
84
|
-
try:
|
|
85
|
-
with self._device_lock:
|
|
86
|
-
values = self._device.get_ch_param(slot, [channel], "Status")
|
|
87
|
-
return int(values[0])
|
|
88
|
-
except Exception:
|
|
89
|
-
return None
|
|
90
|
-
|
|
91
|
-
def _power_off(self, slot: int, channel: int) -> bool:
|
|
92
|
-
try:
|
|
93
|
-
with self._device_lock:
|
|
94
|
-
self._device.set_ch_param(slot, [channel], "Pw", 0)
|
|
95
|
-
return True
|
|
96
|
-
except Exception as exc:
|
|
97
|
-
self._log(f"FAILED to power off {slot}:{channel}: {exc}")
|
|
98
|
-
return False
|
|
99
|
-
|
|
100
|
-
def _read_param_any(self, slot: int, channel: int, names: list[str]) -> Any | None:
|
|
101
|
-
for name in names:
|
|
102
|
-
try:
|
|
103
|
-
with self._device_lock:
|
|
104
|
-
values = self._device.get_ch_param(slot, [channel], name)
|
|
105
|
-
if isinstance(values, list) and values:
|
|
106
|
-
return values[0]
|
|
107
|
-
except Exception:
|
|
108
|
-
continue
|
|
109
|
-
return None
|
|
110
|
-
|
|
111
|
-
def _group_settings_synchronized(self, members: list[tuple[int, int]]) -> bool:
|
|
112
|
-
"""Same-name equality of RUp/RDWn plus PDwn match across the group.
|
|
113
|
-
|
|
114
|
-
The registering client keeps mixed-polarity groups with all four ramp
|
|
115
|
-
values equal, so same-name equality is a valid check for both group
|
|
116
|
-
kinds. Unreadable values are treated as synchronized (cannot judge).
|
|
117
|
-
"""
|
|
118
|
-
for names, numeric in ((["RUp", "RUP"], True), (["RDWn", "RDown", "RDWN"], True), (["PDwn", "PDWN"], False)):
|
|
119
|
-
seen: set[Any] = set()
|
|
120
|
-
for slot, channel in members:
|
|
121
|
-
value = self._read_param_any(slot, channel, names)
|
|
122
|
-
if value is None:
|
|
123
|
-
return True
|
|
124
|
-
if numeric:
|
|
125
|
-
try:
|
|
126
|
-
value = round(float(value), 6)
|
|
127
|
-
except Exception:
|
|
128
|
-
return True
|
|
129
|
-
else:
|
|
130
|
-
value = str(value).strip().lower()
|
|
131
|
-
seen.add(value)
|
|
132
|
-
if len(seen) > 1:
|
|
133
|
-
return False
|
|
134
|
-
return True
|
|
135
|
-
|
|
136
|
-
def _janitor_group(self, client: str, group_idx: int, members: list[tuple[int, int]]) -> bool:
|
|
137
|
-
"""Apply stale-group rules to a group whose owner lease expired.
|
|
138
|
-
|
|
139
|
-
Returns True when the group was removed. Energized groups are always
|
|
140
|
-
kept: dropping protection from powered channels on inference is the
|
|
141
|
-
wrong failure direction.
|
|
142
|
-
"""
|
|
143
|
-
statuses: dict[tuple[int, int], int] = {}
|
|
144
|
-
for slot, channel in members:
|
|
145
|
-
status = self._read_status(slot, channel)
|
|
146
|
-
if status is None:
|
|
147
|
-
return False # cannot judge: keep
|
|
148
|
-
statuses[(slot, channel)] = status
|
|
149
|
-
if any(status & (self.ON_MASK | self.TRIP_MASK) for status in statuses.values()):
|
|
150
|
-
if not self._group_settings_synchronized(members):
|
|
151
|
-
key = (client, int(group_idx))
|
|
152
|
-
if key not in self._warned_unsync:
|
|
153
|
-
self._warned_unsync.add(key)
|
|
154
|
-
names = ", ".join(f"{s}:{c}" for s, c in sorted(members))
|
|
155
|
-
self._log(
|
|
156
|
-
f"WARNING: energized group [{names}] of stale client '{client}' has "
|
|
157
|
-
"unsynchronized settings; keeping protection"
|
|
158
|
-
)
|
|
159
|
-
return False
|
|
160
|
-
removed = self._db.remove_link_group(client, int(group_idx))
|
|
161
|
-
if removed:
|
|
162
|
-
names = ", ".join(f"{s}:{c}" for s, c in sorted(members))
|
|
163
|
-
self._log(
|
|
164
|
-
f"removed stale link group [{names}] of client '{client}': lease expired, all channels off"
|
|
165
|
-
)
|
|
166
|
-
self._latched_groups.discard(frozenset(members))
|
|
167
|
-
self._warned_unsync.discard((client, int(group_idx)))
|
|
168
|
-
return bool(removed)
|
|
169
|
-
|
|
170
|
-
def check_groups_once(self) -> None:
|
|
171
|
-
try:
|
|
172
|
-
registered = self._db.link_groups_by_idx()
|
|
173
|
-
except Exception as exc:
|
|
174
|
-
self._log(f"failed to load link groups: {exc}")
|
|
175
|
-
return
|
|
176
|
-
for client, groups in registered.items():
|
|
177
|
-
live = True
|
|
178
|
-
if self._is_client_live is not None:
|
|
179
|
-
try:
|
|
180
|
-
live = bool(self._is_client_live(client))
|
|
181
|
-
except Exception:
|
|
182
|
-
live = True
|
|
183
|
-
for group_idx, resources in sorted(groups.items()):
|
|
184
|
-
members = self._parse_members(resources)
|
|
185
|
-
if len(members) < 2:
|
|
186
|
-
continue
|
|
187
|
-
if not live and self._janitor_group(client, group_idx, members):
|
|
188
|
-
continue
|
|
189
|
-
self._check_one_group(client, members)
|
|
190
|
-
|
|
191
|
-
def _check_one_group(self, client: str, members: list[tuple[int, int]]) -> None:
|
|
192
|
-
group_key = frozenset(members)
|
|
193
|
-
statuses: dict[tuple[int, int], int] = {}
|
|
194
|
-
for slot, channel in members:
|
|
195
|
-
status = self._read_status(slot, channel)
|
|
196
|
-
if status is not None:
|
|
197
|
-
statuses[(slot, channel)] = status
|
|
198
|
-
tripped = [key for key, status in statuses.items() if status & self.TRIP_MASK]
|
|
199
|
-
if not tripped:
|
|
200
|
-
self._latched_groups.discard(group_key)
|
|
201
|
-
return
|
|
202
|
-
if group_key in self._latched_groups:
|
|
203
|
-
return
|
|
204
|
-
self._latched_groups.add(group_key)
|
|
205
|
-
powered_off: list[str] = []
|
|
206
|
-
for slot, channel in sorted(members):
|
|
207
|
-
if (slot, channel) in tripped:
|
|
208
|
-
continue
|
|
209
|
-
status = statuses.get((slot, channel))
|
|
210
|
-
if status is None or not status & self.ON_MASK:
|
|
211
|
-
continue
|
|
212
|
-
if self._power_off(slot, channel):
|
|
213
|
-
powered_off.append(f"{slot}:{channel}")
|
|
214
|
-
tripped_names = ", ".join(f"{s}:{c}" for s, c in sorted(tripped))
|
|
215
|
-
self._log(
|
|
216
|
-
f"TRIP on {tripped_names} (group of client '{client}'); "
|
|
217
|
-
f"powered off partners: {', '.join(powered_off) or 'none'}"
|
|
218
|
-
)
|
|
219
|
-
|
|
220
|
-
def _loop(self) -> None:
|
|
221
|
-
while not self._stop.wait(self._interval_sec):
|
|
222
|
-
self.check_groups_once()
|
|
223
|
-
|
|
224
|
-
|
|
225
24
|
def _expand_resource_template(template: str, context: dict[str, Any]) -> list[str]:
|
|
226
25
|
expand_fields = _EXPAND_FIELD_RE.findall(template)
|
|
227
26
|
if not expand_fields:
|
|
@@ -689,7 +488,6 @@ def serve_manager(
|
|
|
689
488
|
singleton_file: str | None = None,
|
|
690
489
|
singleton_file_function: str = "get_singleton",
|
|
691
490
|
verbose: bool = False,
|
|
692
|
-
trip_watchdog_interval: float = 0.0,
|
|
693
491
|
client_lease_sec: float = 90.0,
|
|
694
492
|
) -> None:
|
|
695
493
|
if init_file and init_function:
|
|
@@ -716,6 +514,10 @@ def serve_manager(
|
|
|
716
514
|
"db_path": db_path,
|
|
717
515
|
"hook_args": dict(hook_args or {}),
|
|
718
516
|
"extra_args": list(extra_args or []),
|
|
517
|
+
# Device-specific extensions (e.g. safety monitors in hook files)
|
|
518
|
+
# can use core.db, core.is_client_live, and the ownership/link
|
|
519
|
+
# registry through this reference.
|
|
520
|
+
"core": core,
|
|
719
521
|
}
|
|
720
522
|
periodic_thread: Thread | None = None
|
|
721
523
|
periodic_stop = Event()
|
|
@@ -761,23 +563,8 @@ def serve_manager(
|
|
|
761
563
|
periodic_thread.start()
|
|
762
564
|
core._log(f"periodic hook started interval={interval_sec:.3f}s")
|
|
763
565
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
watchdog = TripWatchdog(
|
|
767
|
-
core._singleton_object,
|
|
768
|
-
core.db,
|
|
769
|
-
trip_watchdog_interval,
|
|
770
|
-
is_client_live=core.is_client_live,
|
|
771
|
-
device_lock=core._singleton_lock,
|
|
772
|
-
)
|
|
773
|
-
watchdog.start()
|
|
774
|
-
|
|
775
|
-
try:
|
|
776
|
-
with _ManagerTCPServer((host, port), core) as server:
|
|
777
|
-
server.serve_forever()
|
|
778
|
-
finally:
|
|
779
|
-
if watchdog is not None:
|
|
780
|
-
watchdog.stop()
|
|
566
|
+
with _ManagerTCPServer((host, port), core) as server:
|
|
567
|
+
server.serve_forever()
|
|
781
568
|
finally:
|
|
782
569
|
periodic_stop.set()
|
|
783
570
|
if periodic_thread is not None:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devman-runtime
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Python runtime (protocol, client, server core) for devman-gen generated device-manager bridges
|
|
5
5
|
Author: Kenji Shu
|
|
6
6
|
License-Expression: MIT
|
|
@@ -30,20 +30,21 @@ a normal dependency upgrade instead of regeneration.
|
|
|
30
30
|
(`set_link_groups` / `list_link_groups`).
|
|
31
31
|
- `devman_runtime.server` — `ManagerCore` and `serve_manager`: request
|
|
32
32
|
dispatch with ownership enforcement, device lifecycle/periodic hooks,
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
client leases. Hook files receive `core` in their context, so
|
|
34
|
+
device-specific safety monitors (e.g. the CAEN trip watchdog in
|
|
35
|
+
caenhv-devman's server_hooks.py) can use the registry and leases.
|
|
35
36
|
- `devman_runtime.db` — SQLite-backed ownership and link-group registry.
|
|
36
37
|
|
|
37
38
|
## Server features
|
|
38
39
|
|
|
39
|
-
- `trip_watchdog_interval` / `--trip-watchdog-interval`: poll registered
|
|
40
|
-
link groups and power off partners of a tripped channel, even when the
|
|
41
|
-
registering client is offline.
|
|
42
40
|
- `client_lease_sec` / `--client-lease-sec`: any authenticated request
|
|
43
|
-
renews a client lease;
|
|
44
|
-
|
|
41
|
+
renews a client lease; `core.is_client_live(name)` exposes it to hooks
|
|
42
|
+
(used e.g. for janitoring stale link groups).
|
|
45
43
|
- Periodic hook (`periodic_file` / `periodic_function` +
|
|
46
44
|
`periodic_interval_sec`): run a user callback on an interval with access
|
|
47
|
-
to the device singleton.
|
|
45
|
+
to the device singleton and the core.
|
|
46
|
+
|
|
47
|
+
Device-specific policies (what a "trip" means, when to power off partners)
|
|
48
|
+
belong in per-bridge hook files, not in this package.
|
|
48
49
|
|
|
49
50
|
This package is used by, e.g., `caenhv-devman-client`.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{devman_runtime-0.1.0 → devman_runtime-0.2.0}/src/devman_runtime.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|