diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2018-04-18 14:56:58 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2018-04-18 16:12:14 +0200 |
| commit | 3735d5c2a7b7040b2b9be53f200252ad538dce7e (patch) | |
| tree | 29a8f44fa94e70a9bf8ae03b5d8249ace5f817a0 | |
| parent | d28e918c183509666254c2e4cafceb86f43c048d (diff) | |
| download | mullvadvpn-3735d5c2a7b7040b2b9be53f200252ad538dce7e.tar.xz mullvadvpn-3735d5c2a7b7040b2b9be53f200252ad538dce7e.zip | |
Add example of simple service
| -rw-r--r-- | Cargo.lock | 12 | ||||
| -rw-r--r-- | windows-service/Cargo.toml | 5 | ||||
| -rw-r--r-- | windows-service/examples/simple_service.rs | 451 |
3 files changed, 468 insertions, 0 deletions
diff --git a/Cargo.lock b/Cargo.lock index 527ef532a3..b096d0c5b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1189,6 +1189,15 @@ dependencies = [ ] [[package]] +name = "simplelog" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "chrono 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] name = "slab" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1587,6 +1596,8 @@ version = "0.1.0" dependencies = [ "bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "simplelog 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "widestring 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1749,6 +1760,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum shell-escape 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "dd5cc96481d54583947bfe88bf30c23d53f883c6cd0145368b69989d97b84ef8" "checksum shell32-sys 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9ee04b46101f57121c9da2b151988283b6beb79b34f5bb29a58ee48cb695122c" "checksum simple-signal 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c1eb01a0c2d12db9e52684e73038eac812494e5937571ae2631f5cf53dc56687" +"checksum simplelog 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ce595117de34b75e057b41e99079e43e9fcc4e5ec9c7ba5f2fea55321f0c624e" "checksum slab 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d807fd58c4181bbabed77cb3b891ba9748241a552bcc5be698faaebefc54f46e" "checksum slab 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "17b4fcaed89ab08ef143da37bc52adbcc04d4a69014f4c1208d6b51f0c47bc23" "checksum slab 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fdeff4cd9ecff59ec7e3744cbca73dfe5ac35c2aedb2cfba8a1c715a18912e9d" diff --git a/windows-service/Cargo.toml b/windows-service/Cargo.toml index 536dbca559..0e73261ce8 100644 --- a/windows-service/Cargo.toml +++ b/windows-service/Cargo.toml @@ -8,3 +8,8 @@ bitflags = "1.0.1" error-chain = "0.11" winapi = { version = "0.3", features = ["std", "winsvc", "winerror"] } widestring = "0.3.0" + +[dev-dependencies] +error-chain = "0.11" +log = "0.4" +simplelog = { version = "0.5", default-features = false } diff --git a/windows-service/examples/simple_service.rs b/windows-service/examples/simple_service.rs new file mode 100644 index 0000000000..d175e69564 --- /dev/null +++ b/windows-service/examples/simple_service.rs @@ -0,0 +1,451 @@ +// Simple service example. +// +// All commands mentioned below shall be executed in Command Prompt with Administrator privileges. +// +// Service self-installation: `simple_service.exe --install-service` +// Service self-removal: `simple_service.exe --remove-service` +// +// Start the service: `net start simpleservice` +// Pause the service: `net pause simpleservice` +// Resume the service: `net continue simpleservice` +// Stop the service: `net stop simpleservice` +// +// Simple service outputs all logs in C:\Windows\Temp\simple-service.log. +// If you have GNU tools installed, you can follow the log using: +// `tail -F C:\Windows\Temp\simple-service.log` +// + +#[macro_use] +extern crate error_chain; +#[macro_use] +extern crate log; +extern crate simplelog; + +#[cfg(windows)] +#[macro_use] +extern crate windows_service; + +#[cfg(not(windows))] +fn main() { + panic!("This program is only intended to run on Windows."); +} + +#[cfg(windows)] +fn main() { + simple_service::run(); +} + +#[cfg(windows)] +mod simple_service { + use std::ffi::OsString; + use std::fs::OpenOptions; + use std::path::PathBuf; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::sync::mpsc; + use std::time::Duration; + use std::{env, io, thread, time}; + + use log::LevelFilter; + use simplelog::{CombinedLogger, Config, WriteLogger}; + + use windows_service::service::{ServiceAccess, ServiceControl, ServiceControlAccept, + ServiceErrorControl, ServiceExitCode, ServiceInfo, + ServiceStartType, ServiceState, ServiceStatus, ServiceType}; + use windows_service::service_control_handler::{self, ServiceControlHandlerResult, + ServiceStatusHandle}; + use windows_service::service_dispatcher; + use windows_service::service_manager::{ServiceManager, ServiceManagerAccess}; + use windows_service::ChainedError; + + static SERVICE_NAME: &'static str = "SimpleService"; + static SERVICE_DISPLAY_NAME: &'static str = "Simple Service"; + + error_chain! { + errors { + InstallService { + description("Failed to install the service") + } + RemoveService { + description("Failed to remove the service") + } + OpenLogFile(path: PathBuf) { + description("Unable to open log file for writing") + display("Unable to open log file for writing: {}", path.to_string_lossy()) + } + InitLogger { + description("Cannot initialize logger") + } + } + foreign_links { + SetLoggerError(::log::SetLoggerError); + } + } + + static CHECKPOINT_COUNTER: AtomicUsize = AtomicUsize::new(0); + + pub fn update_service_status( + status_handle: &ServiceStatusHandle, + next_state: ServiceState, + exit_code: ServiceExitCode, + wait_hint: Duration, + ) -> io::Result<()> { + // Automatically bump the checkpoint when updating the pending events to tell the system + // that the service is making a progress in transition from pending to final state. + // `wait_hint` should reflect the estimated time for transition to complete. + let checkpoint = match next_state { + ServiceState::ContinuePending + | ServiceState::PausePending + | ServiceState::StartPending + | ServiceState::StopPending => CHECKPOINT_COUNTER.fetch_add(1, Ordering::SeqCst) + 1, + _ => 0, + }; + let service_status = ServiceStatus { + service_type: ServiceType::OwnProcess, + current_state: next_state, + controls_accepted: accepted_controls_by_state(next_state), + exit_code: exit_code, + checkpoint: checkpoint as u32, + wait_hint: wait_hint, + }; + info!( + "Update service status: {:?}, checkpoint: {}, wait_hint: {:?}", + service_status.current_state, service_status.checkpoint, service_status.wait_hint + ); + status_handle.set_service_status(service_status) + } + + pub fn run() { + if let Some(command) = env::args().nth(1) { + match command.as_ref() { + "--install-service" => { + if let Err(e) = install_service() { + println!("{}", e.display_chain()); + } else { + println!("Installed the service."); + } + } + "--remove-service" => { + if let Err(e) = remove_service() { + println!("{}", e.display_chain()); + } else { + println!("Removed the service."); + } + } + "--run-service" => { + // Setup file logger since there is no stdout when running as a service. + if let Err(err) = init_logger() { + panic!("Unable to initialize logger: {}", err.display_chain()); + } + + // Start the service dispatcher. + // This will block current thread until the service stopped. + let result = service_dispatcher::start_dispatcher(SERVICE_NAME, service_main); + + match result { + Err(ref e) => { + error!("Failed to start service dispatcher: {}", e.display_chain()); + } + Ok(_) => { + info!("Service dispatcher exited."); + } + }; + } + _ => println!("Unsupported command: {}", command), + } + } else { + println!("Usage:"); + println!("--install-service to install the service"); + println!("--remove-service to uninstall the service"); + println!("--run-service to run the service"); + } + } + + define_windows_service!(service_main, handle_service_main); + + pub fn handle_service_main(arguments: Vec<OsString>) { + // Create a shutdown channel to release this thread when stopping the service + let (event_tx, event_rx) = mpsc::channel(); + + info!("Received arguments: {:?}", arguments); + + // Register service event handler + let event_handler = move |control_event| -> ServiceControlHandlerResult { + match control_event { + // Notifies a service to report its current status information to the service + // control manager. Always return NO_ERROR even if not implemented. + ServiceControl::Interrogate => ServiceControlHandlerResult::NoError, + + // Handle primary control events + ServiceControl::Pause + | ServiceControl::Continue + | ServiceControl::Stop + | ServiceControl::Shutdown => { + event_tx.send(control_event).unwrap(); + ServiceControlHandlerResult::NoError + } + + _ => ServiceControlHandlerResult::NotImplemented, + } + }; + + let result = service_control_handler::register_control_handler(SERVICE_NAME, event_handler); + match result { + Ok(status_handle) => { + run_service(status_handle, event_rx); + } + Err(ref e) => { + error!("Cannot register a service control handler: {}", e); + } + }; + + info!("Quit service main."); + } + + #[derive(Debug, Copy, Clone)] + enum DaemonEvent { + Continue, + Pause, + Stop, + } + + fn start_event_monitor( + service_status_handle: ServiceStatusHandle, + event_rx: mpsc::Receiver<ServiceControl>, + daemon_tx: mpsc::Sender<DaemonEvent>, + ) -> thread::JoinHandle<()> { + thread::spawn(move || { + loop { + match event_rx.recv().unwrap() { + ServiceControl::Pause => { + info!("Pausing the service."); + + update_service_status( + &service_status_handle, + ServiceState::PausePending, + ServiceExitCode::Win32(0), + Duration::from_secs(2), + ).unwrap(); + + daemon_tx.send(DaemonEvent::Pause).unwrap(); + } + + ServiceControl::Continue => { + info!("Continuing the service."); + + update_service_status( + &service_status_handle, + ServiceState::ContinuePending, + ServiceExitCode::Win32(0), + Duration::from_secs(2), + ).unwrap(); + + daemon_tx.send(DaemonEvent::Continue).unwrap(); + } + + ServiceControl::Stop => { + info!("Stopping the service."); + + update_service_status( + &service_status_handle, + ServiceState::StopPending, + ServiceExitCode::Win32(0), + Duration::from_secs(2), + ).unwrap(); + + daemon_tx.send(DaemonEvent::Stop).unwrap(); + break; // break the loop + } + + ServiceControl::Shutdown => { + info!("Exiting due to shutdown."); + + update_service_status( + &service_status_handle, + ServiceState::StopPending, + ServiceExitCode::Win32(0), + Duration::from_secs(1), + ).unwrap(); + + daemon_tx.send(DaemonEvent::Stop).unwrap(); + break; // break the loop + } + + _ => (), + }; + } + }) + } + + fn start_worker( + service_status_handle: ServiceStatusHandle, + daemon_rx: mpsc::Receiver<DaemonEvent>, + ) -> thread::JoinHandle<()> { + thread::spawn(move || { + let mut is_running = true; + let mut is_paused = false; + + // Tell Windows that the service is running now + update_service_status( + &service_status_handle, + ServiceState::Running, + ServiceExitCode::Win32(0), + Duration::default(), + ).unwrap(); + + while is_running { + // Do some work + if !is_paused { + info!("Working..."); + } + + // Poll events + match daemon_rx.recv_timeout(Duration::from_secs(1)) { + Ok(DaemonEvent::Pause) => { + is_paused = true; + + update_service_status( + &service_status_handle, + ServiceState::Paused, + ServiceExitCode::Win32(0), + Duration::default(), + ).unwrap(); + } + Ok(DaemonEvent::Continue) => { + is_paused = false; + + update_service_status( + &service_status_handle, + ServiceState::Running, + ServiceExitCode::Win32(0), + Duration::default(), + ).unwrap(); + } + Ok(DaemonEvent::Stop) | Err(mpsc::RecvTimeoutError::Disconnected) => { + is_running = false; + + update_service_status( + &service_status_handle, + ServiceState::Stopped, + ServiceExitCode::Win32(0), + Duration::default(), + ).unwrap(); + } + Err(mpsc::RecvTimeoutError::Timeout) => (), + }; + } + }) + } + + fn run_service(status_handle: ServiceStatusHandle, event_rx: mpsc::Receiver<ServiceControl>) { + let (daemon_tx, daemon_rx) = mpsc::channel(); + + // Tell Windows that the service is starting up + update_service_status( + &status_handle, + ServiceState::StartPending, + ServiceExitCode::Win32(0), + Duration::from_secs(5), + ).unwrap(); + + let event_monitor_handle = start_event_monitor(status_handle, event_rx, daemon_tx); + let worker_thread_handle = start_worker(status_handle, daemon_rx); + + // Block current thread until other threads complete execution + event_monitor_handle.join().unwrap(); + worker_thread_handle.join().unwrap(); + } + + /// Returns the list of accepted service events at each stage of the service lifecycle. + fn accepted_controls_by_state(state: ServiceState) -> ServiceControlAccept { + match state { + ServiceState::StartPending + | ServiceState::PausePending + | ServiceState::ContinuePending => ServiceControlAccept::empty(), + ServiceState::Running => { + ServiceControlAccept::STOP | ServiceControlAccept::PAUSE_CONTINUE + | ServiceControlAccept::SHUTDOWN + } + ServiceState::Paused => { + ServiceControlAccept::STOP | ServiceControlAccept::PAUSE_CONTINUE + | ServiceControlAccept::SHUTDOWN + } + ServiceState::StopPending | ServiceState::Stopped => ServiceControlAccept::empty(), + } + } + + fn install_service() -> Result<()> { + let manager_access = ServiceManagerAccess::CONNECT | ServiceManagerAccess::CREATE_SERVICE; + let service_manager = ServiceManager::local_computer(None::<&str>, manager_access) + .chain_err(|| ErrorKind::InstallService)?; + let service_info = get_service_info(); + service_manager + .create_service(service_info, ServiceAccess::empty()) + .map(|_| ()) + .chain_err(|| ErrorKind::InstallService) + } + + fn remove_service() -> Result<()> { + let manager_access = ServiceManagerAccess::CONNECT; + let service_manager = ServiceManager::local_computer(None::<&str>, manager_access) + .chain_err(|| ErrorKind::RemoveService)?; + + let service_access = + ServiceAccess::QUERY_STATUS | ServiceAccess::STOP | ServiceAccess::DELETE; + let service = service_manager + .open_service(SERVICE_NAME, service_access) + .chain_err(|| ErrorKind::RemoveService)?; + + loop { + let service_status = service + .query_status() + .chain_err(|| ErrorKind::RemoveService)?; + + match service_status.current_state { + ServiceState::StopPending => (), + ServiceState::Stopped => { + println!("Removing the service..."); + service.delete().chain_err(|| ErrorKind::RemoveService)?; + return Ok(()); // explicit return + } + _ => { + println!("Stopping the service..."); + service.stop().chain_err(|| ErrorKind::RemoveService)?; + } + } + + thread::sleep(time::Duration::from_secs(1)) + } + } + + fn get_service_info() -> ServiceInfo { + ServiceInfo { + name: OsString::from(SERVICE_NAME), + display_name: OsString::from(SERVICE_DISPLAY_NAME), + service_type: ServiceType::OwnProcess, + start_type: ServiceStartType::OnDemand, + error_control: ServiceErrorControl::Normal, + executable_path: env::current_exe().unwrap(), + launch_arguments: vec![OsString::from("--run-service")], + account_name: None, // run as System + account_password: None, + } + } + + fn init_logger() -> Result<()> { + let windows_directory = env::var_os("WINDIR").unwrap(); + let log_file_path = PathBuf::from(windows_directory) + .join("Temp") + .join("simple-service.log"); + + let log_file = OpenOptions::new() + .create(true) + .append(true) + .open(log_file_path.as_path()) + .chain_err(|| ErrorKind::OpenLogFile(log_file_path))?; + + let file_logger = WriteLogger::new(LevelFilter::Trace, Config::default(), log_file); + + CombinedLogger::init(vec![file_logger]).chain_err(|| ErrorKind::InitLogger) + } + +} |
