remoteRF-server 1.4.3__tar.gz → 1.4.4__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 (180) hide show
  1. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/PKG-INFO +1 -1
  2. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/pyproject.toml +1 -1
  3. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/server/device_manager.py +6 -3
  4. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/server/grpc_server.py +10 -3
  5. remoterf_server-1.4.4/src/remoteRF_server/server/startup_check.py +308 -0
  6. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server.egg-info/PKG-INFO +1 -1
  7. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server.egg-info/SOURCES.txt +2 -0
  8. remoterf_server-1.4.4/tests/test_startup_check.py +209 -0
  9. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/LICENSE +0 -0
  10. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/README.md +0 -0
  11. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/setup.cfg +0 -0
  12. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/__init__.py +0 -0
  13. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/common/__init__.py +0 -0
  14. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/common/grpc/__init__.py +0 -0
  15. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/common/grpc/deployment_capabilities.proto +0 -0
  16. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/common/grpc/deployment_capabilities_pb2.py +0 -0
  17. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/common/grpc/deployment_capabilities_pb2_grpc.py +0 -0
  18. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/common/grpc/federation.proto +0 -0
  19. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/common/grpc/federation_pb2.py +0 -0
  20. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/common/grpc/federation_pb2_grpc.py +0 -0
  21. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/common/grpc/grpc.proto +0 -0
  22. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/common/grpc/grpc_host.proto +0 -0
  23. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/common/grpc/grpc_host_pb2.py +0 -0
  24. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/common/grpc/grpc_host_pb2_grpc.py +0 -0
  25. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/common/grpc/grpc_pb2.py +0 -0
  26. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/common/grpc/grpc_pb2_grpc.py +0 -0
  27. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/common/grpc/v2_codec.py +0 -0
  28. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/common/idl/__init__.py +0 -0
  29. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/common/idl/device_schema.py +0 -0
  30. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/common/idl/pluto_mimo_schema.py +0 -0
  31. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/common/idl/pluto_schema.py +0 -0
  32. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/common/idl/schema.py +0 -0
  33. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/common/idl/v2.py +0 -0
  34. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/common/utils/__init__.py +0 -0
  35. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/common/utils/ansi_codes.py +0 -0
  36. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/common/utils/api_token.py +0 -0
  37. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/common/utils/db_connection.py +0 -0
  38. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/common/utils/db_location.py +0 -0
  39. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/common/utils/list_string.py +0 -0
  40. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/common/utils/panel.py +0 -0
  41. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/common/utils/process_arg.py +0 -0
  42. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/drivers/__init__.py +0 -0
  43. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/drivers/adalm_pluto/__init__.py +0 -0
  44. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/drivers/adalm_pluto/pluto_mimo_schema.py +0 -0
  45. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/drivers/adalm_pluto/pluto_remote_server.py +0 -0
  46. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/drivers/adalm_pluto/pluto_schema.py +0 -0
  47. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/drivers/adalm_pluto/poe.py +0 -0
  48. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/drivers/hackrf/__init__.py +0 -0
  49. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/drivers/hackrf/hackrf_schema.py +0 -0
  50. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/drivers/rtl_sdr/__init__.py +0 -0
  51. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/drivers/rtl_sdr/rtl_sdr_schema.py +0 -0
  52. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/drivers/ti_mmwave/__init__.py +0 -0
  53. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/drivers/ti_mmwave/ti_mmwave_schema.py +0 -0
  54. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/drivers/usrp/__init__.py +0 -0
  55. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/drivers/usrp/policy.py +0 -0
  56. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/drivers/usrp/profiles.py +0 -0
  57. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/drivers/usrp/uhd_manifest.py +0 -0
  58. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/drivers/usrp/usrp_schema.py +0 -0
  59. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/drivers/usrp_phaser/__init__.py +0 -0
  60. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/drivers/usrp_phaser/usrp_phaser_schema.py +0 -0
  61. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/host/__init__.py +0 -0
  62. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/host/host_auth_token.py +0 -0
  63. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/host/host_directory_store.py +0 -0
  64. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/host/host_tunnel_server.py +0 -0
  65. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/server/__init__.py +0 -0
  66. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/server/acc_perms.py +0 -0
  67. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/server/cert_provider.py +0 -0
  68. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/server/commands.py +0 -0
  69. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/server/console.py +0 -0
  70. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/server/control.py +0 -0
  71. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/server/deployment/__init__.py +0 -0
  72. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/server/deployment/capabilities.py +0 -0
  73. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/server/direct_path.py +0 -0
  74. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/server/dynamic_v2.py +0 -0
  75. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/server/dynamic_v2_bridge.py +0 -0
  76. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/server/federation/__init__.py +0 -0
  77. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/server/federation/admin.py +0 -0
  78. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/server/federation/cli.py +0 -0
  79. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/server/federation/cluster.py +0 -0
  80. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/server/federation/contracts.py +0 -0
  81. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/server/federation/destination.py +0 -0
  82. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/server/federation/devices.py +0 -0
  83. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/server/federation/enrollment.py +0 -0
  84. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/server/federation/global_home.py +0 -0
  85. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/server/federation/home.py +0 -0
  86. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/server/federation/home_devices.py +0 -0
  87. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/server/federation/identity.py +0 -0
  88. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/server/federation/legacy_state.py +0 -0
  89. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/server/federation/migrations.py +0 -0
  90. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/server/federation/peers.py +0 -0
  91. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/server/federation/permissions.py +0 -0
  92. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/server/federation/principals.py +0 -0
  93. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/server/federation/runtime.py +0 -0
  94. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/server/federation/trust.py +0 -0
  95. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/server/global_onboarding.py +0 -0
  96. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/server/host_dynamic_v2.py +0 -0
  97. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/server/reservation.py +0 -0
  98. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/server/rpc_manager.py +0 -0
  99. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/server/user_group_cli.py +0 -0
  100. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/server/user_group_handler.py +0 -0
  101. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/server/v2_errors.py +0 -0
  102. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/serverrf_cli.py +0 -0
  103. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/tools/__init__.py +0 -0
  104. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/tools/gen_certs.py +0 -0
  105. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/tools/gist_status.py +0 -0
  106. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/tools/gist_status_testing.py +0 -0
  107. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server/tools/introspect_uhd.py +0 -0
  108. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server.egg-info/dependency_links.txt +0 -0
  109. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server.egg-info/entry_points.txt +0 -0
  110. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server.egg-info/requires.txt +0 -0
  111. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/src/remoteRF_server.egg-info/top_level.txt +0 -0
  112. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_account_hardening.py +0 -0
  113. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_account_names.py +0 -0
  114. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_account_removal.py +0 -0
  115. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_activity_log.py +0 -0
  116. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_bind_host.py +0 -0
  117. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_cert_provider.py +0 -0
  118. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_client_sessions.py +0 -0
  119. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_cluster.py +0 -0
  120. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_commands.py +0 -0
  121. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_console.py +0 -0
  122. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_contrib_drivers.py +0 -0
  123. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_default_rtl_sdr_schema.py +0 -0
  124. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_default_usrp_schema.py +0 -0
  125. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_deployment_capabilities.py +0 -0
  126. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_detached_device_rpc.py +0 -0
  127. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_device_handles.py +0 -0
  128. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_device_init.py +0 -0
  129. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_devices_list.py +0 -0
  130. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_direct_fixed_port.py +0 -0
  131. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_direct_path_server.py +0 -0
  132. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_dynamic_v2.py +0 -0
  133. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_federated_direct_path.py +0 -0
  134. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_federated_dynamic_v2.py +0 -0
  135. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_federation_edges.py +0 -0
  136. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_first_run.py +0 -0
  137. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_gate_d_contracts.py +0 -0
  138. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_gate_e_federation.py +0 -0
  139. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_gate_e_proto.py +0 -0
  140. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_gate_f_enrollment.py +0 -0
  141. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_gate_g1_logging.py +0 -0
  142. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_gate_g_permissions.py +0 -0
  143. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_gate_h_devices.py +0 -0
  144. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_gate_i_direct_path.py +0 -0
  145. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_gen_certs.py +0 -0
  146. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_global_accounts_migration.py +0 -0
  147. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_global_home.py +0 -0
  148. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_global_onboarding.py +0 -0
  149. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_global_v1_disposal.py +0 -0
  150. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_hackrf_schema.py +0 -0
  151. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_home_device_plane.py +0 -0
  152. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_host_directory_store.py +0 -0
  153. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_host_dynamic_v2.py +0 -0
  154. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_idl_schema.py +0 -0
  155. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_panel.py +0 -0
  156. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_peer_transport.py +0 -0
  157. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_pluto_compat.py +0 -0
  158. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_pluto_poe.py +0 -0
  159. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_process_arg.py +0 -0
  160. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_reservation_handoff.py +0 -0
  161. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_reservation_policy_b.py +0 -0
  162. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_reservation_timestamps.py +0 -0
  163. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_rpc_manager_batch.py +0 -0
  164. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_rpc_manager_calln.py +0 -0
  165. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_rpc_manager_rx_n.py +0 -0
  166. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_rtl_sdr_schema.py +0 -0
  167. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_soak_federation.py +0 -0
  168. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_status_push.py +0 -0
  169. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_ti_mmwave_hardware.py +0 -0
  170. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_ti_mmwave_schema.py +0 -0
  171. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_user_group_cli.py +0 -0
  172. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_usrp_b205mini_uhd410_hardware.py +0 -0
  173. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_usrp_client_server_hardware.py +0 -0
  174. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_usrp_client_server_localhost.py +0 -0
  175. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_usrp_n210_client_server_hardware.py +0 -0
  176. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_usrp_preload.py +0 -0
  177. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_usrp_profiles.py +0 -0
  178. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_usrp_remoterf_rx_hardware.py +0 -0
  179. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_usrp_uhd410_hardware.py +0 -0
  180. {remoterf_server-1.4.3 → remoterf_server-1.4.4}/tests/test_wipe.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remoteRF-server
