This commit is contained in:
mofeng-git
2025-12-28 18:19:16 +08:00
commit d143d158e4
771 changed files with 220548 additions and 0 deletions

View File

@@ -0,0 +1,362 @@
//! 配置热重载逻辑
//!
//! 从 handlers.rs 中抽取的配置应用函数,负责将配置变更应用到各个子系统。
use std::sync::Arc;
use crate::config::*;
use crate::error::{AppError, Result};
use crate::state::AppState;
/// 应用 Video 配置变更
pub async fn apply_video_config(
state: &Arc<AppState>,
old_config: &VideoConfig,
new_config: &VideoConfig,
) -> Result<()> {
// 检查配置是否实际变更
if old_config == new_config {
tracing::info!("Video config unchanged, skipping reload");
return Ok(());
}
tracing::info!("Applying video config changes...");
let device = new_config
.device
.clone()
.ok_or_else(|| AppError::BadRequest("video_device is required".to_string()))?;
let format = new_config
.format
.as_ref()
.and_then(|f| {
serde_json::from_value::<crate::video::format::PixelFormat>(
serde_json::Value::String(f.clone()),
)
.ok()
})
.unwrap_or(crate::video::format::PixelFormat::Mjpeg);
let resolution =
crate::video::format::Resolution::new(new_config.width, new_config.height);
// Step 1: 更新 WebRTC streamer 配置(停止现有 pipeline 和 sessions
state
.stream_manager
.webrtc_streamer()
.update_video_config(resolution, format, new_config.fps)
.await;
tracing::info!("WebRTC streamer config updated");
// Step 2: 应用视频配置到 streamer重新创建 capturer
state
.stream_manager
.streamer()
.apply_video_config(&device, format, resolution, new_config.fps)
.await
.map_err(|e| AppError::VideoError(format!("Failed to apply video config: {}", e)))?;
tracing::info!("Video config applied to streamer");
// Step 3: 重启 streamer
if let Err(e) = state.stream_manager.start().await {
tracing::error!("Failed to start streamer after config change: {}", e);
} else {
tracing::info!("Streamer started after config change");
}
// Step 4: 更新 WebRTC frame source
if let Some(frame_tx) = state.stream_manager.frame_sender().await {
let receiver_count = frame_tx.receiver_count();
state
.stream_manager
.webrtc_streamer()
.set_video_source(frame_tx)
.await;
tracing::info!(
"WebRTC streamer frame source updated (receiver_count={})",
receiver_count
);
} else {
tracing::warn!("No frame source available after config change");
}
tracing::info!("Video config applied successfully");
Ok(())
}
/// 应用 Stream 配置变更
pub async fn apply_stream_config(
state: &Arc<AppState>,
old_config: &StreamConfig,
new_config: &StreamConfig,
) -> Result<()> {
tracing::info!("Applying stream config changes...");
// 更新编码器后端
if old_config.encoder != new_config.encoder {
let encoder_backend = new_config.encoder.to_backend();
tracing::info!(
"Updating encoder backend to: {:?} (from config: {:?})",
encoder_backend,
new_config.encoder
);
state
.stream_manager
.webrtc_streamer()
.update_encoder_backend(encoder_backend)
.await;
}
// 更新码率
if old_config.bitrate_kbps != new_config.bitrate_kbps {
state
.stream_manager
.webrtc_streamer()
.set_bitrate(new_config.bitrate_kbps)
.await
.ok(); // Ignore error if no active stream
}
// 更新 ICE 配置 (STUN/TURN)
let ice_changed = old_config.stun_server != new_config.stun_server
|| old_config.turn_server != new_config.turn_server
|| old_config.turn_username != new_config.turn_username
|| old_config.turn_password != new_config.turn_password;
if ice_changed {
tracing::info!(
"Updating ICE config: STUN={:?}, TURN={:?}",
new_config.stun_server,
new_config.turn_server
);
state
.stream_manager
.webrtc_streamer()
.update_ice_config(
new_config.stun_server.clone(),
new_config.turn_server.clone(),
new_config.turn_username.clone(),
new_config.turn_password.clone(),
)
.await;
}
tracing::info!(
"Stream config applied: encoder={:?}, bitrate={} kbps",
new_config.encoder,
new_config.bitrate_kbps
);
Ok(())
}
/// 应用 HID 配置变更
pub async fn apply_hid_config(
state: &Arc<AppState>,
old_config: &HidConfig,
new_config: &HidConfig,
) -> Result<()> {
// 检查是否需要重载
if old_config.backend == new_config.backend
&& old_config.ch9329_port == new_config.ch9329_port
&& old_config.ch9329_baudrate == new_config.ch9329_baudrate
&& old_config.otg_udc == new_config.otg_udc
{
tracing::info!("HID config unchanged, skipping reload");
return Ok(());
}
tracing::info!("Applying HID config changes...");
let new_hid_backend = match new_config.backend {
HidBackend::Otg => crate::hid::HidBackendType::Otg,
HidBackend::Ch9329 => crate::hid::HidBackendType::Ch9329 {
port: new_config.ch9329_port.clone(),
baud_rate: new_config.ch9329_baudrate,
},
HidBackend::None => crate::hid::HidBackendType::None,
};
state
.hid
.reload(new_hid_backend)
.await
.map_err(|e| AppError::Config(format!("HID reload failed: {}", e)))?;
tracing::info!("HID backend reloaded successfully: {:?}", new_config.backend);
// When switching to OTG backend, automatically enable MSD if not already enabled
// OTG HID and MSD share the same USB gadget, so it makes sense to enable both
if new_config.backend == HidBackend::Otg && old_config.backend != HidBackend::Otg {
let msd_guard = state.msd.read().await;
if msd_guard.is_none() {
drop(msd_guard); // Release read lock before acquiring write lock
tracing::info!("OTG HID enabled, automatically initializing MSD...");
// Get MSD config from store
let config = state.config.get();
let msd = crate::msd::MsdController::new(
state.otg_service.clone(),
&config.msd.images_path,
&config.msd.drive_path,
);
if let Err(e) = msd.init().await {
tracing::warn!("Failed to auto-initialize MSD for OTG: {}", e);
} else {
let events = state.events.clone();
msd.set_event_bus(events).await;
*state.msd.write().await = Some(msd);
tracing::info!("MSD automatically initialized for OTG mode");
}
}
}
Ok(())
}
/// 应用 MSD 配置变更
pub async fn apply_msd_config(
state: &Arc<AppState>,
old_config: &MsdConfig,
new_config: &MsdConfig,
) -> Result<()> {
tracing::info!("MSD config sent, checking if reload needed...");
tracing::debug!("Old MSD config: {:?}", old_config);
tracing::debug!("New MSD config: {:?}", new_config);
// Check if MSD enabled state changed
let old_msd_enabled = old_config.enabled;
let new_msd_enabled = new_config.enabled;
tracing::info!("MSD enabled: old={}, new={}", old_msd_enabled, new_msd_enabled);
if old_msd_enabled != new_msd_enabled {
if new_msd_enabled {
// MSD was disabled, now enabled - need to initialize
tracing::info!("MSD enabled in config, initializing...");
let msd = crate::msd::MsdController::new(
state.otg_service.clone(),
&new_config.images_path,
&new_config.drive_path,
);
msd.init().await.map_err(|e| {
AppError::Config(format!("MSD initialization failed: {}", e))
})?;
// Set event bus
let events = state.events.clone();
msd.set_event_bus(events).await;
// Store the initialized controller
*state.msd.write().await = Some(msd);
tracing::info!("MSD initialized successfully");
} else {
// MSD was enabled, now disabled - shutdown
tracing::info!("MSD disabled in config, shutting down...");
if let Some(msd) = state.msd.write().await.as_mut() {
if let Err(e) = msd.shutdown().await {
tracing::warn!("MSD shutdown failed: {}", e);
}
}
*state.msd.write().await = None;
tracing::info!("MSD shutdown complete");
}
} else {
tracing::info!(
"MSD enabled state unchanged ({}), no reload needed",
new_msd_enabled
);
}
Ok(())
}
/// 应用 ATX 配置变更
pub async fn apply_atx_config(
state: &Arc<AppState>,
_old_config: &AtxConfig,
new_config: &AtxConfig,
) -> Result<()> {
tracing::info!("Applying ATX config changes...");
// Convert AtxConfig to AtxControllerConfig
let controller_config = new_config.to_controller_config();
// Reload the ATX controller with new configuration
let atx_guard = state.atx.read().await;
if let Some(atx) = atx_guard.as_ref() {
if let Err(e) = atx.reload(controller_config).await {
tracing::error!("ATX reload failed: {}", e);
return Err(AppError::Config(format!("ATX reload failed: {}", e)));
}
tracing::info!("ATX controller reloaded successfully");
} else {
// ATX controller not initialized, create a new one if enabled
drop(atx_guard);
if new_config.enabled {
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");
}
}
}
Ok(())
}
/// 应用 Audio 配置变更
pub async fn apply_audio_config(
state: &Arc<AppState>,
_old_config: &AudioConfig,
new_config: &AudioConfig,
) -> Result<()> {
tracing::info!("Applying audio config changes...");
// Create audio controller config from new config
let audio_config = crate::audio::AudioControllerConfig {
enabled: new_config.enabled,
device: new_config.device.clone(),
quality: crate::audio::AudioQuality::from_str(&new_config.quality),
};
// Update audio controller
if let Err(e) = state.audio.update_config(audio_config).await {
tracing::error!("Audio config update failed: {}", e);
// Don't fail - audio errors are not critical
} else {
tracing::info!(
"Audio config applied: enabled={}, device={}",
new_config.enabled,
new_config.device
);
}
// Also update WebRTC audio enabled state
if let Err(e) = 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);
}
// Reconnect audio sources for existing WebRTC sessions
if new_config.enabled {
state.stream_manager.reconnect_webrtc_audio_sources().await;
}
Ok(())
}

