[refactor](trx-rs): resolve all P1/P2 improvement areas

P1 (High Priority):
- Fix LIFO command batching in rig_task.rs (batch.pop→batch.remove(0))
- Add ±25% jitter to ExponentialBackoff to prevent thundering herd
- Add 10,000-entry capacity bounds to decoder history queues
- Add rig task crash detection with Error state broadcast
- Decompose FrontendRuntimeContext 50-field god-struct into 9 sub-structs
  (AudioContext, DecodeHistoryContext, HttpAuthConfig, HttpUiConfig,
   RigRoutingContext, OwnerInfo, VChanContext, SpectrumContext, PerRigAudioContext)
- Migrate std::sync::RwLock to tokio::sync::RwLock in background_decode.rs
- Extract find_input_device/find_output_device helpers from audio pipeline

P2 (Medium Priority):
- Introduce SoapySdrConfig builder struct (replaces 20+ positional params)
- Add define_command_mappings! macro for ClientCommand↔RigCommand mapping
- Replace silent lock poison recovery with lock_or_recover() warning logger
- Make timeouts configurable via RigTaskConfig/ListenerConfig and TOML
- Extract shared config types to trx-app/src/shared_config.rs

Documentation updated in CLAUDE.md, Architecture.md, Improvement-Areas.md.

