refactor: 修改为同步请求

This commit is contained in:
mofeng-git
2026-05-01 20:06:22 +08:00
parent 0d47d8395d
commit 89b19ea7dd
16 changed files with 210 additions and 488 deletions

View File

@@ -1,5 +1,5 @@
use std::{collections::VecDeque, sync::Arc};
use tokio::sync::{broadcast, watch, RwLock};
use tokio::sync::{broadcast, watch, Mutex, RwLock};
use crate::atx::AtxController;
use crate::audio::AudioController;
@@ -20,6 +20,31 @@ use crate::update::UpdateService;
use crate::video::VideoStreamManager;
use crate::webrtc::WebRtcStreamer;
#[derive(Clone)]
pub struct ConfigApplyLocks {
pub video: Arc<Mutex<()>>,
pub stream: Arc<Mutex<()>>,
pub otg: Arc<Mutex<()>>,
pub audio: Arc<Mutex<()>>,
pub atx: Arc<Mutex<()>>,
pub rustdesk: Arc<Mutex<()>>,
pub rtsp: Arc<Mutex<()>>,
}
impl ConfigApplyLocks {
fn new() -> Self {
Self {
video: Arc::new(Mutex::new(())),
stream: Arc::new(Mutex::new(())),
otg: Arc::new(Mutex::new(())),
audio: Arc::new(Mutex::new(())),
atx: Arc::new(Mutex::new(())),
rustdesk: Arc::new(Mutex::new(())),
rtsp: Arc::new(Mutex::new(())),
}
}
}
/// Shared Axum/App state: video flows through [`VideoStreamManager`]; WebRTC SDP/ICE/sessions on [`WebRtcStreamer`].
pub struct AppState {
pub db: DatabasePool,
@@ -41,6 +66,7 @@ pub struct AppState {
pub update: Arc<UpdateService>,
pub shutdown_tx: broadcast::Sender<()>,
pub revoked_sessions: Arc<RwLock<VecDeque<String>>>,
pub config_apply_locks: ConfigApplyLocks,
data_dir: std::path::PathBuf,
}
@@ -88,6 +114,7 @@ impl AppState {
update,
shutdown_tx,
revoked_sessions: Arc::new(RwLock::new(VecDeque::new())),
config_apply_locks: ConfigApplyLocks::new(),
data_dir,
})
}

View File

