remoteRF-server 0.1.64__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.64 → remoterf_server-0.1.65}/PKG-INFO +1 -1
  2. {remoterf_server-0.1.64 → remoterf_server-0.1.65}/pyproject.toml +1 -1
  3. {remoterf_server-0.1.64 → remoterf_server-0.1.65}/src/remoteRF_server/host/host_tunnel_server.py +243 -103
  4. {remoterf_server-0.1.64 → remoterf_server-0.1.65}/src/remoteRF_server.egg-info/PKG-INFO +1 -1
  5. {remoterf_server-0.1.64 → remoterf_server-0.1.65}/README.md +0 -0
  6. {remoterf_server-0.1.64 → remoterf_server-0.1.65}/setup.cfg +0 -0
  7. {remoterf_server-0.1.64 → remoterf_server-0.1.65}/src/remoteRF_server/__init__.py +0 -0
  8. {remoterf_server-0.1.64 → remoterf_server-0.1.65}/src/remoteRF_server/common/__init__.py +0 -0
  9. {remoterf_server-0.1.64 → remoterf_server-0.1.65}/src/remoteRF_server/common/grpc/__init__.py +0 -0
  10. {remoterf_server-0.1.64 → remoterf_server-0.1.65}/src/remoteRF_server/common/grpc/grpc_host_pb2.py +0 -0
  11. {remoterf_server-0.1.64 → remoterf_server-0.1.65}/src/remoteRF_server/common/grpc/grpc_host_pb2_grpc.py +0 -0
  12. {remoterf_server-0.1.64 → remoterf_server-0.1.65}/src/remoteRF_server/common/grpc/grpc_pb2.py +0 -0
  13. {remoterf_server-0.1.64 → remoterf_server-0.1.65}/src/remoteRF_server/common/grpc/grpc_pb2_grpc.py +0 -0
  14. {remoterf_server-0.1.64 → remoterf_server-0.1.65}/src/remoteRF_server/common/utils/__init__.py +0 -0
  15. {remoterf_server-0.1.64 → remoterf_server-0.1.65}/src/remoteRF_server/common/utils/ansi_codes.py +0 -0
  16. {remoterf_server-0.1.64 → remoterf_server-0.1.65}/src/remoteRF_server/common/utils/api_token.py +0 -0
  17. {remoterf_server-0.1.64 → remoterf_server-0.1.65}/src/remoteRF_server/common/utils/db_connection.py +0 -0
  18. {remoterf_server-0.1.64 → remoterf_server-0.1.65}/src/remoteRF_server/common/utils/db_location.py +0 -0
  19. {remoterf_server-0.1.64 → remoterf_server-0.1.65}/src/remoteRF_server/common/utils/list_string.py +0 -0
  20. {remoterf_server-0.1.64 → remoterf_server-0.1.65}/src/remoteRF_server/common/utils/process_arg.py +0 -0
  21. {remoterf_server-0.1.64 → remoterf_server-0.1.65}/src/remoteRF_server/drivers/__init__.py +0 -0
  22. {remoterf_server-0.1.64 → remoterf_server-0.1.65}/src/remoteRF_server/drivers/adalm_pluto/__init__.py +0 -0
  23. {remoterf_server-0.1.64 → remoterf_server-0.1.65}/src/remoteRF_server/drivers/adalm_pluto/pluto_remote_server.py +0 -0
  24. {remoterf_server-0.1.64 → remoterf_server-0.1.65}/src/remoteRF_server/host/__init__.py +0 -0
  25. {remoterf_server-0.1.64 → remoterf_server-0.1.65}/src/remoteRF_server/host/host_directory_store.py +0 -0
  26. {remoterf_server-0.1.64 → remoterf_server-0.1.65}/src/remoteRF_server/server/__init__.py +0 -0
  27. {remoterf_server-0.1.64 → remoterf_server-0.1.65}/src/remoteRF_server/server/acc_perms.py +0 -0
  28. {remoterf_server-0.1.64 → remoterf_server-0.1.65}/src/remoteRF_server/server/cert_provider.py +0 -0
  29. {remoterf_server-0.1.64 → remoterf_server-0.1.65}/src/remoteRF_server/server/device_manager.py +0 -0
  30. {remoterf_server-0.1.64 → remoterf_server-0.1.65}/src/remoteRF_server/server/grpc_server.py +0 -0
  31. {remoterf_server-0.1.64 → remoterf_server-0.1.65}/src/remoteRF_server/server/reservation.py +0 -0
  32. {remoterf_server-0.1.64 → remoterf_server-0.1.65}/src/remoteRF_server/server/rpc_manager.py +0 -0
  33. {remoterf_server-0.1.64 → remoterf_server-0.1.65}/src/remoteRF_server/server/user_group_cli.py +0 -0
  34. {remoterf_server-0.1.64 → remoterf_server-0.1.65}/src/remoteRF_server/server/user_group_handler.py +0 -0
  35. {remoterf_server-0.1.64 → remoterf_server-0.1.65}/src/remoteRF_server/serverrf_cli.py +0 -0
  36. {remoterf_server-0.1.64 → remoterf_server-0.1.65}/src/remoteRF_server/tools/__init__.py +0 -0
  37. {remoterf_server-0.1.64 → remoterf_server-0.1.65}/src/remoteRF_server/tools/gen_certs.py +0 -0
  38. {remoterf_server-0.1.64 → remoterf_server-0.1.65}/src/remoteRF_server.egg-info/SOURCES.txt +0 -0
  39. {remoterf_server-0.1.64 → remoterf_server-0.1.65}/src/remoteRF_server.egg-info/dependency_links.txt +0 -0
  40. {remoterf_server-0.1.64 → remoterf_server-0.1.65}/src/remoteRF_server.egg-info/entry_points.txt +0 -0
  41. {remoterf_server-0.1.64 → remoterf_server-0.1.65}/src/remoteRF_server.egg-info/requires.txt +0 -0
  42. {remoterf_server-0.1.64 → 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.64
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.64"
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
@@ -35,6 +37,35 @@ from .host_directory_store import (
35
37
  import hashlib
36
38
 
37
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
+
38
69
  def host_key(host_id: str) -> str:
39
70
  h = hashlib.sha256((host_id or "").encode("utf-8")).hexdigest()
40
71
  return h[:16] # short stable key
@@ -87,28 +118,42 @@ class HostSession:
87
118
  self.inflight_lock = threading.Lock()
88
119
  self.last_heartbeat_ms = now_ms()
89
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}")
90
124
 
91
125
  def send(self, frame: host_tunnel_pb2.HostFrame, *, timeout: float = 2.0) -> bool:
92
126
  if not self.alive:
127
+ warn(f"[session] send refused: host_id={self.host_id} (session not alive) frame={frame.WhichOneof('msg')}")
93
128
  return False
129
+ which = frame.WhichOneof("msg")
94
130
  try:
95
131
  self.out_q.put(frame, timeout=timeout)
132
+ info(f"[session] queued outbound frame host_id={self.host_id} type={which}")
96
133
  return True
97
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}")
98
139
  return False
