remoteRF-server 0.1.63__tar.gz → 0.1.65__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.63 → remoterf_server-0.1.65}/PKG-INFO +1 -1
  2. {remoterf_server-0.1.63 → remoterf_server-0.1.65}/pyproject.toml +1 -1
  3. {remoterf_server-0.1.63 → remoterf_server-0.1.65}/src/remoteRF_server/host/host_tunnel_server.py +275 -111
  4. {remoterf_server-0.1.63 → remoterf_server-0.1.65}/src/remoteRF_server.egg-info/PKG-INFO +1 -1
  5. {remoterf_server-0.1.63 → remoterf_server-0.1.65}/README.md +0 -0
  6. {remoterf_server-0.1.63 → remoterf_server-0.1.65}/setup.cfg +0 -0
  7. {remoterf_server-0.1.63 → remoterf_server-0.1.65}/src/remoteRF_server/__init__.py +0 -0
  8. {remoterf_server-0.1.63 → remoterf_server-0.1.65}/src/remoteRF_server/common/__init__.py +0 -0
  9. {remoterf_server-0.1.63 → remoterf_server-0.1.65}/src/remoteRF_server/common/grpc/__init__.py +0 -0
  10. {remoterf_server-0.1.63 → remoterf_server-0.1.65}/src/remoteRF_server/common/grpc/grpc_host_pb2.py +0 -0
  11. {remoterf_server-0.1.63 → remoterf_server-0.1.65}/src/remoteRF_server/common/grpc/grpc_host_pb2_grpc.py +0 -0
  12. {remoterf_server-0.1.63 → remoterf_server-0.1.65}/src/remoteRF_server/common/grpc/grpc_pb2.py +0 -0
  13. {remoterf_server-0.1.63 → remoterf_server-0.1.65}/src/remoteRF_server/common/grpc/grpc_pb2_grpc.py +0 -0
  14. {remoterf_server-0.1.63 → remoterf_server-0.1.65}/src/remoteRF_server/common/utils/__init__.py +0 -0
  15. {remoterf_server-0.1.63 → remoterf_server-0.1.65}/src/remoteRF_server/common/utils/ansi_codes.py +0 -0
  16. {remoterf_server-0.1.63 → remoterf_server-0.1.65}/src/remoteRF_server/common/utils/api_token.py +0 -0
  17. {remoterf_server-0.1.63 → remoterf_server-0.1.65}/src/remoteRF_server/common/utils/db_connection.py +0 -0
  18. {remoterf_server-0.1.63 → remoterf_server-0.1.65}/src/remoteRF_server/common/utils/db_location.py +0 -0
  19. {remoterf_server-0.1.63 → remoterf_server-0.1.65}/src/remoteRF_server/common/utils/list_string.py +0 -0
  20. {remoterf_server-0.1.63 → remoterf_server-0.1.65}/src/remoteRF_server/common/utils/process_arg.py +0 -0
  21. {remoterf_server-0.1.63 → remoterf_server-0.1.65}/src/remoteRF_server/drivers/__init__.py +0 -0
  22. {remoterf_server-0.1.63 → remoterf_server-0.1.65}/src/remoteRF_server/drivers/adalm_pluto/__init__.py +0 -0
  23. {remoterf_server-0.1.63 → remoterf_server-0.1.65}/src/remoteRF_server/drivers/adalm_pluto/pluto_remote_server.py +0 -0
  24. {remoterf_server-0.1.63 → remoterf_server-0.1.65}/src/remoteRF_server/host/__init__.py +0 -0
  25. {remoterf_server-0.1.63 → remoterf_server-0.1.65}/src/remoteRF_server/host/host_directory_store.py +0 -0
  26. {remoterf_server-0.1.63 → remoterf_server-0.1.65}/src/remoteRF_server/server/__init__.py +0 -0
  27. {remoterf_server-0.1.63 → remoterf_server-0.1.65}/src/remoteRF_server/server/acc_perms.py +0 -0
  28. {remoterf_server-0.1.63 → remoterf_server-0.1.65}/src/remoteRF_server/server/cert_provider.py +0 -0
  29. {remoterf_server-0.1.63 → remoterf_server-0.1.65}/src/remoteRF_server/server/device_manager.py +0 -0
  30. {remoterf_server-0.1.63 → remoterf_server-0.1.65}/src/remoteRF_server/server/grpc_server.py +0 -0
  31. {remoterf_server-0.1.63 → remoterf_server-0.1.65}/src/remoteRF_server/server/reservation.py +0 -0
  32. {remoterf_server-0.1.63 → remoterf_server-0.1.65}/src/remoteRF_server/server/rpc_manager.py +0 -0
  33. {remoterf_server-0.1.63 → remoterf_server-0.1.65}/src/remoteRF_server/server/user_group_cli.py +0 -0
  34. {remoterf_server-0.1.63 → remoterf_server-0.1.65}/src/remoteRF_server/server/user_group_handler.py +0 -0
  35. {remoterf_server-0.1.63 → remoterf_server-0.1.65}/src/remoteRF_server/serverrf_cli.py +0 -0
  36. {remoterf_server-0.1.63 → remoterf_server-0.1.65}/src/remoteRF_server/tools/__init__.py +0 -0
  37. {remoterf_server-0.1.63 → remoterf_server-0.1.65}/src/remoteRF_server/tools/gen_certs.py +0 -0
  38. {remoterf_server-0.1.63 → remoterf_server-0.1.65}/src/remoteRF_server.egg-info/SOURCES.txt +0 -0
  39. {remoterf_server-0.1.63 → remoterf_server-0.1.65}/src/remoteRF_server.egg-info/dependency_links.txt +0 -0
  40. {remoterf_server-0.1.63 → remoterf_server-0.1.65}/src/remoteRF_server.egg-info/entry_points.txt +0 -0
  41. {remoterf_server-0.1.63 → remoterf_server-0.1.65}/src/remoteRF_server.egg-info/requires.txt +0 -0
  42. {remoterf_server-0.1.63 → remoterf_server-0.1.65}/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.63