View File

@@ -0,0 +1,46 @@
//! ATX 配置 Handler
use axum::{extract::State, Json};
use std::sync::Arc;
use crate::config::AtxConfig;
use crate::error::Result;
use crate::state::AppState;
use super::apply::apply_atx_config;
use super::types::AtxConfigUpdate;
/// 获取 ATX 配置
pub async fn get_atx_config(State(state): State<Arc<AppState>>) -> Json<AtxConfig> {
Json(state.config.get().atx.clone())
}
/// 更新 ATX 配置
pub async fn update_atx_config(
State(state): State<Arc<AppState>>,
Json(req): Json<AtxConfigUpdate>,
) -> Result<Json<AtxConfig>> {
// 1. 验证请求
req.validate()?;
// 2. 获取旧配置
let old_atx_config = state.config.get().atx.clone();
// 3. 应用更新到配置存储
state
.config
.update(|config| {
req.apply_to(&mut config.atx);
})
.await?;
// 4. 获取新配置
let new_atx_config = state.config.get().atx.clone();
// 5. 应用到子系统(热重载)
if let Err(e) = apply_atx_config(&state, &old_atx_config, &new_atx_config).await {
tracing::error!("Failed to apply ATX config: {}", e);
}
Ok(Json(new_atx_config))
}

View File

@@ -0,0 +1,46 @@
//! Audio 配置 Handler
use axum::{extract::State, Json};
use std::sync::Arc;
use crate::config::AudioConfig;
use crate::error::Result;
use crate::state::AppState;
use super::apply::apply_audio_config;
use super::types::AudioConfigUpdate;
/// 获取 Audio 配置
pub async fn get_audio_config(State(state): State<Arc<AppState>>) -> Json<AudioConfig> {
Json(state.config.get().audio.clone())
}
/// 更新 Audio 配置
pub async fn update_audio_config(
State(state): State<Arc<AppState>>,
Json(req): Json<AudioConfigUpdate>,
) -> Result<Json<AudioConfig>> {
// 1. 验证请求
req.validate()?;
// 2. 获取旧配置
let old_audio_config = state.config.get().audio.clone();
// 3. 应用更新到配置存储
state
.config
.update(|config| {
req.apply_to(&mut config.audio);
})
.await?;
// 4. 获取新配置
let new_audio_config = state.config.get().audio.clone();
// 5. 应用到子系统(热重载)
if let Err(e) = apply_audio_config(&state, &old_audio_config, &new_audio_config).await {
tracing::error!("Failed to apply audio config: {}", e);
}
Ok(Json(new_audio_config))
}

