remoteRF-server 0.1.65__tar.gz → 0.1.67__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.65 → remoterf_server-0.1.67}/PKG-INFO +1 -1
- {remoterf_server-0.1.65 → remoterf_server-0.1.67}/pyproject.toml +1 -1
- {remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server/host/host_tunnel_server.py +144 -96
- {remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server/server/grpc_server.py +95 -2
- {remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server.egg-info/PKG-INFO +1 -1
- {remoterf_server-0.1.65 → remoterf_server-0.1.67}/README.md +0 -0
- {remoterf_server-0.1.65 → remoterf_server-0.1.67}/setup.cfg +0 -0
- {remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server/__init__.py +0 -0
- {remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server/common/__init__.py +0 -0
- {remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server/common/grpc/__init__.py +0 -0
- {remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server/common/grpc/grpc_host_pb2.py +0 -0
- {remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server/common/grpc/grpc_host_pb2_grpc.py +0 -0
- {remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server/common/grpc/grpc_pb2.py +0 -0
- {remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server/common/grpc/grpc_pb2_grpc.py +0 -0
- {remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server/common/utils/__init__.py +0 -0
- {remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server/common/utils/ansi_codes.py +0 -0
- {remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server/common/utils/api_token.py +0 -0
- {remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server/common/utils/db_connection.py +0 -0
- {remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server/common/utils/db_location.py +0 -0
- {remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server/common/utils/list_string.py +0 -0
- {remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server/common/utils/process_arg.py +0 -0
- {remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server/drivers/__init__.py +0 -0
- {remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server/drivers/adalm_pluto/__init__.py +0 -0
- {remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server/drivers/adalm_pluto/pluto_remote_server.py +0 -0
- {remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server/host/__init__.py +0 -0
- {remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server/host/host_directory_store.py +0 -0
- {remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server/server/__init__.py +0 -0
- {remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server/server/acc_perms.py +0 -0
- {remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server/server/cert_provider.py +0 -0
- {remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server/server/device_manager.py +0 -0
- {remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server/server/reservation.py +0 -0
- {remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server/server/rpc_manager.py +0 -0
- {remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server/server/user_group_cli.py +0 -0
- {remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server/server/user_group_handler.py +0 -0
- {remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server/serverrf_cli.py +0 -0
- {remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server/tools/__init__.py +0 -0
- {remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server/tools/gen_certs.py +0 -0
- {remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server.egg-info/SOURCES.txt +0 -0
- {remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server.egg-info/dependency_links.txt +0 -0
- {remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server.egg-info/entry_points.txt +0 -0
- {remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server.egg-info/requires.txt +0 -0
- {remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server.egg-info/top_level.txt +0 -0
{remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server/host/host_tunnel_server.py
RENAMED
|
@@ -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 (
|
|
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
|
|
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
|
|
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] = {}
|
|
179
|
-
self._host_devices: Dict[str, Set[str]] = {}
|
|
180
|
-
self._device_infos: Dict[str, host_tunnel_pb2.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,
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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]
|
|
801
|
+
warn(f"[inbound] frame with no msg peer={peer}")
|
|
752
802
|
continue
|
|
753
803
|
|
|
754
|
-
info(f"[inbound]
|
|
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
|
-
#
|
|
833
|
+
# heartbeat ack
|
|
784
834
|
try:
|
|
785
|
-
ok = session.send(
|
|
786
|
-
|
|
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
|
|
842
|
+
warn(f"[inbound] device_announce before hello/session: ignoring peer={peer}")
|
|
797
843
|
continue
|
|
798
844
|
|
|
799
845
|
ann = frame.device_announce
|
|
800
|
-
|
|
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
|
|
873
|
+
# heartbeat ack after announce
|
|
824
874
|
try:
|
|
825
|
-
ok = session.send(
|
|
826
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
925
|
+
warn(f"[inbound] CANCEL received from host?? req_id={rid} peer={peer}")
|
|
882
926
|
except Exception:
|
|
883
|
-
exception("[inbound] cancel frame
|
|
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_{
|
|
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
|
-
|
|
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
|
|
964
|
+
if not printed_waiting:
|
|
922
965
|
info(f"[Connect] waiting for HELLO peer={peer}")
|
|
923
|
-
|
|
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
|
|
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
|
-
|
|
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}")
|
|
@@ -17,6 +17,8 @@ from datetime import datetime
|
|
|
17
17
|
# os.environ.setdefault("GRPC_VERBOSITY", "ERROR")
|
|
18
18
|
# os.environ.setdefault("GRPC_TRACE", "")
|
|
19
19
|
|
|
20
|
+
TUNNEL_REGISTRY: HostTunnelRegistry | None = None
|
|
21
|
+
|
|
20
22
|
import grpc
|
|
21
23
|
|
|
22
24
|
import traceback
|
|
@@ -201,10 +203,13 @@ def serve():
|
|
|
201
203
|
|
|
202
204
|
# --- attach HostTunnel to the SAME grpc server/port/creds ---
|
|
203
205
|
directory_env = Path(__file__).resolve().parent.parent / "config" / ".env.host_directory"
|
|
204
|
-
|
|
206
|
+
|
|
207
|
+
# --- attach HostTunnel to the SAME grpc server/port/creds ---
|
|
208
|
+
global TUNNEL_REGISTRY
|
|
209
|
+
TUNNEL_REGISTRY = HostTunnelRegistry()
|
|
205
210
|
|
|
206
211
|
host_tunnel_pb2_grpc.add_HostTunnelServicer_to_server(
|
|
207
|
-
HostTunnelServicer(
|
|
212
|
+
HostTunnelServicer(TUNNEL_REGISTRY),
|
|
208
213
|
server
|
|
209
214
|
)
|
|
210
215
|
|
|
@@ -366,6 +371,81 @@ def get_devices():
|
|
|
366
371
|
for key, value in data.items():
|
|
367
372
|
printf(f"Device ID:", Sty.DEFAULT, f' {key}', Sty.MAGENTA, f" Device Name: ", Sty.DEFAULT, f"{(value)}", Sty.GRAY)
|
|
368
373
|
|
|
374
|
+
def _fmt_ms_ago(ms: int) -> str:
|
|
375
|
+
if not ms:
|
|
376
|
+
return "-"
|
|
377
|
+
now = int(time.time() * 1000)
|
|
378
|
+
dt = max(0, now - int(ms))
|
|
379
|
+
if dt < 1000:
|
|
380
|
+
return f"{dt}ms"
|
|
381
|
+
if dt < 60_000:
|
|
382
|
+
return f"{dt/1000:.1f}s"
|
|
383
|
+
return f"{dt/60_000:.1f}m"
|
|
384
|
+
|
|
385
|
+
def _cmd_hosts_status() -> None:
|
|
386
|
+
global TUNNEL_REGISTRY
|
|
387
|
+
reg = TUNNEL_REGISTRY
|
|
388
|
+
if reg is None:
|
|
389
|
+
printf("Host tunnel registry not initialized yet.", Sty.RED)
|
|
390
|
+
return
|
|
391
|
+
|
|
392
|
+
hosts = reg.list_hosts() # host_id -> HostStatus
|
|
393
|
+
devices = reg.list_devices() # device_id -> DeviceStatus
|
|
394
|
+
|
|
395
|
+
# Build host -> device list
|
|
396
|
+
by_host: dict[str, list[str]] = {}
|
|
397
|
+
for did, ds in devices.items():
|
|
398
|
+
by_host.setdefault(ds.host_id or "-", []).append(did)
|
|
399
|
+
|
|
400
|
+
printf("Hosts (live):", Sty.BOLD)
|
|
401
|
+
if not hosts:
|
|
402
|
+
printf(" (none)", Sty.GRAY)
|
|
403
|
+
return
|
|
404
|
+
|
|
405
|
+
for hid, hs in sorted(hosts.items(), key=lambda kv: kv[0]):
|
|
406
|
+
dlist = sorted(by_host.get(hid, []))
|
|
407
|
+
printf(
|
|
408
|
+
" ", Sty.DEFAULT,
|
|
409
|
+
f"{hid}", Sty.MAGENTA,
|
|
410
|
+
" online=", Sty.DEFAULT, f"{hs.online}", Sty.GREEN if hs.online else Sty.RED,
|
|
411
|
+
" last_seen=", Sty.DEFAULT, f"{_fmt_ms_ago(hs.last_seen_ms)} ago", Sty.BLUE,
|
|
412
|
+
" devices=", Sty.DEFAULT, f"{len(dlist)}", Sty.GRAY
|
|
413
|
+
)
|
|
414
|
+
for did in dlist:
|
|
415
|
+
ds = devices.get(did)
|
|
416
|
+
if ds is None:
|
|
417
|
+
continue
|
|
418
|
+
printf(
|
|
419
|
+
" - ", Sty.DEFAULT,
|
|
420
|
+
f"{did}", Sty.MAGENTA,
|
|
421
|
+
" local_id=", Sty.DEFAULT, f"{ds.host_local_id}", Sty.GRAY,
|
|
422
|
+
" online=", Sty.DEFAULT, f"{ds.online}", Sty.GREEN if ds.online else Sty.RED,
|
|
423
|
+
" last_seen=", Sty.DEFAULT, f"{_fmt_ms_ago(ds.last_seen_ms)} ago", Sty.BLUE
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
def _cmd_devices_status() -> None:
|
|
427
|
+
global TUNNEL_REGISTRY
|
|
428
|
+
reg = TUNNEL_REGISTRY
|
|
429
|
+
if reg is None:
|
|
430
|
+
printf("Host tunnel registry not initialized yet.", Sty.RED)
|
|
431
|
+
return
|
|
432
|
+
|
|
433
|
+
devices = reg.list_devices()
|
|
434
|
+
printf("Devices (live):", Sty.BOLD)
|
|
435
|
+
if not devices:
|
|
436
|
+
printf(" (none)", Sty.GRAY)
|
|
437
|
+
return
|
|
438
|
+
|
|
439
|
+
for did, ds in sorted(devices.items(), key=lambda kv: kv[0]):
|
|
440
|
+
printf(
|
|
441
|
+
" ", Sty.DEFAULT,
|
|
442
|
+
f"{did}", Sty.MAGENTA,
|
|
443
|
+
" host=", Sty.DEFAULT, f"{ds.host_id or '-'}", Sty.GRAY,
|
|
444
|
+
" local_id=", Sty.DEFAULT, f"{ds.host_local_id}", Sty.GRAY,
|
|
445
|
+
" online=", Sty.DEFAULT, f"{ds.online}", Sty.GREEN if ds.online else Sty.RED,
|
|
446
|
+
" last_seen=", Sty.DEFAULT, f"{_fmt_ms_ago(ds.last_seen_ms)} ago", Sty.BLUE
|
|
447
|
+
)
|
|
448
|
+
|
|
369
449
|
# def help():
|
|
370
450
|
# printf("/------- HELP -------/", Sty.BOLD)
|
|
371
451
|
# printf("'exit' ", Sty.MAGENTA, " - Stop server.", Sty.DEFAULT)
|
|
@@ -488,6 +568,11 @@ def help() -> None:
|
|
|
488
568
|
printf(" codes delete ", Sty.MAGENTA, "- Delete an enrollment code (interactive)", Sty.DEFAULT)
|
|
489
569
|
printf(" codes csv ", Sty.MAGENTA, "- Export ALL enrollment codes as CSV to Downloads", Sty.DEFAULT)
|
|
490
570
|
print()
|
|
571
|
+
|
|
572
|
+
printf("Host Tunnel (live):", Sty.BOLD)
|
|
573
|
+
printf(" hosts status ", Sty.MAGENTA, "- Live host online/last_seen/devices", Sty.DEFAULT)
|
|
574
|
+
printf(" devices status ", Sty.MAGENTA, "- Live device online/last_seen/route", Sty.DEFAULT)
|
|
575
|
+
print()
|
|
491
576
|
|
|
492
577
|
printf("Database:", Sty.BOLD)
|
|
493
578
|
printf(" db purge ", Sty.MAGENTA, "- Remove all database entries", Sty.DEFAULT)
|
|
@@ -581,6 +666,14 @@ def server_input(inpu: str) -> bool:
|
|
|
581
666
|
"rm code": lambda: _stay(lambda: remove_enrollment_code_local(session)), # old
|
|
582
667
|
"rm c": lambda: _stay(lambda: remove_enrollment_code_local(session)), # old
|
|
583
668
|
|
|
669
|
+
# ---- host tunnel ----
|
|
670
|
+
"hosts status": lambda: _stay(_cmd_hosts_status),
|
|
671
|
+
"host status": lambda: _stay(_cmd_hosts_status),
|
|
672
|
+
"tunnel hosts": lambda: _stay(_cmd_hosts_status),
|
|
673
|
+
|
|
674
|
+
"devices status": lambda: _stay(_cmd_devices_status),
|
|
675
|
+
"tunnel devices": lambda: _stay(_cmd_devices_status),
|
|
676
|
+
|
|
584
677
|
"codes csv": lambda: _stay(export_enrollment_codes_csv_local),
|
|
585
678
|
}
|
|
586
679
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server/common/grpc/__init__.py
RENAMED
|
File without changes
|
{remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server/common/grpc/grpc_host_pb2.py
RENAMED
|
File without changes
|
|
File without changes
|
{remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server/common/grpc/grpc_pb2.py
RENAMED
|
File without changes
|
{remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server/common/grpc/grpc_pb2_grpc.py
RENAMED
|
File without changes
|
{remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server/common/utils/__init__.py
RENAMED
|
File without changes
|
{remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server/common/utils/ansi_codes.py
RENAMED
|
File without changes
|
{remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server/common/utils/api_token.py
RENAMED
|
File without changes
|
{remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server/common/utils/db_connection.py
RENAMED
|
File without changes
|
{remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server/common/utils/db_location.py
RENAMED
|
File without changes
|
{remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server/common/utils/list_string.py
RENAMED
|
File without changes
|
{remoterf_server-0.1.65 → remoterf_server-0.1.67}/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.65 → remoterf_server-0.1.67}/src/remoteRF_server/host/host_directory_store.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server/server/cert_provider.py
RENAMED
|
File without changes
|
{remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server/server/device_manager.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server/server/user_group_cli.py
RENAMED
|
File without changes
|
{remoterf_server-0.1.65 → remoterf_server-0.1.67}/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.65 → remoterf_server-0.1.67}/src/remoteRF_server.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
{remoterf_server-0.1.65 → remoterf_server-0.1.67}/src/remoteRF_server.egg-info/top_level.txt
RENAMED
|
File without changes
|