Skip to content

Commit 3808f9d

Browse files
Erik Schillingstefano-garzarella
authored andcommitted
vhost-user-backend: add convenience function serve
We figured that all vhost-device daemons just ended up copying code from each other. During an earlier attempt to solve that with a helper crate for vhost-device [1], it was suggested to extend the vhost-user-backend API directly. This way more people can benefit from this. The function just groups together some functions that (at least in vhost-device) are always called together. Some usually "expected" error results are filtered out. The remaining additions are extending the mock backend so that it allows sensible testing of exit events and the test for the functionality itself. [1] rust-vmm/vhost-device#362 Signed-off-by: Erik Schilling <[email protected]>
1 parent f699ca9 commit 3808f9d

File tree

3 files changed

+116
-6
lines changed

3 files changed

+116
-6
lines changed

crates/vhost-user-backend/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
## [Unreleased]
33

44
### Added
5+
- [[#173]](https://github.com/rust-vmm/vhost/pull/173) vhost-user-backend: Added convenience function `serve`
56

67
### Changed
78
- Change uses of master/slave for frontend/backend in the codebase.

crates/vhost-user-backend/src/backend.rs

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -395,22 +395,34 @@ where
395395
pub mod tests {
396396
use super::*;
397397
use crate::VringRwLock;
398+
use libc::EFD_NONBLOCK;
398399
use std::sync::Mutex;
399400
use vm_memory::{GuestAddress, GuestMemoryAtomic, GuestMemoryMmap};
400401

401402
pub struct MockVhostBackend {
402403
events: u64,
403404
event_idx: bool,
404405
acked_features: u64,
406+
exit_event_fds: Vec<EventFd>,
405407
}
406408

407409
impl MockVhostBackend {
408410
pub fn new() -> Self {
409-
MockVhostBackend {
411+
let mut backend = MockVhostBackend {
410412
events: 0,
411413
event_idx: false,
412414
acked_features: 0,
413-
}
415+
exit_event_fds: vec![],
416+
};
417+
418+
// Create a event_fd for each thread. We make it NONBLOCKing in
419+
// order to allow tests maximum flexibility in checking whether
420+
// signals arrived or not.
421+
backend.exit_event_fds = (0..backend.queues_per_thread().len())
422+
.map(|_| EventFd::new(EFD_NONBLOCK).unwrap())
423+
.collect();
424+
425+
backend
414426
}
415427
}
416428

@@ -464,10 +476,13 @@ pub mod tests {
464476
vec![1, 1]
465477
}
466478

467-
fn exit_event(&self, _thread_index: usize) -> Option<EventFd> {
468-
let event_fd = EventFd::new(0).unwrap();
469-
470-
Some(event_fd)
479+
fn exit_event(&self, thread_index: usize) -> Option<EventFd> {
480+
Some(
481+
self.exit_event_fds
482+
.get(thread_index)?
483+
.try_clone()
484+
.expect("Could not clone exit eventfd"),
485+
)
471486
}
472487

473488
fn handle_event(

crates/vhost-user-backend/src/lib.rs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
extern crate log;
1010

1111
use std::fmt::{Display, Formatter};
12+
use std::path::Path;
1213
use std::sync::{Arc, Mutex};
1314
use std::thread;
1415

@@ -45,6 +46,8 @@ pub enum Error {
4546
CreateBackendListener(VhostUserError),
4647
/// Failed creating vhost-user backend handler.
4748
CreateBackendReqHandler(VhostUserError),
49+
/// Failed creating listener socket
50+
CreateVhostUserListener(VhostUserError),
4851
/// Failed starting daemon thread.
4952
StartDaemon(std::io::Error),
5053
/// Failed waiting for daemon thread.
@@ -61,6 +64,9 @@ impl Display for Error {
6164
Error::CreateBackendReqHandler(e) => {
6265
write!(f, "cannot create backend req handler: {}", e)
6366
}
67+
Error::CreateVhostUserListener(e) => {
68+
write!(f, "cannot create vhost-user listener: {}", e)
69+
}
6470
Error::StartDaemon(e) => write!(f, "failed to start daemon: {}", e),
6571
Error::WaitDaemon(_e) => write!(f, "failed to wait for daemon exit"),
6672
Error::HandleRequest(e) => write!(f, "failed to handle request: {}", e),
@@ -145,6 +151,9 @@ where
145151
///
146152
/// This runs in an infinite loop that should be terminating once the other end of the socket
147153
/// (the VMM) disconnects.
154+
///
155+
/// *Note:* A convenience function [VhostUserDaemon::serve] exists that
156+
/// may be a better option than this for simple use-cases.
148157
// TODO: the current implementation has limitations that only one incoming connection will be
149158
// handled from the listener. Should it be enhanced to support reconnection?
150159
pub fn start(&mut self, listener: Listener) -> Result<()> {
@@ -168,6 +177,9 @@ where
168177
}
169178

170179
/// Wait for the thread handling the vhost-user socket connection to terminate.
180+
///
181+
/// *Note:* A convenience function [VhostUserDaemon::serve] exists that
182+
/// may be a better option than this for simple use-cases.
171183
pub fn wait(&mut self) -> Result<()> {
172184
if let Some(handle) = self.main_thread.take() {
173185
match handle.join().map_err(Error::WaitDaemon)? {
@@ -180,6 +192,42 @@ where
180192
}
181193
}
182194

195+
/// Bind to socket, handle a single connection and shutdown
196+
///
197+
/// This is a convenience function that provides an easy way to handle the
198+
/// following actions without needing to call the low-level functions:
199+
/// - Create a listener
200+
/// - Start listening
201+
/// - Handle a single event
202+
/// - Send the exit event to all handler threads
203+
///
204+
/// Internal `Err` results that indicate a device disconnect will be treated
205+
/// as success and `Ok(())` will be returned in those cases.
206+
///
207+
/// *Note:* See [VhostUserDaemon::start] and [VhostUserDaemon::wait] if you
208+
/// need more flexibility.
209+
pub fn serve<P: AsRef<Path>>(&mut self, socket: P) -> Result<()> {
210+
let listener = Listener::new(socket, true).map_err(Error::CreateVhostUserListener)?;
211+
212+
self.start(listener)?;
213+
let result = self.wait();
214+
215+
// Regardless of the result, we want to signal worker threads to exit
216+
self.handler.lock().unwrap().send_exit_event();
217+
218+
// For this convenience function we are not treating certain "expected"
219+
// outcomes as error. Disconnects and partial messages can be usual
220+
// behaviour seen from quitting guests.
221+
match &result {
222+
Err(e) => match e {
223+
Error::HandleRequest(VhostUserError::Disconnected) => Ok(()),
224+
Error::HandleRequest(VhostUserError::PartialMessage) => Ok(()),
225+
_ => result,
226+
},
227+
_ => result,
228+
}
229+
}
230+
183231
/// Retrieve the vring epoll handler.
184232
///
185233
/// This is necessary to perform further actions like registering and unregistering some extra
@@ -194,8 +242,10 @@ where
194242
mod tests {
195243
use super::backend::tests::MockVhostBackend;
196244
use super::*;
245+
use libc::EAGAIN;
197246
use std::os::unix::net::{UnixListener, UnixStream};
198247
use std::sync::Barrier;
248+
use std::time::Duration;
199249
use vm_memory::{GuestAddress, GuestMemoryAtomic, GuestMemoryMmap};
200250

201251
#[test]
@@ -264,6 +314,50 @@ mod tests {
264314
daemon.wait().unwrap_err();
265315
daemon.wait().unwrap();
266316
});
317+
}
318+
319+
#[test]
320+
fn test_daemon_serve() {
321+
let mem = GuestMemoryAtomic::new(
322+
GuestMemoryMmap::<()>::from_ranges(&[(GuestAddress(0x100000), 0x10000)]).unwrap(),
323+
);
324+
let backend = Arc::new(Mutex::new(MockVhostBackend::new()));
325+
let mut daemon = VhostUserDaemon::new("test".to_owned(), backend.clone(), mem).unwrap();
326+
let tmpdir = tempfile::tempdir().unwrap();
327+
let socket_path = tmpdir.path().join("socket");
328+
329+
thread::scope(|s| {
330+
s.spawn(|| {
331+
let _ = daemon.serve(&socket_path);
332+
});
333+
334+
// We have no way to wait for when the server becomes available...
335+
// So we will have to spin!
336+
while !socket_path.exists() {
337+
thread::sleep(Duration::from_millis(10));
338+
}
339+
340+
// Check that no exit events got triggered yet
341+
for thread_id in 0..backend.queues_per_thread().len() {
342+
let fd = backend.exit_event(thread_id).unwrap();
343+
// Reading from exit fd should fail since nothing was written yet
344+
assert_eq!(
345+
fd.read().unwrap_err().raw_os_error().unwrap(),
346+
EAGAIN,
347+
"exit event should not have been raised yet!"
348+
);
349+
}
350+
351+
let socket = UnixStream::connect(&socket_path).unwrap();
352+
// disconnect immediately again
353+
drop(socket);
354+
});
267355

356+
// Check that exit events got triggered
357+
let backend = backend.lock().unwrap();
358+
for thread_id in 0..backend.queues_per_thread().len() {
359+
let fd = backend.exit_event(thread_id).unwrap();
360+
assert!(fd.read().is_ok(), "No exit event was raised!");
361+
}
268362
}
269363
}

0 commit comments

Comments
 (0)