View File

@@ -0,0 +1,46 @@
//! HID 配置 Handler
use axum::{extract::State, Json};
use std::sync::Arc;
use crate::config::HidConfig;
use crate::error::Result;
use crate::state::AppState;
use super::apply::apply_hid_config;
use super::types::HidConfigUpdate;
/// 获取 HID 配置
pub async fn get_hid_config(State(state): State<Arc<AppState>>) -> Json<HidConfig> {
Json(state.config.get().hid.clone())
}
/// 更新 HID 配置
pub async fn update_hid_config(
State(state): State<Arc<AppState>>,
Json(req): Json<HidConfigUpdate>,
) -> Result<Json<HidConfig>> {
// 1. 验证请求
req.validate()?;
// 2. 获取旧配置
let old_hid_config = state.config.get().hid.clone();
// 3. 应用更新到配置存储
state
.config
.update(|config| {
req.apply_to(&mut config.hid);
})
.await?;
// 4. 获取新配置
let new_hid_config = state.config.get().hid.clone();
// 5. 应用到子系统(热重载)
if let Err(e) = apply_hid_config(&state, &old_hid_config, &new_hid_config).await {
tracing::error!("Failed to apply HID config: {}", e);
}
Ok(Json(new_hid_config))
}

View File

@@ -0,0 +1,48 @@
//! 配置管理 Handler 模块
//!
//! 提供 RESTful 域分离的配置 API
//! - GET /api/config/video - 获取视频配置
//! - PATCH /api/config/video - 更新视频配置
//! - GET /api/config/stream - 获取流配置
//! - PATCH /api/config/stream - 更新流配置
//! - GET /api/config/hid - 获取 HID 配置
//! - PATCH /api/config/hid - 更新 HID 配置
//! - GET /api/config/msd - 获取 MSD 配置
//! - PATCH /api/config/msd - 更新 MSD 配置
//! - GET /api/config/atx - 获取 ATX 配置
//! - PATCH /api/config/atx - 更新 ATX 配置
//! - GET /api/config/audio - 获取音频配置
//! - PATCH /api/config/audio - 更新音频配置
mod apply;
mod types;
mod video;
mod stream;
mod hid;
mod msd;
mod atx;
mod audio;
// 导出 handler 函数
pub use video::{get_video_config, update_video_config};
pub use stream::{get_stream_config, update_stream_config};
pub use hid::{get_hid_config, update_hid_config};
pub use msd::{get_msd_config, update_msd_config};
pub use atx::{get_atx_config, update_atx_config};
pub use audio::{get_audio_config, update_audio_config};
// 保留全局配置查询(向后兼容)
use axum::{extract::State, Json};
use std::sync::Arc;
use crate::config::AppConfig;
use crate::state::AppState;
/// 获取完整配置
pub async fn get_all_config(State(state): State<Arc<AppState>>) -> Json<AppConfig> {
let mut config = (*state.config.get()).clone();
// 不暴露敏感信息
config.auth.totp_secret = None;
Json(config)
}

View File

@@ -0,0 +1,46 @@
//! MSD 配置 Handler
use axum::{extract::State, Json};
use std::sync::Arc;
use crate::config::MsdConfig;
use crate::error::Result;
use crate::state::AppState;
use super::apply::apply_msd_config;
use super::types::MsdConfigUpdate;
/// 获取 MSD 配置
pub async fn get_msd_config(State(state): State<Arc<AppState>>) -> Json<MsdConfig> {
Json(state.config.get().msd.clone())
}
/// 更新 MSD 配置
pub async fn update_msd_config(
State(state): State<Arc<AppState>>,
Json(req): Json<MsdConfigUpdate>,
) -> Result<Json<MsdConfig>> {
// 1. 验证请求
req.validate()?;
// 2. 获取旧配置
let old_msd_config = state.config.get().msd.clone();
// 3. 应用更新到配置存储
state
.config
.update(|config| {
req.apply_to(&mut config.msd);
})
.await?;
// 4. 获取新配置
let new_msd_config = state.config.get().msd.clone();
// 5. 应用到子系统(热重载)
if let Err(e) = apply_msd_config(&state, &old_msd_config, &new_msd_config).await {
tracing::error!("Failed to apply MSD config: {}", e);
}
Ok(Json(new_msd_config))
}

View File

@@ -0,0 +1,46 @@
//! Stream 配置 Handler
use axum::{extract::State, Json};
use std::sync::Arc;
use crate::error::Result;
use crate::state::AppState;
use super::apply::apply_stream_config;
use super::types::{StreamConfigResponse, StreamConfigUpdate};
/// 获取 Stream 配置
pub async fn get_stream_config(State(state): State<Arc<AppState>>) -> Json<StreamConfigResponse> {
let config = state.config.get();
Json(StreamConfigResponse::from(&config.stream))
}
/// 更新 Stream 配置
pub async fn update_stream_config(
State(state): State<Arc<AppState>>,
Json(req): Json<StreamConfigUpdate>,
) -> Result<Json<StreamConfigResponse>> {
// 1. 验证请求
req.validate()?;
// 2. 获取旧配置
let old_stream_config = state.config.get().stream.clone();
// 3. 应用更新到配置存储
state
.config
.update(|config| {
req.apply_to(&mut config.stream);
})
.await?;
// 4. 获取新配置
let new_stream_config = state.config.get().stream.clone();
// 5. 应用到子系统(热重载)
if let Err(e) = apply_stream_config(&state, &old_stream_config, &new_stream_config).await {
tracing::error!("Failed to apply stream config: {}", e);
}
Ok(Json(StreamConfigResponse::from(&new_stream_config)))
}

View File

