remoteRF-server 0.1.72__tar.gz → 0.1.74__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. {remoterf_server-0.1.72 → remoterf_server-0.1.74}/PKG-INFO +4 -2
  2. {remoterf_server-0.1.72 → remoterf_server-0.1.74}/README.md +3 -1
  3. {remoterf_server-0.1.72 → remoterf_server-0.1.74}/pyproject.toml +1 -1
  4. {remoterf_server-0.1.72 → remoterf_server-0.1.74}/src/remoteRF_server/host/host_tunnel_server.py +65 -4
  5. {remoterf_server-0.1.72 → remoterf_server-0.1.74}/src/remoteRF_server/server/grpc_server.py +188 -1
  6. {remoterf_server-0.1.72 → remoterf_server-0.1.74}/src/remoteRF_server.egg-info/PKG-INFO +4 -2
  7. {remoterf_server-0.1.72 → remoterf_server-0.1.74}/setup.cfg +0 -0
  8. {remoterf_server-0.1.72 → remoterf_server-0.1.74}/src/remoteRF_server/__init__.py +0 -0
  9. {remoterf_server-0.1.72 → remoterf_server-0.1.74}/src/remoteRF_server/common/__init__.py +0 -0
  10. {remoterf_server-0.1.72 → remoterf_server-0.1.74}/src/remoteRF_server/common/grpc/__init__.py +0 -0
  11. {remoterf_server-0.1.72 → remoterf_server-0.1.74}/src/remoteRF_server/common/grpc/grpc_host_pb2.py +0 -0
  12. {remoterf_server-0.1.72 → remoterf_server-0.1.74}/src/remoteRF_server/common/grpc/grpc_host_pb2_grpc.py +0 -0
  13. {remoterf_server-0.1.72 → remoterf_server-0.1.74}/src/remoteRF_server/common/grpc/grpc_pb2.py +0 -0
  14. {remoterf_server-0.1.72 → remoterf_server-0.1.74}/src/remoteRF_server/common/grpc/grpc_pb2_grpc.py +0 -0
  15. {remoterf_server-0.1.72 → remoterf_server-0.1.74}/src/remoteRF_server/common/utils/__init__.py +0 -0
  16. {remoterf_server-0.1.72 → remoterf_server-0.1.74}/src/remoteRF_server/common/utils/ansi_codes.py +0 -0
  17. {remoterf_server-0.1.72 → remoterf_server-0.1.74}/src/remoteRF_server/common/utils/api_token.py +0 -0
  18. {remoterf_server-0.1.72 → remoterf_server-0.1.74}/src/remoteRF_server/common/utils/db_connection.py +0 -0
  19. {remoterf_server-0.1.72 → remoterf_server-0.1.74}/src/remoteRF_server/common/utils/db_location.py +0 -0
  20. {remoterf_server-0.1.72 → remoterf_server-0.1.74}/src/remoteRF_server/common/utils/list_string.py +0 -0
  21. {remoterf_server-0.1.72 → remoterf_server-0.1.74}/src/remoteRF_server/common/utils/process_arg.py +0 -0
  22. {remoterf_server-0.1.72 → remoterf_server-0.1.74}/src/remoteRF_server/drivers/__init__.py +0 -0
  23. {remoterf_server-0.1.72 → remoterf_server-0.1.74}/src/remoteRF_server/drivers/adalm_pluto/__init__.py +0 -0
  24. {remoterf_server-0.1.72 → remoterf_server-0.1.74}/src/remoteRF_server/drivers/adalm_pluto/pluto_remote_server.py +0 -0
  25. {remoterf_server-0.1.72 → remoterf_server-0.1.74}/src/remoteRF_server/host/__init__.py +0 -0
  26. {remoterf_server-0.1.72 → remoterf_server-0.1.74}/src/remoteRF_server/host/host_directory_store.py +0 -0
  27. {remoterf_server-0.1.72 → remoterf_server-0.1.74}/src/remoteRF_server/server/__init__.py +0 -0
  28. {remoterf_server-0.1.72 → remoterf_server-0.1.74}/src/remoteRF_server/server/acc_perms.py +0 -0
  29. {remoterf_server-0.1.72 → remoterf_server-0.1.74}/src/remoteRF_server/server/cert_provider.py +0 -0
  30. {remoterf_server-0.1.72 → remoterf_server-0.1.74}/src/remoteRF_server/server/device_manager.py +0 -0
  31. {remoterf_server-0.1.72 → remoterf_server-0.1.74}/src/remoteRF_server/server/reservation.py +0 -0
  32. {remoterf_server-0.1.72 → remoterf_server-0.1.74}/src/remoteRF_server/server/rpc_manager.py +0 -0
  33. {remoterf_server-0.1.72 → remoterf_server-0.1.74}/src/remoteRF_server/server/user_group_cli.py +0 -0
  34. {remoterf_server-0.1.72 → remoterf_server-0.1.74}/src/remoteRF_server/server/user_group_handler.py +0 -0
  35. {remoterf_server-0.1.72 → remoterf_server-0.1.74}/src/remoteRF_server/serverrf_cli.py +0 -0
  36. {remoterf_server-0.1.72 → remoterf_server-0.1.74}/src/remoteRF_server/tools/__init__.py +0 -0
  37. {remoterf_server-0.1.72 → remoterf_server-0.1.74}/src/remoteRF_server/tools/gen_certs.py +0 -0
  38. {remoterf_server-0.1.72 → remoterf_server-0.1.74}/src/remoteRF_server.egg-info/SOURCES.txt +0 -0
  39. {remoterf_server-0.1.72 → remoterf_server-0.1.74}/src/remoteRF_server.egg-info/dependency_links.txt +0 -0
  40. {remoterf_server-0.1.72 → remoterf_server-0.1.74}/src/remoteRF_server.egg-info/entry_points.txt +0 -0
  41. {remoterf_server-0.1.72 → remoterf_server-0.1.74}/src/remoteRF_server.egg-info/requires.txt +0 -0
  42. {remoterf_server-0.1.72 → remoterf_server-0.1.74}/src/remoteRF_server.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remoteRF-server
