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.
- {remoterf_server-0.1.64 → remoterf_server-0.1.66}/PKG-INFO +1 -1
- {remoterf_server-0.1.64 → remoterf_server-0.1.66}/pyproject.toml +1 -1
- {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/host/host_tunnel_server.py +299 -111
- {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server.egg-info/PKG-INFO +1 -1
- {remoterf_server-0.1.64 → remoterf_server-0.1.66}/README.md +0 -0
- {remoterf_server-0.1.64 → remoterf_server-0.1.66}/setup.cfg +0 -0
- {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/__init__.py +0 -0
- {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/common/__init__.py +0 -0
- {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/common/grpc/__init__.py +0 -0
- {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/common/grpc/grpc_host_pb2.py +0 -0
- {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/common/grpc/grpc_host_pb2_grpc.py +0 -0
- {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/common/grpc/grpc_pb2.py +0 -0
- {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/common/grpc/grpc_pb2_grpc.py +0 -0
- {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/common/utils/__init__.py +0 -0
- {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/common/utils/ansi_codes.py +0 -0
- {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/common/utils/api_token.py +0 -0
- {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/common/utils/db_connection.py +0 -0
- {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/common/utils/db_location.py +0 -0
- {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/common/utils/list_string.py +0 -0
- {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/common/utils/process_arg.py +0 -0
- {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/drivers/__init__.py +0 -0
- {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/drivers/adalm_pluto/__init__.py +0 -0
- {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/drivers/adalm_pluto/pluto_remote_server.py +0 -0
- {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/host/__init__.py +0 -0
- {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/host/host_directory_store.py +0 -0
- {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/server/__init__.py +0 -0
- {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/server/acc_perms.py +0 -0
- {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/server/cert_provider.py +0 -0
- {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/server/device_manager.py +0 -0
- {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/server/grpc_server.py +0 -0
- {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/server/reservation.py +0 -0
- {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/server/rpc_manager.py +0 -0
- {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/server/user_group_cli.py +0 -0
- {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/server/user_group_handler.py +0 -0
- {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/serverrf_cli.py +0 -0
- {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/tools/__init__.py +0 -0
- {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/tools/gen_certs.py +0 -0
- {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server.egg-info/SOURCES.txt +0 -0
- {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server.egg-info/dependency_links.txt +0 -0
- {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server.egg-info/entry_points.txt +0 -0
- {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server.egg-info/requires.txt +0 -0
- {remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server.egg-info/top_level.txt +0 -0
{remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/host/host_tunnel_server.py
RENAMED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
#
|
|
141
|
-
self._routes: Dict[str, DeviceRoute] = {}
|
|
142
|
-
self._host_devices: Dict[str, Set[str]] = {}
|
|
143
|
-
self._device_infos: Dict[str, host_tunnel_pb2.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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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=
|
|
426
|
-
serial=
|
|
427
|
-
kind=
|
|
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
|
-
|
|
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
|
-
|
|
562
|
+
info_obj = tmp
|
|
489
563
|
except Exception:
|
|
490
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
831
|
+
exception("[inbound] FAILED sending MetaRequest")
|
|
707
832
|
|
|
708
|
-
#
|
|
833
|
+
# heartbeat ack
|
|
709
834
|
try:
|
|
710
|
-
session.send(
|
|
711
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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
|
-
|
|
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
|
-
|
|
783
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
867
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/common/grpc/__init__.py
RENAMED
|
File without changes
|
{remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/common/grpc/grpc_host_pb2.py
RENAMED
|
File without changes
|
|
File without changes
|
{remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/common/grpc/grpc_pb2.py
RENAMED
|
File without changes
|
{remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/common/grpc/grpc_pb2_grpc.py
RENAMED
|
File without changes
|
{remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/common/utils/__init__.py
RENAMED
|
File without changes
|
{remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/common/utils/ansi_codes.py
RENAMED
|
File without changes
|
{remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/common/utils/api_token.py
RENAMED
|
File without changes
|
{remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/common/utils/db_connection.py
RENAMED
|
File without changes
|
{remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/common/utils/db_location.py
RENAMED
|
File without changes
|
{remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/common/utils/list_string.py
RENAMED
|
File without changes
|
{remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/common/utils/process_arg.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/host/host_directory_store.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/server/cert_provider.py
RENAMED
|
File without changes
|
{remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/server/device_manager.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/server/user_group_cli.py
RENAMED
|
File without changes
|
{remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server/server/user_group_handler.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
{remoterf_server-0.1.64 → remoterf_server-0.1.66}/src/remoteRF_server.egg-info/top_level.txt
RENAMED
|
File without changes
|