service_manager/
winsw.rs

1use crate::utils::wrap_output;
2use crate::ServiceStatus;
3
4use super::{
5    ServiceInstallCtx, ServiceLevel, ServiceManager, ServiceStartCtx, ServiceStopCtx,
6    ServiceUninstallCtx,
7};
8use std::ffi::OsString;
9use std::fs::File;
10use std::io::{self, BufWriter, Cursor, Write};
11use std::path::{Path, PathBuf};
12use std::process::{Command, Output, Stdio};
13use xml::common::XmlVersion;
14use xml::reader::EventReader;
15use xml::writer::{EmitterConfig, EventWriter, XmlEvent};
16
17static WINSW_EXE: &str = "winsw.exe";
18
19///
20/// Service configuration
21///
22
23#[derive(Clone, Debug, PartialEq, Eq)]
24pub struct WinSwConfig {
25    pub install: WinSwInstallConfig,
26    pub options: WinSwOptionsConfig,
27    pub service_definition_dir_path: PathBuf,
28}
29
30impl Default for WinSwConfig {
31    fn default() -> Self {
32        WinSwConfig {
33            install: WinSwInstallConfig::default(),
34            options: WinSwOptionsConfig::default(),
35            service_definition_dir_path: PathBuf::from("C:\\ProgramData\\service-manager"),
36        }
37    }
38}
39
40#[derive(Clone, Debug, Default, PartialEq, Eq)]
41pub struct WinSwInstallConfig {
42    pub failure_action: WinSwOnFailureAction,
43    pub reset_failure_time: Option<String>,
44    pub security_descriptor: Option<String>,
45}
46
47#[derive(Clone, Debug, Default, PartialEq, Eq)]
48pub struct WinSwOptionsConfig {
49    pub priority: Option<WinSwPriority>,
50    pub stop_timeout: Option<String>,
51    pub stop_executable: Option<PathBuf>,
52    pub stop_args: Option<Vec<OsString>>,
53    pub start_mode: Option<WinSwStartType>,
54    pub delayed_autostart: Option<bool>,
55    pub dependent_services: Option<Vec<String>>,
56    pub interactive: Option<bool>,
57    pub beep_on_shutdown: Option<bool>,
58}
59
60#[derive(Clone, Debug, Default, PartialEq, Eq)]
61pub enum WinSwOnFailureAction {
62    Restart(Option<String>),
63    Reboot,
64    #[default]
65    None,
66}
67
68#[derive(Copy, Clone, Debug, PartialEq, Eq)]
69pub enum WinSwStartType {
70    // The service automatically starts along with the OS, before user login.
71    Automatic,
72    /// The service is a device driver loaded by the boot loader.
73    Boot,
74    /// The service must be started manually.
75    Manual,
76    /// The service is a device driver started during kernel initialization.
77    System,
78}
79
80#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
81pub enum WinSwPriority {
82    #[default]
83    Normal,
84    Idle,
85    High,
86    RealTime,
87    BelowNormal,
88    AboveNormal,
89}
90
91///
92/// Service manager implementation
93///
94
95/// Implementation of [`ServiceManager`] for [Window Service](https://en.wikipedia.org/wiki/Windows_service)
96/// leveraging [`winsw.exe`](https://github.com/winsw/winsw)
97#[derive(Clone, Debug, Default, PartialEq, Eq)]
98pub struct WinSwServiceManager {
99    pub config: WinSwConfig,
100}
101
102impl WinSwServiceManager {
103    pub fn system() -> Self {
104        let config = WinSwConfig {
105            install: WinSwInstallConfig::default(),
106            options: WinSwOptionsConfig::default(),
107            service_definition_dir_path: PathBuf::from("C:\\ProgramData\\service-manager"),
108        };
109        Self { config }
110    }
111
112    pub fn with_config(self, config: WinSwConfig) -> Self {
113        Self { config }
114    }
115
116    pub fn write_service_configuration(
117        path: &PathBuf,
118        ctx: &ServiceInstallCtx,
119        config: &WinSwConfig,
120    ) -> io::Result<()> {
121        let mut file = File::create(path).unwrap();
122        if let Some(contents) = &ctx.contents {
123            if Self::is_valid_xml(contents) {
124                file.write_all(contents.as_bytes())?;
125                return Ok(());
126            }
127            return Err(io::Error::new(
128                io::ErrorKind::InvalidData,
129                "The contents override was not a valid XML document",
130            ));
131        }
132
133        let file = BufWriter::new(file);
134        let mut writer = EmitterConfig::new()
135            .perform_indent(true)
136            .create_writer(file);
137        writer
138            .write(XmlEvent::StartDocument {
139                version: XmlVersion::Version10,
140                encoding: Some("UTF-8"),
141                standalone: None,
142            })
143            .map_err(|e| {
144                io::Error::new(
145                    io::ErrorKind::Other,
146                    format!("Writing service config failed: {}", e),
147                )
148            })?;
149
150        // <service>
151        writer
152            .write(XmlEvent::start_element("service"))
153            .map_err(|e| {
154                io::Error::new(
155                    io::ErrorKind::Other,
156                    format!("Writing service config failed: {}", e),
157                )
158            })?;
159
160        // Mandatory values
161        Self::write_element(&mut writer, "id", &ctx.label.to_qualified_name())?;
162        Self::write_element(&mut writer, "name", &ctx.label.to_qualified_name())?;
163        Self::write_element(&mut writer, "executable", &ctx.program.to_string_lossy())?;
164        Self::write_element(
165            &mut writer,
166            "description",
167            &format!("Service for {}", ctx.label.to_qualified_name()),
168        )?;
169        let args = ctx
170            .args
171            .clone()
172            .into_iter()
173            .map(|s| s.into_string().unwrap_or_default())
174            .collect::<Vec<String>>()
175            .join(" ");
176        Self::write_element(&mut writer, "arguments", &args)?;
177
178        if let Some(working_directory) = &ctx.working_directory {
179            Self::write_element(
180                &mut writer,
181                "workingdirectory",
182                &working_directory.to_string_lossy(),
183            )?;
184        }
185        if let Some(env_vars) = &ctx.environment {
186            for var in env_vars.iter() {
187                Self::write_element_with_attributes(
188                    &mut writer,
189                    "env",
190                    &[("name", &var.0), ("value", &var.1)],
191                    None,
192                )?;
193            }
194        }
195
196        // Optional install elements
197        let (action, delay) = if ctx.disable_restart_on_failure {
198            ("none", None)
199        } else {
200            match &config.install.failure_action {
201                WinSwOnFailureAction::Restart(delay) => ("restart", delay.as_deref()),
202                WinSwOnFailureAction::Reboot => ("reboot", None),
203                WinSwOnFailureAction::None => ("none", None),
204            }
205        };
206
207        let attributes = delay.map_or_else(
208            || vec![("action", action)],
209            |d| vec![("action", action), ("delay", d)],
210        );
211        Self::write_element_with_attributes(&mut writer, "onfailure", &attributes, None)?;
212
213        if let Some(reset_time) = &config.install.reset_failure_time {
214            Self::write_element(&mut writer, "resetfailure", reset_time)?;
215        }
216        if let Some(security_descriptor) = &config.install.security_descriptor {
217            Self::write_element(&mut writer, "securityDescriptor", security_descriptor)?;
218        }
219
220        // Other optional elements
221        if let Some(priority) = &config.options.priority {
222            Self::write_element(&mut writer, "priority", &format!("{:?}", priority))?;
223        }
224        if let Some(stop_timeout) = &config.options.stop_timeout {
225            Self::write_element(&mut writer, "stoptimeout", stop_timeout)?;
226        }
227        if let Some(stop_executable) = &config.options.stop_executable {
228            Self::write_element(
229                &mut writer,
230                "stopexecutable",
231                &stop_executable.to_string_lossy(),
232            )?;
233        }
234        if let Some(stop_args) = &config.options.stop_args {
235            let stop_args = stop_args
236                .iter()
237                .map(|s| s.to_string_lossy().into_owned())
238                .collect::<Vec<String>>()
239                .join(" ");
240            Self::write_element(&mut writer, "stoparguments", &stop_args)?;
241        }
242
243        if let Some(start_mode) = &config.options.start_mode {
244            Self::write_element(&mut writer, "startmode", &format!("{:?}", start_mode))?;
245        } else if ctx.autostart {
246            Self::write_element(&mut writer, "startmode", "Automatic")?;
247        } else {
248            Self::write_element(&mut writer, "startmode", "Manual")?;
249        }
250
251        if let Some(delayed_autostart) = config.options.delayed_autostart {
252            Self::write_element(
253                &mut writer,
254                "delayedAutoStart",
255                &delayed_autostart.to_string(),
256            )?;
257        }
258        if let Some(dependent_services) = &config.options.dependent_services {
259            for service in dependent_services {
260                Self::write_element(&mut writer, "depend", service)?;
261            }
262        }
263        if let Some(interactive) = config.options.interactive {
264            Self::write_element(&mut writer, "interactive", &interactive.to_string())?;
265        }
266        if let Some(beep_on_shutdown) = config.options.beep_on_shutdown {
267            Self::write_element(&mut writer, "beeponshutdown", &beep_on_shutdown.to_string())?;
268        }
269
270        // </service>
271        writer.write(XmlEvent::end_element()).map_err(|e| {
272            io::Error::new(
273                io::ErrorKind::Other,
274                format!("Writing service config failed: {}", e),
275            )
276        })?;
277
278        Ok(())
279    }
280
281    fn write_element<W: Write>(
282        writer: &mut EventWriter<W>,
283        name: &str,
284        value: &str,
285    ) -> io::Result<()> {
286        writer.write(XmlEvent::start_element(name)).map_err(|e| {
287            io::Error::new(
288                io::ErrorKind::Other,
289                format!("Failed to write element '{}': {}", name, e),
290            )
291        })?;
292        writer.write(XmlEvent::characters(value)).map_err(|e| {
293            io::Error::new(
294                io::ErrorKind::Other,
295                format!("Failed to write value for element '{}': {}", name, e),
296            )
297        })?;
298        writer.write(XmlEvent::end_element()).map_err(|e| {
299            io::Error::new(
300                io::ErrorKind::Other,
301                format!("Failed to end element '{}': {}", name, e),
302            )
303        })?;
304        Ok(())
305    }
306
307    fn write_element_with_attributes<W: Write>(
308        writer: &mut EventWriter<W>,
309        name: &str,
310        attributes: &[(&str, &str)],
311        value: Option<&str>,
312    ) -> io::Result<()> {
313        let mut start_element = XmlEvent::start_element(name);
314        for &(attr_name, attr_value) in attributes {
315            start_element = start_element.attr(attr_name, attr_value);
316        }
317        writer.write(start_element).map_err(|e| {
318            io::Error::new(
319                io::ErrorKind::Other,
320                format!("Failed to write value for element '{}': {}", name, e),
321            )
322        })?;
323
324        if let Some(val) = value {
325            writer.write(XmlEvent::characters(val)).map_err(|e| {
326                io::Error::new(
327                    io::ErrorKind::Other,
328                    format!("Failed to write value for element '{}': {}", name, e),
329                )
330            })?;
331        }
332
333        writer.write(XmlEvent::end_element()).map_err(|e| {
334            io::Error::new(
335                io::ErrorKind::Other,
336                format!("Failed to end element '{}': {}", name, e),
337            )
338        })?;
339
340        Ok(())
341    }
342
343    fn is_valid_xml(xml_string: &str) -> bool {
344        let cursor = Cursor::new(xml_string);
345        let parser = EventReader::new(cursor);
346        for e in parser {
347            if e.is_err() {
348                return false;
349            }
350        }
351        true
352    }
353}
354
355impl ServiceManager for WinSwServiceManager {
356    fn available(&self) -> io::Result<bool> {
357        match which::which(WINSW_EXE) {
358            Ok(_) => Ok(true),
359            Err(which::Error::CannotFindBinaryPath) => match std::env::var("WINSW_PATH") {
360                Ok(val) => {
361                    let path = PathBuf::from(val);
362                    Ok(path.exists())
363                }
364                Err(_) => Ok(false),
365            },
366            Err(x) => Err(io::Error::new(io::ErrorKind::Other, x)),
367        }
368    }
369
370    fn install(&self, ctx: ServiceInstallCtx) -> io::Result<()> {
371        let service_name = ctx.label.to_qualified_name();
372        let service_instance_path = self
373            .config
374            .service_definition_dir_path
375            .join(service_name.clone());
376        std::fs::create_dir_all(&service_instance_path)?;
377
378        let service_config_path = service_instance_path.join(format!("{service_name}.xml"));
379        Self::write_service_configuration(&service_config_path, &ctx, &self.config)?;
380
381        wrap_output(winsw_exe("install", &service_name, &service_instance_path)?)?;
382        Ok(())
383    }
384
385    fn uninstall(&self, ctx: ServiceUninstallCtx) -> io::Result<()> {
386        let service_name = ctx.label.to_qualified_name();
387        let service_instance_path = self
388            .config
389            .service_definition_dir_path
390            .join(service_name.clone());
391        wrap_output(winsw_exe(
392            "uninstall",
393            &service_name,
394            &service_instance_path,
395        )?)?;
396
397        // The service directory is populated with the service definition, and other log files that
398        // get generated by WinSW. It can be problematic if a service is later created with the
399        // same name. Things are easier to manage if the directory is deleted.
400        std::fs::remove_dir_all(service_instance_path)?;
401
402        Ok(())
403    }
404
405    fn start(&self, ctx: ServiceStartCtx) -> io::Result<()> {
406        let service_name = ctx.label.to_qualified_name();
407        let service_instance_path = self
408            .config
409            .service_definition_dir_path
410            .join(service_name.clone());
411        wrap_output(winsw_exe("start", &service_name, &service_instance_path)?)?;
412        Ok(())
413    }
414
415    fn stop(&self, ctx: ServiceStopCtx) -> io::Result<()> {
416        let service_name = ctx.label.to_qualified_name();
417        let service_instance_path = self
418            .config
419            .service_definition_dir_path
420            .join(service_name.clone());
421        wrap_output(winsw_exe("stop", &service_name, &service_instance_path)?)?;
422        Ok(())
423    }
424
425    fn level(&self) -> ServiceLevel {
426        ServiceLevel::System
427    }
428
429    fn set_level(&mut self, level: ServiceLevel) -> io::Result<()> {
430        match level {
431            ServiceLevel::System => Ok(()),
432            ServiceLevel::User => Err(io::Error::new(
433                io::ErrorKind::Unsupported,
434                "Windows does not support user-level services",
435            )),
436        }
437    }
438
439    fn status(&self, ctx: crate::ServiceStatusCtx) -> io::Result<ServiceStatus> {
440        let service_name = ctx.label.to_qualified_name();
441        let service_instance_path = self
442            .config
443            .service_definition_dir_path
444            .join(service_name.clone());
445        if !service_instance_path.exists() {
446            return Ok(ServiceStatus::NotInstalled);
447        }
448        let output = winsw_exe("status", &service_name, &service_instance_path)?;
449        if !output.status.success() {
450            let stderr = String::from_utf8_lossy(&output.stderr);
451            // It seems the error message is thrown by WinSW v2.x because only WinSW.[xml|yml] is supported
452            if stderr.contains("System.IO.FileNotFoundException: Unable to locate WinSW.[xml|yml] file within executable directory") {
453                return Ok(ServiceStatus::NotInstalled);
454            }
455            let stdout = String::from_utf8_lossy(&output.stdout);
456            // Unsuccessful output status seems to be incorrect sometimes
457            if stdout.contains("Active") {
458                return Ok(ServiceStatus::Running);
459            }
460            return Err(io::Error::new(
461                io::ErrorKind::Other,
462                format!("Failed to get service status: {}", stderr),
463            ));
464        }
465        let stdout = String::from_utf8_lossy(&output.stdout);
466        if stdout.contains("NonExistent") {
467            Ok(ServiceStatus::NotInstalled)
468        } else if stdout.contains("running") {
469            Ok(ServiceStatus::Running)
470        } else {
471            Ok(ServiceStatus::Stopped(None))
472        }
473    }
474}
475
476fn winsw_exe(cmd: &str, service_name: &str, working_dir_path: &Path) -> io::Result<Output> {
477    let winsw_path = match std::env::var("WINSW_PATH") {
478        Ok(val) => {
479            let path = PathBuf::from(val);
480            if path.exists() {
481                path
482            } else {
483                PathBuf::from(WINSW_EXE)
484            }
485        }
486        Err(_) => PathBuf::from(WINSW_EXE),
487    };
488
489    let mut command = Command::new(winsw_path);
490    command
491        .stdin(Stdio::null())
492        .stdout(Stdio::piped())
493        .stderr(Stdio::piped());
494    command.current_dir(working_dir_path);
495    command.arg(cmd).arg(format!("{}.xml", service_name));
496
497    command.output()
498}
499
500#[cfg(test)]
501mod tests {
502    use super::*;
503    use assert_fs::prelude::*;
504    use indoc::indoc;
505    use std::ffi::OsString;
506    use std::io::Cursor;
507    use xml::reader::{EventReader, XmlEvent};
508
509    fn get_element_value(xml: &str, element_name: &str) -> String {
510        let cursor = Cursor::new(xml);
511        let parser = EventReader::new(cursor);
512        let mut inside_target_element = false;
513
514        for e in parser {
515            match e {
516                Ok(XmlEvent::StartElement { name, .. }) if name.local_name == element_name => {
517                    inside_target_element = true;
518                }
519                Ok(XmlEvent::Characters(text)) if inside_target_element => {
520                    return text;
521                }
522                Ok(XmlEvent::EndElement { name }) if name.local_name == element_name => {
523                    inside_target_element = false;
524                }
525                Err(e) => panic!("Error while parsing XML: {}", e),
526                _ => {}
527            }
528        }
529
530        panic!("Element {} not found", element_name);
531    }
532
533    fn get_element_attribute_value(xml: &str, element_name: &str, attribute_name: &str) -> String {
534        let cursor = Cursor::new(xml);
535        let parser = EventReader::new(cursor);
536
537        for e in parser {
538            match e {
539                Ok(XmlEvent::StartElement {
540                    name, attributes, ..
541                }) if name.local_name == element_name => {
542                    for attr in attributes {
543                        if attr.name.local_name == attribute_name {
544                            return attr.value;
545                        }
546                    }
547                }
548                Err(e) => panic!("Error while parsing XML: {}", e),
549                _ => {}
550            }
551        }
552
553        panic!("Attribute {} not found", attribute_name);
554    }
555
556    fn get_element_values(xml: &str, element_name: &str) -> Vec<String> {
557        let cursor = Cursor::new(xml);
558        let parser = EventReader::new(cursor);
559        let mut values = Vec::new();
560        let mut inside_target_element = false;
561
562        for e in parser {
563            match e {
564                Ok(XmlEvent::StartElement { name, .. }) if name.local_name == element_name => {
565                    inside_target_element = true;
566                }
567                Ok(XmlEvent::Characters(text)) if inside_target_element => {
568                    values.push(text);
569                }
570                Ok(XmlEvent::EndElement { name }) if name.local_name == element_name => {
571                    inside_target_element = false;
572                }
573                Err(e) => panic!("Error while parsing XML: {}", e),
574                _ => {}
575            }
576        }
577
578        values
579    }
580
581    fn get_environment_variables(xml: &str) -> Vec<(String, String)> {
582        let cursor = Cursor::new(xml);
583        let parser = EventReader::new(cursor);
584        let mut env_vars = Vec::new();
585
586        for e in parser.into_iter().flatten() {
587            if let XmlEvent::StartElement {
588                name, attributes, ..
589            } = e
590            {
591                if name.local_name == "env" {
592                    let mut name_value_pair = (String::new(), String::new());
593                    for attr in attributes {
594                        match attr.name.local_name.as_str() {
595                            "name" => name_value_pair.0 = attr.value,
596                            "value" => name_value_pair.1 = attr.value,
597                            _ => {}
598                        }
599                    }
600                    if !name_value_pair.0.is_empty() && !name_value_pair.1.is_empty() {
601                        env_vars.push(name_value_pair);
602                    }
603                }
604            }
605        }
606        env_vars
607    }
608
609    #[test]
610    fn test_service_configuration_with_mandatory_elements() {
611        let temp_dir = assert_fs::TempDir::new().unwrap();
612        let service_config_file = temp_dir.child("service_config.xml");
613
614        let ctx = ServiceInstallCtx {
615            label: "org.example.my_service".parse().unwrap(),
616            program: PathBuf::from("C:\\Program Files\\org.example\\my_service.exe"),
617            args: vec![
618                OsString::from("--arg"),
619                OsString::from("value"),
620                OsString::from("--another-arg"),
621            ],
622            contents: None,
623            username: None,
624            working_directory: None,
625            environment: None,
626            autostart: true,
627            disable_restart_on_failure: false,
628        };
629
630        WinSwServiceManager::write_service_configuration(
631            &service_config_file.to_path_buf(),
632            &ctx,
633            &WinSwConfig::default(),
634        )
635        .unwrap();
636
637        let xml = std::fs::read_to_string(service_config_file.path()).unwrap();
638
639        service_config_file.assert(predicates::path::is_file());
640        assert_eq!("org.example.my_service", get_element_value(&xml, "id"));
641        assert_eq!("org.example.my_service", get_element_value(&xml, "name"));
642        assert_eq!(
643            "C:\\Program Files\\org.example\\my_service.exe",
644            get_element_value(&xml, "executable")
645        );
646        assert_eq!(
647            "Service for org.example.my_service",
648            get_element_value(&xml, "description")
649        );
650        assert_eq!(
651            "--arg value --another-arg",
652            get_element_value(&xml, "arguments")
653        );
654        assert_eq!("Automatic", get_element_value(&xml, "startmode"));
655    }
656
657    #[test]
658    fn test_service_configuration_with_autostart_false() {
659        let temp_dir = assert_fs::TempDir::new().unwrap();
660        let service_config_file = temp_dir.child("service_config.xml");
661
662        let ctx = ServiceInstallCtx {
663            label: "org.example.my_service".parse().unwrap(),
664            program: PathBuf::from("C:\\Program Files\\org.example\\my_service.exe"),
665            args: vec![
666                OsString::from("--arg"),
667                OsString::from("value"),
668                OsString::from("--another-arg"),
669            ],
670            contents: None,
671            username: None,
672            working_directory: None,
673            environment: None,
674            autostart: false,
675            disable_restart_on_failure: false,
676        };
677
678        WinSwServiceManager::write_service_configuration(
679            &service_config_file.to_path_buf(),
680            &ctx,
681            &WinSwConfig::default(),
682        )
683        .unwrap();
684
685        let xml = std::fs::read_to_string(service_config_file.path()).unwrap();
686
687        service_config_file.assert(predicates::path::is_file());
688        assert_eq!("org.example.my_service", get_element_value(&xml, "id"));
689        assert_eq!("org.example.my_service", get_element_value(&xml, "name"));
690        assert_eq!(
691            "C:\\Program Files\\org.example\\my_service.exe",
692            get_element_value(&xml, "executable")
693        );
694        assert_eq!(
695            "Service for org.example.my_service",
696            get_element_value(&xml, "description")
697        );
698        assert_eq!(
699            "--arg value --another-arg",
700            get_element_value(&xml, "arguments")
701        );
702        assert_eq!("Manual", get_element_value(&xml, "startmode"));
703    }
704
705    #[test]
706    fn test_service_configuration_with_special_start_type_should_override_autostart() {
707        let temp_dir = assert_fs::TempDir::new().unwrap();
708        let service_config_file = temp_dir.child("service_config.xml");
709
710        let ctx = ServiceInstallCtx {
711            label: "org.example.my_service".parse().unwrap(),
712            program: PathBuf::from("C:\\Program Files\\org.example\\my_service.exe"),
713            args: vec![
714                OsString::from("--arg"),
715                OsString::from("value"),
716                OsString::from("--another-arg"),
717            ],
718            contents: None,
719            username: None,
720            working_directory: None,
721            environment: None,
722            autostart: false,
723            disable_restart_on_failure: false,
724        };
725
726        let mut config = WinSwConfig::default();
727        config.options.start_mode = Some(WinSwStartType::Boot);
728        WinSwServiceManager::write_service_configuration(
729            &service_config_file.to_path_buf(),
730            &ctx,
731            &config,
732        )
733        .unwrap();
734
735        let xml = std::fs::read_to_string(service_config_file.path()).unwrap();
736
737        service_config_file.assert(predicates::path::is_file());
738        assert_eq!("org.example.my_service", get_element_value(&xml, "id"));
739        assert_eq!("org.example.my_service", get_element_value(&xml, "name"));
740        assert_eq!(
741            "C:\\Program Files\\org.example\\my_service.exe",
742            get_element_value(&xml, "executable")
743        );
744        assert_eq!(
745            "Service for org.example.my_service",
746            get_element_value(&xml, "description")
747        );
748        assert_eq!(
749            "--arg value --another-arg",
750            get_element_value(&xml, "arguments")
751        );
752        assert_eq!("Boot", get_element_value(&xml, "startmode"));
753    }
754
755    #[test]
756    fn test_service_configuration_with_full_options() {
757        let temp_dir = assert_fs::TempDir::new().unwrap();
758        let service_config_file = temp_dir.child("service_config.xml");
759
760        let ctx = ServiceInstallCtx {
761            label: "org.example.my_service".parse().unwrap(),
762            program: PathBuf::from("C:\\Program Files\\org.example\\my_service.exe"),
763            args: vec![
764                OsString::from("--arg"),
765                OsString::from("value"),
766                OsString::from("--another-arg"),
767            ],
768            contents: None,
769            username: None,
770            working_directory: Some(PathBuf::from("C:\\Program Files\\org.example")),
771            environment: Some(vec![
772                ("ENV1".to_string(), "val1".to_string()),
773                ("ENV2".to_string(), "val2".to_string()),
774            ]),
775            autostart: true,
776            disable_restart_on_failure: false,
777        };
778
779        let config = WinSwConfig {
780            install: WinSwInstallConfig {
781                failure_action: WinSwOnFailureAction::Restart(Some("10 sec".to_string())),
782                reset_failure_time: Some("1 hour".to_string()),
783                security_descriptor: Some(
784                    "O:AOG:DAD:(A;;RPWPCCDCLCSWRCWDWOGA;;;S-1-0-0)".to_string(),
785                ),
786            },
787            options: WinSwOptionsConfig {
788                priority: Some(WinSwPriority::High),
789                stop_timeout: Some("15 sec".to_string()),
790                stop_executable: Some(PathBuf::from("C:\\Temp\\stop.exe")),
791                stop_args: Some(vec![
792                    OsString::from("--stop-arg1"),
793                    OsString::from("arg1val"),
794                    OsString::from("--stop-arg2-flag"),
795                ]),
796                start_mode: Some(WinSwStartType::Manual),
797                delayed_autostart: Some(true),
798                dependent_services: Some(vec!["service1".to_string(), "service2".to_string()]),
799                interactive: Some(true),
800                beep_on_shutdown: Some(true),
801            },
802            service_definition_dir_path: PathBuf::from("C:\\Temp\\service-definitions"),
803        };
804
805        WinSwServiceManager::write_service_configuration(
806            &service_config_file.to_path_buf(),
807            &ctx,
808            &config,
809        )
810        .unwrap();
811
812        let xml = std::fs::read_to_string(service_config_file.path()).unwrap();
813        println!("{xml}");
814
815        service_config_file.assert(predicates::path::is_file());
816        assert_eq!("org.example.my_service", get_element_value(&xml, "id"));
817        assert_eq!("org.example.my_service", get_element_value(&xml, "name"));
818        assert_eq!(
819            "C:\\Program Files\\org.example\\my_service.exe",
820            get_element_value(&xml, "executable")
821        );
822        assert_eq!(
823            "Service for org.example.my_service",
824            get_element_value(&xml, "description")
825        );
826        assert_eq!(
827            "--arg value --another-arg",
828            get_element_value(&xml, "arguments")
829        );
830        assert_eq!(
831            "C:\\Program Files\\org.example",
832            get_element_value(&xml, "workingdirectory")
833        );
834
835        let attributes = get_environment_variables(&xml);
836        assert_eq!(attributes[0].0, "ENV1");
837        assert_eq!(attributes[0].1, "val1");
838        assert_eq!(attributes[1].0, "ENV2");
839        assert_eq!(attributes[1].1, "val2");
840
841        // Install options
842        assert_eq!(
843            "restart",
844            get_element_attribute_value(&xml, "onfailure", "action")
845        );
846        assert_eq!(
847            "10 sec",
848            get_element_attribute_value(&xml, "onfailure", "delay")
849        );
850        assert_eq!("1 hour", get_element_value(&xml, "resetfailure"));
851        assert_eq!(
852            "O:AOG:DAD:(A;;RPWPCCDCLCSWRCWDWOGA;;;S-1-0-0)",
853            get_element_value(&xml, "securityDescriptor")
854        );
855
856        // Other options
857        assert_eq!("High", get_element_value(&xml, "priority"));
858        assert_eq!("15 sec", get_element_value(&xml, "stoptimeout"));
859        assert_eq!(
860            "C:\\Temp\\stop.exe",
861            get_element_value(&xml, "stopexecutable")
862        );
863        assert_eq!(
864            "--stop-arg1 arg1val --stop-arg2-flag",
865            get_element_value(&xml, "stoparguments")
866        );
867        assert_eq!("Manual", get_element_value(&xml, "startmode"));
868        assert_eq!("true", get_element_value(&xml, "delayedAutoStart"));
869
870        let dependent_services = get_element_values(&xml, "depend");
871        assert_eq!("service1", dependent_services[0]);
872        assert_eq!("service2", dependent_services[1]);
873
874        assert_eq!("true", get_element_value(&xml, "interactive"));
875        assert_eq!("true", get_element_value(&xml, "beeponshutdown"));
876    }
877
878    #[test]
879    fn test_service_configuration_with_contents() {
880        let temp_dir = assert_fs::TempDir::new().unwrap();
881        let service_config_file = temp_dir.child("service_config.xml");
882
883        let contents = indoc! {r#"
884            <service>
885                <id>jenkins</id>
886                <name>Jenkins</name>
887                <description>This service runs Jenkins continuous integration system.</description>
888                <executable>java</executable>
889                <arguments>-Xrs -Xmx256m -jar "%BASE%\jenkins.war" --httpPort=8080</arguments>
890                <startmode>Automatic</startmode>
891            </service>
892        "#};
893        let ctx = ServiceInstallCtx {
894            label: "org.example.my_service".parse().unwrap(),
895            program: PathBuf::from("C:\\Program Files\\org.example\\my_service.exe"),
896            args: vec![
897                OsString::from("--arg"),
898                OsString::from("value"),
899                OsString::from("--another-arg"),
900            ],
901            contents: Some(contents.to_string()),
902            username: None,
903            working_directory: None,
904            environment: None,
905            autostart: true,
906            disable_restart_on_failure: false,
907        };
908
909        WinSwServiceManager::write_service_configuration(
910            &service_config_file.to_path_buf(),
911            &ctx,
912            &WinSwConfig::default(),
913        )
914        .unwrap();
915
916        let xml = std::fs::read_to_string(service_config_file.path()).unwrap();
917
918        service_config_file.assert(predicates::path::is_file());
919        assert_eq!("jenkins", get_element_value(&xml, "id"));
920        assert_eq!("Jenkins", get_element_value(&xml, "name"));
921        assert_eq!("java", get_element_value(&xml, "executable"));
922        assert_eq!(
923            "This service runs Jenkins continuous integration system.",
924            get_element_value(&xml, "description")
925        );
926        assert_eq!(
927            "-Xrs -Xmx256m -jar \"%BASE%\\jenkins.war\" --httpPort=8080",
928            get_element_value(&xml, "arguments")
929        );
930    }
931
932    #[test]
933    fn test_service_configuration_with_invalid_contents() {
934        let temp_dir = assert_fs::TempDir::new().unwrap();
935        let service_config_file = temp_dir.child("service_config.xml");
936
937        let ctx = ServiceInstallCtx {
938            label: "org.example.my_service".parse().unwrap(),
939            program: PathBuf::from("C:\\Program Files\\org.example\\my_service.exe"),
940            args: vec![
941                OsString::from("--arg"),
942                OsString::from("value"),
943                OsString::from("--another-arg"),
944            ],
945            contents: Some("this is not an XML document".to_string()),
946            username: None,
947            working_directory: None,
948            environment: None,
949            autostart: true,
950            disable_restart_on_failure: false,
951        };
952
953        let result = WinSwServiceManager::write_service_configuration(
954            &service_config_file.to_path_buf(),
955            &ctx,
956            &WinSwConfig::default(),
957        );
958
959        match result {
960            Ok(()) => panic!("This test should result in a failure"),
961            Err(e) => assert_eq!(
962                "The contents override was not a valid XML document",
963                e.to_string()
964            ),
965        }
966    }
967}