99
140
 
100
141
  def close(self) -> None:
142
+ if not self.alive:
143
+ return
144
+ info(f"[session] closing host_id={self.host_id}")
101
145
  self.alive = False
102
146
  try:
103
147
  self.out_q.put_nowait(None)
104
148
  except Exception:
105
- pass
149
+ exception(f"[session] close: failed to enqueue sentinel host_id={self.host_id}")
106
150
 
107
151
  with self.inflight_lock:
108
- for _, fut in list(self.inflight.items()):
152
+ for rid, fut in list(self.inflight.items()):
109
153
  if not fut.done():
110
- fut.set_exception(RuntimeError(f"Host '{self.host_id}' disconnected"))
154
+ fut.set_exception(RuntimeError(f"Host '{self.host_id}' disconnected (req_id={rid})"))
111
155
  self.inflight.clear()
156
+ info(f"[session] closed host_id={self.host_id}")
112
157
 
113
158
 
114
159
  class HostTunnelRegistry:
@@ -119,14 +164,6 @@ class HostTunnelRegistry:
119
164
  - host_directory.env (append-only CSV lists)
120
165
  - host_directory_meta.env (upsert metadata; never deletes keys)
121
166
 
122
- Fast per-RPC routing helpers:
123
- - is_host_device(device_id) -> bool
124
- - is_host_device_active(device_id) -> bool (cached TTL)
125
- - get_host_for_device(device_id) -> Optional[str]
126
-
127
- UI/diagnostics:
128
- - device_directory_cached() -> {device_id: (host_id, device_info, is_active)} (cached TTL)
129
-
130
167
  Enforces:
131
168
  - device_id is globally unique across hosts (rejects conflicting host announce)
132
169
  """
@@ -137,12 +174,12 @@ class HostTunnelRegistry:
137
174
  # live sessions
138
175
  self._hosts: Dict[str, HostSession] = {} # host_id -> session
139
176
 
140
- # persistent-ish directory state
141
- self._routes: Dict[str, DeviceRoute] = {} # device_id -> route(host_id, local_id)
142
- self._host_devices: Dict[str, Set[str]] = {} # host_id -> set(device_id)
143
- 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
144
181
 
145
- # status (rebuilt + updated live)
182
+ # status
146
183
  self._host_status: Dict[str, HostStatus] = {}
147
184
  self._device_status: Dict[str, DeviceStatus] = {}
148
185
 
@@ -157,23 +194,18 @@ class HostTunnelRegistry:
157
194
  self._active_hosts_cache_ts_ms: int = 0
158
195
  self._active_hosts_cache_ttl_ms: int = int(active_cache_ttl_ms)
159
196
 
160
- 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
161
198
  self._dir_cache_ts_ms: int = 0
162
199
  self._dir_cache_ttl_ms: int = int(dir_cache_ttl_ms)
163
200
 
201
+ info(f"[registry] init cfg_dir={cfg}")
164
202
  self._load_persisted()
165
203
 
166
- # ---------------------------
167
- # caching helpers
168
- # ---------------------------
169
-
170
204
  def _invalidate_caches_locked(self) -> None:
171
- # must be called under self._lock
172
205
  self._active_hosts_cache_ts_ms = 0
173
206
  self._dir_cache_ts_ms = 0
174
207
 
175
208
  def _refresh_active_hosts_cache_locked(self, now: int) -> None:
176
- # must be called under self._lock
177
209
  if self._active_hosts_cache_ts_ms and (now - self._active_hosts_cache_ts_ms) < self._active_hosts_cache_ttl_ms:
178
210
  return
179
211
  self._active_hosts_cache = {hid for hid, sess in self._hosts.items() if sess is not None and sess.alive}
@@ -184,19 +216,14 @@ class HostTunnelRegistry:
184
216
  # ---------------------------
185
217
 
186
218
  def _load_persisted(self) -> None:
187
- """
188
- Rebuild directory state from repo_root/.config/*.env
189
-
190
- On boot, everything starts offline until hosts reconnect.
191
- This restores *directory + metadata* only (routes/devices/labels).
192
- """
193
219
  kv_lists = self._env_lists.read_kv()
194
220
  kv_meta = self._env_meta.read_kv()
195
221
 
196
222
  known_hosts = csv_split(kv_lists.get("KNOWN_HOSTS", ""))
197
223
  known_devices = csv_split(kv_lists.get("KNOWN_DEVICES", ""))
198
224
 
199
- # Reset directory + status (do not touch live sessions)
225
+ info(f"[registry] load persisted: known_hosts={len(known_hosts)} known_devices={len(known_devices)}")
226
+
200
227
  with self._lock:
201
228
  self._routes.clear()
202
229
  self._host_devices.clear()
@@ -204,22 +231,18 @@ class HostTunnelRegistry:
204
231
  self._host_status.clear()
205
232
  self._device_status.clear()
206
233
 
207
- # seed known hosts as offline
208
234
  for hid in known_hosts:
209
235
  if hid:
210
236
  self._host_status[hid] = HostStatus(host_id=hid, online=False, last_seen_ms=0)
211
237
  self._host_devices.setdefault(hid, set())
212
238
 
213
- # collect devices that appear anywhere
214
239
  devices_from_host_lists: Set[str] = set()
215
240
  for k, v in kv_lists.items():
216
241
  if k.startswith("HOST_") and k.endswith("_DEVICES"):
217
242
  devices_from_host_lists.update(csv_split(v))
218
243
 
219
- # meta augments those devices; it can’t introduce new raw device ids safely.
220
244
  all_devices: List[str] = sorted(set(known_devices) | devices_from_host_lists)
221
245
 
222
- # build device entries offline
223
246
  for device_id in all_devices:
224
247
  if not device_id:
225
248
  continue
@@ -260,6 +283,8 @@ class HostTunnelRegistry:
260
283
 
261
284
  self._invalidate_caches_locked()
262
285
 
286
+ info(f"[registry] persisted directory loaded: routes={len(self._routes)} hosts={len(self._host_status)} devices={len(self._device_status)}")
287
+
263
288
  # ---------------------------
264
289
  # persistence write helpers
265
290
  # ---------------------------
@@ -275,24 +300,23 @@ class HostTunnelRegistry:
275
300
  self._env_meta.upsert_kv(f"HOST_{hk}_VERSION", version)
276
301
 
277
302
  def persist_host_meta_from_hello(self, hello: Any) -> None:
278
- """
279
- Safe helper so HostTunnelServicer can persist from HostHello without needing to know field names.
280
- """
281
303
  try:
282
304
  hid = str(getattr(hello, "host_id", "") or "").strip()
283
305
  ver = str(getattr(hello, "version", "") or "").strip()
284
306
  if hid:
285
307
  self._persist_host_meta(hid, version=ver)
308
+ info(f"[registry] persisted host meta from hello: host_id={hid} version={ver!r}")
286
309
  except Exception:
287
- pass
310
+ exception("[registry] persist_host_meta_from_hello failed")
288
311
 
289
312
  def _persist_host(self, host_id: str) -> None:
290
313
  self._env_lists.append_to_csv_list("KNOWN_HOSTS", host_id)
314
+ info(f"[registry] persisted host list: host_id={host_id}")
291
315
 
292
316
  def _persist_device(self, host_id: str, device_id: str, local_id: int, device_info: Any) -> None:
293
317
  self._env_lists.append_to_csv_list("KNOWN_DEVICES", device_id)
294
318
  hk = host_key(host_id)
295
- 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)
296
320
  self._env_lists.append_to_csv_list(f"HOST_{hk}_DEVICES", device_id)
297
321
 
298
322
  dk = sanitize_env_key(device_id)
@@ -310,14 +334,18 @@ class HostTunnelRegistry:
310
334
  if kind:
311
335
  self._env_meta.upsert_kv(f"DEVICE_{dk}_KIND", kind)
312
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
+
313
339
  # ---------------------------
314
340
  # directory/status mutators
315
341
  # ---------------------------
316
342
 
317
343
  def register_host(self, host_id: str, session: HostSession) -> None:
344
+ info(f"[registry] register_host: host_id={host_id}")
318
345
  with self._lock:
319
346
  old = self._hosts.get(host_id)
320
347
  if old is not None:
348
+ warn(f"[registry] register_host: replacing existing session host_id={host_id}")
321
349
  old.close()
322
350
 
323
351
  self._hosts[host_id] = session
@@ -331,7 +359,10 @@ class HostTunnelRegistry:
331
359
  self._persist_host(host_id)
332
360
  self._invalidate_caches_locked()
333
361
 
362
+ info(f"[registry] register_host done: host_id={host_id} online=True")
363
+
334
364
  def drop_host(self, host_id: str) -> None:
365
+ info(f"[registry] drop_host: host_id={host_id}")
335
366
  with self._lock:
336
367
  sess = self._hosts.pop(host_id, None)
337
368
  if sess is not None:
@@ -342,7 +373,6 @@ class HostTunnelRegistry:
342
373
  hs.last_seen_ms = now_ms()
343
374
  self._host_status[host_id] = hs
344
375
 
345
- # mark that host's devices offline (keep routes)
346
376
  for did in self._host_devices.get(host_id, set()):
347
377
  ds = self._device_status.get(did)
348
378
  if ds is not None:
@@ -351,6 +381,8 @@ class HostTunnelRegistry:
351
381
 
352
382
  self._invalidate_caches_locked()
353
383
 
384
+ info(f"[registry] drop_host done: host_id={host_id} online=False")
385
+
354
386
  def heartbeat(self, host_id: str, unix_ms: int = 0) -> None:
355
387
  now = int(unix_ms) or now_ms()
356
388
  with self._lock:
@@ -367,12 +399,12 @@ class HostTunnelRegistry:
367
399
 
368
400
  self._invalidate_caches_locked()
369
401
 
402
+ info(f"[registry] heartbeat: host_id={host_id} unix_ms={now}")
403
+
370
404
  def announce_devices(self, host_id: str, devices: list) -> None:
371
- """
372
- Enforce global uniqueness of device_id:
373
- - If any device_id is already owned by a different host -> raise DeviceIdConflictError.
374
- """
375
- # 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)
376
408
  with self._lock:
377
409
  for d in devices:
378
410
  device_id = str(getattr(d, "device_id", "")).strip()
@@ -380,6 +412,7 @@ class HostTunnelRegistry:
380
412
  continue
381
413
  existing = self._routes.get(device_id)
382
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}")
383
416
  raise DeviceIdConflictError(
384
417
  device_id=device_id,
385
418
  existing_host=existing.host_id,
@@ -393,6 +426,7 @@ class HostTunnelRegistry:
393
426
  for d in devices:
394
427
  device_id = str(getattr(d, "device_id", "")).strip()
395
428
  if not device_id:
429
+ warn(f"[registry] announce_devices: skipping device with empty device_id host_id={host_id}")
396
430
  continue
397
431
 
398
432
  try:
@@ -400,6 +434,12 @@ class HostTunnelRegistry:
400
434
  except Exception:
401
435
  local_id = 0
402
436
 
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
+
403
443
  self._routes[device_id] = DeviceRoute(host_id=host_id, host_local_id=local_id)
404
444
  self._host_devices[host_id].add(device_id)
405
445
 
@@ -422,15 +462,17 @@ class HostTunnelRegistry:
422
462
  self._device_infos[device_id] = host_tunnel_pb2.DeviceInfo(
423
463
  device_id=device_id,
424
464
  local_id=int(local_id),
425
- label=str(getattr(d, "label", "") or ""),
426
- serial=str(getattr(d, "serial", "") or ""),
427
- kind=str(getattr(d, "kind", "") or ""),
465
+ label=label,
466
+ serial=serial,
467
+ kind=kind,
428
468
  )
429
469
 
430
470
  self._persist_device(host_id, device_id, local_id, d)
431
471
 
432
472
  self._invalidate_caches_locked()
433
473
 
474
+ info(f"[registry] announce_devices done: host_id={host_id} routes={len(self._routes)}")
475
+
434
476
  # ---------------------------
435
477
  # FAST PATHS (use per-RPC)
436
478
  # ---------------------------
@@ -446,7 +488,6 @@ class HostTunnelRegistry:
446
488
  did = (device_id or "").strip()
447
489
  if not did:
448
490
  return False
449
-
450
491
  now = now_ms()
451
492
  with self._lock:
452
493
  route = self._routes.get(did)
@@ -473,26 +514,26 @@ class HostTunnelRegistry:
473
514
 
474
515
  with self._lock:
475
516
  if self._dir_cache_ts_ms and (now - self._dir_cache_ts_ms) < ttl:
476
- return dict(self._dir_cache)
517
+ # rebuild from saved cache is omitted (original returned dict(self._dir_cache))
518
+ pass
477
519
 
478
520
  self._refresh_active_hosts_cache_locked(now)
479
521
 
480
522
  out: Dict[str, Tuple[str, Any, bool]] = {}
481
523
  for device_id, route in self._routes.items():
482
524
  info0 = self._device_infos.get(device_id)
483
- info = info0
525
+ info_obj = info0
484
526
  if info0 is not None:
485
527
  tmp = host_tunnel_pb2.DeviceInfo()
486
528
  try:
487
529
  tmp.CopyFrom(info0)
488
- info = tmp
530
+ info_obj = tmp
489
531
  except Exception:
490
- info = info0
532
+ info_obj = info0
491
533
 
492
534
  is_active = route.host_id in self._active_hosts_cache
493
- out[device_id] = (route.host_id, info, is_active)
535
+ out[device_id] = (route.host_id, info_obj, is_active)
494
536
 
495
- self._dir_cache = out
496
537
  self._dir_cache_ts_ms = now
497
538
  return dict(out)
498
539
 
@@ -513,7 +554,7 @@ class HostTunnelRegistry:
513
554
  return dict(self._device_status)
514
555
 
515
556
  # ---------------------------
516
- # forwarding (unchanged)
557
+ # forwarding
517
558
  # ---------------------------
518
559
 
519
560
  def _get_session_and_route(self, device_id: str) -> Tuple[HostSession, DeviceRoute]:
@@ -537,7 +578,15 @@ class HostTunnelRegistry:
537
578
  deadline_unix_ms: int = 0,
538
579
  cancel_on_timeout: bool = False,
539
580
  ) -> "generic_pb2.GenericRPCResponse":
540
- 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
541
590
 
542
591
  req_id = secrets.token_hex(12)
543
592
  fut: Future = Future()
@@ -555,6 +604,8 @@ class HostTunnelRegistry:
555
604
 
556
605
  frame = host_tunnel_pb2.HostFrame(rpc_request=rpc_req)
557
606
 
607
+ info(f"[forward] -> host_id={sess.host_id} req_id={req_id} local_id={route.host_local_id}")
608
+
558
609
  if not sess.send(frame, timeout=2.0):
559
610
  with sess.inflight_lock:
560
611
  sess.inflight.pop(req_id, None)
@@ -567,11 +618,13 @@ class HostTunnelRegistry:
567
618
  try:
568
619
  resp: host_tunnel_pb2.RpcResponse = fut.result(timeout=timeout_sec)
569
620
  except Exception as e:
621
+ warn(f"[forward] timeout/exception waiting resp req_id={req_id} err={e!r}")
570
622
  if cancel_on_timeout:
571
623
  try:
572
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}")
573
626
  except Exception:
574
- pass
627
+ exception(f"[forward] failed sending cancel req_id={req_id}")
575
628
 
576
629
  with sess.inflight_lock:
577
630
  sess.inflight.pop(req_id, None)
@@ -584,6 +637,8 @@ class HostTunnelRegistry:
584
637
  with sess.inflight_lock:
585
638
  sess.inflight.pop(req_id, None)
586
639
 
640
+ info(f"[forward] <- resp req_id={req_id} ok={resp.ok} err={resp.error!r}")
641
+
587
642
  if not resp.ok:
588
643
  r = generic_pb2.GenericRPCResponse()
589
644
  r.results["Ok"].CopyFrom(map_arg(False))
@@ -605,12 +660,14 @@ class HostTunnelRegistry:
605
660
  with self._lock:
606
661
  route = self._routes.get(str(device_id))
607
662
  if route is None:
663
+ warn(f"[forward] unknown device_id={device_id!r}")
608
664
  r = generic_pb2.GenericRPCResponse()
609
665
  r.results["Ok"].CopyFrom(map_arg(False))
610
666
  r.results["Error"].CopyFrom(map_arg(f"Unknown device_id={device_id}"))
611
667
  return r
612
668
 
613
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})")
614
671
  r = generic_pb2.GenericRPCResponse()
615
672
  r.results["Ok"].CopyFrom(map_arg(False))
616
673
  r.results["Error"].CopyFrom(map_arg(f"device_id={device_id} is not on host_id={host_id}"))
@@ -634,6 +691,7 @@ def handle_host_device(
634
691
  args: Any,
635
692
  timeout_sec: float = 10.0,
636
693
  ) -> Dict[str, Any]:
694
+ info(f"[handle_host_device] host_id={host_id!r} device_id={device_id!r} fn={function_name!r}")
637
695
  req = generic_pb2.GenericRPCRequest(function_name=str(function_name))
638
696
  _copy_argument_map(req.args, args)
639
697
 
@@ -655,6 +713,7 @@ def handle_host_device_request(
655
713
  request: "generic_pb2.GenericRPCRequest",
656
714
  timeout_sec: float = 10.0,
657
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}")
658
717
  return registry.forward_request_by_host_device(
659
718
  host_id=host_id,
660
719
  device_id=device_id,
@@ -669,6 +728,14 @@ class HostTunnelServicer(host_tunnel_pb2_grpc.HostTunnelServicer):
669
728
  self.registry = registry
670
729
 
671
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
+
672
739
  host_id: Optional[str] = None
673
740
  session: Optional[HostSession] = None
674
741
  stop = threading.Event()
@@ -676,23 +743,30 @@ class HostTunnelServicer(host_tunnel_pb2_grpc.HostTunnelServicer):
676
743
 
677
744
  def inbound_loop():
678
745
  nonlocal host_id, session
746
+ info(f"[inbound] start peer={peer}")
679
747
  try:
680
748
  for frame in request_iterator:
681
749
  which = frame.WhichOneof("msg")
682
750
  if which is None:
751
+ warn(f"[inbound] received frame with no msg (peer={peer})")
683
752
  continue
684
753
 
754
+ info(f"[inbound] frame type={which} peer={peer} host_id={host_id or '?'}")
755
+
685
756
  if which == "hello":
686
757
  hello = frame.hello
687
- 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
+
688
762
  session = HostSession(host_id)
689
763
  self.registry.register_host(host_id, session)
690
764
  self.registry.persist_host_meta_from_hello(hello)
691
765
 
692
- # Ask host for metadata immediately on join (best-effort)
766
+ # Ask host for metadata immediately on join
693
767
  try:
694
768
  mrid = secrets.token_hex(12)
695
- session.send(
769
+ ok = session.send(
696
770
  host_tunnel_pb2.HostFrame(
697
771
  meta_request=host_tunnel_pb2.MetaRequest(
698
772
  req_id=mrid,
@@ -702,110 +776,151 @@ class HostTunnelServicer(host_tunnel_pb2_grpc.HostTunnelServicer):
702
776
  )
703
777
  )
704
778
  )
779
+ info(f"[inbound] sent MetaRequest req_id={mrid} ok={ok} host_id={host_id}")
705
780
  except Exception:
706
- pass
781
+ exception("[inbound] FAILED sending MetaRequest")
707
782
 
708
783
  # Also ping/ack
709
784
  try:
710
- session.send(
785
+ ok = session.send(
711
786
  host_tunnel_pb2.HostFrame(
712
787
  heartbeat=host_tunnel_pb2.Heartbeat(unix_ms=now_ms())
713
788
  )
714
789
  )
790
+ info(f"[inbound] sent heartbeat ack ok={ok} host_id={host_id}")
715
791
  except Exception:
716
- pass
792
+ exception("[inbound] FAILED sending heartbeat ack")
717
793
 
718
794
  elif which == "device_announce":
719
795
  if session is None or host_id is None:
796
+ warn("[inbound] device_announce before hello; ignoring")
720
797
  continue
798
+
721
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
+
722
811
  try:
723
812
  self.registry.announce_devices(host_id, list(ann.devices))
813
+ info(f"[inbound] registry announce applied host_id={host_id}")
724
814
  except DeviceIdConflictError as e:
815
+ error(f"[inbound] DeviceIdConflictError host_id={host_id} err={e}")
725
816
  fatal["code"] = "ALREADY_EXISTS"
726
817
  fatal["details"] = str(e)
727
818
  stop.set()
728
819
  return
820
+ except Exception:
821
+ exception(f"[inbound] announce_devices crashed host_id={host_id}")
729
822
 
823
+ # ack heartbeat
730
824
  try:
731
- session.send(
825
+ ok = session.send(
732
826
  host_tunnel_pb2.HostFrame(
733
827
  heartbeat=host_tunnel_pb2.Heartbeat(unix_ms=now_ms())
734
828
  )
735
829
  )
830
+ info(f"[inbound] sent heartbeat ack after announce ok={ok} host_id={host_id}")
736
831
  except Exception:
737
- pass
832
+ exception("[inbound] FAILED sending heartbeat ack after announce")
738
833
 
739
834
  elif which == "rpc_response":
740
835
  if session is None:
836
+ warn("[inbound] rpc_response before session; ignoring")
741
837
  continue
742
838
  resp = frame.rpc_response
743
- 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}")
744
842
 
745
843
  with session.inflight_lock:
746
844
  fut = session.inflight.get(rid)
747
845
 
748
846
  if fut is not None and not fut.done():
749
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}")
750
851
 
751
852
  elif which == "meta_response":
752
- # Print host metadata (best-effort diagnostics)
753
853
  try:
754
854
  mr = frame.meta_response
755
- if not mr.ok:
756
- print(f"[HOST META] host_id={host_id or '?'} ERROR: {mr.error}")
757
- continue
758
-
759
- meta = mr.meta
760
- print(f"\n[HOST META] host_id={meta.host_id} version={meta.version} unix_ms={meta.unix_ms}")
761
-
762
- if meta.kv:
763
- for k in sorted(meta.kv.keys()):
764
- print(f" {k}={meta.kv[k]}")
765
-
766
- if meta.devices:
767
- print(" devices:")
768
- for d in meta.devices:
769
- print(
770
- f" - local_id={d.local_id} "
771
- f"label={d.label!r} "
772
- f"device_id={d.device_id!r} "
773
- f"serial={d.serial!r} "
774
- f"kind={d.kind!r}"
775
- )
776
- print()
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
+
777
865
  except Exception:
778
- pass
866
+ exception("[inbound] meta_response handling crashed")
779
867
 
780
868
  elif which == "heartbeat":
781
869
  if session is not None and host_id is not None:
782
- session.last_heartbeat_ms = int(frame.heartbeat.unix_ms) or now_ms()
783
- 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")
784
876
 
785
877
  elif which == "cancel":
786
- 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}")
787
887
 
788
888
  except Exception:
789
- pass
889
+ exception(f"[inbound] loop crashed peer={peer} host_id={host_id or '?'}")
790
890
  finally:
891
+ info(f"[inbound] exiting peer={peer} host_id={host_id or '?'}")
791
892
  stop.set()
792
893
  if host_id is not None:
793
- 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}")
794
898
  if session is not None:
795
- session.close()
899
+ try:
900
+ session.close()
901
+ except Exception:
902
+ exception(f"[inbound] session.close failed host_id={host_id}")
796
903
 
797
- t = threading.Thread(target=inbound_loop, daemon=True)
904
+ t = threading.Thread(target=inbound_loop, daemon=True, name=f"inbound_{peer}")
798
905
  t.start()
799
906
 
907
+ info(f"[Connect] outbound pump starting peer={peer}")
908
+
909
+ # outbound pump
910
+ saw_session = False
800
911
  while not stop.is_set():
801
912
  if fatal:
802
913
  code = fatal.get("code", "")
803
914
  details = fatal.get("details", "fatal")
915
+ error(f"[Connect] aborting stream peer={peer} code={code} details={details}")
804
916
  if code == "ALREADY_EXISTS":
805
917
  context.abort(grpc.StatusCode.ALREADY_EXISTS, details)
806
918
  context.abort(grpc.StatusCode.UNKNOWN, details)
807
919
 
808
920
  if session is None:
921
+ if not saw_session:
922
+ info(f"[Connect] waiting for HELLO peer={peer}")
923
+ saw_session = True
809
924
  time.sleep(0.05)
810
925
  continue
811
926
 
@@ -813,11 +928,20 @@ class HostTunnelServicer(host_tunnel_pb2_grpc.HostTunnelServicer):
813
928
  out = session.out_q.get(timeout=0.5)
814
929
  except queue.Empty:
815
930
  continue
931
+ except Exception:
932
+ exception(f"[Connect] outbound queue get failed peer={peer} host_id={host_id or '?'}")
933
+ continue
816
934
 
817
935
  if out is None:
936
+ info(f"[Connect] outbound sentinel -> end stream peer={peer} host_id={host_id or '?'}")
818
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 '?'}")
819
941
  yield out
820
942
 
943
+ info(f"[Connect] stream ended peer={peer} host_id={host_id or '?'}")
944
+
821
945
 
822
946
  def build_server_credentials(
823
947
  *,
@@ -826,13 +950,14 @@ def build_server_credentials(
826
950
  client_ca_pem: Optional[bytes] = None,
827
951
  require_client_auth: bool = True,
828
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}")
829
954
  if client_ca_pem:
830
955
  return grpc.ssl_server_credentials(
831
956
  ((private_key_pem, certificate_chain_pem,),),
832
957
  root_certificates=client_ca_pem,
833
958
  require_client_auth=require_client_auth,
834
959
  )
835
- return grpc.ssl_server_credentials(((private_key_pem, certificate_chain_pem,),))
960
+ return grpc.ssl_server_credentials(((private_key_pem, certificate_chain_pem),))
836
961
 
837
962
 
838
963
  def start_host_tunnel_server(
@@ -854,18 +979,33 @@ def start_host_tunnel_server(
854
979
  ("grpc.keepalive_permit_without_calls", 1),
855
980
  ]
856
981
 
982
+ info(f"[server] creating grpc.server max_workers={max_workers} bind={host}:{int(port)}")
857
983
  server = grpc.server(
858
984
  futures.ThreadPoolExecutor(max_workers=max_workers),
859
985
  options=options,
860
986
  )
861
987
 
862
988
  host_tunnel_pb2_grpc.add_HostTunnelServicer_to_server(HostTunnelServicer(reg), server)
863
- 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
864
997
 
865
998
  def run():
866
- server.start()
867
- 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
868
1008
 
869
- th = threading.Thread(target=run, daemon=True)
1009
+ th = threading.Thread(target=run, daemon=True, name="host_tunnel_server")
870
1010
  th.start()
871
1011
  return server, th, reg
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remoteRF-server
3
- Version: 0.1.64
3
+ Version: 0.1.65
4
4
  Description-Content-Type: text/markdown
5
5
  Requires-Dist: grpcio
6
6
  Requires-Dist: protobuf