3
- Version: 0.1.72
3
+ Version: 0.1.74
4
4
  Description-Content-Type: text/markdown
5
5
  Requires-Dist: grpcio
6
6
  Requires-Dist: protobuf
@@ -346,7 +346,9 @@ todo:
346
346
  <!-- Testing functionality (disregarding Host pipeline) -->
347
347
  <!-- Clean up serverrf CLI + basic documentation for all commands, usergroups, export codes by CSV. -->
348
348
 
349
- HostRF meta data -> push upon connection + retry connection on host side.
349
+ <!-- HostRF meta data -> push upon connection + retry connection on host side. -->
350
+
351
+ Host ID (NEEDS TO BE UNIQUE AND TELL THE HOST THAT) (or if device also does it as well.)
350
352
 
351
353
  ServerRF display meta data as part of devices list / reservation + online/activity status (appended into device name) + (plus host side warning if the device id is NOT globally unique!)
352
354
 
@@ -335,7 +335,9 @@ todo:
335
335
  <!-- Testing functionality (disregarding Host pipeline) -->
336
336
  <!-- Clean up serverrf CLI + basic documentation for all commands, usergroups, export codes by CSV. -->
337
337
 
338
- HostRF meta data -> push upon connection + retry connection on host side.
338
+ <!-- HostRF meta data -> push upon connection + retry connection on host side. -->
339
+
340
+ Host ID (NEEDS TO BE UNIQUE AND TELL THE HOST THAT) (or if device also does it as well.)
339
341
 
340
342
  ServerRF display meta data as part of devices list / reservation + online/activity status (appended into device name) + (plus host side warning if the device id is NOT globally unique!)
