diff --git a/examples/monitor_control.py b/examples/monitor_control.py index c22c9fb4..cfd46ee6 100644 --- a/examples/monitor_control.py +++ b/examples/monitor_control.py @@ -131,12 +131,15 @@ # you can ask to be notified. # this represents the complete current state of the driver # -# DriverClient.receive(Callable[list[Monitor], None]) +# DriverClient.receive(Optional[Callable[list[Monitor], None]]) client.receive(lambda d: print(d)) # one way to use this might be to auto update your driver instance def set_monitors(data): client.monitors = data +# calling it on a new callback will cancel the old one and set the new one client.receive(set_monitors) +# calling it with no args will cancel the current receiver +client.receive() # gets latest states from driver # diff --git a/rust/Cargo.lock b/rust/Cargo.lock index ef84dcb8..087604b7 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -388,7 +388,7 @@ dependencies = [ "serde", "serde_json", "thiserror", - "win-pipes", + "tokio", "windows", "winreg", ] @@ -999,7 +999,6 @@ version = "0.1.0" dependencies = [ "driver-ipc", "pyo3", - "windows", ] [[package]] @@ -1156,6 +1155,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + [[package]] name = "smallvec" version = "1.13.2" @@ -1278,7 +1286,9 @@ dependencies = [ "libc", "mio", "num_cpus", + "parking_lot", "pin-project-lite", + "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.48.0", diff --git a/rust/bindings/pyvdd/Cargo.toml b/rust/bindings/pyvdd/Cargo.toml index 9286a3d1..989433a3 100644 --- a/rust/bindings/pyvdd/Cargo.toml +++ b/rust/bindings/pyvdd/Cargo.toml @@ -10,9 +10,5 @@ crate-type = ["cdylib"] pyo3 = "0.21.1" driver-ipc = { path = "../../driver-ipc" } -[dependencies.windows] -version = "0.54.0" -features = ["Win32_Foundation", "Win32_System_Threading"] - [lints] workspace = true diff --git a/rust/bindings/pyvdd/src/lib.rs b/rust/bindings/pyvdd/src/lib.rs index 114b7d91..f64939d7 100644 --- a/rust/bindings/pyvdd/src/lib.rs +++ b/rust/bindings/pyvdd/src/lib.rs @@ -5,20 +5,15 @@ mod utils; +use std::fmt::Debug; use std::{ borrow::{Borrow, Cow}, collections::HashSet, fmt::Display, - sync::{ - atomic::{AtomicBool, Ordering}, - Arc, - }, + sync::atomic::{AtomicBool, Ordering}, }; -use std::{fmt::Debug, sync::Mutex}; -use driver_ipc::{ - ClientCommand, Dimen, DriverClient, EventCommand, Id, Mode, Monitor, RefreshRate, -}; +use driver_ipc::{Dimen, DriverClient, EventCommand, Id, Mode, Monitor, RefreshRate}; use pyo3::prelude::*; use pyo3::{ exceptions::{PyIndexError, PyRuntimeError, PyTypeError}, @@ -26,13 +21,6 @@ use pyo3::{ types::{DerefToPyAny, PyList, PyLong}, DowncastIntoError, PyClass, PyTypeCheck, }; -use windows::Win32::{ - Foundation::{DuplicateHandle, DUPLICATE_SAME_ACCESS, HANDLE}, - System::{ - Threading::{GetCurrentProcess, GetCurrentThread}, - IO::CancelSynchronousIo, - }, -}; use self::utils::IntoPyErr as _; @@ -336,7 +324,6 @@ impl IntoPyListIter for Py { #[pyo3(name = "DriverClient")] struct PyDriverClient { client: DriverClient, - thread_registry: Arc>>, /// The list of monitors /// Sig: list[Monitor] #[pyo3(get)] @@ -358,11 +345,7 @@ impl PyDriverClient { let monitors = state_to_pytypedlist(py, client.monitors())?; - let slf = Self { - client, - monitors, - thread_registry: Arc::new(Mutex::new(None)), - }; + let slf = Self { client, monitors }; Ok(slf) } @@ -417,58 +400,28 @@ impl PyDriverClient { } /// Get notified of other clients changing driver configuration - /// Sig: receive(Callable[list[Monitor], None]) - fn receive(&self, callback: PyObject) { - // cancel the receiver internally if called again - { - let lock = self.thread_registry.lock().unwrap().take(); - if let Some(thread) = lock { - unsafe { - _ = CancelSynchronousIo(thread); - } - } + /// Sig: receive(Optional[Callable[list[Monitor], None]]) + fn receive(&self, callback: Option) { + if let Some(callback) = callback { + self.client.set_event_receiver(move |cmd| { + let EventCommand::Changed(data) = cmd else { + unreachable!() + }; + + Python::with_gil(|py| { + let state = state_to_pylist(py, &data); + let Ok(state) = state else { + return; + }; + + if let Err(e) = callback.call1(py, (state,)) { + e.print(py); + } + }); + }); + } else { + self.client.terminate_receiver(); } - - let registry = self.thread_registry.clone(); - self.client.set_receiver( - // init - store thread handle for later - Some(move || { - let mut lock = registry.lock().unwrap(); - - let pseudo_handle = unsafe { GetCurrentThread() }; - let current_process = unsafe { GetCurrentProcess() }; - - let mut thread_handle = HANDLE::default(); - unsafe { - _ = DuplicateHandle( - current_process, - pseudo_handle, - current_process, - &mut thread_handle, - 0, - false, - DUPLICATE_SAME_ACCESS, - ); - } - - *lock = Some(thread_handle); - }), - // cb - move |command| { - if let ClientCommand::Event(EventCommand::Changed(data)) = command { - Python::with_gil(|py| { - let state = state_to_pylist(py, &data); - let Ok(state) = state else { - return; - }; - - if let Err(e) = callback.call1(py, (state,)) { - e.print(py); - } - }); - } - }, - ); } /// Find a monitor by Id diff --git a/rust/driver-ipc/Cargo.toml b/rust/driver-ipc/Cargo.toml index a2daf56b..b44fb0f1 100644 --- a/rust/driver-ipc/Cargo.toml +++ b/rust/driver-ipc/Cargo.toml @@ -10,7 +10,7 @@ thiserror = "1.0.58" owo-colors = "4.0.0" serde_json = "1.0.114" windows = { version = "0.54.0", features = ["Win32_Foundation"] } -win-pipes = { git = "https://github.com/MolotovCherry/WinPipes-rs" } lazy_format = "2.0.3" joinery = "3.1.0" winreg = "0.52.0" +tokio = { version = "1.37.0", features = ["full"] } diff --git a/rust/driver-ipc/src/client.rs b/rust/driver-ipc/src/client.rs index 321b7af5..775d823a 100644 --- a/rust/driver-ipc/src/client.rs +++ b/rust/driver-ipc/src/client.rs @@ -1,74 +1,194 @@ -use std::io::{prelude::Read, Write as _}; +use std::{ + convert::Infallible, + io, + sync::{ + atomic::{AtomicBool, Ordering}, + mpsc::{channel, Receiver, Sender}, + Arc, + }, + thread, +}; use log::error; -use serde::{de::DeserializeOwned, Serialize}; -use win_pipes::{NamedPipeClientReader, NamedPipeClientWriter}; +use serde::Serialize; +use tokio::{ + net::windows::named_pipe::{ClientOptions, NamedPipeClient, PipeMode}, + runtime::{Builder, Runtime}, + sync::{ + mpsc::{unbounded_channel, UnboundedReceiver}, + Mutex, + }, +}; use winreg::{ enums::{HKEY_CURRENT_USER, KEY_WRITE}, RegKey, }; -use crate::{ClientCommand, DriverCommand, Id, Monitor, RequestCommand, Result}; +use crate::{ + utils::LazyLock, ClientCommand, DriverCommand, EventCommand, Id, IpcError, Monitor, + ReplyCommand, RequestCommand, Result, +}; // EOF byte used to separate messages const EOF: u8 = 0x4; +pub(crate) static RUNTIME: LazyLock = + LazyLock::new(|| Builder::new_multi_thread().enable_all().build().unwrap()); + /// A thin api client over the driver api with all the essential api. /// Does not track state for you /// -/// This is cloneable and won't drop underlying handle until all instances -/// are dropped +/// This is cloneable and won't drop the client connection until all +/// instances are dropped #[derive(Debug, Clone)] pub struct Client { - pub(crate) writer: NamedPipeClientWriter, - pub(crate) reader: NamedPipeClientReader, + client: Arc, + is_event: Arc, + is_event_async: Arc, + event_recv: Arc>>, + event_recv_async: Arc>>, + client_recv: Arc>>, } impl Client { + /// connect to pipe virtualdisplaydriver pub fn connect() -> Result { - let (reader, writer) = win_pipes::NamedPipeClientOptions::new("virtualdisplaydriver") - .wait() - .access_duplex() - .mode_byte() - .create()?; + Self::connect_to("virtualdisplaydriver") + } + + // choose which pipe name you connect to + // pipe name is ONLY the name, only the {name} portion of \\.\pipe\{name} + pub fn connect_to(name: &str) -> Result { + let fut = async { + ClientOptions::new() + .read(true) + .write(true) + .pipe_mode(PipeMode::Byte) + .open(format!(r"\\.\pipe\{name}")) + .map_err(IpcError::ConnectionFailed) + }; - Ok(Self { reader, writer }) + let client = Arc::new(RUNTIME.block_on(fut)?); + let is_event = Arc::new(AtomicBool::new(false)); + let is_event_async = Arc::new(AtomicBool::new(false)); + let (event_send, event_recv) = channel(); + let (event_send_async, event_recv_async) = unbounded_channel(); + let (client_send, client_recv) = channel(); + + let slf = Self { + client: client.clone(), + is_event: is_event.clone(), + is_event_async: is_event_async.clone(), + event_recv: Arc::new(Mutex::new(event_recv)), + event_recv_async: Arc::new(Mutex::new(event_recv_async)), + client_recv: Arc::new(Mutex::new(client_recv)), + }; + + let (tx, rx) = channel(); + + // receive command thread + thread::spawn(move || { + _ = RUNTIME.block_on(receive_command(&client, tx)); + }); + + thread::spawn(move || loop { + let Ok(data) = rx.recv() else { + // sender closed + break; + }; + + let is_event = is_event.load(Ordering::Acquire); + let is_event_async = is_event_async.load(Ordering::Acquire); + let data_is_event = matches!(data, ClientCommand::Event(_)); + + if (is_event || is_event_async) && data_is_event { + let ClientCommand::Event(e) = data else { + unreachable!() + }; + + if is_event { + _ = event_send.send(e.clone()); + } + + if is_event_async { + _ = event_send_async.send(e); + } + } else if let ClientCommand::Reply(r) = data { + _ = client_send.send(r); + } + }); + + Ok(slf) } /// Notifies driver of changes (additions/updates/removals) - pub fn notify(&mut self, monitors: &[Monitor]) -> Result<()> { + pub fn notify(&self, monitors: &[Monitor]) -> Result<()> { let command = DriverCommand::Notify(monitors.to_owned()); - send_command(&mut self.writer, &command) + send_command(&self.client, &command) } /// Remove specific monitors by id - pub fn remove(&mut self, ids: &[Id]) -> Result<()> { + pub fn remove(&self, ids: &[Id]) -> Result<()> { let command = DriverCommand::Remove(ids.to_owned()); - send_command(&mut self.writer, &command) + send_command(&self.client, &command) } /// Remove all monitors - pub fn remove_all(&mut self) -> Result<()> { + pub fn remove_all(&self) -> Result<()> { let command = DriverCommand::RemoveAll; - send_command(&mut self.writer, &command) + send_command(&self.client, &command) } /// Receive generic reply /// - /// This is required because a reply could be any of these at any moment - pub fn receive(&mut self) -> Result { - receive_command(&mut self.reader) + /// If `last` is false, will only receive new messages from the point of calling + /// If `last` is true, will receive the the last message received, or if none, blocks until the next one + /// + /// The reason for the `last` flag is that replies are auto buffered in the background, so if you send a + /// request, the reply may be missed + pub fn receive_reply(&self, last: bool) -> Option { + let lock = self.client_recv.blocking_lock(); + + if last { + last_recv(&lock) + } else { + latest_recv(&lock) + } + } + + /// Receive an event. Only new events after calling this are received + pub fn receive_event(&self) -> EventCommand { + self.is_event.store(true, Ordering::Release); + + let lock = self.event_recv.blocking_lock(); + let event = latest_recv(&lock); + + self.is_event.store(false, Ordering::Release); + + event.unwrap() + } + + /// Receive an event. Only new events after calling this are received + pub async fn receive_event_async(&self) -> EventCommand { + self.is_event_async.store(true, Ordering::Release); + + let mut lock = self.event_recv_async.lock().await; + let event = latest_event_recv_await(&mut lock).await; + + self.is_event_async.store(false, Ordering::Release); + + event.unwrap() } /// Request state update /// use `receive()` to get the reply - pub fn request_state(&mut self) -> Result<()> { + pub fn request_state(&self) -> Result<()> { let command = RequestCommand::State; - send_command(&mut self.writer, &command) + send_command(&self.client, &command) } /// Persist changes to registry for current user @@ -99,45 +219,158 @@ impl Client { } } -fn send_command(writer: &mut NamedPipeClientWriter, command: &impl Serialize) -> Result<()> { +fn send_command(client: &NamedPipeClient, command: &impl Serialize) -> Result<()> { // Create a vector with the full message, then send it as a single // write. This is required because the pipe is in message mode. let mut message = serde_json::to_vec(command)?; message.push(EOF); - writer.write_all(&message)?; - writer.flush()?; + + // write to pipe without needing to block or split it + let fut = async { + let mut written = 0; + loop { + // wait for pipe to be writable + client.writable().await?; + + match client.try_write(&message[written..]) { + // we wrote less than the entire size + Ok(n) if written + n < message.len() => { + written += n; + continue; + } + + // write succeeded + Ok(n) if written + n >= message.len() => { + break; + } + + // nothing wrote, retry again + Err(e) if e.kind() == io::ErrorKind::WouldBlock => { + continue; + } + + // actual error + Err(e) => { + return Err(e); + } + + _ => unreachable!(), + } + } + + std::io::Result::Ok(()) + }; + + RUNTIME.block_on(fut)?; Ok(()) } -fn receive_command(reader: &mut NamedPipeClientReader) -> Result { - let mut msg_buf = Vec::with_capacity(4096); +// receive all commands and send them back to the receiver +async fn receive_command( + client: &NamedPipeClient, + tx: Sender, +) -> Result { let mut buf = vec![0; 4096]; + let mut recv_buf = Vec::with_capacity(4096); loop { - let Ok(size) = reader.read(&mut buf) else { - break; - }; + // wait for client to be readable + client.readable().await?; - msg_buf.extend_from_slice(&buf[..size]); + match client.try_read(&mut buf) { + Ok(0) => return Err(IpcError::Io(io::Error::last_os_error())), - if msg_buf.last().is_some_and(|&byte| byte == EOF) { - // pop off EOF - msg_buf.pop(); + Ok(n) => recv_buf.extend(&buf[..n]), - break; + Err(e) if e.kind() == io::ErrorKind::WouldBlock => { + continue; + } + + Err(e) => { + return Err(e.into()); + } + } + + let eof_iter = + recv_buf.iter().enumerate().filter_map( + |(i, &byte)| { + if byte == EOF { + Some(i) + } else { + None + } + }, + ); + + let mut bidx = 0; + for pos in eof_iter { + let data = &recv_buf[bidx..pos]; + bidx = pos + 1; + + let Ok(command) = serde_json::from_slice::(data) else { + continue; + }; + + if tx.send(command).is_err() { + return Err(IpcError::SendFailed); + } + } + + // drain all processed messages + recv_buf.drain(..bidx); + } +} + +/// Drains the channel and returns last message in the queue. If channel was empty, blocks for the next message +fn last_recv(receiver: &Receiver) -> Option { + use std::sync::mpsc::TryRecvError; + + let mut buf = None; + + loop { + match receiver.try_recv() { + Ok(t) => buf = Some(t), + + Err(TryRecvError::Empty) => { + break if buf.is_none() { + receiver.recv().ok() + } else { + buf + }; + } + + Err(TryRecvError::Disconnected) => break None, + } + } +} + +/// Drains the channel and blocks for the next message +fn latest_recv(receiver: &Receiver) -> Option { + use std::sync::mpsc::TryRecvError; + + loop { + match receiver.try_recv() { + Ok(_) => (), + + Err(TryRecvError::Empty) => break receiver.recv().ok(), + + Err(TryRecvError::Disconnected) => break None, } } +} - // in the following we assume we always get a fully formed message, e.g. no multiple messages +/// Drains the channel and blocks for the next message +async fn latest_event_recv_await(receiver: &mut UnboundedReceiver) -> Option { + use tokio::sync::mpsc::error::TryRecvError; - // interior EOF is not valid - assert!( - !msg_buf.contains(&EOF), - "interior eof detected, this is a bug; msg: {msg_buf:?}" - ); + loop { + match receiver.try_recv() { + Ok(_) => (), - let command = serde_json::from_slice(&msg_buf)?; + Err(TryRecvError::Empty) => break receiver.recv().await, - Ok(command) + Err(TryRecvError::Disconnected) => break None, + } + } } diff --git a/rust/driver-ipc/src/core.rs b/rust/driver-ipc/src/core.rs index ffa4fc78..0047ba7f 100644 --- a/rust/driver-ipc/src/core.rs +++ b/rust/driver-ipc/src/core.rs @@ -62,7 +62,7 @@ pub enum EventCommand { /// This makes the deserialization process much easier to handle /// when a received command could be of multiple types #[non_exhaustive] -#[derive(Debug, Clone, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum ServerCommand { Driver(DriverCommand), @@ -73,7 +73,7 @@ pub enum ServerCommand { /// This makes the deserialization process much easier to handle /// when a received command could be of multiple types #[non_exhaustive] -#[derive(Debug, Clone, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum ClientCommand { Reply(ReplyCommand), diff --git a/rust/driver-ipc/src/driver_client.rs b/rust/driver-ipc/src/driver_client.rs index f953b68e..c30b391d 100644 --- a/rust/driver-ipc/src/driver_client.rs +++ b/rust/driver-ipc/src/driver_client.rs @@ -1,42 +1,59 @@ -use std::{collections::HashSet, mem::ManuallyDrop, thread}; +use std::{ + collections::HashSet, + sync::{Mutex, OnceLock}, + thread, +}; -use windows::Win32::Foundation::{CloseHandle, HANDLE}; +use tokio::sync::mpsc::{channel, Sender}; use crate::{ - Client, ClientCommand, ClientError, Id, IpcError, Mode, Monitor, ReplyCommand, Result, + client::RUNTIME, Client, ClientError, EventCommand, Id, IpcError, Mode, Monitor, ReplyCommand, + Result, }; +// used to terminate existing receivers +static RECEIVER_SHUTDOWN_TOKEN: Mutex>> = Mutex::new(OnceLock::new()); + /// Extra API over Client which allows nice fancy things #[derive(Debug)] pub struct DriverClient { - // I'll handle dropping of this field manually - client: ManuallyDrop, + client: Client, state: Vec, } impl DriverClient { + /// connect to default driver name pub fn new() -> Result { - let mut client = ManuallyDrop::new(Client::connect()?); + let mut client = Client::connect()?; let state = Self::_get_state(&mut client)?; Ok(Self { client, state }) } - fn _get_state(client: &mut ManuallyDrop) -> Result> { + /// specify pipe name to connect to + pub fn new_with(name: &str) -> Result { + let mut client = Client::connect_to(name)?; + + let state = Self::_get_state(&mut client)?; + + Ok(Self { client, state }) + } + + fn _get_state(client: &mut Client) -> Result> { client.request_state()?; - while let Ok(command) = client.receive() { - match command { - ClientCommand::Reply(ReplyCommand::State(state)) => { + loop { + match client.receive_reply(true) { + Some(ReplyCommand::State(state)) => { return Ok(state); } - _ => continue, + _ => { + continue; + } } } - - Err(IpcError::RequestState) } fn get_state(&mut self) -> Result> { @@ -84,27 +101,50 @@ impl DriverClient { /// Supply a callback used to receive commands from the driver /// + /// This only allows one receiver at a time. Setting a new cb will also terminate existing reciever + /// /// Note: DriverClient DOES NOT do any hidden state changes! Only calling proper api will change internal state. /// driver state IS NOT updated on its own when Event commands are received! /// if you want to update internal state, call set_monitors on DriverClient in your callback /// to properly handle it! - pub fn set_receiver( - &self, - init: Option, - cb: impl Fn(ClientCommand) + Send + 'static, - ) { - let mut client = self.client.clone(); - thread::spawn(move || { - if let Some(init) = init { - init(); - } + pub fn set_event_receiver(&self, cb: impl Fn(EventCommand) + Send + 'static) { + // stop currently running task if any exist + if let Some(sender) = RECEIVER_SHUTDOWN_TOKEN.lock().unwrap().take() { + sender.blocking_send(()).unwrap(); + } - while let Ok(command) = client.receive() { - cb(command); + let client = self.client.clone(); + let fut = async move { + let (tx, mut rx) = channel(1); + RECEIVER_SHUTDOWN_TOKEN.lock().unwrap().set(tx).unwrap(); + + loop { + tokio::select! { + cmd = client.receive_event_async() => { + cb(cmd); + } + + // shutdown signal + _ = rx.recv() => { + break; + } + } } + }; + + thread::spawn(move || { + RUNTIME.block_on(fut); }); } + /// Terminate a receiver without setting a new one + pub fn terminate_receiver(&self) { + // stop currently running task if any exist + if let Some(sender) = RECEIVER_SHUTDOWN_TOKEN.lock().unwrap().take() { + sender.blocking_send(()).unwrap(); + } + } + /// Get the current monitor state pub fn monitors(&self) -> &[Monitor] { &self.state @@ -348,19 +388,6 @@ impl DriverClient { } } -impl Drop for DriverClient { - fn drop(&mut self) { - use std::os::windows::io::AsRawHandle as _; - // get raw pipe handle. reader/writer doesn't matter, they're all the same handle - let handle = self.client.writer.as_raw_handle(); - - // manually close handle so that ReadFile stops blocking our thread - unsafe { - _ = CloseHandle(HANDLE(handle as _)); - } - } -} - fn mons_have_duplicates(monitors: &[Monitor]) -> Result<()> { let mut monitor_iter = monitors.iter(); while let Some(monitor) = monitor_iter.next() { diff --git a/rust/driver-ipc/src/lib.rs b/rust/driver-ipc/src/lib.rs index 90264a5a..2e5b3174 100644 --- a/rust/driver-ipc/src/lib.rs +++ b/rust/driver-ipc/src/lib.rs @@ -1,6 +1,7 @@ mod client; mod core; mod driver_client; +mod utils; pub use client::Client; pub use core::*; @@ -20,6 +21,12 @@ pub enum IpcError { Client(#[from] ClientError), #[error("failed to get request state")] RequestState, + #[error("failed to receive command")] + Receive, + #[error("failed to open pipe. is driver installed and working?\nerror: {0}")] + ConnectionFailed(std::io::Error), + #[error("channel closed")] + SendFailed, } #[derive(Debug, thiserror::Error)] diff --git a/rust/driver-ipc/src/utils.rs b/rust/driver-ipc/src/utils.rs new file mode 100644 index 00000000..e9d108ef --- /dev/null +++ b/rust/driver-ipc/src/utils.rs @@ -0,0 +1,21 @@ +pub struct LazyLock T> { + data: ::std::sync::OnceLock, + f: F, +} + +impl LazyLock { + pub const fn new(f: F) -> LazyLock { + Self { + data: ::std::sync::OnceLock::new(), + f, + } + } +} + +impl ::std::ops::Deref for LazyLock { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.data.get_or_init(self.f) + } +} diff --git a/rust/vdd-user-session-service/Cargo.toml b/rust/vdd-user-session-service/Cargo.toml index c62717bc..b93ead61 100644 --- a/rust/vdd-user-session-service/Cargo.toml +++ b/rust/vdd-user-session-service/Cargo.toml @@ -18,6 +18,7 @@ features = [ "Win32_System_Services", "Win32_UI_WindowsAndMessaging", "Win32_System_Threading", + "Win32_Security", ] [lints] diff --git a/rust/vdd-user-session-service/src/service.rs b/rust/vdd-user-session-service/src/service.rs index de98955f..7b44972c 100644 --- a/rust/vdd-user-session-service/src/service.rs +++ b/rust/vdd-user-session-service/src/service.rs @@ -73,7 +73,7 @@ fn run_service(_arguments: &[OsString]) -> windows_service::Result<()> { } SessionChangeReason::SessionLogoff => { - let Ok(mut client) = Client::connect() else { + let Ok(client) = Client::connect() else { return ServiceControlHandlerResult::Other(0x3); };