3
- Version: 1.4.3
3
+ Version: 1.4.4
4
4
  Summary: RemoteRF server-side control package
5
5
  License: GPL-3.0-or-later
6
6
  Classifier: Programming Language :: Python :: 3
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "remoteRF-server"
7
- version = "1.4.3"
7
+ version = "1.4.4"
8
8
  description = "RemoteRF server-side control package"
9
9
  license = { text = "GPL-3.0-or-later" }
10
10
  requires-python = ">=3.10"
@@ -1294,23 +1294,24 @@ def device_inventory_snapshot() -> Dict[int, Dict[str, object]]:
1294
1294
 
1295
1295
  # Optional: reload device definitions (atomic swap)
1296
1296
 
1297
- def reload_devices() -> None:
1297
+ def reload_devices(on_device=None) -> None:
1298
1298
  """
1299
1299
  Open the local devices in devices.yml: once at server startup
1300
1300
  (start_server_runtime), and again after `devices add/remove/rename/wipe`.
1301
1301
  Preserves existing salt/hash for devices that remain (same gid),
1302
1302
  and preserves host virtual devices + their auth state.
1303
+ on_device(gid, name) is called as each local device starts opening.
1303
1304
  """
1304
1305
  # The console and the control socket can both reload; two at once would
1305
1306
  # each make every device and leave one set open.
