diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bcb5f50f28..b0c1d080ec 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -141,6 +141,8 @@ jobs: run: cargo test -p sample_service_sys --target ${{ matrix.target }} - name: Test sample_service_thread run: cargo test -p sample_service_thread --target ${{ matrix.target }} + - name: Test sample_service_time + run: cargo test -p sample_service_time --target ${{ matrix.target }} - name: Test sample_shell run: cargo test -p sample_shell --target ${{ matrix.target }} - name: Test sample_simple @@ -159,10 +161,10 @@ jobs: run: cargo test -p sample_uiautomation --target ${{ matrix.target }} - name: Test sample_wmi run: cargo test -p sample_wmi --target ${{ matrix.target }} - - name: Test sample_xml - run: cargo test -p sample_xml --target ${{ matrix.target }} - name: Clean run: cargo clean + - name: Test sample_xml + run: cargo test -p sample_xml --target ${{ matrix.target }} - name: Test test_agile run: cargo test -p test_agile --target ${{ matrix.target }} - name: Test test_agile_reference @@ -261,10 +263,10 @@ jobs: run: cargo test -p test_metadata --target ${{ matrix.target }} - name: Test test_msrv run: cargo test -p test_msrv --target ${{ matrix.target }} - - name: Test test_no_std - run: cargo test -p test_no_std --target ${{ matrix.target }} - name: Clean run: cargo clean + - name: Test test_no_std + run: cargo test -p test_no_std --target ${{ matrix.target }} - name: Test test_no_use run: cargo test -p test_no_use --target ${{ matrix.target }} - name: Test test_noexcept @@ -311,6 +313,8 @@ jobs: run: cargo test -p test_return_handle --target ${{ matrix.target }} - name: Test test_return_struct run: cargo test -p test_return_struct --target ${{ matrix.target }} + - name: Test test_services + run: cargo test -p test_services --target ${{ matrix.target }} - name: Test test_standalone run: cargo test -p test_standalone --target ${{ matrix.target }} - name: Test test_string_param @@ -361,12 +365,12 @@ jobs: run: cargo test -p tool_msvc --target ${{ matrix.target }} - name: Test tool_standalone run: cargo test -p tool_standalone --target ${{ matrix.target }} + - name: Clean + run: cargo clean - name: Test tool_test_all run: cargo test -p tool_test_all --target ${{ matrix.target }} - name: Test tool_workspace run: cargo test -p tool_workspace --target ${{ matrix.target }} - - name: Clean - run: cargo clean - name: Test tool_yml run: cargo test -p tool_yml --target ${{ matrix.target }} - name: Test windows diff --git a/crates/libs/services/readme.md b/crates/libs/services/readme.md index 9a178f58c8..b1054466bd 100644 --- a/crates/libs/services/readme.md +++ b/crates/libs/services/readme.md @@ -20,8 +20,9 @@ fn main() { windows_services::Service::new() .can_pause() .can_stop() - .run(|command| { + .run(|service, command| { // Respond to service commands... }) + .unwrap(); } ``` diff --git a/crates/libs/services/src/bindings.rs b/crates/libs/services/src/bindings.rs index 9f4cff14fc..fb6752cc3a 100644 --- a/crates/libs/services/src/bindings.rs +++ b/crates/libs/services/src/bindings.rs @@ -1,19 +1,19 @@ -#![allow( - non_snake_case, - non_upper_case_globals, - non_camel_case_types, - dead_code, - clippy::all -)] - -windows_link::link!("advapi32.dll" "system" fn RegisterServiceCtrlHandlerW(lpservicename : PCWSTR, lphandlerproc : LPHANDLER_FUNCTION) -> SERVICE_STATUS_HANDLE); +windows_link::link!("advapi32.dll" "system" fn RegisterServiceCtrlHandlerExW(lpservicename : PCWSTR, lphandlerproc : LPHANDLER_FUNCTION_EX, lpcontext : *const core::ffi::c_void) -> SERVICE_STATUS_HANDLE); windows_link::link!("advapi32.dll" "system" fn SetServiceStatus(hservicestatus : SERVICE_STATUS_HANDLE, lpservicestatus : *const SERVICE_STATUS) -> BOOL); windows_link::link!("advapi32.dll" "system" fn StartServiceCtrlDispatcherW(lpservicestarttable : *const SERVICE_TABLE_ENTRYW) -> BOOL); pub type BOOL = i32; pub type ENUM_SERVICE_TYPE = u32; -pub type LPHANDLER_FUNCTION = Option; +pub type LPHANDLER_FUNCTION_EX = Option< + unsafe extern "system" fn( + dwcontrol: u32, + dweventtype: u32, + lpeventdata: *mut core::ffi::c_void, + lpcontext: *mut core::ffi::c_void, + ) -> u32, +>; pub type LPSERVICE_MAIN_FUNCTIONW = Option; +pub const NO_ERROR: WIN32_ERROR = 0u32; pub type PCWSTR = *const u16; pub type PWSTR = *mut u16; pub const SERVICE_ACCEPT_PAUSE_CONTINUE: u32 = 2u32; @@ -55,3 +55,4 @@ impl Default for SERVICE_TABLE_ENTRYW { } } pub const SERVICE_WIN32_OWN_PROCESS: ENUM_SERVICE_TYPE = 16u32; +pub type WIN32_ERROR = u32; diff --git a/crates/libs/services/src/lib.rs b/crates/libs/services/src/lib.rs index f3944dbc94..abe845cec6 100644 --- a/crates/libs/services/src/lib.rs +++ b/crates/libs/services/src/lib.rs @@ -1,11 +1,18 @@ #![doc = include_str!("../readme.md")] #![cfg(windows)] -#![allow(clippy::needless_doctest_main)] +#![allow( + non_camel_case_types, + non_snake_case, + clippy::needless_doctest_main, + clippy::upper_case_acronyms, + clippy::type_complexity +)] mod bindings; use bindings::*; use std::boxed::Box; -use std::sync::{OnceLock, RwLock}; +use std::ffi::c_void; +use std::sync::RwLock; /// The commands are sent by the service control manager to the service through the closure or callback /// passed to the service `run` method. @@ -28,8 +35,30 @@ pub enum Command { /// /// This command will only be sent if the `can_pause` method is called as part of construction. Resume, + + /// An extended command. + /// + /// Specific commands will only be received if the `can_accept` methods is called to specify those + /// commands the service accepts. + Extended(ExtendedCommand), +} + +/// A command not specifically covered by the `Command` enum. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct ExtendedCommand { + /// The control code for the command. + pub control: u32, + + /// The event type, if any. + pub ty: u32, + + /// The event data, if any. + pub data: *const c_void, } +unsafe impl Send for ExtendedCommand {} +unsafe impl Sync for ExtendedCommand {} + /// Possible service states including transitional states. /// /// This can be useful to query the current state with the `state` function or set the state with @@ -45,67 +74,67 @@ pub enum State { StopPending, } -/// The current state the service. -pub fn state() -> State { - let reader = STATUS.read().unwrap(); - - match reader.dwCurrentState { - SERVICE_CONTINUE_PENDING => State::ContinuePending, - SERVICE_PAUSED => State::Paused, - SERVICE_PAUSE_PENDING => State::PausePending, - SERVICE_RUNNING => State::Running, - SERVICE_START_PENDING => State::StartPending, - SERVICE_STOPPED => State::Stopped, - SERVICE_STOP_PENDING => State::StopPending, - _ => panic!("unexpected state"), - } +/// A service builder, providing control over what commands the service supports before the service begins to run. +pub struct Service<'a> { + accept: u32, + fallback: Option>, + handle: RwLock, + callback: RwLock>>, + status: RwLock, } -/// Sets the current state of the service. -/// -/// In most cases, the service state is updated automatically and does not need to be set directly. -pub fn set_state(state: State) { - let mut writer = STATUS.write().unwrap(); - writer.dwCurrentState = match state { - State::ContinuePending => SERVICE_CONTINUE_PENDING, - State::Paused => SERVICE_PAUSED, - State::PausePending => SERVICE_PAUSE_PENDING, - State::Running => SERVICE_RUNNING, - State::StartPending => SERVICE_START_PENDING, - State::Stopped => SERVICE_STOPPED, - State::StopPending => SERVICE_STOP_PENDING, - }; - - // Makes a copy to avoid holding a lock while calling `SetServiceStatus`. - let status: SERVICE_STATUS = *writer; - drop(writer); - - unsafe { - SetServiceStatus(HANDLE.get().unwrap().0, &status); +impl Default for Service<'_> { + fn default() -> Self { + Self::new() } } -/// A service builder, providing control over what commands the service supports before the service begins to run. -#[derive(Default)] -pub struct Service(u32); +unsafe impl Send for Service<'_> {} +unsafe impl Sync for Service<'_> {} -impl Service { +impl<'a> Service<'a> { /// Creates a new `Service` object. /// /// By default, the service does not accept any service commands other than start. pub fn new() -> Self { - Self(0) + Self { + accept: 0, + fallback: None, + handle: RwLock::new(std::ptr::null_mut()), + callback: RwLock::new(None), + status: RwLock::new(SERVICE_STATUS { + dwServiceType: SERVICE_WIN32_OWN_PROCESS, + dwCurrentState: SERVICE_STOPPED, + dwControlsAccepted: 0, + dwWin32ExitCode: 0, + dwServiceSpecificExitCode: 0, + dwCheckPoint: 0, + dwWaitHint: 0, + }), + } } /// The service accepts stop and shutdown commands. pub fn can_stop(&mut self) -> &mut Self { - self.0 |= SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN; + self.accept |= SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN; self } /// The service accepts pause and resume commands. pub fn can_pause(&mut self) -> &mut Self { - self.0 |= SERVICE_ACCEPT_PAUSE_CONTINUE; + self.accept |= SERVICE_ACCEPT_PAUSE_CONTINUE; + self + } + + /// The service accepts other specified commands. + pub fn can_accept(&mut self, accept: u32) -> &mut Self { + self.accept |= accept; + self + } + + /// Runs the fallback closure if the service is not started by the Service Control Manager. + pub fn can_fallback(&mut self, f: F) -> &mut Self { + self.fallback = Some(Box::new(f)); self } @@ -115,11 +144,22 @@ impl Service { /// This method will block for the life of the service. It will never return and immediately /// terminate the current process after indicating to the service control manager that the /// service has stopped. - pub fn run(&self, callback: F) -> ! { - STATUS.write().unwrap().dwControlsAccepted = self.0; - CALLBACK - .set(Callback(Box::into_raw(Box::new(callback)) as *mut _)) - .unwrap(); + pub fn run( + &mut self, + callback: F, + ) -> Result<(), &'static str> { + debug_assert!(self.status.read().unwrap().dwCurrentState == SERVICE_STOPPED); + self.status.write().unwrap().dwControlsAccepted = self.accept; + + { + let mut write = self.callback.write().unwrap(); + + if write.is_some() { + panic!("`run` was already called") + } + + *write = Some(Box::new(callback)); + } let table = [ SERVICE_TABLE_ENTRYW { @@ -129,87 +169,129 @@ impl Service { SERVICE_TABLE_ENTRYW::default(), ]; - if unsafe { StartServiceCtrlDispatcherW(table.as_ptr()) } == 0 { - println!( - r#"Use service control manager to start service. - -Install: - > sc create ServiceName binPath= "{}" + SERVICE_CONTEXT.write().unwrap().0 = self as *const _ as _; + + let fallback = unsafe { StartServiceCtrlDispatcherW(table.as_ptr()) == 0 }; + + if fallback { + if let Some(fallback) = self.fallback.take() { + self.set_state(State::StartPending); + self.command(Command::Start); + self.set_state(State::Running); + fallback(self); + self.set_state(State::StopPending); + self.command(Command::Stop); + } else { + return Err("Use service control manager to start service"); + } + } -Start: - > sc start ServiceName + Ok(()) + } -Query status: - > sc query ServiceName + /// Sets the current state of the service. + /// + /// In most cases, the service state is updated automatically and does not need to be set directly. + pub fn set_state(&self, state: State) { + let mut writer = self.status.write().unwrap(); + writer.dwCurrentState = match state { + State::ContinuePending => SERVICE_CONTINUE_PENDING, + State::Paused => SERVICE_PAUSED, + State::PausePending => SERVICE_PAUSE_PENDING, + State::Running => SERVICE_RUNNING, + State::StartPending => SERVICE_START_PENDING, + State::Stopped => SERVICE_STOPPED, + State::StopPending => SERVICE_STOP_PENDING, + }; -Stop: - > sc stop ServiceName + // Makes a copy to avoid holding a lock while calling `SetServiceStatus`. + let status: SERVICE_STATUS = *writer; + drop(writer); -Delete (uninstall): - > sc delete ServiceName -"#, - std::env::current_exe().unwrap().display() - ); + unsafe { + SetServiceStatus(self.handle(), &status); } + } - std::process::exit(0); + /// The raw handle representing the service. + pub fn handle(&self) -> *mut core::ffi::c_void { + *self.handle.read().unwrap() } -} -#[derive(Debug)] -struct Handle(SERVICE_STATUS_HANDLE); -static HANDLE: OnceLock = OnceLock::new(); -unsafe impl Send for Handle {} -unsafe impl Sync for Handle {} + /// The current state the service. + pub fn state(&self) -> State { + let reader = self.status.read().unwrap(); -#[derive(Debug)] -struct Callback(*mut (dyn FnMut(Command) + Send + Sync)); -static CALLBACK: OnceLock = OnceLock::new(); -unsafe impl Send for Callback {} -unsafe impl Sync for Callback {} - -static STATUS: RwLock = RwLock::new(SERVICE_STATUS { - dwServiceType: SERVICE_WIN32_OWN_PROCESS, - dwCurrentState: SERVICE_STOPPED, - dwControlsAccepted: 0, - dwWin32ExitCode: 0, - dwServiceSpecificExitCode: 0, - dwCheckPoint: 0, - dwWaitHint: 0, -}); - -fn callback(command: Command) { - unsafe { - (*CALLBACK.get().unwrap().0)(command); + match reader.dwCurrentState { + SERVICE_CONTINUE_PENDING => State::ContinuePending, + SERVICE_PAUSED => State::Paused, + SERVICE_PAUSE_PENDING => State::PausePending, + SERVICE_RUNNING => State::Running, + SERVICE_START_PENDING => State::StartPending, + SERVICE_STOPPED => State::Stopped, + SERVICE_STOP_PENDING => State::StopPending, + _ => panic!("unexpected state"), + } + } + + /// Sends the command to the service callback. + pub fn command(&self, command: Command) { + let mut write = self.callback.write().unwrap(); + (write.as_deref_mut().unwrap())(self, command); + } + + /// Low-level dispatcher to send control commands directly to the service. + pub fn handler(&self, control: u32, event_type: u32, event_data: *const c_void) -> u32 { + handler( + control, + event_type, + event_data as *mut _, + self as *const _ as _, + ) } } extern "system" fn service_main(_len: u32, _args: *mut PWSTR) { - let handle = unsafe { RegisterServiceCtrlHandlerW(std::ptr::null(), Some(handler)) }; + let service: &Service = unsafe { &*(SERVICE_CONTEXT.read().unwrap().0 as *const Service) }; - HANDLE.set(Handle(handle)).unwrap(); - set_state(State::StartPending); - callback(Command::Start); - set_state(State::Running); + *service.handle.write().unwrap() = unsafe { + RegisterServiceCtrlHandlerExW(std::ptr::null(), Some(handler), service as *const _ as _) + }; + + service.set_state(State::StartPending); + service.command(Command::Start); + service.set_state(State::Running); } -extern "system" fn handler(control: u32) { +extern "system" fn handler(control: u32, ty: u32, data: *mut c_void, context: *mut c_void) -> u32 { + let service = unsafe { &*(context as *const Service) }; + match control { - SERVICE_CONTROL_CONTINUE if state() == State::Paused => { - set_state(State::ContinuePending); - callback(Command::Resume); - set_state(State::Running); + SERVICE_CONTROL_CONTINUE if service.state() == State::Paused => { + service.set_state(State::ContinuePending); + service.command(Command::Resume); + service.set_state(State::Running); } - SERVICE_CONTROL_PAUSE if state() == State::Running => { - set_state(State::PausePending); - callback(Command::Pause); - set_state(State::Paused); + SERVICE_CONTROL_PAUSE if service.state() == State::Running => { + service.set_state(State::PausePending); + service.command(Command::Pause); + service.set_state(State::Paused); } SERVICE_CONTROL_SHUTDOWN | SERVICE_CONTROL_STOP => { - set_state(State::StopPending); - callback(Command::Stop); - set_state(State::Stopped); + service.set_state(State::StopPending); + service.command(Command::Stop); + service.set_state(State::Stopped); } - _ => {} + _ => service.command(Command::Extended(ExtendedCommand { control, ty, data })), } + + NO_ERROR } + +// Unlike `RegisterServiceCtrlHandlerExW`, `StartServiceCtrlDispatcherW` has no user-defined "context" parameter. +// This lock allows us to safely retrieve the service instance from the service callback. +#[derive(Debug)] +struct ServiceContext(*const c_void); +static SERVICE_CONTEXT: RwLock = RwLock::new(ServiceContext(std::ptr::null())); +unsafe impl Send for ServiceContext {} +unsafe impl Sync for ServiceContext {} diff --git a/crates/samples/services/simple/src/main.rs b/crates/samples/services/simple/src/main.rs index 7b810f75b9..650f4178ea 100644 --- a/crates/samples/services/simple/src/main.rs +++ b/crates/samples/services/simple/src/main.rs @@ -4,10 +4,34 @@ fn main() { // Simple log file can be used to observe the service commands. let mut log = std::fs::File::create("D:\\service.txt").unwrap(); - windows_services::Service::new() - .can_pause() - .can_stop() - .run(|command| { - writeln!(log, "Command: {command:?}").unwrap(); - }) + let result = + windows_services::Service::new() + .can_pause() + .can_stop() + .run(|_service, command| { + writeln!(log, "Command: {command:?}").unwrap(); + }); + + if result.is_err() { + println!( + r#"Use service control manager to start service. + +Install: + > sc create ServiceName binPath= "{}" + +Start: + > sc start ServiceName + +Query status: + > sc query ServiceName + +Stop: + > sc stop ServiceName + +Delete (uninstall): + > sc delete ServiceName +"#, + std::env::current_exe().unwrap().display() + ); + } } diff --git a/crates/samples/services/thread/src/main.rs b/crates/samples/services/thread/src/main.rs index 199f91a5ac..bd9cc5dd06 100644 --- a/crates/samples/services/thread/src/main.rs +++ b/crates/samples/services/thread/src/main.rs @@ -5,17 +5,27 @@ fn main() { let pool = Pool::new(); pool.set_thread_limits(1, 1); - Service::new().can_pause().can_stop().run(|command| { - log(&format!("Command: {command:?}\n")); + Service::new() + .can_pause() + .can_stop() + .can_fallback(|_| { + println!("Press Enter to stop service."); + use std::io::Read; + _ = std::io::stdin().read(&mut [0]); + }) + .run(|service, command| { + log(&format!("Command: {command:?}\n")); - match command { - Command::Start | Command::Resume => pool.submit(service_thread), - Command::Pause | Command::Stop => pool.join(), - } - }) + match command { + Command::Start | Command::Resume => pool.submit(|| service_thread(service)), + Command::Pause | Command::Stop => pool.join(), + _ => {} + } + }) + .unwrap(); } -fn service_thread() { +fn service_thread(service: &Service) { for i in 0..10 { log(&format!("Thread:{}... iteration:{i}\n", thread_id())); @@ -23,13 +33,13 @@ fn service_thread() { sleep(1000); // Services can use the `state` function to query the current service state. - if matches!(state(), State::StopPending | State::PausePending) { + if matches!(service.state(), State::StopPending | State::PausePending) { return; } } - // Services can use the `set_state` function to update thye service state. - set_state(State::Stopped); + // Services can use the `set_state` function to update the service state. + service.set_state(State::Stopped); } // Simple log function can be used to observe service behavior. diff --git a/crates/samples/services/time/Cargo.toml b/crates/samples/services/time/Cargo.toml new file mode 100644 index 0000000000..996da6af18 --- /dev/null +++ b/crates/samples/services/time/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "sample_service_time" +version = "0.0.0" +edition = "2021" +publish = false + +[dependencies] +windows-services = { workspace = true } +windows-link = { workspace = true } + +[build-dependencies] +windows-bindgen = { workspace = true } diff --git a/crates/samples/services/time/build.rs b/crates/samples/services/time/build.rs new file mode 100644 index 0000000000..576db4cdc5 --- /dev/null +++ b/crates/samples/services/time/build.rs @@ -0,0 +1,19 @@ +fn main() { + windows_bindgen::bindgen([ + "--out", + "src/bindings.rs", + "--flat", + "--sys", + "--no-deps", + "--filter", + "SERVICE_ACCEPT_TIMECHANGE", + "SERVICE_CONTROL_TIMECHANGE", + "SERVICE_TIMECHANGE_INFO", + "FileTimeToSystemTime", + "FileTimeToLocalFileTime", + "--derive", + "SYSTEMTIME=Debug", + "SERVICE_TIMECHANGE_INFO=Debug", + ]) + .unwrap(); +} diff --git a/crates/samples/services/time/src/bindings.rs b/crates/samples/services/time/src/bindings.rs new file mode 100644 index 0000000000..66767735f9 --- /dev/null +++ b/crates/samples/services/time/src/bindings.rs @@ -0,0 +1,39 @@ +// Bindings generated by `windows-bindgen` 0.62.1 + +#![allow( + non_snake_case, + non_upper_case_globals, + non_camel_case_types, + dead_code, + clippy::all +)] + +windows_link::link!("kernel32.dll" "system" fn FileTimeToLocalFileTime(lpfiletime : *const FILETIME, lplocalfiletime : *mut FILETIME) -> BOOL); +windows_link::link!("kernel32.dll" "system" fn FileTimeToSystemTime(lpfiletime : *const FILETIME, lpsystemtime : *mut SYSTEMTIME) -> BOOL); +pub type BOOL = i32; +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct FILETIME { + pub dwLowDateTime: u32, + pub dwHighDateTime: u32, +} +pub const SERVICE_ACCEPT_TIMECHANGE: u32 = 512u32; +pub const SERVICE_CONTROL_TIMECHANGE: u32 = 16u32; +#[repr(C)] +#[derive(Clone, Copy, Debug, Default)] +pub struct SERVICE_TIMECHANGE_INFO { + pub liNewTime: i64, + pub liOldTime: i64, +} +#[repr(C)] +#[derive(Clone, Copy, Debug, Default)] +pub struct SYSTEMTIME { + pub wYear: u16, + pub wMonth: u16, + pub wDayOfWeek: u16, + pub wDay: u16, + pub wHour: u16, + pub wMinute: u16, + pub wSecond: u16, + pub wMilliseconds: u16, +} diff --git a/crates/samples/services/time/src/main.rs b/crates/samples/services/time/src/main.rs new file mode 100644 index 0000000000..fafd6b134c --- /dev/null +++ b/crates/samples/services/time/src/main.rs @@ -0,0 +1,43 @@ +mod bindings; +use bindings::*; + +use std::io::Write; +use windows_services::*; + +fn main() { + let mut log = std::fs::File::create("D:\\service.txt").unwrap(); + + Service::new() + .can_stop() + .can_accept(SERVICE_ACCEPT_TIMECHANGE) + .run(|_service, command| { + writeln!(log, "Command: {command:?}").unwrap(); + + if let Command::Extended(command) = command { + if command.control == SERVICE_CONTROL_TIMECHANGE { + unsafe { + let data = &*(command.data as *const SERVICE_TIMECHANGE_INFO); + + writeln!(log, "{data:#?}").unwrap(); + + let old = convert(data.liOldTime); + let new = convert(data.liNewTime); + + writeln!(log, "{old:#?}\n{new:#?}").unwrap(); + } + } + } + }) + .unwrap(); +} + +fn convert(time: i64) -> SYSTEMTIME { + unsafe { + let mut local = FILETIME::default(); + FileTimeToLocalFileTime(&time as *const i64 as _, &mut local); + + let mut time = SYSTEMTIME::default(); + FileTimeToSystemTime(&local, &mut time); + time + } +} diff --git a/crates/tests/libs/services/Cargo.toml b/crates/tests/libs/services/Cargo.toml new file mode 100644 index 0000000000..33abd7a517 --- /dev/null +++ b/crates/tests/libs/services/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "test_services" +version = "0.0.0" +edition = "2021" +publish = false + +[lib] +doc = false +doctest = false + +[dependencies] +windows-services = { workspace = true } +windows-sys = { workspace = true, features = ["Win32_System_Services"] } diff --git a/crates/tests/libs/services/src/lib.rs b/crates/tests/libs/services/src/lib.rs new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/crates/tests/libs/services/src/lib.rs @@ -0,0 +1 @@ + diff --git a/crates/tests/libs/services/tests/service.rs b/crates/tests/libs/services/tests/service.rs new file mode 100644 index 0000000000..3effb41995 --- /dev/null +++ b/crates/tests/libs/services/tests/service.rs @@ -0,0 +1,106 @@ +use windows_services::*; +use windows_sys::Win32::System::Services::*; + +#[test] +fn start_stop() { + let mut log = vec![]; + + Service::new() + .can_fallback(|_| {}) + .run(|service, command| { + assert!(service.handle().is_null()); + log.push(command); + }) + .unwrap(); + + assert_eq!(log, [Command::Start, Command::Stop,]); +} + +#[test] +fn pause_resume() { + let mut log = vec![]; + + Service::new() + .can_fallback(|service| { + service.handler(SERVICE_CONTROL_PAUSE, 0, std::ptr::null_mut()); + service.handler(SERVICE_CONTROL_CONTINUE, 0, std::ptr::null_mut()); + }) + .run(|_, command| { + log.push(command); + }) + .unwrap(); + + assert_eq!( + log, + [ + Command::Start, + Command::Pause, + Command::Resume, + Command::Stop, + ] + ); +} + +#[test] +fn extended() { + #[repr(C)] + #[derive(Copy, Clone, PartialEq, Debug)] + struct Data { + a: u8, + b: u8, + c: u8, + } + static DATA: Data = Data { a: 7, b: 8, c: 9 }; + const DATA_PTR: *const std::ffi::c_void = &DATA as *const Data as *const std::ffi::c_void; + + let mut log = vec![]; + + Service::new() + .can_fallback(|service| { + service.handler(123, 456, DATA_PTR); + }) + .run(|_, command| { + log.push(command); + + if let Command::Extended(command) = command { + unsafe { + let data = *(command.data as *const Data); + assert_eq!(data, Data { a: 7, b: 8, c: 9 }); + } + } + }) + .unwrap(); + + assert_eq!( + log, + [ + Command::Start, + Command::Extended(ExtendedCommand { + control: 123, + ty: 456, + data: DATA_PTR + }), + Command::Stop, + ] + ); +} + +#[test] +#[should_panic(expected = "Use service control manager to start service")] +fn panic_fallback() { + Service::new().run(|_, _| {}).unwrap(); +} + +#[test] +fn recover_fallback() { + let error = Service::new().run(|_, _| {}).unwrap_err(); + assert_eq!(error, "Use service control manager to start service"); +} + +#[test] +#[should_panic(expected = "`run` was already called")] +fn rerun() { + let mut service = Service::new(); + service.run(|_, _| {}).unwrap_err(); + _ = service.run(|_, _| {}); +} diff --git a/crates/tools/bindings/src/services.txt b/crates/tools/bindings/src/services.txt index 8bf88fc922..be880ed972 100644 --- a/crates/tools/bindings/src/services.txt +++ b/crates/tools/bindings/src/services.txt @@ -3,14 +3,15 @@ --no-comment --sys --no-deps +--no-allow --filter - RegisterServiceCtrlHandlerW - SetServiceStatus - StartServiceCtrlDispatcherW + NO_ERROR + RegisterServiceCtrlHandlerExW SERVICE_ACCEPT_PAUSE_CONTINUE SERVICE_ACCEPT_SHUTDOWN SERVICE_ACCEPT_STOP + SERVICE_CONTINUE_PENDING SERVICE_CONTROL_CONTINUE SERVICE_CONTROL_PAUSE SERVICE_CONTROL_SHUTDOWN @@ -22,4 +23,5 @@ SERVICE_STOP_PENDING SERVICE_STOPPED SERVICE_WIN32_OWN_PROCESS - SERVICE_CONTINUE_PENDING + SetServiceStatus + StartServiceCtrlDispatcherW