remoteRF-server 0.1.65__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.65 → remoterf_server-0.1.66}/PKG-INFO +1 -1
  2. {remoterf_server-0.1.65 → remoterf_server-0.1.66}/pyproject.toml +1 -1
  3. {remoterf_server-0.1.65 → remoterf_server-0.1.66}/src/remoteRF_server/host/host_tunnel_server.py +144 -96
  4. {remoterf_server-0.1.65 → remoterf_server-0.1.66}/src/remoteRF_server.egg-info/PKG-INFO +1 -1
  5. {remoterf_server-0.1.65 → remoterf_server-0.1.66}/README.md +0 -0
  6. {remoterf_server-0.1.65 → remoterf_server-0.1.66}/setup.cfg +0 -0
  7. {remoterf_server-0.1.65 → remoterf_server-0.1.66}/src/remoteRF_server/__init__.py +0 -0
  8. {remoterf_server-0.1.65 → remoterf_server-0.1.66}/src/remoteRF_server/common/__init__.py +0 -0
  9. {remoterf_server-0.1.65 → remoterf_server-0.1.66}/src/remoteRF_server/common/grpc/__init__.py +0 -0
  10. {remoterf_server-0.1.65 → remoterf_server-0.1.66}/src/remoteRF_server/common/grpc/grpc_host_pb2.py +0 -0
  11. {remoterf_server-0.1.65 → remoterf_server-0.1.66}/src/remoteRF_server/common/grpc/grpc_host_pb2_grpc.py +0 -0
  12. {remoterf_server-0.1.65 → remoterf_server-0.1.66}/src/remoteRF_server/common/grpc/grpc_pb2.py +0 -0
  13. {remoterf_server-0.1.65 → remoterf_server-0.1.66}/src/remoteRF_server/common/grpc/grpc_pb2_grpc.py +0 -0
  14. {remoterf_server-0.1.65 → remoterf_server-0.1.66}/src/remoteRF_server/common/utils/__init__.py +0 -0
  15. {remoterf_server-0.1.65 → remoterf_server-0.1.66}/src/remoteRF_server/common/utils/ansi_codes.py +0 -0
  16. {remoterf_server-0.1.65 → remoterf_server-0.1.66}/src/remoteRF_server/common/utils/api_token.py +0 -0
  17. {remoterf_server-0.1.65 → remoterf_server-0.1.66}/src/remoteRF_server/common/utils/db_connection.py +0 -0
  18. {remoterf_server-0.1.65 → remoterf_server-0.1.66}/src/remoteRF_server/common/utils/db_location.py +0 -0
  19. {remoterf_server-0.1.65 → remoterf_server-0.1.66}/src/remoteRF_server/common/utils/list_string.py +0 -0
  20. {remoterf_server-0.1.65 → remoterf_server-0.1.66}/src/remoteRF_server/common/utils/process_arg.py +0 -0
  21. {remoterf_server-0.1.65 → remoterf_server-0.1.66}/src/remoteRF_server/drivers/__init__.py +0 -0
  22. {remoterf_server-0.1.65 → remoterf_server-0.1.66}/src/remoteRF_server/drivers/adalm_pluto/__init__.py +0 -0
  23. {remoterf_server-0.1.65 → remoterf_server-0.1.66}/src/remoteRF_server/drivers/adalm_pluto/pluto_remote_server.py +0 -0
  24. {remoterf_server-0.1.65 → remoterf_server-0.1.66}/src/remoteRF_server/host/__init__.py +0 -0
  25. {remoterf_server-0.1.65 → remoterf_server-0.1.66}/src/remoteRF_server/host/host_directory_store.py +0 -0
  26. {remoterf_server-0.1.65 → remoterf_server-0.1.66}/src/remoteRF_server/server/__init__.py +0 -0
  27. {remoterf_server-0.1.65 → remoterf_server-0.1.66}/src/remoteRF_server/server/acc_perms.py +0 -0
  28. {remoterf_server-0.1.65 → remoterf_server-0.1.66}/src/remoteRF_server/server/cert_provider.py +0 -0
  29. {remoterf_server-0.1.65 → remoterf_server-0.1.66}/src/remoteRF_server/server/device_manager.py +0 -0
  30. {remoterf_server-0.1.65 → remoterf_server-0.1.66}/src/remoteRF_server/server/grpc_server.py +0 -0
  31. {remoterf_server-0.1.65 → remoterf_server-0.1.66}/src/remoteRF_server/server/reservation.py +0 -0
  32. {remoterf_server-0.1.65 → remoterf_server-0.1.66}/src/remoteRF_server/server/rpc_manager.py +0 -0
  33. {remoterf_server-0.1.65 → remoterf_server-0.1.66}/src/remoteRF_server/server/user_group_cli.py +0 -0
  34. {remoterf_server-0.1.65 → remoterf_server-0.1.66}/src/remoteRF_server/server/user_group_handler.py +0 -0
  35. {remoterf_server-0.1.65 → remoterf_server-0.1.66}/src/remoteRF_server/serverrf_cli.py +0 -0
  36. {remoterf_server-0.1.65 → remoterf_server-0.1.66}/src/remoteRF_server/tools/__init__.py +0 -0
  37. {remoterf_server-0.1.65 → remoterf_server-0.1.66}/src/remoteRF_server/tools/gen_certs.py +0 -0
  38. {remoterf_server-0.1.65 → remoterf_server-0.1.66}/src/remoteRF_server.egg-info/SOURCES.txt +0 -0
  39. {remoterf_server-0.1.65 → remoterf_server-0.1.66}/src/remoteRF_server.egg-info/dependency_links.txt +0 -0
  40. {remoterf_server-0.1.65 → remoterf_server-0.1.66}/src/remoteRF_server.egg-info/entry_points.txt +0 -0
  41. {remoterf_server-0.1.65 → remoterf_server-0.1.66}/src/remoteRF_server.egg-info/requires.txt +0 -0
  42. {remoterf_server-0.1.65 → 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.65
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.65"
7
+ version = "0.1.66"
8
8
  dependencies = [
9
9
  "grpcio",
10
10
  "protobuf",
@@ -8,11 +8,14 @@ import queue
8
8
  import time
9
9
  import secrets
10
10
  import traceback
11
+ import hashlib
12
+ import re
13
+
11
14
  from dataclasses import dataclass
12
15
  from typing import Dict, Optional, Tuple, Any, List, Set, Iterator
16
+
13
17
  from concurrent.futures import Future
14
18
  from concurrent import futures
15
- from pathlib import Path
16
19
 
17
20
  import grpc
18
21
 
@@ -34,11 +37,8 @@ from .host_directory_store import (
34
37
  cfg_dir_from_file,
35
38
  )
36
39
 
37
- import hashlib
38
-
39
-
40
40
  # =============================================================================
41
- # Always-on logging (no levels to configure; INFO/WARN/ERROR always print)
41
+ # Always-on logging (INFO/WARN/ERROR always print; no config/levels)
42
42
  # =============================================================================
43
43
 
44
44
  def _ts() -> str:
@@ -66,10 +66,17 @@ def exception(msg: str) -> None:
66
66
  traceback.print_exc(limit=80, file=sys.stderr)
67
67
 
68
68
 
69
+ # =============================================================================
70
+ # Helpers
71
+ # =============================================================================
72
+
69
73
  def host_key(host_id: str) -> str:
70
74
  h = hashlib.sha256((host_id or "").encode("utf-8")).hexdigest()
71
75
  return h[:16] # short stable key
72
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]
73
80
 
74
81
  def _copy_argument_map(dst_map: Any, src_map: Any) -> None:
75
82
  if src_map is None:
@@ -82,19 +89,21 @@ def _copy_argument_map(dst_map: Any, src_map: Any) -> None:
82
89
  dst_map[str(k)].CopyFrom(v)
83
90
 
84
91
 
92
+ # =============================================================================
93
+ # Types
94
+ # =============================================================================
95
+
85
96
  @dataclass(frozen=True)
86
97
  class DeviceRoute:
87
98
  host_id: str
88
99
  host_local_id: int
89
100
 
90
-
91
101
  @dataclass
92
102
  class HostStatus:
93
103
  host_id: str
94
104
  online: bool = False
95
105
  last_seen_ms: int = 0
96
106
 
97
-
98
107
  @dataclass
99
108
  class DeviceStatus:
100
109
  device_id: str
@@ -104,6 +113,10 @@ class DeviceStatus:
104
113
  last_seen_ms: int = 0
105
114
 
106
115
 
116
+ # =============================================================================
117
+ # Session
118
+ # =============================================================================
119
+
107
120
  class HostSession:
108
121
  """
109
122
  Represents one connected host (one bidi stream).
@@ -113,48 +126,54 @@ class HostSession:
113
126
  """
114
127
  def __init__(self, host_id: str, *, outbound_max: int = 2048) -> None:
115
128
  self.host_id = host_id
116
- 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))
117
130
  self.inflight: Dict[str, Future] = {}