1306
1307
  with _reload_lock:
1307
- _reload_devices_locked()
1308
+ _reload_devices_locked(on_device)
1308
1309
 
1309
1310
 
1310
1311
  _reload_lock = threading.Lock()
1311
1312
 
1312
1313
 
1313
- def _reload_devices_locked() -> None:
1314
+ def _reload_devices_locked(on_device=None) -> None:
1314
1315
  # ponytail: reconnects every local device, not just the changed one;
1315
1316
  # diff against the current table if a reload mid-session ever hurts.
1316
1317
  records = _load_device_records()
@@ -1343,6 +1344,8 @@ def _reload_devices_locked() -> None:
1343
1344
  name = (rec.get("NAME") or f"device-{gid}").strip()
1344
1345
  ident = (rec.get("IDENT") or "").strip()
1345
1346
 
1347
+ if on_device is not None:
1348
+ on_device(int(gid), name)
1346
1349
  dev, ident2, dtype2 = _connect_from_record(rec)
1347
1350
 
1348
1351
  tmp_info[int(gid)] = name
@@ -405,8 +405,15 @@ def start_server_runtime(
405
405
  ca_path=cert_dir / "ca.crt",
406
406
  )
407
407
  # The one place local hardware is opened: importing device_manager does not.
408
- reload_devices()
409
- reservation_handler.update_devices()
408
+ # The startup check runs before server.start(): no call reaches a
409
+ # Pluto it is power-cycling.
410
+ def open_devices(on_device):
411
+ reload_devices(on_device=on_device)
412
+ reservation_handler.update_devices()
413
+
414
+ from . import startup_check
415
+
416
+ startup_check.run(open_devices)
410
417
  server.start()
411
418
  if direct_path is not None:
412
419
  direct_path.start_listener()