@@ -0,0 +1,396 @@
use serde::Deserialize;
use typeshare::typeshare;
use crate::config::*;
use crate::error::AppError;
// ===== Video Config =====
#[typeshare]
#[derive(Debug, Deserialize)]
pub struct VideoConfigUpdate {
pub device: Option<String>,
pub format: Option<String>,
pub width: Option<u32>,
pub height: Option<u32>,
pub fps: Option<u32>,
pub quality: Option<u32>,
}
impl VideoConfigUpdate {
pub fn validate(&self) -> crate::error::Result<()> {
if let Some(width) = self.width {
if !(320..=7680).contains(&width) {
return Err(AppError::BadRequest("Invalid width: must be 320-7680".into()));
}
}
if let Some(height) = self.height {
if !(240..=4320).contains(&height) {
return Err(AppError::BadRequest("Invalid height: must be 240-4320".into()));
}
}
if let Some(fps) = self.fps {
if !(1..=120).contains(&fps) {
return Err(AppError::BadRequest("Invalid fps: must be 1-120".into()));
}
}
if let Some(quality) = self.quality {
if !(1..=100).contains(&quality) {
return Err(AppError::BadRequest("Invalid quality: must be 1-100".into()));
}
}
Ok(())
}
pub fn apply_to(&self, config: &mut VideoConfig) {
if let Some(ref device) = self.device {
config.device = Some(device.clone());
}
if let Some(ref format) = self.format {
config.format = Some(format.clone());
}
if let Some(width) = self.width {
config.width = width;
}
if let Some(height) = self.height {
config.height = height;
}
if let Some(fps) = self.fps {
config.fps = fps;
}
if let Some(quality) = self.quality {
config.quality = quality;
}
}
}
// ===== Stream Config =====
/// Stream 配置响应(包含 has_turn_password 字段)
#[typeshare]
#[derive(Debug, serde::Serialize)]
pub struct StreamConfigResponse {
pub mode: StreamMode,
pub encoder: EncoderType,
pub bitrate_kbps: u32,
pub gop_size: u32,
pub stun_server: Option<String>,
pub turn_server: Option<String>,
pub turn_username: Option<String>,
/// 指示是否已设置 TURN 密码(实际密码不返回)
pub has_turn_password: bool,
}
impl From<&StreamConfig> for StreamConfigResponse {
fn from(config: &StreamConfig) -> Self {
Self {
mode: config.mode.clone(),
encoder: config.encoder.clone(),
bitrate_kbps: config.bitrate_kbps,
gop_size: config.gop_size,
stun_server: config.stun_server.clone(),
turn_server: config.turn_server.clone(),
turn_username: config.turn_username.clone(),
has_turn_password: config.turn_password.is_some(),
}
}
}
#[typeshare]
#[derive(Debug, Deserialize)]
pub struct StreamConfigUpdate {
pub mode: Option<StreamMode>,
pub encoder: Option<EncoderType>,
pub bitrate_kbps: Option<u32>,
pub gop_size: Option<u32>,
/// STUN server URL (e.g., "stun:stun.l.google.com:19302")
pub stun_server: Option<String>,
/// TURN server URL (e.g., "turn:turn.example.com:3478")
pub turn_server: Option<String>,
/// TURN username
pub turn_username: Option<String>,
/// TURN password
pub turn_password: Option<String>,
}
impl StreamConfigUpdate {
pub fn validate(&self) -> crate::error::Result<()> {
if let Some(bitrate) = self.bitrate_kbps {
if !(1000..=15000).contains(&bitrate) {
return Err(AppError::BadRequest("Bitrate must be 1000-15000 kbps".into()));
}
}
if let Some(gop) = self.gop_size {
if !(10..=300).contains(&gop) {
return Err(AppError::BadRequest("GOP size must be 10-300".into()));
}
}
// Validate STUN server format
if let Some(ref stun) = self.stun_server {
if !stun.is_empty() && !stun.starts_with("stun:") {
return Err(AppError::BadRequest(
"STUN server must start with 'stun:' (e.g., stun:stun.l.google.com:19302)".into(),
));
}
}
// Validate TURN server format
if let Some(ref turn) = self.turn_server {
if !turn.is_empty() && !turn.starts_with("turn:") && !turn.starts_with("turns:") {
return Err(AppError::BadRequest(
"TURN server must start with 'turn:' or 'turns:' (e.g., turn:turn.example.com:3478)".into(),
));
}
}
Ok(())
}
pub fn apply_to(&self, config: &mut StreamConfig) {
if let Some(mode) = self.mode.clone() {
config.mode = mode;
}
if let Some(encoder) = self.encoder.clone() {
config.encoder = encoder;
}
if let Some(bitrate) = self.bitrate_kbps {
config.bitrate_kbps = bitrate;
}
if let Some(gop) = self.gop_size {
config.gop_size = gop;
}
// STUN/TURN settings - empty string means clear, Some("value") means set
if let Some(ref stun) = self.stun_server {
config.stun_server = if stun.is_empty() { None } else { Some(stun.clone()) };
}
if let Some(ref turn) = self.turn_server {
config.turn_server = if turn.is_empty() { None } else { Some(turn.clone()) };
}
if let Some(ref username) = self.turn_username {
config.turn_username = if username.is_empty() { None } else { Some(username.clone()) };
}
if let Some(ref password) = self.turn_password {
config.turn_password = if password.is_empty() { None } else { Some(password.clone()) };
}
}
}
// ===== HID Config =====
#[typeshare]
#[derive(Debug, Deserialize)]
pub struct HidConfigUpdate {
pub backend: Option<HidBackend>,
pub ch9329_port: Option<String>,
pub ch9329_baudrate: Option<u32>,
pub otg_udc: Option<String>,
pub mouse_absolute: Option<bool>,
}
impl HidConfigUpdate {
pub fn validate(&self) -> crate::error::Result<()> {
if let Some(baudrate) = self.ch9329_baudrate {
let valid_rates = [9600, 19200, 38400, 57600, 115200];
if !valid_rates.contains(&baudrate) {
return Err(AppError::BadRequest(
"Invalid baudrate: must be 9600, 19200, 38400, 57600, or 115200".into(),
));
}
}
Ok(())
}
pub fn apply_to(&self, config: &mut HidConfig) {
if let Some(backend) = self.backend.clone() {
config.backend = backend;
}
if let Some(ref port) = self.ch9329_port {
config.ch9329_port = port.clone();
}
if let Some(baudrate) = self.ch9329_baudrate {
config.ch9329_baudrate = baudrate;
}
if let Some(ref udc) = self.otg_udc {
config.otg_udc = Some(udc.clone());
}
if let Some(absolute) = self.mouse_absolute {
config.mouse_absolute = absolute;
}
}
}
// ===== MSD Config =====
#[typeshare]
#[derive(Debug, Deserialize)]
pub struct MsdConfigUpdate {
pub enabled: Option<bool>,
pub images_path: Option<String>,
pub drive_path: Option<String>,
pub virtual_drive_size_mb: Option<u32>,
}
impl MsdConfigUpdate {
pub fn validate(&self) -> crate::error::Result<()> {
if let Some(size) = self.virtual_drive_size_mb {
if !(1..=10240).contains(&size) {
return Err(AppError::BadRequest("Drive size must be 1-10240 MB".into()));
}
}
Ok(())
}
pub fn apply_to(&self, config: &mut MsdConfig) {
if let Some(enabled) = self.enabled {
config.enabled = enabled;
}
if let Some(ref path) = self.images_path {
config.images_path = path.clone();
}
if let Some(ref path) = self.drive_path {
config.drive_path = path.clone();
}
if let Some(size) = self.virtual_drive_size_mb {
config.virtual_drive_size_mb = size;
}
}
}
// ===== ATX Config =====
/// Update for a single ATX key configuration
#[typeshare]
#[derive(Debug, Deserialize)]
pub struct AtxKeyConfigUpdate {
pub driver: Option<crate::atx::AtxDriverType>,
pub device: Option<String>,
pub pin: Option<u32>,
pub active_level: Option<crate::atx::ActiveLevel>,
}
/// Update for LED sensing configuration
#[typeshare]
#[derive(Debug, Deserialize)]
pub struct AtxLedConfigUpdate {
pub enabled: Option<bool>,
pub gpio_chip: Option<String>,
pub gpio_pin: Option<u32>,
pub inverted: Option<bool>,
}
/// ATX configuration update request
#[typeshare]
#[derive(Debug, Deserialize)]
pub struct AtxConfigUpdate {
pub enabled: Option<bool>,
/// Power button configuration
pub power: Option<AtxKeyConfigUpdate>,
/// Reset button configuration
pub reset: Option<AtxKeyConfigUpdate>,
/// LED sensing configuration
pub led: Option<AtxLedConfigUpdate>,
/// Network interface for WOL packets (empty = auto)
pub wol_interface: Option<String>,
}
impl AtxConfigUpdate {
pub fn validate(&self) -> crate::error::Result<()> {
// Validate power key config if present
if let Some(ref power) = self.power {
Self::validate_key_config(power, "power")?;
}
// Validate reset key config if present
if let Some(ref reset) = self.reset {
Self::validate_key_config(reset, "reset")?;
}
Ok(())
}
fn validate_key_config(key: &AtxKeyConfigUpdate, name: &str) -> crate::error::Result<()> {
if let Some(ref device) = key.device {
if !device.is_empty() && !std::path::Path::new(device).exists() {
return Err(AppError::BadRequest(format!(
"{} device '{}' does not exist",
name, device
)));
}
}
Ok(())
}
pub fn apply_to(&self, config: &mut AtxConfig) {
if let Some(enabled) = self.enabled {
config.enabled = enabled;
}
if let Some(ref power) = self.power {
Self::apply_key_update(power, &mut config.power);
}
if let Some(ref reset) = self.reset {
Self::apply_key_update(reset, &mut config.reset);
}
if let Some(ref led) = self.led {
Self::apply_led_update(led, &mut config.led);
}
if let Some(ref wol_interface) = self.wol_interface {
config.wol_interface = wol_interface.clone();
}
}
fn apply_key_update(update: &AtxKeyConfigUpdate, config: &mut crate::atx::AtxKeyConfig) {
if let Some(driver) = update.driver {
config.driver = driver;
}
if let Some(ref device) = update.device {
config.device = device.clone();
}
if let Some(pin) = update.pin {
config.pin = pin;
}
if let Some(level) = update.active_level {
config.active_level = level;
}
}
fn apply_led_update(update: &AtxLedConfigUpdate, config: &mut crate::atx::AtxLedConfig) {
if let Some(enabled) = update.enabled {
config.enabled = enabled;
}
if let Some(ref chip) = update.gpio_chip {
config.gpio_chip = chip.clone();
}
if let Some(pin) = update.gpio_pin {
config.gpio_pin = pin;
}
if let Some(inverted) = update.inverted {
config.inverted = inverted;
}
}
}
// ===== Audio Config =====
#[typeshare]
#[derive(Debug, Deserialize)]
pub struct AudioConfigUpdate {
pub enabled: Option<bool>,
pub device: Option<String>,
pub quality: Option<String>,
}
impl AudioConfigUpdate {
pub fn validate(&self) -> crate::error::Result<()> {
if let Some(ref quality) = self.quality {
if !["voice", "balanced", "high"].contains(&quality.as_str()) {
return Err(AppError::BadRequest(
"Invalid quality: must be 'voice', 'balanced', or 'high'".into(),
));
}
}
Ok(())
}
pub fn apply_to(&self, config: &mut AudioConfig) {
if let Some(enabled) = self.enabled {
config.enabled = enabled;
}
if let Some(ref device) = self.device {
config.device = device.clone();
}
if let Some(ref quality) = self.quality {
config.quality = quality.clone();
}
}
}

