remoteRF-server 0.1.94__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.94 → remoterf_server-0.1.96}/PKG-INFO +2 -2
  2. {remoterf_server-0.1.94 → remoterf_server-0.1.96}/README.md +1 -1
  3. {remoterf_server-0.1.94 → remoterf_server-0.1.96}/pyproject.toml +1 -1
  4. {remoterf_server-0.1.94 → remoterf_server-0.1.96}/src/remoteRF_server/server/device_manager.py +292 -131
  5. {remoterf_server-0.1.94 → remoterf_server-0.1.96}/src/remoteRF_server/server/grpc_server.py +4 -4
  6. {remoterf_server-0.1.94 → remoterf_server-0.1.96}/src/remoteRF_server.egg-info/PKG-INFO +2 -2
  7. {remoterf_server-0.1.94 → remoterf_server-0.1.96}/setup.cfg +0 -0
  8. {remoterf_server-0.1.94 → remoterf_server-0.1.96}/src/remoteRF_server/__init__.py +0 -0
  9. {remoterf_server-0.1.94 → remoterf_server-0.1.96}/src/remoteRF_server/common/__init__.py +0 -0
  10. {remoterf_server-0.1.94 → remoterf_server-0.1.96}/src/remoteRF_server/common/grpc/__init__.py +0 -0
  11. {remoterf_server-0.1.94 → remoterf_server-0.1.96}/src/remoteRF_server/common/grpc/grpc_host_pb2.py +0 -0
  12. {remoterf_server-0.1.94 → remoterf_server-0.1.96}/src/remoteRF_server/common/grpc/grpc_host_pb2_grpc.py +0 -0
  13. {remoterf_server-0.1.94 → remoterf_server-0.1.96}/src/remoteRF_server/common/grpc/grpc_pb2.py +0 -0
  14. {remoterf_server-0.1.94 → remoterf_server-0.1.96}/src/remoteRF_server/common/grpc/grpc_pb2_grpc.py +0 -0
  15. {remoterf_server-0.1.94 → remoterf_server-0.1.96}/src/remoteRF_server/common/utils/__init__.py +0 -0
  16. {remoterf_server-0.1.94 → remoterf_server-0.1.96}/src/remoteRF_server/common/utils/ansi_codes.py +0 -0
  17. {remoterf_server-0.1.94 → remoterf_server-0.1.96}/src/remoteRF_server/common/utils/api_token.py +0 -0
  18. {remoterf_server-0.1.94 → remoterf_server-0.1.96}/src/remoteRF_server/common/utils/db_connection.py +0 -0
  19. {remoterf_server-0.1.94 → remoterf_server-0.1.96}/src/remoteRF_server/common/utils/db_location.py +0 -0
  20. {remoterf_server-0.1.94 → remoterf_server-0.1.96}/src/remoteRF_server/common/utils/list_string.py +0 -0
  21. {remoterf_server-0.1.94 → remoterf_server-0.1.96}/src/remoteRF_server/common/utils/process_arg.py +0 -0
  22. {remoterf_server-0.1.94 → remoterf_server-0.1.96}/src/remoteRF_server/drivers/__init__.py +0 -0
  23. {remoterf_server-0.1.94 → remoterf_server-0.1.96}/src/remoteRF_server/drivers/adalm_pluto/__init__.py +0 -0
  24. {remoterf_server-0.1.94 → remoterf_server-0.1.96}/src/remoteRF_server/drivers/adalm_pluto/pluto_remote_server.py +0 -0
  25. {remoterf_server-0.1.94 → remoterf_server-0.1.96}/src/remoteRF_server/host/__init__.py +0 -0
  26. {remoterf_server-0.1.94 → remoterf_server-0.1.96}/src/remoteRF_server/host/host_directory_store.py +0 -0
  27. {remoterf_server-0.1.94 → remoterf_server-0.1.96}/src/remoteRF_server/host/host_tunnel_server.py +0 -0
  28. {remoterf_server-0.1.94 → remoterf_server-0.1.96}/src/remoteRF_server/server/__init__.py +0 -0
  29. {remoterf_server-0.1.94 → remoterf_server-0.1.96}/src/remoteRF_server/server/acc_perms.py +0 -0
  30. {remoterf_server-0.1.94 → remoterf_server-0.1.96}/src/remoteRF_server/server/cert_provider.py +0 -0
  31. {remoterf_server-0.1.94 → remoterf_server-0.1.96}/src/remoteRF_server/server/reservation.py +0 -0
  32. {remoterf_server-0.1.94 → remoterf_server-0.1.96}/src/remoteRF_server/server/rpc_manager.py +0 -0
  33. {remoterf_server-0.1.94 → remoterf_server-0.1.96}/src/remoteRF_server/server/user_group_cli.py +0 -0
  34. {remoterf_server-0.1.94 → remoterf_server-0.1.96}/src/remoteRF_server/server/user_group_handler.py +0 -0
  35. {remoterf_server-0.1.94 → remoterf_server-0.1.96}/src/remoteRF_server/serverrf_cli.py +0 -0
  36. {remoterf_server-0.1.94 → remoterf_server-0.1.96}/src/remoteRF_server/tools/__init__.py +0 -0
  37. {remoterf_server-0.1.94 → remoterf_server-0.1.96}/src/remoteRF_server/tools/gen_certs.py +0 -0
  38. {remoterf_server-0.1.94 → remoterf_server-0.1.96}/src/remoteRF_server.egg-info/SOURCES.txt +0 -0
  39. {remoterf_server-0.1.94 → remoterf_server-0.1.96}/src/remoteRF_server.egg-info/dependency_links.txt +0 -0
  40. {remoterf_server-0.1.94 → remoterf_server-0.1.96}/src/remoteRF_server.egg-info/entry_points.txt +0 -0
  41. {remoterf_server-0.1.94 → remoterf_server-0.1.96}/src/remoteRF_server.egg-info/requires.txt +0 -0
  42. {remoterf_server-0.1.94 → 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.94
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.94"
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,87 +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
- )
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})"
126
243
 
