diff --git a/vhost-device-console/CHANGELOG.md b/vhost-device-console/CHANGELOG.md index ca27dfdf6..8d521ef77 100644 --- a/vhost-device-console/CHANGELOG.md +++ b/vhost-device-console/CHANGELOG.md @@ -4,6 +4,7 @@ ### Added - [#811](https://github.com/rust-vmm/vhost-device/pull/811) Being able to specify max queue size +- [#821](https://github.com/rust-vmm/vhost-device/pull/821) Add Unix domain socket backend support ### Changed diff --git a/vhost-device-console/README.md b/vhost-device-console/README.md index ddec15341..8a8a1e648 100644 --- a/vhost-device-console/README.md +++ b/vhost-device-console/README.md @@ -6,7 +6,7 @@ This program is a vhost-user backend that emulates a VirtIO Console device. The device's binary takes as parameters a socket path, a socket number which is the number of connections, commonly used across all vhost-devices to communicate with the vhost-user frontend devices, and the backend type -"nested" or "network". +"nested" or "network" or "uds". The "nested" backend allows input/output to the guest console through the current terminal. @@ -14,6 +14,9 @@ current terminal. The "network" backend creates a local TCP port (specified on vhost-device-console arguments) and allows input/output to the guest console via that socket. +The "uds" backend creates a unix domain socket (specified on vhost-device-console +arguments) and allows input/output to the guest console via that socket. + This program is tested with QEMU's `vhost-user-device-pci` device. Examples' section below. @@ -39,14 +42,23 @@ vhost-device-console --socket-path= The localhost's port to be used for each guest, this part will be increased with 0,1,2..socket_count-1. + This option is only valid when backend type is "network". --- option:: -b, --backend=nested|network +-- option:: -b, --backend=nested|network|uds The backend type vhost-device-console to be used. The current implementation - supports two types of backends: "nested", "network" (described above). + supports 3 types of backends: "nested", "network", "uds". Note: The nested backend is selected by default and can be used only when socket_count equals 1. +.. option:: --uds-path=uds-file-path + + The unix domain socket to be used for each guest, this path will be suffixed with + 0,1,2..socket_count-1. e.g.: `--uds-path=/tmp/vm.sock --socket-count=2` + leads to two connectable vhost-user sockets: + /tmp/vm.sock0, /tmp/vm.sock1 + This option is only valid when backend type is "uds". + .. option:: -q, --max-queue-size=SIZE The maximum size of virtqueues. It is optional, and the default value is @@ -62,10 +74,10 @@ VIRTIO_CONSOLE_F_SIZE features. ## Features The current device gives access to multiple QEMU guest by providing a login prompt -either by connecting to a localhost server port (network backend) or by creating an -nested command prompt in the current terminal (nested backend). This prompt appears -as soon as the guest is fully booted and gives the ability to user run command as a -in regular terminal. +either by connecting to a localhost server port (network backend) or a unix socket +file (uds backend) or by creating an nested command prompt in the current terminal +(nested backend). This prompt appears as soon as the guest is fully booted and +gives the ability to user run command as a in regular terminal. ## Examples @@ -83,10 +95,15 @@ For testing the device the required dependencies are: The daemon should be started first: ```shell host# vhost-device-console --socket-path=/tmp/console.sock --socket-count=1 \ - --tcp-port=12345 --backend=network + --tcp-port=12345 --backend=network # for network backend +``` +or +```shell +host# vhost-device-console --socket-path=/tmp/console.sock --socket-count=1 \ + --uds-path=/tmp/vm.sock --backend=uds # for uds backend ``` >Note: In case the backend is "nested" there is no need to provide - "--socket-count" and "--tcp-port" parameters. + "--socket-count", "--tcp-port" and "--uds-path" parameters. The QEMU invocation needs to create a chardev socket the device can use to communicate as well as share the guests memory over a memfd. @@ -119,9 +136,26 @@ host# qemu-system ... ``` +#### Test the device with UML +Start the daemon as above section, then run the following cmdline for +Kernel Mode Linux [`virtio console`](https://github.com/torvalds/linux/blob/848e076317446f9c663771ddec142d7c2eb4cb43/include/uapi/linux/virtio_ids.h#L34): +```text +host# linux root=/dev/ubda1 rw ubd0=$YOUR-PATH/kata-ubuntu-latest.image \ + \ + virtio_uml.device=/tmp/console.sock0:3 console=tty0 console=hvc0 \ + init=/bin/systemd \ + systemd.unit=kata-containers.target agent.debug_console +``` +Test with [kata-ubuntu-latest.image](https://github.com/kata-containers/kata-containers/releases/), +you can also use systemd to setup pty manually without kata-agent help. + Eventually, the user can connect to the console by running: ```test -host# stty -icanon -echo && nc localhost 12345 && stty echo +host# stty -icanon -echo && nc localhost 12345 && stty echo # for network backend +``` +or +```test +host# stty -icanon -echo && nc -U /tmp/vm.sock0 && stty echo # for uds backend ``` >Note: `stty -icanon -echo` is used to force the tty layer to disable buffering and send / receive each character individually. After closing the connection please run `stty echo` so character are printed back on the local terminal console. diff --git a/vhost-device-console/src/backend.rs b/vhost-device-console/src/backend.rs index cc5709ef8..55d7ad8a4 100644 --- a/vhost-device-console/src/backend.rs +++ b/vhost-device-console/src/backend.rs @@ -42,11 +42,16 @@ pub enum Error { ThreadPanic(String, Box), #[error("Error using multiple sockets with Nested backend")] WrongBackendSocket, + #[error("Invalid cmdline option")] + InvalidCmdlineOption, + #[error("Invalid uds file")] + InvalidUdsFile, } #[derive(PartialEq, Eq, Debug)] pub struct VuConsoleConfig { pub socket_path: PathBuf, + pub uds_path: PathBuf, pub backend: BackendType, pub tcp_port: String, pub socket_count: u32, @@ -73,15 +78,40 @@ impl VuConsoleConfig { (0..self.socket_count).map(make_socket_path).collect() } - pub fn generate_tcp_addrs(&self) -> Vec { - let tcp_port_base = self.tcp_port.clone(); - - let make_tcp_port = |i: u32| -> String { - let port_num: u32 = tcp_port_base.clone().parse().unwrap(); - "127.0.0.1:".to_owned() + &(port_num + i).to_string() - }; - - (0..self.socket_count).map(make_tcp_port).collect() + pub fn generate_vm_socks(&self) -> Vec { + match self.backend { + // if type is Nested, result will be dropped. + BackendType::Nested => { + vec![String::new()] + } + + BackendType::Network => { + let port_base: u32 = self.tcp_port.parse().unwrap(); + let make_tcp_port = + |i: u32| -> String { "127.0.0.1:".to_owned() + &(port_base + i).to_string() }; + (0..self.socket_count).map(make_tcp_port).collect() + } + + BackendType::Uds => { + let uds_filename = self.uds_path.file_name().expect("uds has no filename."); + let uds_parent = self + .uds_path + .parent() + .expect("uds has no parent directory."); + + let make_uds_path = |i: u32| -> String { + let mut filename = uds_filename.to_os_string(); + filename.push(std::ffi::OsStr::new(&i.to_string())); + uds_parent + .join(&filename) + .to_str() + .expect("Path contains invalid UTF-8 characters") + .to_string() + }; + + (0..self.socket_count).map(make_uds_path).collect() + } + } } } @@ -89,7 +119,7 @@ impl VuConsoleConfig { /// vhost-device-console backend server. pub fn start_backend_server( socket: PathBuf, - tcp_addr: String, + vm_sock: String, backend: BackendType, max_queue_size: usize, ) -> Result<()> { @@ -104,7 +134,7 @@ pub fn start_backend_server( vu_console_backend .write() .unwrap() - .assign_input_method(tcp_addr.clone()) + .assign_input_method(vm_sock.clone()) .map_err(Error::CouldNotInitBackend)?; let mut daemon = VhostUserDaemon::new( @@ -132,26 +162,26 @@ pub fn start_backend_server( pub fn start_backend(config: VuConsoleConfig) -> Result<()> { let mut handles = HashMap::new(); let (senders, receiver) = std::sync::mpsc::channel(); - let tcp_addrs = config.generate_tcp_addrs(); + let vm_socks = config.generate_vm_socks(); let backend = config.backend; let max_queue_size = config.max_queue_size; - for (thread_id, (socket, tcp_addr)) in config + for (thread_id, (socket, vm_sock)) in config .generate_socket_paths() .into_iter() - .zip(tcp_addrs.iter()) + .zip(vm_socks.iter()) .enumerate() { - let tcp_addr = tcp_addr.clone(); + let vm_sock = vm_sock.clone(); info!("thread_id: {}, socket: {:?}", thread_id, socket); - let name = format!("vhu-console-{}", tcp_addr); + let name = format!("vhu-console-{}", vm_sock); let sender = senders.clone(); let handle = Builder::new() .name(name.clone()) .spawn(move || { let result = std::panic::catch_unwind(move || { - start_backend_server(socket, tcp_addr.to_string(), backend, max_queue_size) + start_backend_server(socket, vm_sock.to_string(), backend, max_queue_size) }); // Notify the main thread that we are done. @@ -187,8 +217,9 @@ mod tests { fn test_console_valid_configuration_nested() { let args = ConsoleArgs { socket_path: String::from("/tmp/vhost.sock").into(), + uds_path: None, backend: BackendType::Nested, - tcp_port: String::from("12345"), + tcp_port: None, socket_count: 1, max_queue_size: DEFAULT_QUEUE_SIZE, }; @@ -200,8 +231,9 @@ mod tests { fn test_console_invalid_configuration_nested_1() { let args = ConsoleArgs { socket_path: String::from("/tmp/vhost.sock").into(), + uds_path: None, backend: BackendType::Nested, - tcp_port: String::from("12345"), + tcp_port: None, socket_count: 0, max_queue_size: DEFAULT_QUEUE_SIZE, }; @@ -216,8 +248,9 @@ mod tests { fn test_console_invalid_configuration_nested_2() { let args = ConsoleArgs { socket_path: String::from("/tmp/vhost.sock").into(), + uds_path: None, backend: BackendType::Nested, - tcp_port: String::from("12345"), + tcp_port: None, socket_count: 2, max_queue_size: DEFAULT_QUEUE_SIZE, }; @@ -232,8 +265,9 @@ mod tests { fn test_console_valid_configuration_network_1() { let args = ConsoleArgs { socket_path: String::from("/tmp/vhost.sock").into(), + uds_path: None, backend: BackendType::Network, - tcp_port: String::from("12345"), + tcp_port: Some(String::from("12345")), socket_count: 1, max_queue_size: DEFAULT_QUEUE_SIZE, }; @@ -245,8 +279,9 @@ mod tests { fn test_console_valid_configuration_network_2() { let args = ConsoleArgs { socket_path: String::from("/tmp/vhost.sock").into(), + uds_path: None, backend: BackendType::Network, - tcp_port: String::from("12345"), + tcp_port: Some(String::from("12345")), socket_count: 2, max_queue_size: DEFAULT_QUEUE_SIZE, }; @@ -257,16 +292,16 @@ mod tests { fn test_backend_start_and_stop(args: ConsoleArgs) -> Result<()> { let config = VuConsoleConfig::try_from(args).expect("Wrong config"); - let tcp_addrs = config.generate_tcp_addrs(); + let vm_socks = config.generate_vm_socks(); let backend = config.backend; let max_queue_size = config.max_queue_size; - for (socket, tcp_addr) in config + for (socket, vm_sock) in config .generate_socket_paths() .into_iter() - .zip(tcp_addrs.iter()) + .zip(vm_socks.iter()) { - start_backend_server(socket, tcp_addr.to_string(), backend, max_queue_size)?; + start_backend_server(socket, vm_sock.to_string(), backend, max_queue_size)?; } Ok(()) } @@ -275,8 +310,9 @@ mod tests { fn test_start_backend_server_success() { let args = ConsoleArgs { socket_path: String::from("/not_a_dir/vhost.sock").into(), + uds_path: None, backend: BackendType::Network, - tcp_port: String::from("12345"), + tcp_port: Some(String::from("12345")), socket_count: 1, max_queue_size: DEFAULT_QUEUE_SIZE, }; @@ -288,6 +324,7 @@ mod tests { fn test_start_backend_success() { let config = VuConsoleConfig { socket_path: String::from("/not_a_dir/vhost.sock").into(), + uds_path: PathBuf::new(), backend: BackendType::Network, tcp_port: String::from("12346"), socket_count: 1, @@ -296,4 +333,51 @@ mod tests { assert!(start_backend(config).is_err()); } + + #[test] + fn test_console_invalid_uds_path() { + let args = ConsoleArgs { + socket_path: PathBuf::from("/tmp/vhost.sock"), + uds_path: Some("/non_existing_dir/test.sock".to_string().into()), + backend: BackendType::Uds, + tcp_port: Some(String::new()), + socket_count: 1, + max_queue_size: 128, + }; + + assert_matches!(VuConsoleConfig::try_from(args), Err(Error::InvalidUdsFile)); + } + + #[test] + fn test_generate_vm_sock_addrs_uds() { + let config = VuConsoleConfig { + socket_path: PathBuf::new(), + uds_path: "/tmp/vm.sock".to_string().into(), + backend: BackendType::Uds, + tcp_port: String::new(), + socket_count: 3, + max_queue_size: 128, + }; + + let addrs = config.generate_vm_socks(); + assert_eq!( + addrs, + vec!["/tmp/vm.sock0", "/tmp/vm.sock1", "/tmp/vm.sock2"] + ); + } + + #[test] + fn test_start_uds_backend_with_invalid_path() { + let config = VuConsoleConfig { + socket_path: PathBuf::from("/tmp/vhost.sock"), + uds_path: "/invalid/path/uds.sock".to_string().into(), + backend: BackendType::Uds, + tcp_port: String::new(), + socket_count: 1, + max_queue_size: 128, + }; + + let result = start_backend(config); + assert!(result.is_err()); + } } diff --git a/vhost-device-console/src/console.rs b/vhost-device-console/src/console.rs index 4646e14a1..985ede70b 100644 --- a/vhost-device-console/src/console.rs +++ b/vhost-device-console/src/console.rs @@ -15,6 +15,7 @@ pub enum BackendType { #[default] Nested, Network, + Uds, } #[derive(Debug)] diff --git a/vhost-device-console/src/main.rs b/vhost-device-console/src/main.rs index 508e6ff69..a7a4fa9f1 100644 --- a/vhost-device-console/src/main.rs +++ b/vhost-device-console/src/main.rs @@ -54,19 +54,24 @@ struct ConsoleArgs { #[clap(short = 's', long, value_name = "SOCKET")] socket_path: PathBuf, + /// Virtual machine communication endpoint. + /// Unix domain socket path (e.g., "/tmp/vm.sock"). + #[clap(long, required(false), value_name = "VM_SOCKET")] + uds_path: Option, + /// Number of guests (sockets) to connect to. #[clap(short = 'c', long, default_value_t = 1)] socket_count: u32, - /// Console backend (Network, Nested) to be used. + /// Console backend (Network, Nested, Uds) to be used. #[clap(short = 'b', long, value_enum, default_value = "nested")] backend: BackendType, /// Initial tcp port to be used with `network` backend. If socket_count is /// `N` then the following tcp ports will be created: `tcp_port`, /// `tcp_port + 1`, ... , `tcp_port + (N - 1)`. - #[clap(short = 'p', long, value_name = "PORT", default_value = "12345")] - tcp_port: String, + #[clap(short = 'p', long, value_name = "PORT")] + tcp_port: Option, /// Specify the maximum size of virtqueue, the default is 128. #[clap(short = 'q', long, default_value_t = DEFAULT_QUEUE_SIZE)] @@ -81,22 +86,64 @@ impl TryFrom for VuConsoleConfig { return Err(Error::SocketCountInvalid(0)); } - if (args.backend == BackendType::Nested) && (args.socket_count != 1) { - return Err(Error::WrongBackendSocket); + if args.backend == BackendType::Nested { + if args.socket_count != 1 { + return Err(Error::WrongBackendSocket); + } + + if (args.tcp_port.as_ref().map_or(false, |s| !s.is_empty())) + || (args + .uds_path + .as_ref() + .map_or(false, |path| !path.as_os_str().is_empty())) + { + return Err(Error::InvalidCmdlineOption); + } + } + + if args.backend == BackendType::Network + && args + .uds_path + .as_ref() + .map_or(false, |path| !path.as_os_str().is_empty()) + { + return Err(Error::InvalidCmdlineOption); + } + + if args.backend == BackendType::Uds + && args.tcp_port.as_ref().map_or(false, |s| !s.is_empty()) + { + return Err(Error::InvalidCmdlineOption); } let ConsoleArgs { socket_path, + uds_path, backend, tcp_port, socket_count, max_queue_size, } = args; + // check validation of uds_path under Uds mode. + if backend == BackendType::Uds { + let path = uds_path + .as_ref() + .filter(|p| !p.as_os_str().is_empty()) + .ok_or(Error::InvalidUdsFile)?; + + if let Some(parent_dir) = path.parent() { + if !parent_dir.exists() { + return Err(Error::InvalidUdsFile); + } + } + } + Ok(Self { socket_path, + uds_path: uds_path.unwrap_or_default(), backend, - tcp_port, + tcp_port: tcp_port.unwrap_or_default(), socket_count, max_queue_size, }) diff --git a/vhost-device-console/src/vhu_console.rs b/vhost-device-console/src/vhu_console.rs index e6e532fdb..7f8e71b37 100644 --- a/vhost-device-console/src/vhu_console.rs +++ b/vhost-device-console/src/vhu_console.rs @@ -9,6 +9,7 @@ use std::{ io::{self, Read, Result as IoResult, Write}, net::TcpListener, os::fd::{AsRawFd, RawFd}, + os::unix::net::UnixListener, slice::from_raw_parts, sync::{Arc, RwLock}, }; @@ -136,7 +137,8 @@ pub struct VhostUserConsoleBackend { pub ready_to_write: bool, pub output_queue: Queue, pub stdin: Option>, - pub listener: Option, + pub tcp_listener: Option, + pub uds_listener: Option, pub stream: Option>, pub rx_event: EventFd, pub rx_ctrl_event: EventFd, @@ -165,7 +167,8 @@ impl VhostUserConsoleBackend { output_queue: Queue::new(), stdin: None, stream: None, - listener: None, + tcp_listener: None, + uds_listener: None, rx_event: EventFd::new(EFD_NONBLOCK).map_err(|_| Error::EventFdFailed)?, rx_ctrl_event: EventFd::new(EFD_NONBLOCK).map_err(|_| Error::EventFdFailed)?, exit_event: EventFd::new(EFD_NONBLOCK).map_err(|_| Error::EventFdFailed)?, @@ -173,20 +176,28 @@ impl VhostUserConsoleBackend { }) } - pub fn assign_input_method(&mut self, tcpaddr_str: String) -> Result<()> { - if self.controller.read().unwrap().backend == BackendType::Nested { - // Enable raw mode for local terminal if backend is nested - enable_raw_mode().expect("Raw mode error"); + pub fn assign_input_method(&mut self, vm_sock: String) -> Result<()> { + let backend_type = self.controller.read().unwrap().backend; + match backend_type { + BackendType::Nested => { + // Enable raw mode for local terminal if backend is nested + enable_raw_mode().expect("Raw mode error"); + let stdin_fd = io::stdin().as_raw_fd(); + let stdin: Box = Box::new(io::stdin()); + self.stdin = Some(stdin); + Self::epoll_register(self.epoll_fd.as_raw_fd(), stdin_fd, epoll::Events::EPOLLIN) + .map_err(|_| Error::EpollAdd)?; + } - let stdin_fd = io::stdin().as_raw_fd(); - let stdin: Box = Box::new(io::stdin()); - self.stdin = Some(stdin); + BackendType::Network => { + let tcp_listener = TcpListener::bind(vm_sock).expect("Failed bind tcp address"); + self.tcp_listener = Some(tcp_listener); + } - Self::epoll_register(self.epoll_fd.as_raw_fd(), stdin_fd, epoll::Events::EPOLLIN) - .map_err(|_| Error::EpollAdd)?; - } else { - let listener = TcpListener::bind(tcpaddr_str).expect("asdasd"); - self.listener = Some(listener); + BackendType::Uds => { + let uds_listener = UnixListener::bind(vm_sock).expect("Failed bind uds address"); + self.uds_listener = Some(uds_listener); + } } Ok(()) } @@ -264,14 +275,19 @@ impl VhostUserConsoleBackend { } let my_string = String::from_utf8(tx_data).unwrap(); - if self.controller.read().unwrap().backend == BackendType::Nested { - print!("{}", my_string); - io::stdout().flush().unwrap(); - } else { - self.output_queue - .add(my_string) - .map_err(|_| Error::RxCtrlQueueAddFailed)?; - self.write_tcp_stream(); + let backend_type = self.controller.read().unwrap().backend; + match backend_type { + BackendType::Nested => { + print!("{}", my_string); + io::stdout().flush().unwrap(); + } + + BackendType::Network | BackendType::Uds => { + self.output_queue + .add(my_string) + .map_err(|_| Error::RxCtrlQueueAddFailed)?; + self.write_stream(); + } } vring @@ -529,15 +545,39 @@ impl VhostUserConsoleBackend { .register_listener(epoll_fd, EventSet::IN, u64::from(QueueEvents::KEY_EFD)) .unwrap(); - if self.controller.read().unwrap().backend == BackendType::Network { - let listener_fd = self.listener.as_ref().expect("asd").as_raw_fd(); - vring_worker - .register_listener( - listener_fd, - EventSet::IN, - u64::from(QueueEvents::LISTENER_EFD), - ) - .unwrap(); + let backend_type = self.controller.read().unwrap().backend; + match backend_type { + BackendType::Nested => {} + + BackendType::Network => { + let listener_fd = self + .tcp_listener + .as_ref() + .expect("Failed get tcp listener ref") + .as_raw_fd(); + vring_worker + .register_listener( + listener_fd, + EventSet::IN, + u64::from(QueueEvents::LISTENER_EFD), + ) + .unwrap(); + } + + BackendType::Uds => { + let listener_fd = self + .uds_listener + .as_ref() + .expect("uds listener") + .as_raw_fd(); + vring_worker + .register_listener( + listener_fd, + EventSet::IN, + u64::from(QueueEvents::LISTENER_EFD), + ) + .unwrap(); + } } } @@ -566,40 +606,76 @@ impl VhostUserConsoleBackend { Ok(()) } + /// Sets up a new stream connection with proper epoll registration + fn handle_stream_connection(&mut self, stream: T, addr_desc: String) + where + T: ReadWrite + Send + Sync + AsRawFd + 'static, + { + println!("New connection on: {}", addr_desc); + let stream_raw_fd = stream.as_raw_fd(); + self.stream_fd = Some(stream_raw_fd); + + if let Err(err) = Self::epoll_register( + self.epoll_fd.as_raw_fd(), + stream_raw_fd, + epoll::Events::EPOLLIN, + ) { + warn!("Failed to register with epoll: {:?}", err); + } + + self.stream = Some(Box::new(stream)); + self.write_stream(); + } + fn create_new_stream_thread(&mut self) { // Accept only one incoming connection - if let Some(stream) = self.listener.as_ref().expect("asd").incoming().next() { - match stream { - Ok(stream) => { - let local_addr = self - .listener - .as_ref() - .expect("No listener") - .local_addr() - .unwrap(); - println!("New connection on: {}", local_addr); - let stream_raw_fd = stream.as_raw_fd(); - self.stream_fd = Some(stream_raw_fd); - if let Err(err) = Self::epoll_register( - self.epoll_fd.as_raw_fd(), - stream_raw_fd, - epoll::Events::EPOLLIN, - ) { - warn!("Failed to register with epoll: {:?}", err); - } - - let stream: Box = Box::new(stream); - self.stream = Some(stream); - self.write_tcp_stream(); + let backend_type = self.controller.read().unwrap().backend; + match backend_type { + BackendType::Nested => {} + + BackendType::Network => match self + .tcp_listener + .as_ref() + .expect("No tcp listener") + .incoming() + .next() + { + Some(Ok(tcp_stream)) => { + let addr_desc = tcp_stream + .peer_addr() + .map(|addr| format!("TCP {}", addr)) + .unwrap_or_else(|_| "unknown TCP peer".to_string()); + self.handle_stream_connection(tcp_stream, addr_desc); } - Err(e) => { - eprintln!("Stream error: {}", e); + Some(Err(e)) => eprintln!("TCP stream error: {}", e), + None => {} + }, + + BackendType::Uds => match self + .uds_listener + .as_ref() + .expect("uds connection") + .incoming() + .next() + { + Some(Ok(uds_stream)) => { + let addr_desc = self + .uds_listener + .as_ref() + .and_then(|l| l.local_addr().ok()) + .and_then(|addr| { + addr.as_pathname().map(|p| p.to_string_lossy().into_owned()) + }) + .unwrap_or_else(|| "unknown UDS path".to_string()); + self.handle_stream_connection(uds_stream, addr_desc); } - } + Some(Err(e)) => eprintln!("UDS stream error: {}", e), + None => {} + }, } } - fn write_tcp_stream(&mut self) { + fn write_stream(&mut self) { if self.stream.is_some() { while self.output_queue.size() > 0 { let byte_stream = self @@ -620,18 +696,38 @@ impl VhostUserConsoleBackend { } } - fn read_tcp_stream(&mut self) { + fn read_stream(&mut self) { let mut buffer = [0; 1024]; match self.stream.as_mut().expect("No stream").read(&mut buffer) { Ok(bytes_read) => { if bytes_read == 0 { - let local_addr = self - .listener - .as_ref() - .expect("No listener") - .local_addr() - .unwrap(); - println!("Close connection on: {}", local_addr); + let backend_type = self.controller.read().unwrap().backend; + + // Get connection address for logging + let conn_addr = match backend_type { + BackendType::Nested => String::new(), + + BackendType::Network => self + .tcp_listener + .as_ref() + .and_then(|l| l.local_addr().ok()) + .map(|addr| addr.to_string()) + .unwrap_or_else(|| "unknown TCP address".to_string()), + + BackendType::Uds => self + .uds_listener + .as_ref() + .and_then(|l| l.local_addr().ok()) + .and_then(|addr| { + addr.as_pathname().map(|p| p.to_string_lossy().into_owned()) + }) + .unwrap_or_else(|| "unknown UDS path".to_string()), + }; + + if !conn_addr.is_empty() { + println!("Close connection on: {}", conn_addr); + } + if let Err(err) = Self::epoll_unregister( self.epoll_fd.as_raw_fd(), self.stream_fd.expect("No stream fd"), @@ -682,9 +778,24 @@ impl VhostUserConsoleBackend { } pub fn prepare_exit(&self) { - /* For the nested backend */ - if self.controller.read().unwrap().backend == BackendType::Nested { - disable_raw_mode().expect("Raw mode error"); + let backend_type = self.controller.read().unwrap().backend; + match backend_type { + /* For the nested backend */ + BackendType::Nested => { + disable_raw_mode().expect("Cannot reset terminal"); + } + BackendType::Network => {} + BackendType::Uds => { + // Clean up UDS socket file + if let Some(path) = self + .uds_listener + .as_ref() + .and_then(|listener| listener.local_addr().ok()) + .and_then(|addr| addr.as_pathname().map(|p| p.to_owned())) + { + std::fs::remove_file(&path).expect("Failed to remove UDS socket file"); + } + } } } } @@ -772,11 +883,15 @@ impl VhostUserBackendMut for VhostUserConsoleBackend { } if device_event == QueueEvents::KEY_EFD { - if self.controller.read().unwrap().backend == BackendType::Nested { - return self.read_char_thread(); - } else { - self.read_tcp_stream(); - return Ok(()); + let backend_type = self.controller.read().unwrap().backend; + match backend_type { + BackendType::Nested => { + return self.read_char_thread(); + } + BackendType::Network | BackendType::Uds => { + self.read_stream(); + return Ok(()); + } } } @@ -1418,7 +1533,7 @@ mod tests { .output_queue .add("Test".to_string()) .unwrap(); - vu_console_backend.write_tcp_stream(); + vu_console_backend.write_stream(); // All data has been consumed by the cursor assert_eq!(vu_console_backend.output_queue.size(), 0);