diff --git a/Cargo.lock b/Cargo.lock index f69bfd5..fcbe489 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -654,9 +654,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.25" +version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" +checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" [[package]] name = "match_cfg" @@ -892,6 +892,22 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "provider-clipboard" +version = "0.1.0" +dependencies = [ + "anyhow", + "flume", + "futures", + "guest-metrics", + "log", + "windows 0.61.1", + "windows-service", + "xen-win-utils", + "xenstore-rs", + "xenstore-win", +] + [[package]] name = "provider-memory" version = "0.1.0" @@ -900,7 +916,7 @@ dependencies = [ "futures", "guest-metrics", "smol", - "windows", + "windows 0.61.1", ] [[package]] @@ -930,6 +946,7 @@ dependencies = [ "guest-metrics", "smol", "uname", + "xen-win-utils", ] [[package]] @@ -1238,6 +1255,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "tracing" version = "0.1.41" @@ -1254,6 +1280,17 @@ version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +[[package]] +name = "trait-variant" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70977707304198400eb4835a78f6a9f928bf41bba420deb8fdb175cd965d77a7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "uname" version = "0.1.1" @@ -1425,23 +1462,68 @@ version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" dependencies = [ - "windows-core", + "windows-core 0.58.0", "windows-targets", ] +[[package]] +name = "windows" +version = "0.61.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419" +dependencies = [ + "windows-collections", + "windows-core 0.61.0", + "windows-future", + "windows-link", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.0", +] + [[package]] name = "windows-core" version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" dependencies = [ - "windows-implement", - "windows-interface", - "windows-result", - "windows-strings", + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", + "windows-strings 0.1.0", "windows-targets", ] +[[package]] +name = "windows-core" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" +dependencies = [ + "windows-implement 0.60.0", + "windows-interface 0.59.1", + "windows-link", + "windows-result 0.3.2", + "windows-strings 0.4.0", +] + +[[package]] +name = "windows-future" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32" +dependencies = [ + "windows-core 0.61.0", + "windows-link", +] + [[package]] name = "windows-implement" version = "0.58.0" @@ -1453,6 +1535,17 @@ dependencies = [ "syn", ] +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-interface" version = "0.58.0" @@ -1464,6 +1557,33 @@ dependencies = [ "syn", ] +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.0", + "windows-link", +] + [[package]] name = "windows-result" version = "0.2.0" @@ -1473,6 +1593,15 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-result" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-service" version = "0.8.0" @@ -1490,10 +1619,19 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" dependencies = [ - "windows-result", + "windows-result 0.2.0", "windows-targets", ] +[[package]] +name = "windows-strings" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -1576,6 +1714,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winres" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b68db261ef59e9e52806f688020631e987592bd83619edccda9c47d42cde4f6c" +dependencies = [ + "toml", +] + [[package]] name = "wit-bindgen-rt" version = "0.33.0" @@ -1597,6 +1744,7 @@ dependencies = [ "futures", "guest-metrics", "log", + "provider-clipboard", "provider-memory", "provider-netlink", "provider-os", @@ -1606,8 +1754,28 @@ dependencies = [ "smol", "sysctl", "syslog", - "windows", "windows-service", + "winres", + "xen-win-utils", +] + +[[package]] +name = "xen-win-clipboard" +version = "0.5.0-dev" +dependencies = [ + "anyhow", + "log", + "windows 0.61.1", + "winres", + "xen-win-utils", +] + +[[package]] +name = "xen-win-utils" +version = "0.5.0-dev" +dependencies = [ + "log", + "windows 0.61.1", ] [[package]] @@ -1615,13 +1783,20 @@ name = "xenstore-rs" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8df0b81a6d7038b1540a5368e43febc3b48c05d9426c96f815aba9a33b0e8d45" +dependencies = [ + "futures", + "trait-variant", +] [[package]] name = "xenstore-win" -version = "0.1.0" -source = "git+https://github.com/TSnake41/xenstore-win.git#d79e6dfa10748839403c2a6134a98660d814c0d9" +version = "0.2.0" +source = "git+https://github.com/TSnake41/xenstore-win.git#ea69897c08db0310b8dac50bd5db8b027b03876c" dependencies = [ + "async-io", + "futures", "log", - "windows", + "trait-variant", + "windows 0.58.0", "xenstore-rs", ] diff --git a/Cargo.toml b/Cargo.toml index 71d5f0d..85d0149 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,8 @@ members = [ "providers/provider-simple", "publishers/publisher-xenstore", "publishers/publisher-console", + "xen-win-utils", + "xen-win-clipboard", ] [profile.release] diff --git a/guest-metrics/src/lib.rs b/guest-metrics/src/lib.rs index 04df3a3..00fc702 100644 --- a/guest-metrics/src/lib.rs +++ b/guest-metrics/src/lib.rs @@ -54,6 +54,8 @@ pub struct MemoryInfo { pub mem_total: usize, } +pub type ClipboardData = Box<[u8]>; + pub enum GuestMetric { OperatingSystem(OsInfo), AddIface(NetInterface), @@ -61,6 +63,8 @@ pub enum GuestMetric { Memory(MemoryInfo), Network(NetEvent), CleanupIfaces, + /// clipboard data coming from the guest + GetClipboard(ClipboardData), } -pub use os_info; \ No newline at end of file +pub use os_info; diff --git a/providers/provider-clipboard/Cargo.toml b/providers/provider-clipboard/Cargo.toml new file mode 100644 index 0000000..5ac3e29 --- /dev/null +++ b/providers/provider-clipboard/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "provider-clipboard" +version = "0.1.0" +edition = "2021" + +[dependencies] +futures = "0.3.31" +xenstore-rs = "0.8" +guest-metrics = { path = "../../guest-metrics" } +anyhow = "1.0" +log = "0.4" +flume = "0.11" + +[target."cfg(windows)".dependencies] +windows = {version = "0.61", features = [ + "Win32_System_Pipes", + "Win32_System_RemoteDesktop", + "Win32_System_IO", +]} +windows-service = "0.8" +xen-win-utils = { path = "../../xen-win-utils" } +xenstore-win = { git = "https://github.com/TSnake41/xenstore-win.git", features = ["smol"] } diff --git a/providers/provider-clipboard/src/lib.rs b/providers/provider-clipboard/src/lib.rs new file mode 100644 index 0000000..5c9dabe --- /dev/null +++ b/providers/provider-clipboard/src/lib.rs @@ -0,0 +1,2 @@ +pub mod windows; +mod windows_worker; diff --git a/providers/provider-clipboard/src/windows.rs b/providers/provider-clipboard/src/windows.rs new file mode 100644 index 0000000..774a840 --- /dev/null +++ b/providers/provider-clipboard/src/windows.rs @@ -0,0 +1,157 @@ +use std::thread::JoinHandle; + +use futures::{select, FutureExt, StreamExt}; +use guest_metrics::{plugin::GuestAgentPlugin, GuestMetric}; +use windows::{ + core::Free, + Win32::{ + Foundation::HANDLE, + System::Threading::{CreateEventW, SetEvent}, + }, +}; +use xenstore_rs::{AsyncWatch, AsyncXs}; +use xenstore_win::smol::XsSmolWindows; + +use crate::windows_worker::WindowsClipboardWorker; + +// used for sending event handles across await boundary +#[derive(Clone)] +struct SendHandle(HANDLE); +unsafe impl Send for SendHandle {} + +struct OwnedSendHandle(HANDLE); +unsafe impl Send for OwnedSendHandle {} +impl Drop for OwnedSendHandle { + fn drop(&mut self) { + unsafe { self.0.free() } + } +} + +struct WindowsClipboardPluginState { + worker_thread: JoinHandle>, + stop_event: OwnedSendHandle, +} + +pub struct WindowsClipboardPlugin { + state: Option, +} + +impl WindowsClipboardPlugin { + pub fn new() -> anyhow::Result { + Ok(WindowsClipboardPlugin { state: None }) + } + + async fn rm_set_clipboard(xs: &impl AsyncXs) { + let _ = xs + .rm("data/set_clipboard") + .await + .inspect_err(|e| log::error!("cannot clean up set_clipboard: {e}")); + } + + async fn do_set_clipboard( + xs: &impl AsyncXs, + lines: &mut Vec, + my_sender: &flume::Sender>, + send_event: SendHandle, + ) -> anyhow::Result<()> { + let xs_data = xs + .read("data/set_clipboard") + .await + .inspect_err(|e| log::error!("cannot receive set_clipboard: {e}")) + .unwrap_or(String::new().into()); + Self::rm_set_clipboard(xs).await; + + if xs_data.is_empty() { + let mut multiline = String::new(); + for line in &mut *lines { + multiline.push_str(&line); + multiline.push_str("\r\n"); + } + let _ = my_sender + .send_async(multiline.into_boxed_str().into()) + .await + .inspect_err(|e| log::error!("cannot send clipboard to client: {e}")); + lines.clear(); + unsafe { SetEvent(send_event.0)? }; + } else { + lines.push(xs_data.into_string()); + } + + Ok(()) + } + + pub async fn do_run(&mut self, provider: flume::Sender) -> anyhow::Result<()> { + log::info!("Starting clipboard worker"); + // for sending SetClipboard to guest + let (my_sender, their_receiver) = flume::bounded(1); + // for receiving GetClipboard from guest + let (their_sender, my_receiver) = flume::bounded(1); + + let xs_smol = XsSmolWindows::new() + .await + .expect("Unable to start async xenstore"); + Self::rm_set_clipboard(&xs_smol).await; + + let mut clipboard_watch = xs_smol + .watch("data/set_clipboard") + .await + .expect("Unable to watch clipboard node"); + + let stop_event = unsafe { OwnedSendHandle(CreateEventW(None, false, false, None)?) }; + let send_event = unsafe { OwnedSendHandle(CreateEventW(None, false, false, None)?) }; + let mut worker = + WindowsClipboardWorker::new(stop_event.0, their_sender, their_receiver, send_event.0)?; + + assert!(self.state.is_none()); + self.state.replace(WindowsClipboardPluginState { + worker_thread: std::thread::spawn(move || -> anyhow::Result<()> { + worker + .run() + .inspect_err(|e| log::error!("worker error {e}")) + }), + stop_event, + }); + + let mut lines = Vec::::new(); + loop { + let mut xs_fut = clipboard_watch.next().fuse(); + let mut guest_fut = my_receiver.recv_async().fuse(); + + select! { + _ = xs_fut => { + Self::do_set_clipboard(&xs_smol, &mut lines, &my_sender, SendHandle(send_event.0)).await?; + } + guest_data = guest_fut => { + if let Ok(guest_clipboard) = guest_data { + provider.send_async(GuestMetric::GetClipboard(guest_clipboard)).await?; + } + } + complete => break, + } + } + Ok(()) + } +} + +impl Drop for WindowsClipboardPlugin { + fn drop(&mut self) { + if let Some(state) = self.state.take() { + log::debug!("Stopping clipboard worker thread"); + unsafe { + let _ = SetEvent(state.stop_event.0) + .inspect_err(|e| log::error!("cannot send clipboard worker stop event: {e}")); + }; + let _ = state + .worker_thread + .join() + .inspect_err(|e| log::info!("clipboard worker error: {e:?}")); + log::debug!("Clipboard worker thread stopped"); + } + } +} + +impl GuestAgentPlugin for WindowsClipboardPlugin { + async fn run(mut self, channel: flume::Sender) { + self.do_run(channel).await.expect("clipboard plugin failed"); + } +} diff --git a/providers/provider-clipboard/src/windows_worker.rs b/providers/provider-clipboard/src/windows_worker.rs new file mode 100644 index 0000000..6fbf683 --- /dev/null +++ b/providers/provider-clipboard/src/windows_worker.rs @@ -0,0 +1,305 @@ +use std::collections::HashMap; + +use guest_metrics::ClipboardData; +use windows::{ + core::Owned, + Win32::{ + Foundation::{ERROR_FILE_NOT_FOUND, HANDLE, INVALID_HANDLE_VALUE}, + System::{ + Pipes::GetNamedPipeClientSessionId, + RemoteDesktop::WTSGetActiveConsoleSessionId, + Threading::INFINITE, + IO::{CreateIoCompletionPort, GetQueuedCompletionStatus, OVERLAPPED}, + }, + }, +}; +use xen_win_utils::{ + iocp::EventCompletion, + overlapped::clear_io_completion_port, + pipe_talker::{PipeTalker, PipeTalkerResult}, +}; + +const INVALID_SESSION_ID: u32 = 0xffffffff; + +const FREE_CLIENT_KEY: usize = usize::MAX; +const SET_CLIPBOARD_SIGNALED: usize = usize::MAX - 1; +const STOP_SIGNALED: usize = usize::MAX - 2; + +// limit the creation of pipe instances with a magic prefix +const CLIPBOARD_PIPE_SERVER_PATH: &str = + r"\\.\pipe\ProtectedPrefix\Administrators\XenWinClipboardService"; +// grant FILE_GENERIC_READ | FILE_WRITE_DATA to NT AUTHORITY\INTERACTIVE +// technically FILE_GENERIC_WRITE is of no concern thanks to ProtectedPrefix\Administrators, +// but a custom access mask doesn't cost anything +// no need to specify an owner, it errors out otherwise +const CLIPBOARD_PIPE_SERVER_SDDL: &str = r"D:(A;;0x12008b;;;IU)(A;;FA;;;SY)(A;;FA;;;BA)"; +// arbitrary size limits +const MAX_MESSAGE_SIZE: u32 = 65535; +const MAX_WRITE_QUEUE_SIZE: u32 = 262143; + +struct CompletionPacket { + overlapped: *mut OVERLAPPED, + key: usize, + result: windows::core::Result, +} + +pub(crate) struct WindowsClipboardWorker { + stop_completion: EventCompletion, + sender: flume::Sender, + receiver: flume::Receiver, + recv_completion: EventCompletion, + free_client: Option, + clients: HashMap, + completion_port: Owned, +} + +unsafe impl Send for WindowsClipboardWorker {} + +impl WindowsClipboardWorker { + pub(crate) fn new( + stop_event: HANDLE, + sender: flume::Sender, + receiver: flume::Receiver, + recv_event: HANDLE, + ) -> windows::core::Result { + assert!( + FREE_CLIENT_KEY != INVALID_SESSION_ID as usize, + "32-bit systems are not supported" + ); + let completion_port = + unsafe { Owned::new(CreateIoCompletionPort(INVALID_HANDLE_VALUE, None, 0, 0)?) }; + + let stop_completion = EventCompletion::new(stop_event)?; + let recv_completion = EventCompletion::new(recv_event)?; + + Ok(Self { + stop_completion, + sender, + receiver, + recv_completion, + free_client: None, + clients: HashMap::::new(), + completion_port, + }) + } + + unsafe fn get_client_sid(pipe: HANDLE) -> windows::core::Result { + let mut sid = 0u32; + unsafe { GetNamedPipeClientSessionId(pipe, &mut sid)? }; + Ok(sid) + } + + fn on_client_connected(&mut self, new_client: PipeTalker) -> windows::core::Result<()> { + unsafe { + clear_io_completion_port(new_client.get_handle())?; + } + // TODO: review get_client_sid failure? + let sid = unsafe { Self::get_client_sid(new_client.get_handle())? }; + if let Some(_) = self.clients.insert(sid, new_client) {} + let new_client = self.clients.get_mut(&sid).unwrap(); + unsafe { + CreateIoCompletionPort( + new_client.get_handle(), + Some(*self.completion_port), + sid as usize, + 0, + )?; + } + new_client.begin_read()?; + Ok(()) + } + + fn get_active_console_session_id() -> windows::core::Result { + let active = unsafe { WTSGetActiveConsoleSessionId() }; + match active { + INVALID_SESSION_ID => Err(ERROR_FILE_NOT_FOUND.into()), + _ => Ok(active), + } + } + + fn send_host_msg_to_client( + &mut self, + msg: ClipboardData, + ) -> (u32, windows::core::Result) { + let active = Self::get_active_console_session_id().unwrap_or(INVALID_SESSION_ID); + ( + active, + match self.clients.get_mut(&active) { + Some(client) => client.queue_write(Some(&msg)), + None => Err(ERROR_FILE_NOT_FOUND.into()), + }, + ) + } + + fn on_client_read(sender: &flume::Sender, key: usize, msg: ClipboardData) { + let key = key; + match Self::get_active_console_session_id() { + Ok(active) if active as usize == key => sender.send(msg).unwrap(), + _ => (), + } + } + + fn complete_client( + &mut self, + completion_packet: CompletionPacket, + ) -> windows::core::Result<()> { + let key = completion_packet.key; + let client = self.clients.get_mut(&(key as u32)).unwrap(); + match unsafe { client.complete_io(completion_packet.overlapped, completion_packet.result)? } + { + // pump the pipe again + PipeTalkerResult::Read(read) => { + if let Some(message) = read { + Self::on_client_read(&self.sender, key, message); + } + client.begin_read()?; + } + PipeTalkerResult::Written => { + client.queue_write(None)?; + } + _ => panic!("unexpected completion event"), + } + Ok(()) + } + + fn check_free_client(&mut self) -> anyhow::Result<()> { + self.free_client = match self.free_client.take() { + Some(c) => Some(c), + None => { + let mut new_client = PipeTalker::create( + CLIPBOARD_PIPE_SERVER_PATH, + MAX_MESSAGE_SIZE, + MAX_WRITE_QUEUE_SIZE, + false, + Some(CLIPBOARD_PIPE_SERVER_SDDL), + )?; + unsafe { + CreateIoCompletionPort( + new_client.get_handle(), + Some(*self.completion_port), + FREE_CLIENT_KEY, + 0, + )?; + } + if new_client.begin_connect()? { + self.on_client_connected(new_client)?; + None + } else { + Some(new_client) + } + } + }; + Ok(()) + } + + fn complete_free_client( + &mut self, + completion_packet: CompletionPacket, + ) -> windows::core::Result<()> { + let mut new_client = self.free_client.take().unwrap(); + match unsafe { + new_client.complete_io(completion_packet.overlapped, completion_packet.result)? + } { + PipeTalkerResult::Connected => { + self.on_client_connected(new_client)?; + } + _ => panic!("unexpected free client completion at this state"), + }; + Ok(()) + } + + fn wait_completion_packet(&mut self) -> windows::core::Result { + let mut bytes = 0u32; + let mut key = 0; + let mut overlapped: *mut OVERLAPPED = std::ptr::null_mut(); + match unsafe { + GetQueuedCompletionStatus( + *self.completion_port, + &mut bytes, + &mut key, + &mut overlapped, + INFINITE, + ) + } { + Ok(_) => Ok(CompletionPacket { + overlapped, + key, + result: Ok(bytes), + }), + Err(e) => { + if overlapped.is_null() { + Err(e) + } else { + Ok(CompletionPacket { + overlapped, + key, + result: Err(e), + }) + } + } + } + } + + pub(crate) fn run(&mut self) -> anyhow::Result<()> { + unsafe { + self.stop_completion + .rearm(*self.completion_port, STOP_SIGNALED)?; + self.recv_completion + .rearm(*self.completion_port, SET_CLIPBOARD_SIGNALED)?; + } + + loop { + // begin of day: set up new free PipeTalker for clients to take if needed + self.check_free_client()?; + + // wait for something interesting to happen + let completion_packet = self.wait_completion_packet()?; + + let key = completion_packet.key; + // what kind of event is it? + let client_result: Result<(), (windows::core::Error, usize)> = match key { + FREE_CLIENT_KEY => self + .complete_free_client(completion_packet) + .map_err(|e| (e, key)), + SET_CLIPBOARD_SIGNALED => { + unsafe { + self.recv_completion + .rearm(*self.completion_port, SET_CLIPBOARD_SIGNALED)?; + } + // obviously cannot set clipboard more than once per event; consume everything + if let Some(host_msg) = self.receiver.drain().last() { + match self.send_host_msg_to_client(host_msg) { + (_, Ok(_)) => Ok(()), + (active, Err(e)) => Err((e, active as usize)), + } + } else { + Ok(()) + } + } + STOP_SIGNALED => { + log::debug!("Clipboard stop signaled"); + break; + } + _ => self + .complete_client(completion_packet) + .map_err(|e| (e, key)), + }; + + // remove the ones that failed + if let Err((_e, failed_key)) = client_result { + match failed_key { + FREE_CLIENT_KEY => { + self.free_client.take(); + } + SET_CLIPBOARD_SIGNALED => panic!("signal path cannot be a failed_key"), + _ => { + self.clients.remove(&(failed_key as u32)); + } + } + } + } + + log::debug!("Leaving clipboard main loop"); + Ok(()) + } +} diff --git a/providers/provider-memory/Cargo.toml b/providers/provider-memory/Cargo.toml index bf9f623..9848826 100644 --- a/providers/provider-memory/Cargo.toml +++ b/providers/provider-memory/Cargo.toml @@ -10,4 +10,4 @@ flume = "0.11.1" smol = "2.0.2" [target.'cfg(target_os = "windows")'.dependencies] -windows = { version = "0.58", features = ["Win32_System_SystemInformation"] } +windows = { version = "0.61", features = ["Win32_System_SystemInformation"] } diff --git a/providers/provider-os/Cargo.toml b/providers/provider-os/Cargo.toml index f999cab..f11efe6 100644 --- a/providers/provider-os/Cargo.toml +++ b/providers/provider-os/Cargo.toml @@ -11,4 +11,7 @@ smol = "2.0.2" flume = "0.11.1" [target.'cfg(unix)'.dependencies] -uname = "0.1.1" \ No newline at end of file +uname = "0.1.1" + +[target."cfg(windows)".dependencies] +xen-win-utils = { path = "../../xen-win-utils" } diff --git a/providers/provider-os/src/lib.rs b/providers/provider-os/src/lib.rs index c79aab7..d970a1e 100644 --- a/providers/provider-os/src/lib.rs +++ b/providers/provider-os/src/lib.rs @@ -1,6 +1,9 @@ use guest_metrics::{os_info, plugin::GuestAgentPlugin, GuestMetric, KernelInfo, OsInfo}; use std::io; +#[cfg(windows)] +use xen_win_utils; + // UNIX uname() implementation #[cfg(unix)] pub fn collect_kernel() -> io::Result> { @@ -10,8 +13,16 @@ pub fn collect_kernel() -> io::Result> { })) } +#[cfg(windows)] +pub fn collect_kernel() -> io::Result> { + let version_info = xen_win_utils::get_version()?; + Ok(Some(guest_metrics::KernelInfo { + release: version_info, + })) +} + // default implementation -#[cfg(not(unix))] +#[cfg(all(not(unix), not(windows)))] pub fn collect_kernel() -> io::Result> { Ok(None) } diff --git a/providers/vif-detect/Cargo.toml b/providers/vif-detect/Cargo.toml index 0b0c42b..ad37cce 100644 --- a/providers/vif-detect/Cargo.toml +++ b/providers/vif-detect/Cargo.toml @@ -9,4 +9,4 @@ guest-metrics = { path = "../../guest-metrics" } [target.'cfg(target_os = "windows")'.dependencies] xenstore-rs = "0.8" -xenstore-win = { git = "https://github.com/TSnake41/xenstore-win.git" } \ No newline at end of file +xenstore-win = { git = "https://github.com/TSnake41/xenstore-win.git", features = ["smol"] } diff --git a/publishers/publisher-console/src/lib.rs b/publishers/publisher-console/src/lib.rs index 76030fc..976f47b 100644 --- a/publishers/publisher-console/src/lib.rs +++ b/publishers/publisher-console/src/lib.rs @@ -64,6 +64,9 @@ impl ConsolePublisher { } } GuestMetric::CleanupIfaces => {} + GuestMetric::GetClipboard(clipboard) => { + println!("Clipboard: {}", String::from_utf8_lossy(&clipboard)); + } } } } diff --git a/publishers/publisher-xenstore/Cargo.toml b/publishers/publisher-xenstore/Cargo.toml index 34bdfd2..5b91f62 100644 --- a/publishers/publisher-xenstore/Cargo.toml +++ b/publishers/publisher-xenstore/Cargo.toml @@ -12,4 +12,4 @@ xenstore-rs = "0.8" # Also use xenstore-win on Windows [target.'cfg(target_os = "windows")'.dependencies] -xenstore-win = { git = "https://github.com/TSnake41/xenstore-win.git" } \ No newline at end of file +xenstore-win = { git = "https://github.com/TSnake41/xenstore-win.git", features = ["smol"] } diff --git a/publishers/publisher-xenstore/src/lib.rs b/publishers/publisher-xenstore/src/lib.rs index 40e2eef..ece69e9 100644 --- a/publishers/publisher-xenstore/src/lib.rs +++ b/publishers/publisher-xenstore/src/lib.rs @@ -1,21 +1,40 @@ mod rfc; mod std; +mod version; use ::std::io; use guest_metrics::plugin::GuestAgentPublisher; -use xenstore_rs::Xs; +use xenstore_rs::{AsyncWatch, AsyncXs, Xs}; +use xenstore_win::smol::XsSmolWindows; pub fn xs_publish(xs: &impl Xs, key: &str, value: &str) -> io::Result<()> { log::trace!("+ {}={:?}", key, value); xs.write(key, value) } +pub async fn xs_publish_async(xs: &impl AsyncXs, key: &str, value: &str) -> io::Result<()> { + log::trace!("+ {}={:?}", key, value); + xs.write(key, value).await +} + pub fn xs_unpublish(xs: &impl Xs, key: &str) -> io::Result<()> { log::trace!("- {}", key); xs.rm(key) } +pub async fn xs_unpublish_async(xs: &impl AsyncXs, key: &str) -> io::Result<()> { + log::trace!("- {}", key); + xs.rm(key).await +} + +pub async fn xs_watch_oneshot_async(xs: &impl AsyncWatch, key: &str) -> io::Result<()> { + log::trace!("? {}", key); + let s = xs.watch(key).await; + s.iter().next(); + Ok(()) +} + pub struct XenstoreRfcPublisher; impl GuestAgentPublisher for XenstoreRfcPublisher { @@ -41,7 +60,9 @@ impl GuestAgentPublisher for XenstoreStdPublisher { let xs = xenstore_rs::unix::XsUnix::new().expect("Unable to initialize xenstore"); #[cfg(target_os = "windows")] - let xs = xenstore_win::XsWindows::new().expect("Unable to initialize xenstore"); + let xs = XsSmolWindows::new() + .await + .expect("Unable to initialize xenstore"); std::XenstoreStd::new(xs) .run(channel) diff --git a/publishers/publisher-xenstore/src/rfc.rs b/publishers/publisher-xenstore/src/rfc.rs index c4efc31..49b5d5b 100644 --- a/publishers/publisher-xenstore/src/rfc.rs +++ b/publishers/publisher-xenstore/src/rfc.rs @@ -118,6 +118,9 @@ impl XenstoreRfc { } } } + GuestMetric::GetClipboard(_) => { + // TODO + } } } diff --git a/publishers/publisher-xenstore/src/std.rs b/publishers/publisher-xenstore/src/std.rs index 2a422e2..590f54f 100644 --- a/publishers/publisher-xenstore/src/std.rs +++ b/publishers/publisher-xenstore/src/std.rs @@ -1,16 +1,21 @@ use guest_metrics::{ - os_info, GuestMetric, MemoryInfo, NetEvent, NetEventOp, NetInterface, OsInfo, + os_info, ClipboardData, GuestMetric, MemoryInfo, NetEvent, NetEventOp, NetInterface, OsInfo, ToolstackNetInterface, }; use std::collections::HashMap; use std::io; use std::net::IpAddr; use uuid::Uuid; -use xenstore_rs::Xs; +use xenstore_rs::{AsyncWatch, AsyncXs}; -use super::{xs_publish, xs_unpublish}; +use crate::{ + version::{AGENT_VERSION_BUILD, AGENT_VERSION_MAJOR, AGENT_VERSION_MICRO, AGENT_VERSION_MINOR}, + xs_watch_oneshot_async, +}; + +use super::{xs_publish_async, xs_unpublish_async}; -pub struct XenstoreStd { +pub struct XenstoreStd { xs: XS, // use of integer indices for IP addresses requires to keep a mapping ip_addresses: IpList, @@ -32,13 +37,7 @@ struct IfaceIpStruct { } type IpList = HashMap; -// pseudo version for xe-daemon compatibility, real agent version in -// BuildVersion below -const AGENT_VERSION_MAJOR: &str = "1"; // XO does not show version at all if 0 -const AGENT_VERSION_MINOR: &str = "0"; -const AGENT_VERSION_MICRO: &str = "0"; // XAPI exposes "-1" if missing - -impl XenstoreStd { +impl XenstoreStd { pub fn new(xs: XS) -> Self { let ip_addresses = IpList::new(); XenstoreStd { @@ -54,33 +53,41 @@ fn iface_prefix(iface_id: u32) -> String { format!("attr/vif/{iface_id}") } -impl XenstoreStd { - fn publish_osinfo(&mut self, info: &OsInfo) -> io::Result<()> { +impl XenstoreStd { + async fn publish_osinfo(&mut self, info: &OsInfo) -> io::Result<()> { // FIXME this is not anywhere standard, just minimal XS compatibility - xs_publish(&self.xs, "attr/PVAddons/MajorVersion", AGENT_VERSION_MAJOR)?; - xs_publish(&self.xs, "attr/PVAddons/MinorVersion", AGENT_VERSION_MINOR)?; - xs_publish(&self.xs, "attr/PVAddons/MicroVersion", AGENT_VERSION_MICRO)?; - let agent_version_build = format!("proto-{}", &env!("CARGO_PKG_VERSION")); - xs_publish(&self.xs, "attr/PVAddons/BuildVersion", &agent_version_build)?; + xs_publish_async(&self.xs, "attr/PVAddons/Installed", "1").await?; + xs_publish_async(&self.xs, "attr/PVAddons/MajorVersion", AGENT_VERSION_MAJOR).await?; + xs_publish_async(&self.xs, "attr/PVAddons/MinorVersion", AGENT_VERSION_MINOR).await?; + xs_publish_async(&self.xs, "attr/PVAddons/MicroVersion", AGENT_VERSION_MICRO).await?; + let agent_version_build = if AGENT_VERSION_BUILD.is_empty() { + &format!("proto-{}", &env!("CARGO_PKG_VERSION")) + } else { + AGENT_VERSION_BUILD + }; + xs_publish_async(&self.xs, "attr/PVAddons/BuildVersion", &agent_version_build).await?; - xs_publish( + xs_publish_async( &self.xs, "data/os_distro", &info.os_info.os_type().to_string(), - )?; - xs_publish( + ) + .await?; + xs_publish_async( &self.xs, "data/os_name", &format!("{} {}", info.os_info.os_type(), info.os_info.version()), - )?; + ) + .await?; // FIXME .version only has "major" component right now; not a // big deal for a proto, os_minorver is known to be unreliable // in xe-guest-utilities at least for Debian let os_version = info.os_info.version(); match os_version { - os_info::Version::Semantic(major, minor, _patch) => { - xs_publish(&self.xs, "data/os_majorver", &major.to_string())?; - xs_publish(&self.xs, "data/os_minorver", &minor.to_string())?; + os_info::Version::Semantic(major, minor, patch) => { + xs_publish_async(&self.xs, "data/os_majorver", &major.to_string()).await?; + xs_publish_async(&self.xs, "data/os_minorver", &minor.to_string()).await?; + xs_publish_async(&self.xs, "data/os_buildver", &patch.to_string()).await?; } _ => { // FIXME what to do with strings? @@ -89,7 +96,7 @@ impl XenstoreStd { } } if let Some(kernel_info) = &info.kernel_info { - xs_publish(&self.xs, "data/os_uname", &kernel_info.release)?; + xs_publish_async(&self.xs, "data/os_uname", &kernel_info.release).await?; } if !self.forbidden_control_feature_balloon { @@ -99,7 +106,7 @@ impl XenstoreStd { // `~/memory/target` value (or, possibly, to rely on the // balloon driver to do the job of signaling this // condition) - match xs_publish(&self.xs, "control/feature-balloon", "1") { + match xs_publish_async(&self.xs, "control/feature-balloon", "1").await { Err(e) if e.kind() == io::ErrorKind::PermissionDenied => { log::warn!("cannot write control/feature-balloon (impacts XAPI's squeezed)"); self.forbidden_control_feature_balloon = true; @@ -112,23 +119,25 @@ impl XenstoreStd { Ok(()) } - fn publish_memory(&mut self, mem_info: &MemoryInfo) -> io::Result<()> { - xs_publish( + async fn publish_memory(&mut self, mem_info: &MemoryInfo) -> io::Result<()> { + xs_publish_async( &self.xs, "data/meminfo_free", &(mem_info.mem_free / 1024).to_string(), - )?; - xs_publish( + ) + .await?; + xs_publish_async( &self.xs, "data/meminfo_total", &(mem_info.mem_total / 1024).to_string(), - )?; + ) + .await?; Ok(()) } // see https://xenbits.xen.org/docs/unstable/misc/xenstore-paths.html#domain-controlled-paths - fn publish_netevent(&mut self, event: &NetEvent) -> io::Result<()> { + async fn publish_netevent(&mut self, event: &NetEvent) -> io::Result<()> { let Some(iface) = self.ifaces.get(&event.iface_id) else { return Err(io::Error::new( io::ErrorKind::NotFound, @@ -145,15 +154,16 @@ impl XenstoreStd { match &event.op { NetEventOp::AddIp(address) => { let key_suffix = self.munged_address(address, iface_id)?; - xs_publish( + xs_publish_async( &self.xs, &format!("{xs_iface_prefix}/{key_suffix}"), &address.to_string(), - )?; + ) + .await?; } NetEventOp::RmIp(address) => { let key_suffix = self.munged_address(address, iface_id)?; - xs_unpublish(&self.xs, &format!("{xs_iface_prefix}/{key_suffix}"))?; + xs_unpublish_async(&self.xs, &format!("{xs_iface_prefix}/{key_suffix}")).await?; } // FIXME extend IfaceIpStruct for this @@ -167,9 +177,44 @@ impl XenstoreStd { Ok(()) } - fn cleanup_ifaces(&mut self) -> io::Result<()> { + async fn report_clipboard_one(xs: &XS, data: &str) -> io::Result<()> { + xs_publish_async(xs, "data/report_clipboard", data).await?; + xs_watch_oneshot_async(xs, "data/report_clipboard").await?; + Ok(()) + } + + async fn publish_clipboard(&mut self, clipboard_data: &ClipboardData) -> io::Result<()> { + let data_str = String::from_utf8_lossy(&clipboard_data); + // why in the world does xenstore not support line breaks? + for line in data_str.trim_end_matches('\0').lines() { + // break up long lines + let mut line_remain = line; + while line_remain.len() > 0 { + let mut bound = std::cmp::min(line_remain.len(), 1000); + while bound > 0 && !line_remain.is_char_boundary(bound) { + bound -= 1; + } + assert!(bound > 0); + + // avoid chars outside of 0x20..0x7f range before reporting (see xenstore.txt) + let subslice: &str; + (subslice, line_remain) = line_remain.split_at(bound); + let to_report: String = subslice + .chars() + .filter(|c| matches!(c, ' '..'\u{7f}')) + .collect(); + + log::debug!("reporting {}", to_report.len()); + Self::report_clipboard_one(&self.xs, &to_report).await?; + } + } + Self::report_clipboard_one(&self.xs, "").await?; + Ok(()) + } + + async fn cleanup_ifaces(&mut self) -> io::Result<()> { // Currently only vif interfaces are cleaned - xs_unpublish(&self.xs, "attr/vif") + xs_unpublish_async(&self.xs, "attr/vif").await } fn munged_address(&mut self, addr: &IpAddr, iface_index: u32) -> io::Result { @@ -194,13 +239,13 @@ impl XenstoreStd { pub async fn run(mut self, channel: flume::Receiver) -> io::Result<()> { while let Ok(metric) = channel.recv_async().await { match metric { - GuestMetric::OperatingSystem(os_info) => self.publish_osinfo(&os_info)?, - GuestMetric::Memory(memory_info) => self.publish_memory(&memory_info)?, - GuestMetric::Network(net_event) => self.publish_netevent(&net_event)?, - GuestMetric::CleanupIfaces => self.cleanup_ifaces()?, + GuestMetric::OperatingSystem(os_info) => self.publish_osinfo(&os_info).await?, + GuestMetric::Memory(memory_info) => self.publish_memory(&memory_info).await?, + GuestMetric::Network(net_event) => self.publish_netevent(&net_event).await?, + GuestMetric::CleanupIfaces => self.cleanup_ifaces().await?, GuestMetric::AddIface(net_interface) => { if let ToolstackNetInterface::Vif(iface_id) = net_interface.toolstack_iface { - xs_publish(&self.xs, &iface_prefix(iface_id), "")?; + xs_publish_async(&self.xs, &iface_prefix(iface_id), "").await?; } self.ifaces.insert(net_interface.uuid, net_interface); @@ -208,10 +253,16 @@ impl XenstoreStd { GuestMetric::RmIface(uuid) => { if let Some(interface) = self.ifaces.remove(&uuid) { if let ToolstackNetInterface::Vif(iface_id) = interface.toolstack_iface { - xs_unpublish(&self.xs, &iface_prefix(iface_id))?; + xs_unpublish_async(&self.xs, &iface_prefix(iface_id)).await?; } } } + GuestMetric::GetClipboard(clipboard) => { + let _ = self + .publish_clipboard(&clipboard) + .await + .inspect_err(|e| log::error!("cannot publish clipboard: {e}")); + } } } diff --git a/publishers/publisher-xenstore/src/version.rs b/publishers/publisher-xenstore/src/version.rs new file mode 100644 index 0000000..102f9d5 --- /dev/null +++ b/publishers/publisher-xenstore/src/version.rs @@ -0,0 +1,4 @@ +pub(crate) const AGENT_VERSION_MAJOR: &str = "1"; // XO does not show version at all if 0 +pub(crate) const AGENT_VERSION_MINOR: &str = "0"; +pub(crate) const AGENT_VERSION_MICRO: &str = "0"; // XAPI exposes "-1" if missing +pub(crate) const AGENT_VERSION_BUILD: &str = ""; // dummy value, to be filled later diff --git a/xen-guest-agent/Cargo.toml b/xen-guest-agent/Cargo.toml index afa5598..d1d0b99 100644 --- a/xen-guest-agent/Cargo.toml +++ b/xen-guest-agent/Cargo.toml @@ -7,7 +7,7 @@ rust-version = "1.76" license = "AGPL-3.0-only" [dependencies] -futures = "0.3.26" +futures = "0.3.31" smol = "2.0.2" log = "0.4.0" env_logger = { version = ">=0.10.0", default-features = false } @@ -33,11 +33,12 @@ sysctl = "0.5.0" syslog = "6.0" [target."cfg(windows)".dependencies] -windows = { version = "0.58", features = [ - "Win32_Foundation", - "Win32_System_Diagnostics_Debug", -] } windows-service = "0.8" +xen-win-utils = { path = "../xen-win-utils" } +provider-clipboard = { path = "../providers/provider-clipboard" } + +[target."cfg(windows)".build-dependencies] +winres = "0.1" [features] netlink = ["dep:provider-netlink"] diff --git a/xen-guest-agent/build.rs b/xen-guest-agent/build.rs new file mode 100644 index 0000000..df3d4e1 --- /dev/null +++ b/xen-guest-agent/build.rs @@ -0,0 +1,12 @@ +#[cfg(windows)] +extern crate winres; + +#[cfg(windows)] +fn main() { + let mut res = winres::WindowsResource::new(); + res.set_manifest_file("manifest.xml"); + res.compile().unwrap(); +} + +#[cfg(unix)] +fn main() {} diff --git a/xen-guest-agent/manifest.xml b/xen-guest-agent/manifest.xml new file mode 100644 index 0000000..be0e2c1 --- /dev/null +++ b/xen-guest-agent/manifest.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + diff --git a/xen-guest-agent/src/main.rs b/xen-guest-agent/src/main.rs index b3bd54e..52d1944 100644 --- a/xen-guest-agent/src/main.rs +++ b/xen-guest-agent/src/main.rs @@ -1,8 +1,6 @@ mod plugins; mod publisher; #[cfg(windows)] -mod windows_debug_logger; -#[cfg(windows)] mod windows_service_main; use clap::Parser; @@ -16,7 +14,7 @@ use plugins::{NetworkPlugin, NetworkPluginKind}; use publisher::{AgentPublisher, PublisherKind}; #[cfg(windows)] -use windows_debug_logger::WindowsDebugLogger; +use xen_win_utils::windows_debug_logger::WindowsDebugLogger; const MEM_PERIOD_SECONDS: f64 = 5.0; @@ -59,7 +57,7 @@ pub(crate) async fn run_async( let mut tasks = vec![]; let executor = Executor::new(); - tasks.push(executor.spawn(publisher.run(rx))); + tasks.push(executor.spawn(publisher.run(rx.clone()))); if config.report_nics { // Remove old entries from previous agent to avoid having unknown @@ -71,6 +69,11 @@ pub(crate) async fn run_async( tasks.push(executor.spawn(provider_os::OsInfoPlugin.run(tx.clone()))); tasks.push(executor.spawn(provider_memory::MemoryPlugin.run(tx.clone()))); + #[cfg(windows)] + tasks.push( + executor.spawn(provider_clipboard::windows::WindowsClipboardPlugin::new()?.run(tx.clone())), + ); + executor .run(async { log::info!("Waiting for exit command"); @@ -146,7 +149,9 @@ fn setup_system_logger(level: LevelFilter) -> anyhow::Result<()> { #[cfg(windows)] fn setup_system_logger(level: LevelFilter) -> anyhow::Result<()> { - log::set_boxed_logger(Box::new(WindowsDebugLogger {}))?; + log::set_boxed_logger(Box::new(WindowsDebugLogger { + prefix: "[xen-guest-agent]".to_string(), + }))?; log::set_max_level(level); Ok(()) } diff --git a/xen-guest-agent/src/windows_service_main.rs b/xen-guest-agent/src/windows_service_main.rs index 1501fb9..317419c 100644 --- a/xen-guest-agent/src/windows_service_main.rs +++ b/xen-guest-agent/src/windows_service_main.rs @@ -1,7 +1,6 @@ use std::time::Duration; use clap::Parser; -use windows::Win32::Foundation::{ERROR_INVALID_PARAMETER, ERROR_SUCCESS}; use windows_service::service::{ ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceState, ServiceStatus, ServiceType, @@ -34,7 +33,7 @@ fn service_main() -> anyhow::Result<()> { service_type: ServiceType::OWN_PROCESS, current_state: ServiceState::Running, controls_accepted: ServiceControlAccept::STOP, - exit_code: ServiceExitCode::Win32(ERROR_SUCCESS.0), + exit_code: ServiceExitCode::Win32(0), checkpoint: 0, wait_hint: Duration::default(), process_id: None, @@ -57,9 +56,9 @@ fn service_main() -> anyhow::Result<()> { current_state: ServiceState::Stopped, controls_accepted: ServiceControlAccept::empty(), exit_code: if service_result.is_ok() { - ServiceExitCode::Win32(ERROR_SUCCESS.0) + ServiceExitCode::Win32(0) } else { - ServiceExitCode::Win32(ERROR_INVALID_PARAMETER.0) + ServiceExitCode::Win32(0x57) // ERROR_INVALID_PARAMETER }, checkpoint: 0, wait_hint: Duration::default(), diff --git a/xen-win-clipboard/Cargo.toml b/xen-win-clipboard/Cargo.toml new file mode 100644 index 0000000..9784966 --- /dev/null +++ b/xen-win-clipboard/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "xen-win-clipboard" +version = "0.5.0-dev" +edition = "2021" +rust-version = "1.76" +license = "AGPL-3.0-only" + +[dependencies] +anyhow = "1.0" +# not sure why explicitly specifying std is needed here +log = { version = "0.4.0", features = ["std"] } +windows = { version = "0.61", features = [ + "Win32_Foundation", + "Win32_System_Diagnostics_Debug", + "Win32_UI_WindowsAndMessaging", + "Win32_Graphics_Gdi", + "Win32_System_DataExchange", + "Win32_System_LibraryLoader", + "Win32_System_Ole", + "Win32_System_Memory", + "Win32_System_Recovery", +] } +xen-win-utils = { path = "../xen-win-utils" } + +[build-dependencies] +winres = "0.1" diff --git a/xen-win-clipboard/README.md b/xen-win-clipboard/README.md new file mode 100644 index 0000000..8b5ed2f --- /dev/null +++ b/xen-win-clipboard/README.md @@ -0,0 +1,14 @@ +# xen-win-clipboard per-session clipboard agent + +xen-win-clipboard is a per-session agent that communicates with xen-guest-agent's clipboard provider to exchange clipboard data. + +It uses a clipboard format listener to watch its clipboard contents, then passes them over to the clipboard provider over named pipes. +The clipboard provider accepts connections from all sessions, but only allows the console session to publish/subscribe to clipboard contents. + +## Pipe talker message format + +UTF-8 string containing clipboard contents. + +Note that due to xenstore/vnc limitations, we currently strip away characters not in the 0x20..0xff range. + +This is eventually converted into UTF-16 by xen-win-clipboard for use with CF_UNICODETEXT. diff --git a/xen-win-clipboard/build.rs b/xen-win-clipboard/build.rs new file mode 100644 index 0000000..df3d4e1 --- /dev/null +++ b/xen-win-clipboard/build.rs @@ -0,0 +1,12 @@ +#[cfg(windows)] +extern crate winres; + +#[cfg(windows)] +fn main() { + let mut res = winres::WindowsResource::new(); + res.set_manifest_file("manifest.xml"); + res.compile().unwrap(); +} + +#[cfg(unix)] +fn main() {} diff --git a/xen-win-clipboard/manifest.xml b/xen-win-clipboard/manifest.xml new file mode 100644 index 0000000..22cfb34 --- /dev/null +++ b/xen-win-clipboard/manifest.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + diff --git a/xen-win-clipboard/src/clipboard.rs b/xen-win-clipboard/src/clipboard.rs new file mode 100644 index 0000000..5359e27 --- /dev/null +++ b/xen-win-clipboard/src/clipboard.rs @@ -0,0 +1,86 @@ +use std::marker::PhantomData; + +use windows::Win32::{ + Foundation::{ERROR_BUFFER_OVERFLOW, HANDLE, HGLOBAL, HWND}, + System::{ + DataExchange::{CloseClipboard, GetClipboardData, OpenClipboard, SetClipboardData}, + Memory::{GlobalAlloc, GMEM_MOVEABLE}, + Ole::{CF_UNICODETEXT, CLIPBOARD_FORMAT}, + }, +}; +use xen_win_utils::heap::GlobalLockGuard; + +fn as_u8_slice(input: &[u16]) -> windows::core::Result<&[u8]> { + let count = input.len().checked_mul(2).ok_or(ERROR_BUFFER_OVERFLOW)?; + unsafe { + Ok(core::slice::from_raw_parts( + input.as_ptr() as *const u8, + count, + )) + } +} + +fn as_u16_box(input: &[u8]) -> Box<[u16]> { + let count = input.len() / 2; + let mut bx = vec![0u16; count]; + bx.copy_from_slice(unsafe { core::slice::from_raw_parts(input.as_ptr().cast(), count) }); + bx.into_boxed_slice() +} + +pub(crate) struct Clipboard { + _private: PhantomData<()>, +} + +impl Clipboard { + pub fn new(hwnd: HWND) -> windows::core::Result { + unsafe { + OpenClipboard(Some(hwnd))?; + } + Ok(Self { + _private: PhantomData, + }) + } + + fn get_raw(&self, format: CLIPBOARD_FORMAT) -> windows::core::Result> { + let hcb = unsafe { GetClipboardData(format.0.into()) }?; + let hmem = HGLOBAL(hcb.0); + + Ok(GlobalLockGuard::lock(hmem)?) + } + + #[allow(dead_code)] + pub fn get(&self, format: CLIPBOARD_FORMAT) -> windows::core::Result> { + let hmem_lock = self.get_raw(format)?; + Ok(hmem_lock.get().to_vec().into_boxed_slice()) + } + + /// Null-terminated. + pub fn get_wide_z(&self) -> windows::core::Result> { + let hmem_lock = self.get_raw(CF_UNICODETEXT)?; + Ok(as_u16_box(hmem_lock.get())) + } + + pub fn set(&self, format: CLIPBOARD_FORMAT, data: &[u8]) -> windows::core::Result<()> { + // ownership transferred to the system + let hmem = unsafe { GlobalAlloc(GMEM_MOVEABLE, data.len())? }; + { + let mut hmem_lock = GlobalLockGuard::lock(hmem)?; + hmem_lock.get_mut().copy_from_slice(data); + } + unsafe { SetClipboardData(format.0.into(), Some(HANDLE(hmem.0))) }?; + Ok(()) + } + + /// Null-terminated. + pub fn set_wide_z(&self, data_z: &[u16]) -> windows::core::Result<()> { + self.set(CF_UNICODETEXT, as_u8_slice(data_z)?) + } +} + +impl Drop for Clipboard { + fn drop(&mut self) { + unsafe { + let _ = CloseClipboard(); + } + } +} diff --git a/xen-win-clipboard/src/main.rs b/xen-win-clipboard/src/main.rs new file mode 100644 index 0000000..31f6f89 --- /dev/null +++ b/xen-win-clipboard/src/main.rs @@ -0,0 +1,235 @@ +#![windows_subsystem = "windows"] + +mod clipboard; + +use std::process::ExitCode; + +use clipboard::Clipboard; +use xen_win_utils::{ + named_mutex::NamedMutexGuard, + overlapped::{windowed_wait, WindowedWaitResult}, + pipe_talker::PipeTalker, +}; + +use windows::{ + core::{w, PCWSTR}, + Win32::{ + Foundation::{WPARAM, *}, + System::{ + DataExchange::*, + LibraryLoader::GetModuleHandleW, + Ole::CF_UNICODETEXT, + Recovery::{RegisterApplicationRestart, RESTART_NO_REBOOT}, + Threading::INFINITE, + }, + UI::WindowsAndMessaging::*, + }, +}; + +use xen_win_utils::windows_debug_logger::WindowsDebugLogger; + +const CLASS_NAME: PCWSTR = w!("XenWinClipboardAgent"); +const CLIPBOARD_PIPE_SERVER_PATH: &str = + r"\\.\pipe\ProtectedPrefix\Administrators\XenWinClipboardService"; +const MAX_MESSAGE_SIZE: u32 = 65535; +const MAX_WRITE_QUEUE_SIZE: u32 = 262143; + +struct App { + client: PipeTalker, + hwnd: HWND, +} + +impl App { + fn new() -> windows::core::Result> { + let hwnd = unsafe { + let wcex = WNDCLASSEXW { + cbSize: size_of::() as u32, + lpfnWndProc: Some(Self::wndproc), + hInstance: GetModuleHandleW(None)?.into(), + lpszClassName: CLASS_NAME, + ..Default::default() + }; + + let atom = RegisterClassExW(&wcex); + if atom == 0 { + return Err(windows::core::Error::from_win32().into()); + } + + let hwnd = CreateWindowExW( + WINDOW_EX_STYLE(0), + CLASS_NAME, + None, + WINDOW_STYLE(0), + 0, + 0, + 0, + 0, + Some(HWND_MESSAGE), + None, + None, + None, + )?; + + hwnd + }; + + let client = PipeTalker::open( + CLIPBOARD_PIPE_SERVER_PATH, + MAX_MESSAGE_SIZE, + MAX_WRITE_QUEUE_SIZE, + true, + )?; + + let mut result = Box::new(Self { client, hwnd }); + unsafe { + SetWindowLongPtrW( + result.hwnd, + GWLP_USERDATA, + &mut *result as *mut Self as isize, + ) + }; + + Ok(result) + } + + fn on_clipboard_update( + &mut self, + hwnd: HWND, + _msg: u32, + _wparam: WPARAM, + _lparam: LPARAM, + ) -> windows::core::Result { + if let Err(_) = unsafe { IsClipboardFormatAvailable(CF_UNICODETEXT.0 as u32) } { + return Ok(LRESULT(0)); + } + + let cb = Clipboard::new(hwnd)?; + let cb_text = cb.get_wide_z()?; + + // TODO: convert to ipc byte format? rmp? + // replicate String::from_utf16_lossy and break at null at the same time + let str: String = char::decode_utf16(cb_text.iter().copied().take_while(|c| *c != 0u16)) + .map(|r| r.unwrap_or(char::REPLACEMENT_CHARACTER)) + .collect(); + if self.client.queue_write(Some(str.as_bytes()))? { + while self.client.queue_write(None)? {} + } + + Ok(LRESULT(0)) + } + + extern "system" fn wndproc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT { + unsafe { + match msg { + WM_CREATE => { + if let Err(e) = AddClipboardFormatListener(hwnd) { + panic!("AddClipboardFormatListener error {e}"); + } + DefWindowProcW(hwnd, msg, wparam, lparam) + } + WM_CLIPBOARDUPDATE => { + let this = (GetWindowLongPtrW(hwnd, GWLP_USERDATA) as *mut Self) + .as_mut() + .unwrap(); + + if let Err(e) = this.on_clipboard_update(hwnd, msg, wparam, lparam) { + log::error!("WM_CLIPBOARDUPDATE error {e}"); + } + LRESULT(0) + } + WM_CLOSE => { + let _ = RemoveClipboardFormatListener(hwnd); + let _ = DestroyWindow(hwnd); + LRESULT(0) + } + WM_DESTROY => { + PostQuitMessage(0); + LRESULT(0) + } + _ => DefWindowProcW(hwnd, msg, wparam, lparam), + } + } + } + + // None to continue, or Some(ExitCode) to exit + fn on_window_msg(&self, msg: &mut MSG) -> Option { + while unsafe { PeekMessageW(msg, None, 0, 0, PM_REMOVE) } == TRUE { + if msg.message == WM_QUIT { + return Some(ExitCode::from(msg.wParam.0.try_into().unwrap_or(1))); + } + unsafe { + let _ = DispatchMessageW(msg); + } + } + None + } + + fn on_pipe_msg(hwnd: HWND, msg: Box<[u8]>) -> windows::core::Result<()> { + // TODO: convert to ipc byte format? rmp? + let mut cb_z: Vec = String::from_utf8_lossy(&msg).encode_utf16().collect(); + cb_z.push(0); + + let cb = Clipboard::new(hwnd)?; + cb.set_wide_z(&cb_z)?; + Ok(()) + } + + fn run(&mut self) -> windows::core::Result { + let handles = unsafe { + [ + self.client.get_read_event().unwrap(), + self.client.get_write_event().unwrap(), + ] + }; + while self.client.begin_read()? { + if let Some(msg) = self.client.end_read()? { + Self::on_pipe_msg(self.hwnd, msg)?; + } + } + let mut msg = MSG::default(); + + loop { + match windowed_wait(Some(&handles), INFINITE, QS_ALLINPUT, false, true, false)? { + WindowedWaitResult::Input => { + if let Some(value) = self.on_window_msg(&mut msg) { + return Ok(value); + } + } + // pump the pipe again + WindowedWaitResult::Handle(0) => { + if let Some(msg) = self.client.end_read()? { + Self::on_pipe_msg(self.hwnd, msg)?; + } + while self.client.begin_read()? { + if let Some(msg) = self.client.end_read()? { + Self::on_pipe_msg(self.hwnd, msg)?; + } + } + } + WindowedWaitResult::Handle(1) => { + self.client.end_write()?; + while self.client.queue_write(None)? {} + } + _ => unreachable!(), + } + } + } +} + +fn main() -> anyhow::Result { + log::set_boxed_logger(Box::new(WindowsDebugLogger { + prefix: "[xen-win-clipboard]".to_string(), + }))?; + log::set_max_level(log::LevelFilter::Trace); + + let single = NamedMutexGuard::new(Some("Local\\XenWinClipboardAgent"), true)?; + if let None = single { + log::info!("Another instance is already running"); + return Ok(ExitCode::from(ERROR_ALREADY_EXISTS.0 as u8)); + } + + unsafe { RegisterApplicationRestart(PCWSTR::null(), RESTART_NO_REBOOT)? }; + + let mut app = App::new()?; + Ok(app.run()?) +} diff --git a/xen-win-utils/Cargo.toml b/xen-win-utils/Cargo.toml new file mode 100644 index 0000000..eff202d --- /dev/null +++ b/xen-win-utils/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "xen-win-utils" +version = "0.5.0-dev" +edition = "2021" +rust-version = "1.76" +license = "AGPL-3.0-only" + +[dependencies] +log = "0.4.0" +windows = { version = "0.61", features = [ + "Win32_Foundation", + "Win32_Security_Authorization", + "Win32_Security", + "Win32_Storage_FileSystem", + "Win32_System_Diagnostics_Debug", + "Win32_System_IO", + "Win32_System_LibraryLoader", + "Win32_System_Pipes", + "Win32_System_SystemInformation", + "Win32_System_SystemServices", + "Win32_System_Threading", + "Win32_UI_WindowsAndMessaging", + "Wdk_Foundation", + "Wdk_System_SystemServices", + "Wdk_Storage_FileSystem", +] } diff --git a/xen-win-utils/src/heap.rs b/xen-win-utils/src/heap.rs new file mode 100644 index 0000000..3e801e8 --- /dev/null +++ b/xen-win-utils/src/heap.rs @@ -0,0 +1,87 @@ +use std::ops::{Deref, DerefMut}; + +use windows::Win32::{ + Foundation::{LocalFree, HGLOBAL, HLOCAL}, + System::Memory::{GlobalLock, GlobalSize, GlobalUnlock, LocalSize}, +}; + +pub struct GlobalLockGuard<'a, T> { + hmem: HGLOBAL, + data: &'a mut [T], +} + +impl<'a, T> GlobalLockGuard<'a, T> { + pub fn lock(hmem: HGLOBAL) -> windows::core::Result { + let p = unsafe { GlobalLock(hmem) } as *mut T; + if p.is_null() { + return Err(windows::core::Error::from_win32()); + } + let len_bytes = unsafe { GlobalSize(hmem) }; + assert!(len_bytes > 0); + assert!(len_bytes % align_of::() == 0); + let data = unsafe { core::slice::from_raw_parts_mut(p, len_bytes / size_of::()) }; + Ok(Self { hmem, data }) + } + + pub fn get(&self) -> &[T] { + self.data + } + + pub fn get_mut(&mut self) -> &mut [T] { + self.data + } +} + +impl<'a, T> Drop for GlobalLockGuard<'a, T> { + fn drop(&mut self) { + unsafe { + let _ = GlobalUnlock(self.hmem); + } + } +} + +/// This struct is only safe to use with LMEM_FIXED pointers! +pub struct LocalPointer<'a, T> { + data: &'a mut [T], +} + +impl<'a, T> LocalPointer<'a, T> { + pub unsafe fn slice_from_raw_mut(p: *mut T) -> LocalPointer<'a, T> { + assert!(!p.is_null()); + let len_bytes = LocalSize(HLOCAL(p.cast())); + assert!(len_bytes > 0); + assert!(len_bytes % align_of::() == 0); + LocalPointer { + data: core::slice::from_raw_parts_mut(p, len_bytes / size_of::()), + } + } + + pub unsafe fn from_raw_mut(p: *mut T) -> LocalPointer<'a, T> { + assert!(!p.is_null()); + LocalPointer { + data: core::slice::from_raw_parts_mut(p, 1), + } + } +} + +impl<'a, T> Drop for LocalPointer<'a, T> { + fn drop(&mut self) { + unsafe { + let _ = LocalFree(Some(HLOCAL(self.data.as_mut_ptr().cast()))); + } + } +} + +impl<'a, T> Deref for LocalPointer<'a, T> { + type Target = [T]; + + fn deref(&self) -> &Self::Target { + self.data + } +} + +impl<'a, T> DerefMut for LocalPointer<'a, T> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.data + } +} diff --git a/xen-win-utils/src/iocp.rs b/xen-win-utils/src/iocp.rs new file mode 100644 index 0000000..b5d44c2 --- /dev/null +++ b/xen-win-utils/src/iocp.rs @@ -0,0 +1,147 @@ +use std::{ffi::c_void, sync::LazyLock}; + +use windows::{ + core::{s, w, Owned, BOOL}, + Wdk::Foundation::OBJECT_ATTRIBUTES, + Win32::{ + Foundation::{GENERIC_ALL, HANDLE, NTSTATUS, STATUS_SUCCESS}, + System::{ + LibraryLoader::{GetProcAddress, LoadLibraryW}, + IO::OVERLAPPED, + }, + }, +}; + +type NtCreateWaitCompletionPacket = unsafe extern "system" fn( + WaitCompletionPacketHandle: *mut HANDLE, + DesiredAccess: u32, + ObjectAttributes: *const OBJECT_ATTRIBUTES, +) -> NTSTATUS; + +type NtAssociateWaitCompletionPacket = unsafe extern "system" fn( + WaitCompletionPacketHandle: HANDLE, + IoCompletionHandle: HANDLE, + TargetObjectHandle: HANDLE, + KeyContext: *mut c_void, + ApcContext: *mut c_void, + IoStatus: NTSTATUS, + IoStatusInformation: usize, + AlreadySignaled: *mut BOOL, +) -> NTSTATUS; + +type NtCancelWaitCompletionPacket = unsafe extern "system" fn( + WaitCompletionPacketHandle: HANDLE, + RemoveSignaledPacket: BOOL, +) -> NTSTATUS; + +pub struct WaitCompletionHandle { + handle: Owned, +} + +pub struct CompletionHelpers { + nt_create_wait_completion_packet: NtCreateWaitCompletionPacket, + nt_associate_wait_completion_packet: NtAssociateWaitCompletionPacket, + nt_cancel_wait_completion_packet: NtCancelWaitCompletionPacket, +} + +impl CompletionHelpers { + pub fn new() -> windows::core::Result { + unsafe { + let ntdll = LoadLibraryW(w!("ntdll.dll"))?; + let create_fn = GetProcAddress(ntdll, s!("NtCreateWaitCompletionPacket")) + .ok_or(windows::core::Error::from_win32())?; + let associate_fn = GetProcAddress(ntdll, s!("NtAssociateWaitCompletionPacket")) + .ok_or(windows::core::Error::from_win32())?; + let cancel_fn = GetProcAddress(ntdll, s!("NtCancelWaitCompletionPacket")) + .ok_or(windows::core::Error::from_win32())?; + Ok(CompletionHelpers { + nt_create_wait_completion_packet: std::mem::transmute(create_fn), + nt_associate_wait_completion_packet: std::mem::transmute(associate_fn), + nt_cancel_wait_completion_packet: std::mem::transmute(cancel_fn), + }) + // HACK: leak the ntdll.dll reference so that the static COMPLETION_HELPER is shareable + } + } + + pub unsafe fn create_wait(&self) -> windows::core::Result { + unsafe { + let mut handle = Owned::::new(HANDLE::default()); + (self.nt_create_wait_completion_packet)(&mut *handle, GENERIC_ALL.0, std::ptr::null()) + .ok()?; + Ok(WaitCompletionHandle { handle }) + } + } + + pub unsafe fn associate_wait( + &self, + waiter: &mut WaitCompletionHandle, + completion_port: HANDLE, + event: HANDLE, + key: usize, + overlapped: *mut OVERLAPPED, + ) -> windows::core::Result { + unsafe { + let mut already_signaled = BOOL::default(); + (self.nt_associate_wait_completion_packet)( + *waiter.handle, + completion_port, + event, + key as *mut c_void, + overlapped.cast(), + STATUS_SUCCESS, + 1, + &mut already_signaled, + ) + .ok()?; + Ok(already_signaled.into()) + } + } + + pub unsafe fn cancel_wait( + &self, + waiter: &mut WaitCompletionHandle, + remove_signaled_packet: bool, + ) -> windows::core::Result<()> { + unsafe { + (self.nt_cancel_wait_completion_packet)(*waiter.handle, remove_signaled_packet.into()) + .ok()?; + Ok(()) + } + } +} + +pub static COMPLETION_HELPER: LazyLock = + LazyLock::new(|| CompletionHelpers::new().expect("cannot resolve completion helper functions")); + +pub struct EventCompletion { + event: HANDLE, + completion: WaitCompletionHandle, + overlapped: Box, +} + +impl EventCompletion { + pub fn new(event: HANDLE) -> windows::core::Result { + Ok(Self { + event, + completion: unsafe { COMPLETION_HELPER.create_wait()? }, + overlapped: Box::new(OVERLAPPED::default()), + }) + } + + pub unsafe fn rearm( + &mut self, + completion_port: HANDLE, + key: usize, + ) -> windows::core::Result<()> { + unsafe { + let _ = COMPLETION_HELPER.associate_wait( + &mut self.completion, + completion_port, + self.event, + key, + &mut *self.overlapped, + )?; + }; + Ok(()) + } +} diff --git a/xen-win-utils/src/lib.rs b/xen-win-utils/src/lib.rs new file mode 100644 index 0000000..8ff0433 --- /dev/null +++ b/xen-win-utils/src/lib.rs @@ -0,0 +1,26 @@ +use windows::{ + Wdk::System::SystemServices::RtlGetVersion, Win32::System::SystemInformation::OSVERSIONINFOW, +}; + +pub mod heap; +pub mod iocp; +pub mod named_mutex; +pub mod named_pipe; +pub mod overlapped; +pub mod pipe_talker; +pub mod windows_debug_logger; + +pub fn get_version() -> windows::core::Result { + let mut version = OSVERSIONINFOW { + dwOSVersionInfoSize: size_of::() as u32, + ..Default::default() + }; + unsafe { + let ntstatus = RtlGetVersion(&mut version); + ntstatus.ok()?; + } + Ok(format!( + "{0}.{1}.{2}", + version.dwMajorVersion, version.dwMinorVersion, version.dwBuildNumber + )) +} diff --git a/xen-win-utils/src/named_mutex.rs b/xen-win-utils/src/named_mutex.rs new file mode 100644 index 0000000..1482a73 --- /dev/null +++ b/xen-win-utils/src/named_mutex.rs @@ -0,0 +1,28 @@ +use windows::{ + core::{Owned, HSTRING}, + Win32::{ + Foundation::{GetLastError, ERROR_ALREADY_EXISTS, ERROR_SUCCESS, HANDLE}, + System::Threading::CreateMutexW, + }, +}; + +pub struct NamedMutexGuard { + _handle: Owned, +} + +impl NamedMutexGuard { + pub fn new( + name: Option<&str>, + initial_owner: bool, + ) -> windows::core::Result> { + let wname = name.map_or(Default::default(), |x| HSTRING::from(x)); + let handle = unsafe { CreateMutexW(None, initial_owner, &wname)? }; + match unsafe { GetLastError() } { + ERROR_SUCCESS => Ok(Some(NamedMutexGuard { + _handle: unsafe { Owned::new(handle) }, + })), + ERROR_ALREADY_EXISTS => Ok(None), + e => panic!("unexpected error code {}", e.0), + } + } +} diff --git a/xen-win-utils/src/named_pipe.rs b/xen-win-utils/src/named_pipe.rs new file mode 100644 index 0000000..fe8bce5 --- /dev/null +++ b/xen-win-utils/src/named_pipe.rs @@ -0,0 +1,538 @@ +use std::{ + io::{self, Read, Write}, + os::raw::c_void, +}; + +use windows::{ + core::{Owned, HSTRING}, + Win32::{ + Foundation::{ERROR_IO_PENDING, ERROR_PIPE_CONNECTED, HANDLE}, + Security::{SECURITY_ATTRIBUTES, SECURITY_DESCRIPTOR}, + Storage::FileSystem::{ + CreateFileW, FlushFileBuffers, ReadFile, WriteFile, FILE_FLAGS_AND_ATTRIBUTES, + FILE_FLAG_OVERLAPPED, FILE_GENERIC_READ, FILE_SHARE_NONE, FILE_WRITE_DATA, + OPEN_EXISTING, PIPE_ACCESS_DUPLEX, SECURITY_IDENTIFICATION, SECURITY_SQOS_PRESENT, + }, + System::{ + Pipes::{ + ConnectNamedPipe, CreateNamedPipeW, DisconnectNamedPipe, + PIPE_REJECT_REMOTE_CLIENTS, PIPE_TYPE_BYTE, PIPE_UNLIMITED_INSTANCES, + }, + Threading::CreateEventW, + IO::{CancelIoEx, GetOverlappedResult, OVERLAPPED}, + }, + }, +}; + +#[repr(C)] +struct NamedPipeOverlapped(OVERLAPPED, T); + +struct ReadState { + buffer: Box<[u8]>, +} + +struct WriteState { + buffer: Box<[u8]>, +} + +struct ConnectState; + +struct OperationState { + event: Option>, + state: Option>>, +} + +impl OperationState { + fn new(evented: bool) -> windows::core::Result> { + Ok(OperationState { + event: if evented { + Some(unsafe { Owned::new(CreateEventW(None, false, false, None)?) }) + } else { + None + }, + state: None, + }) + } + + unsafe fn get_event(&self) -> Option { + self.event.as_deref().map(|e| *e) + } + + fn is_none(&self) -> bool { + self.state.as_ref().is_none() + } + + fn matches(&self, overlapped: *const OVERLAPPED) -> bool { + self.state + .as_deref() + .is_some_and(|s| &s.0 as *const OVERLAPPED == overlapped) + } + + fn take(&mut self) -> Option>> { + self.state.take() + } + + fn create(&self, t: T) -> Box> { + Box::new(NamedPipeOverlapped( + OVERLAPPED { + hEvent: unsafe { self.get_event().unwrap_or_default() }, + ..Default::default() + }, + t, + )) + } + + fn put(&mut self, bx: Box>) { + let _ = self.state.insert(bx); + } +} + +pub enum NamedPipeResult { + Read(Box<[u8]>), + Written(u32), + Connected, +} + +pub struct NamedPipe { + connect_state: OperationState, + read_state: OperationState, + write_state: OperationState, + pipe: Owned, + overlapped: bool, + server: bool, +} + +// not sure why HANDLEs are not Send by default, but whatever +unsafe impl Send for NamedPipe {} + +impl NamedPipe { + fn new( + pipe: Owned, + overlapped: bool, + server: bool, + evented: bool, + ) -> windows::core::Result { + Ok(NamedPipe { + connect_state: OperationState::::new(evented)?, + read_state: OperationState::::new(evented)?, + write_state: OperationState::::new(evented)?, + pipe, + overlapped, + server, + }) + } + + pub fn create( + name: &str, + overlapped: bool, + evented: bool, + security_attributes: Option<(&SECURITY_DESCRIPTOR, bool)>, + ) -> windows::core::Result { + let wname = HSTRING::from(name); + + let dwopenmode = PIPE_ACCESS_DUPLEX + | if overlapped { + FILE_FLAG_OVERLAPPED + } else { + FILE_FLAGS_AND_ATTRIBUTES(0u32) + }; + let sa = security_attributes.map(|(sd, inherit)| SECURITY_ATTRIBUTES { + nLength: size_of::() as u32, + lpSecurityDescriptor: sd as *const SECURITY_DESCRIPTOR as *mut c_void, + bInheritHandle: inherit.into(), + }); + let pipe = unsafe { + Owned::new(CreateNamedPipeW( + &wname, + dwopenmode, + PIPE_TYPE_BYTE | PIPE_REJECT_REMOTE_CLIENTS, + PIPE_UNLIMITED_INSTANCES, + 0, + 0, + 0, + sa.as_ref().map(|x| x as *const SECURITY_ATTRIBUTES), + )) + }; + if pipe.is_invalid() { + return Err(windows::core::Error::from_win32()); + } + + NamedPipe::new(pipe, overlapped, true, evented) + } + + pub fn open(name: &str, overlapped: bool, evented: bool) -> windows::core::Result { + let wname = HSTRING::from(name); + + // Don't let the server impersonate us. + let mut flags = SECURITY_SQOS_PRESENT | SECURITY_IDENTIFICATION; + if overlapped { + flags |= FILE_FLAG_OVERLAPPED; + }; + let pipe = unsafe { + Owned::new(CreateFileW( + &wname, + (FILE_GENERIC_READ | FILE_WRITE_DATA).0, + FILE_SHARE_NONE, + None, + OPEN_EXISTING, + flags, + None, + )?) + }; + + NamedPipe::new(pipe, overlapped, false, evented) + } + + pub unsafe fn get_handle(&self) -> HANDLE { + *self.pipe + } + + pub unsafe fn get_read_event(&self) -> Option { + self.read_state.get_event() + } + + pub unsafe fn get_write_event(&self) -> Option { + self.write_state.get_event() + } + + pub unsafe fn get_connect_event(&self) -> Option { + self.connect_state.get_event() + } + + pub fn is_server(&self) -> bool { + self.server + } + + fn get_overlapped_result(&self, overlapped: &OVERLAPPED) -> windows::core::Result { + let mut count = 0u32; + unsafe { + GetOverlappedResult(*self.pipe, overlapped, &mut count, false)?; + } + Ok(count) + } + + fn get_async_result(result: windows::core::Result) -> windows::core::Result> { + match result { + Ok(t) => Ok(Some(t)), + Err(e) => { + if e.code() == ERROR_IO_PENDING.into() { + Ok(None) + } else { + Err(e) + } + } + } + } + + // returns true if data was read successfully, false if the read is async + pub fn begin_read(&mut self, count: u32) -> windows::core::Result { + assert!(self.overlapped); + assert!( + self.read_state.is_none(), + "begin_read is not appropriate at this time" + ); + + let mut state = self.read_state.create(ReadState { + buffer: vec![0u8; count as usize].into_boxed_slice(), + }); + + { + let pstate = &mut state; + unsafe { + match Self::get_async_result(ReadFile( + *self.pipe, + Some(&mut pstate.1.buffer), + None, + Some(&mut pstate.0), + ))? { + Some(_) => { + // after an early success, a completion packet is still queued + // must keep the state until it's completed + self.read_state.put(state); + Ok(true) + } + None => { + self.read_state.put(state); + Ok(false) + } + } + } + } + } + + unsafe fn complete_read( + &mut self, + state: Box>, + result: windows::core::Result, + ) -> windows::core::Result> { + // Low-level completion callback for IOCP and the like. + match result { + Ok(count) => { + self.read_state.take(); + Ok(state.1.buffer[..(count as usize)] + .to_vec() + .into_boxed_slice()) + } + Err(e) => { + if e.code() == ERROR_IO_PENDING.into() { + panic!("read still pending"); + } else { + self.read_state.take(); + } + Err(e) + } + } + } + + // for use when the user is signaled by an event + pub fn end_read_evented(&mut self) -> windows::core::Result> { + let state = self + .read_state + .take() + .expect("end_read is not appropriate at this time"); + let result = self.get_overlapped_result(&state.0); + unsafe { self.complete_read(state, result) } + } + + pub fn cancel_read(&mut self) -> windows::core::Result<()> { + if let Some(state) = self.read_state.take() { + self.do_cancel_io(state)?; + } + Ok(()) + } + + pub fn begin_write(&mut self, data: &[u8]) -> windows::core::Result { + assert!(self.overlapped); + assert!( + self.write_state.is_none(), + "begin_write is not appropriate at this time" + ); + + let mut state = self.write_state.create(WriteState { + buffer: data.to_vec().into_boxed_slice(), + }); + + { + let pstate = &mut state; + unsafe { + match Self::get_async_result(WriteFile( + *self.pipe, + Some(&pstate.1.buffer), + None, + Some(&mut pstate.0), + ))? { + Some(_) => { + self.write_state.put(state); + Ok(true) + } + None => { + self.write_state.put(state); + Ok(false) + } + } + } + } + } + + unsafe fn complete_write( + &mut self, + _state: Box>, + result: windows::core::Result, + ) -> windows::core::Result { + match result { + Ok(count) => { + self.write_state.take(); + Ok(count) + } + Err(e) => { + if e.code() == ERROR_IO_PENDING.into() { + panic!("write still pending"); + } else { + self.write_state.take(); + } + Err(e) + } + } + } + + pub fn end_write_evented(&mut self) -> windows::core::Result { + let state = self + .write_state + .take() + .expect("end_write is not appropriate at this time"); + let result = self.get_overlapped_result(&state.0); + unsafe { self.complete_write(state, result) } + } + + pub fn cancel_write(&mut self) -> windows::core::Result<()> { + if let Some(state) = self.write_state.take() { + self.do_cancel_io(state)?; + } + Ok(()) + } + + /// returns true if there's a client already connected + /// + /// if true, no need to call end_connect() + pub fn begin_connect(&mut self) -> windows::core::Result { + assert!(self.overlapped); + assert!(self.server, "pipe is not a server"); + assert!( + self.connect_state.is_none(), + "begin_connect is not appropriate at this time" + ); + + let mut state = self.connect_state.create(ConnectState); + + let pstate = &mut state; + unsafe { + match ConnectNamedPipe(*self.pipe, Some(&mut pstate.0)) { + Ok(_) => panic!("Unexpected ConnectNamedPipe result"), + Err(e) => match e.code() { + hr if hr == ERROR_IO_PENDING.into() => { + self.connect_state.put(state); + Ok(false) + } + hr if hr == ERROR_PIPE_CONNECTED.into() => { + // TODO: ERROR_PIPE_CONNECTED should not generate a completion, but is it really the case? + self.connect_state.take(); + Ok(true) + } + _ => { + log::error!("ConnectNamedPipe error {e}"); + self.connect_state.take(); + Err(e) + } + }, + } + } + } + + unsafe fn complete_connect( + &mut self, + _state: Box>, + result: windows::core::Result, + ) -> windows::core::Result<()> { + match result { + Ok(_) => { + self.connect_state.take(); + Ok(()) + } + Err(e) => { + assert!(e.code() != ERROR_PIPE_CONNECTED.into()); + if e.code() == ERROR_IO_PENDING.into() { + panic!("connect still pending"); + } else { + self.connect_state.take(); + } + Err(e) + } + } + } + + pub fn end_connect_evented(&mut self) -> windows::core::Result<()> { + let state = self + .connect_state + .take() + .expect("end_connect is not appropriate at this time"); + let result = self.get_overlapped_result(&state.0); + unsafe { self.complete_connect(state, result) } + } + + pub fn cancel_connect(&mut self) -> windows::core::Result<()> { + if let Some(state) = self.connect_state.take() { + self.do_cancel_io(state)?; + } + Ok(()) + } + + pub unsafe fn complete_io( + &mut self, + overlapped: *const OVERLAPPED, + result: windows::core::Result, + ) -> windows::core::Result { + // this function is not meant to be used in evented mode + debug_assert!(self.get_read_event().is_none()); + if self.read_state.matches(overlapped) { + let state = self.read_state.take().unwrap(); + let new_result = unsafe { self.complete_read(state, result)? }; + Ok(NamedPipeResult::Read(new_result)) + } else if self.write_state.matches(overlapped) { + let state = self.write_state.take().unwrap(); + let new_result = unsafe { self.complete_write(state, result)? }; + Ok(NamedPipeResult::Written(new_result)) + } else if self.connect_state.matches(overlapped) { + let state = self.connect_state.take().unwrap(); + unsafe { self.complete_connect(state, result)? }; + Ok(NamedPipeResult::Connected) + } else { + panic!("invalid overlapped pointer {overlapped:?}"); + } + } + + fn cancel_all_io_silent(&mut self) { + let _ = self + .cancel_read() + .inspect_err(|e| log::debug!("cancel_read {e}")); + let _ = self + .cancel_write() + .inspect_err(|e| log::debug!("cancel_write {e}")); + let _ = self + .cancel_connect() + .inspect_err(|e| log::debug!("cancel_connect {e}")); + } + + pub fn disconnect(&mut self) -> windows::core::Result<()> { + assert!(self.server); + if unsafe { self.get_read_event().is_some() } { + self.cancel_all_io_silent(); + } + unsafe { DisconnectNamedPipe(*self.pipe)? }; + Ok(()) + } + + fn do_cancel_io(&mut self, state: Box>) -> windows::core::Result<()> { + let mut count = 0u32; + unsafe { + CancelIoEx(*self.pipe, Some(&state.0)) + .and_then(|_| GetOverlappedResult(*self.pipe, &state.0, &mut count, true)) + } + } +} + +impl Read for NamedPipe { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + assert!(!self.overlapped); + let mut read_count = 0u32; + unsafe { + ReadFile(*self.pipe, Some(buf), Some(&mut read_count), None)?; + } + Ok(read_count as usize) + } +} + +impl Write for NamedPipe { + fn write(&mut self, buf: &[u8]) -> io::Result { + assert!(!self.overlapped); + let mut write_count = 0u32; + unsafe { + WriteFile(*self.pipe, Some(buf), Some(&mut write_count), None)?; + } + Ok(write_count as usize) + } + + fn flush(&mut self) -> io::Result<()> { + unsafe { + FlushFileBuffers(*self.pipe)?; + } + Ok(()) + } +} + +impl Drop for NamedPipe { + fn drop(&mut self) { + if unsafe { self.get_read_event().is_some() } { + self.cancel_all_io_silent(); + } + } +} diff --git a/xen-win-utils/src/overlapped.rs b/xen-win-utils/src/overlapped.rs new file mode 100644 index 0000000..1114fac --- /dev/null +++ b/xen-win-utils/src/overlapped.rs @@ -0,0 +1,82 @@ +use windows::{ + Wdk::Storage::FileSystem::{ + FileReplaceCompletionInformation, NtSetInformationFile, FILE_COMPLETION_INFORMATION, + }, + Win32::{ + Foundation::{ + HANDLE, WAIT_ABANDONED_0, WAIT_EVENT, WAIT_IO_COMPLETION, WAIT_OBJECT_0, WAIT_TIMEOUT, + }, + System::{Threading::INFINITE, IO::IO_STATUS_BLOCK}, + UI::WindowsAndMessaging::{ + MsgWaitForMultipleObjectsEx, MWMO_ALERTABLE, MWMO_INPUTAVAILABLE, MWMO_NONE, + MWMO_WAITALL, QUEUE_STATUS_FLAGS, + }, + }, +}; + +const MAXIMUM_WAIT_OBJECTS: u32 = WAIT_IO_COMPLETION.0 - WAIT_ABANDONED_0.0; + +pub enum WindowedWaitResult { + Handle(u32), + Input, + Abandoned(u32), + IoCompletion, + Timeout, +} + +pub fn windowed_wait( + handles: Option<&[HANDLE]>, + timeout_msec: u32, + wake_mask: QUEUE_STATUS_FLAGS, + alertable: bool, + // level-triggered on window messages + input_available: bool, + wait_all: bool, +) -> windows::core::Result { + let mut flags = MWMO_NONE; + if alertable { + flags |= MWMO_ALERTABLE; + } + if input_available { + flags |= MWMO_INPUTAVAILABLE; + } + if wait_all { + flags |= MWMO_WAITALL; + } + + let wait_count = handles.map(|h| h.len()).unwrap_or(0).try_into().unwrap(); + assert!(wait_count < MAXIMUM_WAIT_OBJECTS); + let WAIT_EVENT(wait_result) = + unsafe { MsgWaitForMultipleObjectsEx(handles, timeout_msec, wake_mask, flags) }; + + if wait_result == wait_count { + Ok(WindowedWaitResult::Input) + } else if wait_result >= WAIT_OBJECT_0.0 && wait_result < WAIT_OBJECT_0.0 + wait_count { + Ok(WindowedWaitResult::Handle(wait_result - WAIT_OBJECT_0.0)) + } else if wait_result >= WAIT_ABANDONED_0.0 && wait_result < WAIT_ABANDONED_0.0 + wait_count { + Ok(WindowedWaitResult::Abandoned( + wait_result - WAIT_ABANDONED_0.0, + )) + } else if wait_result == WAIT_IO_COMPLETION.0 { + assert!(alertable); + Ok(WindowedWaitResult::IoCompletion) + } else if wait_result == WAIT_TIMEOUT.0 { + assert!(timeout_msec != INFINITE); + Ok(WindowedWaitResult::Timeout) + } else { + return Err(windows::core::Error::from_win32().into()); + } +} + +pub unsafe fn clear_io_completion_port(handle: HANDLE) -> Result<(), windows::core::Error> { + let mut iosb = IO_STATUS_BLOCK::default(); + let frci = FILE_COMPLETION_INFORMATION::default(); + NtSetInformationFile( + handle, + &mut iosb, + (&frci as *const FILE_COMPLETION_INFORMATION).cast(), + size_of::() as u32, + FileReplaceCompletionInformation, + ) + .ok() +} diff --git a/xen-win-utils/src/pipe_talker.rs b/xen-win-utils/src/pipe_talker.rs new file mode 100644 index 0000000..eac52c4 --- /dev/null +++ b/xen-win-utils/src/pipe_talker.rs @@ -0,0 +1,358 @@ +use std::mem; + +use windows::{ + core::HSTRING, + Win32::{ + Foundation::{ERROR_NOT_ENOUGH_MEMORY, HANDLE}, + Security::{ + Authorization::{ + ConvertStringSecurityDescriptorToSecurityDescriptorW, SDDL_REVISION_1, + }, + PSECURITY_DESCRIPTOR, SECURITY_DESCRIPTOR, + }, + System::IO::OVERLAPPED, + }, +}; + +use crate::{ + heap::LocalPointer, + named_pipe::{NamedPipe, NamedPipeResult}, +}; + +// Pipe message format: u32 (native order) followed by message + +enum PipeTalkerReadState { + // no pending read, no pending operation + Ready, + // pending read, waiting for size; u32 = remaining bytes (of 4); vec: existing valid data (already read) + PendingSized(u32, Vec), + // no pending read, waiting for data; u32 = bytes needed + WaitingForData(u32, Vec), + // pending read, waiting for data; u32 = remaining bytes + PendingData(u32, Vec), + Error, +} + +enum PipeTalkerWriteState { + // no pending write; vec: queued bytes (not yet written) + Queued(Vec), + // pending write + Pending(Vec), + Error, +} + +enum PipeTalkerConnectState { + Ready, + Pending, + Error, +} + +pub struct PipeTalker { + read_state: PipeTalkerReadState, + write_state: PipeTalkerWriteState, + connect_state: PipeTalkerConnectState, + pipe: NamedPipe, + max_message_size: u32, + max_write_queue_size: u32, +} + +pub enum PipeAsyncResult { + Message(T), + More, + Blocked, +} + +pub enum PipeTalkerResult { + Read(Option>), + Written, + Connected, +} + +impl PipeTalker { + fn create_sd(sddl: &str) -> windows::core::Result> { + let mut sd = PSECURITY_DESCRIPTOR::default(); + let mut sdlen = 0u32; + unsafe { + ConvertStringSecurityDescriptorToSecurityDescriptorW( + &HSTRING::from(sddl), + SDDL_REVISION_1, + &mut sd, + Some(&mut sdlen), + )?; + } + unsafe { Ok(LocalPointer::from_raw_mut(sd.0.cast())) } + } + + fn new( + path: &str, + max_message_size: u32, + max_write_queue_size: u32, + evented: bool, + create: bool, + create_sddl: Option<&str>, + ) -> windows::core::Result { + let pipe = if create { + let sd = create_sddl.map(Self::create_sd).transpose()?; + NamedPipe::create(path, true, evented, sd.as_deref().map(|x| (&x[0], false))) + } else { + assert!(create_sddl.is_none()); + NamedPipe::open(path, true, evented) + }?; + Ok(PipeTalker { + read_state: PipeTalkerReadState::Ready, + write_state: PipeTalkerWriteState::Queued(vec![]), + connect_state: if create { + PipeTalkerConnectState::Ready + } else { + PipeTalkerConnectState::Error + }, + pipe, + max_message_size, + max_write_queue_size, + }) + } + + pub fn create( + path: &str, + max_message_size: u32, + max_write_queue_size: u32, + evented: bool, + sddl: Option<&str>, + ) -> windows::core::Result { + PipeTalker::new( + path, + max_message_size, + max_write_queue_size, + evented, + true, + sddl, + ) + } + + pub fn open( + path: &str, + max_message_size: u32, + max_write_queue_size: u32, + evented: bool, + ) -> windows::core::Result { + PipeTalker::new( + path, + max_message_size, + max_write_queue_size, + evented, + false, + None, + ) + } + + pub unsafe fn get_handle(&self) -> HANDLE { + self.pipe.get_handle() + } + + pub unsafe fn get_read_event(&self) -> Option { + self.pipe.get_read_event() + } + + pub unsafe fn get_write_event(&self) -> Option { + self.pipe.get_write_event() + } + + pub unsafe fn get_connect_event(&self) -> Option { + self.pipe.get_connect_event() + } + + // if function returns bool, there's a completion being queued already + pub fn begin_read(&mut self) -> windows::core::Result { + let result: bool; + self.read_state = match mem::replace(&mut self.read_state, PipeTalkerReadState::Error) { + PipeTalkerReadState::Ready => { + let count = size_of::() as u32; + result = self.pipe.begin_read(count)?; + windows::core::Result::Ok(PipeTalkerReadState::PendingSized(count, vec![])) + } + PipeTalkerReadState::WaitingForData(remain, data) => { + result = self.pipe.begin_read(remain)?; + Ok(PipeTalkerReadState::PendingData(remain, data)) + } + _ => panic!("begin_read is inappropriate at this time"), + }?; + Ok(result) + } + + fn on_read_message_size( + &mut self, + remain: u32, + mut data: Vec, + new_data: Box<[u8]>, + ) -> windows::core::Result { + assert!(new_data.len() <= remain as usize); + data.extend_from_slice(&new_data); + if new_data.len() < remain as usize { + windows::core::Result::Ok(PipeTalkerReadState::PendingSized( + remain - new_data.len() as u32, + data, + )) + } else { + match u32::from_ne_bytes(data.try_into().unwrap()) { + // ping + 0 => Ok(PipeTalkerReadState::Ready), + message_size => { + if message_size > self.max_message_size { + Err(ERROR_NOT_ENOUGH_MEMORY.into()) + } else { + Ok(PipeTalkerReadState::WaitingForData(message_size, vec![])) + } + } + } + } + } + + fn on_read_data( + &mut self, + remain: u32, + mut data: Vec, + new_data: Box<[u8]>, + ) -> windows::core::Result<(PipeTalkerReadState, Option>)> { + assert!(new_data.len() <= remain as usize); + data.extend_from_slice(&new_data); + if new_data.len() < remain as usize { + windows::core::Result::Ok(( + PipeTalkerReadState::PendingData(remain - new_data.len() as u32, data), + None, + )) + } else { + Ok((PipeTalkerReadState::Ready, Some(data.into_boxed_slice()))) + } + } + + fn complete_read(&mut self, new_data: Box<[u8]>) -> windows::core::Result>> { + let mut result: Option> = None; + self.read_state = match mem::replace(&mut self.read_state, PipeTalkerReadState::Error) { + PipeTalkerReadState::PendingSized(remain, data) => { + self.on_read_message_size(remain, data, new_data) + } + PipeTalkerReadState::PendingData(remain, data) => { + let (state, message) = self.on_read_data(remain, data, new_data)?; + result = message; + Ok(state) + } + _ => panic!("end_read is inappropriate at this time"), + }?; + Ok(result) + } + + pub fn end_read(&mut self) -> windows::core::Result>> { + let new_data = self.pipe.end_read_evented()?; + self.complete_read(new_data) + } + + fn do_queue_write( + &mut self, + message: Option<&[u8]>, + mut remain: Vec, + is_pending: bool, + ) -> windows::core::Result<(bool, PipeTalkerWriteState)> { + let mut result = false; + if let Some(msg) = message { + if msg.len() > self.max_message_size as usize { + return Err(ERROR_NOT_ENOUGH_MEMORY.into()); + } + remain.extend_from_slice((msg.len() as u32).to_ne_bytes().as_slice()); + remain.extend_from_slice(msg); + if remain.len() > self.max_write_queue_size as usize { + return Err(ERROR_NOT_ENOUGH_MEMORY.into()); + } + } + if !(is_pending || remain.is_empty()) { + result = self.pipe.begin_write(remain.as_slice())?; + } + let state: PipeTalkerWriteState = if remain.is_empty() { + PipeTalkerWriteState::Queued(remain) + } else { + PipeTalkerWriteState::Pending(remain) + }; + Ok((result, state)) + } + + pub fn queue_write(&mut self, message: Option<&[u8]>) -> windows::core::Result { + let (result, new_state) = + match mem::replace(&mut self.write_state, PipeTalkerWriteState::Error) { + PipeTalkerWriteState::Queued(remain) => self.do_queue_write(message, remain, false), + PipeTalkerWriteState::Pending(remain) => self.do_queue_write(message, remain, true), + _ => panic!("begin_write is inappropriate at this time"), + } + .inspect_err(|e| log::error!("do_queue_write {e}"))?; + self.write_state = new_state; + Ok(result) + } + + fn complete_write(&mut self, written: u32) -> windows::core::Result<()> { + self.write_state = match mem::replace(&mut self.write_state, PipeTalkerWriteState::Error) { + PipeTalkerWriteState::Pending(mut remain) => { + assert!(written as usize <= remain.len()); + remain.drain(0..written as usize); + windows::core::Result::Ok(PipeTalkerWriteState::Queued(remain)) + } + _ => panic!("end_write is inappropriate at this time"), + }?; + Ok(()) + } + + pub fn end_write(&mut self) -> windows::core::Result<()> { + let count = self.pipe.end_write_evented()?; + self.complete_write(count) + } + + pub fn begin_connect(&mut self) -> windows::core::Result { + let result: bool; + self.connect_state = + match mem::replace(&mut self.connect_state, PipeTalkerConnectState::Error) { + PipeTalkerConnectState::Ready => { + result = self.pipe.begin_connect()?; + windows::core::Result::Ok(if result { + PipeTalkerConnectState::Ready + } else { + PipeTalkerConnectState::Pending + }) + } + _ => panic!("begin_connect is inappropriate at this time"), + }?; + Ok(result) + } + + fn complete_connect(&mut self) -> windows::core::Result<()> { + self.connect_state = + match mem::replace(&mut self.connect_state, PipeTalkerConnectState::Error) { + PipeTalkerConnectState::Pending => { + windows::core::Result::Ok(PipeTalkerConnectState::Ready) + } + _ => panic!("end_connect is inappropriate at this time"), + }?; + Ok(()) + } + + pub fn end_connect(&mut self) -> windows::core::Result<()> { + self.pipe.end_connect_evented()?; + self.complete_connect() + } + + pub unsafe fn complete_io( + &mut self, + overlapped: *const OVERLAPPED, + result: windows::core::Result, + ) -> windows::core::Result { + match self.pipe.complete_io(overlapped, result)? { + NamedPipeResult::Read(new_data) => { + Ok(PipeTalkerResult::Read(self.complete_read(new_data)?)) + } + NamedPipeResult::Written(count) => { + self.complete_write(count)?; + Ok(PipeTalkerResult::Written) + } + NamedPipeResult::Connected => { + self.complete_connect()?; + Ok(PipeTalkerResult::Connected) + } + } + } +} diff --git a/xen-guest-agent/src/windows_debug_logger.rs b/xen-win-utils/src/windows_debug_logger.rs similarity index 80% rename from xen-guest-agent/src/windows_debug_logger.rs rename to xen-win-utils/src/windows_debug_logger.rs index 9613230..c2907b9 100644 --- a/xen-guest-agent/src/windows_debug_logger.rs +++ b/xen-win-utils/src/windows_debug_logger.rs @@ -1,7 +1,9 @@ use log::Log; use windows::{core::HSTRING, Win32::System::Diagnostics::Debug::OutputDebugStringW}; -pub(crate) struct WindowsDebugLogger {} +pub struct WindowsDebugLogger { + pub prefix: String, +} impl Log for WindowsDebugLogger { fn enabled(&self, _metadata: &log::Metadata) -> bool { @@ -10,7 +12,8 @@ impl Log for WindowsDebugLogger { fn log(&self, record: &log::Record) { let message = format!( - "[xen-guest-agent] {}: {}\r\n", + "{} {}: {}\r\n", + self.prefix, record.level().as_str(), record.args() );