127
244
  return out
128
245
 
129
- # -----------------------------
246
+
247
+ # ============================================================
130
248
  # Config paths (server-side)
131
- # -----------------------------
249
+ # ============================================================
132
250
 
133
251
  def _xdg_config_home() -> Path:
134
252
  return Path(os.getenv("XDG_CONFIG_HOME", Path.home() / ".config"))
135
253
 
254
+
136
255
  def _cfg_dir() -> Path:
137
256
  # default: ~/.config/remoterf
138
257
  return Path(os.getenv("REMOTERF_CONFIG_DIR", _xdg_config_home() / "remoterf"))
139
258
 
259
+
140
260
  def _devices_env_path() -> Path:
141
261
  return _cfg_dir() / "devices.env"
142
262
 
143
- # -----------------------------
263
+
264
+ # ============================================================
144
265
  # Pluto helpers
145
- # -----------------------------
266
+ # ============================================================
146
267
 
147
268
  def connect_pluto(*, ip: str = "", usb: str = ""):
148
269
  try:
@@ -156,7 +277,8 @@ def connect_pluto(*, ip: str = "", usb: str = ""):
156
277
  except Exception as e:
157
278
  print(f"Pluto {ip}: {e}")
158
279
  return None
159
-
280
+
281
+
160
282
  def get_usb_port_from_serial(serial: str) -> str | None:
161
283
  serial = (serial or "").strip()
162
284
  if not serial:
@@ -168,7 +290,6 @@ def get_usb_port_from_serial(serial: str) -> str | None:
168
290
  print(f"Error running iio_info: {e}")
169
291
  return None
170
292
 
171
- # match either serial= or hw_serial= and extract usb context anywhere on the line
172
293
  for line in out.splitlines():
173
294
  if (f"serial={serial}" in line) or (f"hw_serial={serial}" in line):
174
295
  m = re.search(r"\[usb:([^\]]+)\]", line)
@@ -179,19 +300,21 @@ def get_usb_port_from_serial(serial: str) -> str | None:
179
300
  return None
180
301
 
181
302
 
182
- # -----------------------------
303
+ # ============================================================
183
304
  # Parse devices.env
184
- # -----------------------------
305
+ # ============================================================
185
306
 
186
307
  _ENV_LINE = re.compile(r"^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$")
187
308
  _DEVICE_KEY_RE = re.compile(r"^DEVICE_(\d+)_(.+)$", re.IGNORECASE)
188
309
 
310
+
189
311
  def _strip_quotes(v: str) -> str:
190
312
  v = (v or "").strip()
191
313
  if len(v) >= 2 and ((v[0] == v[-1] == '"') or (v[0] == v[-1] == "'")):
192
314
  return v[1:-1]
193
315
  return v
194
316
 
317
+
195
318
  def _read_env_file(path: Path) -> Dict[str, str]:
196
319
  out: Dict[str, str] = {}
