use std::{
fs::File,
io::Write,
path::Path,
sync::{Arc, Mutex},
};
use anyhow::{anyhow, Context, Result};
use nusb::MaybeFuture;
use tokio::runtime;
use tracing::{debug, info, trace};
use usbvfiod::hotplug_protocol::{device_paths::resolve_path, response::Response};
use vfio_bindings::bindings::vfio::{
vfio_region_info, VFIO_PCI_BAR0_REGION_INDEX, VFIO_PCI_BAR1_REGION_INDEX,
VFIO_PCI_BAR2_REGION_INDEX, VFIO_PCI_BAR3_REGION_INDEX, VFIO_PCI_BAR4_REGION_INDEX,
VFIO_PCI_BAR5_REGION_INDEX, VFIO_PCI_CONFIG_REGION_INDEX, VFIO_PCI_MSIX_IRQ_INDEX,
VFIO_PCI_NUM_IRQS, VFIO_PCI_NUM_REGIONS, VFIO_REGION_INFO_FLAG_READ,
VFIO_REGION_INFO_FLAG_WRITE,
};
use vfio_user::{IrqInfo, ServerBackend, ServerRegion};
use crate::device::{
bus::{Request, RequestSize},
interrupt_line::{DummyInterruptLine, InterruptLine},
pci::{traits::PciDevice, xhci::XhciController},
xhci::{
nusb::NusbRealDevice,
port::HotplugControl,
real_device::{CompleteRealDevice, CompleteRealDeviceImpl},
},
};
use crate::{dynamic_bus::DynamicBus, memory_segment::MemorySegment};
#[derive(Debug)]
pub struct XhciBackend<CRD: CompleteRealDevice> {
dma_bus: Arc<DynamicBus>,
controller: XhciController<CRD>,
}
#[derive(Debug)]
struct InterruptEventFd {
fd: Mutex<File>,
}
impl InterruptLine for InterruptEventFd {
fn interrupt(&self) {
std::sync::atomic::fence(std::sync::atomic::Ordering::SeqCst);
let _amount = self
.fd
.lock()
.unwrap()
.write(&1u64.to_le_bytes())
.expect("should always be able to write event fd");
}
}
impl<CRD: CompleteRealDevice> XhciBackend<CRD> {
pub fn new(async_runtime: runtime::Handle) -> Result<Self> {
let dma_bus = Arc::new(DynamicBus::new());
let controller = XhciController::new(dma_bus.clone(), async_runtime);
let backend = Self {
controller,
dma_bus,
};
Ok(backend)
}
pub fn hotplug_control(&self) -> HotplugControl<CRD> {
self.controller.hotplug_control()
}
}
impl XhciBackend<CompleteRealDeviceImpl<NusbRealDevice, (u8, u8)>> {
pub async fn add_device_from_path(
&self,
path: impl AsRef<Path>,
async_runtime: runtime::Handle,
) -> Result<()> {
let (bus, dev, path) = resolve_path(path)?;
let open_file = |err_msg| {
std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(&path)
.with_context(|| format!("{}: {}", err_msg, path.display()))
};
let file = open_file("Failed to open USB device file")?;
let device = nusb::Device::from_fd(file.into()).wait()?;
device.reset().wait()?;
let file = open_file("Failed to open USB device file after device reset")?;
let device = nusb::Device::from_fd(file.into()).wait()?;
let real_device = NusbRealDevice::try_new(device, async_runtime.clone())?;
let complete_device = CompleteRealDeviceImpl::new((bus, dev), real_device);
let response = self.hotplug_control().attach(complete_device).await;
if response != Response::SuccessfulOperation {
return Err(anyhow!(
"initial attach of device {bus:03}:{dev:03} failed: {response:?}"
));
}
Ok(())
}
}
impl<CRD: CompleteRealDevice> XhciBackend<CRD> {
pub fn regions(&self) -> Vec<ServerRegion> {
(0..VFIO_PCI_NUM_REGIONS)
.map(|i| {
let empty_region = ServerRegion {
region_info: vfio_region_info {
argsz: size_of::<vfio_region_info>() as u32,
index: i,
..Default::default()
},
sparse_areas: vec![],
mmap_fd: None,
};
match i {
VFIO_PCI_CONFIG_REGION_INDEX => {
debug!("Client queried config space region");
ServerRegion {
region_info: vfio_region_info {
argsz: size_of::<vfio_region_info>() as u32,
index: i,
size: 256,
flags: VFIO_REGION_INFO_FLAG_READ | VFIO_REGION_INFO_FLAG_WRITE,
..Default::default()
},
sparse_areas: vec![],
mmap_fd: None,
}
}
VFIO_PCI_BAR0_REGION_INDEX
| VFIO_PCI_BAR1_REGION_INDEX
| VFIO_PCI_BAR2_REGION_INDEX
| VFIO_PCI_BAR3_REGION_INDEX
| VFIO_PCI_BAR4_REGION_INDEX
| VFIO_PCI_BAR5_REGION_INDEX => {
let bar_no = i - VFIO_PCI_BAR0_REGION_INDEX;
u8::try_from(bar_no)
.ok()
.and_then(|bar_no| self.controller.bar(bar_no))
.map_or_else(
|| {
debug!("Client queried BAR{bar_no} region: (empty)");
empty_region
},
|bar_info| {
debug!("Client queried BAR{bar_no} region: {:?}", bar_info);
ServerRegion {
region_info: vfio_region_info {
argsz: size_of::<vfio_region_info>() as u32,
index: i,
size: bar_info.size.into(),
flags: VFIO_REGION_INFO_FLAG_READ
| VFIO_REGION_INFO_FLAG_WRITE,
..Default::default()
},
sparse_areas: vec![],
mmap_fd: None,
}
},
)
}
unknown => {
debug!("Client queried unknown VFIO region: {unknown}");
empty_region
}
}
})
.collect()
}
pub fn irqs(&self) -> Vec<IrqInfo> {
(0..VFIO_PCI_NUM_IRQS)
.map(|index| IrqInfo {
index,
count: match index {
VFIO_PCI_MSIX_IRQ_INDEX => 1,
_ => 0,
},
flags: 0,
})
.collect()
}
}
impl<CRD: CompleteRealDevice> ServerBackend for XhciBackend<CRD> {
fn region_read(
&mut self,
region: u32,
offset: u64,
data: &mut [u8],
) -> Result<(), std::io::Error> {
let value: u64 = match region {
VFIO_PCI_CONFIG_REGION_INDEX => self.controller.read_cfg(Request::new(
offset,
RequestSize::try_from(data.len() as u64).expect("should use valid request size"),
)),
0 => self.controller.read_io(
0,
Request::new(
offset,
RequestSize::try_from(data.len() as u64)
.expect("should use valid request size"),
),
),
_ => !0u64,
};
data.copy_from_slice(&value.to_le_bytes()[0..data.len()]);
trace!(
"read region {region} offset {offset:#x}+{} val {:?}",
data.len(),
data
);
Ok(())
}
fn region_write(
&mut self,
region: u32,
offset: u64,
data: &[u8],
) -> Result<(), std::io::Error> {
trace!(
"write region {region} offset {offset:#x}+{} val {:?}",
data.len(),
data
);
match region {
VFIO_PCI_CONFIG_REGION_INDEX => self.controller.write_cfg(
Request::new(
offset,
RequestSize::try_from(data.len() as u64)
.expect("should use valid request size"),
),
match data.len() {
1 => data[0].into(),
2 => {
let val: [u8; 2] = data.try_into().unwrap();
u16::from_le_bytes(val).into()
}
4 => {
let val: [u8; 4] = data.try_into().unwrap();
u32::from_le_bytes(val).into()
}
_ => todo!(),
},
),
0 => self.controller.write_io(
0,
Request::new(
offset,
RequestSize::try_from(data.len() as u64)
.expect("should use valid request size"),
),
match data.len() {
1 => data[0].into(),
2 => {
let val: [u8; 2] = data.try_into().unwrap();
u16::from_le_bytes(val).into()
}
4 => {
let val: [u8; 4] = data.try_into().unwrap();
u32::from_le_bytes(val).into()
}
_ => todo!(),
},
),
_ => todo!(),
}
Ok(())
}
fn dma_map(
&mut self,
flags: vfio_user::DmaMapFlags,
offset: u64,
address: u64,
size: u64,
fd: Option<File>,
) -> Result<(), std::io::Error> {
info!("dma_map flags = {flags:?} offset = {offset} address = {address} size = {size} fd = {fd:?}");
if let Some(fd) = fd {
let mseg = MemorySegment::new_from_fd(
&fd,
offset,
size,
flags.try_into().expect("Failed to convert flags"),
)?;
self.dma_bus.add(address, Arc::new(mseg)).unwrap();
} else {
todo!("Memory region without file descriptor");
}
Ok(())
}
fn dma_unmap(
&mut self,
_flags: vfio_user::DmaUnmapFlags,
_address: u64,
_size: u64,
) -> Result<(), std::io::Error> {
todo!()
}
fn reset(&mut self) -> Result<(), std::io::Error> {
todo!()
}
fn set_irqs(
&mut self,
index: u32,
flags: u32,
start: u32,
count: u32,
fds: Vec<File>,
) -> Result<(), std::io::Error> {
debug!(
"set IRQs: {index} flags: {flags:#x} start: {start:#x} count: {count:#x} #fds: {}",
fds.len()
);
assert_eq!(
index, VFIO_PCI_MSIX_IRQ_INDEX,
"Only MSI-X interrupts are supported"
);
assert!(count <= 1, "Only a single interrupt is supported");
let irqs: Vec<Arc<InterruptEventFd>> = fds
.into_iter()
.map(|file| {
Arc::new(InterruptEventFd {
fd: Mutex::new(file),
})
})
.collect();
#[allow(clippy::option_if_let_else)]
let irq: Arc<dyn InterruptLine> = match irqs.first() {
Some(eventfd) => eventfd.clone(),
_ => Arc::new(DummyInterruptLine::default()),
};
self.controller.connect_irq(irq);
Ok(())
}
}