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#[derive(Clone, Debug, PartialEq, Eq)]
24pub struct WinSwConfig {
25 pub install: WinSwInstallConfig,
26 pub options: WinSwOptionsConfig,
27 pub service_definition_dir_path: PathBuf,
28}
29
30impl Default for WinSwConfig {
31 fn default() -> Self {
32 WinSwConfig {
33 install: WinSwInstallConfig::default(),
34 options: WinSwOptionsConfig::default(),
35 service_definition_dir_path: PathBuf::from("C:\\ProgramData\\service-manager"),
36 }
37 }
38}
39
40#[derive(Clone, Debug, Default, PartialEq, Eq)]
41pub struct WinSwInstallConfig {
42 pub failure_action: 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 Automatic,
74 Boot,
76 Manual,
78 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#[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 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 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 if let Some(failure_action) = &config.install.failure_action {
201 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 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 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 Self::write_element_with_attributes(
266 &mut writer,
267 "onfailure",
268 &attributes,
269 None,
270 )?;
271 }
272
273 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 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 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 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 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 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 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 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 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 let onfailure_elements = get_all_elements_attributes(&xml, "onfailure");
1141 assert_eq!(4, onfailure_elements.len());
1142
1143 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 assert_eq!(
1151 Some(&"none".to_string()),
1152 onfailure_elements[3].get("action")
1153 );
1154 assert!(!onfailure_elements[3].contains_key("delay"));
1155
1156 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 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 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 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 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 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 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 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 assert_eq!("2 hours", get_element_value(&xml, "resetfailure"));
1388 }
1389}