197
320
  if not path.exists():
@@ -213,6 +336,7 @@ def _read_env_file(path: Path) -> Dict[str, str]:
213
336
  out[k] = v
214
337
  return out
215
338
 
339
+
216
340
  def _load_device_records() -> Dict[int, Dict[str, str]]:
217
341
  """
218
342
  Returns:
@@ -251,6 +375,7 @@ def _load_device_records() -> Dict[int, Dict[str, str]]:
251
375
 
252
376
  return recs
253
377
 
378
+
254
379
  def _connect_from_record(rec: Dict[str, str]):
255
380
  """
256
381
  Returns (device_obj, ident, dtype).
@@ -283,9 +408,9 @@ def _connect_from_record(rec: Dict[str, str]):
283
408
  return (None, ident, dtype)
284
409
 
285
410
 
286
- # -----------------------------
411
+ # ============================================================
287
412
  # Thread-safe runtime state
288
- # -----------------------------
413
+ # ============================================================
289
414
 
290
415
  # legacy maps (kept for your existing server calls/UI)
291
416
  devices_info: Dict[int, str] = {}
@@ -316,6 +441,8 @@ def _init_from_env() -> None:
316
441
 
317
442
  tmp_devices[int(gid)] = _DeviceState(
318
443
  dev=dev,
444
+ origin="local",
445
+ online=(dev is not None),
319
446
  salt="",
320
447
  hsh="",
321
448
  name=name,
@@ -335,13 +462,14 @@ def _init_from_env() -> None:
335
462
  device_serialization.clear()
336
463
  device_serialization.update(tmp_ser)
337
464
 
465
+
338
466
  # initialize on import
339
467
  _init_from_env()
340
468
 
341
469
 
342
- # -----------------------------
470
+ # ============================================================
343
471
  # Legacy reservation helpers (master token parsing)
344
- # -----------------------------
472
+ # ============================================================
345
473
 
346
474
  def parse_mastertoken(token: str):
347
475
  """
@@ -364,9 +492,9 @@ def parse_mastertoken(token: str):
364
492
  return (device_id, force)
365
493
 
366
494
 
367
- # -----------------------------
495
+ # ============================================================
368
496
  # Transmitter stubs (legacy)
369
- # -----------------------------
497
+ # ============================================================
370
498
 
371
499
  _transmitter = None
372
500
 
@@ -380,57 +508,52 @@ def get_transmitter_state() -> bool:
380
508
  return False
381
509
 
382
510
 
383
- # -----------------------------
511
+ # ============================================================
384
512
  # Legacy API (state-safe)
385
- # -----------------------------
513
+ # ============================================================
386
514
 
387
515
  def get_all_devices() -> Dict[int, Tuple[object, str, str]]:
388
- # snapshot: connected devices only
516
+ _sync_host_devices()
517
+
389
518
  with _state_lock:
390
519
  out: Dict[int, Tuple[object, str, str]] = {}
391
520
  for gid, st in _devices.items():
392
- if st.dev is not None:
393
- 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
394
523
  return out
395
524
 
525
+
396
526
  def get_all_devices_str() -> Dict[int, str]:
397
- # local snapshot (includes offline local definitions)
398
- out = _local_devices_str_snapshot()
527
+ _sync_host_devices()
399
528
 
400
- # host snapshot (includes online + offline-but-known host devices)
529
+ out = _local_devices_str_snapshot()
401
530
  host_out = _host_devices_str_snapshot()
402
531
 
403
- # ---- DEBUG: print full host_out ----
404
- # try:
405
- # from ..host import host_tunnel_server as hts # adjust if your canonical import differs
406
- # reg = hts.get_tunnel_registry(create=False) if hasattr(hts, "get_tunnel_registry") else None
407
- # print("\n[device_manager] host_out dump")
408
- # print(f" hts module: {getattr(hts, '__name__', '<?>')}")
409
- # print(f" registry id: {id(reg) if reg is not None else None}")
410
- # print(f" host_out count: {len(host_out)}")
411
-
412
- # if not host_out:
413
- # print(" (empty)")
414
- # else:
415
- # for gid, s in sorted(host_out.items(), key=lambda kv: int(kv[0])):
416
- # print(f" {gid}: {s}")
417
- # print()
418
- # except Exception as e:
419
- # print(f"[device_manager] host_out dump failed: {e}")
420
-
421
- # merge with collision handling
532
+ # merge with collision handling (shouldn't happen; keeps UI stable if it does)
422
533
  for gid, s in host_out.items():
423
534
  if gid in out:
424
- # preserve both entries in the UI list; avoid overwriting
425
- alt = -int(gid) - 1 # deterministic "shadow key"
535
+ alt = -int(gid) - 1
426
536
  out[alt] = s + " [gid-collision]"
427
537
  else:
428
538
  out[gid] = s
429
539
 
430
540
  return out
431
541
 
542
+
432
543
  def set_device(device_id: int, salt: str, hash: str):
433
- # 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
+
434
557
  with _state_lock:
435
558
  st = _devices.get(int(device_id))
436
559
  if not st:
@@ -438,39 +561,48 @@ def set_device(device_id: int, salt: str, hash: str):
438
561
  st.salt = str(salt or "")
439
562
  st.hsh = str(hash or "")
440
563
 
564
+
441
565
  def device_exists(device_id: int) -> bool:
442
566
  did = int(device_id)
443
567
 
444
- # local
568
+ _sync_host_devices()
569
+
570
+ # local: connected only (unchanged semantics)
445
571
  with _state_lock:
446
572
  st = _devices.get(did)
447
- if st and st.dev is not None:
573
+ if st and st.origin == "local" and st.dev is not None:
448
574
  return True
449
575
 
450
- # host
576
+ # host: existence as known by registry (unchanged)
451
577
  reg = _get_host_registry()
452
578
  if reg is None:
453
579
  return False
454
580
  return bool(reg.is_host_device(str(did)))
455
581
 
582
+
456
583
  def device_is_available(device_id: int) -> bool:
584
+ _sync_host_devices()
585
+
457
586
  with _state_lock:
458
587
  st = _devices.get(int(device_id))
459
- if not st or st.dev is None:
588
+ if not st or not _is_connected(st):
460
589
  return False
461
590
  return (st.salt == "") and (st.hsh == "")
462
591
 
592
+
463
593
  def get_device_by_id(device_id: int):
464
- # legacy: returns (dev, salt, hash) tuple
594
+ _sync_host_devices()
595
+
465
596
  with _state_lock:
466
597
  st = _devices.get(int(device_id))
467
598
  if not st:
468
599
  return None
469
600
  return (st.dev, st.salt, st.hsh)
470
601
 
602
+
471
603
  def get_device(*, api_token: str):
472
604
  """
473
- Legacy behavior:
605
+ Legacy behavior preserved:
474
606
  - master_token returns first available device object
475
607
  - master_token_<id>[_force] selects a specific device
476
608
  - otherwise validate against per-device salt/hash (set via reservations)
@@ -481,17 +613,24 @@ def get_device(*, api_token: str):
481
613
  if not api_token:
482
614
  return None
483
615
 
484
- # Take a snapshot of state for validation without holding the global lock
616
+ _sync_host_devices()
617
+
485
618
  with _state_lock:
486
- snapshot: List[Tuple[int, object, str, str]] = [
487
- (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)
488
621
  for gid, st in _devices.items()
489
- if st.dev is not None
622
+ if _is_connected(st)
490
623
  ]
491
624
 
625
+ # Preserve "master prefers local" behavior
492
626
  if api_token == master_token:
493
- for _, dev, salt, hsh in snapshot:
494
- 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 == "":
495
634
  return dev
496
635
  return None
497
636
 
@@ -500,13 +639,13 @@ def get_device(*, api_token: str):
500
639
  device_id, force = parsed
501
640
  with _state_lock:
502
641
  st = _devices.get(int(device_id))
503
- if not st or st.dev is None:
642
+ if not st or not _is_connected(st):
504
643
  return None
505
644
  if force:
506
645
  return st.dev
507
646
  return st.dev if (st.salt == "" and st.hsh == "") else None
508
647
 
509
- for _, dev, salt, hsh in snapshot:
648
+ for _, dev, salt, hsh, _origin in snapshot:
510
649
  if dev is None:
511
650
  continue
512
651
  if salt and hsh and validate_token(salt, hsh, api_token):
@@ -515,47 +654,55 @@ def get_device(*, api_token: str):
515
654
  return None
516
655
 
517
656
 
518
- # -----------------------------
519
- # NEW: Per-device I/O locking (true concurrency support)
520
- # -----------------------------
657
+ # ============================================================
658
+ # Per-device I/O locking (true concurrency support)
659
+ # ============================================================
521
660
 
522
661
  @contextmanager
523
662
  def acquire_device(api_token: str) -> Iterator[Tuple[int, object]]:
524
663
  """
525
664
  Thread-safe way to use a device:
526
665
  with acquire_device(token) as (gid, dev):
527
- # safe: only one thread at a time per device
528
666
  ...
529
667
 
530
668
  - Selects the device using the SAME logic as get_device()
531
669
  - Then acquires that device's io_lock
532
670
  - Yields (gid, dev)
671
+
672
+ NOTE:
673
+ dev can be an adi device (local) OR a VirtualDevice (host).
533
674
  """
534
675
  if not api_token:
535
676
  raise RuntimeError("missing api_token")
536
677
 
537
- # resolve gid first (snapshot-based)
678
+ _sync_host_devices()
679
+
538
680
  gid: Optional[int] = None
539
681
 
540
682
  if api_token == master_token:
541
683
  with _state_lock:
684
+ # prefer local first
542
685
  for k, st in _devices.items():
543
- 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 == "":
544
687
  gid = k
545
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
546
694
  else:
547
695
  parsed = parse_mastertoken(api_token)
548
696
  if parsed:
549
697
  cand, force = parsed
550
698
  with _state_lock:
551
699
  st = _devices.get(int(cand))
552
- if st and st.dev is not None:
700
+ if st and _is_connected(st):
553
701
  if force or (st.salt == "" and st.hsh == ""):
554
702
  gid = int(cand)
555
703
  else:
556
- # validate token against per-device salt/hash
557
704
  with _state_lock:
558
- 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)]
559
706
  for k, dev, salt, hsh in snapshot:
