use std::path::PathBuf; use std::sync::{Condvar, Mutex}; use std::sync::atomic::{AtomicUsize, Ordering}; use crate::core::database::Database; use crate::core::discovery::AppImageType; use crate::core::fuse; use crate::core::inspector; use crate::core::integrator; use crate::core::wayland; /// Maximum number of concurrent background analyses. const MAX_CONCURRENT_ANALYSES: usize = 2; /// Counter for currently running analyses. static RUNNING_ANALYSES: AtomicUsize = AtomicUsize::new(0); /// Condvar to efficiently wait for a slot instead of busy-polling. static SLOT_AVAILABLE: (Mutex<()>, Condvar) = (Mutex::new(()), Condvar::new()); /// Returns the number of currently running background analyses. pub fn running_count() -> usize { RUNNING_ANALYSES.load(Ordering::Relaxed) } /// RAII guard that decrements the analysis counter on drop and notifies waiters. struct AnalysisGuard; impl Drop for AnalysisGuard { fn drop(&mut self) { RUNNING_ANALYSES.fetch_sub(1, Ordering::Release); SLOT_AVAILABLE.1.notify_one(); } } /// Run the heavy analysis steps for a single AppImage on a background thread. /// /// This opens its own database connection and updates results as they complete. /// All errors are logged but non-fatal - fields stay `None`, which the UI /// already handles gracefully. /// /// Blocks until a slot is available if the concurrency limit is reached. pub fn run_background_analysis(id: i64, path: PathBuf, appimage_type: AppImageType, integrate: bool) { // Wait for a slot to become available using condvar instead of busy-polling loop { let current = RUNNING_ANALYSES.load(Ordering::Acquire); if current < MAX_CONCURRENT_ANALYSES { if RUNNING_ANALYSES.compare_exchange(current, current + 1, Ordering::AcqRel, Ordering::Relaxed).is_ok() { break; } } else { let lock = SLOT_AVAILABLE.0.lock().unwrap(); let _ = SLOT_AVAILABLE.1.wait_timeout(lock, std::time::Duration::from_secs(1)); } } let _guard = AnalysisGuard; let db = match Database::open() { Ok(db) => db, Err(e) => { log::error!("Background analysis: failed to open database: {}", e); return; } }; if let Err(e) = db.update_analysis_status(id, "analyzing") { log::warn!("Failed to set analysis status to 'analyzing' for id {}: {}", id, e); } // Inspect metadata (app name, version, icon, desktop entry, AppStream, etc.) if let Ok(meta) = inspector::inspect_appimage(&path, &appimage_type) { log::debug!( "Metadata for id={}: name={:?}, icon_name={:?}", id, meta.app_name.as_deref(), meta.icon_name.as_deref(), ); let categories = if meta.categories.is_empty() { None } else { Some(meta.categories.join(";")) }; if let Err(e) = db.update_metadata( id, meta.app_name.as_deref(), meta.app_version.as_deref(), meta.description.as_deref(), meta.developer.as_deref(), categories.as_deref(), meta.architecture.as_deref(), meta.cached_icon_path .as_ref() .map(|p| p.to_string_lossy()) .as_deref(), Some(&meta.desktop_entry_content), ) { log::warn!("Failed to update metadata for id {}: {}", id, e); } // Store extended metadata from AppStream XML and desktop entry let keywords = if meta.keywords.is_empty() { None } else { Some(meta.keywords.join(",")) }; let mime_types = if meta.mime_types.is_empty() { None } else { Some(meta.mime_types.join(";")) }; let release_json = if meta.releases.is_empty() { None } else { let releases: Vec = meta .releases .iter() .map(|r| { serde_json::json!({ "version": r.version, "date": r.date, "description": r.description, }) }) .collect(); Some(serde_json::to_string(&releases).unwrap_or_default()) }; let actions_json = if meta.desktop_actions.is_empty() { None } else { Some(serde_json::to_string(&meta.desktop_actions).unwrap_or_default()) }; let screenshot_urls_str = if meta.screenshot_urls.is_empty() { None } else { Some(meta.screenshot_urls.join("\n")) }; if let Err(e) = db.update_appstream_metadata( id, meta.appstream_id.as_deref(), meta.appstream_description.as_deref(), meta.generic_name.as_deref(), meta.license.as_deref(), meta.homepage_url.as_deref(), meta.bugtracker_url.as_deref(), meta.donation_url.as_deref(), meta.help_url.as_deref(), meta.vcs_url.as_deref(), keywords.as_deref(), mime_types.as_deref(), meta.content_rating.as_deref(), meta.project_group.as_deref(), release_json.as_deref(), actions_json.as_deref(), meta.has_signature, screenshot_urls_str.as_deref(), ) { log::warn!("Failed to update appstream metadata for id {}: {}", id, e); } } // FUSE status let fuse_info = fuse::detect_system_fuse(); let app_fuse = fuse::determine_app_fuse_status(&fuse_info, &path); if let Err(e) = db.update_fuse_status(id, app_fuse.as_str()) { log::warn!("Failed to update FUSE status for id {}: {}", id, e); } // Wayland status let analysis = wayland::analyze_appimage(&path); if let Err(e) = db.update_wayland_status(id, analysis.status.as_str()) { log::warn!("Failed to update Wayland status for id {}: {}", id, e); } // SHA256 hash if let Ok(hash) = crate::core::discovery::compute_sha256(&path) { if let Err(e) = db.update_sha256(id, &hash) { log::warn!("Failed to update SHA256 for id {}: {}", id, e); } } // Footprint discovery if let Ok(Some(rec)) = db.get_appimage_by_id(id) { crate::core::footprint::discover_and_store(&db, id, &rec); // Integrate if requested if integrate { match integrator::integrate_tracked(&rec, &db) { Ok(result) => { let desktop_path = result.desktop_file_path.to_string_lossy().to_string(); if let Err(e) = db.set_integrated(id, true, Some(&desktop_path)) { log::warn!("Failed to set integration status for id {}: {}", id, e); } } Err(e) => { log::error!("Integration failed for id {}: {}", id, e); } } } } if let Err(e) = db.update_analysis_status(id, "complete") { log::warn!("Failed to set analysis status to 'complete' for id {}: {}", id, e); } // _guard dropped here, decrementing RUNNING_ANALYSES }