341
343
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "remoteRF-server"
7
- version = "0.1.72"
7
+ version = "0.1.74"
8
8
  dependencies = [
9
9
  "grpcio",
10
10
  "protobuf",
@@ -51,6 +51,31 @@ LOG_MODE = "OFF" # <-- change this
51
51
 
52
52
  _LOG_LOCK = threading.Lock()
53
53
 
54
+ HOST_ID_RE = re.compile(r"^[A-Za-z0-9_.-]{1,64}$") # adjust if you want
55
+
56
+ class HostIdConflictError(RuntimeError):
57
+ def __init__(self, host_id: str, *, age_ms: int, replace_after_ms: int) -> None:
58
+ super().__init__(
59
+ f"host_id '{host_id}' already in use (last heartbeat {age_ms}ms ago). "
60
+ f"Try again after {replace_after_ms}ms or fix the conflicting host config."
61
+ )
62
+ self.host_id = host_id
63
+ self.age_ms = age_ms
64
+ self.replace_after_ms = replace_after_ms
65
+
66
+ def _validate_host_id(raw: str) -> str:
67
+ hid = (raw or "").strip()
68
+ if not hid:
69
+ raise ValueError("Missing host_id (set HOST_ID in host.env; do not auto-generate).")
70
+ if hid == "unknown-host":
71
+ raise ValueError("host_id cannot be 'unknown-host' (set HOST_ID in host.env).")
72
+ if not HOST_ID_RE.fullmatch(hid):
73
+ raise ValueError(
74
+ "Invalid host_id. Allowed: 1..64 chars of [A-Za-z0-9_.-]. "
75
+ f"Got: {hid!r}"
76
+ )
77
+ return hid
78
+
54
79
  def _ts() -> str:
55
80
  t = time.time()
56
81
  sec = int(t)
@@ -387,12 +412,25 @@ class HostTunnelRegistry:
387
412
  # directory/status mutators
388
413
  # ---------------------------
389
414
 
390
- def register_host(self, host_id: str, session: HostSession) -> None:
415
+ def register_host(
416
+ self,
417
+ host_id: str,
418
+ session: HostSession,
419
+ *,
420
+ replace_if_stale_ms: int = 15_000, # tweak: how long without heartbeat before we allow replacement
421
+ ) -> None:
391
422
  info(f"[registry] register_host: host_id={host_id!r}")
423
+
392
424
  with self._lock:
393
425
  old = self._hosts.get(host_id)
394
- if old is not None:
395
- warn(f"[registry] register_host: replacing existing session host_id={host_id!r}")
426
+ if old is not None and old.alive:
427
+ age_ms = int(now_ms() - int(getattr(old, "last_heartbeat_ms", 0) or 0))
428
+ if age_ms < replace_if_stale_ms:
429
+ # conflict: someone else is already using this host_id
430
+ raise HostIdConflictError(host_id, age_ms=age_ms, replace_after_ms=replace_if_stale_ms)
431
+
432
+ # stale: allow replacement
433
+ warn(f"[registry] register_host: replacing STALE session host_id={host_id!r} age_ms={age_ms}")
396
434
  old.close()
397
435
 
398
436
  self._hosts[host_id] = session
@@ -823,7 +861,28 @@ class HostTunnelServicer(host_tunnel_pb2_grpc.HostTunnelServicer):
823
861
 
824
862
  if which == "hello":
825
863
  hello = frame.hello
826
- host_id = str(getattr(hello, "host_id", "")).strip() or "unknown-host"
864
+ raw = str(getattr(hello, "host_id", "") or "")
865
+ try:
866
+ host_id = _validate_host_id(raw)
867
+ except ValueError as e:
868
+ error(f"[inbound] invalid hello.host_id peer={peer} err={e}")
869
+ fatal["code"] = "INVALID_ARGUMENT"
870
+ fatal["details"] = str(e)
871
+ stop.set()
872
+ return
873
+
874
+ ver = str(getattr(hello, "version", "") or "")
875
+ info(f"[inbound] HELLO host_id={host_id!r} version={ver!r} peer={peer}")
876
+
877
+ session = HostSession(host_id)
878
+ try:
879
+ self.registry.register_host(host_id, session, replace_if_stale_ms=15_000)
880
+ except HostIdConflictError as e:
881
+ error(f"[inbound] HostIdConflictError host_id={host_id!r} err={e}")
882
+ fatal["code"] = "ALREADY_EXISTS"
883
+ fatal["details"] = str(e)
884
+ stop.set()
885
+ return
827
886
  ver = str(getattr(hello, "version", "") or "")
828
887
  info(f"[inbound] HELLO host_id={host_id!r} version={ver!r} peer={peer}")
829
888
 
@@ -976,6 +1035,8 @@ class HostTunnelServicer(host_tunnel_pb2_grpc.HostTunnelServicer):
976
1035
  error(f"[Connect] aborting stream peer={peer} code={code} details={details}")
977
1036
  if code == "ALREADY_EXISTS":
978
1037
  context.abort(grpc.StatusCode.ALREADY_EXISTS, details)
1038
+ if code == "INVALID_ARGUMENT":
1039
+ context.abort(grpc.StatusCode.INVALID_ARGUMENT, details)
979
1040
  context.abort(grpc.StatusCode.UNKNOWN, details)
980
1041
 
981
1042
  if session is None:
@@ -445,6 +445,187 @@ def _cmd_devices_status() -> None:
445
445
  " online=", Sty.DEFAULT, f"{ds.online}", Sty.GREEN if ds.online else Sty.RED,
446
446
  " last_seen=", Sty.DEFAULT, f"{_fmt_ms_ago(ds.last_seen_ms)} ago", Sty.BLUE
447
447
  )
