remoteRF-server 0.1.95__tar.gz → 0.1.96__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.
Files changed (42) hide show
  1. {remoterf_server-0.1.95 → remoterf_server-0.1.96}/PKG-INFO +2 -2
  2. {remoterf_server-0.1.95 → remoterf_server-0.1.96}/README.md +1 -1
  3. {remoterf_server-0.1.95 → remoterf_server-0.1.96}/pyproject.toml +1 -1
  4. {remoterf_server-0.1.95 → remoterf_server-0.1.96}/src/remoteRF_server/server/device_manager.py +292 -133
  5. {remoterf_server-0.1.95 → remoterf_server-0.1.96}/src/remoteRF_server/server/grpc_server.py +4 -4
  6. {remoterf_server-0.1.95 → remoterf_server-0.1.96}/src/remoteRF_server.egg-info/PKG-INFO +2 -2
  7. {remoterf_server-0.1.95 → remoterf_server-0.1.96}/setup.cfg +0 -0
  8. {remoterf_server-0.1.95 → remoterf_server-0.1.96}/src/remoteRF_server/__init__.py +0 -0
  9. {remoterf_server-0.1.95 → remoterf_server-0.1.96}/src/remoteRF_server/common/__init__.py +0 -0
  10. {remoterf_server-0.1.95 → remoterf_server-0.1.96}/src/remoteRF_server/common/grpc/__init__.py +0 -0
  11. {remoterf_server-0.1.95 → remoterf_server-0.1.96}/src/remoteRF_server/common/grpc/grpc_host_pb2.py +0 -0
  12. {remoterf_server-0.1.95 → remoterf_server-0.1.96}/src/remoteRF_server/common/grpc/grpc_host_pb2_grpc.py +0 -0
  13. {remoterf_server-0.1.95 → remoterf_server-0.1.96}/src/remoteRF_server/common/grpc/grpc_pb2.py +0 -0
  14. {remoterf_server-0.1.95 → remoterf_server-0.1.96}/src/remoteRF_server/common/grpc/grpc_pb2_grpc.py +0 -0
  15. {remoterf_server-0.1.95 → remoterf_server-0.1.96}/src/remoteRF_server/common/utils/__init__.py +0 -0
  16. {remoterf_server-0.1.95 → remoterf_server-0.1.96}/src/remoteRF_server/common/utils/ansi_codes.py +0 -0
  17. {remoterf_server-0.1.95 → remoterf_server-0.1.96}/src/remoteRF_server/common/utils/api_token.py +0 -0
  18. {remoterf_server-0.1.95 → remoterf_server-0.1.96}/src/remoteRF_server/common/utils/db_connection.py +0 -0
  19. {remoterf_server-0.1.95 → remoterf_server-0.1.96}/src/remoteRF_server/common/utils/db_location.py +0 -0
  20. {remoterf_server-0.1.95 → remoterf_server-0.1.96}/src/remoteRF_server/common/utils/list_string.py +0 -0
  21. {remoterf_server-0.1.95 → remoterf_server-0.1.96}/src/remoteRF_server/common/utils/process_arg.py +0 -0
  22. {remoterf_server-0.1.95 → remoterf_server-0.1.96}/src/remoteRF_server/drivers/__init__.py +0 -0
  23. {remoterf_server-0.1.95 → remoterf_server-0.1.96}/src/remoteRF_server/drivers/adalm_pluto/__init__.py +0 -0
  24. {remoterf_server-0.1.95 → remoterf_server-0.1.96}/src/remoteRF_server/drivers/adalm_pluto/pluto_remote_server.py +0 -0
  25. {remoterf_server-0.1.95 → remoterf_server-0.1.96}/src/remoteRF_server/host/__init__.py +0 -0
  26. {remoterf_server-0.1.95 → remoterf_server-0.1.96}/src/remoteRF_server/host/host_directory_store.py +0 -0
  27. {remoterf_server-0.1.95 → remoterf_server-0.1.96}/src/remoteRF_server/host/host_tunnel_server.py +0 -0
  28. {remoterf_server-0.1.95 → remoterf_server-0.1.96}/src/remoteRF_server/server/__init__.py +0 -0
  29. {remoterf_server-0.1.95 → remoterf_server-0.1.96}/src/remoteRF_server/server/acc_perms.py +0 -0
  30. {remoterf_server-0.1.95 → remoterf_server-0.1.96}/src/remoteRF_server/server/cert_provider.py +0 -0
  31. {remoterf_server-0.1.95 → remoterf_server-0.1.96}/src/remoteRF_server/server/reservation.py +0 -0
  32. {remoterf_server-0.1.95 → remoterf_server-0.1.96}/src/remoteRF_server/server/rpc_manager.py +0 -0
  33. {remoterf_server-0.1.95 → remoterf_server-0.1.96}/src/remoteRF_server/server/user_group_cli.py +0 -0
  34. {remoterf_server-0.1.95 → remoterf_server-0.1.96}/src/remoteRF_server/server/user_group_handler.py +0 -0
  35. {remoterf_server-0.1.95 → remoterf_server-0.1.96}/src/remoteRF_server/serverrf_cli.py +0 -0
  36. {remoterf_server-0.1.95 → remoterf_server-0.1.96}/src/remoteRF_server/tools/__init__.py +0 -0
  37. {remoterf_server-0.1.95 → remoterf_server-0.1.96}/src/remoteRF_server/tools/gen_certs.py +0 -0
  38. {remoterf_server-0.1.95 → remoterf_server-0.1.96}/src/remoteRF_server.egg-info/SOURCES.txt +0 -0
  39. {remoterf_server-0.1.95 → remoterf_server-0.1.96}/src/remoteRF_server.egg-info/dependency_links.txt +0 -0
  40. {remoterf_server-0.1.95 → remoterf_server-0.1.96}/src/remoteRF_server.egg-info/entry_points.txt +0 -0
  41. {remoterf_server-0.1.95 → remoterf_server-0.1.96}/src/remoteRF_server.egg-info/requires.txt +0 -0
  42. {remoterf_server-0.1.95 → remoterf_server-0.1.96}/src/remoteRF_server.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remoteRF-server
