remoteRF-server-testing 0.0.0__py3-none-any.whl
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.
- remoteRF_server/__init__.py +0 -0
- remoteRF_server/common/__init__.py +0 -0
- remoteRF_server/common/grpc/__init__.py +1 -0
- remoteRF_server/common/grpc/grpc_host_pb2.py +63 -0
- remoteRF_server/common/grpc/grpc_host_pb2_grpc.py +97 -0
- remoteRF_server/common/grpc/grpc_pb2.py +59 -0
- remoteRF_server/common/grpc/grpc_pb2_grpc.py +97 -0
- remoteRF_server/common/idl/__init__.py +1 -0
- remoteRF_server/common/idl/device_schema.py +39 -0
- remoteRF_server/common/idl/pluto_schema.py +174 -0
- remoteRF_server/common/idl/schema.py +358 -0
- remoteRF_server/common/utils/__init__.py +6 -0
- remoteRF_server/common/utils/ansi_codes.py +120 -0
- remoteRF_server/common/utils/api_token.py +21 -0
- remoteRF_server/common/utils/db_connection.py +35 -0
- remoteRF_server/common/utils/db_location.py +24 -0
- remoteRF_server/common/utils/list_string.py +5 -0
- remoteRF_server/common/utils/process_arg.py +80 -0
- remoteRF_server/drivers/__init__.py +0 -0
- remoteRF_server/drivers/adalm_pluto/__init__.py +0 -0
- remoteRF_server/drivers/adalm_pluto/pluto_remote_server.py +105 -0
- remoteRF_server/host/__init__.py +0 -0
- remoteRF_server/host/host_auth_token.py +292 -0
- remoteRF_server/host/host_directory_store.py +142 -0
- remoteRF_server/host/host_tunnel_server.py +1388 -0
- remoteRF_server/server/__init__.py +0 -0
- remoteRF_server/server/acc_perms.py +317 -0
- remoteRF_server/server/cert_provider.py +184 -0
- remoteRF_server/server/device_manager.py +688 -0
- remoteRF_server/server/grpc_server.py +1023 -0
- remoteRF_server/server/reservation.py +811 -0
- remoteRF_server/server/rpc_manager.py +104 -0
- remoteRF_server/server/user_group_cli.py +723 -0
- remoteRF_server/server/user_group_handler.py +1120 -0
- remoteRF_server/serverrf_cli.py +1377 -0
- remoteRF_server/tools/__init__.py +191 -0
- remoteRF_server/tools/gen_certs.py +274 -0
- remoteRF_server/tools/gist_status.py +139 -0
- remoteRF_server/tools/gist_status_testing.py +67 -0
- remoterf_server_testing-0.0.0.dist-info/METADATA +612 -0
- remoterf_server_testing-0.0.0.dist-info/RECORD +44 -0
- remoterf_server_testing-0.0.0.dist-info/WHEEL +5 -0
- remoterf_server_testing-0.0.0.dist-info/entry_points.txt +2 -0
- remoterf_server_testing-0.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
# src/remoteRF_server/server/device_manager.py
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import time
|
|
8
|
+
import adi
|
|
9
|
+
import yaml
|
|
10
|
+
import subprocess
|
|
11
|
+
import threading
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Dict, Tuple, Optional, Any, List, Iterator
|
|
15
|
+
from contextlib import contextmanager
|
|
16
|
+
|
|
17
|
+
from ..common.utils import validate_token, get_remoterf_root
|
|
18
|
+
from ..host import host_tunnel_server as hts
|
|
19
|
+
|
|
20
|
+
_state_lock = threading.RLock()
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class VirtualDevice:
|
|
24
|
+
gid: int
|
|
25
|
+
host_id: str
|
|
26
|
+
device_id: str
|
|
27
|
+
label: str = ""
|
|
28
|
+
serial: str = ""
|
|
29
|
+
kind: str = ""
|
|
30
|
+
|
|
31
|
+
def __repr__(self) -> str:
|
|
32
|
+
return f"<VirtualDevice gid={self.gid} host={self.host_id} device_id={self.device_id}>"
|
|
33
|
+
|
|
34
|
+
def is_virtual(dev: object) -> bool:
|
|
35
|
+
return isinstance(dev, VirtualDevice)
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class _DeviceState:
|
|
39
|
+
dev: object | None
|
|
40
|
+
origin: str # "local" or "host"
|
|
41
|
+
online: bool # for host devices; for local can mirror (dev is not None)
|
|
42
|
+
salt: str
|
|
43
|
+
hsh: str
|
|
44
|
+
name: str
|
|
45
|
+
ident: str
|
|
46
|
+
dtype: str
|
|
47
|
+
io_lock: threading.RLock = field(default_factory=threading.RLock)
|
|
48
|
+
|
|
49
|
+
# gid -> state
|
|
50
|
+
_devices: Dict[int, _DeviceState] = {}
|
|
51
|
+
|
|
52
|
+
def _is_connected(st: _DeviceState) -> bool:
|
|
53
|
+
if st.origin == "local":
|
|
54
|
+
return st.dev is not None
|
|
55
|
+
# host
|
|
56
|
+
return bool(st.online)
|
|
57
|
+
|
|
58
|
+
# Host device pulling/grabbing
|
|
59
|
+
|
|
60
|
+
def _get_host_registry():
|
|
61
|
+
return hts.get_tunnel_registry()
|
|
62
|
+
|
|
63
|
+
_host_sync_lock = threading.RLock()
|
|
64
|
+
_host_sync_last_ms = 0
|
|
65
|
+
_HOST_SYNC_TTL_MS = 500 # tune later
|
|
66
|
+
|
|
67
|
+
def _sync_host_devices(*, force: bool = False) -> None:
|
|
68
|
+
global _host_sync_last_ms
|
|
69
|
+
|
|
70
|
+
now_ms = int(time.time() * 1000)
|
|
71
|
+
with _host_sync_lock:
|
|
72
|
+
if not force and (now_ms - _host_sync_last_ms) < _HOST_SYNC_TTL_MS:
|
|
73
|
+
return
|
|
74
|
+
_host_sync_last_ms = now_ms
|
|
75
|
+
|
|
76
|
+
reg = _get_host_registry()
|
|
77
|
+
if reg is None:
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
# Do registry calls without holding _state_lock
|
|
81
|
+
snap = reg.device_directory_cached(ttl_ms=0) # device_id -> (host_id, info_obj, is_active)
|
|
82
|
+
|
|
83
|
+
updates: Dict[int, Dict[str, object]] = {}
|
|
84
|
+
for device_id, (host_id, info_obj, is_active) in snap.items():
|
|
85
|
+
# You said device_id is assumed to be the GID
|
|
86
|
+
try:
|
|
87
|
+
gid = int(str(device_id))
|
|
88
|
+
except Exception:
|
|
89
|
+
continue
|
|
90
|
+
|
|
91
|
+
label = str(getattr(info_obj, "label", "") or "").strip()
|
|
92
|
+
serial = str(getattr(info_obj, "serial", "") or "").strip()
|
|
93
|
+
kind = str(getattr(info_obj, "kind", "") or "").strip()
|
|
94
|
+
|
|
95
|
+
updates[gid] = {
|
|
96
|
+
"host_id": str(host_id),
|
|
97
|
+
"device_id": str(device_id),
|
|
98
|
+
"label": label,
|
|
99
|
+
"serial": serial,
|
|
100
|
+
"kind": kind,
|
|
101
|
+
"online": bool(is_active),
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
with _state_lock:
|
|
105
|
+
# Upsert / refresh host entries
|
|
106
|
+
for gid, u in updates.items():
|
|
107
|
+
prev = _devices.get(int(gid))
|
|
108
|
+
|
|
109
|
+
# Collision policy: do not overwrite local devices
|
|
110
|
+
if prev is not None and prev.origin == "local":
|
|
111
|
+
# collision shouldn't happen; keep local stable
|
|
112
|
+
continue
|
|
113
|
+
|
|
114
|
+
salt = prev.salt if prev else ""
|
|
115
|
+
hsh = prev.hsh if prev else ""
|
|
116
|
+
lock = prev.io_lock if prev else threading.RLock()
|
|
117
|
+
|
|
118
|
+
label = str(u["label"] or "")
|
|
119
|
+
serial = str(u["serial"] or "")
|
|
120
|
+
kind = str(u["kind"] or "")
|
|
121
|
+
host_id = str(u["host_id"] or "")
|
|
122
|
+
device_id = str(u["device_id"] or "")
|
|
123
|
+
|
|
124
|
+
name = label or (prev.name if prev else f"host-device-{gid}")
|
|
125
|
+
dtype = (kind or (prev.dtype if prev else "")).strip().lower()
|
|
126
|
+
ident = serial or (prev.ident if prev else device_id)
|
|
127
|
+
|
|
128
|
+
_devices[int(gid)] = _DeviceState(
|
|
129
|
+
dev=VirtualDevice(
|
|
130
|
+
gid=int(gid),
|
|
131
|
+
host_id=host_id,
|
|
132
|
+
device_id=device_id,
|
|
133
|
+
label=label,
|
|
134
|
+
serial=serial,
|
|
135
|
+
kind=kind,
|
|
136
|
+
),
|
|
137
|
+
origin="host",
|
|
138
|
+
online=bool(u["online"]),
|
|
139
|
+
salt=salt,
|
|
140
|
+
hsh=hsh,
|
|
141
|
+
name=name,
|
|
142
|
+
ident=ident,
|
|
143
|
+
dtype=dtype,
|
|
144
|
+
io_lock=lock,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Keep legacy maps coherent for host devices too (best-effort)
|
|
148
|
+
devices_info[int(gid)] = name
|
|
149
|
+
if ident:
|
|
150
|
+
device_serialization[int(gid)] = ident
|
|
151
|
+
|
|
152
|
+
# Mark host entries not present in current snap as offline (but keep proxy + auth)
|
|
153
|
+
present = set(updates.keys())
|
|
154
|
+
for gid, st in _devices.items():
|
|
155
|
+
if st.origin == "host" and gid not in present:
|
|
156
|
+
st.online = False
|
|
157
|
+
|
|
158
|
+
def _local_devices_str_snapshot() -> Dict[int, str]:
|
|
159
|
+
out: Dict[int, str] = {}
|
|
160
|
+
with _state_lock:
|
|
161
|
+
for gid, st in _devices.items():
|
|
162
|
+
if st.origin != "local":
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
name = (st.name or f"device-{gid}").strip()
|
|
166
|
+
|
|
167
|
+
status = "online" if _is_connected(st) else "offline"
|
|
168
|
+
if st.salt or st.hsh:
|
|
169
|
+
status = f"{status}, reserved"
|
|
170
|
+
|
|
171
|
+
out[int(gid)] = f"{name} ({status})"
|
|
172
|
+
return out
|
|
173
|
+
|
|
174
|
+
def _host_devices_str_snapshot() -> Dict[int, str]:
|
|
175
|
+
_sync_host_devices() # ensure host devices are up-to-date before snapshot
|
|
176
|
+
reg = _get_host_registry()
|
|
177
|
+
if reg is None:
|
|
178
|
+
return {}
|
|
179
|
+
|
|
180
|
+
snap = reg.device_directory_cached(ttl_ms=0) # device_id -> (host_id, info_obj, is_active)
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
status_map = reg.list_devices() # device_id -> DeviceStatus
|
|
184
|
+
except Exception:
|
|
185
|
+
status_map = {}
|
|
186
|
+
|
|
187
|
+
now_ms = int(time.time() * 1000)
|
|
188
|
+
|
|
189
|
+
out: Dict[int, str] = {}
|
|
190
|
+
for device_id, (host_id, info_obj, is_active) in snap.items():
|
|
191
|
+
# Prefer numeric device_id (old world), otherwise fall back to local_id (compat/UI key).
|
|
192
|
+
try:
|
|
193
|
+
gid = int(str(device_id))
|
|
194
|
+
except Exception:
|
|
195
|
+
try:
|
|
196
|
+
gid = int(getattr(info_obj, "local_id", 0) or 0)
|
|
197
|
+
except Exception:
|
|
198
|
+
continue
|
|
199
|
+
if gid <= 0:
|
|
200
|
+
continue
|
|
201
|
+
|
|
202
|
+
local_id = int(getattr(info_obj, "local_id", 0) or 0)
|
|
203
|
+
label = str(getattr(info_obj, "label", "") or "").strip()
|
|
204
|
+
serial = str(getattr(info_obj, "serial", "") or "").strip()
|
|
205
|
+
kind = str(getattr(info_obj, "kind", "") or "").strip()
|
|
206
|
+
|
|
207
|
+
status = "online" if bool(is_active) else "offline"
|
|
208
|
+
name = label or f"host-device-{gid}"
|
|
209
|
+
|
|
210
|
+
last_seen = ""
|
|
211
|
+
ds = status_map.get(str(device_id))
|
|
212
|
+
if ds is not None:
|
|
213
|
+
try:
|
|
214
|
+
ls = int(getattr(ds, "last_seen_ms", 0) or 0)
|
|
215
|
+
if ls > 0:
|
|
216
|
+
age_s = max(0, (now_ms - ls) // 1000)
|
|
217
|
+
last_seen = f", seen={age_s}s ago"
|
|
218
|
+
except Exception:
|
|
219
|
+
pass
|
|
220
|
+
|
|
221
|
+
out[gid] = (
|
|
222
|
+
f"{name} "
|
|
223
|
+
f"({status})"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
return out
|
|
227
|
+
|
|
228
|
+
# Config paths (server-side)
|
|
229
|
+
|
|
230
|
+
def _cfg_dir() -> Path:
|
|
231
|
+
return Path(os.getenv("REMOTERF_CONFIG_DIR", get_remoterf_root()))
|
|
232
|
+
|
|
233
|
+
def _devices_yaml_path() -> Path:
|
|
234
|
+
p1 = _cfg_dir() / "devices.yml"
|
|
235
|
+
if p1.exists():
|
|
236
|
+
return p1
|
|
237
|
+
return _cfg_dir() / "devices.yaml"
|
|
238
|
+
|
|
239
|
+
def _load_device_records() -> Dict[int, Dict[str, str]]:
|
|
240
|
+
path = _devices_yaml_path()
|
|
241
|
+
if not path.exists():
|
|
242
|
+
return {}
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
with path.open("r", encoding="utf-8") as f:
|
|
246
|
+
data = yaml.safe_load(f) or {}
|
|
247
|
+
except Exception as e:
|
|
248
|
+
print(f"Error reading YAML config {path}: {e}")
|
|
249
|
+
return {}
|
|
250
|
+
|
|
251
|
+
devices = data.get("devices") or []
|
|
252
|
+
if not isinstance(devices, list):
|
|
253
|
+
print(f"Invalid YAML config {path}: 'devices' must be a list")
|
|
254
|
+
return {}
|
|
255
|
+
|
|
256
|
+
recs: Dict[int, Dict[str, str]] = {}
|
|
257
|
+
|
|
258
|
+
for item in devices:
|
|
259
|
+
if not isinstance(item, dict):
|
|
260
|
+
continue
|
|
261
|
+
|
|
262
|
+
try:
|
|
263
|
+
gid = int(item["device_id"])
|
|
264
|
+
except Exception:
|
|
265
|
+
print(f"Skipping device entry with invalid/missing device_id: {item}")
|
|
266
|
+
continue
|
|
267
|
+
|
|
268
|
+
dtype = str(item.get("device_type") or "pluto").strip().lower()
|
|
269
|
+
if dtype != "pluto":
|
|
270
|
+
print(f"Skipping unsupported device_type={dtype!r} for gid={gid}")
|
|
271
|
+
continue
|
|
272
|
+
|
|
273
|
+
name = str(item.get("name") or f"device-{gid}").strip()
|
|
274
|
+
init = item.get("init") or {}
|
|
275
|
+
if not isinstance(init, dict):
|
|
276
|
+
init = {}
|
|
277
|
+
|
|
278
|
+
serial = str(init.get("serial") or "").strip()
|
|
279
|
+
if not serial:
|
|
280
|
+
print(f"Skipping Pluto gid={gid}: missing init.serial")
|
|
281
|
+
continue
|
|
282
|
+
|
|
283
|
+
recs[gid] = {
|
|
284
|
+
"TYPE": "pluto",
|
|
285
|
+
"NAME": name,
|
|
286
|
+
"IDENT_KIND": "iio_serial",
|
|
287
|
+
"IDENT": serial,
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return recs
|
|
291
|
+
|
|
292
|
+
# Pluto helpers
|
|
293
|
+
|
|
294
|
+
def connect_pluto(*, ip: str = "", usb: str = ""):
|
|
295
|
+
try:
|
|
296
|
+
if ip == "":
|
|
297
|
+
dev = adi.Pluto(f"usb:{usb}")
|
|
298
|
+
print(f"Connected to Pluto usb:{usb}")
|
|
299
|
+
else:
|
|
300
|
+
dev = adi.Pluto(f"ip:{ip}")
|
|
301
|
+
print(f"Connected to Pluto ip:{ip}")
|
|
302
|
+
return dev
|
|
303
|
+
except Exception as e:
|
|
304
|
+
print(f"Pluto {ip}: {e}")
|
|
305
|
+
return None
|
|
306
|
+
|
|
307
|
+
def get_usb_port_from_serial(serial: str) -> str | None:
|
|
308
|
+
serial = (serial or "").strip()
|
|
309
|
+
if not serial:
|
|
310
|
+
return None
|
|
311
|
+
|
|
312
|
+
try:
|
|
313
|
+
out = subprocess.check_output(["iio_info", "-s"], text=True, stderr=subprocess.STDOUT)
|
|
314
|
+
except Exception as e:
|
|
315
|
+
print(f"Error running iio_info: {e}")
|
|
316
|
+
return None
|
|
317
|
+
|
|
318
|
+
for line in out.splitlines():
|
|
319
|
+
if (f"serial={serial}" in line) or (f"hw_serial={serial}" in line):
|
|
320
|
+
m = re.search(r"\[usb:([^\]]+)\]", line)
|
|
321
|
+
if m:
|
|
322
|
+
return m.group(1).strip()
|
|
323
|
+
|
|
324
|
+
print(f"No device found with serial {serial}")
|
|
325
|
+
return None
|
|
326
|
+
|
|
327
|
+
def _connect_from_record(rec: Dict[str, str]):
|
|
328
|
+
# Returns (device_obj, ident, dtype).
|
|
329
|
+
|
|
330
|
+
dtype = (rec.get("TYPE") or "").strip().lower()
|
|
331
|
+
ident_kind = (rec.get("IDENT_KIND") or "").strip().lower()
|
|
332
|
+
ident = (rec.get("IDENT") or "").strip()
|
|
333
|
+
|
|
334
|
+
if dtype != "pluto":
|
|
335
|
+
return (None, ident, dtype)
|
|
336
|
+
|
|
337
|
+
if ident_kind in ("iio_serial", "serial", "iio"):
|
|
338
|
+
if not ident:
|
|
339
|
+
return (None, ident, dtype)
|
|
340
|
+
usb_port = get_usb_port_from_serial(ident)
|
|
341
|
+
if not usb_port:
|
|
342
|
+
return (None, ident, dtype)
|
|
343
|
+
return (connect_pluto(usb=usb_port), ident, dtype)
|
|
344
|
+
|
|
345
|
+
if ident_kind == "usb":
|
|
346
|
+
if not ident:
|
|
347
|
+
return (None, ident, dtype)
|
|
348
|
+
return (connect_pluto(usb=ident), ident, dtype)
|
|
349
|
+
|
|
350
|
+
if ident_kind == "ip":
|
|
351
|
+
if not ident:
|
|
352
|
+
return (None, ident, dtype)
|
|
353
|
+
return (connect_pluto(ip=ident), ident, dtype)
|
|
354
|
+
|
|
355
|
+
return (None, ident, dtype)
|
|
356
|
+
|
|
357
|
+
# Thread-safe runtime state
|
|
358
|
+
|
|
359
|
+
# legacy maps (kept for your existing server calls/UI)
|
|
360
|
+
devices_info: Dict[int, str] = {}
|
|
361
|
+
device_serialization: Dict[int, str] = {}
|
|
362
|
+
|
|
363
|
+
# master token (overrideable)
|
|
364
|
+
master_token = os.getenv("REMOTERF_MASTER_TOKEN", "SuperCoolTokenForIan")
|
|
365
|
+
|
|
366
|
+
def _init_from_env() -> None:
|
|
367
|
+
records = _load_device_records()
|
|
368
|
+
|
|
369
|
+
tmp_devices: Dict[int, _DeviceState] = {}
|
|
370
|
+
tmp_info: Dict[int, str] = {}
|
|
371
|
+
tmp_ser: Dict[int, str] = {}
|
|
372
|
+
|
|
373
|
+
# Build + connect (do the expensive work without holding the global lock)
|
|
374
|
+
for gid, rec in sorted(records.items()):
|
|
375
|
+
name = (rec.get("NAME") or f"device-{gid}").strip()
|
|
376
|
+
ident = (rec.get("IDENT") or "").strip()
|
|
377
|
+
dtype = (rec.get("TYPE") or "").strip().lower()
|
|
378
|
+
|
|
379
|
+
dev, ident2, dtype2 = _connect_from_record(rec)
|
|
380
|
+
|
|
381
|
+
tmp_info[int(gid)] = name
|
|
382
|
+
if ident:
|
|
383
|
+
tmp_ser[int(gid)] = ident
|
|
384
|
+
|
|
385
|
+
tmp_devices[int(gid)] = _DeviceState(
|
|
386
|
+
dev=dev,
|
|
387
|
+
origin="local",
|
|
388
|
+
online=(dev is not None),
|
|
389
|
+
salt="",
|
|
390
|
+
hsh="",
|
|
391
|
+
name=name,
|
|
392
|
+
ident=ident2,
|
|
393
|
+
dtype=dtype2 or dtype,
|
|
394
|
+
io_lock=threading.RLock(),
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
# Swap atomically
|
|
398
|
+
with _state_lock:
|
|
399
|
+
_devices.clear()
|
|
400
|
+
_devices.update(tmp_devices)
|
|
401
|
+
|
|
402
|
+
devices_info.clear()
|
|
403
|
+
devices_info.update(tmp_info)
|
|
404
|
+
|
|
405
|
+
device_serialization.clear()
|
|
406
|
+
device_serialization.update(tmp_ser)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
# initialize on import
|
|
410
|
+
_init_from_env()
|
|
411
|
+
|
|
412
|
+
# Legacy reservation helpers (master token parsing)
|
|
413
|
+
|
|
414
|
+
def parse_mastertoken(token: str):
|
|
415
|
+
if not token:
|
|
416
|
+
return None
|
|
417
|
+
prefix = re.escape(master_token)
|
|
418
|
+
pattern = re.compile(rf"^{prefix}[_-](\d+)(?:_force)?$")
|
|
419
|
+
m = pattern.match(token)
|
|
420
|
+
if not m:
|
|
421
|
+
return None
|
|
422
|
+
device_id = int(m.group(1))
|
|
423
|
+
force = token.endswith("_force")
|
|
424
|
+
return (device_id, force)
|
|
425
|
+
|
|
426
|
+
# Transmitter stubs (legacy)
|
|
427
|
+
|
|
428
|
+
_transmitter = None
|
|
429
|
+
|
|
430
|
+
def start_transmitter():
|
|
431
|
+
pass
|
|
432
|
+
|
|
433
|
+
def terminate_transmitter():
|
|
434
|
+
pass
|
|
435
|
+
|
|
436
|
+
def get_transmitter_state() -> bool:
|
|
437
|
+
return False
|
|
438
|
+
|
|
439
|
+
# Legacy API (state-safe)
|
|
440
|
+
|
|
441
|
+
def get_all_devices() -> Dict[int, Tuple[object, str, str]]:
|
|
442
|
+
_sync_host_devices()
|
|
443
|
+
|
|
444
|
+
with _state_lock:
|
|
445
|
+
out: Dict[int, Tuple[object, str, str]] = {}
|
|
446
|
+
for gid, st in _devices.items():
|
|
447
|
+
if _is_connected(st):
|
|
448
|
+
out[gid] = (st.dev, st.salt, st.hsh) # dev may be VirtualDevice for host
|
|
449
|
+
return out
|
|
450
|
+
|
|
451
|
+
def get_all_devices_str() -> Dict[int, str]:
|
|
452
|
+
_sync_host_devices()
|
|
453
|
+
|
|
454
|
+
out = _local_devices_str_snapshot()
|
|
455
|
+
host_out = _host_devices_str_snapshot()
|
|
456
|
+
|
|
457
|
+
# merge with collision handling (shouldn't happen; keeps UI stable if it does)
|
|
458
|
+
for gid, s in host_out.items():
|
|
459
|
+
if gid in out:
|
|
460
|
+
alt = -int(gid) - 1
|
|
461
|
+
out[alt] = s + " [gid-collision]"
|
|
462
|
+
else:
|
|
463
|
+
out[gid] = s
|
|
464
|
+
|
|
465
|
+
return dict(sorted(out.items(), key=lambda kv: int(kv[0])))
|
|
466
|
+
|
|
467
|
+
def set_device(device_id: int, salt: str, hash: str):
|
|
468
|
+
# ensure host devices are upserted so reservations can apply to them too
|
|
469
|
+
_sync_host_devices()
|
|
470
|
+
|
|
471
|
+
with _state_lock:
|
|
472
|
+
st = _devices.get(int(device_id))
|
|
473
|
+
|
|
474
|
+
if not st:
|
|
475
|
+
_sync_host_devices(force=True)
|
|
476
|
+
with _state_lock:
|
|
477
|
+
st = _devices.get(int(device_id))
|
|
478
|
+
if not st:
|
|
479
|
+
return
|
|
480
|
+
|
|
481
|
+
with _state_lock:
|
|
482
|
+
st = _devices.get(int(device_id))
|
|
483
|
+
if not st:
|
|
484
|
+
return
|
|
485
|
+
st.salt = str(salt or "")
|
|
486
|
+
st.hsh = str(hash or "")
|
|
487
|
+
|
|
488
|
+
def device_exists(device_id: int) -> bool:
|
|
489
|
+
did = int(device_id)
|
|
490
|
+
|
|
491
|
+
_sync_host_devices()
|
|
492
|
+
|
|
493
|
+
# local: connected only (unchanged semantics)
|
|
494
|
+
with _state_lock:
|
|
495
|
+
st = _devices.get(did)
|
|
496
|
+
if st and st.origin == "local" and st.dev is not None:
|
|
497
|
+
return True
|
|
498
|
+
|
|
499
|
+
# host: existence as known by registry (unchanged)
|
|
500
|
+
reg = _get_host_registry()
|
|
501
|
+
if reg is None:
|
|
502
|
+
return False
|
|
503
|
+
return bool(reg.is_host_device(str(did)))
|
|
504
|
+
|
|
505
|
+
def device_is_available(device_id: int) -> bool:
|
|
506
|
+
_sync_host_devices()
|
|
507
|
+
|
|
508
|
+
with _state_lock:
|
|
509
|
+
st = _devices.get(int(device_id))
|
|
510
|
+
if not st or not _is_connected(st):
|
|
511
|
+
return False
|
|
512
|
+
return (st.salt == "") and (st.hsh == "")
|
|
513
|
+
|
|
514
|
+
def get_device_by_id(device_id: int):
|
|
515
|
+
_sync_host_devices()
|
|
516
|
+
|
|
517
|
+
with _state_lock:
|
|
518
|
+
st = _devices.get(int(device_id))
|
|
519
|
+
if not st:
|
|
520
|
+
return None
|
|
521
|
+
return (st.dev, st.salt, st.hsh)
|
|
522
|
+
|
|
523
|
+
def get_device(*, api_token: str):
|
|
524
|
+
if not api_token:
|
|
525
|
+
return None
|
|
526
|
+
|
|
527
|
+
_sync_host_devices()
|
|
528
|
+
|
|
529
|
+
with _state_lock:
|
|
530
|
+
snapshot: List[Tuple[int, object, str, str, str]] = [
|
|
531
|
+
(gid, st.dev, st.salt, st.hsh, st.origin)
|
|
532
|
+
for gid, st in _devices.items()
|
|
533
|
+
if _is_connected(st)
|
|
534
|
+
]
|
|
535
|
+
|
|
536
|
+
# Preserve "master prefers local" behavior
|
|
537
|
+
if api_token == master_token:
|
|
538
|
+
# local first
|
|
539
|
+
for _, dev, salt, hsh, origin in snapshot:
|
|
540
|
+
if origin == "local" and dev is not None and salt == "" and hsh == "":
|
|
541
|
+
return dev
|
|
542
|
+
# then host
|
|
543
|
+
for _, dev, salt, hsh, origin in snapshot:
|
|
544
|
+
if origin == "host" and dev is not None and salt == "" and hsh == "":
|
|
545
|
+
return dev
|
|
546
|
+
return None
|
|
547
|
+
|
|
548
|
+
parsed = parse_mastertoken(api_token)
|
|
549
|
+
if parsed:
|
|
550
|
+
device_id, force = parsed
|
|
551
|
+
with _state_lock:
|
|
552
|
+
st = _devices.get(int(device_id))
|
|
553
|
+
if not st or not _is_connected(st):
|
|
554
|
+
return None
|
|
555
|
+
if force:
|
|
556
|
+
return st.dev
|
|
557
|
+
return st.dev if (st.salt == "" and st.hsh == "") else None
|
|
558
|
+
|
|
559
|
+
for _, dev, salt, hsh, _origin in snapshot:
|
|
560
|
+
if dev is None:
|
|
561
|
+
continue
|
|
562
|
+
if salt and hsh and validate_token(salt, hsh, api_token):
|
|
563
|
+
return dev
|
|
564
|
+
|
|
565
|
+
return None
|
|
566
|
+
|
|
567
|
+
# Per-device I/O locking (true concurrency support)
|
|
568
|
+
|
|
569
|
+
@contextmanager
|
|
570
|
+
def acquire_device(api_token: str) -> Iterator[Tuple[int, object]]:
|
|
571
|
+
if not api_token:
|
|
572
|
+
raise RuntimeError("missing api_token")
|
|
573
|
+
|
|
574
|
+
_sync_host_devices()
|
|
575
|
+
|
|
576
|
+
gid: Optional[int] = None
|
|
577
|
+
|
|
578
|
+
if api_token == master_token:
|
|
579
|
+
with _state_lock:
|
|
580
|
+
# prefer local first
|
|
581
|
+
for k, st in _devices.items():
|
|
582
|
+
if st.origin == "local" and _is_connected(st) and st.salt == "" and st.hsh == "":
|
|
583
|
+
gid = k
|
|
584
|
+
break
|
|
585
|
+
if gid is None:
|
|
586
|
+
for k, st in _devices.items():
|
|
587
|
+
if st.origin == "host" and _is_connected(st) and st.salt == "" and st.hsh == "":
|
|
588
|
+
gid = k
|
|
589
|
+
break
|
|
590
|
+
else:
|
|
591
|
+
parsed = parse_mastertoken(api_token)
|
|
592
|
+
if parsed:
|
|
593
|
+
cand, force = parsed
|
|
594
|
+
with _state_lock:
|
|
595
|
+
st = _devices.get(int(cand))
|
|
596
|
+
if st and _is_connected(st):
|
|
597
|
+
if force or (st.salt == "" and st.hsh == ""):
|
|
598
|
+
gid = int(cand)
|
|
599
|
+
else:
|
|
600
|
+
with _state_lock:
|
|
601
|
+
snapshot = [(k, st.dev, st.salt, st.hsh) for k, st in _devices.items() if _is_connected(st)]
|
|
602
|
+
for k, dev, salt, hsh in snapshot:
|
|
603
|
+
if salt and hsh and validate_token(salt, hsh, api_token):
|
|
604
|
+
gid = k
|
|
605
|
+
break
|
|
606
|
+
|
|
607
|
+
if gid is None:
|
|
608
|
+
raise RuntimeError("no device available / invalid token")
|
|
609
|
+
|
|
610
|
+
with _state_lock:
|
|
611
|
+
st = _devices.get(int(gid))
|
|
612
|
+
if not st or not _is_connected(st) or st.dev is None:
|
|
613
|
+
raise RuntimeError("device disappeared / not connected")
|
|
614
|
+
lock = st.io_lock
|
|
615
|
+
dev = st.dev
|
|
616
|
+
|
|
617
|
+
lock.acquire()
|
|
618
|
+
try:
|
|
619
|
+
yield int(gid), dev
|
|
620
|
+
finally:
|
|
621
|
+
lock.release()
|
|
622
|
+
|
|
623
|
+
# Optional: reload device definitions (atomic swap)
|
|
624
|
+
|
|
625
|
+
def reload_devices() -> None:
|
|
626
|
+
"""
|
|
627
|
+
Reload devices.env and reconnect local devices.
|
|
628
|
+
Preserves existing salt/hash for devices that remain (same gid),
|
|
629
|
+
and preserves host virtual devices + their auth state.
|
|
630
|
+
"""
|
|
631
|
+
records = _load_device_records()
|
|
632
|
+
|
|
633
|
+
_sync_host_devices() # ensure host entries exist before snapshot
|
|
634
|
+
|
|
635
|
+
with _state_lock:
|
|
636
|
+
prev_auth = {gid: (st.salt, st.hsh) for gid, st in _devices.items()}
|
|
637
|
+
prev_host = {gid: st for gid, st in _devices.items() if st.origin == "host"}
|
|
638
|
+
|
|
639
|
+
tmp_local: Dict[int, _DeviceState] = {}
|
|
640
|
+
tmp_info: Dict[int, str] = {}
|
|
641
|
+
tmp_ser: Dict[int, str] = {}
|
|
642
|
+
|
|
643
|
+
for gid, rec in sorted(records.items()):
|
|
644
|
+
name = (rec.get("NAME") or f"device-{gid}").strip()
|
|
645
|
+
ident = (rec.get("IDENT") or "").strip()
|
|
646
|
+
|
|
647
|
+
dev, ident2, dtype2 = _connect_from_record(rec)
|
|
648
|
+
|
|
649
|
+
tmp_info[int(gid)] = name
|
|
650
|
+
if ident:
|
|
651
|
+
tmp_ser[int(gid)] = ident
|
|
652
|
+
|
|
653
|
+
salt, hsh = prev_auth.get(int(gid), ("", ""))
|
|
654
|
+
tmp_local[int(gid)] = _DeviceState(
|
|
655
|
+
dev=dev,
|
|
656
|
+
origin="local",
|
|
657
|
+
online=(dev is not None),
|
|
658
|
+
salt=salt,
|
|
659
|
+
hsh=hsh,
|
|
660
|
+
name=name,
|
|
661
|
+
ident=ident2,
|
|
662
|
+
dtype=dtype2,
|
|
663
|
+
io_lock=threading.RLock(),
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
with _state_lock:
|
|
667
|
+
_devices.clear()
|
|
668
|
+
|
|
669
|
+
# restore host entries (but do not overwrite locals if collision)
|
|
670
|
+
for gid, st in prev_host.items():
|
|
671
|
+
if gid not in tmp_local:
|
|
672
|
+
_devices[gid] = st
|
|
673
|
+
|
|
674
|
+
# add locals
|
|
675
|
+
_devices.update(tmp_local)
|
|
676
|
+
|
|
677
|
+
devices_info.clear()
|
|
678
|
+
devices_info.update(tmp_info)
|
|
679
|
+
|
|
680
|
+
device_serialization.clear()
|
|
681
|
+
device_serialization.update(tmp_ser)
|
|
682
|
+
|
|
683
|
+
# refresh host online/offline status after reload (best-effort)
|
|
684
|
+
_sync_host_devices(force=True)
|
|
685
|
+
|
|
686
|
+
def set_pluto(ip: str = "192.168.2.1"):
|
|
687
|
+
# kept for compatibility (no-op)
|
|
688
|
+
pass
|