448
+
449
+ from typing import Iterable
450
+ import contextlib
451
+
452
+ def _repo_root_guess() -> Path:
453
+ """
454
+ Best-effort: find <repo>/src/... and return <repo>.
455
+ Fallback: walk up a few levels from this file.
456
+ """
457
+ p = Path(__file__).resolve()
458
+ for parent in p.parents:
459
+ if parent.name == "src":
460
+ return parent.parent
461
+ # fallback
462
+ return p.parents[3] if len(p.parents) >= 4 else p.parent
463
+
464
+ def _host_state_dirs() -> list[Path]:
465
+ """
466
+ Places host-directory persistence might live in your codebase:
467
+ - User config dir: ~/.config/remoterf (REMOTERF_CONFIG_DIR override)
468
+ - Repo-local: <repo>/.config
469
+ - Server config dir you referenced: <repo>/src/remoteRF_server/config (because of .env.host_directory)
470
+ """
471
+ cands = [
472
+ _cfg_dir(),
473
+ _repo_root_guess() / ".config",
474
+ (Path(__file__).resolve().parent.parent / "config"), # <repo>/src/remoteRF_server/config
475
+ ]
476
+
477
+ out: list[Path] = []
478
+ seen: set[str] = set()
479
+ for d in cands:
480
+ try:
481
+ rp = str(d.resolve())
482
+ except Exception:
483
+ rp = str(d)
484
+ if rp not in seen:
485
+ seen.add(rp)
486
+ out.append(d)
487
+ return out
488
+
489
+ def _host_state_files() -> list[Path]:
490
+ """
491
+ Exact files your HostTunnelRegistry has historically used.
492
+ Also includes a conservative glob for future additions.
493
+ """
494
+ files: list[Path] = []
495
+ for d in _host_state_dirs():
496
+ files.append(d / "host_directory.env")
497
+ files.append(d / "host_directory_meta.env")
498
+ # If you later add more persisted host-dir files, keep them discoverable.
499
+ try:
500
+ for p in sorted(d.glob("host_directory*.env")):
501
+ files.append(p)
502
+ except Exception:
503
+ pass
504
+
505
+ out: list[Path] = []
506
+ seen: set[str] = set()
507
+ for p in files:
508
+ try:
509
+ rp = str(p.resolve())
510
+ except Exception:
511
+ rp = str(p)
512
+ if rp not in seen:
513
+ seen.add(rp)
514
+ out.append(p)
515
+ return out
516
+
517
+ def _cmd_hosts_persisted() -> None:
518
+ files = _host_state_files()
519
+ found_any = False
520
+ printf("Host directory (persisted) state files:", Sty.BOLD)
521
+ for p in files:
522
+ if p.exists():
523
+ found_any = True
524
+ try:
525
+ size = p.stat().st_size
526
+ except Exception:
527
+ size = -1
528
+ printf(" - ", Sty.DEFAULT, f"{p}", Sty.MAGENTA, f" ({size} bytes)", Sty.GRAY)
529
+ else:
530
+ printf(" - ", Sty.DEFAULT, f"{p}", Sty.GRAY, " (missing)", Sty.GRAY)
531
+ if not found_any:
532
+ printf(" (none found)", Sty.GRAY)
533
+
534
+ def _try_clear_registry_in_memory(reg: object) -> list[str]:
535
+ """
536
+ Best-effort in-memory wipe without knowing exact internals.
537
+ Uses registry lock if present.
538
+ """
539
+ cleared: list[str] = []
540
+
541
+ lock = getattr(reg, "_lock", None)
542
+ acquired = False
543
+ try:
544
+ if lock is not None and hasattr(lock, "acquire") and hasattr(lock, "release"):
545
+ lock.acquire()
546
+ acquired = True
547
+
548
+ # common private dict fields
549
+ for attr in (
550
+ "_hosts",
551
+ "_devices",
552
+ "_host_by_id",
553
+ "_device_by_id",
554
+ "_device_routes",
555
+ "_routes",
556
+ "_sessions",
557
+ "_host_sessions",
558
+ ):
559
+ d = getattr(reg, attr, None)
560
+ if isinstance(d, dict):
561
+ d.clear()
562
+ cleared.append(attr)
563
+
564
+ # common private sets/lists
565
+ for attr in ("_connected", "_online_hosts"):
566
+ s = getattr(reg, attr, None)
567
+ if isinstance(s, set):
568
+ s.clear()
569
+ cleared.append(attr)
570
+
571
+ # if there is an explicit method, prefer it (won’t hurt if absent)
572
+ for meth in ("clear", "reset", "wipe", "wipe_all", "wipe_state"):
573
+ fn = getattr(reg, meth, None)
574
+ if callable(fn):
575
+ try:
576
+ fn()
577
+ cleared.append(f"{meth}()")
578
+ except Exception:
579
+ pass
580
+
581
+ finally:
582
+ if acquired:
583
+ try:
584
+ lock.release()
585
+ except Exception:
586
+ pass
587
+
588
+ return cleared
589
+
590
+ def _cmd_hosts_wipe(*, yes: bool = False) -> None:
591
+ # confirm
592
+ if not yes:
593
+ try:
594
+ resp = session.prompt(stylize("Type 'wipe-hosts' to confirm wiping persisted host state: ", Sty.RED))
595
+ except KeyboardInterrupt:
596
+ printf("Cancelled.", Sty.GRAY)
597
+ return
598
+ if (resp or "").strip().lower() != "wipe-hosts":
599
+ printf("Wipe aborted.", Sty.GRAY)
600
+ return
601
+
602
+ # delete persisted files
603
+ files = [p for p in _host_state_files() if p.exists()]
604
+ if not files:
605
+ printf("No persisted host directory files found to wipe.", Sty.GRAY)
606
+ else:
607
+ for p in files:
608
+ try:
609
+ p.unlink()
610
+ printf("Deleted: ", Sty.DEFAULT, f"{p}", Sty.MAGENTA)
611
+ except Exception as e:
612
+ printf("ERROR deleting: ", Sty.RED, f"{p} -> {e}", Sty.DEFAULT)
613
+ return
614
+
615
+ # clear in-memory registry (so you don’t have to restart for live state)
616
+ global TUNNEL_REGISTRY
617
+ reg = TUNNEL_REGISTRY
618
+ if reg is None:
619
+ printf("Host tunnel registry not initialized; persisted files wiped only.", Sty.GRAY)
620
+ else:
621
+ cleared = _try_clear_registry_in_memory(reg)
622
+ if cleared:
623
+ printf("Cleared in-memory registry fields: ", Sty.DEFAULT, ", ".join(cleared), Sty.GRAY)
624
+ else:
625
+ printf("WARNING: Could not confirm in-memory registry clear (unknown internals). Restart server if needed.", Sty.YELLOW)
626
+
627
+ printf("Host state wipe complete.", Sty.GREEN)
628
+ printf("NOTE: If hosts reconnect, persisted files will be recreated.", Sty.GRAY)
448
629
 
