remoteRF-server 0.1.64__tar.gz → 0.1.66__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.66}/PKG-INFO +1 -1
  2. {remoterf_server-0.1.64 → remoterf_server-0.1.66}/pyproject.toml +1 -1
  3. {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/host/host_tunnel_server.py +299 -111
  4. {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server.egg-info/PKG-INFO +1 -1
  5. {remoterf_server-0.1.64 → remoterf_server-0.1.66}/README.md +0 -0
  6. {remoterf_server-0.1.64 → remoterf_server-0.1.66}/setup.cfg +0 -0
  7. {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/__init__.py +0 -0
  8. {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/common/__init__.py +0 -0
  9. {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/common/grpc/__init__.py +0 -0
  10. {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/common/grpc/grpc_host_pb2.py +0 -0
  11. {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/common/grpc/grpc_host_pb2_grpc.py +0 -0
  12. {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/common/grpc/grpc_pb2.py +0 -0
  13. {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/common/grpc/grpc_pb2_grpc.py +0 -0
  14. {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/common/utils/__init__.py +0 -0
  15. {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/common/utils/ansi_codes.py +0 -0
  16. {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/common/utils/api_token.py +0 -0
  17. {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/common/utils/db_connection.py +0 -0
  18. {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/common/utils/db_location.py +0 -0
  19. {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/common/utils/list_string.py +0 -0
  20. {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/common/utils/process_arg.py +0 -0
  21. {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/drivers/__init__.py +0 -0
  22. {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/drivers/adalm_pluto/__init__.py +0 -0
  23. {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/drivers/adalm_pluto/pluto_remote_server.py +0 -0
  24. {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/host/__init__.py +0 -0
  25. {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/host/host_directory_store.py +0 -0
  26. {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/server/__init__.py +0 -0
  27. {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/server/acc_perms.py +0 -0
  28. {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/server/cert_provider.py +0 -0
  29. {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/server/device_manager.py +0 -0
  30. {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/server/grpc_server.py +0 -0
  31. {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/server/reservation.py +0 -0
  32. {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/server/rpc_manager.py +0 -0
  33. {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/server/user_group_cli.py +0 -0
  34. {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/server/user_group_handler.py +0 -0
  35. {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/serverrf_cli.py +0 -0
  36. {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/tools/__init__.py +0 -0
  37. {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/tools/gen_certs.py +0 -0
  38. {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server.egg-info/SOURCES.txt +0 -0
  39. {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server.egg-info/dependency_links.txt +0 -0
  40. {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server.egg-info/entry_points.txt +0 -0
  41. {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server.egg-info/requires.txt +0 -0
  42. {remoterf_server-0.1.64 → remoterf_server-0.1.66}/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.66
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.66"
8
8
  dependencies = [
9
9
  "grpcio",
10
10
  "protobuf",
@@ -2,15 +2,20 @@
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
11
+ import hashlib
12
+ import re
13
+
9
14
  from dataclasses import dataclass
10
15
  from typing import Dict, Optional, Tuple, Any, List, Set, Iterator
16
+
11
17
  from concurrent.futures import Future
12
18
  from concurrent import futures
13
- from pathlib import Path
14
19
 
15
20
  import grpc
16
21
 
@@ -32,13 +37,46 @@ from .host_directory_store import (
32
37
  cfg_dir_from_file,
33
38
  )
34
39
 
35
- import hashlib
40
+ # =============================================================================
41
+ # Always-on logging (INFO/WARN/ERROR always print; no config/levels)
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)
36
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
+
69
+ # =============================================================================
70
+ # Helpers
71
+ # =============================================================================
37
72
 
38
73
  def host_key(host_id: str) -> str:
39
74
  h = hashlib.sha256((host_id or "").encode("utf-8")).hexdigest()
40
75
  return h[:16] # short stable key
41
76
 
77
+ def _safe_thread_tag(s: str) -> str:
78
+ s = s or "peer"
79
+ return re.sub(r"[^A-Za-z0-9_.-]+", "_", s)[:64]
42
80
 
43
81
  def _copy_argument_map(dst_map: Any, src_map: Any) -> None:
44
82
  if src_map is None:
@@ -51,19 +89,21 @@ def _copy_argument_map(dst_map: Any, src_map: Any) -> None:
51
89
  dst_map[str(k)].CopyFrom(v)
52
90
 
53
91
 
92
+ # =============================================================================
93
+ # Types
94
+ # =============================================================================
95
+
54
96
  @dataclass(frozen=True)
55
97
  class DeviceRoute:
56
98
  host_id: str
57
99
  host_local_id: int
58
100
 
59
-
60
101
  @dataclass
61
102
  class HostStatus:
62
103
  host_id: str
63
104
  online: bool = False
64
105
  last_seen_ms: int = 0
65
106
 
66
-
67
107
  @dataclass
68
108
  class DeviceStatus:
69
109
  device_id: str
@@ -73,6 +113,10 @@ class DeviceStatus:
73
113
  last_seen_ms: int = 0
74
114
 
75
115
 
116
+ # =============================================================================
117
+ # Session
118
+ # =============================================================================
119
+
76
120
  class HostSession:
77
121
  """
78
122
  Represents one connected host (one bidi stream).
@@ -82,34 +126,54 @@ class HostSession:
82
126
  """
83
127
  def __init__(self, host_id: str, *, outbound_max: int = 2048) -> None:
84
128
  self.host_id = host_id
85
- self.out_q: "queue.Queue[Optional[host_tunnel_pb2.HostFrame]]" = queue.Queue(maxsize=outbound_max)
129
+ self.out_q: "queue.Queue[Optional[host_tunnel_pb2.HostFrame]]" = queue.Queue(maxsize=int(outbound_max))
86
130
  self.inflight: Dict[str, Future] = {}
87
131
  self.inflight_lock = threading.Lock()
88
132
  self.last_heartbeat_ms = now_ms()
89
133
  self.alive = True
134
+ self.outbound_max = int(outbound_max)
135
+
136
+ info(f"[session] created host_id={self.host_id!r} outbound_max={self.outbound_max}")
90
137
 
91
138
  def send(self, frame: host_tunnel_pb2.HostFrame, *, timeout: float = 2.0) -> bool:
139
+ which = frame.WhichOneof("msg")
92
140
  if not self.alive:
141
+ warn(f"[session] send refused (dead session) host_id={self.host_id!r} type={which}")
93
142
  return False
94
143
  try:
95
144
  self.out_q.put(frame, timeout=timeout)
145
+ info(f"[session] queued outbound host_id={self.host_id!r} type={which}")
96
146
  return True
97
147
  except queue.Full:
148
+ warn(f"[session] outbound queue FULL host_id={self.host_id!r} max={self.outbound_max} drop type={which}")
149
+ return False
150
+ except Exception:
151
+ exception(f"[session] send exception host_id={self.host_id!r} type={which}")
98
152
  return False
99
153
 
100
154
  def close(self) -> None:
155
+ if not self.alive:
156
+ return
157
+ info(f"[session] closing host_id={self.host_id!r}")
101
158
  self.alive = False
159
+
102
160
  try:
103
161
  self.out_q.put_nowait(None)
104
162
  except Exception:
105
- pass
163
+ exception(f"[session] close: failed to enqueue sentinel host_id={self.host_id!r}")
106
164
 
107
165
  with self.inflight_lock:
108
- for _, fut in list(self.inflight.items()):
166
+ for rid, fut in list(self.inflight.items()):
109
167
  if not fut.done():
110
- fut.set_exception(RuntimeError(f"Host '{self.host_id}' disconnected"))
168
+ fut.set_exception(RuntimeError(f"Host '{self.host_id}' disconnected (req_id={rid})"))
111
169
  self.inflight.clear()
112
170
 
171
+ info(f"[session] closed host_id={self.host_id!r}")
172
+
173
+
174
+ # =============================================================================
175
+ # Registry
176
+ # =============================================================================
113
177
 
114
178
  class HostTunnelRegistry:
115
179
  """
@@ -119,14 +183,6 @@ class HostTunnelRegistry:
119
183
  - host_directory.env (append-only CSV lists)
120
184
  - host_directory_meta.env (upsert metadata; never deletes keys)
121
185
 
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
186
  Enforces:
131
187
  - device_id is globally unique across hosts (rejects conflicting host announce)
132
188
  """
@@ -137,12 +193,12 @@ class HostTunnelRegistry:
137
193
  # live sessions
138
194
  self._hosts: Dict[str, HostSession] = {} # host_id -> session
139
195
 
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
196
+ # directory state
197
+ self._routes: Dict[str, DeviceRoute] = {} # device_id -> route(host_id, local_id)
198
+ self._host_devices: Dict[str, Set[str]] = {} # host_id -> set(device_id)
199
+ self._device_infos: Dict[str, host_tunnel_pb2.DeviceInfo] = {} # device_id -> DeviceInfo
144
200
 
145
- # status (rebuilt + updated live)
201
+ # status
146
202
  self._host_status: Dict[str, HostStatus] = {}
147
203
  self._device_status: Dict[str, DeviceStatus] = {}
148
204
 
@@ -161,6 +217,7 @@ class HostTunnelRegistry:
161
217
  self._dir_cache_ts_ms: int = 0
162
218
  self._dir_cache_ttl_ms: int = int(dir_cache_ttl_ms)
163
219
 
220
+ info(f"[registry] init cfg_dir={cfg}")
164
221
  self._load_persisted()
165
222
 
166
223
  # ---------------------------
@@ -168,12 +225,10 @@ class HostTunnelRegistry:
168
225
  # ---------------------------
169
226
 
170
227
  def _invalidate_caches_locked(self) -> None:
171
- # must be called under self._lock
172
228
  self._active_hosts_cache_ts_ms = 0
173
229
  self._dir_cache_ts_ms = 0
174
230
 
175
231
  def _refresh_active_hosts_cache_locked(self, now: int) -> None:
176
- # must be called under self._lock
177
232
  if self._active_hosts_cache_ts_ms and (now - self._active_hosts_cache_ts_ms) < self._active_hosts_cache_ttl_ms:
178
233
  return
179
234
  self._active_hosts_cache = {hid for hid, sess in self._hosts.items() if sess is not None and sess.alive}
@@ -184,19 +239,14 @@ class HostTunnelRegistry:
184
239
  # ---------------------------
185
240
 
186
241
  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
242
  kv_lists = self._env_lists.read_kv()
194
243
  kv_meta = self._env_meta.read_kv()
195
244
 
196
245
  known_hosts = csv_split(kv_lists.get("KNOWN_HOSTS", ""))
197
246
  known_devices = csv_split(kv_lists.get("KNOWN_DEVICES", ""))
198
247
 
199
- # Reset directory + status (do not touch live sessions)
248
+ info(f"[registry] load persisted: known_hosts={len(known_hosts)} known_devices={len(known_devices)}")
249
+
200
250
  with self._lock:
201
251
  self._routes.clear()
202
252
  self._host_devices.clear()
@@ -204,22 +254,19 @@ class HostTunnelRegistry:
204
254
  self._host_status.clear()
205
255
  self._device_status.clear()
206
256
 
207
- # seed known hosts as offline
257
+ # seed hosts offline
208
258
  for hid in known_hosts:
209
259
  if hid:
210
260
  self._host_status[hid] = HostStatus(host_id=hid, online=False, last_seen_ms=0)
211
261
  self._host_devices.setdefault(hid, set())
212
262
 
213
- # collect devices that appear anywhere
214
263
  devices_from_host_lists: Set[str] = set()
215
264
  for k, v in kv_lists.items():
216
265
  if k.startswith("HOST_") and k.endswith("_DEVICES"):
217
266
  devices_from_host_lists.update(csv_split(v))
218
267
 
219
- # meta augments those devices; it can’t introduce new raw device ids safely.
220
268
  all_devices: List[str] = sorted(set(known_devices) | devices_from_host_lists)
221
269
 
222
- # build device entries offline
223
270
  for device_id in all_devices:
224
271
  if not device_id:
225
272
  continue
@@ -260,6 +307,8 @@ class HostTunnelRegistry:
260
307
 
261
308
  self._invalidate_caches_locked()
262
309
 
310
+ info(f"[registry] persisted directory loaded: routes={len(self._routes)} hosts={len(self._host_status)} devices={len(self._device_status)}")
311
+
263
312
  # ---------------------------
264
313
  # persistence write helpers
265
314
  # ---------------------------
@@ -275,24 +324,23 @@ class HostTunnelRegistry:
275
324
  self._env_meta.upsert_kv(f"HOST_{hk}_VERSION", version)
276
325
 
277
326
  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
327
  try:
282
328
  hid = str(getattr(hello, "host_id", "") or "").strip()
283
329
  ver = str(getattr(hello, "version", "") or "").strip()
284
330
  if hid:
285
331
  self._persist_host_meta(hid, version=ver)
332
+ info(f"[registry] persisted host meta from hello: host_id={hid!r} version={ver!r}")
286
333
  except Exception:
287
- pass
334
+ exception("[registry] persist_host_meta_from_hello failed")
288
335
 
289
336
  def _persist_host(self, host_id: str) -> None:
290
337
  self._env_lists.append_to_csv_list("KNOWN_HOSTS", host_id)
338
+ info(f"[registry] persisted host list: host_id={host_id!r}")
291
339
 
292
340
  def _persist_device(self, host_id: str, device_id: str, local_id: int, device_info: Any) -> None:
293
341
  self._env_lists.append_to_csv_list("KNOWN_DEVICES", device_id)
294
342
  hk = host_key(host_id)
295
- self._env_meta.set_kv_if_absent(f"HOST_{hk}_ID", host_id) # reverse map
343
+ self._env_meta.set_kv_if_absent(f"HOST_{hk}_ID", host_id)
296
344
  self._env_lists.append_to_csv_list(f"HOST_{hk}_DEVICES", device_id)
297
345
 
298
346
  dk = sanitize_env_key(device_id)
@@ -310,14 +358,21 @@ class HostTunnelRegistry:
310
358
  if kind:
311
359
  self._env_meta.upsert_kv(f"DEVICE_{dk}_KIND", kind)
312
360
 
361
+ info(
362
+ f"[registry] persisted device: host_id={host_id!r} device_id={device_id!r} local_id={int(local_id)} "
363
+ f"label={label!r} serial={serial!r} kind={kind!r}"
364
+ )
365
+
313
366
  # ---------------------------
314
367
  # directory/status mutators
315
368
  # ---------------------------
316
369
 
317
370
  def register_host(self, host_id: str, session: HostSession) -> None:
371
+ info(f"[registry] register_host: host_id={host_id!r}")
318
372
  with self._lock:
319
373
  old = self._hosts.get(host_id)
320
374
  if old is not None:
375
+ warn(f"[registry] register_host: replacing existing session host_id={host_id!r}")
321
376
  old.close()
322
377
 
323
378
  self._hosts[host_id] = session
@@ -331,7 +386,10 @@ class HostTunnelRegistry:
331
386
  self._persist_host(host_id)
332
387
  self._invalidate_caches_locked()
333
388
 
389
+ info(f"[registry] register_host done: host_id={host_id!r} online=True")
390
+
334
391
  def drop_host(self, host_id: str) -> None:
392
+ info(f"[registry] drop_host: host_id={host_id!r}")
335
393
  with self._lock:
336
394
  sess = self._hosts.pop(host_id, None)
337
395
  if sess is not None:
@@ -342,7 +400,6 @@ class HostTunnelRegistry:
342
400
  hs.last_seen_ms = now_ms()
343
401
  self._host_status[host_id] = hs
344
402
 
345
- # mark that host's devices offline (keep routes)
346
403
  for did in self._host_devices.get(host_id, set()):
347
404
  ds = self._device_status.get(did)
348
405
  if ds is not None:
@@ -351,6 +408,8 @@ class HostTunnelRegistry:
351
408
 
352
409
  self._invalidate_caches_locked()
353
410
 
411
+ info(f"[registry] drop_host done: host_id={host_id!r} online=False")
412
+
354
413
  def heartbeat(self, host_id: str, unix_ms: int = 0) -> None:
355
414
  now = int(unix_ms) or now_ms()
356
415
  with self._lock:
@@ -367,11 +426,11 @@ class HostTunnelRegistry:
367
426
 
368
427
  self._invalidate_caches_locked()
369
428
 
429
+ info(f"[registry] heartbeat: host_id={host_id!r} unix_ms={now}")
430
+
370
431
  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
- """
432
+ info(f"[registry] announce_devices: host_id={host_id!r} n={len(devices)}")
433
+
375
434
  # conflict check first (no partial apply)
376
435
  with self._lock:
377
436
  for d in devices:
@@ -380,6 +439,10 @@ class HostTunnelRegistry:
380
439
  continue
381
440
  existing = self._routes.get(device_id)
382
441
  if existing is not None and existing.host_id != host_id:
442
+ warn(
443
+ f"[registry] device_id conflict: device_id={device_id!r} "
444
+ f"existing_host={existing.host_id!r} new_host={host_id!r}"
445
+ )
383
446
  raise DeviceIdConflictError(
384
447
  device_id=device_id,
385
448
  existing_host=existing.host_id,
@@ -393,6 +456,7 @@ class HostTunnelRegistry:
393
456
  for d in devices:
394
457
  device_id = str(getattr(d, "device_id", "")).strip()
395
458
  if not device_id:
459
+ warn(f"[registry] announce_devices: skipping device with empty device_id host_id={host_id!r}")
396
460
  continue
397
461
 
398
462
  try:
@@ -400,6 +464,15 @@ class HostTunnelRegistry:
400
464
  except Exception:
401
465
  local_id = 0
402
466
 
467
+ label = str(getattr(d, "label", "") or "")
468
+ serial = str(getattr(d, "serial", "") or "")
469
+ kind = str(getattr(d, "kind", "") or "")
470
+
471
+ info(
472
+ f"[registry] device: host_id={host_id!r} local_id={local_id} "
473
+ f"device_id={device_id!r} label={label!r} serial={serial!r} kind={kind!r}"
474
+ )
475
+
403
476
  self._routes[device_id] = DeviceRoute(host_id=host_id, host_local_id=local_id)
404
477
  self._host_devices[host_id].add(device_id)
405
478
 
@@ -422,15 +495,17 @@ class HostTunnelRegistry:
422
495
  self._device_infos[device_id] = host_tunnel_pb2.DeviceInfo(
423
496
  device_id=device_id,
424
497
  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 ""),
498
+ label=label,
499
+ serial=serial,
500
+ kind=kind,
428
501
  )
429
502
 
430
503
  self._persist_device(host_id, device_id, local_id, d)
431
504
 
432
505
  self._invalidate_caches_locked()
433
506
 
507
+ info(f"[registry] announce_devices done: host_id={host_id!r} routes={len(self._routes)}")
508
+
434
509
  # ---------------------------
435
510
  # FAST PATHS (use per-RPC)
436
511
  # ---------------------------
@@ -446,7 +521,6 @@ class HostTunnelRegistry:
446
521
  did = (device_id or "").strip()
447
522
  if not did:
448
523
  return False
449
-
450
524
  now = now_ms()
451
525
  with self._lock:
452
526
  route = self._routes.get(did)
@@ -480,17 +554,17 @@ class HostTunnelRegistry:
480
554
  out: Dict[str, Tuple[str, Any, bool]] = {}
481
555
  for device_id, route in self._routes.items():
482
556
  info0 = self._device_infos.get(device_id)
483
- info = info0
557
+ info_obj = info0
484
558
  if info0 is not None:
485
559
  tmp = host_tunnel_pb2.DeviceInfo()
486
560
  try:
487
561
  tmp.CopyFrom(info0)
488
- info = tmp
562
+ info_obj = tmp
489
563
  except Exception:
490
- info = info0
564
+ info_obj = info0
491
565
 
492
566
  is_active = route.host_id in self._active_hosts_cache
493
- out[device_id] = (route.host_id, info, is_active)
567
+ out[device_id] = (route.host_id, info_obj, is_active)
494
568
 
495
569
  self._dir_cache = out
496
570
  self._dir_cache_ts_ms = now
@@ -513,7 +587,7 @@ class HostTunnelRegistry:
513
587
  return dict(self._device_status)
514
588
 
515
589
  # ---------------------------
516
- # forwarding (unchanged)
590
+ # forwarding
517
591
  # ---------------------------
518
592
 
519
593
  def _get_session_and_route(self, device_id: str) -> Tuple[HostSession, DeviceRoute]:
@@ -537,7 +611,17 @@ class HostTunnelRegistry:
537
611
  deadline_unix_ms: int = 0,
538
612
  cancel_on_timeout: bool = False,
539
613
  ) -> "generic_pb2.GenericRPCResponse":
540
- sess, route = self._get_session_and_route(str(device_id))
614
+ fn = str(getattr(request, "function_name", "") or "")
615
+ info(f"[forward] device_id={device_id!r} fn={fn!r} timeout_sec={timeout_sec}")
616
+
617
+ try:
618
+ sess, route = self._get_session_and_route(str(device_id))
619
+ except Exception as e:
620
+ exception(f"[forward] route/session lookup failed device_id={device_id!r} err={e!r}")
621
+ r = generic_pb2.GenericRPCResponse()
622
+ r.results["Ok"].CopyFrom(map_arg(False))
623
+ r.results["Error"].CopyFrom(map_arg(f"Route/session lookup failed: {e}"))
624
+ return r
541
625
 
542
626
  req_id = secrets.token_hex(12)
543
627
  fut: Future = Future()
@@ -555,6 +639,8 @@ class HostTunnelRegistry:
555
639
 
556
640
  frame = host_tunnel_pb2.HostFrame(rpc_request=rpc_req)
557
641
 
642
+ info(f"[forward] -> host_id={sess.host_id!r} req_id={req_id} local_id={route.host_local_id}")
643
+
558
644
  if not sess.send(frame, timeout=2.0):
559
645
  with sess.inflight_lock:
560
646
  sess.inflight.pop(req_id, None)
@@ -564,14 +650,19 @@ class HostTunnelRegistry:
564
650
  r.results["Error"].CopyFrom(map_arg(f"Host '{sess.host_id}' outbound queue full / not writable."))
565
651
  return r
566
652
 
653
+ t0 = time.time()
567
654
  try:
568
655
  resp: host_tunnel_pb2.RpcResponse = fut.result(timeout=timeout_sec)
569
656
  except Exception as e:
657
+ dt_ms = int((time.time() - t0) * 1000)
658
+ warn(f"[forward] wait failed req_id={req_id} dt_ms={dt_ms} err={e!r}")
659
+
570
660
  if cancel_on_timeout:
571
661
  try:
572
- sess.send(host_tunnel_pb2.HostFrame(cancel=host_tunnel_pb2.Cancel(req_id=req_id)))
662
+ ok = sess.send(host_tunnel_pb2.HostFrame(cancel=host_tunnel_pb2.Cancel(req_id=req_id)))
663
+ info(f"[forward] sent cancel req_id={req_id} ok={ok}")
573
664
  except Exception:
574
- pass
665
+ exception(f"[forward] failed sending cancel req_id={req_id}")
575
666
 
576
667
  with sess.inflight_lock:
577
668
  sess.inflight.pop(req_id, None)
@@ -584,6 +675,9 @@ class HostTunnelRegistry:
584
675
  with sess.inflight_lock:
585
676
  sess.inflight.pop(req_id, None)
586
677
 
678
+ dt_ms = int((time.time() - t0) * 1000)
679
+ info(f"[forward] <- resp req_id={req_id} ok={bool(resp.ok)} dt_ms={dt_ms} err={str(resp.error or '')!r}")
680
+
587
681
  if not resp.ok:
588
682
  r = generic_pb2.GenericRPCResponse()
589
683
  r.results["Ok"].CopyFrom(map_arg(False))
@@ -605,12 +699,14 @@ class HostTunnelRegistry:
605
699
  with self._lock:
606
700
  route = self._routes.get(str(device_id))
607
701
  if route is None:
702
+ warn(f"[forward] unknown device_id={device_id!r}")
608
703
  r = generic_pb2.GenericRPCResponse()
609
704
  r.results["Ok"].CopyFrom(map_arg(False))
610
705
  r.results["Error"].CopyFrom(map_arg(f"Unknown device_id={device_id}"))
611
706
  return r
612
707
 
613
708
  if route.host_id != host_id:
709
+ warn(f"[forward] device_id={device_id!r} not on host_id={host_id!r} (actual={route.host_id!r})")
614
710
  r = generic_pb2.GenericRPCResponse()
615
711
  r.results["Ok"].CopyFrom(map_arg(False))
616
712
  r.results["Error"].CopyFrom(map_arg(f"device_id={device_id} is not on host_id={host_id}"))
@@ -625,6 +721,10 @@ class HostTunnelRegistry:
625
721
  )
626
722
 
627
723
 
724
+ # =============================================================================
725
+ # Convenience forwarders
726
+ # =============================================================================
727
+
628
728
  def handle_host_device(
629
729
  registry: HostTunnelRegistry,
630
730
  *,
@@ -634,6 +734,7 @@ def handle_host_device(
634
734
  args: Any,
635
735
  timeout_sec: float = 10.0,
636
736
  ) -> Dict[str, Any]:
737
+ info(f"[handle_host_device] host_id={host_id!r} device_id={device_id!r} fn={function_name!r}")
637
738
  req = generic_pb2.GenericRPCRequest(function_name=str(function_name))
638
739
  _copy_argument_map(req.args, args)
639
740
 
@@ -646,7 +747,6 @@ def handle_host_device(
646
747
  )
647
748
  return dict(resp.results)
648
749
 
649
-
650
750
  def handle_host_device_request(
651
751
  registry: HostTunnelRegistry,
652
752
  *,
@@ -655,6 +755,10 @@ def handle_host_device_request(
655
755
  request: "generic_pb2.GenericRPCRequest",
656
756
  timeout_sec: float = 10.0,
657
757
  ) -> "generic_pb2.GenericRPCResponse":
758
+ info(
759
+ f"[handle_host_device_request] host_id={host_id!r} device_id={device_id!r} "
760
+ f"fn={str(getattr(request,'function_name','') or '')!r}"
761
+ )
658
762
  return registry.forward_request_by_host_device(
659
763
  host_id=host_id,
660
764
  device_id=device_id,
@@ -664,11 +768,23 @@ def handle_host_device_request(
664
768
  )
665
769
 
666
770
 
771
+ # =============================================================================
772
+ # Servicer
773
+ # =============================================================================
774
+
667
775
  class HostTunnelServicer(host_tunnel_pb2_grpc.HostTunnelServicer):
668
776
  def __init__(self, registry: HostTunnelRegistry) -> None:
669
777
  self.registry = registry
670
778
 
671
779
  def Connect(self, request_iterator: Iterator[host_tunnel_pb2.HostFrame], context):
780
+ try:
781
+ peer = context.peer()
782
+ except Exception:
783
+ peer = "unknown-peer"
784
+
785
+ peer_tag = _safe_thread_tag(peer)
786
+ info(f"[Connect] new stream peer={peer}")
787
+
672
788
  host_id: Optional[str] = None
673
789
  session: Optional[HostSession] = None
674
790
  stop = threading.Event()
@@ -676,23 +792,31 @@ class HostTunnelServicer(host_tunnel_pb2_grpc.HostTunnelServicer):
676
792
 
677
793
  def inbound_loop():
678
794
  nonlocal host_id, session
795
+ info(f"[inbound] start peer={peer}")
796
+
679
797
  try:
680
798
  for frame in request_iterator:
681
799
  which = frame.WhichOneof("msg")
682
800
  if which is None:
801
+ warn(f"[inbound] frame with no msg peer={peer}")
683
802
  continue
684
803
 
804
+ info(f"[inbound] IN type={which} peer={peer} host_id={host_id or '?'}")
805
+
685
806
  if which == "hello":
686
807
  hello = frame.hello
687
- host_id = str(hello.host_id).strip() or "unknown-host"
808
+ host_id = str(getattr(hello, "host_id", "")).strip() or "unknown-host"
809
+ ver = str(getattr(hello, "version", "") or "")
810
+ info(f"[inbound] HELLO host_id={host_id!r} version={ver!r} peer={peer}")
811
+
688
812
  session = HostSession(host_id)
689
813
  self.registry.register_host(host_id, session)
690
814
  self.registry.persist_host_meta_from_hello(hello)
691
815
 
692
- # Ask host for metadata immediately on join (best-effort)
816
+ # Ask host for metadata immediately on join
693
817
  try:
694
818
  mrid = secrets.token_hex(12)
695
- session.send(
819
+ ok = session.send(
696
820
  host_tunnel_pb2.HostFrame(
697
821
  meta_request=host_tunnel_pb2.MetaRequest(
698
822
  req_id=mrid,
@@ -702,110 +826,144 @@ class HostTunnelServicer(host_tunnel_pb2_grpc.HostTunnelServicer):
702
826
  )
703
827
  )
704
828
  )
829
+ info(f"[inbound] sent MetaRequest req_id={mrid} ok={ok} host_id={host_id!r}")
705
830
  except Exception:
706
- pass
831
+ exception("[inbound] FAILED sending MetaRequest")
707
832
 
708
- # Also ping/ack
833
+ # heartbeat ack
709
834
  try:
710
- session.send(
711
- host_tunnel_pb2.HostFrame(
712
- heartbeat=host_tunnel_pb2.Heartbeat(unix_ms=now_ms())
713
- )
714
- )
835
+ ok = session.send(host_tunnel_pb2.HostFrame(heartbeat=host_tunnel_pb2.Heartbeat(unix_ms=now_ms())))
836
+ info(f"[inbound] sent heartbeat ack ok={ok} host_id={host_id!r}")
715
837
  except Exception:
716
- pass
838
+ exception("[inbound] FAILED sending heartbeat ack")
717
839
 
718
840
  elif which == "device_announce":
719
841
  if session is None or host_id is None:
842
+ warn(f"[inbound] device_announce before hello/session: ignoring peer={peer}")
720
843
  continue
844
+
721
845
  ann = frame.device_announce
846
+ n = len(getattr(ann, "devices", []))
847
+ info(
848
+ f"[inbound] DEVICE_ANNOUNCE host_id={host_id!r} n={n} "
849
+ f"unix_ms={int(getattr(ann,'unix_ms',0) or 0)} full_snapshot={bool(getattr(ann,'full_snapshot',False))}"
850
+ )
851
+
852
+ for d in list(ann.devices):
853
+ info(
854
+ f"[inbound] device local_id={int(getattr(d,'local_id',0))} "
855
+ f"label={str(getattr(d,'label','') or '')!r} "
856
+ f"device_id={str(getattr(d,'device_id','') or '')!r} "
857
+ f"serial={str(getattr(d,'serial','') or '')!r} "
858
+ f"kind={str(getattr(d,'kind','') or '')!r}"
859
+ )
860
+
722
861
  try:
723
862
  self.registry.announce_devices(host_id, list(ann.devices))
863
+ info(f"[inbound] registry announce applied host_id={host_id!r}")
724
864
  except DeviceIdConflictError as e:
865
+ error(f"[inbound] DeviceIdConflictError host_id={host_id!r} err={e}")
725
866
  fatal["code"] = "ALREADY_EXISTS"
726
867
  fatal["details"] = str(e)
727
868
  stop.set()
728
869
  return
870
+ except Exception:
871
+ exception(f"[inbound] announce_devices crashed host_id={host_id!r}")
729
872
 
873
+ # heartbeat ack after announce
730
874
  try:
731
- session.send(
732
- host_tunnel_pb2.HostFrame(
733
- heartbeat=host_tunnel_pb2.Heartbeat(unix_ms=now_ms())
734
- )
735
- )
875
+ ok = session.send(host_tunnel_pb2.HostFrame(heartbeat=host_tunnel_pb2.Heartbeat(unix_ms=now_ms())))
876
+ info(f"[inbound] sent heartbeat ack after announce ok={ok} host_id={host_id!r}")
736
877
  except Exception:
737
- pass
878
+ exception("[inbound] FAILED sending heartbeat ack after announce")
738
879
 
739
880
  elif which == "rpc_response":
740
881
  if session is None:
882
+ warn(f"[inbound] rpc_response before session: ignoring peer={peer}")
741
883
  continue
884
+
742
885
  resp = frame.rpc_response
743
- rid = str(resp.req_id)
886
+ rid = str(getattr(resp, "req_id", "") or "")
887
+ ok = bool(getattr(resp, "ok", False))
888
+ err_s = str(getattr(resp, "error", "") or "")
889
+ info(f"[inbound] RPC_RESPONSE req_id={rid} ok={ok} error={err_s!r}")
744
890
 
745
891
  with session.inflight_lock:
746
892
  fut = session.inflight.get(rid)
747
893
 
748
894
  if fut is not None and not fut.done():
749
895
  fut.set_result(resp)
896
+ info(f"[inbound] delivered rpc_response to waiter req_id={rid}")
897
+ else:
898
+ warn(f"[inbound] rpc_response has no inflight waiter req_id={rid}")
750
899
 
751
900
  elif which == "meta_response":
752
- # Print host metadata (best-effort diagnostics)
753
901
  try:
754
902
  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()
903
+ ok = bool(getattr(mr, "ok", False))
904
+ err_s = str(getattr(mr, "error", "") or "")
905
+ info(f"[inbound] META_RESPONSE ok={ok} error={err_s!r}")
906
+ try:
907
+ info(f"[inbound] META_RESPONSE proto:\n{mr}")
908
+ except Exception:
909
+ pass
777
910
  except Exception:
778
- pass
911
+ exception("[inbound] meta_response handling crashed")
779
912
 
780
913
  elif which == "heartbeat":
781
914
  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)
915
+ ms = int(getattr(frame.heartbeat, "unix_ms", 0) or 0) or now_ms()
916
+ session.last_heartbeat_ms = ms
917
+ self.registry.heartbeat(host_id, ms)
918
+ info(f"[inbound] HEARTBEAT host_id={host_id!r} unix_ms={ms}")
919
+ else:
920
+ warn(f"[inbound] heartbeat before hello/session: ignoring peer={peer} host_id={(host_id or '?')!r}")
784
921
 
785
922
  elif which == "cancel":
786
- pass
923
+ try:
924
+ rid = str(getattr(frame.cancel, "req_id", "") or "")
925
+ warn(f"[inbound] CANCEL received from host?? req_id={rid} peer={peer}")
926
+ except Exception:
927
+ exception("[inbound] cancel frame parse failed")
928
+
929
+ else:
930
+ warn(f"[inbound] unknown frame type={which} peer={peer}")
787
931
 
788
932
  except Exception:
789
- pass
933
+ exception(f"[inbound] loop crashed peer={peer} host_id={host_id or '?'}")
790
934
  finally:
935
+ info(f"[inbound] exiting peer={peer} host_id={host_id or '?'}")
791
936
  stop.set()
792
937
  if host_id is not None:
793
- self.registry.drop_host(host_id)
938
+ try:
939
+ self.registry.drop_host(host_id)
940
+ except Exception:
941
+ exception(f"[inbound] registry.drop_host failed host_id={host_id!r}")
794
942
  if session is not None:
795
- session.close()
943
+ try:
944
+ session.close()
945
+ except Exception:
946
+ exception(f"[inbound] session.close failed host_id={host_id!r}")
796
947
 
797
- t = threading.Thread(target=inbound_loop, daemon=True)
948
+ t = threading.Thread(target=inbound_loop, daemon=True, name=f"inbound_{peer_tag}")
798
949
  t.start()
799
950
 
951
+ info(f"[Connect] outbound pump starting peer={peer}")
952
+
953
+ printed_waiting = False
800
954
  while not stop.is_set():
801
955
  if fatal:
802
956
  code = fatal.get("code", "")
803
957
  details = fatal.get("details", "fatal")
958
+ error(f"[Connect] aborting stream peer={peer} code={code} details={details}")
804
959
  if code == "ALREADY_EXISTS":
805
960
  context.abort(grpc.StatusCode.ALREADY_EXISTS, details)
806
961
  context.abort(grpc.StatusCode.UNKNOWN, details)
807
962
 
808
963
  if session is None:
964
+ if not printed_waiting:
965
+ info(f"[Connect] waiting for HELLO peer={peer}")
966
+ printed_waiting = True
809
967
  time.sleep(0.05)
810
968
  continue
811
969
 
@@ -813,11 +971,24 @@ class HostTunnelServicer(host_tunnel_pb2_grpc.HostTunnelServicer):
813
971
  out = session.out_q.get(timeout=0.5)
814
972
  except queue.Empty:
815
973
  continue
974
+ except Exception:
975
+ exception(f"[Connect] outbound queue get failed peer={peer} host_id={host_id or '?'}")
976
+ continue
816
977
 
817
978
  if out is None:
979
+ info(f"[Connect] outbound sentinel -> end stream peer={peer} host_id={host_id or '?'}")
818
980
  break
981
+
982
+ out_type = out.WhichOneof("msg")
983
+ info(f"[Connect] OUT type={out_type} peer={peer} host_id={host_id or '?'}")
819
984
  yield out
820
985
 
986
+ info(f"[Connect] stream ended peer={peer} host_id={host_id or '?'}")
987
+
988
+
989
+ # =============================================================================
990
+ # Credentials + server start
991
+ # =============================================================================
821
992
 
822
993
  def build_server_credentials(
823
994
  *,
@@ -826,13 +997,14 @@ def build_server_credentials(
826
997
  client_ca_pem: Optional[bytes] = None,
827
998
  require_client_auth: bool = True,
828
999
  ) -> grpc.ServerCredentials:
1000
+ info(f"[creds] build_server_credentials client_ca={'yes' if bool(client_ca_pem) else 'no'} require_client_auth={require_client_auth}")
829
1001
  if client_ca_pem:
830
1002
  return grpc.ssl_server_credentials(
831
1003
  ((private_key_pem, certificate_chain_pem,),),
832
1004
  root_certificates=client_ca_pem,
833
1005
  require_client_auth=require_client_auth,
834
1006
  )
835
- return grpc.ssl_server_credentials(((private_key_pem, certificate_chain_pem,),))
1007
+ return grpc.ssl_server_credentials(((private_key_pem, certificate_chain_pem),))
836
1008
 
837
1009
 
838
1010
  def start_host_tunnel_server(
@@ -854,18 +1026,34 @@ def start_host_tunnel_server(
854
1026
  ("grpc.keepalive_permit_without_calls", 1),
855
1027
  ]
856
1028
 
1029
+ bind_addr = f"{host}:{int(port)}"
1030
+ info(f"[server] creating grpc.server max_workers={max_workers} bind={bind_addr}")
1031
+
857
1032
  server = grpc.server(
858
1033
  futures.ThreadPoolExecutor(max_workers=max_workers),
859
1034
  options=options,
860
1035
  )
861
1036
 
862
1037
  host_tunnel_pb2_grpc.add_HostTunnelServicer_to_server(HostTunnelServicer(reg), server)
863
- server.add_secure_port(f"{host}:{int(port)}", server_credentials)
1038
+
1039
+ try:
1040
+ added = server.add_secure_port(bind_addr, server_credentials)
1041
+ info(f"[server] add_secure_port addr={bind_addr} -> {added}")
1042
+ except Exception:
1043
+ exception(f"[server] add_secure_port failed addr={bind_addr}")
1044
+ raise
864
1045
 
865
1046
  def run():
866
- server.start()
867
- server.wait_for_termination()
1047
+ try:
1048
+ info(f"[server] starting on {bind_addr}")
1049
+ server.start()
1050
+ info(f"[server] started on {bind_addr} (waiting for termination)")
1051
+ server.wait_for_termination()
1052
+ info("[server] wait_for_termination returned")
1053
+ except Exception:
1054
+ exception("[server] run() crashed")
1055
+ raise
868
1056
 
869
- th = threading.Thread(target=run, daemon=True)
1057
+ th = threading.Thread(target=run, daemon=True, name="host_tunnel_server")
870
1058
  th.start()
871
1059
  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.66
4
4
  Description-Content-Type: text/markdown
5
5
  Requires-Dist: grpcio
6
6
  Requires-Dist: protobuf