View File

@@ -0,0 +1,47 @@
//! Video 配置 Handler
use axum::{extract::State, Json};
use std::sync::Arc;
use crate::config::VideoConfig;
use crate::error::Result;
use crate::state::AppState;
use super::apply::apply_video_config;
use super::types::VideoConfigUpdate;
/// 获取 Video 配置
pub async fn get_video_config(State(state): State<Arc<AppState>>) -> Json<VideoConfig> {
Json(state.config.get().video.clone())
}
/// 更新 Video 配置
pub async fn update_video_config(
State(state): State<Arc<AppState>>,
Json(req): Json<VideoConfigUpdate>,
) -> Result<Json<VideoConfig>> {
// 1. 验证请求
req.validate()?;
// 2. 获取旧配置
let old_video_config = state.config.get().video.clone();
// 3. 应用更新到配置存储
state
.config
.update(|config| {
req.apply_to(&mut config.video);
})
.await?;
// 4. 获取新配置
let new_video_config = state.config.get().video.clone();
// 5. 应用到子系统(热重载)
if let Err(e) = apply_video_config(&state, &old_video_config, &new_video_config).await {
tracing::error!("Failed to apply video config: {}", e);
// 根据用户选择,仅记录错误,不回滚
}
Ok(Json(new_video_config))
}

