test_bd/lib.rs
1//! A library for creating procedurally generated test block devices using ublk.
2//!
3//! `test-bd` provides a simple API for creating userspace block devices with
4//! deterministic, procedurally generated data patterns. This is useful for testing
5//! storage systems, compression algorithms, deduplication systems, and other
6//! block-level operations.
7//!
8//! # Features
9//!
10//! - **Procedural Data Generation**: Create block devices with deterministic data patterns
11//! using a seed-based random number generator
12//! - **Multiple Pattern Types**: Support for fill (zeros), duplicate (repeating blocks),
13//! and random data patterns
14//! - **Configurable Segmentation**: Divide devices into multiple segments with different
15//! data patterns
16//! - **Device Management**: High-level API for managing multiple devices with automatic
17//! cleanup
18//!
19//! # Quick Start
20//!
21//! The easiest way to create and manage test block devices is using [`DeviceManager`]:
22//!
23//! ```no_run
24//! use test_bd::{DeviceManager, TestBlockDeviceConfig};
25//!
26//! // Create a device manager
27//! let mut manager = DeviceManager::new();
28//!
29//! // Configure a 1 GiB device with mixed data patterns
30//! let config = TestBlockDeviceConfig {
31//! dev_id: -1, // Auto-assign device ID
32//! size: 1024 * 1024 * 1024, // 1 GiB
33//! seed: 42, // Seed for reproducibility
34//! fill_percent: 40, // 40% zeros (compressible)
35//! duplicate_percent: 30, // 30% duplicate blocks (deduplicatable)
36//! random_percent: 30, // 30% random data
37//! segments: 100, // 100 segments
38//! unprivileged: false,
39//! };
40//!
41//! // Create the device (appears as /dev/ublkbN)
42//! let device = manager.create(config).expect("Failed to create device");
43//! println!("Created device: /dev/ublkb{}", device.dev_id);
44//!
45//! // Use the device...
46//! // The device will be automatically cleaned up when manager is dropped
47//! ```
48//!
49//! # Data Patterns
50//!
51//! Three types of data patterns are supported via the [`Bucket`] enum:
52//!
53//! - **Fill**: All zeros (highly compressible)
54//! - **Duplicate**: Repeating 512-byte blocks (deduplicatable)
55//! - **Random**: Deterministic pseudo-random data (incompressible)
56//!
57//! The device is divided into segments, and each segment is randomly assigned
58//! one of these patterns based on the configured percentages.
59//!
60//! # Low-Level API
61//!
62//! For more control, use the [`TestBlockDevice`] API directly:
63//!
64//! ```no_run
65//! use test_bd::{TestBlockDevice, TestBlockDeviceConfig};
66//!
67//! let config = TestBlockDeviceConfig {
68//! dev_id: -1,
69//! size: 100 * 1024 * 1024, // 100 MiB
70//! seed: 42,
71//! fill_percent: 50,
72//! duplicate_percent: 25,
73//! random_percent: 25,
74//! segments: 50,
75//! unprivileged: false,
76//! };
77//!
78//! // This blocks until the device is stopped
79//! TestBlockDevice::run(config).expect("Failed to run device");
80//! ```
81//!
82//! # Segment Information
83//!
84//! You can inspect the segment layout before or after creating a device:
85//!
86//! ```
87//! use test_bd::{TestBlockDeviceConfig, Bucket};
88//!
89//! let config = TestBlockDeviceConfig {
90//! dev_id: -1,
91//! size: 10240,
92//! seed: 42,
93//! fill_percent: 50,
94//! duplicate_percent: 25,
95//! random_percent: 25,
96//! segments: 10,
97//! unprivileged: false,
98//! };
99//!
100//! let segments = config.generate_segments();
101//! for (i, segment) in segments.iter().enumerate() {
102//! println!("Segment {}: {:?} pattern, {} bytes",
103//! i, segment.pattern, segment.size_bytes());
104//! }
105//! ```
106//!
107//! # Requirements
108//!
109//! - Linux kernel with ublk support (kernel 6.0+)
110//! - Root privileges (unless using unprivileged mode, which requires kernel 6.5+)
111//!
112//! # See Also
113//!
114//! - [`TestBlockDeviceConfig`] - Configuration structure
115//! - [`DeviceManager`] - High-level device management
116//! - [`TestBlockDevice`] - Low-level device API
117//! - [`SegmentInfo`] - Segment layout information
118//! - [`Bucket`] - Data pattern types
119
120use io_uring::IoUring;
121use libublk::ctrl::UblkCtrl;
122use libublk::ctrl_async::UblkCtrlAsync;
123use libublk::helpers::IoBuf;
124use libublk::io::{UblkDev, UblkQueue};
125use libublk::uring_async::{run_uring_tasks, ublk_reap_events_with_handler, ublk_wake_task};
126use libublk::{BufDesc, UblkError, UblkFlags};
127use std::fs::File;
128use std::os::fd::{AsRawFd, FromRawFd};
129use std::rc::Rc;
130use std::sync::{Arc, Mutex};
131
132mod data_pattern;
133pub use data_pattern::Bucket;
134use data_pattern::PercentPattern;
135mod position;
136
137pub use position::IndexPos;
138use serde::{Deserialize, Serialize};
139use std::collections::HashMap;
140use std::sync::mpsc::{self, Receiver, Sender};
141use std::thread::{self, JoinHandle};
142
143/// Information about a contiguous segment of the block device with a specific data pattern.
144///
145/// A test block device is divided into multiple segments, where each segment uses a
146/// different data pattern type (Fill, Duplicate, or Random). This struct describes
147/// a single segment's location and pattern type.
148///
149/// # Examples
150///
151/// ```
152/// use test_bd::{SegmentInfo, IndexPos, Bucket};
153///
154/// let segment = SegmentInfo {
155/// start: IndexPos::new(0),
156/// end: IndexPos::new(1024),
157/// pattern: Bucket::Fill,
158/// };
159///
160/// assert_eq!(segment.count(), 1024);
161/// assert_eq!(segment.size_bytes(), 8192); // 1024 * 8 bytes
162/// ```
163#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
164pub struct SegmentInfo {
165 /// Starting position of this segment (inclusive).
166 pub start: IndexPos,
167
168 /// Ending position of this segment (exclusive).
169 pub end: IndexPos,
170
171 /// The data pattern type used in this segment.
172 pub pattern: Bucket,
173}
174
175impl SegmentInfo {
176 /// Returns the number of 8-byte elements in this segment.
177 ///
178 /// # Examples
179 ///
180 /// ```
181 /// use test_bd::{SegmentInfo, IndexPos, Bucket};
182 ///
183 /// let segment = SegmentInfo {
184 /// start: IndexPos::new(0),
185 /// end: IndexPos::new(100),
186 /// pattern: Bucket::Random,
187 /// };
188 ///
189 /// assert_eq!(segment.count(), 100);
190 /// ```
191 pub fn count(&self) -> u64 {
192 self.end.as_u64() - self.start.as_u64()
193 }
194
195 /// Returns the size of this segment in bytes.
196 ///
197 /// # Examples
198 ///
199 /// ```
200 /// use test_bd::{SegmentInfo, IndexPos, Bucket};
201 ///
202 /// let segment = SegmentInfo {
203 /// start: IndexPos::new(0),
204 /// end: IndexPos::new(100),
205 /// pattern: Bucket::Random,
206 /// };
207 ///
208 /// assert_eq!(segment.size_bytes(), 800); // 100 * 8 bytes
209 /// ```
210 pub fn size_bytes(&self) -> u64 {
211 (self.end.as_u64() - self.start.as_u64()) * 8
212 }
213}
214
215/// Configuration for creating a test block device.
216///
217/// This struct contains all the parameters needed to create a procedurally generated
218/// test block device with specific characteristics for testing storage systems, compression,
219/// deduplication, and other block-level operations.
220///
221/// # Examples
222///
223/// ```no_run
224/// use test_bd::TestBlockDeviceConfig;
225///
226/// let config = TestBlockDeviceConfig {
227/// dev_id: -1, // Auto-assign device ID
228/// size: 1024 * 1024 * 1024, // 1 GiB
229/// seed: 42,
230/// fill_percent: 40,
231/// duplicate_percent: 30,
232/// random_percent: 30,
233/// segments: 100,
234/// unprivileged: false,
235/// };
236///
237/// // Validate the configuration
238/// config.validate().expect("Invalid configuration");
239/// ```
240#[derive(Debug, Clone)]
241pub struct TestBlockDeviceConfig {
242 /// Device ID for the block device.
243 ///
244 /// Use `-1` to automatically assign the next available device ID,
245 /// or specify a non-negative value to request a specific device ID.
246 pub dev_id: i32,
247
248 /// Total size of the block device in bytes.
249 ///
250 /// Must be a multiple of 8 bytes and not exceed `i64::MAX`.
251 pub size: u64,
252
253 /// Seed for the pseudo-random number generator.
254 ///
255 /// Using the same seed with the same configuration will produce
256 /// identical data patterns, enabling reproducible tests.
257 pub seed: u64,
258
259 /// Percentage of the device to fill with zeros (0-100).
260 ///
261 /// Fill segments are useful for testing compression.
262 pub fill_percent: u32,
263
264 /// Percentage of the device to fill with duplicate patterns (0-100).
265 ///
266 /// Duplicate segments contain repeating 512-byte blocks, useful for
267 /// testing deduplication.
268 pub duplicate_percent: u32,
269
270 /// Percentage of the device to fill with pseudo-random data (0-100).
271 ///
272 /// Random segments simulate encrypted or already-compressed data.
273 pub random_percent: u32,
274
275 /// Number of segments to divide the device into.
276 ///
277 /// Must be less than `size / 512`. More segments create more varied
278 /// data distributions but may impact performance.
279 pub segments: usize,
280
281 /// Whether to create an unprivileged device (non-root access).
282 ///
283 /// When `true`, the device can be accessed by non-root users
284 /// (requires kernel support).
285 pub unprivileged: bool,
286}
287
288impl TestBlockDeviceConfig {
289 /// Generates the segment layout for this configuration without creating a device.
290 ///
291 /// This is useful for inspecting what the device layout would be before
292 /// actually creating the device, or for verification purposes.
293 ///
294 /// # Examples
295 ///
296 /// ```
297 /// use test_bd::TestBlockDeviceConfig;
298 ///
299 /// let config = TestBlockDeviceConfig {
300 /// dev_id: -1,
301 /// size: 10240,
302 /// seed: 42,
303 /// fill_percent: 50,
304 /// duplicate_percent: 25,
305 /// random_percent: 25,
306 /// segments: 10,
307 /// unprivileged: false,
308 /// };
309 ///
310 /// let segments = config.generate_segments();
311 /// assert_eq!(segments.len(), 10);
312 /// ```
313 pub fn generate_segments(&self) -> Vec<SegmentInfo> {
314 let percents = self.percent_pattern();
315 let (_, mapping) =
316 data_pattern::DataMix::create(self.size, self.seed, self.segments, &percents);
317
318 mapping
319 .into_iter()
320 .map(|(range, bucket)| SegmentInfo {
321 start: range.start,
322 end: range.end,
323 pattern: bucket,
324 })
325 .collect()
326 }
327
328 /// Validates the configuration parameters.
329 ///
330 /// Ensures that:
331 /// - Percentages sum to exactly 100
332 /// - Size doesn't exceed `i64::MAX`
333 /// - Number of segments is reasonable for the device size
334 ///
335 /// # Returns
336 ///
337 /// - `Ok(())` if the configuration is valid
338 /// - `Err(String)` with a descriptive error message if validation fails
339 ///
340 /// # Examples
341 ///
342 /// ```
343 /// use test_bd::TestBlockDeviceConfig;
344 ///
345 /// let config = TestBlockDeviceConfig {
346 /// dev_id: -1,
347 /// size: 10240,
348 /// seed: 42,
349 /// fill_percent: 50,
350 /// duplicate_percent: 30,
351 /// random_percent: 20,
352 /// segments: 10,
353 /// unprivileged: false,
354 /// };
355 ///
356 /// assert!(config.validate().is_ok());
357 /// ```
358 pub fn validate(&self) -> Result<(), String> {
359 if self.fill_percent + self.duplicate_percent + self.random_percent != 100 {
360 return Err(format!(
361 "Percentages must total 100, got: fill={}, dup={}, random={}",
362 self.fill_percent, self.duplicate_percent, self.random_percent
363 ));
364 }
365
366 if self.size > i64::MAX as u64 {
367 return Err(format!("Size exceeds maximum: {}", i64::MAX));
368 }
369
370 if self.segments as u64 >= self.size / 512 {
371 return Err(format!(
372 "Number of segments ({}) must be less than device size / 512 ({})",
373 self.segments,
374 self.size / 512
375 ));
376 }
377
378 Ok(())
379 }
380
381 pub(crate) fn percent_pattern(&self) -> PercentPattern {
382 PercentPattern {
383 fill: self.fill_percent,
384 duplicates: self.duplicate_percent,
385 random: self.random_percent,
386 }
387 }
388}
389
390fn handle_io(
391 q: &UblkQueue,
392 tag: u16,
393 buf_addr: *mut u8,
394 state: &mut data_pattern::TestBdState,
395) -> i32 {
396 let iod = q.get_iod(tag);
397 let off = iod.start_sector << 9;
398 let bytes = (iod.nr_sectors << 9) as i32;
399 let op = iod.op_flags & 0xff;
400
401 assert!(bytes % 8 == 0);
402
403 match op {
404 libublk::sys::UBLK_IO_OP_READ => unsafe {
405 let offset_index = IndexPos::new(off / 8);
406 let mut p = buf_addr as *mut libc::c_ulonglong;
407 let writes: u64 = (bytes / 8) as u64;
408
409 let mut io_gen = state.s.lock().unwrap();
410 io_gen.setup(offset_index);
411
412 for _ in 0..writes {
413 let v = io_gen.next_u64().to_be();
414 *p = v;
415 p = p.wrapping_add(1);
416 }
417 },
418 libublk::sys::UBLK_IO_OP_WRITE => {
419 return -libc::EINVAL;
420 }
421 libublk::sys::UBLK_IO_OP_FLUSH => {}
422 _ => {
423 return -libc::EINVAL;
424 }
425 }
426
427 bytes
428}
429
430async fn io_task(
431 q: &UblkQueue<'_>,
432 tag: u16,
433 state: &mut data_pattern::TestBdState,
434) -> Result<(), UblkError> {
435 let buf_size = q.dev.dev_info.max_io_buf_bytes as usize;
436 let buffer = IoBuf::<u8>::new(buf_size);
437 let addr = buffer.as_mut_ptr();
438
439 // Submit initial prep command - any error will exit the function
440 q.submit_io_prep_cmd(tag, BufDesc::Slice(buffer.as_slice()), 0, Some(&buffer))
441 .await?;
442
443 loop {
444 let res = handle_io(q, tag, addr, state);
445
446 // Any error (including QueueIsDown) will break the loop by exiting the function
447 q.submit_io_commit_cmd(tag, BufDesc::Slice(buffer.as_slice()), res)
448 .await?;
449 }
450}
451
452/// Poll and handle both QUEUE_RING and CTRL_URING concurrently
453async fn poll_and_handle_rings<R, I>(
454 run_ops: R,
455 is_done: I,
456 check_done: bool,
457) -> Result<(), UblkError>
458where
459 R: Fn(),
460 I: Fn() -> bool,
461{
462 // Helper to create async wrapper for file descriptor
463 let create_async_wrapper = |fd: i32| -> Result<smol::Async<File>, UblkError> {
464 let file = unsafe { File::from_raw_fd(fd) };
465 smol::Async::new(file).map_err(|_| UblkError::OtherError(-libc::EINVAL))
466 };
467
468 // Get file descriptors and create async wrappers
469 let queue_fd = libublk::io::with_task_io_ring(|ring| ring.as_raw_fd());
470 let ctrl_fd = libublk::ctrl::with_ctrl_ring(|ring| ring.as_raw_fd());
471 let async_queue = create_async_wrapper(queue_fd)?;
472 let async_ctrl = create_async_wrapper(ctrl_fd)?;
473
474 // Polling function for both rings
475 let poll_both_rings = || async {
476 // Submit and wait on both rings
477 libublk::io::with_task_io_ring_mut(|ring| ring.submit_and_wait(0))?;
478 libublk::ctrl::with_ctrl_ring_mut(|ring| ring.submit_and_wait(0))?;
479
480 // Wait for either ring to become readable
481 smol::future::race(async_queue.readable(), async_ctrl.readable())
482 .await
483 .map(|_| false) // No timeout
484 .map_err(UblkError::IOError)
485 };
486
487 // Helper to handle events from a ring
488 let handle_ring_events = |cqe: &io_uring::cqueue::Entry| {
489 ublk_wake_task(cqe.user_data(), cqe);
490 cqe.result() == libublk::sys::UBLK_IO_RES_ABORT
491 };
492
493 // Event reaping function for both rings
494 let reap_events = |_poll_timeout| {
495 let mut aborted = check_done;
496
497 // Reap events from both rings
498 let queue_result = libublk::io::with_task_io_ring_mut(|ring| {
499 ublk_reap_events_with_handler(ring, |cqe| {
500 if handle_ring_events(cqe) {
501 aborted = true;
502 }
503 })
504 });
505
506 let ctrl_result = libublk::ctrl::with_ctrl_ring_mut(|ring| {
507 ublk_reap_events_with_handler(ring, |cqe| {
508 if handle_ring_events(cqe) {
509 aborted = true;
510 }
511 })
512 });
513
514 queue_result.and(ctrl_result).map(|_| aborted)
515 };
516
517 run_uring_tasks(poll_both_rings, reap_events, run_ops, is_done).await?;
518
519 // Prevent file descriptors from being closed when async wrappers are dropped
520 let _ = async_queue.into_inner().map(|f| {
521 use std::os::fd::IntoRawFd;
522 f.into_raw_fd()
523 });
524 let _ = async_ctrl.into_inner().map(|f| {
525 use std::os::fd::IntoRawFd;
526 f.into_raw_fd()
527 });
528
529 Ok(())
530}
531
532/// Generic function to run ublk async uring tasks with local executor
533fn ublk_uring_run_async_task<T, F, Fut>(task: Fut) -> Result<T, UblkError>
534where
535 F: std::future::Future<Output = Result<T, UblkError>>,
536 Fut: FnOnce() -> F,
537{
538 let exe_rc = Rc::new(smol::LocalExecutor::new());
539 let task_done = Rc::new(std::cell::RefCell::new(false));
540 let task_done_clone = task_done.clone();
541 let exe = exe_rc.clone();
542
543 // Create the main task with the provided async block/closure
544 let main_task = exe.spawn(async move {
545 let result = task().await;
546 *task_done_clone.borrow_mut() = true;
547 result
548 });
549
550 // Create the event handling task
551 let exe2 = exe_rc.clone();
552 let event_task = exe_rc.spawn(async move {
553 let run_ops = || {
554 while exe2.try_tick() {}
555 };
556 let is_done = || *task_done.borrow();
557 poll_and_handle_rings(run_ops, is_done, true).await
558 });
559
560 // Run both tasks concurrently
561 smol::block_on(exe_rc.run(async {
562 let (task_result, _) = futures::join!(main_task, event_task);
563 task_result
564 }))
565}
566
567/// Create UblkCtrl using UblkCtrlBuilder::build_async() with smol executor
568fn create_ublk_ctrl_async(
569 dev_id: i32,
570 dev_flags: UblkFlags,
571 ctrl_flags: u64,
572) -> Result<UblkCtrlAsync, UblkError> {
573 ublk_uring_run_async_task(|| async move {
574 libublk::ctrl::UblkCtrlBuilder::default()
575 .name("test_block_device")
576 .id(dev_id)
577 .nr_queues(1_u16)
578 .depth(128_u16)
579 .dev_flags(dev_flags)
580 .ctrl_flags(ctrl_flags)
581 .build_async()
582 .await
583 })
584}
585
586fn run_device<F>(
587 dev_id: i32,
588 size: u64,
589 state: &mut data_pattern::TestBdState,
590 ctrl_flags: u64,
591 segments: Vec<SegmentInfo>,
592 on_ready: Option<F>,
593) -> Result<i32, UblkError>
594where
595 F: FnOnce(i32, Vec<SegmentInfo>) + 'static,
596{
597 log::info!(
598 "run_device called: dev_id={}, size={}, ctrl_flags={:#x}, segments={}",
599 dev_id,
600 size,
601 ctrl_flags,
602 segments.len()
603 );
604
605 let dev_flags = UblkFlags::UBLK_DEV_F_ADD_DEV;
606
607 // Initialize control ring for this thread
608 libublk::ctrl::ublk_init_ctrl_task_ring(|ring_opt| {
609 if ring_opt.is_none() {
610 log::debug!(
611 "run_device: Creating new control task ring for device {}",
612 dev_id
613 );
614 let ring = IoUring::<io_uring::squeue::Entry128>::builder()
615 .setup_cqsize(128)
616 .setup_coop_taskrun()
617 .build(128)
618 .map_err(UblkError::IOError)?;
619 *ring_opt = Some(ring);
620 }
621 Ok(())
622 })?;
623
624 log::debug!("run_device: Initializing task ring for device {}", dev_id);
625 // Initialize task ring for this thread
626 libublk::io::ublk_init_task_ring(|cell| {
627 use std::cell::RefCell;
628 if cell.get().is_none() {
629 log::debug!("run_device: Creating new task ring for device {}", dev_id);
630 let ring = IoUring::<io_uring::squeue::Entry, io_uring::cqueue::Entry>::builder()
631 .setup_cqsize(128)
632 .setup_coop_taskrun()
633 .build(128)
634 .map_err(|e| {
635 log::error!(
636 "run_device: Failed to build task ring for device {}: {}. \
637 This likely indicates: \
638 (1) System limit on io_uring instances reached (check /proc/sys/kernel/io_uring/max_*), \
639 (2) Insufficient locked memory (check ulimit -l), \
640 (3) Too many open file descriptors (check ulimit -n), \
641 (4) Kernel resource exhaustion",
642 dev_id, e
643 );
644 UblkError::IOError(e)
645 })?;
646
647 cell.set(RefCell::new(ring))
648 .map_err(|_| {
649 log::error!("run_device: Failed to set task ring cell for device {} (EEXIST)", dev_id);
650 UblkError::OtherError(-libc::EEXIST)
651 })?;
652 log::debug!("run_device: Task ring created successfully for device {}", dev_id);
653 } else {
654 log::debug!("run_device: Task ring already exists for device {}", dev_id);
655 }
656 Ok(())
657 }).map_err(|e| {
658 log::error!("run_device: ublk_init_task_ring failed for device {}: {:?}", dev_id, e);
659 e
660 })?;
661
662 // Create the control using the generic async task runner
663 log::debug!(
664 "run_device: Creating ublk control (requested dev_id={})",
665 dev_id
666 );
667 let ctrl = match create_ublk_ctrl_async(dev_id, dev_flags, ctrl_flags) {
668 Ok(c) => Rc::new(c),
669 Err(e) => {
670 log::error!("Failed to create ublk control device {}: {}", dev_id, e);
671 return Err(e);
672 }
673 };
674
675 let actual_dev_id = ctrl.dev_info().dev_id;
676 log::info!(
677 "run_device: Actual device ID assigned: {} (requested was {})",
678 actual_dev_id,
679 dev_id
680 );
681
682 let tgt_init = |dev: &mut UblkDev| {
683 dev.set_default_params(size);
684 Ok(())
685 };
686 log::debug!("run_device: Creating UblkDev for device {}", actual_dev_id);
687 let dev_rc = match UblkDev::new_async(ctrl.get_name(), tgt_init, &ctrl) {
688 Ok(d) => Arc::new(d),
689 Err(e) => {
690 log::error!("Failed to create ublk device {}: {}", actual_dev_id, e);
691 return Err(e);
692 }
693 };
694 let dev_clone = dev_rc.clone();
695 log::debug!(
696 "run_device: Creating UblkQueue for device {}",
697 actual_dev_id
698 );
699 let q_rc = match UblkQueue::new(0, &dev_clone) {
700 Ok(q) => Rc::new(q),
701 Err(e) => {
702 log::error!(
703 "Failed to create ublk queue for device {}: {}",
704 actual_dev_id,
705 e
706 );
707 return Err(e);
708 }
709 };
710 log::debug!(
711 "run_device: UblkQueue created successfully for device {}",
712 actual_dev_id
713 );
714 let exec_rc = Rc::new(smol::LocalExecutor::new());
715 let exec = exec_rc.clone();
716
717 // spawn async io tasks
718 let mut f_vec = Vec::new();
719
720 for tag in 0..ctrl.dev_info().queue_depth as u16 {
721 let q_clone = q_rc.clone();
722
723 let mut t_c = state.clone();
724 f_vec.push(exec.spawn(async move {
725 match io_task(&q_clone, tag, &mut t_c).await {
726 Err(UblkError::QueueIsDown) | Ok(_) => {}
727 Err(e) => log::warn!("io_task failed for tag {}: {}", tag, e),
728 }
729 }));
730 }
731
732 let ctrl_clone = ctrl.clone();
733 let dev_clone = dev_rc.clone();
734 let ready_callback = Rc::new(std::cell::RefCell::new(on_ready));
735 let ready_callback_clone = ready_callback.clone();
736 let segments_clone = segments.clone();
737 f_vec.push(exec.spawn(async move {
738 match ctrl_clone
739 .configure_queue_async(&dev_clone, 0, unsafe { libc::gettid() })
740 .await
741 {
742 Ok(r) if r >= 0 => match ctrl_clone.start_dev_async(&dev_clone).await {
743 Ok(_) => {
744 log::info!("Device {} started successfully", actual_dev_id);
745 // Call the ready callback if provided
746 if let Some(callback) = ready_callback_clone.borrow_mut().take() {
747 callback(actual_dev_id as i32, segments_clone);
748 }
749 }
750 Err(e) => {
751 log::error!("Failed to start device: {}", e);
752 }
753 },
754 Ok(r) => {
755 log::error!("configure_queue_async returned error code: {}", r);
756 }
757 Err(e) => {
758 log::error!("Failed to configure queue: {}", e);
759 }
760 }
761 }));
762 log::debug!(
763 "run_device: Entering smol::block_on executor loop for device {}",
764 actual_dev_id
765 );
766 smol::block_on(exec_rc.run(async move {
767 let run_ops = || while exec.try_tick() {};
768 let done = || {
769 let all_finished = f_vec.iter().all(|task| task.is_finished());
770 if !all_finished {
771 let unfinished_count = f_vec.iter().filter(|task| !task.is_finished()).count();
772 log::debug!(
773 "run_device: Waiting for {} tasks to finish (out of {} total)",
774 unfinished_count,
775 f_vec.len()
776 );
777 }
778 all_finished
779 };
780
781 log::debug!(
782 "run_device: Calling poll_and_handle_rings for device {}",
783 actual_dev_id
784 );
785 if let Err(e) = poll_and_handle_rings(run_ops, done, false).await {
786 log::error!("poll_and_handle_rings failed: {}", e);
787 }
788 log::debug!(
789 "run_device: poll_and_handle_rings completed for device {}",
790 actual_dev_id
791 );
792 }));
793
794 log::debug!(
795 "run_device: Exited smol::block_on for device {}, returning Ok",
796 actual_dev_id
797 );
798 Ok(actual_dev_id as i32)
799}
800
801/// Main API for creating and managing test block devices.
802///
803/// `TestBlockDevice` provides static methods for creating, deleting,
804/// and inspecting ublk-based test block devices with procedurally generated data.
805///
806/// # Examples
807///
808/// ```no_run
809/// use test_bd::{TestBlockDevice, TestBlockDeviceConfig};
810///
811/// let config = TestBlockDeviceConfig {
812/// dev_id: -1,
813/// size: 1024 * 1024 * 1024, // 1 GiB
814/// seed: 42,
815/// fill_percent: 40,
816/// duplicate_percent: 30,
817/// random_percent: 30,
818/// segments: 100,
819/// unprivileged: false,
820/// };
821///
822/// // This will block until the device is stopped
823/// match TestBlockDevice::run(config) {
824/// Ok(dev_id) => println!("Device created: /dev/ublkb{}", dev_id),
825/// Err(e) => eprintln!("Error: {}", e),
826/// }
827/// ```
828pub struct TestBlockDevice;
829
830impl TestBlockDevice {
831 /// Creates and runs a test block device with the given configuration.
832 ///
833 /// This method blocks until the device is stopped (e.g., via `delete()`).
834 /// The device will appear as `/dev/ublkb{dev_id}` and can be used like
835 /// any other block device.
836 ///
837 /// # Arguments
838 ///
839 /// * `config` - Configuration specifying device size, data patterns, etc.
840 ///
841 /// # Returns
842 ///
843 /// - `Ok(i32)` - The assigned device ID on success
844 /// - `Err(String)` - Error message if device creation fails
845 ///
846 /// # Examples
847 ///
848 /// ```no_run
849 /// use test_bd::{TestBlockDevice, TestBlockDeviceConfig};
850 ///
851 /// let config = TestBlockDeviceConfig {
852 /// dev_id: -1,
853 /// size: 10 * 1024 * 1024, // 10 MiB
854 /// seed: 42,
855 /// fill_percent: 50,
856 /// duplicate_percent: 25,
857 /// random_percent: 25,
858 /// segments: 20,
859 /// unprivileged: false,
860 /// };
861 ///
862 /// let dev_id = TestBlockDevice::run(config).expect("Failed to create device");
863 /// println!("Created device: /dev/ublkb{}", dev_id);
864 /// ```
865 pub fn run(config: TestBlockDeviceConfig) -> Result<i32, String> {
866 Self::run_with_callback(config, None::<fn(i32, Vec<SegmentInfo>)>)
867 }
868
869 /// Creates and runs a test block device with a callback when ready.
870 ///
871 /// Similar to `run()`, but invokes a callback function once the device
872 /// is ready for I/O operations. This is useful for coordinating with
873 /// other tasks that need to wait for device initialization.
874 ///
875 /// # Arguments
876 ///
877 /// * `config` - Configuration specifying device size, data patterns, etc.
878 /// * `on_ready` - Optional callback invoked with device ID and segment info
879 ///
880 /// # Returns
881 ///
882 /// - `Ok(i32)` - The assigned device ID on success
883 /// - `Err(String)` - Error message if device creation fails
884 ///
885 /// # Examples
886 ///
887 /// ```no_run
888 /// use test_bd::{TestBlockDevice, TestBlockDeviceConfig, SegmentInfo};
889 ///
890 /// let config = TestBlockDeviceConfig {
891 /// dev_id: -1,
892 /// size: 10 * 1024 * 1024,
893 /// seed: 42,
894 /// fill_percent: 50,
895 /// duplicate_percent: 25,
896 /// random_percent: 25,
897 /// segments: 20,
898 /// unprivileged: false,
899 /// };
900 ///
901 /// TestBlockDevice::run_with_callback(config, Some(|dev_id, segments: Vec<SegmentInfo>| {
902 /// println!("Device {} ready with {} segments", dev_id, segments.len());
903 /// })).expect("Failed to create device");
904 /// ```
905 pub fn run_with_callback<F>(
906 config: TestBlockDeviceConfig,
907 on_ready: Option<F>,
908 ) -> Result<i32, String>
909 where
910 F: FnOnce(i32, Vec<SegmentInfo>) + 'static,
911 {
912 config.validate()?;
913
914 let percents = config.percent_pattern();
915 let (pattern_gen, mapping) =
916 data_pattern::DataMix::create(config.size, config.seed, config.segments, &percents);
917 let m = Mutex::new(pattern_gen);
918 let mut state = data_pattern::TestBdState { s: Rc::new(m) };
919
920 // Convert mapping to SegmentInfo
921 let segments: Vec<SegmentInfo> = mapping
922 .into_iter()
923 .map(|(range, bucket)| SegmentInfo {
924 start: range.start,
925 end: range.end,
926 pattern: bucket,
927 })
928 .collect();
929
930 let ctrl_flags = if config.unprivileged {
931 libublk::sys::UBLK_F_UNPRIVILEGED_DEV as u64
932 } else {
933 0
934 };
935
936 run_device(
937 config.dev_id,
938 config.size,
939 &mut state,
940 ctrl_flags,
941 segments,
942 on_ready,
943 )
944 .map_err(|e| format!("Failed to run device: {}", e))
945 }
946
947 /// Deletes (stops and removes) a test block device.
948 ///
949 /// This stops the device and removes it from the system. The device node
950 /// `/dev/ublkb{dev_id}` will be removed.
951 ///
952 /// # Arguments
953 ///
954 /// * `dev_id` - The device ID to delete
955 /// * `_async_del` - Reserved for future use (currently ignored)
956 ///
957 /// # Returns
958 ///
959 /// - `Ok(())` on success
960 /// - `Err(String)` if deletion fails
961 ///
962 /// # Examples
963 ///
964 /// ```no_run
965 /// use test_bd::TestBlockDevice;
966 ///
967 /// // Delete device 0
968 /// TestBlockDevice::delete(0, false).expect("Failed to delete device");
969 /// ```
970 pub fn delete(dev_id: i32, _async_del: bool) -> Result<(), String> {
971 log::debug!(
972 "TestBlockDevice::delete ENTRY: dev_id={}, async={}",
973 dev_id,
974 _async_del
975 );
976
977 log::debug!(
978 "TestBlockDevice::delete: Creating UblkCtrl for device {}",
979 dev_id
980 );
981 let ctrl = UblkCtrl::new_simple(dev_id).map_err(|e| {
982 let err_msg = format!(
983 "Failed to open device {} for deletion: {}. \
984 This may indicate: \
985 (1) device doesn't exist, \
986 (2) insufficient permissions, \
987 (3) control ring initialization failed (possibly too many open devices/rings), \
988 (4) resource exhaustion",
989 dev_id, e
990 );
991 log::error!("{}", err_msg);
992 err_msg
993 })?;
994 log::debug!(
995 "TestBlockDevice::delete: UblkCtrl created successfully for device {}",
996 dev_id
997 );
998
999 log::debug!(
1000 "TestBlockDevice::delete: Calling ctrl.kill_dev() for device {}",
1001 dev_id
1002 );
1003 ctrl.kill_dev().map_err(|e| {
1004 let err_msg = format!("Failed to kill device {}: {}", dev_id, e);
1005 log::error!("{}", err_msg);
1006 err_msg
1007 })?;
1008 log::debug!(
1009 "TestBlockDevice::delete: ctrl.kill_dev() completed for device {}",
1010 dev_id
1011 );
1012
1013 log::debug!("TestBlockDevice::delete: del_dev {dev_id}");
1014 ctrl.del_dev().map_err(|e| {
1015 let err_msg = format!("del_dev({}) failed {}", dev_id, e);
1016 log::error!("{}", err_msg);
1017 err_msg
1018 })?;
1019
1020 log::debug!("TestBlockDevice::delete: del_dev complete {dev_id}");
1021
1022 Ok(())
1023 }
1024
1025 /// Dumps information about a test block device to the log.
1026 ///
1027 /// This is primarily useful for debugging and displays internal device state.
1028 ///
1029 /// # Arguments
1030 ///
1031 /// * `dev_id` - The device ID to inspect
1032 ///
1033 /// # Returns
1034 ///
1035 /// - `Ok(())` on success
1036 /// - `Err(String)` if the device cannot be opened
1037 ///
1038 /// # Examples
1039 ///
1040 /// ```no_run
1041 /// use test_bd::TestBlockDevice;
1042 ///
1043 /// TestBlockDevice::dump(0).expect("Failed to dump device info");
1044 /// ```
1045 pub fn dump(dev_id: i32) -> Result<(), String> {
1046 let ctrl = UblkCtrl::new_simple(dev_id)
1047 .map_err(|e| format!("Failed to open device {}: {}", dev_id, e))?;
1048 ctrl.dump();
1049 Ok(())
1050 }
1051}
1052
1053/// A handle to a managed test block device.
1054///
1055/// This struct is returned by `DeviceManager::create()` and contains
1056/// information about a running device including its ID, configuration,
1057/// and segment layout.
1058///
1059/// # Examples
1060///
1061/// ```no_run
1062/// use test_bd::{DeviceManager, TestBlockDeviceConfig};
1063///
1064/// let mut manager = DeviceManager::new();
1065/// let config = TestBlockDeviceConfig {
1066/// dev_id: -1,
1067/// size: 10 * 1024 * 1024,
1068/// seed: 42,
1069/// fill_percent: 50,
1070/// duplicate_percent: 25,
1071/// random_percent: 25,
1072/// segments: 20,
1073/// unprivileged: false,
1074/// };
1075///
1076/// let device = manager.create(config).expect("Failed to create device");
1077/// println!("Created device {} with {} segments", device.dev_id, device.segments.len());
1078/// ```
1079#[derive(Debug, Clone)]
1080pub struct ManagedDevice {
1081 /// The assigned device ID.
1082 pub dev_id: i32,
1083
1084 /// The configuration used to create this device.
1085 pub config: TestBlockDeviceConfig,
1086
1087 /// Information about each segment in the device.
1088 pub segments: Vec<SegmentInfo>,
1089}
1090
1091/// Manager for multiple test block devices.
1092///
1093/// `DeviceManager` provides a higher-level API for managing multiple test block
1094/// devices. It handles device creation in background threads and automatic cleanup
1095/// on drop. This is the recommended way to create devices when you need to manage
1096/// their lifecycle.
1097///
1098/// # Examples
1099///
1100/// ```no_run
1101/// use test_bd::{DeviceManager, TestBlockDeviceConfig};
1102///
1103/// let mut manager = DeviceManager::new();
1104///
1105/// // Create first device
1106/// let config1 = TestBlockDeviceConfig {
1107/// dev_id: -1,
1108/// size: 10 * 1024 * 1024,
1109/// seed: 42,
1110/// fill_percent: 50,
1111/// duplicate_percent: 25,
1112/// random_percent: 25,
1113/// segments: 20,
1114/// unprivileged: false,
1115/// };
1116/// let device1 = manager.create(config1).expect("Failed to create device 1");
1117///
1118/// // Create second device
1119/// let config2 = TestBlockDeviceConfig {
1120/// dev_id: -1,
1121/// size: 20 * 1024 * 1024,
1122/// seed: 123,
1123/// fill_percent: 33,
1124/// duplicate_percent: 33,
1125/// random_percent: 34,
1126/// segments: 50,
1127/// unprivileged: false,
1128/// };
1129/// let device2 = manager.create(config2).expect("Failed to create device 2");
1130///
1131/// // List all managed devices
1132/// for device in manager.list() {
1133/// println!("Device {}: {} bytes", device.dev_id, device.config.size);
1134/// }
1135///
1136/// // Delete a specific device
1137/// manager.delete(device1.dev_id).expect("Failed to delete device");
1138///
1139/// // All remaining devices are automatically deleted when manager is dropped
1140/// ```
1141pub struct DeviceManager {
1142 devices: HashMap<i32, (JoinHandle<Result<i32, String>>, ManagedDevice)>,
1143}
1144
1145type SegInfo = (i32, Vec<SegmentInfo>);
1146
1147impl DeviceManager {
1148 /// Creates a new `DeviceManager`.
1149 ///
1150 /// # Examples
1151 ///
1152 /// ```
1153 /// use test_bd::DeviceManager;
1154 ///
1155 /// let manager = DeviceManager::new();
1156 /// ```
1157 pub fn new() -> Self {
1158 Self {
1159 devices: HashMap::new(),
1160 }
1161 }
1162
1163 /// Creates a new test block device and manages it.
1164 ///
1165 /// The device is created in a background thread and this method blocks until
1166 /// the device is ready for I/O. The device will continue running in the
1167 /// background until explicitly deleted or the manager is dropped.
1168 ///
1169 /// # Arguments
1170 ///
1171 /// * `config` - Configuration for the device
1172 ///
1173 /// # Returns
1174 ///
1175 /// - `Ok(ManagedDevice)` - Handle to the created device
1176 /// - `Err(String)` - Error message if creation fails
1177 ///
1178 /// # Examples
1179 ///
1180 /// ```no_run
1181 /// use test_bd::{DeviceManager, TestBlockDeviceConfig};
1182 ///
1183 /// let mut manager = DeviceManager::new();
1184 /// let config = TestBlockDeviceConfig {
1185 /// dev_id: -1,
1186 /// size: 10 * 1024 * 1024,
1187 /// seed: 42,
1188 /// fill_percent: 50,
1189 /// duplicate_percent: 25,
1190 /// random_percent: 25,
1191 /// segments: 20,
1192 /// unprivileged: false,
1193 /// };
1194 ///
1195 /// let device = manager.create(config).expect("Failed to create device");
1196 /// println!("Created /dev/ublkb{}", device.dev_id);
1197 /// ```
1198 pub fn create(&mut self, config: TestBlockDeviceConfig) -> Result<ManagedDevice, String> {
1199 // Validate configuration first
1200 config.validate()?;
1201
1202 // Create a channel for the device to signal when it's ready
1203 let (tx, rx): (Sender<SegInfo>, Receiver<SegInfo>) = mpsc::channel();
1204
1205 // Clone config for use in thread
1206 let config_clone = config.clone();
1207
1208 // Spawn a thread to run the device
1209 let handle = thread::Builder::new()
1210 .name(format!(
1211 "test-bd-{}",
1212 if config.dev_id >= 0 {
1213 config.dev_id.to_string()
1214 } else {
1215 "auto".to_string()
1216 }
1217 ))
1218 .spawn(move || {
1219 log::debug!("Device thread started for dev_id={}", config_clone.dev_id);
1220 let result = TestBlockDevice::run_with_callback(
1221 config_clone.clone(),
1222 Some(move |dev_id, segments| {
1223 log::debug!("Device {} ready callback invoked", dev_id);
1224 // Notify the main thread that the device is ready
1225 let _ = tx.send((dev_id, segments));
1226 drop(tx);
1227 }),
1228 );
1229 log::debug!("Device thread: run_with_callback returned with result: {:?}", result.is_ok());
1230 log::debug!("Device thread: About to return from closure (this is the last line before thread exit)");
1231 result
1232 })
1233 .map_err(|e| format!("Failed to spawn device thread: {}", e))?;
1234
1235 // Wait for the device to be ready
1236 let (dev_id, segments) = rx.recv().map_err(|_| {
1237 log::error!("Failed to receive device ready signal from thread");
1238 "Failed to receive device ready signal from thread. \
1239 The device thread may have panicked or failed to start."
1240 .to_string()
1241 })?;
1242
1243 drop(rx);
1244
1245 log::debug!("Received ready signal for device {}", dev_id);
1246
1247 let managed_device = ManagedDevice {
1248 dev_id,
1249 config,
1250 segments,
1251 };
1252
1253 // Store the thread handle and device info
1254 self.devices
1255 .insert(dev_id, (handle, managed_device.clone()));
1256
1257 log::debug!(
1258 "Device {} successfully created and added to manager",
1259 dev_id
1260 );
1261
1262 // Run udevadm settle to ensure udev has finished processing the device
1263 // This prevents race conditions when creating multiple devices rapidly
1264 log::debug!(
1265 "Running udevadm settle for device {} with 10 second timeout...",
1266 dev_id
1267 );
1268 let settle_start = std::time::Instant::now();
1269 match std::process::Command::new("udevadm")
1270 .arg("settle")
1271 .arg("--timeout=10")
1272 .output()
1273 {
1274 Ok(output) => {
1275 let settle_duration = settle_start.elapsed();
1276 if output.status.success() {
1277 log::debug!(
1278 "udevadm settle completed successfully for device {} in {:?}",
1279 dev_id,
1280 settle_duration
1281 );
1282 } else {
1283 log::warn!(
1284 "udevadm settle exited with status {} for device {} after {:?}: {}",
1285 output.status,
1286 dev_id,
1287 settle_duration,
1288 String::from_utf8_lossy(&output.stderr)
1289 );
1290 }
1291 }
1292 Err(e) => {
1293 let settle_duration = settle_start.elapsed();
1294 log::warn!(
1295 "Failed to run udevadm settle for device {} after {:?}: {}. \
1296 This may cause issues when creating multiple devices rapidly. \
1297 Continuing anyway.",
1298 dev_id,
1299 settle_duration,
1300 e
1301 );
1302 }
1303 }
1304 log::debug!(
1305 "Finished udevadm settle for device {}, returning from create()",
1306 dev_id
1307 );
1308
1309 Ok(managed_device)
1310 }
1311
1312 /// Returns a list of all currently managed devices.
1313 ///
1314 /// # Examples
1315 ///
1316 /// ```no_run
1317 /// use test_bd::{DeviceManager, TestBlockDeviceConfig};
1318 ///
1319 /// let mut manager = DeviceManager::new();
1320 /// let config = TestBlockDeviceConfig {
1321 /// dev_id: -1,
1322 /// size: 10 * 1024 * 1024,
1323 /// seed: 42,
1324 /// fill_percent: 50,
1325 /// duplicate_percent: 25,
1326 /// random_percent: 25,
1327 /// segments: 20,
1328 /// unprivileged: false,
1329 /// };
1330 ///
1331 /// manager.create(config).expect("Failed to create device");
1332 ///
1333 /// for device in manager.list() {
1334 /// println!("Device {}: {} segments", device.dev_id, device.segments.len());
1335 /// }
1336 /// ```
1337 pub fn list(&self) -> Vec<ManagedDevice> {
1338 self.devices
1339 .values()
1340 .map(|(_, device)| device.clone())
1341 .collect()
1342 }
1343
1344 /// Deletes a specific managed device.
1345 ///
1346 /// This stops and removes the device, then waits for the background thread
1347 /// to complete. The device is removed from the manager's tracking.
1348 ///
1349 /// # Arguments
1350 ///
1351 /// * `dev_id` - The device ID to delete
1352 ///
1353 /// # Returns
1354 ///
1355 /// - `Ok(i32)` - The device ID that was deleted
1356 /// - `Err(String)` - Error message if deletion fails
1357 ///
1358 /// # Examples
1359 ///
1360 /// ```no_run
1361 /// use test_bd::{DeviceManager, TestBlockDeviceConfig};
1362 ///
1363 /// let mut manager = DeviceManager::new();
1364 /// let config = TestBlockDeviceConfig {
1365 /// dev_id: -1,
1366 /// size: 10 * 1024 * 1024,
1367 /// seed: 42,
1368 /// fill_percent: 50,
1369 /// duplicate_percent: 25,
1370 /// random_percent: 25,
1371 /// segments: 20,
1372 /// unprivileged: false,
1373 /// };
1374 ///
1375 /// let device = manager.create(config).expect("Failed to create device");
1376 /// manager.delete(device.dev_id).expect("Failed to delete device");
1377 /// ```
1378 pub fn delete(&mut self, dev_id: i32) -> Result<i32, String> {
1379 log::debug!("DeviceManager::delete called for device {}", dev_id);
1380
1381 // Remove the device from our tracking
1382 let (handle, _) = self.devices.remove(&dev_id).ok_or_else(|| {
1383 let err_msg = format!(
1384 "Device {} is not managed by this DeviceManager. \
1385 Currently managing {} devices: {:?}",
1386 dev_id,
1387 self.devices.len(),
1388 self.devices.keys().collect::<Vec<_>>()
1389 );
1390 log::error!("{}", err_msg);
1391 err_msg
1392 })?;
1393
1394 // Delete the device (this will cause the run() call to exit)
1395 TestBlockDevice::delete(dev_id, false).map_err(|e| {
1396 let err_msg = format!(
1397 "Failed to delete device {}: {}. \
1398 Note: Thread will still be joined if possible.",
1399 dev_id, e
1400 );
1401 log::warn!("{}", err_msg);
1402 err_msg
1403 })?;
1404
1405 handle.join().map_err(|e| {
1406 let err_msg = format!("Error on join {}: {:?}", dev_id, e);
1407 log::warn!("{}", err_msg);
1408 err_msg
1409 })?
1410 }
1411
1412 /// Deletes all managed devices.
1413 ///
1414 /// This stops and removes all devices being managed. Devices are deleted
1415 /// sequentially. If any deletion fails, the method continues attempting to
1416 /// delete remaining devices and returns an error at the end.
1417 ///
1418 /// # Returns
1419 ///
1420 /// - `Ok(())` if all devices were deleted successfully
1421 /// - `Err(String)` if any deletions failed (with details about the first failure)
1422 ///
1423 /// # Examples
1424 ///
1425 /// ```no_run
1426 /// use test_bd::{DeviceManager, TestBlockDeviceConfig};
1427 ///
1428 /// let mut manager = DeviceManager::new();
1429 ///
1430 /// // Create multiple devices
1431 /// for i in 0..3 {
1432 /// let config = TestBlockDeviceConfig {
1433 /// dev_id: -1,
1434 /// size: 10 * 1024 * 1024,
1435 /// seed: i,
1436 /// fill_percent: 50,
1437 /// duplicate_percent: 25,
1438 /// random_percent: 25,
1439 /// segments: 20,
1440 /// unprivileged: false,
1441 /// };
1442 /// manager.create(config).expect("Failed to create device");
1443 /// }
1444 ///
1445 /// // Delete all devices
1446 /// manager.delete_all().expect("Failed to delete all devices");
1447 /// ```
1448 pub fn delete_all(&mut self) -> Result<(), String> {
1449 let dev_ids: Vec<i32> = self.devices.keys().copied().collect();
1450 log::info!(
1451 "DeviceManager::delete_all called for {} devices: {:?}",
1452 dev_ids.len(),
1453 dev_ids
1454 );
1455
1456 let mut first_error = None;
1457 let mut deleted_count = 0;
1458 let mut failed_count = 0;
1459
1460 for (index, dev_id) in dev_ids.iter().enumerate() {
1461 log::debug!(
1462 "Deleting device {}/{}: dev_id={}",
1463 index + 1,
1464 dev_ids.len(),
1465 dev_id
1466 );
1467
1468 match self.delete(*dev_id) {
1469 Ok(rc) => {
1470 deleted_count += 1;
1471 log::debug!(
1472 "Successfully deleted device {} ({}/{}) with return code {}",
1473 dev_id,
1474 deleted_count,
1475 dev_ids.len(),
1476 rc
1477 );
1478 }
1479 Err(e) => {
1480 failed_count += 1;
1481 log::error!(
1482 "Failed to delete device {} ({}/{}): {}",
1483 dev_id,
1484 index + 1,
1485 dev_ids.len(),
1486 e
1487 );
1488 if first_error.is_none() {
1489 first_error = Some(e);
1490 }
1491 }
1492 }
1493 }
1494
1495 log::info!(
1496 "DeviceManager::delete_all completed: {} succeeded, {} failed out of {} total",
1497 deleted_count,
1498 failed_count,
1499 dev_ids.len()
1500 );
1501
1502 if let Some(e) = first_error {
1503 Err(format!(
1504 "Failed to delete all devices: {} succeeded, {} failed. First error: {}",
1505 deleted_count, failed_count, e
1506 ))
1507 } else {
1508 Ok(())
1509 }
1510 }
1511}
1512
1513impl Default for DeviceManager {
1514 fn default() -> Self {
1515 Self::new()
1516 }
1517}
1518
1519impl Drop for DeviceManager {
1520 fn drop(&mut self) {
1521 let _ = self.delete_all();
1522 }
1523}