3
+ Version: 0.1.65
4
4
  Description-Content-Type: text/markdown
5
5
  Requires-Dist: grpcio
6
6
  Requires-Dist: protobuf
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "remoteRF-server"
7
- version = "0.1.63"
7
+ version = "0.1.65"
8
8
  dependencies = [
9
9
  "grpcio",
10
10
  "protobuf",
@@ -2,10 +2,12 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import sys
5
6
  import threading
6
7
  import queue
7
8
  import time
8
9
  import secrets
10
+ import traceback
9
11
  from dataclasses import dataclass
10
12
  from typing import Dict, Optional, Tuple, Any, List, Set, Iterator
11
13
  from concurrent.futures import Future
@@ -34,10 +36,41 @@ from .host_directory_store import (
34
36
 
35
37
  import hashlib
36
38
 
39
+
40
+ # =============================================================================
41
+ # Always-on logging (no levels to configure; INFO/WARN/ERROR always print)
42
+ # =============================================================================
43
+
44
+ def _ts() -> str:
45
+ t = time.time()
46
+ sec = int(t)
47
+ ms = int((t - sec) * 1000)
48
+ lt = time.localtime(sec)
49
+ return time.strftime("%H:%M:%S", lt) + f".{ms:03d}"
50
+
51
+ def _log(level: str, msg: str) -> None:
52
+ tn = threading.current_thread().name
53
+ print(f"{_ts()} {level:<5} [{tn}] hostrf.host_tunnel_server: {msg}", file=sys.stderr, flush=True)
54
+
55
+ def info(msg: str) -> None:
56
+ _log("INFO", msg)
57
+
58
+ def warn(msg: str) -> None:
59
+ _log("WARN", msg)
60
+
61
+ def error(msg: str) -> None:
62
+ _log("ERROR", msg)
63
+
64
+ def exception(msg: str) -> None:
65
+ _log("ERROR", msg)
66
+ traceback.print_exc(limit=80, file=sys.stderr)
67
+
68
+
37
69
  def host_key(host_id: str) -> str:
38
70
  h = hashlib.sha256((host_id or "").encode("utf-8")).hexdigest()
39
71
  return h[:16] # short stable key
40
72
 
73
+
41
74
  def _copy_argument_map(dst_map: Any, src_map: Any) -> None:
42
75
  if src_map is None:
43
76
  return
@@ -85,28 +118,43 @@ class HostSession:
85
118
  self.inflight_lock = threading.Lock()
86
119
  self.last_heartbeat_ms = now_ms()
87
120
  self.alive = True
121
+ self.outbound_max = int(outbound_max)
122
+
123
+ info(f"[session] created host_id={self.host_id} outbound_max={self.outbound_max}")
88
124
 
89
125
  def send(self, frame: host_tunnel_pb2.HostFrame, *, timeout: float = 2.0) -> bool:
90
126
  if not self.alive:
127
+ warn(f"[session] send refused: host_id={self.host_id} (session not alive) frame={frame.WhichOneof('msg')}")
91
128
  return False
129
+ which = frame.WhichOneof("msg")
92
130
  try:
93
131
  self.out_q.put(frame, timeout=timeout)
132
+ info(f"[session] queued outbound frame host_id={self.host_id} type={which}")
94
133
  return True
95
134
  except queue.Full:
135
+ warn(f"[session] outbound queue FULL host_id={self.host_id} max={self.outbound_max} drop type={which}")
136
+ return False
137
+ except Exception:
138
+ exception(f"[session] send exception host_id={self.host_id} type={which}")
96
139
  return False
97
140
 
98
141
  def close(self) -> None:
142
+ if not self.alive:
143
+ return
144
+ info(f"[session] closing host_id={self.host_id}")
99
145
  self.alive = False
100
146
  try:
101
147
  self.out_q.put_nowait(None)
102
148
  except Exception:
103
- pass
149
+ exception(f"[session] close: failed to enqueue sentinel host_id={self.host_id}")
104
150
 
105
151
  with self.inflight_lock:
106
- for _, fut in list(self.inflight.items()):
152
+ for rid, fut in list(self.inflight.items()):
107
153
  if not fut.done():
108
- fut.set_exception(RuntimeError(f"Host '{self.host_id}' disconnected"))
154
+ fut.set_exception(RuntimeError(f"Host '{self.host_id}' disconnected (req_id={rid})"))
109
155
  self.inflight.clear()
156
+ info(f"[session] closed host_id={self.host_id}")
157
+
110
158
 
111
159
  class HostTunnelRegistry:
112
160
  """
@@ -116,14 +164,6 @@ class HostTunnelRegistry:
116
164
  - host_directory.env (append-only CSV lists)
117
165
  - host_directory_meta.env (upsert metadata; never deletes keys)
118
166
 
119
- Fast per-RPC routing helpers:
120
- - is_host_device(device_id) -> bool
121
- - is_host_device_active(device_id) -> bool (cached TTL)
122
- - get_host_for_device(device_id) -> Optional[str]
123
-
124
- UI/diagnostics:
125
- - device_directory_cached() -> {device_id: (host_id, device_info, is_active)} (cached TTL)
126
-
127
167
  Enforces:
128
168
  - device_id is globally unique across hosts (rejects conflicting host announce)
129
169
  """
@@ -134,12 +174,12 @@ class HostTunnelRegistry:
134
174
  # live sessions
135
175
  self._hosts: Dict[str, HostSession] = {} # host_id -> session
136
176
 
137
- # persistent-ish directory state
138
- self._routes: Dict[str, DeviceRoute] = {} # device_id -> route(host_id, local_id)
139
- self._host_devices: Dict[str, Set[str]] = {} # host_id -> set(device_id)
140
- self._device_infos: Dict[str, host_tunnel_pb2.DeviceInfo] = {} # device_id -> DeviceInfo
177
+ # directory state
178
+ self._routes: Dict[str, DeviceRoute] = {} # device_id -> route(host_id, local_id)
179
+ self._host_devices: Dict[str, Set[str]] = {} # host_id -> set(device_id)
180
+ self._device_infos: Dict[str, host_tunnel_pb2.DeviceInfo] = {} # device_id -> DeviceInfo
141
181
 
142
- # status (rebuilt + updated live)
182
+ # status
143
183
  self._host_status: Dict[str, HostStatus] = {}
144
184
  self._device_status: Dict[str, DeviceStatus] = {}
145
185
 
@@ -154,23 +194,18 @@ class HostTunnelRegistry:
154
194
  self._active_hosts_cache_ts_ms: int = 0
155
195
  self._active_hosts_cache_ttl_ms: int = int(active_cache_ttl_ms)
156
196
 
157
- self._dir_cache: Dict[str, Tuple[str, Any, bool]] = {}
197
+ self._dir_cache: Dict[str, Tuple[str, Tuple[str, Any, bool], bool]] = {} # not used externally
158
198
  self._dir_cache_ts_ms: int = 0
159
199
  self._dir_cache_ttl_ms: int = int(dir_cache_ttl_ms)
160
200
 
201
+ info(f"[registry] init cfg_dir={cfg}")
161
202
  self._load_persisted()
162
203
 
163
- # ---------------------------
164
- # caching helpers
165
- # ---------------------------
166
-
167
204
  def _invalidate_caches_locked(self) -> None:
168
- # must be called under self._lock
169
205
  self._active_hosts_cache_ts_ms = 0
170
206
  self._dir_cache_ts_ms = 0
171
207
 
172
208
  def _refresh_active_hosts_cache_locked(self, now: int) -> None:
173
- # must be called under self._lock
174
209
  if self._active_hosts_cache_ts_ms and (now - self._active_hosts_cache_ts_ms) < self._active_hosts_cache_ttl_ms:
175
210
  return
176
211
  self._active_hosts_cache = {hid for hid, sess in self._hosts.items() if sess is not None and sess.alive}
@@ -181,29 +216,14 @@ class HostTunnelRegistry:
181
216
  # ---------------------------
182
217
 
183
218
  def _load_persisted(self) -> None:
184
- """
185
- Rebuild directory state from repo_root/.config/*.env
186
-
187
- On boot, everything starts offline until hosts reconnect.
188
- This restores *directory + metadata* only (routes/devices/labels).
189
- """
190
219
  kv_lists = self._env_lists.read_kv()
191
220
  kv_meta = self._env_meta.read_kv()
192
221
 
193
222
  known_hosts = csv_split(kv_lists.get("KNOWN_HOSTS", ""))
194
223
  known_devices = csv_split(kv_lists.get("KNOWN_DEVICES", ""))
195
224
 
196
- # also accept hosts referenced by HOST_*_DEVICES keys even if KNOWN_HOSTS drifted
197
- hosts_from_keys: Set[str] = set()
198
- for k, v in kv_lists.items():
199
- if k.startswith("HOST_") and k.endswith("_DEVICES"):
200
- # We don't know the original host_id from the sanitized key reliably.
201
- # That's fine; we still load routes primarily from DEVICE_*_HOST in meta.
202
- # But we DO want to retain the device list; we’ll stitch it via meta host ids.
203
- # So: just collect devices from these keys.
204
- pass
225
+ info(f"[registry] load persisted: known_hosts={len(known_hosts)} known_devices={len(known_devices)}")
205
226
 
206
- # Reset directory + status (do not touch live sessions)
207
227
  with self._lock:
208
228
  self._routes.clear()
209
229
  self._host_devices.clear()
@@ -211,25 +231,18 @@ class HostTunnelRegistry:
211
231
  self._host_status.clear()
212
232
  self._device_status.clear()
213
233
 
214
- # seed known hosts as offline
215
234
  for hid in known_hosts:
216
235
  if hid:
217
236
  self._host_status[hid] = HostStatus(host_id=hid, online=False, last_seen_ms=0)
218
237
  self._host_devices.setdefault(hid, set())
219
238
 
220
- # collect devices that appear anywhere
221
239
  devices_from_host_lists: Set[str] = set()
222
240
  for k, v in kv_lists.items():
223
241
  if k.startswith("HOST_") and k.endswith("_DEVICES"):
224
242
  devices_from_host_lists.update(csv_split(v))
225
243
 
226
- devices_from_meta: Set[str] = set()
227
- # any DEVICE_<X>_HOST implies a known device id (but we can’t reverse sanitize_env_key)
228
- # so we rely on KNOWN_DEVICES + HOST_*_DEVICES as primary device-id sources.
229
- # meta augments those devices; it can’t introduce new raw device ids safely.
230
244
  all_devices: List[str] = sorted(set(known_devices) | devices_from_host_lists)
231
245
 
232
- # build device entries offline
233
246
  for device_id in all_devices:
234
247
  if not device_id:
235
248
  continue
@@ -247,15 +260,11 @@ class HostTunnelRegistry:
247
260
  kind = (kv_meta.get(f"DEVICE_{dk}_KIND", "") or "").strip()
248
261
 
249
262
  if host_id:
250
- # ensure host exists
251
263
  if host_id not in self._host_status:
252
264
  self._host_status[host_id] = HostStatus(host_id=host_id, online=False, last_seen_ms=0)
253
265
  self._host_devices.setdefault(host_id, set()).add(device_id)
254
-
255
- # route exists even while offline
256
266
  self._routes[device_id] = DeviceRoute(host_id=host_id, host_local_id=int(local_id))
257
267
 
258
- # keep info visible even when offline
259
268
  self._device_infos[device_id] = host_tunnel_pb2.DeviceInfo(
260
269
  device_id=str(device_id),
261
270
  local_id=int(local_id),
@@ -274,9 +283,12 @@ class HostTunnelRegistry:
274
283
 
275
284
  self._invalidate_caches_locked()
276
285
 
286
+ info(f"[registry] persisted directory loaded: routes={len(self._routes)} hosts={len(self._host_status)} devices={len(self._device_status)}")
287
+
277
288
  # ---------------------------
278
289
  # persistence write helpers
279
290
  # ---------------------------
291
+
280
292
  def _persist_host_meta(self, host_id: str, *, host_name: str = "", platform: str = "", version: str = "") -> None:
281
293
  hk = host_key(host_id)
282
294
  self._env_meta.set_kv_if_absent(f"HOST_{hk}_ID", host_id)
@@ -287,16 +299,26 @@ class HostTunnelRegistry:
287
299
  if version:
288
300
  self._env_meta.upsert_kv(f"HOST_{hk}_VERSION", version)
289
301
 
302
+ def persist_host_meta_from_hello(self, hello: Any) -> None:
303
+ try:
304
+ hid = str(getattr(hello, "host_id", "") or "").strip()
305
+ ver = str(getattr(hello, "version", "") or "").strip()
306
+ if hid:
307
+ self._persist_host_meta(hid, version=ver)
308
+ info(f"[registry] persisted host meta from hello: host_id={hid} version={ver!r}")
309
+ except Exception:
310
+ exception("[registry] persist_host_meta_from_hello failed")
311
+
290
312
  def _persist_host(self, host_id: str) -> None:
291
313
  self._env_lists.append_to_csv_list("KNOWN_HOSTS", host_id)
314
+ info(f"[registry] persisted host list: host_id={host_id}")
292
315
 
293
316
  def _persist_device(self, host_id: str, device_id: str, local_id: int, device_info: Any) -> None:
294
317
  self._env_lists.append_to_csv_list("KNOWN_DEVICES", device_id)
295
318
  hk = host_key(host_id)
296
- self._env_meta.set_kv_if_absent(f"HOST_{hk}_ID", host_id) # reverse map
319
+ self._env_meta.set_kv_if_absent(f"HOST_{hk}_ID", host_id)
297
320
  self._env_lists.append_to_csv_list(f"HOST_{hk}_DEVICES", device_id)
298
321
 
299
-
300
322
  dk = sanitize_env_key(device_id)
301
323
  self._env_meta.upsert_kv(f"DEVICE_{dk}_HOST", host_id)
302
324
  self._env_meta.upsert_kv(f"DEVICE_{dk}_LOCAL_ID", str(int(local_id)))
@@ -312,14 +334,18 @@ class HostTunnelRegistry:
312
334
  if kind:
313
335
  self._env_meta.upsert_kv(f"DEVICE_{dk}_KIND", kind)
314
336
 
337
+ info(f"[registry] persisted device: host_id={host_id} device_id={device_id} local_id={int(local_id)} label={label!r} serial={serial!r} kind={kind!r}")
338
+
315
339
  # ---------------------------
316
340
  # directory/status mutators
317
341
  # ---------------------------
318
342
 
319
343
  def register_host(self, host_id: str, session: HostSession) -> None:
344
+ info(f"[registry] register_host: host_id={host_id}")
320
345
  with self._lock:
321
346
  old = self._hosts.get(host_id)
322
347
  if old is not None:
348
+ warn(f"[registry] register_host: replacing existing session host_id={host_id}")
323
349
  old.close()
324
350
 
325
351
  self._hosts[host_id] = session
@@ -333,7 +359,10 @@ class HostTunnelRegistry:
333
359
  self._persist_host(host_id)
334
360
  self._invalidate_caches_locked()
335
361
 
362
+ info(f"[registry] register_host done: host_id={host_id} online=True")
363
+
336
364
  def drop_host(self, host_id: str) -> None:
365
+ info(f"[registry] drop_host: host_id={host_id}")
337
366
  with self._lock:
338
367
  sess = self._hosts.pop(host_id, None)
339
368
  if sess is not None:
@@ -344,7 +373,6 @@ class HostTunnelRegistry:
344
373
  hs.last_seen_ms = now_ms()
345
374
  self._host_status[host_id] = hs
346
375
 
347
- # mark that host's devices offline (keep routes)
348
376
  for did in self._host_devices.get(host_id, set()):
349
377
  ds = self._device_status.get(did)
350
378
  if ds is not None:
@@ -353,6 +381,8 @@ class HostTunnelRegistry:
353
381
 
354
382
  self._invalidate_caches_locked()
355
383
 
384
+ info(f"[registry] drop_host done: host_id={host_id} online=False")
385
+
356
386
  def heartbeat(self, host_id: str, unix_ms: int = 0) -> None:
357
387
  now = int(unix_ms) or now_ms()
358
388
  with self._lock:
@@ -367,16 +397,14 @@ class HostTunnelRegistry:
367
397
  ds.online = True
368
398
  ds.last_seen_ms = now
369
399
 
370
- # no need to invalidate full cache; active-cache TTL handles this
371
- # but host status flips can matter; be conservative:
372
400
  self._invalidate_caches_locked()
373
401
 
402
+ info(f"[registry] heartbeat: host_id={host_id} unix_ms={now}")
403
+
374
404
  def announce_devices(self, host_id: str, devices: list) -> None:
375
- """
376
- Enforce global uniqueness of device_id:
377
- - If any device_id is already owned by a different host -> raise DeviceIdConflictError.
378
- """
379
- # conflict check first (no partial apply)
405
+ info(f"[registry] announce_devices: host_id={host_id} n={len(devices)}")
406
+
407
+ # conflict check (no partial apply)
380
408
  with self._lock:
381
409
  for d in devices:
382
410
  device_id = str(getattr(d, "device_id", "")).strip()
@@ -384,6 +412,7 @@ class HostTunnelRegistry:
384
412
  continue
385
413
  existing = self._routes.get(device_id)
386
414
  if existing is not None and existing.host_id != host_id:
415
+ warn(f"[registry] device_id conflict: device_id={device_id} existing_host={existing.host_id} new_host={host_id}")
387
416
  raise DeviceIdConflictError(
388
417
  device_id=device_id,
389
418
  existing_host=existing.host_id,
@@ -397,19 +426,23 @@ class HostTunnelRegistry:
397
426
  for d in devices:
398
427
  device_id = str(getattr(d, "device_id", "")).strip()
399
428
  if not device_id:
429
+ warn(f"[registry] announce_devices: skipping device with empty device_id host_id={host_id}")
400
430
  continue
401
431
 
402
- # local_id is basically not used; keep for completeness
403
432
  try:
404
433
  local_id = int(getattr(d, "local_id", 0))
405
434
  except Exception:
406
435
  local_id = 0
407
436
 
408
- # route
437
+ label = str(getattr(d, "label", "") or "")
438
+ serial = str(getattr(d, "serial", "") or "")
439
+ kind = str(getattr(d, "kind", "") or "")
440
+
441
+ info(f"[registry] device: host_id={host_id} local_id={local_id} device_id={device_id!r} label={label!r} serial={serial!r} kind={kind!r}")
442
+
409
443
  self._routes[device_id] = DeviceRoute(host_id=host_id, host_local_id=local_id)
410
444
  self._host_devices[host_id].add(device_id)
411
445
 
412
- # status
413
446
  ds = self._device_status.get(device_id) or DeviceStatus(
414
447
  device_id=device_id,
415
448
  host_id=host_id,
@@ -421,34 +454,30 @@ class HostTunnelRegistry:
421
454
  ds.last_seen_ms = now
422
455
  self._device_status[device_id] = ds
423
456
 
424
- # info (copy protobuf if possible)
425
457
  try:
426
458
  tmp = host_tunnel_pb2.DeviceInfo()
427
459
  tmp.CopyFrom(d)
428
460
  self._device_infos[device_id] = tmp
429
461
  except Exception:
430
- # fallback minimal
431
462
  self._device_infos[device_id] = host_tunnel_pb2.DeviceInfo(
432
463
  device_id=device_id,
433
464
  local_id=int(local_id),
434
- label=str(getattr(d, "label", "") or ""),
435
- serial=str(getattr(d, "serial", "") or ""),
436
- kind=str(getattr(d, "kind", "") or ""),
465
+ label=label,
466
+ serial=serial,
467
+ kind=kind,
437
468
  )
438
469
 
439
470
  self._persist_device(host_id, device_id, local_id, d)
440
471
 
441
472
  self._invalidate_caches_locked()
442
473
 
474
+ info(f"[registry] announce_devices done: host_id={host_id} routes={len(self._routes)}")
475
+
443
476
  # ---------------------------
444
477
  # FAST PATHS (use per-RPC)
445
478
  # ---------------------------
446
479
 
447
480
  def is_host_device(self, device_id: str) -> bool:
448
- """
449
- O(1): does the directory route this device_id to a host?
450
- (Host may be offline.)
451
- """
452
481
  did = (device_id or "").strip()
453
482
  if not did:
454
483
  return False
@@ -456,14 +485,9 @@ class HostTunnelRegistry:
456
485
  return did in self._routes
457
486
 
458
487
  def is_host_device_active(self, device_id: str) -> bool:
459
- """
460
- O(1) route lookup + cached host-alive check (TTL).
461
- True iff device routes to a host AND that host's tunnel session is alive.
462
- """
463
488
  did = (device_id or "").strip()
464
489
  if not did:
465
490
  return False
466
-
467
491
  now = now_ms()
468
492
  with self._lock:
469
493
  route = self._routes.get(did)
@@ -485,37 +509,31 @@ class HostTunnelRegistry:
485
509
  # ---------------------------
486
510
 
487
511
  def device_directory_cached(self, *, ttl_ms: Optional[int] = None) -> Dict[str, Tuple[str, Any, bool]]:
488
- """
489
- Returns:
490
- { device_id: (host_id, device_info, is_active) }
491
-
492
- Cached rebuild at most once per ttl_ms.
493
- """
494
512
  now = now_ms()
495
513
  ttl = int(self._dir_cache_ttl_ms if ttl_ms is None else ttl_ms)
496
514
 
497
515
  with self._lock:
498
516
  if self._dir_cache_ts_ms and (now - self._dir_cache_ts_ms) < ttl:
499
- return dict(self._dir_cache)
517
+ # rebuild from saved cache is omitted (original returned dict(self._dir_cache))
518
+ pass
500
519
 
501
520
  self._refresh_active_hosts_cache_locked(now)
502
521
 
503
522
  out: Dict[str, Tuple[str, Any, bool]] = {}
504
523
  for device_id, route in self._routes.items():
505
524
  info0 = self._device_infos.get(device_id)
506
- info = info0
525
+ info_obj = info0
507
526
  if info0 is not None:
508
527
  tmp = host_tunnel_pb2.DeviceInfo()
509
528
  try:
510
529
  tmp.CopyFrom(info0)
511
- info = tmp
530
+ info_obj = tmp
512
531
  except Exception:
513
- info = info0
532
+ info_obj = info0
514
533
 
515
534
  is_active = route.host_id in self._active_hosts_cache
516
- out[device_id] = (route.host_id, info, is_active)
535
+ out[device_id] = (route.host_id, info_obj, is_active)
517
536
 
518
- self._dir_cache = out
519
537
  self._dir_cache_ts_ms = now
520
538
  return dict(out)
521
539
 
@@ -536,7 +554,7 @@ class HostTunnelRegistry:
536
554
  return dict(self._device_status)
537
555
 
538
556
  # ---------------------------
539
- # forwarding (unchanged)
557
+ # forwarding
540
558
  # ---------------------------
541
559
 
542
560
  def _get_session_and_route(self, device_id: str) -> Tuple[HostSession, DeviceRoute]:
@@ -560,7 +578,15 @@ class HostTunnelRegistry:
560
578
  deadline_unix_ms: int = 0,
561
579
  cancel_on_timeout: bool = False,
562
580
  ) -> "generic_pb2.GenericRPCResponse":
563
- sess, route = self._get_session_and_route(str(device_id))
581
+ info(f"[forward] device_id={device_id!r} fn={getattr(request,'function_name','')!r} timeout={timeout_sec}")
582
+ try:
583
+ sess, route = self._get_session_and_route(str(device_id))
584
+ except Exception:
585
+ exception(f"[forward] route/session lookup failed device_id={device_id!r}")
586
+ r = generic_pb2.GenericRPCResponse()
587
+ r.results["Ok"].CopyFrom(map_arg(False))
588
+ r.results["Error"].CopyFrom(map_arg("Route/session lookup failed (see server logs)."))
589
+ return r
564
590
 
565
591
  req_id = secrets.token_hex(12)
566
592
  fut: Future = Future()
@@ -578,6 +604,8 @@ class HostTunnelRegistry:
578
604
 
579
605
  frame = host_tunnel_pb2.HostFrame(rpc_request=rpc_req)
580
606
 
607
+ info(f"[forward] -> host_id={sess.host_id} req_id={req_id} local_id={route.host_local_id}")
608
+
581
609
  if not sess.send(frame, timeout=2.0):
582
610
  with sess.inflight_lock:
583
611
  sess.inflight.pop(req_id, None)
@@ -590,11 +618,13 @@ class HostTunnelRegistry:
590
618
  try:
591
619
  resp: host_tunnel_pb2.RpcResponse = fut.result(timeout=timeout_sec)
592
620
  except Exception as e:
621
+ warn(f"[forward] timeout/exception waiting resp req_id={req_id} err={e!r}")
593
622
  if cancel_on_timeout:
594
623
  try:
595
624
  sess.send(host_tunnel_pb2.HostFrame(cancel=host_tunnel_pb2.Cancel(req_id=req_id)))
625
+ info(f"[forward] sent cancel req_id={req_id}")
596
626
  except Exception:
597
- pass
627
+ exception(f"[forward] failed sending cancel req_id={req_id}")
598
628
 
599
629
  with sess.inflight_lock:
600
630
  sess.inflight.pop(req_id, None)
@@ -607,6 +637,8 @@ class HostTunnelRegistry:
607
637
  with sess.inflight_lock:
608
638
  sess.inflight.pop(req_id, None)
609
639
 
640
+ info(f"[forward] <- resp req_id={req_id} ok={resp.ok} err={resp.error!r}")
641
+
610
642
  if not resp.ok:
611
643
  r = generic_pb2.GenericRPCResponse()
612
644
  r.results["Ok"].CopyFrom(map_arg(False))
@@ -628,12 +660,14 @@ class HostTunnelRegistry:
628
660
  with self._lock:
629
661
  route = self._routes.get(str(device_id))
630
662
  if route is None:
663
+ warn(f"[forward] unknown device_id={device_id!r}")
631
664
  r = generic_pb2.GenericRPCResponse()
632
665
  r.results["Ok"].CopyFrom(map_arg(False))
633
666
  r.results["Error"].CopyFrom(map_arg(f"Unknown device_id={device_id}"))
634
667
  return r
635
668
 
636
669
  if route.host_id != host_id:
670
+ warn(f"[forward] device_id={device_id!r} not on host_id={host_id!r} (actual={route.host_id!r})")
637
671
  r = generic_pb2.GenericRPCResponse()
638
672
  r.results["Ok"].CopyFrom(map_arg(False))
639
673
  r.results["Error"].CopyFrom(map_arg(f"device_id={device_id} is not on host_id={host_id}"))
@@ -647,6 +681,7 @@ class HostTunnelRegistry:
647
681
  cancel_on_timeout=cancel_on_timeout,
648
682
  )
649
683
 
684
+
650
685
  def handle_host_device(
651
686
  registry: HostTunnelRegistry,
652
687
  *,
@@ -656,6 +691,7 @@ def handle_host_device(
656
691
  args: Any,
657
692
  timeout_sec: float = 10.0,
658
693
  ) -> Dict[str, Any]:
694
+ info(f"[handle_host_device] host_id={host_id!r} device_id={device_id!r} fn={function_name!r}")
659
695
  req = generic_pb2.GenericRPCRequest(function_name=str(function_name))
660
696
  _copy_argument_map(req.args, args)
661
697
 
@@ -677,6 +713,7 @@ def handle_host_device_request(
677
713
  request: "generic_pb2.GenericRPCRequest",
678
714
  timeout_sec: float = 10.0,
679
715
  ) -> "generic_pb2.GenericRPCResponse":
716
+ info(f"[handle_host_device_request] host_id={host_id!r} device_id={device_id!r} fn={getattr(request,'function_name','')!r}")
680
717
  return registry.forward_request_by_host_device(
681
718
  host_id=host_id,
682
719
  device_id=device_id,
@@ -691,6 +728,14 @@ class HostTunnelServicer(host_tunnel_pb2_grpc.HostTunnelServicer):
691
728
  self.registry = registry
692
729
 
693
730
  def Connect(self, request_iterator: Iterator[host_tunnel_pb2.HostFrame], context):
731
+ peer = ""
732
+ try:
733
+ peer = context.peer()
734
+ except Exception:
735
+ peer = "unknown-peer"
736
+
737
+ info(f"[Connect] new stream peer={peer}")
738
+
694
739
  host_id: Optional[str] = None
695
740
  session: Optional[HostSession] = None
696
741
  stop = threading.Event()
@@ -698,90 +743,184 @@ class HostTunnelServicer(host_tunnel_pb2_grpc.HostTunnelServicer):
698
743
 
699
744
  def inbound_loop():
700
745
  nonlocal host_id, session
746
+ info(f"[inbound] start peer={peer}")
701
747
  try:
702
748
  for frame in request_iterator:
703
749
  which = frame.WhichOneof("msg")
704
750
  if which is None:
751
+ warn(f"[inbound] received frame with no msg (peer={peer})")
705
752
  continue
706
753
 
754
+ info(f"[inbound] frame type={which} peer={peer} host_id={host_id or '?'}")
755
+
707
756
  if which == "hello":
708
757
  hello = frame.hello
709
- host_id = str(hello.host_id).strip() or "unknown-host"
758
+ host_id = str(getattr(hello, "host_id", "")).strip() or "unknown-host"
759
+ ver = str(getattr(hello, "version", "") or "")
760
+ info(f"[inbound] HELLO host_id={host_id!r} version={ver!r} peer={peer}")
761
+
710
762
  session = HostSession(host_id)
711
763
  self.registry.register_host(host_id, session)
712
764
  self.registry.persist_host_meta_from_hello(hello)
713
765
 
766
+ # Ask host for metadata immediately on join
714
767
  try:
715
- session.send(
768
+ mrid = secrets.token_hex(12)
769
+ ok = session.send(
770
+ host_tunnel_pb2.HostFrame(
771
+ meta_request=host_tunnel_pb2.MetaRequest(
772
+ req_id=mrid,
773
+ include_platform=True,
774
+ include_env=True,
775
+ include_devices=True,
776
+ )
777
+ )
778
+ )
779
+ info(f"[inbound] sent MetaRequest req_id={mrid} ok={ok} host_id={host_id}")
780
+ except Exception:
781
+ exception("[inbound] FAILED sending MetaRequest")
782
+
783
+ # Also ping/ack
784
+ try:
785
+ ok = session.send(
716
786
  host_tunnel_pb2.HostFrame(
717
787
  heartbeat=host_tunnel_pb2.Heartbeat(unix_ms=now_ms())
718
788
  )
719
789
  )
790
+ info(f"[inbound] sent heartbeat ack ok={ok} host_id={host_id}")
720
791
  except Exception:
721
- pass
792
+ exception("[inbound] FAILED sending heartbeat ack")
722
793
 
723
794
  elif which == "device_announce":
724
795
  if session is None or host_id is None:
796
+ warn("[inbound] device_announce before hello; ignoring")
725
797
  continue
798
+
726
799
  ann = frame.device_announce
800
+ info(f"[inbound] DEVICE_ANNOUNCE host_id={host_id} n={len(ann.devices)} unix_ms={int(getattr(ann,'unix_ms',0) or 0)} full_snapshot={bool(getattr(ann,'full_snapshot',False))}")
801
+
802
+ for d in list(ann.devices):
803
+ info(
804
+ f"[inbound] device local_id={int(getattr(d,'local_id',0))} "
805
+ f"label={str(getattr(d,'label','') or '')!r} "
806
+ f"device_id={str(getattr(d,'device_id','') or '')!r} "
807
+ f"serial={str(getattr(d,'serial','') or '')!r} "
808
+ f"kind={str(getattr(d,'kind','') or '')!r}"
809
+ )
810
+
727
811
  try:
728
812
  self.registry.announce_devices(host_id, list(ann.devices))
813
+ info(f"[inbound] registry announce applied host_id={host_id}")
729
814
  except DeviceIdConflictError as e:
815
+ error(f"[inbound] DeviceIdConflictError host_id={host_id} err={e}")
730
816
  fatal["code"] = "ALREADY_EXISTS"
731
817
  fatal["details"] = str(e)
732
818
  stop.set()
733
819
  return
820
+ except Exception:
821
+ exception(f"[inbound] announce_devices crashed host_id={host_id}")
734
822
 
823
+ # ack heartbeat
735
824
  try:
736
- session.send(
825
+ ok = session.send(
737
826
  host_tunnel_pb2.HostFrame(
738
827
  heartbeat=host_tunnel_pb2.Heartbeat(unix_ms=now_ms())
739
828
  )
740
829
  )
830
+ info(f"[inbound] sent heartbeat ack after announce ok={ok} host_id={host_id}")
741
831
  except Exception:
742
- pass
832
+ exception("[inbound] FAILED sending heartbeat ack after announce")
743
833
 
744
834
  elif which == "rpc_response":
745
835
  if session is None:
836
+ warn("[inbound] rpc_response before session; ignoring")
746
837
  continue
747
838
  resp = frame.rpc_response
748
- rid = str(resp.req_id)
839
+ rid = str(getattr(resp, "req_id", ""))
840
+
841
+ info(f"[inbound] RPC_RESPONSE req_id={rid} ok={bool(getattr(resp,'ok',False))} err={str(getattr(resp,'error','') or '')!r}")
749
842
 
750
843
  with session.inflight_lock:
751
844
  fut = session.inflight.get(rid)
752
845
 
753
846
  if fut is not None and not fut.done():
754
847
  fut.set_result(resp)
848
+ info(f"[inbound] rpc_response delivered to inflight future req_id={rid}")
849
+ else:
850
+ warn(f"[inbound] rpc_response has no inflight waiter req_id={rid}")
851
+
852
+ elif which == "meta_response":
853
+ try:
854
+ mr = frame.meta_response
855
+ ok = bool(getattr(mr, "ok", False))
856
+ err_s = str(getattr(mr, "error", "") or "")
857
+ info(f"[inbound] META_RESPONSE ok={ok} error={err_s!r}")
858
+
859
+ # Print full proto for maximum visibility
860
+ try:
861
+ info(f"[inbound] META_RESPONSE proto:\n{mr}")
862
+ except Exception:
863
+ pass
864
+
865
+ except Exception:
866
+ exception("[inbound] meta_response handling crashed")
755
867
 
756
868
  elif which == "heartbeat":
757
869
  if session is not None and host_id is not None:
758
- session.last_heartbeat_ms = int(frame.heartbeat.unix_ms) or now_ms()
759
- self.registry.heartbeat(host_id, int(frame.heartbeat.unix_ms) or 0)
870
+ ms = int(getattr(frame.heartbeat, "unix_ms", 0) or 0) or now_ms()
871
+ session.last_heartbeat_ms = ms
872
+ self.registry.heartbeat(host_id, ms)
873
+ info(f"[inbound] HEARTBEAT host_id={host_id} unix_ms={ms}")
874
+ else:
875
+ warn("[inbound] heartbeat before hello/session; ignoring")
760
876
 
761
877
  elif which == "cancel":
762
- pass
878
+ # not used on server inbound, but log it if it happens
879
+ try:
880
+ rid = str(getattr(frame.cancel, "req_id", "") or "")
881
+ warn(f"[inbound] CANCEL frame received from host?? req_id={rid}")
882
+ except Exception:
883
+ exception("[inbound] cancel frame parsing failed")
884
+
885
+ else:
886
+ warn(f"[inbound] unknown frame type={which}")
763
887
 
764
888
  except Exception:
765
- pass
889
+ exception(f"[inbound] loop crashed peer={peer} host_id={host_id or '?'}")
766
890
  finally:
891
+ info(f"[inbound] exiting peer={peer} host_id={host_id or '?'}")
767
892
  stop.set()
768
893
  if host_id is not None:
769
- self.registry.drop_host(host_id)
894
+ try:
895
+ self.registry.drop_host(host_id)
896
+ except Exception:
897
+ exception(f"[inbound] registry.drop_host failed host_id={host_id}")
770
898
  if session is not None:
771
- session.close()
899
+ try:
900
+ session.close()
901
+ except Exception:
902
+ exception(f"[inbound] session.close failed host_id={host_id}")
772
903
 
773
- t = threading.Thread(target=inbound_loop, daemon=True)
904
+ t = threading.Thread(target=inbound_loop, daemon=True, name=f"inbound_{peer}")
774
905
  t.start()
775
906
 
907
+ info(f"[Connect] outbound pump starting peer={peer}")
908
+
909
+ # outbound pump
910
+ saw_session = False
776
911
  while not stop.is_set():
777
912
  if fatal:
778
913
  code = fatal.get("code", "")
779
914
  details = fatal.get("details", "fatal")
915
+ error(f"[Connect] aborting stream peer={peer} code={code} details={details}")
780
916
  if code == "ALREADY_EXISTS":
781
917
  context.abort(grpc.StatusCode.ALREADY_EXISTS, details)
782
918
  context.abort(grpc.StatusCode.UNKNOWN, details)
783
919
 
784
920
  if session is None:
921
+ if not saw_session:
922
+ info(f"[Connect] waiting for HELLO peer={peer}")
923
+ saw_session = True
785
924
  time.sleep(0.05)
786
925
  continue
787
926
 
@@ -789,11 +928,20 @@ class HostTunnelServicer(host_tunnel_pb2_grpc.HostTunnelServicer):
789
928
  out = session.out_q.get(timeout=0.5)
790
929
  except queue.Empty:
791
930
  continue
931
+ except Exception:
932
+ exception(f"[Connect] outbound queue get failed peer={peer} host_id={host_id or '?'}")
933
+ continue
792
934
 
793
935
  if out is None:
936
+ info(f"[Connect] outbound sentinel -> end stream peer={peer} host_id={host_id or '?'}")
794
937
  break
938
+
939
+ out_type = out.WhichOneof("msg")
940
+ info(f"[Connect] OUT frame type={out_type} peer={peer} host_id={host_id or '?'}")
795
941
  yield out
796
942
 
943
+ info(f"[Connect] stream ended peer={peer} host_id={host_id or '?'}")
944
+
797
945
 
798
946
  def build_server_credentials(
799
947
  *,
@@ -802,13 +950,14 @@ def build_server_credentials(
802
950
  client_ca_pem: Optional[bytes] = None,
803
951
  require_client_auth: bool = True,
804
952
  ) -> grpc.ServerCredentials:
953
+ info(f"[creds] build_server_credentials client_ca={'yes' if bool(client_ca_pem) else 'no'} require_client_auth={require_client_auth}")
805
954
  if client_ca_pem:
806
955
  return grpc.ssl_server_credentials(
807
956
  ((private_key_pem, certificate_chain_pem,),),
808
957
  root_certificates=client_ca_pem,
809
958
  require_client_auth=require_client_auth,
810
959
  )
811
- return grpc.ssl_server_credentials(((private_key_pem, certificate_chain_pem,),))
960
+ return grpc.ssl_server_credentials(((private_key_pem, certificate_chain_pem),))
812
961
 
813
962
 
814
963
  def start_host_tunnel_server(
@@ -830,18 +979,33 @@ def start_host_tunnel_server(
830
979
  ("grpc.keepalive_permit_without_calls", 1),
831
980
  ]
832
981
 
982
+ info(f"[server] creating grpc.server max_workers={max_workers} bind={host}:{int(port)}")
833
983
  server = grpc.server(
834
984
  futures.ThreadPoolExecutor(max_workers=max_workers),
835
985
  options=options,
836
986
  )
837
987
 
838
988
  host_tunnel_pb2_grpc.add_HostTunnelServicer_to_server(HostTunnelServicer(reg), server)
839
- server.add_secure_port(f"{host}:{int(port)}", server_credentials)
989
+
990
+ bind_addr = f"{host}:{int(port)}"
991
+ try:
992
+ added = server.add_secure_port(bind_addr, server_credentials)
993
+ info(f"[server] add_secure_port addr={bind_addr} -> {added}")
994
+ except Exception:
995
+ exception(f"[server] add_secure_port failed addr={bind_addr}")
996
+ raise
840
997
 
841
998
  def run():
842
- server.start()
843
- server.wait_for_termination()
999
+ try:
1000
+ info(f"[server] starting on {bind_addr}")
1001
+ server.start()
1002
+ info(f"[server] started on {bind_addr} (waiting for termination)")
1003
+ server.wait_for_termination()
1004
+ info("[server] wait_for_termination returned")
1005
+ except Exception:
1006
+ exception("[server] run() crashed")
1007
+ raise
844
1008
 
845
- th = threading.Thread(target=run, daemon=True)
1009
+ th = threading.Thread(target=run, daemon=True, name="host_tunnel_server")
846
1010
  th.start()
847
- return server, th, reg
1011
+ return server, th, reg
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remoteRF-server
3
- Version: 0.1.63
3
+ Version: 0.1.65
4
4
  Description-Content-Type: text/markdown
5
5
  Requires-Dist: grpcio
6
6
  Requires-Dist: protobuf