View File

@@ -0,0 +1,14 @@
//! Device discovery handlers
//!
//! Provides API endpoints for discovering available hardware devices.
use axum::Json;
use crate::atx::{discover_devices, AtxDevices};
/// GET /api/devices/atx - List available ATX devices
///
/// Returns lists of available GPIO chips and USB HID relay devices.
pub async fn list_atx_devices() -> Json<AtxDevices> {
Json(discover_devices())
}

View File

@@ -0,0 +1,352 @@
//! Extension management API handlers
use axum::{
extract::{Path, Query, State},
Json,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use typeshare::typeshare;
use crate::error::{AppError, Result};
use crate::extensions::{
EasytierConfig, EasytierInfo, ExtensionId, ExtensionInfo, ExtensionLogs,
ExtensionsStatus, GostcConfig, GostcInfo, TtydConfig, TtydInfo,
};
use crate::state::AppState;
// ============================================================================
// Get all extensions status
// ============================================================================
/// Get status of all extensions
/// GET /api/extensions
pub async fn list_extensions(State(state): State<Arc<AppState>>) -> Json<ExtensionsStatus> {
let config = state.config.get();
let mgr = &state.extensions;
Json(ExtensionsStatus {
ttyd: TtydInfo {
available: mgr.check_available(ExtensionId::Ttyd),
status: mgr.status(ExtensionId::Ttyd).await,
config: config.extensions.ttyd.clone(),
},
gostc: GostcInfo {
available: mgr.check_available(ExtensionId::Gostc),
status: mgr.status(ExtensionId::Gostc).await,
config: config.extensions.gostc.clone(),
},
easytier: EasytierInfo {
available: mgr.check_available(ExtensionId::Easytier),
status: mgr.status(ExtensionId::Easytier).await,
config: config.extensions.easytier.clone(),
},
})
}
// ============================================================================
// Individual extension status
// ============================================================================
/// Get status of a single extension
/// GET /api/extensions/:id
pub async fn get_extension(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
) -> Result<Json<ExtensionInfo>> {
let ext_id: ExtensionId = id
.parse()
.map_err(|_| AppError::NotFound(format!("Unknown extension: {}", id)))?;
let mgr = &state.extensions;
Ok(Json(ExtensionInfo {
available: mgr.check_available(ext_id),
status: mgr.status(ext_id).await,
}))
}
// ============================================================================
// Start/Stop extensions
// ============================================================================
/// Start an extension
/// POST /api/extensions/:id/start
pub async fn start_extension(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
) -> Result<Json<ExtensionInfo>> {
let ext_id: ExtensionId = id
.parse()
.map_err(|_| AppError::NotFound(format!("Unknown extension: {}", id)))?;
let config = state.config.get();
let mgr = &state.extensions;
// Start the extension
mgr.start(ext_id, &config.extensions)
.await
.map_err(|e| AppError::Internal(e))?;
// Return updated status
Ok(Json(ExtensionInfo {
available: mgr.check_available(ext_id),
status: mgr.status(ext_id).await,
}))
}
/// Stop an extension
/// POST /api/extensions/:id/stop
pub async fn stop_extension(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
) -> Result<Json<ExtensionInfo>> {
let ext_id: ExtensionId = id
.parse()
.map_err(|_| AppError::NotFound(format!("Unknown extension: {}", id)))?;
let mgr = &state.extensions;
// Stop the extension
mgr.stop(ext_id)
.await
.map_err(|e| AppError::Internal(e))?;
// Return updated status
Ok(Json(ExtensionInfo {
available: mgr.check_available(ext_id),
status: mgr.status(ext_id).await,
}))
}
// ============================================================================
// Extension logs
// ============================================================================
/// Query parameters for logs
#[derive(Deserialize, Default)]
pub struct LogsQuery {
/// Number of lines to return (default: 100, max: 500)
pub lines: Option<usize>,
}
/// Get extension logs
/// GET /api/extensions/:id/logs
pub async fn get_extension_logs(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
Query(params): Query<LogsQuery>,
) -> Result<Json<ExtensionLogs>> {
let ext_id: ExtensionId = id
.parse()
.map_err(|_| AppError::NotFound(format!("Unknown extension: {}", id)))?;
let lines = params.lines.unwrap_or(100).min(500);
let logs = state.extensions.logs(ext_id, lines).await;
Ok(Json(ExtensionLogs { id: ext_id, logs }))
}
// ============================================================================
// Update extension config
// ============================================================================
/// Update ttyd config
#[typeshare]
#[derive(Debug, Deserialize)]
pub struct TtydConfigUpdate {
pub enabled: Option<bool>,
pub port: Option<u16>,
pub shell: Option<String>,
pub credential: Option<String>,
}
/// Update gostc config
#[typeshare]
#[derive(Debug, Deserialize)]
pub struct GostcConfigUpdate {
pub enabled: Option<bool>,
pub addr: Option<String>,
pub key: Option<String>,
pub tls: Option<bool>,
}
/// Update easytier config
#[typeshare]
#[derive(Debug, Deserialize)]
pub struct EasytierConfigUpdate {
pub enabled: Option<bool>,
pub network_name: Option<String>,
pub network_secret: Option<String>,
pub peer_urls: Option<Vec<String>>,
pub virtual_ip: Option<String>,
}
/// Update ttyd configuration
/// PATCH /api/extensions/ttyd/config
pub async fn update_ttyd_config(
State(state): State<Arc<AppState>>,
Json(req): Json<TtydConfigUpdate>,
) -> Result<Json<TtydConfig>> {
// Get current config
let was_enabled = state.config.get().extensions.ttyd.enabled;
// Update config
state
.config
.update(|config| {
let ttyd = &mut config.extensions.ttyd;
if let Some(enabled) = req.enabled {
ttyd.enabled = enabled;
}
if let Some(port) = req.port {
ttyd.port = port;
}
if let Some(ref shell) = req.shell {
ttyd.shell = shell.clone();
}
if req.credential.is_some() {
ttyd.credential = req.credential.clone();
}
})
.await?;
let new_config = state.config.get();
let is_enabled = new_config.extensions.ttyd.enabled;
// Handle enable/disable state change
if was_enabled && !is_enabled {
// Was running, now disabled - stop it
state.extensions.stop(ExtensionId::Ttyd).await.ok();
} else if !was_enabled && is_enabled {
// Was disabled, now enabled - start it
if state.extensions.check_available(ExtensionId::Ttyd) {
state
.extensions
.start(ExtensionId::Ttyd, &new_config.extensions)
.await
.ok();
}
}
Ok(Json(new_config.extensions.ttyd.clone()))
}
/// Update gostc configuration
/// PATCH /api/extensions/gostc/config
pub async fn update_gostc_config(
State(state): State<Arc<AppState>>,
Json(req): Json<GostcConfigUpdate>,
) -> Result<Json<GostcConfig>> {
let was_enabled = state.config.get().extensions.gostc.enabled;
state
.config
.update(|config| {
let gostc = &mut config.extensions.gostc;
if let Some(enabled) = req.enabled {
gostc.enabled = enabled;
}
if let Some(ref addr) = req.addr {
gostc.addr = addr.clone();
}
if let Some(ref key) = req.key {
gostc.key = key.clone();
}
if let Some(tls) = req.tls {
gostc.tls = tls;
}
})
.await?;
let new_config = state.config.get();
let is_enabled = new_config.extensions.gostc.enabled;
let has_key = !new_config.extensions.gostc.key.is_empty();
if was_enabled && !is_enabled {
state.extensions.stop(ExtensionId::Gostc).await.ok();
} else if !was_enabled && is_enabled && has_key {
if state.extensions.check_available(ExtensionId::Gostc) {
state
.extensions
.start(ExtensionId::Gostc, &new_config.extensions)
.await
.ok();
}
}
Ok(Json(new_config.extensions.gostc.clone()))
}
/// Update easytier configuration
/// PATCH /api/extensions/easytier/config
pub async fn update_easytier_config(
State(state): State<Arc<AppState>>,
Json(req): Json<EasytierConfigUpdate>,
) -> Result<Json<EasytierConfig>> {
let was_enabled = state.config.get().extensions.easytier.enabled;
state
.config
.update(|config| {
let et = &mut config.extensions.easytier;
if let Some(enabled) = req.enabled {
et.enabled = enabled;
}
if let Some(ref name) = req.network_name {
et.network_name = name.clone();
}
if let Some(ref secret) = req.network_secret {
et.network_secret = secret.clone();
}
if let Some(ref peers) = req.peer_urls {
et.peer_urls = peers.clone();
}
if req.virtual_ip.is_some() {
et.virtual_ip = req.virtual_ip.clone();
}
})
.await?;
let new_config = state.config.get();
let is_enabled = new_config.extensions.easytier.enabled;
let has_name = !new_config.extensions.easytier.network_name.is_empty();
if was_enabled && !is_enabled {
state.extensions.stop(ExtensionId::Easytier).await.ok();
} else if !was_enabled && is_enabled && has_name {
if state.extensions.check_available(ExtensionId::Easytier) {
state
.extensions
.start(ExtensionId::Easytier, &new_config.extensions)
.await
.ok();
}
}
Ok(Json(new_config.extensions.easytier.clone()))
}
// ============================================================================
// Ttyd status for console (simplified)
// ============================================================================
/// Simple ttyd status for console view
#[typeshare]
#[derive(Debug, Serialize)]
pub struct TtydStatus {
pub available: bool,
pub running: bool,
}
/// Get ttyd status for console view
/// GET /api/extensions/ttyd/status
pub async fn get_ttyd_status(State(state): State<Arc<AppState>>) -> Json<TtydStatus> {
let mgr = &state.extensions;
let status = mgr.status(ExtensionId::Ttyd).await;
Json(TtydStatus {
available: mgr.check_available(ExtensionId::Ttyd),
running: status.is_running(),
})
}

2583
src/web/handlers/mod.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,239 @@
//! Terminal proxy handler - reverse proxy to ttyd via Unix socket
use axum::{
body::Body,
extract::{
ws::{Message as AxumMessage, WebSocket, WebSocketUpgrade},
OriginalUri, Path, State,
},
http::{Request, StatusCode},
response::Response,
};
use futures::{SinkExt, StreamExt};
use std::sync::Arc;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::UnixStream;
use tokio_tungstenite::tungstenite::{
client::IntoClientRequest,
http::HeaderValue,
Message as TungsteniteMessage,
};
use crate::error::AppError;
use crate::extensions::TTYD_SOCKET_PATH;
use crate::state::AppState;
/// Handle WebSocket upgrade for terminal
pub async fn terminal_ws(
State(_state): State<Arc<AppState>>,
OriginalUri(original_uri): OriginalUri,
ws: WebSocketUpgrade,
) -> Response {
let query_string = original_uri
.query()
.map(|q| format!("?{}", q))
.unwrap_or_default();
// Use the tty subprotocol that ttyd expects
ws.protocols(["tty"])
.on_upgrade(move |socket| handle_terminal_websocket(socket, query_string))
}
/// Handle terminal WebSocket connection - bridge browser and ttyd
async fn handle_terminal_websocket(client_ws: WebSocket, query_string: String) {
// Connect to ttyd Unix socket
let unix_stream = match UnixStream::connect(TTYD_SOCKET_PATH).await {
Ok(s) => s,
Err(e) => {
tracing::error!("Failed to connect to ttyd socket: {}", e);
return;
}
};
// Build WebSocket request for ttyd with tty subprotocol
let uri_str = format!("ws://localhost/api/terminal/ws{}", query_string);
let mut request = match uri_str.into_client_request() {
Ok(r) => r,
Err(e) => {
tracing::error!("Failed to create WebSocket request: {}", e);
return;
}
};
request.headers_mut().insert(
"Sec-WebSocket-Protocol",
HeaderValue::from_static("tty"),
);
// Create WebSocket connection to ttyd
let ws_stream = match tokio_tungstenite::client_async(request, unix_stream).await {
Ok((ws, _)) => ws,
Err(e) => {
tracing::error!("Failed to establish WebSocket with ttyd: {}", e);
return;
}
};
// Split both WebSocket connections
let (mut client_tx, mut client_rx) = client_ws.split();
let (mut ttyd_tx, mut ttyd_rx) = ws_stream.split();
// Forward messages from browser to ttyd
let client_to_ttyd = tokio::spawn(async move {
while let Some(msg) = client_rx.next().await {
let ttyd_msg = match msg {
Ok(AxumMessage::Text(text)) => TungsteniteMessage::Text(text),
Ok(AxumMessage::Binary(data)) => TungsteniteMessage::Binary(data),
Ok(AxumMessage::Ping(data)) => TungsteniteMessage::Ping(data),
Ok(AxumMessage::Pong(data)) => TungsteniteMessage::Pong(data),
Ok(AxumMessage::Close(_)) => {
let _ = ttyd_tx.send(TungsteniteMessage::Close(None)).await;
break;
}
Err(_) => break,
};
if ttyd_tx.send(ttyd_msg).await.is_err() {
break;
}
}
});
// Forward messages from ttyd to browser
let ttyd_to_client = tokio::spawn(async move {
while let Some(msg) = ttyd_rx.next().await {
let client_msg = match msg {
Ok(TungsteniteMessage::Text(text)) => AxumMessage::Text(text),
Ok(TungsteniteMessage::Binary(data)) => AxumMessage::Binary(data),
Ok(TungsteniteMessage::Ping(data)) => AxumMessage::Ping(data),
Ok(TungsteniteMessage::Pong(data)) => AxumMessage::Pong(data),
Ok(TungsteniteMessage::Close(_)) => {
let _ = client_tx.send(AxumMessage::Close(None)).await;
break;
}
Ok(TungsteniteMessage::Frame(_)) => continue,
Err(_) => break,
};
if client_tx.send(client_msg).await.is_err() {
break;
}
}
});
// Wait for either direction to complete
tokio::select! {
_ = client_to_ttyd => {}
_ = ttyd_to_client => {}
}
}
/// Proxy HTTP requests to ttyd
pub async fn terminal_proxy(
State(_state): State<Arc<AppState>>,
path: Option<Path<String>>,
req: Request<Body>,
) -> Result<Response, AppError> {
let path_str = path.map(|p| p.0).unwrap_or_default();
// Connect to ttyd Unix socket
let mut unix_stream = UnixStream::connect(TTYD_SOCKET_PATH)
.await
.map_err(|e| AppError::ServiceUnavailable(format!("ttyd not running: {}", e)))?;
// Build HTTP request to forward
let method = req.method().as_str();
let query = req.uri().query().map(|q| format!("?{}", q)).unwrap_or_default();
let uri_path = if path_str.is_empty() {
format!("/api/terminal/{}", query)
} else {
format!("/api/terminal/{}{}", path_str, query)
};
// Forward relevant headers
let mut headers_str = String::new();
for (name, value) in req.headers() {
if let Ok(v) = value.to_str() {
let name_lower = name.as_str().to_lowercase();
if !matches!(
name_lower.as_str(),
"connection" | "keep-alive" | "transfer-encoding" | "upgrade"
) {
headers_str.push_str(&format!("{}: {}\r\n", name, v));
}
}
}
let http_request = format!(
"{} {} HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n{}\r\n",
method, uri_path, headers_str
);
// Send request
unix_stream
.write_all(http_request.as_bytes())
.await
.map_err(|e| AppError::Internal(format!("Failed to send request: {}", e)))?;
// Read response
let mut response_buf = Vec::new();
unix_stream
.read_to_end(&mut response_buf)
.await
.map_err(|e| AppError::Internal(format!("Failed to read response: {}", e)))?;
// Parse HTTP response
let response_str = String::from_utf8_lossy(&response_buf);
let header_end = response_str
.find("\r\n\r\n")
.ok_or_else(|| AppError::Internal("Invalid HTTP response".to_string()))?;
let headers_part = &response_str[..header_end];
let body_start = header_end + 4;
// Parse status line
let status_line = headers_part
.lines()
.next()
.ok_or_else(|| AppError::Internal("Missing status line".to_string()))?;
let status_code: u16 = status_line
.split_whitespace()
.nth(1)
.and_then(|s| s.parse().ok())
.unwrap_or(200);
// Build response
let mut builder = Response::builder().status(StatusCode::from_u16(status_code).unwrap_or(StatusCode::OK));
// Forward response headers
for line in headers_part.lines().skip(1) {
if let Some((name, value)) = line.split_once(':') {
let name = name.trim();
let value = value.trim();
if !matches!(
name.to_lowercase().as_str(),
"connection" | "keep-alive" | "transfer-encoding"
) {
builder = builder.header(name, value);
}
}
}
let body = if body_start < response_buf.len() {
Body::from(response_buf[body_start..].to_vec())
} else {
Body::empty()
};
builder
.body(body)
.map_err(|e| AppError::Internal(format!("Failed to build response: {}", e)))
}
/// Terminal index page
pub async fn terminal_index(
State(state): State<Arc<AppState>>,
req: Request<Body>,
) -> Result<Response, AppError> {
terminal_proxy(State(state), None, req).await
}