Skip to main content

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        if let Some(failure_action) = &config.install.failure_action {
201            // Use WinSW-specific failure action configuration (single element)
202            let (action, delay) = match failure_action {
203                WinSwOnFailureAction::Restart(delay) => ("restart", delay.as_deref()),
204                WinSwOnFailureAction::Reboot => ("reboot", None),
205                WinSwOnFailureAction::None => ("none", None),
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        } else {
213            // Fall back to generic RestartPolicy
214            match ctx.restart_policy {
215                RestartPolicy::Never => {
216                    Self::write_element_with_attributes(
217                        &mut writer,
218                        "onfailure",
219                        &[("action", "none")],
220                        None,
221                    )?;
222                }
223                RestartPolicy::Always { delay_secs } => {
224                    let delay_str = delay_secs.map(|secs| format!("{} sec", secs));
225                    let attributes = delay_str.as_deref().map_or_else(
226                        || vec![("action", "restart")],
227                        |d| vec![("action", "restart"), ("delay", d)],
228                    );
229                    Self::write_element_with_attributes(
230                        &mut writer,
231                        "onfailure",
232                        &attributes,
233                        None,
234                    )?;
235                }
236                RestartPolicy::OnFailure {
237                    delay_secs,
238                    max_retries,
239                    reset_after_secs,
240                } => {
241                    let delay_str = delay_secs.map(|secs| format!("{} sec", secs));
242                    let attributes = delay_str.as_deref().map_or_else(
243                        || vec![("action", "restart")],
244                        |d| vec![("action", "restart"), ("delay", d)],
245                    );
246
247                    if let Some(n) = max_retries {
248                        // Write n restart elements followed by a "none" element to stop
249                        for _ in 0..n {
250                            Self::write_element_with_attributes(
251                                &mut writer,
252                                "onfailure",
253                                &attributes,
254                                None,
255                            )?;
256                        }
257                        Self::write_element_with_attributes(
258                            &mut writer,
259                            "onfailure",
260                            &[("action", "none")],
261                            None,
262                        )?;
263                    } else {
264                        // No retry limit: single restart element (repeats forever)
265                        Self::write_element_with_attributes(
266                            &mut writer,
267                            "onfailure",
268                            &attributes,
269                            None,
270                        )?;
271                    }
272
273                    // Map reset_after_secs to <resetfailure> unless WinSW-specific
274                    // reset_failure_time is already set
275                    if config.install.reset_failure_time.is_none() {
276                        if let Some(secs) = reset_after_secs {
277                            Self::write_element(
278                                &mut writer,
279                                "resetfailure",
280                                &format!("{} sec", secs),
281                            )?;
282                        }
283                    }
284                }
285                RestartPolicy::OnSuccess { delay_secs } => {
286                    log::warn!(
287                        "WinSW does not support restart on success; falling back to 'always' for service '{}'",
288                        ctx.label
289                    );
290                    let delay_str = delay_secs.map(|secs| format!("{} sec", secs));
291                    let attributes = delay_str.as_deref().map_or_else(
292                        || vec![("action", "restart")],
293                        |d| vec![("action", "restart"), ("delay", d)],
294                    );
295                    Self::write_element_with_attributes(
296                        &mut writer,
297                        "onfailure",
298                        &attributes,
299                        None,
300                    )?;
301                }
302            }
303        }
304
305        if let Some(reset_time) = &config.install.reset_failure_time {
306            Self::write_element(&mut writer, "resetfailure", reset_time)?;
307        }
308        if let Some(security_descriptor) = &config.install.security_descriptor {
309            Self::write_element(&mut writer, "securityDescriptor", security_descriptor)?;
310        }
311
312        // Other optional elements
313        if let Some(priority) = &config.options.priority {
314            Self::write_element(&mut writer, "priority", &format!("{:?}", priority))?;
315        }
316        if let Some(stop_timeout) = &config.options.stop_timeout {
317            Self::write_element(&mut writer, "stoptimeout", stop_timeout)?;
318        }
319        if let Some(stop_executable) = &config.options.stop_executable {
320            Self::write_element(
321                &mut writer,
322                "stopexecutable",
323                &stop_executable.to_string_lossy(),
324            )?;
325        }
326        if let Some(stop_args) = &config.options.stop_args {
327            let stop_args = stop_args
328                .iter()
329                .map(|s| s.to_string_lossy().into_owned())
330                .collect::<Vec<String>>()
331                .join(" ");
332            Self::write_element(&mut writer, "stoparguments", &stop_args)?;
333        }
334
335        if let Some(start_mode) = &config.options.start_mode {
336            Self::write_element(&mut writer, "startmode", &format!("{:?}", start_mode))?;
337        } else if ctx.autostart {
338            Self::write_element(&mut writer, "startmode", "Automatic")?;
339        } else {
340            Self::write_element(&mut writer, "startmode", "Manual")?;
341        }
342
343        if let Some(delayed_autostart) = config.options.delayed_autostart {
344            Self::write_element(
345                &mut writer,
346                "delayedAutoStart",
347                &delayed_autostart.to_string(),
348            )?;
349        }
350        if let Some(dependent_services) = &config.options.dependent_services {
351            for service in dependent_services {
352                Self::write_element(&mut writer, "depend", service)?;
353            }
354        }
355        if let Some(interactive) = config.options.interactive {
356            Self::write_element(&mut writer, "interactive", &interactive.to_string())?;
357        }
358        if let Some(beep_on_shutdown) = config.options.beep_on_shutdown {
359            Self::write_element(&mut writer, "beeponshutdown", &beep_on_shutdown.to_string())?;
360        }
361
362        // </service>
363        writer.write(XmlEvent::end_element()).map_err(|e| {
364            io::Error::new(
365                io::ErrorKind::Other,
366                format!("Writing service config failed: {}", e),
367            )
368        })?;
369
370        Ok(())
371    }
372
373    fn write_element<W: Write>(
374        writer: &mut EventWriter<W>,
375        name: &str,
376        value: &str,
377    ) -> io::Result<()> {
378        writer.write(XmlEvent::start_element(name)).map_err(|e| {
379            io::Error::new(
380                io::ErrorKind::Other,
381                format!("Failed to write element '{}': {}", name, e),
382            )
383        })?;
384        writer.write(XmlEvent::characters(value)).map_err(|e| {
385            io::Error::new(
386                io::ErrorKind::Other,
387                format!("Failed to write value for element '{}': {}", name, e),
388            )
389        })?;
390        writer.write(XmlEvent::end_element()).map_err(|e| {
391            io::Error::new(
392                io::ErrorKind::Other,
393                format!("Failed to end element '{}': {}", name, e),
394            )
395        })?;
396        Ok(())
397    }
398
399    fn write_element_with_attributes<W: Write>(
400        writer: &mut EventWriter<W>,
401        name: &str,
402        attributes: &[(&str, &str)],
403        value: Option<&str>,
404    ) -> io::Result<()> {
405        let mut start_element = XmlEvent::start_element(name);
406        for &(attr_name, attr_value) in attributes {
407            start_element = start_element.attr(attr_name, attr_value);
408        }
409        writer.write(start_element).map_err(|e| {
410            io::Error::new(
411                io::ErrorKind::Other,
412                format!("Failed to write value for element '{}': {}", name, e),
413            )
414        })?;
415
416        if let Some(val) = value {
417            writer.write(XmlEvent::characters(val)).map_err(|e| {
418                io::Error::new(
419                    io::ErrorKind::Other,
420                    format!("Failed to write value for element '{}': {}", name, e),
421                )
422            })?;
423        }
424
425        writer.write(XmlEvent::end_element()).map_err(|e| {
426            io::Error::new(
427                io::ErrorKind::Other,
428                format!("Failed to end element '{}': {}", name, e),
429            )
430        })?;
431
432        Ok(())
433    }
434
435    fn is_valid_xml(xml_string: &str) -> bool {
436        let cursor = Cursor::new(xml_string);
437        let parser = EventReader::new(cursor);
438        for e in parser {
439            if e.is_err() {
440                return false;
441            }
442        }
443        true
444    }
445}
446
447impl ServiceManager for WinSwServiceManager {
448    fn available(&self) -> io::Result<bool> {
449        match which::which(WINSW_EXE) {
450            Ok(_) => Ok(true),
451            Err(which::Error::CannotFindBinaryPath) => match std::env::var("WINSW_PATH") {
452                Ok(val) => {
453                    let path = PathBuf::from(val);
454                    Ok(path.exists())
455                }
456                Err(_) => Ok(false),
457            },
458            Err(x) => Err(io::Error::new(io::ErrorKind::Other, x)),
459        }
460    }
461
462    fn install(&self, ctx: ServiceInstallCtx) -> io::Result<()> {
463        let service_name = ctx.label.to_qualified_name();
464        let service_instance_path = self
465            .config
466            .service_definition_dir_path
467            .join(service_name.clone());
468        std::fs::create_dir_all(&service_instance_path)?;
469
470        let service_config_path = service_instance_path.join(format!("{service_name}.xml"));
471        Self::write_service_configuration(&service_config_path, &ctx, &self.config)?;
472
473        wrap_output(winsw_exe("install", &service_name, &service_instance_path)?)?;
474        Ok(())
475    }
476
477    fn uninstall(&self, ctx: ServiceUninstallCtx) -> io::Result<()> {
478        let service_name = ctx.label.to_qualified_name();
479        let service_instance_path = self
480            .config
481            .service_definition_dir_path
482            .join(service_name.clone());
483        wrap_output(winsw_exe(
484            "uninstall",
485            &service_name,
486            &service_instance_path,
487        )?)?;
488
489        // The service directory is populated with the service definition, and other log files that
490        // get generated by WinSW. It can be problematic if a service is later created with the
491        // same name. Things are easier to manage if the directory is deleted.
492        std::fs::remove_dir_all(service_instance_path)?;
493
494        Ok(())
495    }
496
497    fn start(&self, ctx: ServiceStartCtx) -> io::Result<()> {
498        let service_name = ctx.label.to_qualified_name();
499        let service_instance_path = self
500            .config
501            .service_definition_dir_path
502            .join(service_name.clone());
503        wrap_output(winsw_exe("start", &service_name, &service_instance_path)?)?;
504        Ok(())
505    }
506
507    fn stop(&self, ctx: ServiceStopCtx) -> io::Result<()> {
508        let service_name = ctx.label.to_qualified_name();
509        let service_instance_path = self
510            .config
511            .service_definition_dir_path
512            .join(service_name.clone());
513        wrap_output(winsw_exe("stop", &service_name, &service_instance_path)?)?;
514        Ok(())
515    }
516
517    fn level(&self) -> ServiceLevel {
518        ServiceLevel::System
519    }
520
521    fn set_level(&mut self, level: ServiceLevel) -> io::Result<()> {
522        match level {
523            ServiceLevel::System => Ok(()),
524            ServiceLevel::User => Err(io::Error::new(
525                io::ErrorKind::Unsupported,
526                "Windows does not support user-level services",
527            )),
528        }
529    }
530
531    fn status(&self, ctx: crate::ServiceStatusCtx) -> io::Result<ServiceStatus> {
532        let service_name = ctx.label.to_qualified_name();
533        let service_instance_path = self
534            .config
535            .service_definition_dir_path
536            .join(service_name.clone());
537        if !service_instance_path.exists() {
538            return Ok(ServiceStatus::NotInstalled);
539        }
540        let output = winsw_exe("status", &service_name, &service_instance_path)?;
541        if !output.status.success() {
542            let stderr = String::from_utf8_lossy(&output.stderr);
543            // It seems the error message is thrown by WinSW v2.x because only WinSW.[xml|yml] is supported
544            if stderr.contains("System.IO.FileNotFoundException: Unable to locate WinSW.[xml|yml] file within executable directory") {
545                return Ok(ServiceStatus::NotInstalled);
546            }
547            let stdout = String::from_utf8_lossy(&output.stdout);
548            // Unsuccessful output status seems to be incorrect sometimes
549            if stdout.contains("Active") {
550                return Ok(ServiceStatus::Running);
551            }
552            return Err(io::Error::new(
553                io::ErrorKind::Other,
554                format!("Failed to get service status: {}", stderr),
555            ));
556        }
557        let stdout = String::from_utf8_lossy(&output.stdout);
558        if stdout.contains("NonExistent") {
559            Ok(ServiceStatus::NotInstalled)
560        } else if stdout.contains("running") {
561            Ok(ServiceStatus::Running)
562        } else {
563            Ok(ServiceStatus::Stopped(None))
564        }
565    }
566}
567
568fn winsw_exe(cmd: &str, service_name: &str, working_dir_path: &Path) -> io::Result<Output> {
569    let winsw_path = match std::env::var("WINSW_PATH") {
570        Ok(val) => {
571            let path = PathBuf::from(val);
572            if path.exists() {
573                path
574            } else {
575                PathBuf::from(WINSW_EXE)
576            }
577        }
578        Err(_) => PathBuf::from(WINSW_EXE),
579    };
580
581    let mut command = Command::new(winsw_path);
582    command
583        .stdin(Stdio::null())
584        .stdout(Stdio::piped())
585        .stderr(Stdio::piped());
586    command.current_dir(working_dir_path);
587    command.arg(cmd).arg(format!("{}.xml", service_name));
588
589    command.output()
590}
591
592#[cfg(test)]
593mod tests {
594    use super::*;
595    use assert_fs::prelude::*;
596    use indoc::indoc;
597    use std::ffi::OsString;
598    use std::io::Cursor;
599    use xml::reader::{EventReader, XmlEvent};
600
601    fn get_element_value(xml: &str, element_name: &str) -> String {
602        let cursor = Cursor::new(xml);
603        let parser = EventReader::new(cursor);
604        let mut inside_target_element = false;
605
606        for e in parser {
607            match e {
608                Ok(XmlEvent::StartElement { name, .. }) if name.local_name == element_name => {
609                    inside_target_element = true;
610                }
611                Ok(XmlEvent::Characters(text)) if inside_target_element => {
612                    return text;
613                }
614                Ok(XmlEvent::EndElement { name }) if name.local_name == element_name => {
615                    inside_target_element = false;
616                }
617                Err(e) => panic!("Error while parsing XML: {}", e),
618                _ => {}
619            }
620        }
621
622        panic!("Element {} not found", element_name);
623    }
624
625    fn get_element_attribute_value(xml: &str, element_name: &str, attribute_name: &str) -> String {
626        let cursor = Cursor::new(xml);
627        let parser = EventReader::new(cursor);
628
629        for e in parser {
630            match e {
631                Ok(XmlEvent::StartElement {
632                    name, attributes, ..
633                }) if name.local_name == element_name => {
634                    for attr in attributes {
635                        if attr.name.local_name == attribute_name {
636                            return attr.value;
637                        }
638                    }
639                }
640                Err(e) => panic!("Error while parsing XML: {}", e),
641                _ => {}
642            }
643        }
644
645        panic!("Attribute {} not found", attribute_name);
646    }
647
648    fn get_element_values(xml: &str, element_name: &str) -> Vec<String> {
649        let cursor = Cursor::new(xml);
650        let parser = EventReader::new(cursor);
651        let mut values = Vec::new();
652        let mut inside_target_element = false;
653
654        for e in parser {
655            match e {
656                Ok(XmlEvent::StartElement { name, .. }) if name.local_name == element_name => {
657                    inside_target_element = true;
658                }
659                Ok(XmlEvent::Characters(text)) if inside_target_element => {
660                    values.push(text);
661                }
662                Ok(XmlEvent::EndElement { name }) if name.local_name == element_name => {
663                    inside_target_element = false;
664                }
665                Err(e) => panic!("Error while parsing XML: {}", e),
666                _ => {}
667            }
668        }
669
670        values
671    }
672
673    fn get_environment_variables(xml: &str) -> Vec<(String, String)> {
674        let cursor = Cursor::new(xml);
675        let parser = EventReader::new(cursor);
676        let mut env_vars = Vec::new();
677
678        for e in parser.into_iter().flatten() {
679            if let XmlEvent::StartElement {
680                name, attributes, ..
681            } = e
682            {
683                if name.local_name == "env" {
684                    let mut name_value_pair = (String::new(), String::new());
685                    for attr in attributes {
686                        match attr.name.local_name.as_str() {
687                            "name" => name_value_pair.0 = attr.value,
688                            "value" => name_value_pair.1 = attr.value,
689                            _ => {}
690                        }
691                    }
692                    if !name_value_pair.0.is_empty() && !name_value_pair.1.is_empty() {
693                        env_vars.push(name_value_pair);
694                    }
695                }
696            }
697        }
698        env_vars
699    }
700
701    /// Returns all occurrences of the named element as a vec of attribute maps.
702    fn get_all_elements_attributes(
703        xml: &str,
704        element_name: &str,
705    ) -> Vec<std::collections::HashMap<String, String>> {
706        let cursor = Cursor::new(xml);
707        let parser = EventReader::new(cursor);
708        let mut results = Vec::new();
709
710        for e in parser {
711            match e {
712                Ok(XmlEvent::StartElement {
713                    name, attributes, ..
714                }) if name.local_name == element_name => {
715                    let mut map = std::collections::HashMap::new();
716                    for attr in attributes {
717                        map.insert(attr.name.local_name, attr.value);
718                    }
719                    results.push(map);
720                }
721                Err(e) => panic!("Error while parsing XML: {}", e),
722                _ => {}
723            }
724        }
725
726        results
727    }
728
729    fn element_exists(xml: &str, element_name: &str) -> bool {
730        let cursor = Cursor::new(xml);
731        let parser = EventReader::new(cursor);
732
733        for e in parser {
734            match e {
735                Ok(XmlEvent::StartElement { name, .. }) if name.local_name == element_name => {
736                    return true;
737                }
738                Err(e) => panic!("Error while parsing XML: {}", e),
739                _ => {}
740            }
741        }
742
743        false
744    }
745
746    #[test]
747    fn test_service_configuration_with_mandatory_elements() {
748        let temp_dir = assert_fs::TempDir::new().unwrap();
749        let service_config_file = temp_dir.child("service_config.xml");
750
751        let ctx = ServiceInstallCtx {
752            label: "org.example.my_service".parse().unwrap(),
753            program: PathBuf::from("C:\\Program Files\\org.example\\my_service.exe"),
754            args: vec![
755                OsString::from("--arg"),
756                OsString::from("value"),
757                OsString::from("--another-arg"),
758            ],
759            contents: None,
760            username: None,
761            working_directory: None,
762            environment: None,
763            autostart: true,
764            restart_policy: RestartPolicy::default(),
765        };
766
767        WinSwServiceManager::write_service_configuration(
768            &service_config_file.to_path_buf(),
769            &ctx,
770            &WinSwConfig::default(),
771        )
772        .unwrap();
773
774        let xml = std::fs::read_to_string(service_config_file.path()).unwrap();
775
776        service_config_file.assert(predicates::path::is_file());
777        assert_eq!("org.example.my_service", get_element_value(&xml, "id"));
778        assert_eq!("org.example.my_service", get_element_value(&xml, "name"));
779        assert_eq!(
780            "C:\\Program Files\\org.example\\my_service.exe",
781            get_element_value(&xml, "executable")
782        );
783        assert_eq!(
784            "Service for org.example.my_service",
785            get_element_value(&xml, "description")
786        );
787        assert_eq!(
788            "--arg value --another-arg",
789            get_element_value(&xml, "arguments")
790        );
791        assert_eq!("Automatic", get_element_value(&xml, "startmode"));
792    }
793
794    #[test]
795    fn test_service_configuration_with_autostart_false() {
796        let temp_dir = assert_fs::TempDir::new().unwrap();
797        let service_config_file = temp_dir.child("service_config.xml");
798
799        let ctx = ServiceInstallCtx {
800            label: "org.example.my_service".parse().unwrap(),
801            program: PathBuf::from("C:\\Program Files\\org.example\\my_service.exe"),
802            args: vec![
803                OsString::from("--arg"),
804                OsString::from("value"),
805                OsString::from("--another-arg"),
806            ],
807            contents: None,
808            username: None,
809            working_directory: None,
810            environment: None,
811            autostart: false,
812            restart_policy: RestartPolicy::default(),
813        };
814
815        WinSwServiceManager::write_service_configuration(
816            &service_config_file.to_path_buf(),
817            &ctx,
818            &WinSwConfig::default(),
819        )
820        .unwrap();
821
822        let xml = std::fs::read_to_string(service_config_file.path()).unwrap();
823
824        service_config_file.assert(predicates::path::is_file());
825        assert_eq!("org.example.my_service", get_element_value(&xml, "id"));
826        assert_eq!("org.example.my_service", get_element_value(&xml, "name"));
827        assert_eq!(
828            "C:\\Program Files\\org.example\\my_service.exe",
829            get_element_value(&xml, "executable")
830        );
831        assert_eq!(
832            "Service for org.example.my_service",
833            get_element_value(&xml, "description")
834        );
835        assert_eq!(
836            "--arg value --another-arg",
837            get_element_value(&xml, "arguments")
838        );
839        assert_eq!("Manual", get_element_value(&xml, "startmode"));
840    }
841
842    #[test]
843    fn test_service_configuration_with_special_start_type_should_override_autostart() {
844        let temp_dir = assert_fs::TempDir::new().unwrap();
845        let service_config_file = temp_dir.child("service_config.xml");
846
847        let ctx = ServiceInstallCtx {
848            label: "org.example.my_service".parse().unwrap(),
849            program: PathBuf::from("C:\\Program Files\\org.example\\my_service.exe"),
850            args: vec![
851                OsString::from("--arg"),
852                OsString::from("value"),
853                OsString::from("--another-arg"),
854            ],
855            contents: None,
856            username: None,
857            working_directory: None,
858            environment: None,
859            autostart: false,
860            restart_policy: RestartPolicy::default(),
861        };
862
863        let mut config = WinSwConfig::default();
864        config.options.start_mode = Some(WinSwStartType::Boot);
865        WinSwServiceManager::write_service_configuration(
866            &service_config_file.to_path_buf(),
867            &ctx,
868            &config,
869        )
870        .unwrap();
871
872        let xml = std::fs::read_to_string(service_config_file.path()).unwrap();
873
874        service_config_file.assert(predicates::path::is_file());
875        assert_eq!("org.example.my_service", get_element_value(&xml, "id"));
876        assert_eq!("org.example.my_service", get_element_value(&xml, "name"));
877        assert_eq!(
878            "C:\\Program Files\\org.example\\my_service.exe",
879            get_element_value(&xml, "executable")
880        );
881        assert_eq!(
882            "Service for org.example.my_service",
883            get_element_value(&xml, "description")
884        );
885        assert_eq!(
886            "--arg value --another-arg",
887            get_element_value(&xml, "arguments")
888        );
889        assert_eq!("Boot", get_element_value(&xml, "startmode"));
890    }
891
892    #[test]
893    fn test_service_configuration_with_full_options() {
894        let temp_dir = assert_fs::TempDir::new().unwrap();
895        let service_config_file = temp_dir.child("service_config.xml");
896
897        let ctx = ServiceInstallCtx {
898            label: "org.example.my_service".parse().unwrap(),
899            program: PathBuf::from("C:\\Program Files\\org.example\\my_service.exe"),
900            args: vec![
901                OsString::from("--arg"),
902                OsString::from("value"),
903                OsString::from("--another-arg"),
904            ],
905            contents: None,
906            username: None,
907            working_directory: Some(PathBuf::from("C:\\Program Files\\org.example")),
908            environment: Some(vec![
909                ("ENV1".to_string(), "val1".to_string()),
910                ("ENV2".to_string(), "val2".to_string()),
911            ]),
912            autostart: true,
913            restart_policy: RestartPolicy::OnFailure {
914                delay_secs: Some(10),
915                max_retries: None,
916                reset_after_secs: None,
917            },
918        };
919
920        let config = WinSwConfig {
921            install: WinSwInstallConfig {
922                failure_action: Some(WinSwOnFailureAction::Restart(Some("10 sec".to_string()))),
923                reset_failure_time: Some("1 hour".to_string()),
924                security_descriptor: Some(
925                    "O:AOG:DAD:(A;;RPWPCCDCLCSWRCWDWOGA;;;S-1-0-0)".to_string(),
926                ),
927            },
928            options: WinSwOptionsConfig {
929                priority: Some(WinSwPriority::High),
930                stop_timeout: Some("15 sec".to_string()),
931                stop_executable: Some(PathBuf::from("C:\\Temp\\stop.exe")),
932                stop_args: Some(vec![
933                    OsString::from("--stop-arg1"),
934                    OsString::from("arg1val"),
935                    OsString::from("--stop-arg2-flag"),
936                ]),
937                start_mode: Some(WinSwStartType::Manual),
938                delayed_autostart: Some(true),
939                dependent_services: Some(vec!["service1".to_string(), "service2".to_string()]),
940                interactive: Some(true),
941                beep_on_shutdown: Some(true),
942            },
943            service_definition_dir_path: PathBuf::from("C:\\Temp\\service-definitions"),
944        };
945
946        WinSwServiceManager::write_service_configuration(
947            &service_config_file.to_path_buf(),
948            &ctx,
949            &config,
950        )
951        .unwrap();
952
953        let xml = std::fs::read_to_string(service_config_file.path()).unwrap();
954        println!("{xml}");
955
956        service_config_file.assert(predicates::path::is_file());
957        assert_eq!("org.example.my_service", get_element_value(&xml, "id"));
958        assert_eq!("org.example.my_service", get_element_value(&xml, "name"));
959        assert_eq!(
960            "C:\\Program Files\\org.example\\my_service.exe",
961            get_element_value(&xml, "executable")
962        );
963        assert_eq!(
964            "Service for org.example.my_service",
965            get_element_value(&xml, "description")
966        );
967        assert_eq!(
968            "--arg value --another-arg",
969            get_element_value(&xml, "arguments")
970        );
971        assert_eq!(
972            "C:\\Program Files\\org.example",
973            get_element_value(&xml, "workingdirectory")
974        );
975
976        let attributes = get_environment_variables(&xml);
977        assert_eq!(attributes[0].0, "ENV1");
978        assert_eq!(attributes[0].1, "val1");
979        assert_eq!(attributes[1].0, "ENV2");
980        assert_eq!(attributes[1].1, "val2");
981
982        // Install options
983        assert_eq!(
984            "restart",
985            get_element_attribute_value(&xml, "onfailure", "action")
986        );
987        assert_eq!(
988            "10 sec",
989            get_element_attribute_value(&xml, "onfailure", "delay")
990        );
991        assert_eq!("1 hour", get_element_value(&xml, "resetfailure"));
992        assert_eq!(
993            "O:AOG:DAD:(A;;RPWPCCDCLCSWRCWDWOGA;;;S-1-0-0)",
994            get_element_value(&xml, "securityDescriptor")
995        );
996
997        // Other options
998        assert_eq!("High", get_element_value(&xml, "priority"));
999        assert_eq!("15 sec", get_element_value(&xml, "stoptimeout"));
1000        assert_eq!(
1001            "C:\\Temp\\stop.exe",
1002            get_element_value(&xml, "stopexecutable")
1003        );
1004        assert_eq!(
1005            "--stop-arg1 arg1val --stop-arg2-flag",
1006            get_element_value(&xml, "stoparguments")
1007        );
1008        assert_eq!("Manual", get_element_value(&xml, "startmode"));
1009        assert_eq!("true", get_element_value(&xml, "delayedAutoStart"));
1010
1011        let dependent_services = get_element_values(&xml, "depend");
1012        assert_eq!("service1", dependent_services[0]);
1013        assert_eq!("service2", dependent_services[1]);
1014
1015        assert_eq!("true", get_element_value(&xml, "interactive"));
1016        assert_eq!("true", get_element_value(&xml, "beeponshutdown"));
1017    }
1018
1019    #[test]
1020    fn test_service_configuration_with_contents() {
1021        let temp_dir = assert_fs::TempDir::new().unwrap();
1022        let service_config_file = temp_dir.child("service_config.xml");
1023
1024        let contents = indoc! {r#"
1025            <service>
1026                <id>jenkins</id>
1027                <name>Jenkins</name>
1028                <description>This service runs Jenkins continuous integration system.</description>
1029                <executable>java</executable>
1030                <arguments>-Xrs -Xmx256m -jar "%BASE%\jenkins.war" --httpPort=8080</arguments>
1031                <startmode>Automatic</startmode>
1032            </service>
1033        "#};
1034        let ctx = ServiceInstallCtx {
1035            label: "org.example.my_service".parse().unwrap(),
1036            program: PathBuf::from("C:\\Program Files\\org.example\\my_service.exe"),
1037            args: vec![
1038                OsString::from("--arg"),
1039                OsString::from("value"),
1040                OsString::from("--another-arg"),
1041            ],
1042            contents: Some(contents.to_string()),
1043            username: None,
1044            working_directory: None,
1045            environment: None,
1046            autostart: true,
1047            restart_policy: RestartPolicy::default(),
1048        };
1049
1050        WinSwServiceManager::write_service_configuration(
1051            &service_config_file.to_path_buf(),
1052            &ctx,
1053            &WinSwConfig::default(),
1054        )
1055        .unwrap();
1056
1057        let xml = std::fs::read_to_string(service_config_file.path()).unwrap();
1058
1059        service_config_file.assert(predicates::path::is_file());
1060        assert_eq!("jenkins", get_element_value(&xml, "id"));
1061        assert_eq!("Jenkins", get_element_value(&xml, "name"));
1062        assert_eq!("java", get_element_value(&xml, "executable"));
1063        assert_eq!(
1064            "This service runs Jenkins continuous integration system.",
1065            get_element_value(&xml, "description")
1066        );
1067        assert_eq!(
1068            "-Xrs -Xmx256m -jar \"%BASE%\\jenkins.war\" --httpPort=8080",
1069            get_element_value(&xml, "arguments")
1070        );
1071    }
1072
1073    #[test]
1074    fn test_service_configuration_with_invalid_contents() {
1075        let temp_dir = assert_fs::TempDir::new().unwrap();
1076        let service_config_file = temp_dir.child("service_config.xml");
1077
1078        let ctx = ServiceInstallCtx {
1079            label: "org.example.my_service".parse().unwrap(),
1080            program: PathBuf::from("C:\\Program Files\\org.example\\my_service.exe"),
1081            args: vec![
1082                OsString::from("--arg"),
1083                OsString::from("value"),
1084                OsString::from("--another-arg"),
1085            ],
1086            contents: Some("this is not an XML document".to_string()),
1087            username: None,
1088            working_directory: None,
1089            environment: None,
1090            autostart: true,
1091            restart_policy: RestartPolicy::default(),
1092        };
1093
1094        let result = WinSwServiceManager::write_service_configuration(
1095            &service_config_file.to_path_buf(),
1096            &ctx,
1097            &WinSwConfig::default(),
1098        );
1099
1100        match result {
1101            Ok(()) => panic!("This test should result in a failure"),
1102            Err(e) => assert_eq!(
1103                "The contents override was not a valid XML document",
1104                e.to_string()
1105            ),
1106        }
1107    }
1108
1109    #[test]
1110    fn test_service_configuration_with_max_retries() {
1111        let temp_dir = assert_fs::TempDir::new().unwrap();
1112        let service_config_file = temp_dir.child("service_config.xml");
1113
1114        let ctx = ServiceInstallCtx {
1115            label: "org.example.my_service".parse().unwrap(),
1116            program: PathBuf::from("C:\\Program Files\\org.example\\my_service.exe"),
1117            args: vec![OsString::from("--arg")],
1118            contents: None,
1119            username: None,
1120            working_directory: None,
1121            environment: None,
1122            autostart: true,
1123            restart_policy: RestartPolicy::OnFailure {
1124                delay_secs: Some(10),
1125                max_retries: Some(3),
1126                reset_after_secs: None,
1127            },
1128        };
1129
1130        WinSwServiceManager::write_service_configuration(
1131            &service_config_file.to_path_buf(),
1132            &ctx,
1133            &WinSwConfig::default(),
1134        )
1135        .unwrap();
1136
1137        let xml = std::fs::read_to_string(service_config_file.path()).unwrap();
1138
1139        // Should have 4 onfailure elements: 3 restart + 1 none
1140        let onfailure_elements = get_all_elements_attributes(&xml, "onfailure");
1141        assert_eq!(4, onfailure_elements.len());
1142
1143        // First 3 should be restart with delay
1144        for elem in &onfailure_elements[..3] {
1145            assert_eq!(Some(&"restart".to_string()), elem.get("action"));
1146            assert_eq!(Some(&"10 sec".to_string()), elem.get("delay"));
1147        }
1148
1149        // Last should be none
1150        assert_eq!(
1151            Some(&"none".to_string()),
1152            onfailure_elements[3].get("action")
1153        );
1154        assert!(!onfailure_elements[3].contains_key("delay"));
1155
1156        // No resetfailure element since reset_after_secs is None
1157        assert!(!element_exists(&xml, "resetfailure"));
1158    }
1159
1160    #[test]
1161    fn test_service_configuration_with_reset_after_secs() {
1162        let temp_dir = assert_fs::TempDir::new().unwrap();
1163        let service_config_file = temp_dir.child("service_config.xml");
1164
1165        let ctx = ServiceInstallCtx {
1166            label: "org.example.my_service".parse().unwrap(),
1167            program: PathBuf::from("C:\\Program Files\\org.example\\my_service.exe"),
1168            args: vec![OsString::from("--arg")],
1169            contents: None,
1170            username: None,
1171            working_directory: None,
1172            environment: None,
1173            autostart: true,
1174            restart_policy: RestartPolicy::OnFailure {
1175                delay_secs: None,
1176                max_retries: None,
1177                reset_after_secs: Some(3600),
1178            },
1179        };
1180
1181        WinSwServiceManager::write_service_configuration(
1182            &service_config_file.to_path_buf(),
1183            &ctx,
1184            &WinSwConfig::default(),
1185        )
1186        .unwrap();
1187
1188        let xml = std::fs::read_to_string(service_config_file.path()).unwrap();
1189
1190        // Single onfailure element (no max_retries)
1191        let onfailure_elements = get_all_elements_attributes(&xml, "onfailure");
1192        assert_eq!(1, onfailure_elements.len());
1193        assert_eq!(
1194            Some(&"restart".to_string()),
1195            onfailure_elements[0].get("action")
1196        );
1197
1198        // resetfailure should be set
1199        assert_eq!("3600 sec", get_element_value(&xml, "resetfailure"));
1200    }
1201
1202    #[test]
1203    fn test_service_configuration_with_max_retries_and_reset_after_secs() {
1204        let temp_dir = assert_fs::TempDir::new().unwrap();
1205        let service_config_file = temp_dir.child("service_config.xml");
1206
1207        let ctx = ServiceInstallCtx {
1208            label: "org.example.my_service".parse().unwrap(),
1209            program: PathBuf::from("C:\\Program Files\\org.example\\my_service.exe"),
1210            args: vec![OsString::from("--arg")],
1211            contents: None,
1212            username: None,
1213            working_directory: None,
1214            environment: None,
1215            autostart: true,
1216            restart_policy: RestartPolicy::OnFailure {
1217                delay_secs: Some(5),
1218                max_retries: Some(2),
1219                reset_after_secs: Some(1800),
1220            },
1221        };
1222
1223        WinSwServiceManager::write_service_configuration(
1224            &service_config_file.to_path_buf(),
1225            &ctx,
1226            &WinSwConfig::default(),
1227        )
1228        .unwrap();
1229
1230        let xml = std::fs::read_to_string(service_config_file.path()).unwrap();
1231
1232        // 3 onfailure elements: 2 restart + 1 none
1233        let onfailure_elements = get_all_elements_attributes(&xml, "onfailure");
1234        assert_eq!(3, onfailure_elements.len());
1235
1236        for elem in &onfailure_elements[..2] {
1237            assert_eq!(Some(&"restart".to_string()), elem.get("action"));
1238            assert_eq!(Some(&"5 sec".to_string()), elem.get("delay"));
1239        }
1240        assert_eq!(
1241            Some(&"none".to_string()),
1242            onfailure_elements[2].get("action")
1243        );
1244
1245        // resetfailure should be set
1246        assert_eq!("1800 sec", get_element_value(&xml, "resetfailure"));
1247    }
1248
1249    #[test]
1250    fn test_service_configuration_with_no_retries_preserves_current_behavior() {
1251        let temp_dir = assert_fs::TempDir::new().unwrap();
1252        let service_config_file = temp_dir.child("service_config.xml");
1253
1254        let ctx = ServiceInstallCtx {
1255            label: "org.example.my_service".parse().unwrap(),
1256            program: PathBuf::from("C:\\Program Files\\org.example\\my_service.exe"),
1257            args: vec![OsString::from("--arg")],
1258            contents: None,
1259            username: None,
1260            working_directory: None,
1261            environment: None,
1262            autostart: true,
1263            restart_policy: RestartPolicy::OnFailure {
1264                delay_secs: Some(10),
1265                max_retries: None,
1266                reset_after_secs: None,
1267            },
1268        };
1269
1270        WinSwServiceManager::write_service_configuration(
1271            &service_config_file.to_path_buf(),
1272            &ctx,
1273            &WinSwConfig::default(),
1274        )
1275        .unwrap();
1276
1277        let xml = std::fs::read_to_string(service_config_file.path()).unwrap();
1278
1279        // Single onfailure element (backwards compatible)
1280        let onfailure_elements = get_all_elements_attributes(&xml, "onfailure");
1281        assert_eq!(1, onfailure_elements.len());
1282        assert_eq!(
1283            Some(&"restart".to_string()),
1284            onfailure_elements[0].get("action")
1285        );
1286        assert_eq!(
1287            Some(&"10 sec".to_string()),
1288            onfailure_elements[0].get("delay")
1289        );
1290
1291        // No resetfailure
1292        assert!(!element_exists(&xml, "resetfailure"));
1293    }
1294
1295    #[test]
1296    fn test_winsw_specific_config_takes_precedence_over_max_retries() {
1297        let temp_dir = assert_fs::TempDir::new().unwrap();
1298        let service_config_file = temp_dir.child("service_config.xml");
1299
1300        let ctx = ServiceInstallCtx {
1301            label: "org.example.my_service".parse().unwrap(),
1302            program: PathBuf::from("C:\\Program Files\\org.example\\my_service.exe"),
1303            args: vec![OsString::from("--arg")],
1304            contents: None,
1305            username: None,
1306            working_directory: None,
1307            environment: None,
1308            autostart: true,
1309            restart_policy: RestartPolicy::OnFailure {
1310                delay_secs: Some(10),
1311                max_retries: Some(5),
1312                reset_after_secs: Some(3600),
1313            },
1314        };
1315
1316        let config = WinSwConfig {
1317            install: WinSwInstallConfig {
1318                failure_action: Some(WinSwOnFailureAction::Restart(Some("20 sec".to_string()))),
1319                reset_failure_time: None,
1320                security_descriptor: None,
1321            },
1322            ..WinSwConfig::default()
1323        };
1324
1325        WinSwServiceManager::write_service_configuration(
1326            &service_config_file.to_path_buf(),
1327            &ctx,
1328            &config,
1329        )
1330        .unwrap();
1331
1332        let xml = std::fs::read_to_string(service_config_file.path()).unwrap();
1333
1334        // WinSW-specific config should produce single element, ignoring max_retries
1335        let onfailure_elements = get_all_elements_attributes(&xml, "onfailure");
1336        assert_eq!(1, onfailure_elements.len());
1337        assert_eq!(
1338            Some(&"restart".to_string()),
1339            onfailure_elements[0].get("action")
1340        );
1341        assert_eq!(
1342            Some(&"20 sec".to_string()),
1343            onfailure_elements[0].get("delay")
1344        );
1345    }
1346
1347    #[test]
1348    fn test_winsw_specific_reset_failure_time_takes_precedence() {
1349        let temp_dir = assert_fs::TempDir::new().unwrap();
1350        let service_config_file = temp_dir.child("service_config.xml");
1351
1352        let ctx = ServiceInstallCtx {
1353            label: "org.example.my_service".parse().unwrap(),
1354            program: PathBuf::from("C:\\Program Files\\org.example\\my_service.exe"),
1355            args: vec![OsString::from("--arg")],
1356            contents: None,
1357            username: None,
1358            working_directory: None,
1359            environment: None,
1360            autostart: true,
1361            restart_policy: RestartPolicy::OnFailure {
1362                delay_secs: None,
1363                max_retries: Some(3),
1364                reset_after_secs: Some(3600),
1365            },
1366        };
1367
1368        let config = WinSwConfig {
1369            install: WinSwInstallConfig {
1370                failure_action: None,
1371                reset_failure_time: Some("2 hours".to_string()),
1372                security_descriptor: None,
1373            },
1374            ..WinSwConfig::default()
1375        };
1376
1377        WinSwServiceManager::write_service_configuration(
1378            &service_config_file.to_path_buf(),
1379            &ctx,
1380            &config,
1381        )
1382        .unwrap();
1383
1384        let xml = std::fs::read_to_string(service_config_file.path()).unwrap();
1385
1386        // WinSW-specific reset_failure_time should be used, not reset_after_secs
1387        assert_eq!("2 hours", get_element_value(&xml, "resetfailure"));
1388    }
1389}