@@ -424,7 +431,7 @@ def start_server_runtime(
424
431
  start_usergroup_cleanup_loop(interval_sec=60)
425
432
  if publish_status:
426
433
  start_status_publisher()
427
- except Exception:
434
+ except BaseException: # Ctrl-C during the startup check too: close_local_devices powers Plutos down
428
435
  stop_event.set()
429
436
  server.stop(0)
430
437
  session_manager.close_all()
@@ -0,0 +1,308 @@
1
+ # Copyright (C) 2026 RemoteRF
2
+ #
3
+ # This program is free software: you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by
5
+ # the Free Software Foundation, either version 3 of the License, or
6
+ # (at your option) any later version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ # GNU General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU General Public License
14
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
15
+
16
+ """Startup device check: open every local device quietly, test each one, and
17
+ show one line per device plus a summary. Everything the drivers print,
18
+ C libraries writing straight to fd 1/2 included, goes to
19
+ <config>/logs/startup-<UTC>.log instead of the console."""
20
+
21
+ from __future__ import annotations
22
+
23
+ import ctypes
24
+ import os
25
+ import shutil
26
+ import sys
27
+ import threading
28
+ import time
29
+ import traceback
30
+ from concurrent.futures import Future, TimeoutError as FutureTimeout
31
+ from datetime import datetime, timezone
32
+ from pathlib import Path
33
+
34
+ from prompt_toolkit import print_formatted_text
35
+
36
+ from ..common.utils import Sty, stylize
37
+ from ..common.utils.ansi_codes import style
38
+ from ..common.utils.panel import supports_unicode
39
+ from ..drivers.adalm_pluto.poe import PoePluto
40
+ from . import device_manager as dm
41
+
42
+ DEADLINE_S = 120 # all device checks, counted from when opening finishes (opening is unbounded, as before)
43
+ POE_TIMEOUT_S = 90 # power off, OFF_S, boot (BOOT_TIMEOUT_S), IIO context
44
+ CHECK_TIMEOUT_S = 15
45
+ GPS_WAIT_S = 5
46
+ KEEP_LOGS = 10
47
+ MIN_FREE_BYTES = 16 << 20 # less than this: open the devices without capture
48
+ FRAMES = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
49
+
50
+
51
+ def enabled() -> bool:
52
+ return os.getenv("REMOTERF_STARTUP_TEST", "1").strip().lower() not in ("0", "false", "no", "off")
53
+
54
+
55
+ def _start(fn, *args) -> Future:
56
+ """fn(*args) on a daemon thread: one that never returns cannot hold up exit."""
57
+ fut: Future = Future()
58
+
59
+ def run():
60
+ try:
61
+ fut.set_result(fn(*args))
62
+ except BaseException as exc: # noqa: BLE001 - handed to the waiter
63
+ fut.set_exception(exc)
64
+
65
+ threading.Thread(target=run, daemon=True, name="remoterf-startup-check").start()
66
+ return fut
67
+
68
+
69
+ def _short(text: str, limit: int = 72) -> str:
70
+ text = " ".join(str(text).split())
71
+ return text if len(text) <= limit else text[: limit - 1] + "…"
72
+
73
+
74
+ def _log(fd: int, text: str) -> None:
75
+ try:
76
+ os.write(fd, text.encode())
77
+ except OSError: # a full disk must not fail opening or the check
78
+ pass
79
+
80
+
81
+ def _restore_power(st) -> None:
82
+ """A tested PoE Pluto goes back to standby unless a reservation holds it (pre-warm keeps it on)."""
83
+ with dm._state_lock:
84
+ reserved = bool(st.salt and st.hsh)
85
+ if not reserved:
86
+ st.dev.call_power_down()
87
+
88
+
89
+ def _check(st) -> None:
90
+ """Raise unless the device answers. Holds io_lock, so a call cannot reach it mid-check."""
91
+ with st.io_lock:
92
+ dev = st.dev
93
+ native = getattr(dev, "device", None)
94
+ if isinstance(native, PoePluto):
95
+ # Its own RLock across cycle, read and restore: a close() (quit) runs after the restore.
96
+ with native._lock:
97
+ try:
98
+ if native.power_state != "on": # already on: its reservation pre-warm booted it
99
+ native.power_cycle()
100
+ int(native.sample_rate)
101
+ finally:
102
+ try:
103
+ _restore_power(st)
104
+ except Exception as exc: # noqa: BLE001 - the check's own result matters more
105
+ print(f"could not return {st.name} to standby: {exc}")
106
+ elif st.dtype == "rtl_sdr":
107
+ dev.call_read_bytes(512)
108
+ elif st.dtype == "gps":
109
+ end = time.monotonic() + GPS_WAIT_S
110
+ while not dev.call_read_raw(1):
111
+ if time.monotonic() > end:
112
+ raise RuntimeError(f"no NMEA sentence in {GPS_WAIT_S} s")
113
+ time.sleep(0.2)
114
+ elif st.dtype == "bluetooth":
115
+ if not dev.get_powered():
116
+ raise RuntimeError("adapter is not powered")
117
+ # ponytail: USRPs and other drivers count as operational once they open;
118
+ # add a dtype branch here when one gets a cheap probe.
119
+
120
+
121
+ def _timeout_for(st) -> float:
122
+ return POE_TIMEOUT_S if isinstance(getattr(st.dev, "device", None), PoePluto) else CHECK_TIMEOUT_S
123
+
124
+
125
+ class _Display:
126
+ def __init__(self, out):
127
+ self.out = out
128
+ self.tty = out.isatty()
129
+ self.uni = supports_unicode(getattr(out, "encoding", None))
130
+ self.frame = 0
131
+
132
+ def _print(self, *pairs, end="\n") -> None:
133
+ if self.tty:
134
+ self.out.write("\r\x1b[2K")
135
+ print_formatted_text(stylize(*pairs), style=style, file=self.out, end=end)
136
+
137
+ def spin(self, text: str) -> None:
138
+ """Gray spinner line, redrawn in place; nothing at all off a TTY."""
139
+ if not self.tty:
140
+ return
141
+ frames = FRAMES if self.uni else "|/-\\"
142
+ self.frame += 1
143
+ self._print(f"{frames[self.frame % len(frames)]} {text}", Sty.GRAY, end="")
144
+
145
+ def clear(self) -> None:
146
+ if self.tty:
147
+ self.out.write("\r\x1b[2K")
148
+ self.out.flush()
149
+
150
+ def result(self, ok: bool, label: str, detail: str) -> None:
151
+ mark = ("✓" if ok else "✗") if self.uni else ("ok" if ok else "x")
152
+ self._print(mark, Sty.BRIGHT_GREEN if ok else Sty.BRIGHT_RED, f" {label} · {detail}", Sty.GRAY)
153
+
154
+ def summary(self, results, word: str) -> None:
155
+ n = len(results)
156
+ ok = sum(1 for r in results if r[1])
157
+ noun = "device" if n == 1 else "devices"
158
+ if ok == n:
159
+ self._print(f"All {n} {noun} {word}", Sty.BRIGHT_GREEN)
160
+ return
161
+ issues = ", ".join(r[0] for r in results if not r[1])
162
+ self._print(f"{ok} of {n} {noun} {word}", Sty.YELLOW if ok else Sty.BRIGHT_RED,
163
+ f" · issues: {issues}", Sty.GRAY)
164
+
165
+
166
+ def _wait(display: _Display, fut: Future, until: float, text):
167
+ """fut's result while a spinner shows `text` (a str or a callable); FutureTimeout once `until` passes."""
168
+ while True:
169
+ try:
170
+ return fut.result(timeout=max(0.0, min(0.1, until - time.monotonic())))
171
+ except FutureTimeout:
172
+ if time.monotonic() >= until:
173
+ raise
174
+ display.spin(text() if callable(text) else text)
175
+
176
+
177
+ def _new_log(cfg_dir: Path) -> Path:
178
+ logs = cfg_dir / "logs"
179
+ logs.mkdir(parents=True, exist_ok=True)
180
+ for old in sorted(logs.glob("startup-*.log"))[: -(KEEP_LOGS - 1)]:
181
+ old.unlink(missing_ok=True)
182
+ return logs / f"startup-{datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%SZ')}.log"
183
+
184
+
185
+ def _flush_all() -> None:
186
+ for stream in (sys.stdout, sys.stderr):
187
+ try:
188
+ stream.flush()
189
+ except Exception: # noqa: BLE001
190
+ pass
191
+ try:
192
+ ctypes.CDLL(None).fflush(None) # C stdio buffers (librtlsdr, UHD) before fd 1/2 move
193
+ except Exception: # noqa: BLE001
194
+ pass
195
+
196
+
197
+ def run(open_devices, *, cfg_dir: Path | None = None) -> None:
198
+ """open_devices(on_device) opens the hardware (device_manager.reload_devices);
199
+ its errors propagate as before. A failure of the check itself never does."""
200
+ log_fd = -1
201
+ try:
202
+ log_path = _new_log(Path(cfg_dir or dm._cfg_dir()))
203
+ if shutil.disk_usage(log_path.parent).free < MIN_FREE_BYTES:
204
+ raise OSError("low disk space")
205
+ log_fd = os.open(log_path, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o644)
206
+ os.write(log_fd, b"remoterf startup check\n")
207
+ except Exception as exc: # noqa: BLE001 - no log: open the devices the old way
208
+ if log_fd >= 0:
209
+ os.close(log_fd)
210
+ print(f"Warning: startup check log unavailable ({exc}); opening devices without it")
211
+ open_devices(None)
212
+ return
213
+
214
+ _flush_all()
215
+ saved = (os.dup(1), os.dup(2))
216
+ out = os.fdopen(os.dup(saved[0]), "w", encoding=getattr(sys.stdout, "encoding", None) or "utf-8",
217
+ errors="replace")
218
+ display = _Display(out)
219
+ results = None
220
+ try:
221
+ os.dup2(log_fd, 1)
222
+ os.dup2(log_fd, 2)
223
+ try:
224
+ results = _run_captured(open_devices, display, log_fd, log_path)
225
+ finally:
226
+ _flush_all()
227
+ os.dup2(saved[0], 1)
228
+ os.dup2(saved[1], 2)
229
+ display.clear()
230
+ if results:
231
+ display.summary(results, "operational" if enabled() else "opened")
232
+ elif results == []:
233
+ display._print("No local devices opened", Sty.YELLOW)
234
+ display._print(f"log: {log_path}", Sty.GRAY)
235
+ finally:
236
+ out.close()
237
+ for fd in (*saved, log_fd):
238
+ os.close(fd)
239
+
240
+
241
+ def _run_captured(open_devices, display: _Display, log_fd: int, log_path: Path):
242
+ marks: dict[int, int] = {} # gid -> where its open output starts in the log
243
+ breaks: list[int] = [] # where each device's "--- open" line starts
244
+ current = {"name": "devices"}
245
+
246
+ def on_device(gid: int, name: str) -> None:
247
+ _flush_all() # the previous device's buffered prints land in its own slice
248
+ current["name"] = name
249
+ breaks.append(os.fstat(log_fd).st_size)
250
+ _log(log_fd, f"\n--- [{gid}] {name} · open\n")
251
+ marks[gid] = os.fstat(log_fd).st_size
252
+
253
+ # Opening has no deadline: no device is served before it finishes, as before.
254
+ _wait(display, _start(open_devices, on_device), float("inf"), lambda: f"opening {current['name']}…")
255
+ _flush_all()
256
+ opened_end = os.fstat(log_fd).st_size
257
+
258
+ try:
259
+ with dm._state_lock:
260
+ states = sorted((gid, st) for gid, st in dm._devices.items() if st.origin == "local")
261
+ if not states:
262
+ return []
263
+ captured = log_path.read_bytes()
264
+
265
+ def open_error(gid: int) -> str:
266
+ """The last line its driver printed while opening it."""
267
+ start = marks.get(gid)
268
+ if start is None:
269
+ return "not opened"
270
+ end = min([b for b in breaks if b >= start] + [opened_end])
271
+ lines = [l for l in captured[start:end].decode("utf-8", "replace").splitlines() if l.strip()]
272
+ return _short(lines[-1]) if lines else "not found"
273
+
274
+ test = enabled()
275
+ checks = {}
276
+ launched = time.monotonic()
277
+ deadline = launched + DEADLINE_S
278
+ for gid, st in states:
279
+ if st.dev is not None and test:
280
+ _log(log_fd, f"\n--- [{gid}] {st.name} · test\n")
281
+ checks[gid] = _start(_check, st)
282
+
283
+ results = []
284
+ for gid, st in states:
285
+ label = f"[{gid}] {st.name}"
286
+ if st.dev is None:
287
+ ok, detail = False, open_error(gid)
288
+ elif gid not in checks:
289
+ ok, detail = True, "operational" if test else "opened"
290
+ else:
291
+ until = min(launched + _timeout_for(st), deadline)
292
+ try:
293
+ _wait(display, checks[gid], until, f"{label} · testing…")
294
+ ok, detail = True, "operational"
295
+ except FutureTimeout:
296
+ ok, detail = False, f"no answer after {int(time.monotonic() - launched)} s"
297
+ except Exception as exc: # noqa: BLE001 - the device's failure, shown on its line
298
+ ok, detail = False, _short(exc) or type(exc).__name__
299
+ _log(log_fd, "".join(traceback.format_exception(type(exc), exc, exc.__traceback__)))
300
+ _flush_all()
301
+ display.result(ok, label, detail)
302
+ results.append((st.name, ok))
303
+ _log(log_fd, f"[{gid}] {st.name}: {'ok' if ok else 'ISSUE'} {detail}\n")
304
+ return results
305
+ except Exception as exc: # noqa: BLE001 - never stops the server
306
+ _log(log_fd, f"startup check failed: {type(exc).__name__}: {exc}\n")
307
+ display.result(False, "startup check", _short(f"failed: {exc}"))
308
+ return None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remoteRF-server
3
- Version: 1.4.3
3
+ Version: 1.4.4
4
4
  Summary: RemoteRF server-side control package
5
5
  License: GPL-3.0-or-later
6
6
  Classifier: Programming Language :: Python :: 3
@@ -76,6 +76,7 @@ src/remoteRF_server/server/grpc_server.py
76
76
  src/remoteRF_server/server/host_dynamic_v2.py
77
77
  src/remoteRF_server/server/reservation.py
78
78
  src/remoteRF_server/server/rpc_manager.py
79
+ src/remoteRF_server/server/startup_check.py
79
80
  src/remoteRF_server/server/user_group_cli.py
80
81
  src/remoteRF_server/server/user_group_handler.py
81
82
  src/remoteRF_server/server/v2_errors.py
@@ -161,6 +162,7 @@ tests/test_rpc_manager_calln.py
161
162
  tests/test_rpc_manager_rx_n.py
162
163
  tests/test_rtl_sdr_schema.py
163
164
  tests/test_soak_federation.py
165
+ tests/test_startup_check.py
164
166
  tests/test_status_push.py
165
167
  tests/test_ti_mmwave_hardware.py
166
168
  tests/test_ti_mmwave_schema.py
@@ -0,0 +1,209 @@
1
+ from __future__ import annotations
2
+
3
+ import ctypes
4
+ import io
5
+ import os
6
+ import pty
7
+ import sys
8
+ import threading
9
+ import time
10
+ import types
11
+ from unittest.mock import MagicMock
12
+
13
+ import pytest
14
+
15
+ from remoteRF_server.drivers.adalm_pluto.poe import PoePluto
16
+ from remoteRF_server.server import device_manager as dm
17
+ from remoteRF_server.server import startup_check as sc
18
+
19
+
20
+ def state(name, dev, dtype="custom", reserved=False):
21
+ return dm._DeviceState(dev=dev, origin="local", online=dev is not None,
22
+ salt="s" if reserved else "", hsh="h" if reserved else "",
23
+ name=name, ident="", dtype=dtype)
24
+
25
+
26
+ class Rtl:
27
+ def __init__(self, block=None, delay=0):
28
+ self.block = block
29
+ self.delay = delay
30
+ self.reads = []
31
+
32
+ def call_read_bytes(self, n):
33
+ self.reads.append(n)
34
+ time.sleep(self.delay)
35
+ if self.block is not None:
36
+ self.block.wait()
37
+
38
+
39
+ def opener(devices, noise=None):
40
+ def open_devices(on_device):
41
+ for gid, st in devices.items():
42
+ on_device(gid, st.name)
43
+ for line in (noise or {}).get(gid, []):
44
+ os.write(2 if "error" in line else 1, line.encode() + b"\n")
45
+ dm._devices[gid] = st
46
+ return open_devices
47
+
48
+
49
+ @pytest.fixture(autouse=True)
50
+ def clean(monkeypatch):
51
+ monkeypatch.setattr(dm, "_devices", {})
52
+ monkeypatch.delenv("REMOTERF_STARTUP_TEST", raising=False)
53
+
54
+
55
+ def test_quiet_lines_summary_and_log(tmp_path, capfd):
56
+ devices = {100: state("Pluto 00", object()), 101: state("RTL 01", None, "rtl_sdr"),
57
+ 102: state("RTL 02", Rtl(), "rtl_sdr")}
58
+
59
+ def open_devices(on_device):
60
+ opener(devices, {101: ["Found Rafael Micro R828D tuner", "usb_open error -3"]})(on_device)
61
+ ctypes.CDLL(None).printf(b"RTL-SDR Blog V4 Detected\n") # C stdio, buffered
62
+
63
+ sc.run(open_devices, cfg_dir=tmp_path)
64
+ out = capfd.readouterr().out
65
+ assert out.splitlines() == [
66
+ "✓ [100] Pluto 00 · operational",
67
+ "✗ [101] RTL 01 · usb_open error -3",
68
+ "✓ [102] RTL 02 · operational",
69
+ "2 of 3 devices operational · issues: RTL 01",
70
+ f"log: {next((tmp_path / 'logs').glob('startup-*.log'))}",
71
+ ]
72
+ assert devices[102].dev.reads == [512]
73
+ log = next((tmp_path / "logs").glob("startup-*.log")).read_text()
74
+ for text in ("Found Rafael Micro", "RTL-SDR Blog V4 Detected", "[101] RTL 01: ISSUE usb_open error -3"):
75
+ assert text in log
76
+
77
+
78
+ def test_all_operational_and_test_off(tmp_path, capfd, monkeypatch):
79
+ rtl = Rtl()
80
+ sc.run(opener({7: state("RTL", rtl, "rtl_sdr")}), cfg_dir=tmp_path)
81
+ assert capfd.readouterr().out.splitlines()[:2] == ["✓ [7] RTL · operational", "All 1 device operational"]
82
+
83
+ monkeypatch.setenv("REMOTERF_STARTUP_TEST", "0")
84
+ sc.run(opener({7: state("RTL", rtl, "rtl_sdr")}), cfg_dir=tmp_path)
85
+ assert capfd.readouterr().out.splitlines()[:2] == ["✓ [7] RTL · opened", "All 1 device opened"]
86
+ assert rtl.reads == [512] # the second run did not test it
87
+
88
+
89
+ def test_timeout_and_deadline(tmp_path, capfd, monkeypatch):
90
+ block = threading.Event()
91
+ monkeypatch.setattr(sc, "CHECK_TIMEOUT_S", 0.3)
92
+ t = time.monotonic()
93
+ sc.run(opener({1: state("Hung", Rtl(block), "rtl_sdr")}), cfg_dir=tmp_path)
94
+ assert time.monotonic() - t < 2
95
+ out = capfd.readouterr().out
96
+ assert "✗ [1] Hung · no answer after 0 s" in out
97
+ assert "\x1b" not in out and not set(sc.FRAMES) & set(out) # no spinner off a TTY
98
+
99
+ monkeypatch.setattr(sc, "CHECK_TIMEOUT_S", 30)
100
+ monkeypatch.setattr(sc, "DEADLINE_S", 0.4)
101
+ t = time.monotonic()
102
+ sc.run(opener({1: state("A", Rtl(block), "rtl_sdr"), 2: state("B", Rtl(block), "rtl_sdr")}), cfg_dir=tmp_path)
103
+ assert time.monotonic() - t < 2
104
+ assert capfd.readouterr().out.count("no answer after") == 2
105
+ block.set()
106
+
107
+
108
+ def test_log_unavailable_still_opens_devices(tmp_path, monkeypatch):
109
+ (tmp_path / "logs").write_text("not a directory")
110
+ calls = []
111
+ sc.run(calls.append, cfg_dir=tmp_path)
112
+ assert calls == [None]
113
+
114
+ (tmp_path / "logs").unlink()
115
+ monkeypatch.setattr(sc.shutil, "disk_usage", lambda p: types.SimpleNamespace(free=0))
116
+ sc.run(calls.append, cfg_dir=tmp_path)
117
+ assert calls == [None, None]
118
+
119
+
120
+ def test_silent_failure_does_not_borrow_next_devices_output(tmp_path, capfd):
121
+ devices = {20: state("GPS", None, "gps"), 30: state("RTL 01", Rtl(), "rtl_sdr")}
122
+ sc.run(opener(devices, {30: ["RTL-SDR Blog V4 Detected"]}), cfg_dir=tmp_path)
123
+ assert "✗ [20] GPS · not found" in capfd.readouterr().out
124
+
125
+
126
+ def test_buffered_print_is_the_failure_reason(tmp_path, capfd, monkeypatch):
127
+ monkeypatch.setattr(sys, "stdout", io.TextIOWrapper(open(1, "wb", closefd=False), line_buffering=False))
128
+ devices = {10: state("BT", None, "bluetooth"), 20: state("GPS", Rtl(), "rtl_sdr")}
129
+
130
+ def open_devices(on_device):
131
+ for gid, st in devices.items():
132
+ on_device(gid, st.name)
133
+ if st.dev is None:
134
+ print("make_device() failed for bluetooth: No adapter found")
135
+ dm._devices[gid] = st
136
+
137
+ sc.run(open_devices, cfg_dir=tmp_path)
138
+ assert "✗ [10] BT · make_device() failed for bluetooth: No adapter found" in capfd.readouterr().out
139
+ log = next((tmp_path / "logs").glob("startup-*.log")).read_text()
140
+ assert log.index("No adapter found") < log.index("[10] BT: ISSUE")
141
+
142
+
143
+ def test_checks_run_concurrently(tmp_path, capfd, monkeypatch):
144
+ monkeypatch.setattr(sc, "CHECK_TIMEOUT_S", 0.5)
145
+ sc.run(opener({1: state("A", Rtl(delay=0.3), "rtl_sdr"), 2: state("B", Rtl(delay=0.3), "rtl_sdr")}),
146
+ cfg_dir=tmp_path)
147
+ assert "All 2 devices operational" in capfd.readouterr().out
148
+
149
+
150
+ def test_no_devices_and_machinery_failure_never_raise(tmp_path, capfd, monkeypatch):
151
+ sc.run(opener({}), cfg_dir=tmp_path)
152
+ out = capfd.readouterr().out.splitlines()
153
+ assert out[0] == "No local devices opened" and out[1].startswith("log: ")
154
+
155
+ monkeypatch.setattr(dm, "_state_lock", None) # `with None:` raises inside the check
156
+ sc.run(opener({1: state("A", Rtl(), "rtl_sdr")}), cfg_dir=tmp_path)
157
+ assert "✗ startup check · failed" in capfd.readouterr().out
158
+ logs = sorted((tmp_path / "logs").glob("startup-*.log"))
159
+ assert any("startup check failed" in p.read_text() for p in logs)
160
+
161
+
162
+ def test_log_rotation(tmp_path):
163
+ logs = tmp_path / "logs"
164
+ logs.mkdir()
165
+ for i in range(15):
166
+ (logs / f"startup-2026010{i:02d}.log").write_text("")
167
+ sc.run(opener({}), cfg_dir=tmp_path)
168
+ assert len(list(logs.glob("startup-*.log"))) == sc.KEEP_LOGS
169
+
170
+
171
+ @pytest.mark.parametrize("reserved,power_state,cycled,powered_down", [
172
+ (False, "standby", True, True),
173
+ (True, "standby", True, False),
174
+ (True, "on", False, False), # its reservation pre-warm already booted it
175
+ ])
176
+ def test_poe_restore_rule(reserved, power_state, cycled, powered_down):
177
+ native = MagicMock(spec=PoePluto)
178
+ native._lock = threading.RLock()
179
+ native.power_state = power_state
180
+ native.sample_rate = 30_720_000
181
+ schema = types.SimpleNamespace(device=native, call_power_down=MagicMock())
182
+ sc._check(state("Pluto 00", schema, "adalm_pluto", reserved=reserved))
183
+ assert native.power_cycle.called is cycled
184
+ assert schema.call_power_down.called is powered_down
185
+
186
+
187
+ def test_poe_failure_still_returns_to_standby():
188
+ native = MagicMock(spec=PoePluto)
189
+ native._lock = threading.RLock()
190
+ native.power_state = "standby"
191
+ native.power_cycle.side_effect = RuntimeError("did not boot within 45 s")
192
+ schema = types.SimpleNamespace(device=native, call_power_down=MagicMock())
193
+ with pytest.raises(RuntimeError, match="45 s"):
194
+ sc._check(state("Pluto 04", schema, "adalm_pluto"))
195
+ schema.call_power_down.assert_called_once()
196
+
197
+
198
+ def test_tty_spinner_redraws_in_place():
199
+ master, slave = pty.openpty()
200
+ out = os.fdopen(slave, "w", encoding="utf-8")
201
+ display = sc._Display(out)
202
+ display.spin("[100] Pluto 00 · testing…")
203
+ display.result(True, "[100] Pluto 00", "operational")
204
+ out.flush()
205
+ text = os.read(master, 4096).decode()
206
+ out.close()
207
+ os.close(master)
208
+ assert "\r\x1b[2K" in text and "testing…" in text and "operational" in text
209
+ assert text.index("testing…") < text.rindex("\r\x1b[2K") < text.index("✓")
File without changes