remoteRF-server 1.4.2__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.
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/PKG-INFO +1 -1
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/pyproject.toml +1 -1
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/server/device_manager.py +6 -3
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/server/grpc_server.py +13 -3
- remoterf_server-1.4.4/src/remoteRF_server/server/startup_check.py +308 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server.egg-info/PKG-INFO +1 -1
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server.egg-info/SOURCES.txt +2 -0
- remoterf_server-1.4.4/tests/test_startup_check.py +209 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/LICENSE +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/README.md +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/setup.cfg +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/__init__.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/common/__init__.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/common/grpc/__init__.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/common/grpc/deployment_capabilities.proto +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/common/grpc/deployment_capabilities_pb2.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/common/grpc/deployment_capabilities_pb2_grpc.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/common/grpc/federation.proto +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/common/grpc/federation_pb2.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/common/grpc/federation_pb2_grpc.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/common/grpc/grpc.proto +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/common/grpc/grpc_host.proto +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/common/grpc/grpc_host_pb2.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/common/grpc/grpc_host_pb2_grpc.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/common/grpc/grpc_pb2.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/common/grpc/grpc_pb2_grpc.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/common/grpc/v2_codec.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/common/idl/__init__.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/common/idl/device_schema.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/common/idl/pluto_mimo_schema.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/common/idl/pluto_schema.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/common/idl/schema.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/common/idl/v2.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/common/utils/__init__.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/common/utils/ansi_codes.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/common/utils/api_token.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/common/utils/db_connection.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/common/utils/db_location.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/common/utils/list_string.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/common/utils/panel.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/common/utils/process_arg.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/drivers/__init__.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/drivers/adalm_pluto/__init__.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/drivers/adalm_pluto/pluto_mimo_schema.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/drivers/adalm_pluto/pluto_remote_server.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/drivers/adalm_pluto/pluto_schema.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/drivers/adalm_pluto/poe.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/drivers/hackrf/__init__.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/drivers/hackrf/hackrf_schema.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/drivers/rtl_sdr/__init__.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/drivers/rtl_sdr/rtl_sdr_schema.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/drivers/ti_mmwave/__init__.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/drivers/ti_mmwave/ti_mmwave_schema.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/drivers/usrp/__init__.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/drivers/usrp/policy.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/drivers/usrp/profiles.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/drivers/usrp/uhd_manifest.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/drivers/usrp/usrp_schema.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/drivers/usrp_phaser/__init__.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/drivers/usrp_phaser/usrp_phaser_schema.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/host/__init__.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/host/host_auth_token.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/host/host_directory_store.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/host/host_tunnel_server.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/server/__init__.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/server/acc_perms.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/server/cert_provider.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/server/commands.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/server/console.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/server/control.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/server/deployment/__init__.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/server/deployment/capabilities.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/server/direct_path.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/server/dynamic_v2.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/server/dynamic_v2_bridge.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/server/federation/__init__.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/server/federation/admin.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/server/federation/cli.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/server/federation/cluster.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/server/federation/contracts.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/server/federation/destination.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/server/federation/devices.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/server/federation/enrollment.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/server/federation/global_home.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/server/federation/home.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/server/federation/home_devices.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/server/federation/identity.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/server/federation/legacy_state.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/server/federation/migrations.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/server/federation/peers.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/server/federation/permissions.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/server/federation/principals.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/server/federation/runtime.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/server/federation/trust.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/server/global_onboarding.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/server/host_dynamic_v2.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/server/reservation.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/server/rpc_manager.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/server/user_group_cli.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/server/user_group_handler.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/server/v2_errors.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/serverrf_cli.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/tools/__init__.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/tools/gen_certs.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/tools/gist_status.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/tools/gist_status_testing.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/tools/introspect_uhd.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server.egg-info/dependency_links.txt +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server.egg-info/entry_points.txt +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server.egg-info/requires.txt +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server.egg-info/top_level.txt +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_account_hardening.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_account_names.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_account_removal.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_activity_log.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_bind_host.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_cert_provider.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_client_sessions.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_cluster.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_commands.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_console.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_contrib_drivers.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_default_rtl_sdr_schema.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_default_usrp_schema.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_deployment_capabilities.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_detached_device_rpc.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_device_handles.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_device_init.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_devices_list.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_direct_fixed_port.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_direct_path_server.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_dynamic_v2.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_federated_direct_path.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_federated_dynamic_v2.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_federation_edges.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_first_run.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_gate_d_contracts.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_gate_e_federation.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_gate_e_proto.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_gate_f_enrollment.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_gate_g1_logging.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_gate_g_permissions.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_gate_h_devices.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_gate_i_direct_path.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_gen_certs.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_global_accounts_migration.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_global_home.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_global_onboarding.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_global_v1_disposal.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_hackrf_schema.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_home_device_plane.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_host_directory_store.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_host_dynamic_v2.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_idl_schema.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_panel.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_peer_transport.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_pluto_compat.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_pluto_poe.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_process_arg.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_reservation_handoff.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_reservation_policy_b.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_reservation_timestamps.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_rpc_manager_batch.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_rpc_manager_calln.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_rpc_manager_rx_n.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_rtl_sdr_schema.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_soak_federation.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_status_push.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_ti_mmwave_hardware.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_ti_mmwave_schema.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_user_group_cli.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_usrp_b205mini_uhd410_hardware.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_usrp_client_server_hardware.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_usrp_client_server_localhost.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_usrp_n210_client_server_hardware.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_usrp_preload.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_usrp_profiles.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_usrp_remoterf_rx_hardware.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_usrp_uhd410_hardware.py +0 -0
- {remoterf_server-1.4.2 → remoterf_server-1.4.4}/tests/test_wipe.py +0 -0
{remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/server/device_manager.py
RENAMED
|
@@ -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
|
-
|
|
409
|
-
|
|
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
|
|
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()
|
|
@@ -500,6 +507,9 @@ def welcome():
|
|
|
500
507
|
try:
|
|
501
508
|
from . import global_onboarding as g
|
|
502
509
|
|
|
510
|
+
lan = g.lan_endpoint()
|
|
511
|
+
if lan and lan != f"{local_ip}:{local_port}":
|
|
512
|
+
rows.append(f"lan {lan}")
|
|
503
513
|
if g.participating() and g.state() == "active" and g.hostname():
|
|
504
514
|
rows.append(f"global {g.hostname()}")
|
|
505
515
|
except Exception: # noqa: BLE001 - the banner never fails on Global state
|
|
@@ -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
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/common/grpc/federation.proto
RENAMED
|
File without changes
|
{remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/common/grpc/federation_pb2.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/common/grpc/grpc_host.proto
RENAMED
|
File without changes
|
{remoterf_server-1.4.2 → remoterf_server-1.4.4}/src/remoteRF_server/common/grpc/grpc_host_pb2.py
RENAMED
|
File without changes
|
|
File without changes
|