use super::config::VirtualDeviceConfig;
use super::state::{RescanSummary, VirtualDeviceState};
use crate::mtp::MtpDeviceInfo;
use std::path::PathBuf;
use std::sync::{Arc, Mutex, OnceLock};
const VIRTUAL_LOCATION_BASE: u64 = 0xFFFF_0000_0000_0000;
struct VirtualRegistration {
info: MtpDeviceInfo,
config: VirtualDeviceConfig,
}
struct Registry {
devices: Vec<VirtualRegistration>,
next_index: u64,
}
fn registry() -> &'static Mutex<Registry> {
static REGISTRY: OnceLock<Mutex<Registry>> = OnceLock::new();
REGISTRY.get_or_init(|| {
Mutex::new(Registry {
devices: Vec::new(),
next_index: 0,
})
})
}
#[must_use]
pub fn register_virtual_device(config: &VirtualDeviceConfig) -> MtpDeviceInfo {
let mut reg = registry().lock().unwrap();
let index = reg.next_index;
reg.next_index += 1;
let location_id = VIRTUAL_LOCATION_BASE + index;
let info = MtpDeviceInfo {
vendor_id: 0xFFFF,
product_id: 0x0001,
manufacturer: Some(config.manufacturer.clone()),
product: Some(config.model.clone()),
serial_number: Some(config.serial.clone()),
location_id,
speed: None,
};
reg.devices.push(VirtualRegistration {
info: info.clone(),
config: config.clone(),
});
info
}
pub fn unregister_virtual_device(location_id: u64) {
let mut reg = registry().lock().unwrap();
reg.devices.retain(|r| r.info.location_id != location_id);
}
pub(crate) fn list_virtual_devices() -> Vec<MtpDeviceInfo> {
let reg = registry().lock().unwrap();
reg.devices.iter().map(|r| r.info.clone()).collect()
}
pub(crate) fn find_virtual_config_by_location(location_id: u64) -> Option<VirtualDeviceConfig> {
let reg = registry().lock().unwrap();
reg.devices
.iter()
.find(|r| r.info.location_id == location_id)
.map(|r| r.config.clone())
}
pub(crate) fn find_virtual_config_by_serial(serial: &str) -> Option<VirtualDeviceConfig> {
let reg = registry().lock().unwrap();
reg.devices
.iter()
.find(|r| r.info.serial_number.as_deref() == Some(serial))
.map(|r| r.config.clone())
}
type ActiveEntry = (String, Arc<Mutex<VirtualDeviceState>>);
fn active_states() -> &'static Mutex<Vec<ActiveEntry>> {
static ACTIVE: OnceLock<Mutex<Vec<ActiveEntry>>> = OnceLock::new();
ACTIVE.get_or_init(|| Mutex::new(Vec::new()))
}
pub(super) fn register_active_state(serial: String, state: Arc<Mutex<VirtualDeviceState>>) {
let mut active = active_states().lock().unwrap();
active.push((serial, state));
}
pub(super) fn unregister_active_state(serial: &str) {
let mut active = active_states().lock().unwrap();
if let Some(pos) = active.iter().position(|(s, _)| s == serial) {
active.remove(pos);
}
}
pub struct WatcherGuard {
state: Arc<Mutex<VirtualDeviceState>>,
}
impl Drop for WatcherGuard {
fn drop(&mut self) {
if let Ok(mut state) = self.state.lock() {
state.pause_count = state.pause_count.saturating_sub(1);
}
}
}
pub fn pause_watcher(serial: &str) -> Option<WatcherGuard> {
let active = active_states().lock().unwrap();
let state_arc = active
.iter()
.find(|(s, _)| s == serial)
.map(|(_, state)| Arc::clone(state))?;
drop(active);
state_arc.lock().unwrap().pause_count += 1;
Some(WatcherGuard { state: state_arc })
}
pub fn dropped_paths_since_pause(serial: &str) -> Vec<PathBuf> {
let active = active_states().lock().unwrap();
let state_arc = match active
.iter()
.find(|(s, _)| s == serial)
.map(|(_, s)| Arc::clone(s))
{
Some(s) => s,
None => return Vec::new(),
};
drop(active);
let state = state_arc.lock().unwrap();
state.dropped_paths.iter().cloned().collect()
}
pub fn was_path_dropped(serial: &str, suffix: &str) -> bool {
dropped_paths_since_pause(serial)
.iter()
.any(|p| p.to_string_lossy().ends_with(suffix))
}
pub fn clear_dropped_paths(serial: &str) {
let active = active_states().lock().unwrap();
let state_arc = match active
.iter()
.find(|(s, _)| s == serial)
.map(|(_, s)| Arc::clone(s))
{
Some(s) => s,
None => return,
};
drop(active);
state_arc.lock().unwrap().dropped_paths.clear();
}
pub fn rescan_virtual_device(serial: &str) -> Option<RescanSummary> {
let active = active_states().lock().unwrap();
let state_arc = active
.iter()
.find(|(s, _)| s == serial)
.map(|(_, state)| Arc::clone(state))?;
drop(active); let mut state = state_arc.lock().unwrap();
Some(state.rescan_backing_dirs())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::transport::virtual_device::config::VirtualStorageConfig;
use std::time::Duration;
fn make_config(serial: &str) -> (VirtualDeviceConfig, tempfile::TempDir) {
let dir = tempfile::tempdir().unwrap();
let config = VirtualDeviceConfig {
manufacturer: "TestCorp".into(),
model: "Virtual Phone".into(),
serial: serial.into(),
storages: vec![VirtualStorageConfig {
description: "Internal Storage".into(),
capacity: 1024 * 1024 * 1024,
backing_dir: dir.path().to_path_buf(),
read_only: false,
}],
supports_rename: true,
event_poll_interval: Duration::ZERO,
watch_backing_dirs: false,
};
(config, dir)
}
#[test]
fn register_and_list() {
let (config, _dir) = make_config("reg-test-001");
let info = register_virtual_device(&config);
assert!(info.location_id >= VIRTUAL_LOCATION_BASE);
assert_eq!(info.serial_number.as_deref(), Some("reg-test-001"));
assert_eq!(info.manufacturer.as_deref(), Some("TestCorp"));
let devices = list_virtual_devices();
assert!(devices
.iter()
.any(|d| d.serial_number.as_deref() == Some("reg-test-001")));
unregister_virtual_device(info.location_id);
}
#[test]
fn find_by_location() {
let (config, _dir) = make_config("reg-test-002");
let info = register_virtual_device(&config);
let found = find_virtual_config_by_location(info.location_id);
assert!(found.is_some());
assert_eq!(found.unwrap().serial, "reg-test-002");
unregister_virtual_device(info.location_id);
}
#[test]
fn find_by_serial() {
let (config, _dir) = make_config("reg-test-003");
let info = register_virtual_device(&config);
let found = find_virtual_config_by_serial("reg-test-003");
assert!(found.is_some());
assert_eq!(found.unwrap().serial, "reg-test-003");
assert!(find_virtual_config_by_serial("nonexistent").is_none());
unregister_virtual_device(info.location_id);
}
#[test]
fn unregister() {
let (config, _dir) = make_config("reg-test-004");
let info = register_virtual_device(&config);
unregister_virtual_device(info.location_id);
assert!(find_virtual_config_by_location(info.location_id).is_none());
assert!(find_virtual_config_by_serial("reg-test-004").is_none());
}
#[test]
fn location_id_unique_after_unregister() {
let (config_a, _dir_a) = make_config("reg-test-unique-a");
let info_a = register_virtual_device(&config_a);
let (config_b, _dir_b) = make_config("reg-test-unique-b");
let info_b = register_virtual_device(&config_b);
unregister_virtual_device(info_a.location_id);
let (config_c, _dir_c) = make_config("reg-test-unique-c");
let info_c = register_virtual_device(&config_c);
assert_ne!(info_c.location_id, info_a.location_id);
assert_ne!(info_c.location_id, info_b.location_id);
unregister_virtual_device(info_b.location_id);
unregister_virtual_device(info_c.location_id);
}
#[tokio::test]
async fn open_by_location_integration() {
let dir = tempfile::tempdir().unwrap();
let config = VirtualDeviceConfig {
manufacturer: "TestCorp".into(),
model: "Registry Phone".into(),
serial: "reg-test-005".into(),
storages: vec![VirtualStorageConfig {
description: "Internal Storage".into(),
capacity: 1024 * 1024 * 1024,
backing_dir: dir.path().to_path_buf(),
read_only: false,
}],
supports_rename: true,
event_poll_interval: Duration::ZERO,
watch_backing_dirs: false,
};
let info = register_virtual_device(&config);
let device = crate::MtpDevice::builder()
.open_by_location(info.location_id)
.await
.unwrap();
assert_eq!(device.device_info().model, "Registry Phone");
unregister_virtual_device(info.location_id);
}
#[tokio::test]
async fn open_by_serial_integration() {
let dir = tempfile::tempdir().unwrap();
let config = VirtualDeviceConfig {
manufacturer: "TestCorp".into(),
model: "Registry Phone".into(),
serial: "reg-test-006".into(),
storages: vec![VirtualStorageConfig {
description: "Internal Storage".into(),
capacity: 1024 * 1024 * 1024,
backing_dir: dir.path().to_path_buf(),
read_only: false,
}],
supports_rename: true,
event_poll_interval: Duration::ZERO,
watch_backing_dirs: false,
};
let info = register_virtual_device(&config);
let device = crate::MtpDevice::builder()
.open_by_serial("reg-test-006")
.await
.unwrap();
assert_eq!(device.device_info().model, "Registry Phone");
unregister_virtual_device(info.location_id);
}
async fn open_test_device(serial: &str) -> (crate::MtpDevice, MtpDeviceInfo) {
let dir = tempfile::tempdir().unwrap();
let backing = dir.keep();
let config = VirtualDeviceConfig {
manufacturer: "TestCorp".into(),
model: "Drain Phone".into(),
serial: serial.into(),
storages: vec![VirtualStorageConfig {
description: "Internal Storage".into(),
capacity: 1024 * 1024 * 1024,
backing_dir: backing,
read_only: false,
}],
supports_rename: true,
event_poll_interval: Duration::ZERO,
watch_backing_dirs: false,
};
let info = register_virtual_device(&config);
let device = crate::MtpDevice::builder()
.open_by_serial(serial)
.await
.unwrap();
(device, info)
}
fn state_of(serial: &str) -> Arc<Mutex<VirtualDeviceState>> {
let active = active_states().lock().unwrap();
active
.iter()
.find(|(s, _)| s == serial)
.map(|(_, s)| Arc::clone(s))
.expect("device must be opened (not just registered) before state lookup")
}
#[tokio::test]
async fn pause_refcount_composes_across_concurrent_guards() {
let (device, info) = open_test_device("pause-refcount-001").await;
let guard_a = pause_watcher("pause-refcount-001").expect("device is open");
assert_eq!(
state_of("pause-refcount-001").lock().unwrap().pause_count,
1
);
let guard_b = pause_watcher("pause-refcount-001").expect("still open");
assert_eq!(
state_of("pause-refcount-001").lock().unwrap().pause_count,
2
);
drop(guard_a);
assert_eq!(
state_of("pause-refcount-001").lock().unwrap().pause_count,
1
);
drop(guard_b);
assert_eq!(
state_of("pause-refcount-001").lock().unwrap().pause_count,
0
);
drop(device); unregister_virtual_device(info.location_id);
}
#[test]
fn pause_watcher_returns_none_for_unknown_serial() {
assert!(pause_watcher("pause-refcount-no-such-serial").is_none());
}
#[tokio::test]
async fn dropped_paths_observation_round_trip() {
let (device, info) = open_test_device("dropped-paths-001").await;
assert!(dropped_paths_since_pause("dropped-paths-001").is_empty());
assert!(!was_path_dropped("dropped-paths-001", "sentinel-xyz"));
{
let state_arc = state_of("dropped-paths-001");
let mut state = state_arc.lock().unwrap();
state
.dropped_paths
.push_back(PathBuf::from("/tmp/cmdr-mtp/internal/foo.txt"));
state
.dropped_paths
.push_back(PathBuf::from("/tmp/cmdr-mtp/internal/sentinel-xyz"));
}
let paths = dropped_paths_since_pause("dropped-paths-001");
assert_eq!(paths.len(), 2);
assert_eq!(paths[0], PathBuf::from("/tmp/cmdr-mtp/internal/foo.txt"));
assert!(was_path_dropped("dropped-paths-001", "sentinel-xyz"));
assert!(was_path_dropped("dropped-paths-001", "/foo.txt")); assert!(!was_path_dropped("dropped-paths-001", "not-there"));
clear_dropped_paths("dropped-paths-001");
assert!(dropped_paths_since_pause("dropped-paths-001").is_empty());
drop(device);
unregister_virtual_device(info.location_id);
}
#[test]
fn dropped_paths_for_unknown_serial_returns_empty() {
assert!(dropped_paths_since_pause("dropped-paths-no-such-serial").is_empty());
assert!(!was_path_dropped(
"dropped-paths-no-such-serial",
"anything"
));
clear_dropped_paths("dropped-paths-no-such-serial"); }
#[tokio::test]
async fn dropped_paths_ring_evicts_oldest_past_cap() {
use crate::transport::virtual_device::state::DROPPED_PATHS_CAP;
let (device, info) = open_test_device("dropped-paths-cap-001").await;
{
let state_arc = state_of("dropped-paths-cap-001");
let mut state = state_arc.lock().unwrap();
for i in 0..(DROPPED_PATHS_CAP + 5) {
state
.dropped_paths
.push_back(PathBuf::from(format!("/tmp/drop-{i}")));
if state.dropped_paths.len() > DROPPED_PATHS_CAP {
state.dropped_paths.pop_front();
}
}
}
let paths = dropped_paths_since_pause("dropped-paths-cap-001");
assert_eq!(paths.len(), DROPPED_PATHS_CAP);
assert_eq!(paths[0], PathBuf::from("/tmp/drop-5"));
assert_eq!(
paths[DROPPED_PATHS_CAP - 1],
PathBuf::from(format!("/tmp/drop-{}", DROPPED_PATHS_CAP + 4))
);
drop(device);
unregister_virtual_device(info.location_id);
}
}