Add Phase 5 enhancements: security, i18n, analysis, backup, notifications
- Database v8 migration: tags, pinned, avg_startup_ms columns - Security scanning with CVE matching and batch scan - Bundled library extraction and vulnerability reports - Desktop notification system for security alerts - Backup/restore system for AppImage configurations - i18n framework with gettext support - Runtime analysis and Wayland compatibility detection - AppStream metadata and Flatpak-style build support - File watcher module for live directory monitoring - Preferences panel with GSettings integration - CLI interface for headless operation - Detail view: tabbed layout with ViewSwitcher in title bar, health score, sandbox controls, changelog links - Library view: sort dropdown, context menu enhancements - Dashboard: system status, disk usage, launch history - Security report page with scan and export - Packaging: meson build, PKGBUILD, metainfo
This commit is contained in:
133
src/core/analysis.rs
Normal file
133
src/core/analysis.rs
Normal file
@@ -0,0 +1,133 @@
|
||||
use std::path::PathBuf;
|
||||
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);
|
||||
|
||||
/// 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.
|
||||
struct AnalysisGuard;
|
||||
|
||||
impl Drop for AnalysisGuard {
|
||||
fn drop(&mut self) {
|
||||
RUNNING_ANALYSES.fetch_sub(1, Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
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 {
|
||||
std::thread::sleep(std::time::Duration::from_millis(200));
|
||||
}
|
||||
}
|
||||
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, etc.)
|
||||
if let Ok(meta) = inspector::inspect_appimage(&path, &appimage_type) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// 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(&rec) {
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user