118
131
  self.inflight_lock = threading.Lock()
119
132
  self.last_heartbeat_ms = now_ms()
120
133
  self.alive = True
121
134
  self.outbound_max = int(outbound_max)
122
135
 
123
- info(f"[session] created host_id={self.host_id} outbound_max={self.outbound_max}")
136
+ info(f"[session] created host_id={self.host_id!r} outbound_max={self.outbound_max}")
124
137
 
125
138
  def send(self, frame: host_tunnel_pb2.HostFrame, *, timeout: float = 2.0) -> bool:
139
+ which = frame.WhichOneof("msg")
126
140
  if not self.alive:
127
- warn(f"[session] send refused: host_id={self.host_id} (session not alive) frame={frame.WhichOneof('msg')}")
141
+ warn(f"[session] send refused (dead session) host_id={self.host_id!r} type={which}")
128
142
  return False
129
- which = frame.WhichOneof("msg")
130
143
  try:
131
144
  self.out_q.put(frame, timeout=timeout)
132
- info(f"[session] queued outbound frame host_id={self.host_id} type={which}")
145
+ info(f"[session] queued outbound host_id={self.host_id!r} type={which}")
133
146
  return True
134
147
  except queue.Full:
135
- warn(f"[session] outbound queue FULL host_id={self.host_id} max={self.outbound_max} drop type={which}")
148
+ warn(f"[session] outbound queue FULL host_id={self.host_id!r} max={self.outbound_max} drop type={which}")
136
149
  return False
