use crate::models::image::{ImageInfo, Stats};
use crate::models::partition::{OperatingSystem, Partition, PartitionScheme};
use crate::models::software::{GuestInfo, 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,
}
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),
}
}
pub fn with_cancellation_token(token: Option<&Arc<AtomicBool>>) -> Self {
let cancelled = if let Some(t) = token {
AtomicBool::new(t.load(Ordering::Acquire))
} else {
AtomicBool::new(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,
}
}
#[inline]
pub fn completion_percentage(&self) -> f32 {
let pct = self.percentage.load(Ordering::Relaxed);
let total = self.total_tasks.load(Ordering::Relaxed);
if total > 0 {
let done = self.completed_tasks.load(Ordering::Relaxed);
let calc = (done as f32 / total as f32) * 100.0;
calc.clamp(pct as f32, 100.0)
} else {
(pct.min(100)) as f32
}
}
#[inline]
pub fn is_cancelled(&self) -> bool {
self.cancelled.load(Ordering::Acquire)
}
#[inline]
pub fn cancel(&self) {
self.cancelled.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)
}
#[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) {
self.completed_tasks.fetch_add(1, Ordering::Relaxed);
}
#[inline]
pub fn completed_tasks(&self) -> usize {
self.completed_tasks.load(Ordering::Relaxed)
}
pub fn snapshot(&self) -> ProgressSnapshot {
ProgressSnapshot {
percentage: self.percentage.load(Ordering::Relaxed),
stage_id: self.stage_id.load(Ordering::Relaxed),
completed_tasks: self.completed_tasks.load(Ordering::Relaxed),
total_tasks: self.total_tasks.load(Ordering::Relaxed),
bytes_processed: self.bytes_processed.load(Ordering::Relaxed),
total_bytes: self.total_bytes.load(Ordering::Relaxed),
cancelled: self.cancelled.load(Ordering::Acquire),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
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 type InspectionOptions = Options;
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_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 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,
}
#[cfg(test)]
mod tests {
use super::*;
#[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());
}
#[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_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);
}
}