449
630
  # def help():
450
631
  # printf("/------- HELP -------/", Sty.BOLD)
@@ -571,6 +752,7 @@ def help() -> None:
571
752
 
572
753
  printf("Host Tunnel (live):", Sty.BOLD)
573
754
  printf(" hosts status ", Sty.MAGENTA, "- Live host online/last_seen/devices", Sty.DEFAULT)
755
+ printf(" hosts wipe ", Sty.MAGENTA, "- Wipe persisted host directory state (and clear in-memory)", Sty.DEFAULT)
574
756
  printf(" devices status ", Sty.MAGENTA, "- Live device online/last_seen/route", Sty.DEFAULT)
575
757
  print()
576
758
 
@@ -669,7 +851,6 @@ def server_input(inpu: str) -> bool:
669
851
  # ---- host tunnel ----
670
852
  "hosts status": lambda: _stay(_cmd_hosts_status),
671
853
  "host status": lambda: _stay(_cmd_hosts_status),
672
- "tunnel hosts": lambda: _stay(_cmd_hosts_status),
673
854
 
674
855
  "devices status": lambda: _stay(_cmd_devices_status),
675
856
  "tunnel devices": lambda: _stay(_cmd_devices_status),
@@ -679,6 +860,12 @@ def server_input(inpu: str) -> bool:
679
860
 