3
- Version: 0.1.95
3
+ Version: 0.1.96
4
4
  Description-Content-Type: text/markdown
5
5
  Requires-Dist: grpcio
6
6
  Requires-Dist: protobuf
@@ -350,7 +350,7 @@ todo:
350
350
 
351
351
  <!-- Host ID (NEEDS TO BE UNIQUE AND TELL THE HOST THAT) (or if device also does it as well.) -->
352
352
 
353
- ServerRF display meta data as part of devices list / reservation
353
+ <!-- ServerRF display meta data as part of devices list / reservation -->
354
354
 
355
355
  + online/activity status (appended into device name) + (plus host side warning if the device id is NOT globally unique!)
356
356
 
@@ -339,7 +339,7 @@ todo:
339
339
 
340
340
  <!-- Host ID (NEEDS TO BE UNIQUE AND TELL THE HOST THAT) (or if device also does it as well.) -->
341
341
 
342
- ServerRF display meta data as part of devices list / reservation
342
+ <!-- ServerRF display meta data as part of devices list / reservation -->
343
343
 
344
344
  + online/activity status (appended into device name) + (plus host side warning if the device id is NOT globally unique!)
345
345
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "remoteRF-server"
7
- version = "0.1.95"
7
+ version = "0.1.96"
8
8
  dependencies = [
9
9
  "grpcio",
10
10
  "protobuf",
@@ -2,47 +2,188 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import hashlib
6
- from typing import Dict, Tuple, Optional, Any
7
-
8
5
  import os
9
6
  import re
7
+ import time
8
+ import adi
10
9
  import subprocess
11
10
  import threading
12
- from dataclasses import dataclass
11
+ from dataclasses import dataclass, field
13
12
  from pathlib import Path
14
- from typing import Dict, Optional, Tuple, List, Iterator
13
+ from typing import Dict, Tuple, Optional, Any, List, Iterator
15
14
  from contextlib import contextmanager
16
15
 
17
- import adi
18
-
19
16
  from ..common.utils import validate_token # your salt/hash validator
20
17
  from ..host import host_tunnel_server as hts # gives access to hts.TUNNEL_REGISTRY
21
18
 
22
- import time # add near imports
23
-
24
19
  _state_lock = threading.RLock()
25
20
 
21
+
22
+ # ============================================================
23
+ # Virtual device (server-side proxy for host-managed hardware)
24
+ # ============================================================
25
+
26
+ @dataclass(frozen=True)
27
+ class VirtualDevice:
28
+ """
29
+ Server-side proxy for a host-managed device.
30
+
31
+ IMPORTANT:
32
+ - This is NOT an adi device.
33
+ - It only carries routing metadata so your pipeline can forward later.
34
+ """
35
+ gid: int
36
+ host_id: str
37
+ device_id: str # whatever key the host directory uses (stringified)
38
+ label: str = ""
39
+ serial: str = ""
40
+ kind: str = ""
41
+
42
+ def __repr__(self) -> str:
43
+ return f"<VirtualDevice gid={self.gid} host={self.host_id} device_id={self.device_id}>"
44
+
45
+
46
+ def is_virtual(dev: object) -> bool:
47
+ return isinstance(dev, VirtualDevice)
48
+
49
+
26
50
  @dataclass
27
51
  class _DeviceState:
28
- dev: object
52
+ dev: object | None
53
+ origin: str # "local" or "host"
54
+ online: bool # for host devices; for local can mirror (dev is not None)
29
55
  salt: str
30
56
  hsh: str
31
57
  name: str
32
58
  ident: str
33
59
  dtype: str
34
- io_lock: threading.RLock
60
+ io_lock: threading.RLock = field(default_factory=threading.RLock)
61
+
35
62
 
36
63
  # gid -> state
37
64
  _devices: Dict[int, _DeviceState] = {}
38
65
 
39
- # -----------------------------
66
+
67
+ def _is_connected(st: _DeviceState) -> bool:
68
+ if st.origin == "local":
69
+ return st.dev is not None
70
+ # host
71
+ return bool(st.online)
72
+
73
+
74
+ # ============================================================
40
75
  # Host device pulling/grabbing
41
- # -----------------------------
76
+ # ============================================================
42
77
 
43
78
  def _get_host_registry():
44
79
  return hts.get_tunnel_registry()
45
80
 
81
+
82
+ _host_sync_lock = threading.RLock()
83
+ _host_sync_last_ms = 0
84
+ _HOST_SYNC_TTL_MS = 500 # tune later
85
+
86
+
87
+ def _sync_host_devices(*, force: bool = False) -> None:
88
+ """
89
+ Pull host directory and upsert host devices into _devices using their GID.
90
+
91
+ - Does NOT touch local devices (origin="local")
92
+ - Preserves salt/hsh/io_lock for existing entries
93
+ - Marks missing host devices offline (keeps their proxy for reservations)
94
+ - Updates devices_info/device_serialization for host devices (best-effort)
95
+ """
96
+ global _host_sync_last_ms
97
+
98
+ now_ms = int(time.time() * 1000)
99
+ with _host_sync_lock:
100
+ if not force and (now_ms - _host_sync_last_ms) < _HOST_SYNC_TTL_MS:
101
+ return
102
+ _host_sync_last_ms = now_ms
103
+
104
+ reg = _get_host_registry()
105
+ if reg is None:
106
+ return
107
+
108
+ # Do registry calls without holding _state_lock
109
+ snap = reg.device_directory_cached(ttl_ms=0) # device_id -> (host_id, info_obj, is_active)
110
+
111
+ updates: Dict[int, Dict[str, object]] = {}
112
+ for device_id, (host_id, info_obj, is_active) in snap.items():
113
+ # You said device_id is assumed to be the GID
114
+ try:
115
+ gid = int(str(device_id))
116
+ except Exception:
117
+ continue
118
+
119
+ label = str(getattr(info_obj, "label", "") or "").strip()
120
+ serial = str(getattr(info_obj, "serial", "") or "").strip()
121
+ kind = str(getattr(info_obj, "kind", "") or "").strip()
122
+
123
+ updates[gid] = {
124
+ "host_id": str(host_id),
125
+ "device_id": str(device_id),
126
+ "label": label,
127
+ "serial": serial,
128
+ "kind": kind,
129
+ "online": bool(is_active),
130
+ }
131
+
132
+ with _state_lock:
133
+ # Upsert / refresh host entries
134
+ for gid, u in updates.items():
135
+ prev = _devices.get(int(gid))
136
+
137
+ # Collision policy: do not overwrite local devices
138
+ if prev is not None and prev.origin == "local":
139
+ # collision shouldn't happen; keep local stable
140
+ continue
141
+
142
+ salt = prev.salt if prev else ""
143
+ hsh = prev.hsh if prev else ""
144
+ lock = prev.io_lock if prev else threading.RLock()
145
+
146
+ label = str(u["label"] or "")
147
+ serial = str(u["serial"] or "")
148
+ kind = str(u["kind"] or "")
149
+ host_id = str(u["host_id"] or "")
150
+ device_id = str(u["device_id"] or "")
151
+
152
+ name = label or (prev.name if prev else f"host-device-{gid}")
153
+ dtype = (kind or (prev.dtype if prev else "")).strip().lower()
154
+ ident = serial or (prev.ident if prev else device_id)
155
+
156
+ _devices[int(gid)] = _DeviceState(
157
+ dev=VirtualDevice(
158
+ gid=int(gid),
159
+ host_id=host_id,
160
+ device_id=device_id,
161
+ label=label,
162
+ serial=serial,
163
+ kind=kind,
164
+ ),
165
+ origin="host",
166
+ online=bool(u["online"]),
167
+ salt=salt,
168
+ hsh=hsh,
169
+ name=name,
170
+ ident=ident,
171
+ dtype=dtype,
172
+ io_lock=lock,
173
+ )
174
+
175
+ # Keep legacy maps coherent for host devices too (best-effort)
176
+ devices_info[int(gid)] = name
177
+ if ident:
178
+ device_serialization[int(gid)] = ident
179
+
180
+ # Mark host entries not present in current snap as offline (but keep proxy + auth)
181
+ present = set(updates.keys())
182
+ for gid, st in _devices.items():
183
+ if st.origin == "host" and gid not in present:
184
+ st.online = False
185
+
186
+
46
187
  def _local_devices_str_snapshot() -> Dict[int, str]:
47
188
  """
48
189
  Local devices from devices.env (whether connected or not).
@@ -50,8 +191,11 @@ def _local_devices_str_snapshot() -> Dict[int, str]:
50
191
  out: Dict[int, str] = {}
51
192
  with _state_lock:
52
193
  for gid, st in _devices.items():
194
+ if st.origin != "local":
195
+ continue
196
+
53
197
  name = (st.name or f"device-{gid}").strip()
54
- status = "online" if (st.dev is not None) else "offline"
198
+ status = "online" if _is_connected(st) else "offline"
55
199
 
56
200
  dtype = (st.dtype or "").strip()
57
201
  ident = (st.ident or "").strip()
@@ -62,89 +206,64 @@ def _local_devices_str_snapshot() -> Dict[int, str]:
62
206
  if ident:
63
207
  meta_parts.append(f"ident={ident}")
64
208
 
65
- # optional: show reservation state
66
209
  if st.salt or st.hsh:
67
210
  status = status + ", reserved"
68
211
 
69
212
  out[int(gid)] = f"{name} [{', '.join(meta_parts)}] ({status})"
70
213
  return out
71
214
 
215
+
72
216
  def _host_devices_str_snapshot() -> Dict[int, str]:
217
+ _sync_host_devices(force=True)
218
+
73
219
  reg = _get_host_registry()
74
220
  if reg is None:
75
221
  return {}
76
222
 
77
- snap = reg.device_directory_cached(ttl_ms=0) # device_id -> (host_id, info_obj, is_active)
78
-
79
- try:
80
- status_map = reg.list_devices() # device_id -> DeviceStatus
81
- except Exception:
82
- status_map = {}
83
-
84
- now_ms = int(time.time() * 1000)
223
+ snap = reg.device_directory_cached(ttl_ms=0)
85
224
 
86
225
  out: Dict[int, str] = {}
87
226
  for device_id, (host_id, info_obj, is_active) in snap.items():
88
- # Prefer numeric device_id (old world), otherwise fall back to local_id (compat/UI key).
89
227
  try:
90
228
  gid = int(str(device_id))
91
229
  except Exception:
92
- try:
93
- gid = int(getattr(info_obj, "local_id", 0) or 0)
94
- except Exception:
95
- continue
96
- if gid <= 0:
97
- continue
230
+ continue
98
231
 
99
- local_id = int(getattr(info_obj, "local_id", 0) or 0)
100
232
  label = str(getattr(info_obj, "label", "") or "").strip()
101
- serial = str(getattr(info_obj, "serial", "") or "").strip()
102
- kind = str(getattr(info_obj, "kind", "") or "").strip()
103
-
104
233
  status = "online" if bool(is_active) else "offline"
105
234
  name = label or f"host-device-{gid}"
106
235
 
107
- last_seen = ""
108
- ds = status_map.get(str(device_id))
109
- if ds is not None:
110
- try:
111
- ls = int(getattr(ds, "last_seen_ms", 0) or 0)
112
- if ls > 0:
113
- age_s = max(0, (now_ms - ls) // 1000)
114
- last_seen = f", seen={age_s}s ago"
115
- except Exception:
116
- pass
117
-
118
- out[gid] = (
119
- f"{name} "
120
- # f"[host={host_id}, local_id={local_id}"
121
- # f"{', kind=' + kind if kind else ''}"
122
- # f"{', serial=' + serial if serial else ''}"
123
- # f"{last_seen}"
124
- # f"] ({status})"
125
-
126
- f"({status})"
127
- )
236
+ # reservation flag from _devices
237
+ with _state_lock:
238
+ st = _devices.get(int(gid))
239
+ if st and (st.salt or st.hsh):
240
+ status = status + ", reserved"
241
+
242
+ out[gid] = f"{name} ({status})"
128
243
 
129
244
  return out
130
245
 
131
- # -----------------------------
246
+
247
+ # ============================================================
132
248
  # Config paths (server-side)
133
- # -----------------------------
249
+ # ============================================================
134
250
 
135
251
  def _xdg_config_home() -> Path:
136
252
  return Path(os.getenv("XDG_CONFIG_HOME", Path.home() / ".config"))
137
253
 
254
+
138
255
  def _cfg_dir() -> Path:
139
256
  # default: ~/.config/remoterf
140
257
  return Path(os.getenv("REMOTERF_CONFIG_DIR", _xdg_config_home() / "remoterf"))
141
258
 
259
+
142
260
  def _devices_env_path() -> Path:
143
261
  return _cfg_dir() / "devices.env"
144
262
 
145
- # -----------------------------
263
+
264
+ # ============================================================
146
265
  # Pluto helpers
147
- # -----------------------------
266
+ # ============================================================
148
267
 
149
268
  def connect_pluto(*, ip: str = "", usb: str = ""):
150
269
  try:
@@ -158,7 +277,8 @@ def connect_pluto(*, ip: str = "", usb: str = ""):
158
277
  except Exception as e:
159
278
  print(f"Pluto {ip}: {e}")
160
279
  return None
161
-
280
+
281
+
162
282
  def get_usb_port_from_serial(serial: str) -> str | None:
163
283
  serial = (serial or "").strip()
164
284
  if not serial:
@@ -170,7 +290,6 @@ def get_usb_port_from_serial(serial: str) -> str | None:
170
290
  print(f"Error running iio_info: {e}")
171
291
  return None
172
292
 
173
- # match either serial= or hw_serial= and extract usb context anywhere on the line
174
293
  for line in out.splitlines():
175
294
  if (f"serial={serial}" in line) or (f"hw_serial={serial}" in line):
176
295
  m = re.search(r"\[usb:([^\]]+)\]", line)
@@ -181,19 +300,21 @@ def get_usb_port_from_serial(serial: str) -> str | None:
181
300
  return None
182
301
 
183
302
 
184
- # -----------------------------
303
+ # ============================================================
185
304
  # Parse devices.env
186
- # -----------------------------
305
+ # ============================================================
187
306
 
188
307
  _ENV_LINE = re.compile(r"^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$")
189
308
  _DEVICE_KEY_RE = re.compile(r"^DEVICE_(\d+)_(.+)$", re.IGNORECASE)
190
309
 
310
+
191
311
  def _strip_quotes(v: str) -> str:
192
312
  v = (v or "").strip()
193
313
  if len(v) >= 2 and ((v[0] == v[-1] == '"') or (v[0] == v[-1] == "'")):
194
314
  return v[1:-1]
195
315
  return v
196
316
 
317
+
197
318
  def _read_env_file(path: Path) -> Dict[str, str]:
198
319
  out: Dict[str, str] = {}
199
320
  if not path.exists():
@@ -215,6 +336,7 @@ def _read_env_file(path: Path) -> Dict[str, str]:
215
336
  out[k] = v
216
337
  return out
217
338
 
339
+
218
340
  def _load_device_records() -> Dict[int, Dict[str, str]]:
219
341
  """
220
342
  Returns:
@@ -253,6 +375,7 @@ def _load_device_records() -> Dict[int, Dict[str, str]]:
253
375
 
254
376
  return recs
255
377
 
378
+
256
379
  def _connect_from_record(rec: Dict[str, str]):
257
380
  """
258
381
  Returns (device_obj, ident, dtype).
@@ -285,9 +408,9 @@ def _connect_from_record(rec: Dict[str, str]):
285
408
  return (None, ident, dtype)
286
409
 
287
410
 
288
- # -----------------------------
411
+ # ============================================================
289
412
  # Thread-safe runtime state
290
- # -----------------------------
413
+ # ============================================================
291
414
 
292
415
  # legacy maps (kept for your existing server calls/UI)
293
416
  devices_info: Dict[int, str] = {}
@@ -318,6 +441,8 @@ def _init_from_env() -> None:
318
441
 
319
442
  tmp_devices[int(gid)] = _DeviceState(
320
443
  dev=dev,
444
+ origin="local",
445
+ online=(dev is not None),
321
446
  salt="",
322
447
  hsh="",
323
448
  name=name,
@@ -337,13 +462,14 @@ def _init_from_env() -> None:
337
462
  device_serialization.clear()
338
463
  device_serialization.update(tmp_ser)
339
464
 
465
+
340
466
  # initialize on import
341
467
  _init_from_env()
342
468
 
343
469
 
344
- # -----------------------------
470
+ # ============================================================
345
471
  # Legacy reservation helpers (master token parsing)
346
- # -----------------------------
472
+ # ============================================================
347
473
 
348
474
  def parse_mastertoken(token: str):
349
475
  """
@@ -366,9 +492,9 @@ def parse_mastertoken(token: str):
366
492
  return (device_id, force)
367
493
 
368
494
 
369
- # -----------------------------
495
+ # ============================================================
370
496
  # Transmitter stubs (legacy)
371
- # -----------------------------
497
+ # ============================================================
372
498
 
373
499
  _transmitter = None
374
500
 
@@ -382,57 +508,52 @@ def get_transmitter_state() -> bool:
382
508
  return False
383
509
 
384
510
 
385
- # -----------------------------
511
+ # ============================================================
386
512
  # Legacy API (state-safe)
387
- # -----------------------------
513
+ # ============================================================
388
514
 
389
515
  def get_all_devices() -> Dict[int, Tuple[object, str, str]]:
390
- # snapshot: connected devices only
516
+ _sync_host_devices()
517
+
391
518
  with _state_lock:
392
519
  out: Dict[int, Tuple[object, str, str]] = {}
393
520
  for gid, st in _devices.items():
394
- if st.dev is not None:
395
- out[gid] = (st.dev, st.salt, st.hsh)
521
+ if _is_connected(st):
522
+ out[gid] = (st.dev, st.salt, st.hsh) # dev may be VirtualDevice for host
396
523
  return out
397
524
 
525
+
398
526
  def get_all_devices_str() -> Dict[int, str]:
399
- # local snapshot (includes offline local definitions)
400
- out = _local_devices_str_snapshot()
527
+ _sync_host_devices()
401
528
 
402
- # host snapshot (includes online + offline-but-known host devices)
529
+ out = _local_devices_str_snapshot()
403
530
  host_out = _host_devices_str_snapshot()
404
531
 
405
- # ---- DEBUG: print full host_out ----
406
- # try:
407
- # from ..host import host_tunnel_server as hts # adjust if your canonical import differs
408
- # reg = hts.get_tunnel_registry(create=False) if hasattr(hts, "get_tunnel_registry") else None
409
- # print("\n[device_manager] host_out dump")
410
- # print(f" hts module: {getattr(hts, '__name__', '<?>')}")
411
- # print(f" registry id: {id(reg) if reg is not None else None}")
412
- # print(f" host_out count: {len(host_out)}")
413
-
414
- # if not host_out:
415
- # print(" (empty)")
416
- # else:
417
- # for gid, s in sorted(host_out.items(), key=lambda kv: int(kv[0])):
418
- # print(f" {gid}: {s}")
419
- # print()
420
- # except Exception as e:
421
- # print(f"[device_manager] host_out dump failed: {e}")
422
-
423
- # merge with collision handling
532
+ # merge with collision handling (shouldn't happen; keeps UI stable if it does)
424
533
  for gid, s in host_out.items():
425
534
  if gid in out:
426
- # preserve both entries in the UI list; avoid overwriting
427
- alt = -int(gid) - 1 # deterministic "shadow key"
535
+ alt = -int(gid) - 1
428
536
  out[alt] = s + " [gid-collision]"
429
537
  else:
430
538
  out[gid] = s
431
539
 
432
540
  return out
433
541
 
542
+
434
543
  def set_device(device_id: int, salt: str, hash: str):
435
- # update ONLY reservation/auth state
544
+ # ensure host devices are upserted so reservations can apply to them too
545
+ _sync_host_devices()
546
+
547
+ with _state_lock:
548
+ st = _devices.get(int(device_id))
549
+
550
+ if not st:
551
+ _sync_host_devices(force=True)
552
+ with _state_lock:
553
+ st = _devices.get(int(device_id))
554
+ if not st:
555
+ return
556
+
436
557
  with _state_lock:
437
558
  st = _devices.get(int(device_id))
438
559
  if not st:
@@ -440,39 +561,48 @@ def set_device(device_id: int, salt: str, hash: str):
440
561
  st.salt = str(salt or "")
441
562
  st.hsh = str(hash or "")
442
563
 
564
+
443
565
  def device_exists(device_id: int) -> bool:
444
566
  did = int(device_id)
445
567
 
446
- # local
568
+ _sync_host_devices()
569
+
570
+ # local: connected only (unchanged semantics)
447
571
  with _state_lock:
448
572
  st = _devices.get(did)
449
- if st and st.dev is not None:
573
+ if st and st.origin == "local" and st.dev is not None:
450
574
  return True
451
575
 
452
- # host
576
+ # host: existence as known by registry (unchanged)
453
577
  reg = _get_host_registry()
454
578
  if reg is None:
455
579
  return False
456
580
  return bool(reg.is_host_device(str(did)))
457
581
 
582
+
458
583
  def device_is_available(device_id: int) -> bool:
584
+ _sync_host_devices()
585
+
459
586
  with _state_lock:
460
587
  st = _devices.get(int(device_id))
461
- if not st or st.dev is None:
588
+ if not st or not _is_connected(st):
462
589
  return False
463
590
  return (st.salt == "") and (st.hsh == "")
464
591
 
592
+
465
593
  def get_device_by_id(device_id: int):
466
- # legacy: returns (dev, salt, hash) tuple
594
+ _sync_host_devices()
595
+
467
596
  with _state_lock:
468
597
  st = _devices.get(int(device_id))
469
598
  if not st:
470
599
  return None
471
600
  return (st.dev, st.salt, st.hsh)
472
601
 
602
+
473
603
  def get_device(*, api_token: str):
474
604
  """
475
- Legacy behavior:
605
+ Legacy behavior preserved:
476
606
  - master_token returns first available device object
477
607
  - master_token_<id>[_force] selects a specific device
478
608
  - otherwise validate against per-device salt/hash (set via reservations)
@@ -483,17 +613,24 @@ def get_device(*, api_token: str):
483
613
  if not api_token:
484
614
  return None
485
615
 
486
- # Take a snapshot of state for validation without holding the global lock
616
+ _sync_host_devices()
617
+
487
618
  with _state_lock:
488
- snapshot: List[Tuple[int, object, str, str]] = [
489
- (gid, st.dev, st.salt, st.hsh)
619
+ snapshot: List[Tuple[int, object, str, str, str]] = [
620
+ (gid, st.dev, st.salt, st.hsh, st.origin)
490
621
  for gid, st in _devices.items()
491
- if st.dev is not None
622
+ if _is_connected(st)
492
623
  ]
493
624
 
625
+ # Preserve "master prefers local" behavior
494
626
  if api_token == master_token:
495
- for _, dev, salt, hsh in snapshot:
496
- if dev is not None and salt == "" and hsh == "":
627
+ # local first
628
+ for _, dev, salt, hsh, origin in snapshot:
629
+ if origin == "local" and dev is not None and salt == "" and hsh == "":
630
+ return dev
631
+ # then host
632
+ for _, dev, salt, hsh, origin in snapshot:
633
+ if origin == "host" and dev is not None and salt == "" and hsh == "":
497
634
  return dev
498
635
  return None
499
636
 
@@ -502,13 +639,13 @@ def get_device(*, api_token: str):
502
639
  device_id, force = parsed
503
640
  with _state_lock:
504
641
  st = _devices.get(int(device_id))
505
- if not st or st.dev is None:
642
+ if not st or not _is_connected(st):
506
643
  return None
507
644
  if force:
508
645
  return st.dev
509
646
  return st.dev if (st.salt == "" and st.hsh == "") else None
510
647
 
511
- for _, dev, salt, hsh in snapshot:
648
+ for _, dev, salt, hsh, _origin in snapshot:
512
649
  if dev is None:
513
650
  continue
514
651
  if salt and hsh and validate_token(salt, hsh, api_token):
@@ -517,47 +654,55 @@ def get_device(*, api_token: str):
517
654
  return None
518
655
 
519
656
 
520
- # -----------------------------
521
- # NEW: Per-device I/O locking (true concurrency support)
522
- # -----------------------------
657
+ # ============================================================
658
+ # Per-device I/O locking (true concurrency support)
659
+ # ============================================================
523
660
 
524
661
  @contextmanager
525
662
  def acquire_device(api_token: str) -> Iterator[Tuple[int, object]]:
526
663
  """
527
664
  Thread-safe way to use a device:
528
665
  with acquire_device(token) as (gid, dev):
529
- # safe: only one thread at a time per device
530
666
  ...
531
667
 
532
668
  - Selects the device using the SAME logic as get_device()
533
669
  - Then acquires that device's io_lock
534
670
  - Yields (gid, dev)
671
+
672
+ NOTE:
673
+ dev can be an adi device (local) OR a VirtualDevice (host).
535
674
  """
536
675
  if not api_token:
537
676
  raise RuntimeError("missing api_token")
538
677
 
539
- # resolve gid first (snapshot-based)
678
+ _sync_host_devices()
679
+
540
680
  gid: Optional[int] = None
541
681
 
542
682
  if api_token == master_token:
543
683
  with _state_lock:
684
+ # prefer local first
544
685
  for k, st in _devices.items():
545
- if st.dev is not None and st.salt == "" and st.hsh == "":
686
+ if st.origin == "local" and _is_connected(st) and st.salt == "" and st.hsh == "":
546
687
  gid = k
547
688
  break
689
+ if gid is None:
690
+ for k, st in _devices.items():
691
+ if st.origin == "host" and _is_connected(st) and st.salt == "" and st.hsh == "":
692
+ gid = k
693
+ break
548
694
  else:
549
695
  parsed = parse_mastertoken(api_token)
550
696
  if parsed:
551
697
  cand, force = parsed
552
698
  with _state_lock:
553
699
  st = _devices.get(int(cand))
554
- if st and st.dev is not None:
700
+ if st and _is_connected(st):
555
701
  if force or (st.salt == "" and st.hsh == ""):
556
702
  gid = int(cand)
557
703
  else:
558
- # validate token against per-device salt/hash
559
704
  with _state_lock:
560
- snapshot = [(k, st.dev, st.salt, st.hsh) for k, st in _devices.items() if st.dev is not None]
705
+ snapshot = [(k, st.dev, st.salt, st.hsh) for k, st in _devices.items() if _is_connected(st)]
561
706
  for k, dev, salt, hsh in snapshot:
562
707
  if salt and hsh and validate_token(salt, hsh, api_token):
563
708
  gid = k
@@ -566,10 +711,9 @@ def acquire_device(api_token: str) -> Iterator[Tuple[int, object]]:
566
711
  if gid is None:
567
712
  raise RuntimeError("no device available / invalid token")
568
713
 
569
- # lock the selected device for I/O
570
714
  with _state_lock:
571
715
  st = _devices.get(int(gid))
572
- if not st or st.dev is None:
716
+ if not st or not _is_connected(st) or st.dev is None:
573
717
  raise RuntimeError("device disappeared / not connected")
574
718
  lock = st.io_lock
575
719
  dev = st.dev
@@ -581,22 +725,25 @@ def acquire_device(api_token: str) -> Iterator[Tuple[int, object]]:
581
725
  lock.release()
582
726
 
583
727
 
584
- # -----------------------------
728
+ # ============================================================
585
729
  # Optional: reload device definitions (atomic swap)
586
- # -----------------------------
730
+ # ============================================================
587
731
 
588
732
  def reload_devices() -> None:
589
733
  """
590
- Reload devices.env and reconnect devices.
591
- Preserves existing salt/hash for devices that remain (same gid).
734
+ Reload devices.env and reconnect local devices.
735
+ Preserves existing salt/hash for devices that remain (same gid),
736
+ and preserves host virtual devices + their auth state.
592
737
  """
593
738
  records = _load_device_records()
594
739
 
595
- # snapshot current salt/hash so we can carry it over
740
+ _sync_host_devices() # ensure host entries exist before snapshot
741
+
596
742
  with _state_lock:
597
743
  prev_auth = {gid: (st.salt, st.hsh) for gid, st in _devices.items()}
744
+ prev_host = {gid: st for gid, st in _devices.items() if st.origin == "host"}
598
745
 
599
- tmp_devices: Dict[int, _DeviceState] = {}
746
+ tmp_local: Dict[int, _DeviceState] = {}
600
747
  tmp_info: Dict[int, str] = {}
601
748
  tmp_ser: Dict[int, str] = {}
602
749
 
@@ -611,8 +758,10 @@ def reload_devices() -> None:
611
758
  tmp_ser[int(gid)] = ident
612
759
 
613
760
  salt, hsh = prev_auth.get(int(gid), ("", ""))
614
- tmp_devices[int(gid)] = _DeviceState(
761
+ tmp_local[int(gid)] = _DeviceState(
615
762
  dev=dev,
763
+ origin="local",
764
+ online=(dev is not None),
616
765
  salt=salt,
617
766
  hsh=hsh,
618
767
  name=name,
@@ -623,7 +772,14 @@ def reload_devices() -> None:
623
772
 
624
773
  with _state_lock:
625
774
  _devices.clear()
626
- _devices.update(tmp_devices)
775
+
776
+ # restore host entries (but do not overwrite locals if collision)
777
+ for gid, st in prev_host.items():
778
+ if gid not in tmp_local:
779
+ _devices[gid] = st
780
+
781
+ # add locals
782
+ _devices.update(tmp_local)
627
783
 
628
784
  devices_info.clear()
629
785
  devices_info.update(tmp_info)
@@ -631,7 +787,10 @@ def reload_devices() -> None:
631
787
  device_serialization.clear()
632
788
  device_serialization.update(tmp_ser)
633
789
 
790
+ # refresh host online/offline status after reload (best-effort)
791
+ _sync_host_devices(force=True)
792
+
634
793
 
635
794
  def set_pluto(ip: str = "192.168.2.1"):
636
795
  # kept for compatibility (no-op)
637
- pass
796
+ pass
@@ -391,10 +391,10 @@ def _cmd_hosts_status() -> None:
391
391
  reg = hts.get_tunnel_registry()
392
392
  assert reg is not None
393
393
 
394
- print("\n[hosts status] registry debug")
395
- print(f" hts module: {hts.__name__}")
396
- reg = hts.get_tunnel_registry(create=False)
397
- print(f" registry id: {id(reg) if reg else None}\n")
394
+ # print("\n[hosts status] registry debug")
395
+ # print(f" hts module: {hts.__name__}")
396
+ # reg = hts.get_tunnel_registry(create=False)
397
+ # print(f" registry id: {id(reg) if reg else None}\n")
398
398
 
399
399
  hosts = reg.list_hosts() # host_id -> HostStatus
400
400
  devices = reg.list_devices() # device_id -> DeviceStatus
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remoteRF-server
3
- Version: 0.1.95
3
+ Version: 0.1.96
4
4
  Description-Content-Type: text/markdown
5
5
  Requires-Dist: grpcio
6
6
  Requires-Dist: protobuf
@@ -350,7 +350,7 @@ todo:
350
350
 
351
351
  <!-- Host ID (NEEDS TO BE UNIQUE AND TELL THE HOST THAT) (or if device also does it as well.) -->
352
352
 
353
- ServerRF display meta data as part of devices list / reservation
353
+ <!-- ServerRF display meta data as part of devices list / reservation -->
354
354
 
355
355
  + online/activity status (appended into device name) + (plus host side warning if the device id is NOT globally unique!)
356
356