https://claude.ai/code/session_01P9G7QCWfiYbPVJ7cgiXznf
Signed-off-by: Claude <noreply@anthropic.com>
This commit is contained in:
Claude
2026-03-28 23:26:55 +00:00
committed by Stan Grams
parent 0a60684e28
commit 16426548de
22 changed files with 1245 additions and 916 deletions
@@ -211,16 +211,17 @@ fn frontend_meta_from_context(
let server_connected = rig_id
.and_then(|rid| {
context
.routing
.rig_server_connected
.read()
.ok()
.and_then(|m| m.get(rid).copied())
})
.unwrap_or_else(|| context.server_connected.load(Ordering::Relaxed));
.unwrap_or_else(|| context.routing.server_connected.load(Ordering::Relaxed));
FrontendMeta {
http_clients,
rigctl_clients: context.rigctl_clients.load(Ordering::Relaxed),
audio_clients: context.audio_clients.load(Ordering::Relaxed),
audio_clients: context.audio.clients.load(Ordering::Relaxed),
rigctl_addr: rigctl_addr_from_context(context),
active_remote: active_rig_id_from_context(context),
remotes: rig_ids_from_context(context),
@@ -248,7 +249,8 @@ fn rigctl_addr_from_context(context: &FrontendRuntimeContext) -> Option<String>
fn active_rig_id_from_context(context: &FrontendRuntimeContext) -> Option<String> {
context
.remote_active_rig_id
.routing
.active_rig_id
.lock()
.ok()
.and_then(|v| v.clone())
@@ -256,6 +258,7 @@ fn active_rig_id_from_context(context: &FrontendRuntimeContext) -> Option<String
fn rig_ids_from_context(context: &FrontendRuntimeContext) -> Vec<String> {
context
.routing
.remote_rigs
.lock()
.ok()
@@ -264,41 +267,42 @@ fn rig_ids_from_context(context: &FrontendRuntimeContext) -> Vec<String> {
}
fn owner_callsign_from_context(context: &FrontendRuntimeContext) -> Option<String> {
context.owner_callsign.clone()
context.owner.callsign.clone()
}
fn owner_website_url_from_context(context: &FrontendRuntimeContext) -> Option<String> {
context.owner_website_url.clone()
context.owner.website_url.clone()
}
fn owner_website_name_from_context(context: &FrontendRuntimeContext) -> Option<String> {
context.owner_website_name.clone()
context.owner.website_name.clone()
}
fn ais_vessel_url_base_from_context(context: &FrontendRuntimeContext) -> Option<String> {
context.ais_vessel_url_base.clone()
context.owner.ais_vessel_url_base.clone()
}
fn show_sdr_gain_control_from_context(context: &FrontendRuntimeContext) -> bool {
context.http_show_sdr_gain_control
context.http_ui.show_sdr_gain_control
}
fn initial_map_zoom_from_context(context: &FrontendRuntimeContext) -> u8 {
context.http_initial_map_zoom
context.http_ui.initial_map_zoom
}
fn spectrum_coverage_margin_hz_from_context(context: &FrontendRuntimeContext) -> u32 {
context.http_spectrum_coverage_margin_hz
context.http_ui.spectrum_coverage_margin_hz
}
fn spectrum_usable_span_ratio_from_context(context: &FrontendRuntimeContext) -> f32 {
context.http_spectrum_usable_span_ratio
context.http_ui.spectrum_usable_span_ratio
}
fn decode_history_retention_min_from_context(context: &FrontendRuntimeContext) -> u64 {
let default_minutes = context.http_decode_history_retention_min.max(1);
let default_minutes = context.http_ui.decode_history_retention_min.max(1);
let Some(active_rig_id) = context
.remote_active_rig_id
.routing
.active_rig_id
.lock()
.ok()
.and_then(|v| v.clone())
@@ -306,7 +310,8 @@ fn decode_history_retention_min_from_context(context: &FrontendRuntimeContext) -
return default_minutes;
};
context
.http_decode_history_retention_min_by_rig
.http_ui
.decode_history_retention_min_by_rig
.get(&active_rig_id)
.copied()
.filter(|minutes| *minutes > 0)
@@ -343,7 +348,8 @@ pub async fn events(
// rig it has selected without mutating global state.
let active_rig_id = query.remote.clone().filter(|s| !s.is_empty()).or_else(|| {
context
.remote_active_rig_id
.routing
.active_rig_id
.lock()
.ok()
.and_then(|g| g.clone())
@@ -419,7 +425,8 @@ pub async fn events(
state.snapshot().and_then(|v| {
let rig_id_opt = session_rig_mgr.get_rig(session_id).or_else(|| {
context
.remote_active_rig_id
.routing
.active_rig_id
.lock()
.ok()
.and_then(|g| g.clone())
@@ -687,7 +694,7 @@ pub async fn decode_history(
context: web::Data<Arc<FrontendRuntimeContext>>,
query: web::Query<RemoteQuery>,
) -> impl Responder {
if context.decode_rx.is_none() {
if context.audio.decode_rx.is_none() {
return HttpResponse::NotFound().body("decode not enabled");
}
let rig_filter = query.remote.as_deref().filter(|s| !s.is_empty());
@@ -807,7 +814,7 @@ pub async fn spectrum(
let rx = if let Some(ref remote) = query.remote {
context.rig_spectrum_rx(remote)
} else {
context.spectrum.subscribe()
context.spectrum.sender.subscribe()
};
let mut last_rds_json: Option<String> = None;
let mut last_vchan_rds_json: Option<String> = None;
@@ -1351,7 +1358,7 @@ struct SatPassesResponse {
/// are not yet available.
#[get("/sat_passes")]
pub async fn sat_passes(context: web::Data<Arc<FrontendRuntimeContext>>) -> impl Responder {
let cached = context.sat_passes.read().ok().and_then(|g| g.clone());
let cached = context.routing.sat_passes.read().ok().and_then(|g| g.clone());
match cached {
Some(result) => {
let error = match result.tle_source {
@@ -1901,6 +1908,7 @@ struct RigListResponse {
fn build_rig_list_payload(context: &FrontendRuntimeContext) -> RigListResponse {
let active_remote = active_rig_id_from_context(context);
let rigs = context
.routing
.remote_rigs
.lock()
.ok()
@@ -1952,6 +1960,7 @@ pub async fn select_rig(
}
let known = context
.routing
.remote_rigs
.lock()
.ok()
@@ -36,15 +36,17 @@ fn current_timestamp_ms() -> i64 {
}
fn decode_history_retention(context: &FrontendRuntimeContext) -> Duration {
let default_minutes = context.http_decode_history_retention_min.max(1);
let default_minutes = context.http_ui.decode_history_retention_min.max(1);
let minutes = context
.remote_active_rig_id
.routing
.active_rig_id
.lock()
.ok()
.and_then(|v| v.clone())
.and_then(|rig_id| {
context
.http_decode_history_retention_min_by_rig
.http_ui
.decode_history_retention_min_by_rig
.get(&rig_id)
.copied()
})
@@ -111,7 +113,8 @@ fn prune_vdes_history(
fn active_rig_id(context: &FrontendRuntimeContext) -> Option<String> {
context
.remote_active_rig_id
.routing
.active_rig_id
.lock()
.ok()
.and_then(|g| g.clone())
@@ -123,7 +126,7 @@ fn record_ais(context: &FrontendRuntimeContext, mut msg: AisMessage) {
}
let rig_id = msg.rig_id.clone().or_else(|| active_rig_id(context));
let mut history = context
.ais_history
.decode_history.ais
.lock()
.expect("ais history mutex poisoned");
history.push_back((Instant::now(), rig_id, msg));
@@ -136,7 +139,7 @@ fn record_vdes(context: &FrontendRuntimeContext, mut msg: VdesMessage) {
}
let rig_id = msg.rig_id.clone().or_else(|| active_rig_id(context));
let mut history = context
.vdes_history
.decode_history.vdes
.lock()
.expect("vdes history mutex poisoned");
history.push_back((Instant::now(), rig_id, msg));
@@ -214,7 +217,7 @@ fn record_aprs(context: &FrontendRuntimeContext, mut pkt: AprsPacket) {
}
let rig_id = pkt.rig_id.clone().or_else(|| active_rig_id(context));
let mut history = context
.aprs_history
.decode_history.aprs
.lock()
.expect("aprs history mutex poisoned");
history.push_back((Instant::now(), rig_id, pkt));
@@ -227,7 +230,7 @@ fn record_hf_aprs(context: &FrontendRuntimeContext, mut pkt: AprsPacket) {
}
let rig_id = pkt.rig_id.clone().or_else(|| active_rig_id(context));
let mut history = context
.hf_aprs_history
.decode_history.hf_aprs
.lock()
.expect("hf_aprs history mutex poisoned");
history.push_back((Instant::now(), rig_id, pkt));
@@ -237,7 +240,7 @@ fn record_hf_aprs(context: &FrontendRuntimeContext, mut pkt: AprsPacket) {
fn record_cw(context: &FrontendRuntimeContext, event: CwEvent) {
let rig_id = event.rig_id.clone().or_else(|| active_rig_id(context));
let mut history = context
.cw_history
.decode_history.cw
.lock()
.expect("cw history mutex poisoned");
history.push_back((Instant::now(), rig_id, event));
@@ -247,7 +250,7 @@ fn record_cw(context: &FrontendRuntimeContext, event: CwEvent) {
fn record_ft8(context: &FrontendRuntimeContext, msg: Ft8Message) {
let rig_id = msg.rig_id.clone().or_else(|| active_rig_id(context));
let mut history = context
.ft8_history
.decode_history.ft8
.lock()
.expect("ft8 history mutex poisoned");
history.push_back((Instant::now(), rig_id, msg));
@@ -257,7 +260,7 @@ fn record_ft8(context: &FrontendRuntimeContext, msg: Ft8Message) {
fn record_ft4(context: &FrontendRuntimeContext, msg: Ft8Message) {
let rig_id = msg.rig_id.clone().or_else(|| active_rig_id(context));
let mut history = context
.ft4_history
.decode_history.ft4
.lock()
.expect("ft4 history mutex poisoned");
history.push_back((Instant::now(), rig_id, msg));
@@ -267,7 +270,7 @@ fn record_ft4(context: &FrontendRuntimeContext, msg: Ft8Message) {
fn record_ft2(context: &FrontendRuntimeContext, msg: Ft8Message) {
let rig_id = msg.rig_id.clone().or_else(|| active_rig_id(context));
let mut history = context
.ft2_history
.decode_history.ft2
.lock()
.expect("ft2 history mutex poisoned");
history.push_back((Instant::now(), rig_id, msg));
@@ -277,7 +280,7 @@ fn record_ft2(context: &FrontendRuntimeContext, msg: Ft8Message) {
fn record_wspr(context: &FrontendRuntimeContext, msg: WsprMessage) {
let rig_id = msg.rig_id.clone().or_else(|| active_rig_id(context));
let mut history = context
.wspr_history
.decode_history.wspr
.lock()
.expect("wspr history mutex poisoned");
history.push_back((Instant::now(), rig_id, msg));
@@ -298,7 +301,7 @@ pub fn snapshot_aprs_history(
rig_filter: Option<&str>,
) -> Vec<AprsPacket> {
let mut history = context
.aprs_history
.decode_history.aprs
.lock()
.expect("aprs history mutex poisoned");
prune_aprs_history(context, &mut history);
@@ -314,7 +317,7 @@ pub fn snapshot_hf_aprs_history(
rig_filter: Option<&str>,
) -> Vec<AprsPacket> {
let mut history = context
.hf_aprs_history
.decode_history.hf_aprs
.lock()
.expect("hf_aprs history mutex poisoned");
prune_hf_aprs_history(context, &mut history);
@@ -337,7 +340,7 @@ pub fn snapshot_ais_history(
rig_filter: Option<&str>,
) -> Vec<AisMessage> {
let mut history = context
.ais_history
.decode_history.ais
.lock()
.expect("ais history mutex poisoned");
prune_ais_history(context, &mut history);
@@ -359,7 +362,7 @@ pub fn snapshot_vdes_history(
rig_filter: Option<&str>,
) -> Vec<VdesMessage> {
let mut history = context
.vdes_history
.decode_history.vdes
.lock()
.expect("vdes history mutex poisoned");
prune_vdes_history(context, &mut history);
@@ -375,7 +378,7 @@ pub fn snapshot_cw_history(
rig_filter: Option<&str>,
) -> Vec<CwEvent> {
let mut history = context
.cw_history
.decode_history.cw
.lock()
.expect("cw history mutex poisoned");
prune_cw_history(context, &mut history);
@@ -391,7 +394,7 @@ pub fn snapshot_ft8_history(
rig_filter: Option<&str>,
) -> Vec<Ft8Message> {
let mut history = context
.ft8_history
.decode_history.ft8
.lock()
.expect("ft8 history mutex poisoned");
prune_ft8_history(context, &mut history);
@@ -407,7 +410,7 @@ pub fn snapshot_ft4_history(
rig_filter: Option<&str>,
) -> Vec<Ft8Message> {
let mut history = context
.ft4_history
.decode_history.ft4
.lock()
.expect("ft4 history mutex poisoned");
prune_ft4_history(context, &mut history);
@@ -423,7 +426,7 @@ pub fn snapshot_ft2_history(
rig_filter: Option<&str>,
) -> Vec<Ft8Message> {
let mut history = context
.ft2_history
.decode_history.ft2
.lock()
.expect("ft2 history mutex poisoned");
prune_ft2_history(context, &mut history);
@@ -439,7 +442,7 @@ pub fn snapshot_wspr_history(
rig_filter: Option<&str>,
) -> Vec<WsprMessage> {
let mut history = context
.wspr_history
.decode_history.wspr
.lock()
.expect("wspr history mutex poisoned");
prune_wspr_history(context, &mut history);
@@ -452,7 +455,7 @@ pub fn snapshot_wspr_history(
pub fn clear_aprs_history(context: &FrontendRuntimeContext) {
let mut history = context
.aprs_history
.decode_history.aprs
.lock()
.expect("aprs history mutex poisoned");
history.clear();
@@ -460,7 +463,7 @@ pub fn clear_aprs_history(context: &FrontendRuntimeContext) {
pub fn clear_hf_aprs_history(context: &FrontendRuntimeContext) {
let mut history = context
.hf_aprs_history
.decode_history.hf_aprs
.lock()
.expect("hf_aprs history mutex poisoned");
history.clear();
@@ -468,7 +471,7 @@ pub fn clear_hf_aprs_history(context: &FrontendRuntimeContext) {
pub fn clear_ais_history(context: &FrontendRuntimeContext) {
let mut history = context
.ais_history
.decode_history.ais
.lock()
.expect("ais history mutex poisoned");
history.clear();
@@ -476,7 +479,7 @@ pub fn clear_ais_history(context: &FrontendRuntimeContext) {
pub fn clear_vdes_history(context: &FrontendRuntimeContext) {
let mut history = context
.vdes_history
.decode_history.vdes
.lock()
.expect("vdes history mutex poisoned");
history.clear();
@@ -484,7 +487,7 @@ pub fn clear_vdes_history(context: &FrontendRuntimeContext) {
pub fn clear_cw_history(context: &FrontendRuntimeContext) {
let mut history = context
.cw_history
.decode_history.cw
.lock()
.expect("cw history mutex poisoned");
history.clear();
@@ -492,7 +495,7 @@ pub fn clear_cw_history(context: &FrontendRuntimeContext) {
pub fn clear_ft8_history(context: &FrontendRuntimeContext) {
let mut history = context
.ft8_history
.decode_history.ft8
.lock()
.expect("ft8 history mutex poisoned");
history.clear();
@@ -500,7 +503,7 @@ pub fn clear_ft8_history(context: &FrontendRuntimeContext) {
pub fn clear_ft4_history(context: &FrontendRuntimeContext) {
let mut history = context
.ft4_history
.decode_history.ft4
.lock()
.expect("ft4 history mutex poisoned");
history.clear();
@@ -508,7 +511,7 @@ pub fn clear_ft4_history(context: &FrontendRuntimeContext) {
pub fn clear_ft2_history(context: &FrontendRuntimeContext) {
let mut history = context
.ft2_history
.decode_history.ft2
.lock()
.expect("ft2 history mutex poisoned");
history.clear();
@@ -516,7 +519,7 @@ pub fn clear_ft2_history(context: &FrontendRuntimeContext) {
pub fn clear_wspr_history(context: &FrontendRuntimeContext) {
let mut history = context
.wspr_history
.decode_history.wspr
.lock()
.expect("wspr history mutex poisoned");
history.clear();
@@ -525,7 +528,7 @@ pub fn clear_wspr_history(context: &FrontendRuntimeContext) {
pub fn subscribe_decode(
context: &FrontendRuntimeContext,
) -> Option<broadcast::Receiver<DecodedMessage>> {
context.decode_rx.as_ref().map(|tx| tx.subscribe())
context.audio.decode_rx.as_ref().map(|tx| tx.subscribe())
}
pub fn start_decode_history_collector(context: Arc<FrontendRuntimeContext>) {
@@ -536,7 +539,7 @@ pub fn start_decode_history_collector(context: Arc<FrontendRuntimeContext>) {
return;
}
let Some(tx) = context.decode_rx.as_ref().cloned() else {
let Some(tx) = context.audio.decode_rx.as_ref().cloned() else {
return;
};
@@ -576,7 +579,7 @@ pub async fn audio_ws(
query: web::Query<AudioQuery>,
context: web::Data<Arc<FrontendRuntimeContext>>,
) -> Result<HttpResponse, Error> {
let Some(tx_sender) = context.audio_tx.as_ref().cloned() else {
let Some(tx_sender) = context.audio.tx.as_ref().cloned() else {
return Ok(HttpResponse::NotFound().body("audio not enabled"));
};
@@ -596,14 +599,14 @@ pub async fn audio_ws(
let info_rx = if let Some(ref remote) = query.remote {
context.rig_audio_info_rx(remote)
} else {
context.audio_info.as_ref().cloned()
context.audio.info.as_ref().cloned()
};
let Some(info_rx) = info_rx else {
return Ok(HttpResponse::NotFound().body("audio not enabled"));
};
let deadline = Instant::now() + Duration::from_secs(2);
let rx_sub = loop {
match context.vchan_audio.read() {
match context.vchan.audio.read() {
Ok(map) => {
if let Some(tx) = map.get(&ch_id) {
break tx.subscribe();
@@ -639,10 +642,10 @@ pub async fn audio_ws(
};
(rx_sub, info_rx)
} else {
let Some(info_rx) = context.audio_info.as_ref().cloned() else {
let Some(info_rx) = context.audio.info.as_ref().cloned() else {
return Ok(HttpResponse::NotFound().body("audio not enabled"));
};
let Some(rx) = context.audio_rx.as_ref() else {
let Some(rx) = context.audio.rx.as_ref() else {
return Ok(HttpResponse::NotFound().body("audio not enabled"));
};
(rx.subscribe(), info_rx)
@@ -651,7 +654,7 @@ pub async fn audio_ws(
let (response, mut session, mut msg_stream) = actix_ws::handle(&req, body)?;
let audio_clients = context.audio_clients.clone();
let audio_clients = context.audio.clients.clone();
audio_clients.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
actix_web::rt::spawn(async move {
@@ -5,7 +5,8 @@
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::atomic::Ordering;
use std::sync::{Arc, RwLock};
use std::sync::Arc;
use tokio::sync::RwLock;
use std::time::Duration;
use actix_web::{delete, get, put, web, HttpResponse, Responder};
@@ -115,18 +116,18 @@ impl BackgroundDecodeStore {
.unwrap_or_else(|| PathBuf::from("background_decode.db"))
}
pub fn get(&self, rig_id: &str) -> Option<BackgroundDecodeConfig> {
let db = self.db.read().unwrap_or_else(|e| e.into_inner());
pub async fn get(&self, rig_id: &str) -> Option<BackgroundDecodeConfig> {
let db = self.db.read().await;
db.get::<BackgroundDecodeConfig>(&format!("bgd:{rig_id}"))
}
pub fn upsert(&self, config: &BackgroundDecodeConfig) -> bool {
let mut db = self.db.write().unwrap_or_else(|e| e.into_inner());
pub async fn upsert(&self, config: &BackgroundDecodeConfig) -> bool {
let mut db = self.db.write().await;
db.set(&format!("bgd:{}", config.rig_id), config).is_ok()
}
pub fn remove(&self, rig_id: &str) -> bool {
let mut db = self.db.write().unwrap_or_else(|e| e.into_inner());
pub async fn remove(&self, rig_id: &str) -> bool {
let mut db = self.db.write().await;
db.rem(&format!("bgd:{rig_id}")).unwrap_or(false)
}
}
@@ -171,9 +172,10 @@ impl BackgroundDecodeManager {
});
}
pub fn get_config(&self, rig_id: &str) -> BackgroundDecodeConfig {
pub async fn get_config(&self, rig_id: &str) -> BackgroundDecodeConfig {
self.store
.get(rig_id)
.await
.unwrap_or_else(|| BackgroundDecodeConfig {
rig_id: rig_id.to_string(),
enabled: false,
@@ -181,9 +183,9 @@ impl BackgroundDecodeManager {
})
}
pub fn put_config(&self, mut config: BackgroundDecodeConfig) -> Option<BackgroundDecodeConfig> {
pub async fn put_config(&self, mut config: BackgroundDecodeConfig) -> Option<BackgroundDecodeConfig> {
config.bookmark_ids = dedup_ids(&config.bookmark_ids);
if self.store.upsert(&config) {
if self.store.upsert(&config).await {
self.trigger();
Some(config)
} else {
@@ -191,19 +193,20 @@ impl BackgroundDecodeManager {
}
}
pub fn reset_config(&self, rig_id: &str) -> bool {
let removed = self.store.remove(rig_id);
pub async fn reset_config(&self, rig_id: &str) -> bool {
let removed = self.store.remove(rig_id).await;
self.trigger();
removed
}
pub fn status(&self, rig_id: &str) -> BackgroundDecodeStatus {
if let Ok(status) = self.status.read() {
pub async fn status(&self, rig_id: &str) -> BackgroundDecodeStatus {
{
let status = self.status.read().await;
if let Some(entry) = status.get(rig_id) {
return entry.clone();
}
}
let cfg = self.get_config(rig_id);
let cfg = self.get_config(rig_id).await;
let bookmarks: HashMap<String, Bookmark> = self
.bookmarks
.list_for_rig(rig_id)
@@ -243,7 +246,8 @@ impl BackgroundDecodeManager {
fn active_rig_id(&self) -> Option<String> {
self.context
.remote_active_rig_id
.routing
.active_rig_id
.lock()
.ok()
.and_then(|guard| guard.clone())
@@ -252,7 +256,7 @@ impl BackgroundDecodeManager {
fn send_audio_cmd(&self, cmd: VChanAudioCmd) {
// Route through per-rig sender when available.
if let Some(rig_id) = self.active_rig_id() {
if let Ok(map) = self.context.rig_vchan_audio_cmd.read() {
if let Ok(map) = self.context.vchan.rig_audio_cmd.read() {
if let Some(tx) = map.get(&rig_id) {
let _ = tx.try_send(cmd);
return;
@@ -260,7 +264,7 @@ impl BackgroundDecodeManager {
}
}
// Fall back to global sender.
if let Ok(guard) = self.context.vchan_audio_cmd.lock() {
if let Ok(guard) = self.context.vchan.audio_cmd.lock() {
if let Some(tx) = guard.as_ref() {
let _ = tx.try_send(cmd);
}
@@ -316,15 +320,14 @@ impl BackgroundDecodeManager {
.any(|channel| channel_matches_bookmark(&channel, bookmark))
}
fn reconcile(&self, runtime: &mut BackgroundRuntimeState, spectrum: &SharedSpectrum) {
async fn reconcile(&self, runtime: &mut BackgroundRuntimeState, spectrum: &SharedSpectrum) {
let active_rig_id = self.active_rig_id();
if runtime.current_rig_id != active_rig_id {
if let Some(prev_rig_id) = runtime.current_rig_id.clone() {
if let Ok(mut guard) = self.status.write() {
if let Some(prev_status) = guard.get_mut(&prev_rig_id) {
prev_status.active_rig = false;
}
let mut guard = self.status.write().await;
if let Some(prev_status) = guard.get_mut(&prev_rig_id) {
prev_status.active_rig = false;
}
}
self.clear_runtime_channels(runtime);
@@ -335,7 +338,7 @@ impl BackgroundDecodeManager {
};
runtime.current_rig_id = Some(rig_id.clone());
let config = self.get_config(&rig_id);
let config = self.get_config(&rig_id).await;
let selected = dedup_ids(&config.bookmark_ids);
let users_connected = self.context.sse_clients.load(Ordering::Relaxed) > 0;
let scheduler_has_control = self.scheduler_control.scheduler_allowed() && users_connected;
@@ -467,19 +470,18 @@ impl BackgroundDecodeManager {
runtime.active_channels.insert(bookmark_id, desired);
}
if let Ok(mut guard) = self.status.write() {
guard.insert(
rig_id.clone(),
BackgroundDecodeStatus {
rig_id,
enabled: config.enabled,
active_rig: true,
center_hz,
sample_rate,
entries: statuses,
},
);
}
let mut guard = self.status.write().await;
guard.insert(
rig_id.clone(),
BackgroundDecodeStatus {
rig_id,
enabled: config.enabled,
active_rig: true,
center_hz,
sample_rate,
entries: statuses,
},
);
}
fn scheduler_bookmark_ids(&self, rig_id: &str) -> Vec<String> {
@@ -513,7 +515,7 @@ impl BackgroundDecodeManager {
loop {
let users_connected = self.context.sse_clients.load(Ordering::Relaxed) > 0;
if users_connected && spectrum_rx.is_none() {
spectrum_rx = Some(self.context.spectrum.subscribe());
spectrum_rx = Some(self.context.spectrum.sender.subscribe());
} else if !users_connected {
spectrum_rx = None;
}
@@ -522,7 +524,7 @@ impl BackgroundDecodeManager {
.as_ref()
.map(|rx| rx.borrow().clone())
.unwrap_or_default();
self.reconcile(&mut runtime, &spectrum);
self.reconcile(&mut runtime, &spectrum).await;
tokio::select! {
changed = async {
match spectrum_rx.as_mut() {
@@ -599,7 +601,7 @@ pub async fn get_background_decode(
path: web::Path<String>,
manager: web::Data<Arc<BackgroundDecodeManager>>,
) -> impl Responder {
HttpResponse::Ok().json(manager.get_config(&path.into_inner()))
HttpResponse::Ok().json(manager.get_config(&path.into_inner()).await)
}
#[put("/background-decode/{rig_id}")]
@@ -611,7 +613,7 @@ pub async fn put_background_decode(
let rig_id = path.into_inner();
let mut config = body.into_inner();
config.rig_id = rig_id;
match manager.put_config(config) {
match manager.put_config(config).await {
Some(saved) => HttpResponse::Ok().json(saved),
None => HttpResponse::InternalServerError().body("failed to save background decode config"),
}
@@ -623,7 +625,7 @@ pub async fn delete_background_decode(
manager: web::Data<Arc<BackgroundDecodeManager>>,
) -> impl Responder {
let rig_id = path.into_inner();
manager.reset_config(&rig_id);
manager.reset_config(&rig_id).await;
HttpResponse::Ok().json(BackgroundDecodeConfig {
rig_id,
enabled: false,
@@ -636,5 +638,5 @@ pub async fn get_background_decode_status(
path: web::Path<String>,
manager: web::Data<Arc<BackgroundDecodeManager>>,
) -> impl Responder {
HttpResponse::Ok().json(manager.status(&path.into_inner()))
HttpResponse::Ok().json(manager.status(&path.into_inner()).await)
}
@@ -73,6 +73,7 @@ async fn serve(
// Collect rig IDs for per-rig store initialisation / migration.
let rig_ids: Vec<String> = context
.routing
.remote_rigs
.lock()
.unwrap_or_else(|e| e.into_inner())
@@ -98,7 +99,7 @@ async fn serve(
let background_decode_store = Arc::new(BackgroundDecodeStore::open(&background_decode_path));
let vchan_mgr = Arc::new(ClientChannelManager::new(
4,
context.rig_vchan_audio_cmd.clone(),
context.vchan.rig_audio_cmd.clone(),
));
let session_rig_mgr = Arc::new(api::SessionRigManager::default());
let background_decode_mgr = BackgroundDecodeManager::new(
@@ -113,7 +114,7 @@ async fn serve(
// Wire the audio-command sender so allocate/delete/freq/mode operations on
// virtual channels are forwarded to the audio-client task.
if let Ok(guard) = context.vchan_audio_cmd.lock() {
if let Ok(guard) = context.vchan.audio_cmd.lock() {
if let Some(tx) = guard.as_ref() {
vchan_mgr.set_audio_cmd(tx.clone());
}
@@ -121,7 +122,7 @@ async fn serve(
// Spawn a task that removes channels destroyed server-side (OOB) from the
// client-side registry so the SSE channel list stays in sync.
if let Some(ref destroyed_tx) = context.vchan_destroyed {
if let Some(ref destroyed_tx) = context.vchan.destroyed {
let mut destroyed_rx = destroyed_tx.subscribe();
let mgr_for_destroyed = vchan_mgr.clone();
tokio::spawn(async move {
@@ -193,18 +194,18 @@ fn build_server(
let background_decode_mgr = web::Data::new(background_decode_mgr);
// Extract auth config values before moving context
let same_site = match context.http_auth_cookie_same_site.as_str() {
let same_site = match context.http_auth.cookie_same_site.as_str() {
"Strict" => SameSite::Strict,
"None" => SameSite::None,
_ => SameSite::Lax, // default
};
let auth_config = AuthConfig::new(
context.http_auth_enabled,
context.http_auth_rx_passphrase.clone(),
context.http_auth_control_passphrase.clone(),
context.http_auth_tx_access_control_enabled,
Duration::from_secs(context.http_auth_session_ttl_secs),
context.http_auth_cookie_secure,
context.http_auth.enabled,
context.http_auth.rx_passphrase.clone(),
context.http_auth.control_passphrase.clone(),
context.http_auth.tx_access_control_enabled,
Duration::from_secs(context.http_auth.session_ttl_secs),
context.http_auth.cookie_secure,
same_site,
);