137
150
  except Exception:
138
- exception(f"[session] send exception host_id={self.host_id} type={which}")
151
+ exception(f"[session] send exception host_id={self.host_id!r} type={which}")
139
152
  return False
140
153
 
141
154
  def close(self) -> None:
142
155
  if not self.alive:
143
156
  return
144
- info(f"[session] closing host_id={self.host_id}")
157
+ info(f"[session] closing host_id={self.host_id!r}")
145
158
  self.alive = False
159
+
146
160
  try:
147
161
  self.out_q.put_nowait(None)
148
162
  except Exception:
149
- exception(f"[session] close: failed to enqueue sentinel host_id={self.host_id}")
163
+ exception(f"[session] close: failed to enqueue sentinel host_id={self.host_id!r}")
150
164
 
151
165
  with self.inflight_lock:
152
166
  for rid, fut in list(self.inflight.items()):
153
167
  if not fut.done():
154
168
  fut.set_exception(RuntimeError(f"Host '{self.host_id}' disconnected (req_id={rid})"))
155
169
  self.inflight.clear()
156
- info(f"[session] closed host_id={self.host_id}")
157
170
 
171
+ info(f"[session] closed host_id={self.host_id!r}")
172
+
173
+
174
+ # =============================================================================
175
+ # Registry
176
+ # =============================================================================
158
177
 
159
178
  class HostTunnelRegistry:
160
179
  """
@@ -175,9 +194,9 @@ class HostTunnelRegistry:
175
194
  self._hosts: Dict[str, HostSession] = {} # host_id -> session
176
195
 
177
196
  # 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
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
181
200
 
182
201
  # status
183
202
  self._host_status: Dict[str, HostStatus] = {}
@@ -194,13 +213,17 @@ class HostTunnelRegistry:
194
213
  self._active_hosts_cache_ts_ms: int = 0
195
214
  self._active_hosts_cache_ttl_ms: int = int(active_cache_ttl_ms)
196
215
 
197
- self._dir_cache: Dict[str, Tuple[str, Tuple[str, Any, bool], bool]] = {} # not used externally
216
+ self._dir_cache: Dict[str, Tuple[str, Any, bool]] = {}
198
217
  self._dir_cache_ts_ms: int = 0
199
218
  self._dir_cache_ttl_ms: int = int(dir_cache_ttl_ms)
200
219
 
201
220
  info(f"[registry] init cfg_dir={cfg}")
202
221
  self._load_persisted()
203
222
 
223
+ # ---------------------------
224
+ # caching helpers
225
+ # ---------------------------
226
+
204
227
  def _invalidate_caches_locked(self) -> None:
205
228
  self._active_hosts_cache_ts_ms = 0
206
229
  self._dir_cache_ts_ms = 0
@@ -231,6 +254,7 @@ class HostTunnelRegistry:
231
254
  self._host_status.clear()
232
255
  self._device_status.clear()
233
256
 
257
+ # seed hosts offline
234
258
  for hid in known_hosts:
235
259
  if hid:
236
260
  self._host_status[hid] = HostStatus(host_id=hid, online=False, last_seen_ms=0)
@@ -305,13 +329,13 @@ class HostTunnelRegistry:
305
329
  ver = str(getattr(hello, "version", "") or "").strip()
306
330
  if hid:
307
331
  self._persist_host_meta(hid, version=ver)
308
- info(f"[registry] persisted host meta from hello: host_id={hid} version={ver!r}")
332
+ info(f"[registry] persisted host meta from hello: host_id={hid!r} version={ver!r}")
309
333
  except Exception:
310
334
  exception("[registry] persist_host_meta_from_hello failed")
311
335
 
312
336
  def _persist_host(self, host_id: str) -> None:
313
337
  self._env_lists.append_to_csv_list("KNOWN_HOSTS", host_id)
314
- info(f"[registry] persisted host list: host_id={host_id}")
338
+ info(f"[registry] persisted host list: host_id={host_id!r}")
315
339
 
316
340
  def _persist_device(self, host_id: str, device_id: str, local_id: int, device_info: Any) -> None:
317
341
  self._env_lists.append_to_csv_list("KNOWN_DEVICES", device_id)
@@ -334,18 +358,21 @@ class HostTunnelRegistry:
334
358
  if kind:
335
359
  self._env_meta.upsert_kv(f"DEVICE_{dk}_KIND", kind)
336
360
 
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}")
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
+ )
338
365
 
339
366
  # ---------------------------
340
367
  # directory/status mutators
341
368
  # ---------------------------
342
369
 
343
370
  def register_host(self, host_id: str, session: HostSession) -> None:
344
- info(f"[registry] register_host: host_id={host_id}")
371
+ info(f"[registry] register_host: host_id={host_id!r}")
345
372
  with self._lock:
346
373
  old = self._hosts.get(host_id)
347
374
  if old is not None:
348
- warn(f"[registry] register_host: replacing existing session host_id={host_id}")
375
+ warn(f"[registry] register_host: replacing existing session host_id={host_id!r}")
349
376
  old.close()
350
377
 
351
378
  self._hosts[host_id] = session
@@ -359,10 +386,10 @@ class HostTunnelRegistry:
359
386
  self._persist_host(host_id)
360
387
  self._invalidate_caches_locked()
361
388
 
362
- info(f"[registry] register_host done: host_id={host_id} online=True")
389
+ info(f"[registry] register_host done: host_id={host_id!r} online=True")
363
390
 
364
391
  def drop_host(self, host_id: str) -> None:
365
- info(f"[registry] drop_host: host_id={host_id}")
392
+ info(f"[registry] drop_host: host_id={host_id!r}")
366
393
  with self._lock:
367
394
  sess = self._hosts.pop(host_id, None)
368
395
  if sess is not None:
@@ -381,7 +408,7 @@ class HostTunnelRegistry:
381
408
 
382
409
  self._invalidate_caches_locked()
383
410
 
384
- info(f"[registry] drop_host done: host_id={host_id} online=False")
411
+ info(f"[registry] drop_host done: host_id={host_id!r} online=False")
385
412
 
386
413
  def heartbeat(self, host_id: str, unix_ms: int = 0) -> None:
387
414
  now = int(unix_ms) or now_ms()
@@ -399,12 +426,12 @@ class HostTunnelRegistry:
399
426
 
400
427
  self._invalidate_caches_locked()
401
428
 
402
- info(f"[registry] heartbeat: host_id={host_id} unix_ms={now}")
429
+ info(f"[registry] heartbeat: host_id={host_id!r} unix_ms={now}")
403
430
 
404
431
  def announce_devices(self, host_id: str, devices: list) -> None:
405
- info(f"[registry] announce_devices: host_id={host_id} n={len(devices)}")
432
+ info(f"[registry] announce_devices: host_id={host_id!r} n={len(devices)}")
406
433
 
407
- # conflict check (no partial apply)
434
+ # conflict check first (no partial apply)
408
435
  with self._lock:
409
436
  for d in devices:
410
437
  device_id = str(getattr(d, "device_id", "")).strip()
@@ -412,7 +439,10 @@ class HostTunnelRegistry:
412
439
  continue
413
440
  existing = self._routes.get(device_id)
414
441
  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}")
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
+ )
416
446
  raise DeviceIdConflictError(
417
447
  device_id=device_id,
418
448
  existing_host=existing.host_id,
@@ -426,7 +456,7 @@ class HostTunnelRegistry:
426
456
  for d in devices:
427
457
  device_id = str(getattr(d, "device_id", "")).strip()
428
458
  if not device_id:
429
- warn(f"[registry] announce_devices: skipping device with empty device_id host_id={host_id}")
459
+ warn(f"[registry] announce_devices: skipping device with empty device_id host_id={host_id!r}")
430
460
  continue
431
461
 
432
462
  try:
@@ -438,7 +468,10 @@ class HostTunnelRegistry:
438
468
  serial = str(getattr(d, "serial", "") or "")
439
469
  kind = str(getattr(d, "kind", "") or "")
440
470
 
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}")
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
+ )
442
475
 
443
476
  self._routes[device_id] = DeviceRoute(host_id=host_id, host_local_id=local_id)
444
477
  self._host_devices[host_id].add(device_id)
@@ -471,7 +504,7 @@ class HostTunnelRegistry:
471
504
 
472
505
  self._invalidate_caches_locked()
473
506
 
474
- info(f"[registry] announce_devices done: host_id={host_id} routes={len(self._routes)}")
507
+ info(f"[registry] announce_devices done: host_id={host_id!r} routes={len(self._routes)}")
475
508
 
476
509
  # ---------------------------
477
510
  # FAST PATHS (use per-RPC)
@@ -514,8 +547,7 @@ class HostTunnelRegistry:
514
547
 
515
548
  with self._lock:
516
549
  if self._dir_cache_ts_ms and (now - self._dir_cache_ts_ms) < ttl:
517
- # rebuild from saved cache is omitted (original returned dict(self._dir_cache))
518
- pass
550
+ return dict(self._dir_cache)
519
551
 
520
552
  self._refresh_active_hosts_cache_locked(now)
521
553
 
@@ -534,6 +566,7 @@ class HostTunnelRegistry:
534
566
  is_active = route.host_id in self._active_hosts_cache
535
567
  out[device_id] = (route.host_id, info_obj, is_active)
536
568
 
569
+ self._dir_cache = out
537
570
  self._dir_cache_ts_ms = now
538
571
  return dict(out)
539
572
 
@@ -578,14 +611,16 @@ class HostTunnelRegistry:
578
611
  deadline_unix_ms: int = 0,
579
612
  cancel_on_timeout: bool = False,
580
613
  ) -> "generic_pb2.GenericRPCResponse":
581
- info(f"[forward] device_id={device_id!r} fn={getattr(request,'function_name','')!r} timeout={timeout_sec}")
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
+
582
617
  try:
583
618
  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}")
619
+ except Exception as e:
620
+ exception(f"[forward] route/session lookup failed device_id={device_id!r} err={e!r}")
586
621
  r = generic_pb2.GenericRPCResponse()
587
622
  r.results["Ok"].CopyFrom(map_arg(False))
588
- r.results["Error"].CopyFrom(map_arg("Route/session lookup failed (see server logs)."))
623
+ r.results["Error"].CopyFrom(map_arg(f"Route/session lookup failed: {e}"))
589
624
  return r
590
625
 
591
626
  req_id = secrets.token_hex(12)
@@ -604,7 +639,7 @@ class HostTunnelRegistry:
604
639
 
605
640
  frame = host_tunnel_pb2.HostFrame(rpc_request=rpc_req)
606
641
 
607
- info(f"[forward] -> host_id={sess.host_id} req_id={req_id} local_id={route.host_local_id}")
642
+ info(f"[forward] -> host_id={sess.host_id!r} req_id={req_id} local_id={route.host_local_id}")
608
643
 
609
644
  if not sess.send(frame, timeout=2.0):
610
645
  with sess.inflight_lock:
@@ -615,14 +650,17 @@ class HostTunnelRegistry:
615
650
  r.results["Error"].CopyFrom(map_arg(f"Host '{sess.host_id}' outbound queue full / not writable."))
616
651
  return r
617
652
 
653
+ t0 = time.time()
618
654
  try:
619
655
  resp: host_tunnel_pb2.RpcResponse = fut.result(timeout=timeout_sec)
620
656
  except Exception as e:
621
- warn(f"[forward] timeout/exception waiting resp req_id={req_id} err={e!r}")
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
+
622
660
  if cancel_on_timeout:
623
661
  try:
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}")
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}")
626
664
  except Exception:
627
665
  exception(f"[forward] failed sending cancel req_id={req_id}")
628
666
 
@@ -637,7 +675,8 @@ class HostTunnelRegistry:
637
675
  with sess.inflight_lock:
638
676
  sess.inflight.pop(req_id, None)
639
677
 
640
- info(f"[forward] <- resp req_id={req_id} ok={resp.ok} err={resp.error!r}")
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}")
641
680
 
642
681
  if not resp.ok:
643
682
  r = generic_pb2.GenericRPCResponse()
@@ -682,6 +721,10 @@ class HostTunnelRegistry:
682
721
  )
683
722
 
684
723
 
724
+ # =============================================================================
725
+ # Convenience forwarders
726
+ # =============================================================================
727
+
685
728
  def handle_host_device(
686
729
  registry: HostTunnelRegistry,
687
730
  *,
@@ -704,7 +747,6 @@ def handle_host_device(
704
747
  )
705
748
  return dict(resp.results)
706
749
 
707
-
708
750
  def handle_host_device_request(
709
751
  registry: HostTunnelRegistry,
710
752
  *,
@@ -713,7 +755,10 @@ def handle_host_device_request(
713
755
  request: "generic_pb2.GenericRPCRequest",
714
756
  timeout_sec: float = 10.0,
715
757
  ) -> "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}")
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
+ )
717
762
  return registry.forward_request_by_host_device(
718
763
  host_id=host_id,
719
764
  device_id=device_id,
@@ -723,17 +768,21 @@ def handle_host_device_request(
723
768
  )
724
769
 
725
770
 
771
+ # =============================================================================
772
+ # Servicer
773
+ # =============================================================================
774
+
726
775
  class HostTunnelServicer(host_tunnel_pb2_grpc.HostTunnelServicer):
727
776
  def __init__(self, registry: HostTunnelRegistry) -> None:
728
777
  self.registry = registry
729
778
 
730
779
  def Connect(self, request_iterator: Iterator[host_tunnel_pb2.HostFrame], context):
731
- peer = ""
732
780
  try:
733
781
  peer = context.peer()
734
782
  except Exception:
735
783
  peer = "unknown-peer"
736
784
 
785
+ peer_tag = _safe_thread_tag(peer)
737
786
  info(f"[Connect] new stream peer={peer}")
738
787
 
739
788
  host_id: Optional[str] = None
@@ -744,14 +793,15 @@ class HostTunnelServicer(host_tunnel_pb2_grpc.HostTunnelServicer):
744
793
  def inbound_loop():
745
794
  nonlocal host_id, session
746
795
  info(f"[inbound] start peer={peer}")
796
+
747
797
  try:
748
798
  for frame in request_iterator:
749
799
  which = frame.WhichOneof("msg")
750
800
  if which is None:
751
- warn(f"[inbound] received frame with no msg (peer={peer})")
801
+ warn(f"[inbound] frame with no msg peer={peer}")
752
802
  continue
753
803
 
754
- info(f"[inbound] frame type={which} peer={peer} host_id={host_id or '?'}")
804
+ info(f"[inbound] IN type={which} peer={peer} host_id={host_id or '?'}")
755
805
 
756
806
  if which == "hello":
757
807
  hello = frame.hello
@@ -776,28 +826,28 @@ class HostTunnelServicer(host_tunnel_pb2_grpc.HostTunnelServicer):
776
826
  )
777
827
  )
778
828
  )
779
- info(f"[inbound] sent MetaRequest req_id={mrid} ok={ok} host_id={host_id}")
829
+ info(f"[inbound] sent MetaRequest req_id={mrid} ok={ok} host_id={host_id!r}")
780
830
  except Exception:
781
831
  exception("[inbound] FAILED sending MetaRequest")
782
832
 
783
- # Also ping/ack
833
+ # heartbeat ack
784
834
  try:
785
- ok = session.send(
786
- host_tunnel_pb2.HostFrame(
787
- heartbeat=host_tunnel_pb2.Heartbeat(unix_ms=now_ms())
788
- )
789
- )
790
- info(f"[inbound] sent heartbeat ack ok={ok} host_id={host_id}")
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}")
791
837
  except Exception:
792
838
  exception("[inbound] FAILED sending heartbeat ack")
793
839
 
794
840
  elif which == "device_announce":
795
841
  if session is None or host_id is None:
796
- warn("[inbound] device_announce before hello; ignoring")
842
+ warn(f"[inbound] device_announce before hello/session: ignoring peer={peer}")
797
843
  continue
798
844
 
799
845
  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))}")
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
+ )
801
851
 
802
852
  for d in list(ann.devices):
803
853
  info(
@@ -810,42 +860,40 @@ class HostTunnelServicer(host_tunnel_pb2_grpc.HostTunnelServicer):
810
860
 
811
861
  try:
812
862
  self.registry.announce_devices(host_id, list(ann.devices))
813
- info(f"[inbound] registry announce applied host_id={host_id}")
863
+ info(f"[inbound] registry announce applied host_id={host_id!r}")
814
864
  except DeviceIdConflictError as e:
815
- error(f"[inbound] DeviceIdConflictError host_id={host_id} err={e}")
865
+ error(f"[inbound] DeviceIdConflictError host_id={host_id!r} err={e}")
816
866
  fatal["code"] = "ALREADY_EXISTS"
817
867
  fatal["details"] = str(e)
818
868
  stop.set()
819
869
  return
820
870
  except Exception:
821
- exception(f"[inbound] announce_devices crashed host_id={host_id}")
871
+ exception(f"[inbound] announce_devices crashed host_id={host_id!r}")
822
872
 
823
- # ack heartbeat
873
+ # heartbeat ack after announce
824
874
  try:
825
- ok = session.send(
826
- host_tunnel_pb2.HostFrame(
827
- heartbeat=host_tunnel_pb2.Heartbeat(unix_ms=now_ms())
828
- )
829
- )
830
- info(f"[inbound] sent heartbeat ack after announce ok={ok} host_id={host_id}")
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}")
831
877
  except Exception:
832
878
  exception("[inbound] FAILED sending heartbeat ack after announce")
833
879
 
834
880
  elif which == "rpc_response":
835
881
  if session is None:
836
- warn("[inbound] rpc_response before session; ignoring")
882
+ warn(f"[inbound] rpc_response before session: ignoring peer={peer}")
837
883
  continue
838
- resp = frame.rpc_response
839
- rid = str(getattr(resp, "req_id", ""))
840
884
 
841
- info(f"[inbound] RPC_RESPONSE req_id={rid} ok={bool(getattr(resp,'ok',False))} err={str(getattr(resp,'error','') or '')!r}")
885
+ resp = frame.rpc_response
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}")
842
890
 
843
891
  with session.inflight_lock:
844
892
  fut = session.inflight.get(rid)
845
893
 
846
894
  if fut is not None and not fut.done():
847
895
  fut.set_result(resp)
848
- info(f"[inbound] rpc_response delivered to inflight future req_id={rid}")
896
+ info(f"[inbound] delivered rpc_response to waiter req_id={rid}")
849
897
  else:
850
898
  warn(f"[inbound] rpc_response has no inflight waiter req_id={rid}")
851
899
 
@@ -855,13 +903,10 @@ class HostTunnelServicer(host_tunnel_pb2_grpc.HostTunnelServicer):
855
903
  ok = bool(getattr(mr, "ok", False))
856
904
  err_s = str(getattr(mr, "error", "") or "")
857
905
  info(f"[inbound] META_RESPONSE ok={ok} error={err_s!r}")
858
-
859
- # Print full proto for maximum visibility
860
906
  try:
861
907
  info(f"[inbound] META_RESPONSE proto:\n{mr}")
862
908
  except Exception:
863
909
  pass
864
-
865
910
  except Exception:
866
911
  exception("[inbound] meta_response handling crashed")
867
912
 
@@ -870,20 +915,19 @@ class HostTunnelServicer(host_tunnel_pb2_grpc.HostTunnelServicer):
870
915
  ms = int(getattr(frame.heartbeat, "unix_ms", 0) or 0) or now_ms()
871
916
  session.last_heartbeat_ms = ms
872
917
  self.registry.heartbeat(host_id, ms)
873
- info(f"[inbound] HEARTBEAT host_id={host_id} unix_ms={ms}")
918
+ info(f"[inbound] HEARTBEAT host_id={host_id!r} unix_ms={ms}")
874
919
  else:
875
- warn("[inbound] heartbeat before hello/session; ignoring")
920
+ warn(f"[inbound] heartbeat before hello/session: ignoring peer={peer} host_id={(host_id or '?')!r}")
876
921
 
877
922
  elif which == "cancel":
878
- # not used on server inbound, but log it if it happens
879
923
  try:
880
924
  rid = str(getattr(frame.cancel, "req_id", "") or "")
881
- warn(f"[inbound] CANCEL frame received from host?? req_id={rid}")
925
+ warn(f"[inbound] CANCEL received from host?? req_id={rid} peer={peer}")
882
926
  except Exception:
883
- exception("[inbound] cancel frame parsing failed")
927
+ exception("[inbound] cancel frame parse failed")
884
928
 
885
929
  else:
886
- warn(f"[inbound] unknown frame type={which}")
930
+ warn(f"[inbound] unknown frame type={which} peer={peer}")
887
931
 
888
932
  except Exception:
889
933
  exception(f"[inbound] loop crashed peer={peer} host_id={host_id or '?'}")
@@ -894,20 +938,19 @@ class HostTunnelServicer(host_tunnel_pb2_grpc.HostTunnelServicer):
894
938
  try:
895
939
  self.registry.drop_host(host_id)
896
940
  except Exception:
897
- exception(f"[inbound] registry.drop_host failed host_id={host_id}")
941
+ exception(f"[inbound] registry.drop_host failed host_id={host_id!r}")
898
942
  if session is not None:
899
943
  try:
900
944
  session.close()
901
945
  except Exception:
902
- exception(f"[inbound] session.close failed host_id={host_id}")
946
+ exception(f"[inbound] session.close failed host_id={host_id!r}")
903
947
 
904
- t = threading.Thread(target=inbound_loop, daemon=True, name=f"inbound_{peer}")
948
+ t = threading.Thread(target=inbound_loop, daemon=True, name=f"inbound_{peer_tag}")
905
949
  t.start()
906
950
 
907
951
  info(f"[Connect] outbound pump starting peer={peer}")
908
952
 
909
- # outbound pump
910
- saw_session = False
953
+ printed_waiting = False
911
954
  while not stop.is_set():
912
955
  if fatal:
913
956
  code = fatal.get("code", "")
@@ -918,9 +961,9 @@ class HostTunnelServicer(host_tunnel_pb2_grpc.HostTunnelServicer):
918
961
  context.abort(grpc.StatusCode.UNKNOWN, details)
919
962
 
920
963
  if session is None:
921
- if not saw_session:
964
+ if not printed_waiting:
922
965
  info(f"[Connect] waiting for HELLO peer={peer}")
923
- saw_session = True
966
+ printed_waiting = True
924
967
  time.sleep(0.05)
925
968
  continue
926
969
 
@@ -937,12 +980,16 @@ class HostTunnelServicer(host_tunnel_pb2_grpc.HostTunnelServicer):
937
980
  break
938
981
 
939
982
  out_type = out.WhichOneof("msg")
940
- info(f"[Connect] OUT frame type={out_type} peer={peer} host_id={host_id or '?'}")
983
+ info(f"[Connect] OUT type={out_type} peer={peer} host_id={host_id or '?'}")
941
984
  yield out
942
985
 
943
986
  info(f"[Connect] stream ended peer={peer} host_id={host_id or '?'}")
944
987
 
945
988
 
989
+ # =============================================================================
990
+ # Credentials + server start
991
+ # =============================================================================
992
+
946
993
  def build_server_credentials(
947
994
  *,
948
995
  private_key_pem: bytes,
@@ -979,7 +1026,9 @@ def start_host_tunnel_server(
979
1026
  ("grpc.keepalive_permit_without_calls", 1),
980
1027
  ]
981
1028
 
982
- info(f"[server] creating grpc.server max_workers={max_workers} bind={host}:{int(port)}")
1029
+ bind_addr = f"{host}:{int(port)}"
1030
+ info(f"[server] creating grpc.server max_workers={max_workers} bind={bind_addr}")
1031
+
983
1032
  server = grpc.server(
984
1033
  futures.ThreadPoolExecutor(max_workers=max_workers),
985
1034
  options=options,
@@ -987,7 +1036,6 @@ def start_host_tunnel_server(
987
1036
 
988
1037
  host_tunnel_pb2_grpc.add_HostTunnelServicer_to_server(HostTunnelServicer(reg), server)
989
1038
 
990
- bind_addr = f"{host}:{int(port)}"
991
1039
  try:
992
1040
  added = server.add_secure_port(bind_addr, server_credentials)
993
1041
  info(f"[server] add_secure_port addr={bind_addr} -> {added}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remoteRF-server
3
- Version: 0.1.65
3
+ Version: 0.1.66
4
4
  Description-Content-Type: text/markdown
5
5
  Requires-Dist: grpcio
6
6
  Requires-Dist: protobuf