560
707
  if salt and hsh and validate_token(salt, hsh, api_token):
561
708
  gid = k
@@ -564,10 +711,9 @@ def acquire_device(api_token: str) -> Iterator[Tuple[int, object]]:
564
711
  if gid is None:
565
712
  raise RuntimeError("no device available / invalid token")
566
713
 
567
- # lock the selected device for I/O
568
714
  with _state_lock:
569
715
  st = _devices.get(int(gid))
570
- if not st or st.dev is None:
716
+ if not st or not _is_connected(st) or st.dev is None:
571
717
  raise RuntimeError("device disappeared / not connected")
572
718
  lock = st.io_lock
573
719
  dev = st.dev
@@ -579,22 +725,25 @@ def acquire_device(api_token: str) -> Iterator[Tuple[int, object]]:
579
725
  lock.release()
580
726
 
581
727
 
582
- # -----------------------------
728
+ # ============================================================
583
729
  # Optional: reload device definitions (atomic swap)
584
- # -----------------------------
730
+ # ============================================================
585
731
 
586
732
  def reload_devices() -> None:
587
733
  """
588
- Reload devices.env and reconnect devices.
589
- 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.
590
737
  """
591
738
  records = _load_device_records()
592
739
 
593
- # snapshot current salt/hash so we can carry it over
740
+ _sync_host_devices() # ensure host entries exist before snapshot
741
+
594
742
  with _state_lock:
595
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"}
596
745
 
597
- tmp_devices: Dict[int, _DeviceState] = {}
746
+ tmp_local: Dict[int, _DeviceState] = {}
598
747
  tmp_info: Dict[int, str] = {}
599
748
  tmp_ser: Dict[int, str] = {}
600
749
 
@@ -609,8 +758,10 @@ def reload_devices() -> None:
609
758
  tmp_ser[int(gid)] = ident
610
759
 
611
760
  salt, hsh = prev_auth.get(int(gid), ("", ""))
612
- tmp_devices[int(gid)] = _DeviceState(
761
+ tmp_local[int(gid)] = _DeviceState(
613
762
  dev=dev,
763
+ origin="local",
764
+ online=(dev is not None),
614
765
  salt=salt,
615
766
  hsh=hsh,
616
767
  name=name,
@@ -621,7 +772,14 @@ def reload_devices() -> None:
621
772
 
622
773
  with _state_lock:
623
774
  _devices.clear()
624
- _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)
625
783
 
626
784
  devices_info.clear()
627
785
  devices_info.update(tmp_info)
@@ -629,7 +787,10 @@ def reload_devices() -> None:
629
787
  device_serialization.clear()
630
788
  device_serialization.update(tmp_ser)
631
789
 
790
+ # refresh host online/offline status after reload (best-effort)
791
+ _sync_host_devices(force=True)
792
+
632
793
 
633
794
  def set_pluto(ip: str = "192.168.2.1"):
634
795
  # kept for compatibility (no-op)
635
- 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.94
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