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