@@ -8,6 +8,24 @@ use crate::stream_encoder::encoder_type_to_backend;
use crate::video::codec_constraints::{
enforce_constraints_with_stream_manager, StreamCodecConstraints,
};
use tokio::sync::{Mutex, OwnedMutexGuard};
#[derive(Debug, Clone, Copy, Default)]
pub struct ConfigApplyOptions {
pub force: bool,
}
impl ConfigApplyOptions {
pub const fn forced() -> Self {
Self { force: true }
}
}
pub fn try_apply_lock(lock: &Arc<Mutex<()>>, domain: &str) -> Result<OwnedMutexGuard<()>> {
lock.clone().try_lock_owned().map_err(|_| {
AppError::ServiceUnavailable(format!("{domain} configuration is already applying"))
})
}
fn hid_backend_type(config: &HidConfig) -> crate::hid::HidBackendType {
match config.backend {
@@ -33,8 +51,9 @@ pub async fn apply_video_config(
state: &Arc<AppState>,
old_config: &VideoConfig,
new_config: &VideoConfig,
options: ConfigApplyOptions,
) -> Result<()> {
if old_config == new_config {
if old_config == new_config && !options.force {
tracing::info!("Video config unchanged, skipping reload");
return Ok(());
}
@@ -73,10 +92,11 @@ pub async fn apply_stream_config(
state: &Arc<AppState>,
old_config: &StreamConfig,
new_config: &StreamConfig,
options: ConfigApplyOptions,
) -> Result<()> {
tracing::info!("Applying stream config changes...");
if old_config.encoder != new_config.encoder {
if options.force || old_config.encoder != new_config.encoder {
let encoder_backend = encoder_type_to_backend(new_config.encoder.clone());
tracing::info!(
"Updating encoder backend to: {:?} (from config: {:?})",
@@ -86,12 +106,11 @@ pub async fn apply_stream_config(
state.webrtc.update_encoder_backend(encoder_backend).await;
}
if old_config.bitrate_preset != new_config.bitrate_preset {
if options.force || old_config.bitrate_preset != new_config.bitrate_preset {
state
.stream_manager
.set_bitrate_preset(new_config.bitrate_preset)
.await
.ok(); // Ignore error if no active stream
.await?;
}
let ice_changed = old_config.stun_server != new_config.stun_server
@@ -99,7 +118,7 @@ pub async fn apply_stream_config(
|| old_config.turn_username != new_config.turn_username
|| old_config.turn_password != new_config.turn_password;
if ice_changed {
if options.force || ice_changed {
tracing::info!(
"Updating ICE config: STUN={:?}, TURN={:?}",
new_config.stun_server,
@@ -128,6 +147,7 @@ pub async fn apply_hid_config(
state: &Arc<AppState>,
old_config: &HidConfig,
new_config: &HidConfig,
options: ConfigApplyOptions,
) -> Result<()> {
let current_msd_enabled = state.config.get().msd.enabled;
new_config.validate_otg_endpoint_budget(current_msd_enabled)?;
@@ -149,6 +169,7 @@ pub async fn apply_hid_config(
&& !hid_functions_changed
&& !keyboard_leds_changed
&& !endpoint_budget_changed
&& !options.force
{
tracing::info!("HID config unchanged, skipping reload");
return Ok(());
@@ -190,6 +211,7 @@ pub async fn apply_msd_config(
state: &Arc<AppState>,
old_config: &MsdConfig,
new_config: &MsdConfig,
options: ConfigApplyOptions,
) -> Result<()> {
state
.config
@@ -222,7 +244,7 @@ pub async fn apply_msd_config(
tracing::warn!("Failed to create MSD ventoy directory: {}", e);
}
let needs_reload = old_msd_enabled != new_msd_enabled || msd_dir_changed;
let needs_reload = options.force || old_msd_enabled != new_msd_enabled || msd_dir_changed;
if !needs_reload {
tracing::info!(
"MSD enabled state unchanged ({}) and directory unchanged, no reload needed",
@@ -272,7 +294,9 @@ pub async fn apply_msd_config(
}
let current_config = state.config.get();
if current_config.hid.backend == HidBackend::Otg && old_msd_enabled != new_msd_enabled {
if current_config.hid.backend == HidBackend::Otg
&& (options.force || old_msd_enabled != new_msd_enabled)
{
state
.hid
.reload(crate::hid::HidBackendType::Otg)
@@ -306,12 +330,11 @@ pub async fn apply_atx_config(
tracing::info!("ATX enabled in config, initializing...");
let atx = crate::atx::AtxController::new(controller_config);
if let Err(e) = atx.init().await {
tracing::warn!("ATX initialization failed: {}", e);
} else {
*state.atx.write().await = Some(atx);
tracing::info!("ATX controller initialized successfully");
}
atx.init()
.await
.map_err(|e| AppError::Config(format!("ATX initialization failed: {}", e)))?;
*state.atx.write().await = Some(atx);
tracing::info!("ATX controller initialized successfully");
}
}
@@ -331,25 +354,18 @@ pub async fn apply_audio_config(
quality: new_config.quality.parse::<crate::audio::AudioQuality>()?,
};
if let Err(e) = state.audio.update_config(audio_config).await {
tracing::error!("Audio config update failed: {}", e);
} else {
tracing::info!(
"Audio config applied: enabled={}, device={}",
new_config.enabled,
new_config.device
);
}
state.audio.update_config(audio_config).await?;
tracing::info!(
"Audio config applied: enabled={}, device={}",
new_config.enabled,
new_config.device
);
if let Err(e) = state
state
.stream_manager
.set_webrtc_audio_enabled(new_config.enabled)
.await
{
tracing::warn!("Failed to update WebRTC audio state: {}", e);
} else {
tracing::info!("WebRTC audio enabled: {}", new_config.enabled);
}
.await?;
tracing::info!("WebRTC audio enabled: {}", new_config.enabled);
if new_config.enabled {
state.stream_manager.reconnect_webrtc_audio_sources().await;
@@ -370,6 +386,7 @@ pub async fn apply_rustdesk_config(
state: &Arc<AppState>,
old_config: &crate::rustdesk::config::RustDeskConfig,
new_config: &crate::rustdesk::config::RustDeskConfig,
options: ConfigApplyOptions,
) -> Result<()> {
tracing::info!("Applying RustDesk config changes...");
@@ -378,16 +395,18 @@ pub async fn apply_rustdesk_config(
if old_config.enabled && !new_config.enabled {
if let Some(ref service) = *rustdesk_guard {
if let Err(e) = service.stop().await {
tracing::error!("Failed to stop RustDesk service: {}", e);
}
service
.stop()
.await
.map_err(|e| AppError::Config(format!("Failed to stop RustDesk service: {}", e)))?;
tracing::info!("RustDesk service stopped");
}
*rustdesk_guard = None;
}
if new_config.enabled {
let need_restart = old_config.rendezvous_server != new_config.rendezvous_server
let need_restart = options.force
|| old_config.rendezvous_server != new_config.rendezvous_server
|| old_config.device_id != new_config.device_id
|| old_config.device_password != new_config.device_password;
@@ -399,24 +418,22 @@ pub async fn apply_rustdesk_config(
state.hid.clone(),
state.audio.clone(),
);
if let Err(e) = service.start().await {
tracing::error!("Failed to start RustDesk service: {}", e);
} else {
tracing::info!("RustDesk service started with ID: {}", new_config.device_id);
credentials_to_save = service.save_credentials();
}
service.start().await.map_err(|e| {
AppError::Config(format!("Failed to start RustDesk service: {}", e))
})?;
tracing::info!("RustDesk service started with ID: {}", new_config.device_id);
credentials_to_save = service.save_credentials();
*rustdesk_guard = Some(std::sync::Arc::new(service));
} else if need_restart {
if let Some(ref service) = *rustdesk_guard {
if let Err(e) = service.restart(new_config.clone()).await {
tracing::error!("Failed to restart RustDesk service: {}", e);
} else {
tracing::info!(
"RustDesk service restarted with ID: {}",
new_config.device_id
);
credentials_to_save = service.save_credentials();
}
service.restart(new_config.clone()).await.map_err(|e| {
AppError::Config(format!("Failed to restart RustDesk service: {}", e))
})?;
tracing::info!(
"RustDesk service restarted with ID: {}",
new_config.device_id
);
credentials_to_save = service.save_credentials();
}
}
}
@@ -424,7 +441,7 @@ pub async fn apply_rustdesk_config(
drop(rustdesk_guard);
if let Some(updated_config) = credentials_to_save {
tracing::info!("Saving RustDesk credentials to config store...");
if let Err(e) = state
state
.config
.update(|cfg| {
cfg.rustdesk.public_key = updated_config.public_key.clone();
@@ -433,12 +450,8 @@ pub async fn apply_rustdesk_config(
cfg.rustdesk.signing_private_key = updated_config.signing_private_key.clone();
cfg.rustdesk.uuid = updated_config.uuid.clone();
})
.await
{
tracing::warn!("Failed to save RustDesk credentials: {}", e);
} else {
tracing::info!("RustDesk credentials saved successfully");
}
.await?;
tracing::info!("RustDesk credentials saved successfully");
}
if let Some(message) = enforce_stream_codec_constraints(state).await? {
@@ -452,6 +465,7 @@ pub async fn apply_rtsp_config(
state: &Arc<AppState>,
old_config: &RtspConfig,
new_config: &RtspConfig,
options: ConfigApplyOptions,
) -> Result<()> {
tracing::info!("Applying RTSP config changes...");
@@ -459,15 +473,17 @@ pub async fn apply_rtsp_config(
if old_config.enabled && !new_config.enabled {
if let Some(ref service) = *rtsp_guard {
if let Err(e) = service.stop().await {
tracing::error!("Failed to stop RTSP service: {}", e);
}
service
.stop()
.await
.map_err(|e| AppError::Config(format!("Failed to stop RTSP service: {}", e)))?;
}
*rtsp_guard = None;
}
if new_config.enabled {
let need_restart = old_config.bind != new_config.bind
let need_restart = options.force
|| old_config.bind != new_config.bind
|| old_config.port != new_config.port
|| old_config.path != new_config.path
|| old_config.codec != new_config.codec

View File

@@ -6,7 +6,7 @@ use crate::config::{AtxConfig, HidBackend, HidConfig};
use crate::error::{AppError, Result};
use crate::state::AppState;
use super::apply::apply_atx_config;
use super::apply::{apply_atx_config, try_apply_lock};
use super::types::AtxConfigUpdate;
pub async fn get_atx_config(State(state): State<Arc<AppState>>) -> Json<AtxConfig> {
@@ -22,6 +22,7 @@ pub async fn update_atx_config(
req.validate_with_current(&old_atx_config)?;
let _apply_guard = try_apply_lock(&state.config_apply_locks.atx, "atx")?;
let mut merged_atx_config = old_atx_config.clone();
req.apply_to(&mut merged_atx_config);
validate_serial_device_conflict(&merged_atx_config, &current_config.hid)?;
@@ -35,9 +36,7 @@ pub async fn update_atx_config(
let new_atx_config = state.config.get().atx.clone();
if let Err(e) = apply_atx_config(&state, &old_atx_config, &new_atx_config).await {
tracing::error!("Failed to apply ATX config: {}", e);
}
apply_atx_config(&state, &old_atx_config, &new_atx_config).await?;
Ok(Json(new_atx_config))
}

View File

@@ -5,7 +5,7 @@ use crate::config::AudioConfig;
use crate::error::Result;
use crate::state::AppState;
use super::apply::apply_audio_config;
use super::apply::{apply_audio_config, try_apply_lock};
use super::types::AudioConfigUpdate;
pub async fn get_audio_config(State(state): State<Arc<AppState>>) -> Json<AudioConfig> {
@@ -18,6 +18,7 @@ pub async fn update_audio_config(
) -> Result<Json<AudioConfig>> {
req.validate()?;
let _apply_guard = try_apply_lock(&state.config_apply_locks.audio, "audio")?;
let old_audio_config = state.config.get().audio.clone();
state
@@ -29,9 +30,7 @@ pub async fn update_audio_config(
let new_audio_config = state.config.get().audio.clone();
if let Err(e) = apply_audio_config(&state, &old_audio_config, &new_audio_config).await {
tracing::error!("Failed to apply audio config: {}", e);
}
apply_audio_config(&state, &old_audio_config, &new_audio_config).await?;
Ok(Json(new_audio_config))
}

View File

@@ -5,7 +5,7 @@ use crate::config::HidConfig;
use crate::error::Result;
use crate::state::AppState;
use super::apply::apply_hid_config;
use super::apply::{apply_hid_config, try_apply_lock, ConfigApplyOptions};
use super::types::HidConfigUpdate;
pub async fn get_hid_config(State(state): State<Arc<AppState>>) -> Json<HidConfig> {
@@ -18,6 +18,7 @@ pub async fn update_hid_config(
) -> Result<Json<HidConfig>> {
req.validate()?;
let _apply_guard = try_apply_lock(&state.config_apply_locks.otg, "otg")?;
let old_hid_config = state.config.get().hid.clone();
state
@@ -29,9 +30,13 @@ pub async fn update_hid_config(
let new_hid_config = state.config.get().hid.clone();
if let Err(e) = apply_hid_config(&state, &old_hid_config, &new_hid_config).await {
tracing::error!("Failed to apply HID config: {}", e);
}
apply_hid_config(
&state,
&old_hid_config,
&new_hid_config,
ConfigApplyOptions::forced(),
)
.await?;
Ok(Json(new_hid_config))
}

View File

@@ -5,7 +5,7 @@ use crate::config::MsdConfig;
use crate::error::Result;
use crate::state::AppState;
use super::apply::apply_msd_config;
use super::apply::{apply_msd_config, try_apply_lock, ConfigApplyOptions};
use super::types::MsdConfigUpdate;
pub async fn get_msd_config(State(state): State<Arc<AppState>>) -> Json<MsdConfig> {
@@ -18,6 +18,7 @@ pub async fn update_msd_config(
) -> Result<Json<MsdConfig>> {
req.validate()?;
let _apply_guard = try_apply_lock(&state.config_apply_locks.otg, "otg")?;
let old_msd_config = state.config.get().msd.clone();
state
@@ -29,9 +30,13 @@ pub async fn update_msd_config(
let new_msd_config = state.config.get().msd.clone();
if let Err(e) = apply_msd_config(&state, &old_msd_config, &new_msd_config).await {
tracing::error!("Failed to apply MSD config: {}", e);
}
apply_msd_config(
&state,
&old_msd_config,
&new_msd_config,
ConfigApplyOptions::forced(),
)
.await?;
Ok(Json(new_msd_config))
}

View File

@@ -1,10 +1,10 @@
use axum::{extract::State, Json};
use std::sync::Arc;
use crate::error::{AppError, Result};
use crate::error::Result;
use crate::state::AppState;
use super::apply::apply_rtsp_config;
use super::apply::{apply_rtsp_config, try_apply_lock, ConfigApplyOptions};
use super::types::{RtspConfigResponse, RtspConfigUpdate, RtspStatusResponse};
pub async fn get_rtsp_config(State(state): State<Arc<AppState>>) -> Json<RtspConfigResponse> {
@@ -32,6 +32,7 @@ pub async fn update_rtsp_config(
) -> Result<Json<RtspConfigResponse>> {
req.validate()?;
let _apply_guard = try_apply_lock(&state.config_apply_locks.rtsp, "rtsp")?;
let old_config = state.config.get().rtsp.clone();
state
@@ -42,26 +43,13 @@ pub async fn update_rtsp_config(
.await?;
let new_config = state.config.get().rtsp.clone();
if let Err(err) = apply_rtsp_config(&state, &old_config, &new_config).await {
tracing::error!("Failed to apply RTSP config: {}", err);
if let Err(rollback_err) = state
.config
.update(|config| {
config.rtsp = old_config.clone();
})
.await
{
tracing::error!(
"Failed to rollback RTSP config after apply failure: {}",
rollback_err
);
return Err(AppError::ServiceUnavailable(format!(
"RTSP apply failed: {}; rollback failed: {}",
err, rollback_err
)));
}
return Err(err);
}
apply_rtsp_config(
&state,
&old_config,
&new_config,
ConfigApplyOptions::forced(),
)
.await?;
Ok(Json(RtspConfigResponse::from(&new_config)))
}

View File

@@ -5,7 +5,7 @@ use crate::error::Result;
use crate::rustdesk::config::RustDeskConfig;
use crate::state::AppState;
use super::apply::apply_rustdesk_config;
use super::apply::{apply_rustdesk_config, try_apply_lock, ConfigApplyOptions};
use super::types::RustDeskConfigUpdate;
#[derive(Debug, serde::Serialize)]
@@ -75,6 +75,7 @@ pub async fn update_rustdesk_config(
) -> Result<Json<RustDeskConfigResponse>> {
req.validate()?;
let _apply_guard = try_apply_lock(&state.config_apply_locks.rustdesk, "rustdesk")?;
let old_config = state.config.get().rustdesk.clone();
state
@@ -86,9 +87,13 @@ pub async fn update_rustdesk_config(
let new_config = state.config.get().rustdesk.clone();
if let Err(e) = apply_rustdesk_config(&state, &old_config, &new_config).await {
tracing::error!("Failed to apply RustDesk config: {}", e);
}
apply_rustdesk_config(
&state,
&old_config,
&new_config,
ConfigApplyOptions::forced(),
)
.await?;
let constraints = state.stream_manager.codec_constraints().await;
if constraints.rustdesk_enabled || constraints.rtsp_enabled {

View File

@@ -4,7 +4,7 @@ use std::sync::Arc;
use crate::error::Result;
use crate::state::AppState;
use super::apply::apply_stream_config;
use super::apply::{apply_stream_config, try_apply_lock, ConfigApplyOptions};
use super::types::{StreamConfigResponse, StreamConfigUpdate};
pub async fn get_stream_config(State(state): State<Arc<AppState>>) -> Json<StreamConfigResponse> {
@@ -18,6 +18,7 @@ pub async fn update_stream_config(
) -> Result<Json<StreamConfigResponse>> {
req.validate()?;
let _apply_guard = try_apply_lock(&state.config_apply_locks.stream, "stream")?;
let old_stream_config = state.config.get().stream.clone();
state
@@ -29,13 +30,15 @@ pub async fn update_stream_config(
let new_stream_config = state.config.get().stream.clone();
if let Err(e) = apply_stream_config(&state, &old_stream_config, &new_stream_config).await {
tracing::error!("Failed to apply stream config: {}", e);
}
apply_stream_config(
&state,
&old_stream_config,
&new_stream_config,
ConfigApplyOptions::forced(),
)
.await?;
if let Err(e) = super::apply::enforce_stream_codec_constraints(&state).await {
tracing::error!("Failed to enforce stream codec constraints: {}", e);
}
super::apply::enforce_stream_codec_constraints(&state).await?;
Ok(Json(StreamConfigResponse::from(&new_stream_config)))
}

View File

@@ -5,7 +5,7 @@ use crate::config::VideoConfig;
use crate::error::Result;
use crate::state::AppState;
use super::apply::apply_video_config;
use super::apply::{apply_video_config, try_apply_lock, ConfigApplyOptions};
use super::types::VideoConfigUpdate;
pub async fn get_video_config(State(state): State<Arc<AppState>>) -> Json<VideoConfig> {
@@ -18,6 +18,7 @@ pub async fn update_video_config(
) -> Result<Json<VideoConfig>> {
req.validate()?;
let _apply_guard = try_apply_lock(&state.config_apply_locks.video, "video")?;
let old_video_config = state.config.get().video.clone();
state
@@ -29,10 +30,13 @@ pub async fn update_video_config(
let new_video_config = state.config.get().video.clone();
if let Err(e) = apply_video_config(&state, &old_video_config, &new_video_config).await {
tracing::error!("Failed to apply video config: {}", e);
// 根据用户选择,仅记录错误,不回滚
}
apply_video_config(
&state,
&old_video_config,
&new_video_config,
ConfigApplyOptions::forced(),
)
.await?;
Ok(Json(new_video_config))
}

View File

@@ -9,8 +9,9 @@ use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tracing::{info, warn};
use self::config::apply::ConfigApplyOptions;
use crate::auth::{Session, SESSION_COOKIE};
use crate::config::{AppConfig, StreamMode};
use crate::config::StreamMode;
use crate::error::{AppError, Result};
use crate::state::AppState;
use crate::update::{UpdateChannel, UpdateOverviewResponse, UpdateStatusResponse, UpgradeRequest};
@@ -739,8 +740,13 @@ pub async fn setup_init(
// Start RustDesk if enabled
if new_config.rustdesk.enabled {
let empty_config = crate::rustdesk::config::RustDeskConfig::default();
if let Err(e) =
config::apply::apply_rustdesk_config(&state, &empty_config, &new_config.rustdesk).await
if let Err(e) = config::apply::apply_rustdesk_config(
&state,
&empty_config,
&new_config.rustdesk,
ConfigApplyOptions::default(),
)
.await
{
tracing::warn!("Failed to start RustDesk during setup: {}", e);
} else {
@@ -751,8 +757,13 @@ pub async fn setup_init(
// Start RTSP if enabled
if new_config.rtsp.enabled {
let empty_config = crate::config::RtspConfig::default();
if let Err(e) =
config::apply::apply_rtsp_config(&state, &empty_config, &new_config.rtsp).await
if let Err(e) = config::apply::apply_rtsp_config(
&state,
&empty_config,
&new_config.rtsp,
ConfigApplyOptions::default(),
)
.await
{
tracing::warn!("Failed to start RTSP during setup: {}", e);
} else {
@@ -792,160 +803,6 @@ pub async fn setup_init(
}))
}
#[derive(Deserialize)]
pub struct UpdateConfigRequest {
#[serde(flatten)]
pub updates: serde_json::Value,
}
pub async fn update_config(
State(state): State<Arc<AppState>>,
Json(req): Json<UpdateConfigRequest>,
) -> Result<Json<LoginResponse>> {
// Keep old config for rollback
let old_config = state.config.get();
tracing::info!("Received config update request");
// Validate and merge config first (outside the update closure)
let config_json = serde_json::to_value(&old_config)
.map_err(|e| AppError::Internal(format!("Failed to serialize config: {}", e)))?;
let merged = merge_json(config_json, req.updates.clone())
.map_err(|_| AppError::Internal("Failed to merge config".to_string()))?;
let new_config: AppConfig = serde_json::from_value(merged)
.map_err(|e| AppError::BadRequest(format!("Invalid config format: {}", e)))?;
let new_config = new_config;
new_config
.hid
.validate_otg_endpoint_budget(new_config.msd.enabled)?;
// Apply the validated config
state.config.set(new_config.clone()).await?;
tracing::info!("Config updated successfully");
// Detect which config sections were sent in the request
let has_video = req.updates.get("video").is_some();
let has_stream = req.updates.get("stream").is_some();
let has_hid = req.updates.get("hid").is_some();
let has_msd = req.updates.get("msd").is_some();
let has_atx = req.updates.get("atx").is_some();
let has_audio = req.updates.get("audio").is_some();
tracing::info!(
"Config sections sent: video={}, stream={}, hid={}, msd={}, atx={}, audio={}",
has_video,
has_stream,
has_hid,
has_msd,
has_atx,
has_audio
);
// Get new config for device reloading
let new_config = state.config.get();
if has_video {
if let Err(e) =
config::apply::apply_video_config(&state, &old_config.video, &new_config.video).await
{
tracing::error!("Failed to apply video config: {}", e);
state.config.set((*old_config).clone()).await?;
return Ok(Json(LoginResponse {
success: false,
message: Some(format!("Video configuration invalid: {}", e)),
}));
}
}
if has_stream {
if let Err(e) =
config::apply::apply_stream_config(&state, &old_config.stream, &new_config.stream).await
{
tracing::error!("Failed to apply stream config: {}", e);
state.config.set((*old_config).clone()).await?;
return Ok(Json(LoginResponse {
success: false,
message: Some(format!("Stream configuration invalid: {}", e)),
}));
}
}
if has_hid {
if let Err(e) =
config::apply::apply_hid_config(&state, &old_config.hid, &new_config.hid).await
{
tracing::error!("HID reload failed: {}", e);
state.config.set((*old_config).clone()).await?;
return Ok(Json(LoginResponse {
success: false,
message: Some(format!("HID configuration invalid: {}", e)),
}));
}
}
if has_audio {
if let Err(e) =
config::apply::apply_audio_config(&state, &old_config.audio, &new_config.audio).await
{
tracing::warn!("Audio config update failed: {}", e);
}
}
if has_msd {
if let Err(e) =
config::apply::apply_msd_config(&state, &old_config.msd, &new_config.msd).await
{
tracing::error!("MSD initialization failed: {}", e);
state.config.set((*old_config).clone()).await?;
return Ok(Json(LoginResponse {
success: false,
message: Some(format!("MSD initialization failed: {}", e)),
}));
}
}
if has_atx {
if let Err(e) =
config::apply::apply_atx_config(&state, &old_config.atx, &new_config.atx).await
{
tracing::error!("ATX configuration invalid: {}", e);
state.config.set((*old_config).clone()).await?;
return Ok(Json(LoginResponse {
success: false,
message: Some(format!("ATX configuration invalid: {}", e)),
}));
}
}
Ok(Json(LoginResponse {
success: true,
message: Some("Configuration updated".to_string()),
}))
}
fn merge_json(
base: serde_json::Value,
updates: serde_json::Value,
) -> std::result::Result<serde_json::Value, ()> {
match (base, updates) {
(serde_json::Value::Object(mut base), serde_json::Value::Object(updates)) => {
for (key, value) in updates {
if let Some(base_value) = base.get(&key).cloned() {
base.insert(key, merge_json(base_value, value)?);
} else {
base.insert(key, value);
}
}
Ok(serde_json::Value::Object(base))
}
(_, updates) => Ok(updates),
}
}
#[derive(Serialize)]
pub struct DeviceList {
pub video: Vec<VideoDevice>,

View File

@@ -78,7 +78,6 @@ pub fn create_router(state: Arc<AppState>) -> Router {
.route("/ws/audio", any(audio_ws_handler))
// Configuration management (domain-separated endpoints)
.route("/config", get(handlers::config::get_all_config))
.route("/config", post(handlers::update_config))
.route("/config/video", get(handlers::config::get_video_config))
.route(
"/config/video",