[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:
@@ -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,
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user