use crate::error::VmSpectError;
use crate::models::image::{ImageInfo, Stats};
use crate::models::partition::{OperatingSystem, Partition, PartitionScheme};
use crate::models::software::{GuestInfo, GuestTools, Program};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicU8, AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct CancellationToken {
inner: Arc<AtomicBool>,
}
impl Default for CancellationToken {
fn default() -> Self {
Self::new()
}
}
impl CancellationToken {
pub fn new() -> Self {
Self {
inner: Arc::new(AtomicBool::new(false)),
}
}
pub fn from_arc(inner: Arc<AtomicBool>) -> Self {
Self { inner }
}
pub fn cancel(&self) {
self.inner.store(true, Ordering::Release);
}
pub fn is_cancelled(&self) -> bool {
self.inner.load(Ordering::Acquire)
}
pub fn as_arc(&self) -> &Arc<AtomicBool> {
&self.inner
}
pub fn clone_arc(&self) -> Arc<AtomicBool> {
self.inner.clone()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ProgressSnapshot {
pub percentage: u8,
pub stage_id: u8,
pub completed_tasks: usize,
pub total_tasks: usize,
pub bytes_processed: u64,
pub total_bytes: u64,
pub cancelled: bool,
}
#[derive(Debug)]
pub struct InspectionProgress {
pub total_tasks: AtomicUsize,
pub completed_tasks: AtomicUsize,
pub bytes_processed: AtomicU64,
pub total_bytes: AtomicU64,
pub percentage: AtomicU8,
pub stage_id: AtomicU8,
pub cancelled: AtomicBool,
cancellation_token: Option<Arc<AtomicBool>>,
}
impl Default for InspectionProgress {
fn default() -> Self {
Self::new()
}
}
impl InspectionProgress {
pub fn new() -> Self {
Self {
total_tasks: AtomicUsize::new(0),
completed_tasks: AtomicUsize::new(0),
bytes_processed: AtomicU64::new(0),
total_bytes: AtomicU64::new(0),
percentage: AtomicU8::new(0),
stage_id: AtomicU8::new(0),
cancelled: AtomicBool::new(false),
cancellation_token: None,
}
}
pub fn with_cancellation_token(token: Option<&Arc<AtomicBool>>) -> Self {
let cancelled = token
.map(|token| token.load(Ordering::Acquire))
.unwrap_or(false);
Self {
total_tasks: AtomicUsize::new(0),
completed_tasks: AtomicUsize::new(0),
bytes_processed: AtomicU64::new(0),
total_bytes: AtomicU64::new(0),
percentage: AtomicU8::new(0),
stage_id: AtomicU8::new(0),
cancelled: AtomicBool::new(cancelled),
cancellation_token: token.cloned(),
}
}
#[inline]
fn task_completion_percentage(completed: usize, total: usize) -> u8 {
if total == 0 {
return 0;
}
((completed.min(total) as u128 * 100) / total as u128) as u8
}
#[inline]
pub fn completion_percentage(&self) -> f32 {
let total = self.total_tasks.load(Ordering::Relaxed);
if total == 0 {
return self.percentage.load(Ordering::Relaxed) as f32;
}
let completed = self.completed_tasks.load(Ordering::Relaxed);
if completed >= total {
100.0
} else {
(((completed as f64 / total as f64) * 100.0).min(99.999_99)) as f32
}
}
#[inline]
pub fn is_cancelled(&self) -> bool {
self.cancelled.load(Ordering::Acquire)
|| self
.cancellation_token
.as_ref()
.map(|token| token.load(Ordering::Acquire))
.unwrap_or(false)
}
#[inline]
pub fn cancel(&self) {
self.cancelled.store(true, Ordering::Release);
if let Some(token) = &self.cancellation_token {
token.store(true, Ordering::Release);
}
}
#[inline]
pub fn set_percentage(&self, pct: u8) {
self.percentage.store(pct.min(100), Ordering::Relaxed);
}
#[inline]
pub fn set_stage_id(&self, stage: u8) {
self.stage_id.store(stage, Ordering::Relaxed);
}
#[inline]
pub fn stage_id(&self) -> u8 {
self.stage_id.load(Ordering::Relaxed)
}
#[inline]
pub fn add_bytes_processed(&self, bytes: u64) {
self.bytes_processed.fetch_add(bytes, Ordering::Relaxed);
}
#[inline]
pub fn bytes_processed(&self) -> u64 {
self.bytes_processed.load(Ordering::Relaxed)
}
#[inline]
pub fn set_total_bytes(&self, total: u64) {
self.total_bytes.store(total, Ordering::Relaxed);
}
#[inline]
pub fn total_bytes(&self) -> u64 {
self.total_bytes.load(Ordering::Relaxed)
}
pub(crate) fn reset_for_batch(&self, total: usize) {
self.completed_tasks.store(0, Ordering::Relaxed);
self.bytes_processed.store(0, Ordering::Relaxed);
self.total_bytes.store(0, Ordering::Relaxed);
self.percentage.store(0, Ordering::Relaxed);
self.stage_id.store(0, Ordering::Relaxed);
self.cancelled.store(false, Ordering::Release);
self.total_tasks.store(total, Ordering::Relaxed);
}
#[inline]
pub fn set_total_tasks(&self, total: usize) {
self.total_tasks.store(total, Ordering::Relaxed);
}
#[inline]
pub fn total_tasks(&self) -> usize {
self.total_tasks.load(Ordering::Relaxed)
}
#[inline]
pub fn increment_completed_tasks(&self) {
let completed = self
.completed_tasks
.fetch_add(1, Ordering::Relaxed)
.saturating_add(1);
let total = self.total_tasks.load(Ordering::Relaxed);
if total > 0 {
self.percentage.store(
Self::task_completion_percentage(completed, total),
Ordering::Relaxed,
);
}
}
#[inline]
pub fn completed_tasks(&self) -> usize {
self.completed_tasks.load(Ordering::Relaxed)
}
pub fn snapshot(&self) -> ProgressSnapshot {
let total_tasks = self.total_tasks.load(Ordering::Relaxed);
let completed_tasks = self.completed_tasks.load(Ordering::Relaxed);
let percentage = if total_tasks > 0 {
Self::task_completion_percentage(completed_tasks, total_tasks)
} else {
self.percentage.load(Ordering::Relaxed)
};
ProgressSnapshot {
percentage,
stage_id: self.stage_id.load(Ordering::Relaxed),
completed_tasks,
total_tasks,
bytes_processed: self.bytes_processed.load(Ordering::Relaxed),
total_bytes: self.total_bytes.load(Ordering::Relaxed),
cancelled: self.is_cancelled(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Options {
pub no_apps: bool,
pub no_system: bool,
pub include_system: bool,
pub qemu_nbd: Option<PathBuf>,
pub chunk_size: Option<u64>,
pub force_nbd: bool,
pub unix_socket: Option<PathBuf>,
pub extra_nbd_args: Vec<String>,
pub connection_timeout: Option<Duration>,
pub nbd_persistent: bool,
#[serde(skip)]
pub cancel_token: Option<Arc<AtomicBool>>,
pub nbd_max_sessions: usize,
}
pub type InspectionOptions = Options;
impl Default for Options {
fn default() -> Self {
Self {
no_apps: false,
no_system: false,
include_system: false,
qemu_nbd: None,
chunk_size: None,
force_nbd: false,
unix_socket: None,
extra_nbd_args: Vec::new(),
connection_timeout: None,
nbd_persistent: false,
cancel_token: None,
nbd_max_sessions: 2,
}
}
}
impl Options {
#[inline]
pub fn should_analyze_apps(&self) -> bool {
!self.no_apps
}
#[inline]
pub fn should_analyze_system(&self) -> bool {
!self.no_system
}
pub fn with_qemu_nbd(mut self, path: PathBuf) -> Self {
self.qemu_nbd = Some(path);
self
}
pub fn with_force_nbd(mut self, force: bool) -> Self {
self.force_nbd = force;
self
}
pub fn with_unix_socket(mut self, path: impl Into<PathBuf>) -> Self {
self.unix_socket = Some(path.into());
self
}
pub fn with_extra_nbd_args(mut self, args: Vec<String>) -> Self {
self.extra_nbd_args = args;
self
}
pub fn with_connection_timeout(mut self, timeout: Duration) -> Self {
self.connection_timeout = Some(timeout);
self
}
pub fn with_nbd_persistent(mut self, persistent: bool) -> Self {
self.nbd_persistent = persistent;
self
}
pub fn with_nbd_max_sessions(mut self, max_sessions: usize) -> Self {
self.nbd_max_sessions = max_sessions.max(1);
self
}
pub fn with_cancel_token(mut self, token: Arc<AtomicBool>) -> Self {
self.cancel_token = Some(token);
self
}
pub fn with_cancellation_token(mut self, token: &CancellationToken) -> Self {
self.cancel_token = Some(token.clone_arc());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InspectionProgressEvent {
pub percentage: u8,
pub stage: String,
pub detail: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InspectionSummary {
pub path: PathBuf,
pub operating_system: OperatingSystem,
pub guest_tools: Option<GuestTools>,
pub duration_ms: u64,
pub access_mode: String,
pub warnings: Vec<String>,
}
#[derive(Debug)]
pub struct ImageInspectionError {
pub path: PathBuf,
pub error: VmSpectError,
}
impl Serialize for ImageInspectionError {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeStruct;
let mut state = serializer.serialize_struct("ImageInspectionError", 2)?;
state.serialize_field("path", &self.path)?;
state.serialize_field("error", &self.error.to_string())?;
state.end()
}
}
#[derive(Debug, Serialize)]
pub struct BatchResult {
pub reports: Vec<InspectionReport>,
pub errors: Vec<ImageInspectionError>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InspectionReport {
pub image: ImageInfo,
pub scheme: PartitionScheme,
pub partitions: Vec<Partition>,
pub operating_system: OperatingSystem,
pub guest_info: GuestInfo,
pub installed_programs: Vec<Program>,
#[serde(default)]
pub warnings: Vec<String>,
pub stats: Stats,
}
impl InspectionReport {
pub fn summary(&self) -> InspectionSummary {
InspectionSummary {
path: self.image.path.clone(),
operating_system: self.operating_system,
guest_tools: self.guest_info.guest_tools.clone(),
duration_ms: self.stats.duration_ms,
access_mode: self.stats.access_mode.clone(),
warnings: self.warnings.clone(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn linked_cancellation_token_is_visible_in_snapshots() {
let token = Arc::new(AtomicBool::new(false));
let progress = InspectionProgress::with_cancellation_token(Some(&token));
token.store(true, Ordering::Release);
assert!(progress.is_cancelled());
assert!(progress.snapshot().cancelled);
let token = Arc::new(AtomicBool::new(false));
let progress = InspectionProgress::with_cancellation_token(Some(&token));
progress.cancel();
assert!(token.load(Ordering::Acquire));
}
#[test]
fn test_options_defaults() {
let opts = Options::default();
assert!(!opts.no_apps);
assert!(!opts.no_system);
assert!(opts.should_analyze_apps());
assert!(opts.should_analyze_system());
assert!(!opts.force_nbd);
assert_eq!(opts.nbd_max_sessions, 2);
}
#[test]
fn test_options_no_apps() {
let opts = Options {
no_apps: true,
..Options::default()
};
assert!(!opts.should_analyze_apps());
assert!(opts.should_analyze_system());
}
#[test]
fn test_options_no_system() {
let opts = Options {
no_system: true,
..Options::default()
};
assert!(opts.should_analyze_apps());
assert!(!opts.should_analyze_system());
}
#[test]
fn test_summary_omits_installed_programs() {
let report = InspectionReport {
image: ImageInfo {
path: PathBuf::from("vm.raw"),
format: "raw".to_string(),
virtual_size: 1,
actual_size: 1,
hypervisor: crate::models::Hypervisor::Unknown,
},
scheme: PartitionScheme::None,
partitions: Vec::new(),
operating_system: OperatingSystem::Unknown,
guest_info: GuestInfo::default(),
installed_programs: vec![Program {
name: "secret app".to_string(),
..Program::default()
}],
warnings: vec!["warning".to_string()],
stats: Stats {
access_mode: "native".to_string(),
duration_ms: 12,
..Stats::default()
},
};
let summary = report.summary();
let json = serde_json::to_value(summary).unwrap();
assert!(json.get("installed_programs").is_none());
assert_eq!(json["path"], "vm.raw");
}
#[test]
fn test_options_nbd_advanced() {
let opts = Options::default()
.with_unix_socket("/tmp/qemu-test.sock")
.with_extra_nbd_args(vec!["--cache=none".into(), "--detect-zeroes=on".into()])
.with_connection_timeout(Duration::from_secs(10))
.with_nbd_persistent(true);
assert_eq!(opts.unix_socket, Some(PathBuf::from("/tmp/qemu-test.sock")));
assert_eq!(
opts.extra_nbd_args,
vec!["--cache=none".to_string(), "--detect-zeroes=on".to_string()]
);
assert_eq!(opts.connection_timeout, Some(Duration::from_secs(10)));
assert!(opts.nbd_persistent);
}
}