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