680
861
  fn = DISPATCH.get(cmd)
681
862
  if fn is None:
863
+ # allow: "hosts wipe" and "hosts wipe -y"
864
+ if cmd.startswith("hosts wipe"):
865
+ yes = ("-y" in cmd.split()) or ("--yes" in cmd.split())
866
+ _cmd_hosts_wipe(yes=yes)
867
+ return True
868
+
682
869
  printf("Unknown command: ", Sty.RED, f"{inpu}", Sty.DEFAULT)
683
870
  printf("Type ", Sty.DEFAULT, "help", Sty.MAGENTA, " to see commands.", Sty.DEFAULT)
684
871
  return True
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remoteRF-server
3
- Version: 0.1.72
3
+ Version: 0.1.74
4
4
  Description-Content-Type: text/markdown
5
5
  Requires-Dist: grpcio
6
6
  Requires-Dist: protobuf
@@ -346,7 +346,9 @@ todo:
346
346
  <!-- Testing functionality (disregarding Host pipeline) -->
347
347
  <!-- Clean up serverrf CLI + basic documentation for all commands, usergroups, export codes by CSV. -->
348
348
 
349
- HostRF meta data -> push upon connection + retry connection on host side.
349
+ <!-- HostRF meta data -> push upon connection + retry connection on host side. -->
350
+
351
+ Host ID (NEEDS TO BE UNIQUE AND TELL THE HOST THAT) (or if device also does it as well.)
350
352
 
351
353
  ServerRF display meta data as part of devices list / reservation + online/activity status (appended into device name) + (plus host side warning if the device id is NOT globally unique!)
352
354