service_manager/
winsw.rs

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