diff --git a/src/db/mod.rs b/src/db/mod.rs index b2935c58..868805a2 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,3 +1,36 @@ mod pool; +use std::path::Path; + +use crate::error::Result; + pub use pool::DatabasePool; + +/// Open the application database stored in `data_dir` and ensure its schema exists. +pub async fn open_database_pool(data_dir: &Path) -> Result { + let db = DatabasePool::new(&data_dir.join("one-kvm.db")).await?; + db.init_schema().await?; + Ok(db) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn open_database_pool_creates_data_dir_and_initializes_schema() { + let temp_dir = tempfile::tempdir().unwrap(); + let data_dir = temp_dir.path().join("nested").join("data"); + + let db = open_database_pool(&data_dir).await.unwrap(); + + assert!(data_dir.join("one-kvm.db").is_file()); + let users_table: Option = sqlx::query_scalar( + "SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'users'", + ) + .fetch_optional(db.pool()) + .await + .unwrap(); + assert_eq!(users_table.as_deref(), Some("users")); + } +} diff --git a/src/main.rs b/src/main.rs index 84d3fa9f..897b9f3d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ use std::collections::HashSet; use std::future::Future; use std::io::Write; use std::net::{IpAddr, SocketAddr}; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use axum_server::tls_rustls::RustlsConfig; use clap::{Args, Parser, Subcommand, ValueEnum}; @@ -12,7 +12,7 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; use one_kvm::auth::{SessionStore, TwoFactorService, UserStore}; use one_kvm::config; -use one_kvm::db::DatabasePool; +use one_kvm::db::open_database_pool; use one_kvm::platform::PlatformCapabilities; use one_kvm::runtime::{RuntimeBuilder, WebConfigOverrides}; use one_kvm::state::ShutdownAction; @@ -298,13 +298,6 @@ async fn shutdown_signal() -> anyhow::Result<()> { Ok(()) } -async fn open_database_pool(data_dir: &Path) -> anyhow::Result { - let db_path = data_dir.join("one-kvm.db"); - let db = DatabasePool::new(&db_path).await?; - db.init_schema().await?; - Ok(db) -} - async fn run_servers_until_shutdown( mut servers: FuturesUnordered, shutdown_signal: impl Future, @@ -348,7 +341,6 @@ fn restart_current_process(exe_path: Option) -> anyhow::Result<()> { } async fn run_cli_command(command: CliCommand, data_dir: PathBuf) -> anyhow::Result<()> { - tokio::fs::create_dir_all(&data_dir).await?; let db = open_database_pool(&data_dir).await?; let users = UserStore::new(db.clone_pool()); let two_factor = TwoFactorService::new(db.clone_pool()); diff --git a/src/msd/controller.rs b/src/msd/controller.rs index 6c7fff2b..027ca973 100644 --- a/src/msd/controller.rs +++ b/src/msd/controller.rs @@ -62,12 +62,8 @@ impl MsdController { ), } - if let Err(e) = std::fs::create_dir_all(&self.images_path) { - warn!("Failed to create images directory: {}", e); - } - if let Err(e) = std::fs::create_dir_all(&self.ventoy_dir) { - warn!("Failed to create ventoy directory: {}", e); - } + tokio::fs::create_dir_all(&self.images_path).await?; + tokio::fs::create_dir_all(&self.ventoy_dir).await?; info!("Fetching MSD function from OtgService"); let msd_func = self diff --git a/src/runtime/builder.rs b/src/runtime/builder.rs index d576adf2..95e453aa 100644 --- a/src/runtime/builder.rs +++ b/src/runtime/builder.rs @@ -8,7 +8,7 @@ use crate::audio::{AudioController, AudioControllerConfig, AudioQuality}; use crate::auth::{SessionStore, TwoFactorService, UserStore}; use crate::computer_use::ComputerUseManager; use crate::config::{self, AppConfig, ConfigStore}; -use crate::db::DatabasePool; +use crate::db::{open_database_pool, DatabasePool}; use crate::events::EventBus; use crate::extensions::ExtensionManager; use crate::hid::{HidBackendType, HidController}; @@ -126,8 +126,6 @@ impl RuntimeBuilder { } } - connect_capture_to_webrtc(&streamer, &webrtc).await; - let stream_manager = VideoStreamManager::with_webrtc_streamer( streamer.clone(), webrtc.clone() as Arc, @@ -226,23 +224,19 @@ impl ApplicationRuntime { async fn load_runtime_config( data_dir: &Path, ) -> anyhow::Result<(DatabasePool, ConfigStore, AppConfig)> { - tokio::fs::create_dir_all(data_dir).await?; - - let db_path = data_dir.join("one-kvm.db"); - let db = DatabasePool::new(&db_path).await?; - db.init_schema().await?; + let db = open_database_pool(data_dir).await?; let config_store = ConfigStore::new(db.clone_pool())?; config_store.load().await?; let mut config = (*config_store.get()).clone(); config.apply_platform_defaults(); - prepare_linux_runtime_dirs(data_dir, &config_store, &mut config).await?; + normalize_msd_config(data_dir, &config_store, &mut config).await?; Ok((db, config_store, config)) } #[cfg(unix)] -async fn prepare_linux_runtime_dirs( +async fn normalize_msd_config( data_dir: &Path, config_store: &ConfigStore, config: &mut AppConfig, @@ -263,19 +257,11 @@ async fn prepare_linux_runtime_dirs( if msd_dir_updated { config_store.set(config.clone()).await?; } - - let msd_dir = PathBuf::from(&config.msd.msd_dir); - if let Err(error) = tokio::fs::create_dir_all(msd_dir.join("images")).await { - tracing::warn!("Failed to create MSD images directory: {}", error); - } - if let Err(error) = tokio::fs::create_dir_all(msd_dir.join("ventoy")).await { - tracing::warn!("Failed to create MSD ventoy directory: {}", error); - } Ok(()) } #[cfg(not(unix))] -async fn prepare_linux_runtime_dirs( +async fn normalize_msd_config( _data_dir: &Path, _config_store: &ConfigStore, _config: &mut AppConfig, @@ -499,27 +485,6 @@ async fn build_audio(config: &AppConfig, events: &Arc) -> Arc, webrtc: &Arc) { - let (device_path, resolution, format, fps, jpeg_quality) = - streamer.current_capture_config().await; - tracing::debug!( - "Initial video config: {}x{} {:?} @ {}fps", - resolution.width, - resolution.height, - format, - fps - ); - webrtc.update_video_config(resolution, format, fps).await; - if let Some(device_path) = device_path { - webrtc - .set_capture_device(device_path, jpeg_quality, streamer.current_device().await) - .await; - tracing::debug!("WebRTC streamer configured for direct capture"); - } else { - tracing::warn!("No capture device configured for WebRTC"); - } -} - async fn connect_audio_recovery( audio: &Arc, stream_manager: &Arc, @@ -595,4 +560,25 @@ mod tests { assert_eq!(config.web.https_port, original_https_port); assert!(config.web.https_enabled); } + + #[cfg(unix)] + #[tokio::test] + async fn normalizing_disabled_msd_does_not_create_module_directories() { + let temp_dir = tempfile::tempdir().unwrap(); + let data_dir = temp_dir.path().join("data"); + let msd_dir = temp_dir.path().join("disabled-msd"); + let db = open_database_pool(&data_dir).await.unwrap(); + let config_store = ConfigStore::new(db.clone_pool()).unwrap(); + config_store.load().await.unwrap(); + let mut config = (*config_store.get()).clone(); + config.msd.enabled = false; + config.msd.msd_dir = msd_dir.to_string_lossy().into_owned(); + + normalize_msd_config(&data_dir, &config_store, &mut config) + .await + .unwrap(); + + assert!(!msd_dir.join("images").exists()); + assert!(!msd_dir.join("ventoy").exists()); + } } diff --git a/src/runtime/usb.rs b/src/runtime/usb.rs index 2dec7d4a..1c3db5c7 100644 --- a/src/runtime/usb.rs +++ b/src/runtime/usb.rs @@ -258,14 +258,6 @@ impl UsbCoordinator { let inquiry_changed = old_config.flash_inquiry_string != new_config.flash_inquiry_string || old_config.cdrom_inquiry_string != new_config.cdrom_inquiry_string; - let msd_dir = new_config.msd_dir_path(); - if let Err(error) = std::fs::create_dir_all(msd_dir.join("images")) { - tracing::warn!("Failed to create MSD images directory: {}", error); - } - if let Err(error) = std::fs::create_dir_all(msd_dir.join("ventoy")) { - tracing::warn!("Failed to create MSD ventoy directory: {}", error); - } - if !options.force && old_enabled == new_enabled && !directory_changed && !inquiry_changed { tracing::info!("MSD configuration unchanged, no reload needed"); return Ok(()); diff --git a/src/video/stream_manager.rs b/src/video/stream_manager.rs index 57343c94..761a90c6 100644 --- a/src/video/stream_manager.rs +++ b/src/video/stream_manager.rs @@ -195,25 +195,15 @@ impl VideoStreamManager { info!("Initializing video stream manager with mode: {:?}", mode); *self.mode.write().await = mode.clone(); - // Check if streamer is already initialized (capturer exists) - let needs_init = self.streamer.state().await == StreamerState::Uninitialized; + // A failed fixed-device configuration can leave the streamer in a transient + // state without a capture device. Treat that the same as an uninitialized + // streamer so the advertised auto-detection fallback actually runs. + let state = self.streamer.state().await; + let (device_path, _, _, _, _) = self.streamer.current_capture_config().await; + let needs_init = state == StreamerState::Uninitialized || device_path.is_none(); if needs_init { - match mode { - StreamMode::Mjpeg => { - // Initialize MJPEG streamer - if let Err(e) = self.streamer.init_auto().await { - warn!("Failed to auto-initialize MJPEG streamer: {}", e); - } - } - StreamMode::WebRTC => { - // WebRTC is initialized on-demand when clients connect - // But we still need to initialize the video capture - if let Err(e) = self.streamer.init_auto().await { - warn!("Failed to auto-initialize video capture for WebRTC: {}", e); - } - } - } + self.streamer.init_auto().await?; } self.sync_webrtc_capture_source("after init").await;