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 let delay_str_always;
201 let delay_str_failure;
202 let (action, delay) = if let Some(failure_action) = &config.install.failure_action {
203 match failure_action {
205 WinSwOnFailureAction::Restart(delay) => ("restart", delay.as_deref()),
206 WinSwOnFailureAction::Reboot => ("reboot", None),
207 WinSwOnFailureAction::None => ("none", None),
208 }
209 } else {
210 match ctx.restart_policy {
212 RestartPolicy::Never => ("none", None),
213 RestartPolicy::Always { delay_secs } => {
214 delay_str_always = delay_secs.map(|secs| format!("{} sec", secs));
215 ("restart", delay_str_always.as_deref())
216 }
217 RestartPolicy::OnFailure { delay_secs } => {
218 delay_str_failure = delay_secs.map(|secs| format!("{} sec", secs));
219 ("restart", delay_str_failure.as_deref())
220 }
221 RestartPolicy::OnSuccess { delay_secs } => {
222 log::warn!(
223 "WinSW does not support restart on success; falling back to 'always' for service '{}'",
224 ctx.label
225 );
226 delay_str_always = delay_secs.map(|secs| format!("{} sec", secs));
227 ("restart", delay_str_always.as_deref())
228 }
229 }
230 };
231
232 let attributes = delay.map_or_else(
233 || vec![("action", action)],
234 |d| vec![("action", action), ("delay", d)],
235 );
236 Self::write_element_with_attributes(&mut writer, "onfailure", &attributes, None)?;
237
238 if let Some(reset_time) = &config.install.reset_failure_time {
239 Self::write_element(&mut writer, "resetfailure", reset_time)?;
240 }
241 if let Some(security_descriptor) = &config.install.security_descriptor {
242 Self::write_element(&mut writer, "securityDescriptor", security_descriptor)?;
243 }
244
245 if let Some(priority) = &config.options.priority {
247 Self::write_element(&mut writer, "priority", &format!("{:?}", priority))?;
248 }
249 if let Some(stop_timeout) = &config.options.stop_timeout {
250 Self::write_element(&mut writer, "stoptimeout", stop_timeout)?;
251 }
252 if let Some(stop_executable) = &config.options.stop_executable {
253 Self::write_element(
254 &mut writer,
255 "stopexecutable",
256 &stop_executable.to_string_lossy(),
257 )?;
258 }
259 if let Some(stop_args) = &config.options.stop_args {
260 let stop_args = stop_args
261 .iter()
262 .map(|s| s.to_string_lossy().into_owned())
263 .collect::<Vec<String>>()
264 .join(" ");
265 Self::write_element(&mut writer, "stoparguments", &stop_args)?;
266 }
267
268 if let Some(start_mode) = &config.options.start_mode {
269 Self::write_element(&mut writer, "startmode", &format!("{:?}", start_mode))?;
270 } else if ctx.autostart {
271 Self::write_element(&mut writer, "startmode", "Automatic")?;
272 } else {
273 Self::write_element(&mut writer, "startmode", "Manual")?;
274 }
275
276 if let Some(delayed_autostart) = config.options.delayed_autostart {
277 Self::write_element(
278 &mut writer,
279 "delayedAutoStart",
280 &delayed_autostart.to_string(),
281 )?;
282 }
283 if let Some(dependent_services) = &config.options.dependent_services {
284 for service in dependent_services {
285 Self::write_element(&mut writer, "depend", service)?;
286 }
287 }
288 if let Some(interactive) = config.options.interactive {
289 Self::write_element(&mut writer, "interactive", &interactive.to_string())?;
290 }
291 if let Some(beep_on_shutdown) = config.options.beep_on_shutdown {
292 Self::write_element(&mut writer, "beeponshutdown", &beep_on_shutdown.to_string())?;
293 }
294
295 writer.write(XmlEvent::end_element()).map_err(|e| {
297 io::Error::new(
298 io::ErrorKind::Other,
299 format!("Writing service config failed: {}", e),
300 )
301 })?;
302
303 Ok(())
304 }
305
306 fn write_element<W: Write>(
307 writer: &mut EventWriter<W>,
308 name: &str,
309 value: &str,
310 ) -> io::Result<()> {
311 writer.write(XmlEvent::start_element(name)).map_err(|e| {
312 io::Error::new(
313 io::ErrorKind::Other,
314 format!("Failed to write element '{}': {}", name, e),
315 )
316 })?;
317 writer.write(XmlEvent::characters(value)).map_err(|e| {
318 io::Error::new(
319 io::ErrorKind::Other,
320 format!("Failed to write value for element '{}': {}", name, e),
321 )
322 })?;
323 writer.write(XmlEvent::end_element()).map_err(|e| {
324 io::Error::new(
325 io::ErrorKind::Other,
326 format!("Failed to end element '{}': {}", name, e),
327 )
328 })?;
329 Ok(())
330 }
331
332 fn write_element_with_attributes<W: Write>(
333 writer: &mut EventWriter<W>,
334 name: &str,
335 attributes: &[(&str, &str)],
336 value: Option<&str>,
337 ) -> io::Result<()> {
338 let mut start_element = XmlEvent::start_element(name);
339 for &(attr_name, attr_value) in attributes {
340 start_element = start_element.attr(attr_name, attr_value);
341 }
342 writer.write(start_element).map_err(|e| {
343 io::Error::new(
344 io::ErrorKind::Other,
345 format!("Failed to write value for element '{}': {}", name, e),
346 )
347 })?;
348
349 if let Some(val) = value {
350 writer.write(XmlEvent::characters(val)).map_err(|e| {
351 io::Error::new(
352 io::ErrorKind::Other,
353 format!("Failed to write value for element '{}': {}", name, e),
354 )
355 })?;
356 }
357
358 writer.write(XmlEvent::end_element()).map_err(|e| {
359 io::Error::new(
360 io::ErrorKind::Other,
361 format!("Failed to end element '{}': {}", name, e),
362 )
363 })?;
364
365 Ok(())
366 }
367
368 fn is_valid_xml(xml_string: &str) -> bool {
369 let cursor = Cursor::new(xml_string);
370 let parser = EventReader::new(cursor);
371 for e in parser {
372 if e.is_err() {
373 return false;
374 }
375 }
376 true
377 }
378}
379
380impl ServiceManager for WinSwServiceManager {
381 fn available(&self) -> io::Result<bool> {
382 match which::which(WINSW_EXE) {
383 Ok(_) => Ok(true),
384 Err(which::Error::CannotFindBinaryPath) => match std::env::var("WINSW_PATH") {
385 Ok(val) => {
386 let path = PathBuf::from(val);
387 Ok(path.exists())
388 }
389 Err(_) => Ok(false),
390 },
391 Err(x) => Err(io::Error::new(io::ErrorKind::Other, x)),
392 }
393 }
394
395 fn install(&self, ctx: ServiceInstallCtx) -> io::Result<()> {
396 let service_name = ctx.label.to_qualified_name();
397 let service_instance_path = self
398 .config
399 .service_definition_dir_path
400 .join(service_name.clone());
401 std::fs::create_dir_all(&service_instance_path)?;
402
403 let service_config_path = service_instance_path.join(format!("{service_name}.xml"));
404 Self::write_service_configuration(&service_config_path, &ctx, &self.config)?;
405
406 wrap_output(winsw_exe("install", &service_name, &service_instance_path)?)?;
407 Ok(())
408 }
409
410 fn uninstall(&self, ctx: ServiceUninstallCtx) -> io::Result<()> {
411 let service_name = ctx.label.to_qualified_name();
412 let service_instance_path = self
413 .config
414 .service_definition_dir_path
415 .join(service_name.clone());
416 wrap_output(winsw_exe(
417 "uninstall",
418 &service_name,
419 &service_instance_path,
420 )?)?;
421
422 std::fs::remove_dir_all(service_instance_path)?;
426
427 Ok(())
428 }
429
430 fn start(&self, ctx: ServiceStartCtx) -> io::Result<()> {
431 let service_name = ctx.label.to_qualified_name();
432 let service_instance_path = self
433 .config
434 .service_definition_dir_path
435 .join(service_name.clone());
436 wrap_output(winsw_exe("start", &service_name, &service_instance_path)?)?;
437 Ok(())
438 }
439
440 fn stop(&self, ctx: ServiceStopCtx) -> io::Result<()> {
441 let service_name = ctx.label.to_qualified_name();
442 let service_instance_path = self
443 .config
444 .service_definition_dir_path
445 .join(service_name.clone());
446 wrap_output(winsw_exe("stop", &service_name, &service_instance_path)?)?;
447 Ok(())
448 }
449
450 fn level(&self) -> ServiceLevel {
451 ServiceLevel::System
452 }
453
454 fn set_level(&mut self, level: ServiceLevel) -> io::Result<()> {
455 match level {
456 ServiceLevel::System => Ok(()),
457 ServiceLevel::User => Err(io::Error::new(
458 io::ErrorKind::Unsupported,
459 "Windows does not support user-level services",
460 )),
461 }
462 }
463
464 fn status(&self, ctx: crate::ServiceStatusCtx) -> io::Result<ServiceStatus> {
465 let service_name = ctx.label.to_qualified_name();
466 let service_instance_path = self
467 .config
468 .service_definition_dir_path
469 .join(service_name.clone());
470 if !service_instance_path.exists() {
471 return Ok(ServiceStatus::NotInstalled);
472 }
473 let output = winsw_exe("status", &service_name, &service_instance_path)?;
474 if !output.status.success() {
475 let stderr = String::from_utf8_lossy(&output.stderr);
476 if stderr.contains("System.IO.FileNotFoundException: Unable to locate WinSW.[xml|yml] file within executable directory") {
478 return Ok(ServiceStatus::NotInstalled);
479 }
480 let stdout = String::from_utf8_lossy(&output.stdout);
481 if stdout.contains("Active") {
483 return Ok(ServiceStatus::Running);
484 }
485 return Err(io::Error::new(
486 io::ErrorKind::Other,
487 format!("Failed to get service status: {}", stderr),
488 ));
489 }
490 let stdout = String::from_utf8_lossy(&output.stdout);
491 if stdout.contains("NonExistent") {
492 Ok(ServiceStatus::NotInstalled)
493 } else if stdout.contains("running") {
494 Ok(ServiceStatus::Running)
495 } else {
496 Ok(ServiceStatus::Stopped(None))
497 }
498 }
499}
500
501fn winsw_exe(cmd: &str, service_name: &str, working_dir_path: &Path) -> io::Result<Output> {
502 let winsw_path = match std::env::var("WINSW_PATH") {
503 Ok(val) => {
504 let path = PathBuf::from(val);
505 if path.exists() {
506 path
507 } else {
508 PathBuf::from(WINSW_EXE)
509 }
510 }
511 Err(_) => PathBuf::from(WINSW_EXE),
512 };
513
514 let mut command = Command::new(winsw_path);
515 command
516 .stdin(Stdio::null())
517 .stdout(Stdio::piped())
518 .stderr(Stdio::piped());
519 command.current_dir(working_dir_path);
520 command.arg(cmd).arg(format!("{}.xml", service_name));
521
522 command.output()
523}
524
525#[cfg(test)]
526mod tests {
527 use super::*;
528 use assert_fs::prelude::*;
529 use indoc::indoc;
530 use std::ffi::OsString;
531 use std::io::Cursor;
532 use xml::reader::{EventReader, XmlEvent};
533
534 fn get_element_value(xml: &str, element_name: &str) -> String {
535 let cursor = Cursor::new(xml);
536 let parser = EventReader::new(cursor);
537 let mut inside_target_element = false;
538
539 for e in parser {
540 match e {
541 Ok(XmlEvent::StartElement { name, .. }) if name.local_name == element_name => {
542 inside_target_element = true;
543 }
544 Ok(XmlEvent::Characters(text)) if inside_target_element => {
545 return text;
546 }
547 Ok(XmlEvent::EndElement { name }) if name.local_name == element_name => {
548 inside_target_element = false;
549 }
550 Err(e) => panic!("Error while parsing XML: {}", e),
551 _ => {}
552 }
553 }
554
555 panic!("Element {} not found", element_name);
556 }
557
558 fn get_element_attribute_value(xml: &str, element_name: &str, attribute_name: &str) -> String {
559 let cursor = Cursor::new(xml);
560 let parser = EventReader::new(cursor);
561
562 for e in parser {
563 match e {
564 Ok(XmlEvent::StartElement {
565 name, attributes, ..
566 }) if name.local_name == element_name => {
567 for attr in attributes {
568 if attr.name.local_name == attribute_name {
569 return attr.value;
570 }
571 }
572 }
573 Err(e) => panic!("Error while parsing XML: {}", e),
574 _ => {}
575 }
576 }
577
578 panic!("Attribute {} not found", attribute_name);
579 }
580
581 fn get_element_values(xml: &str, element_name: &str) -> Vec<String> {
582 let cursor = Cursor::new(xml);
583 let parser = EventReader::new(cursor);
584 let mut values = Vec::new();
585 let mut inside_target_element = false;
586
587 for e in parser {
588 match e {
589 Ok(XmlEvent::StartElement { name, .. }) if name.local_name == element_name => {
590 inside_target_element = true;
591 }
592 Ok(XmlEvent::Characters(text)) if inside_target_element => {
593 values.push(text);
594 }
595 Ok(XmlEvent::EndElement { name }) if name.local_name == element_name => {
596 inside_target_element = false;
597 }
598 Err(e) => panic!("Error while parsing XML: {}", e),
599 _ => {}
600 }
601 }
602
603 values
604 }
605
606 fn get_environment_variables(xml: &str) -> Vec<(String, String)> {
607 let cursor = Cursor::new(xml);
608 let parser = EventReader::new(cursor);
609 let mut env_vars = Vec::new();
610
611 for e in parser.into_iter().flatten() {
612 if let XmlEvent::StartElement {
613 name, attributes, ..
614 } = e
615 {
616 if name.local_name == "env" {
617 let mut name_value_pair = (String::new(), String::new());
618 for attr in attributes {
619 match attr.name.local_name.as_str() {
620 "name" => name_value_pair.0 = attr.value,
621 "value" => name_value_pair.1 = attr.value,
622 _ => {}
623 }
624 }
625 if !name_value_pair.0.is_empty() && !name_value_pair.1.is_empty() {
626 env_vars.push(name_value_pair);
627 }
628 }
629 }
630 }
631 env_vars
632 }
633
634 #[test]
635 fn test_service_configuration_with_mandatory_elements() {
636 let temp_dir = assert_fs::TempDir::new().unwrap();
637 let service_config_file = temp_dir.child("service_config.xml");
638
639 let ctx = ServiceInstallCtx {
640 label: "org.example.my_service".parse().unwrap(),
641 program: PathBuf::from("C:\\Program Files\\org.example\\my_service.exe"),
642 args: vec![
643 OsString::from("--arg"),
644 OsString::from("value"),
645 OsString::from("--another-arg"),
646 ],
647 contents: None,
648 username: None,
649 working_directory: None,
650 environment: None,
651 autostart: true,
652 restart_policy: RestartPolicy::default(),
653 };
654
655 WinSwServiceManager::write_service_configuration(
656 &service_config_file.to_path_buf(),
657 &ctx,
658 &WinSwConfig::default(),
659 )
660 .unwrap();
661
662 let xml = std::fs::read_to_string(service_config_file.path()).unwrap();
663
664 service_config_file.assert(predicates::path::is_file());
665 assert_eq!("org.example.my_service", get_element_value(&xml, "id"));
666 assert_eq!("org.example.my_service", get_element_value(&xml, "name"));
667 assert_eq!(
668 "C:\\Program Files\\org.example\\my_service.exe",
669 get_element_value(&xml, "executable")
670 );
671 assert_eq!(
672 "Service for org.example.my_service",
673 get_element_value(&xml, "description")
674 );
675 assert_eq!(
676 "--arg value --another-arg",
677 get_element_value(&xml, "arguments")
678 );
679 assert_eq!("Automatic", get_element_value(&xml, "startmode"));
680 }
681
682 #[test]
683 fn test_service_configuration_with_autostart_false() {
684 let temp_dir = assert_fs::TempDir::new().unwrap();
685 let service_config_file = temp_dir.child("service_config.xml");
686
687 let ctx = ServiceInstallCtx {
688 label: "org.example.my_service".parse().unwrap(),
689 program: PathBuf::from("C:\\Program Files\\org.example\\my_service.exe"),
690 args: vec![
691 OsString::from("--arg"),
692 OsString::from("value"),
693 OsString::from("--another-arg"),
694 ],
695 contents: None,
696 username: None,
697 working_directory: None,
698 environment: None,
699 autostart: false,
700 restart_policy: RestartPolicy::default(),
701 };
702
703 WinSwServiceManager::write_service_configuration(
704 &service_config_file.to_path_buf(),
705 &ctx,
706 &WinSwConfig::default(),
707 )
708 .unwrap();
709
710 let xml = std::fs::read_to_string(service_config_file.path()).unwrap();
711
712 service_config_file.assert(predicates::path::is_file());
713 assert_eq!("org.example.my_service", get_element_value(&xml, "id"));
714 assert_eq!("org.example.my_service", get_element_value(&xml, "name"));
715 assert_eq!(
716 "C:\\Program Files\\org.example\\my_service.exe",
717 get_element_value(&xml, "executable")
718 );
719 assert_eq!(
720 "Service for org.example.my_service",
721 get_element_value(&xml, "description")
722 );
723 assert_eq!(
724 "--arg value --another-arg",
725 get_element_value(&xml, "arguments")
726 );
727 assert_eq!("Manual", get_element_value(&xml, "startmode"));
728 }
729
730 #[test]
731 fn test_service_configuration_with_special_start_type_should_override_autostart() {
732 let temp_dir = assert_fs::TempDir::new().unwrap();
733 let service_config_file = temp_dir.child("service_config.xml");
734
735 let ctx = ServiceInstallCtx {
736 label: "org.example.my_service".parse().unwrap(),
737 program: PathBuf::from("C:\\Program Files\\org.example\\my_service.exe"),
738 args: vec![
739 OsString::from("--arg"),
740 OsString::from("value"),
741 OsString::from("--another-arg"),
742 ],
743 contents: None,
744 username: None,
745 working_directory: None,
746 environment: None,
747 autostart: false,
748 restart_policy: RestartPolicy::default(),
749 };
750
751 let mut config = WinSwConfig::default();
752 config.options.start_mode = Some(WinSwStartType::Boot);
753 WinSwServiceManager::write_service_configuration(
754 &service_config_file.to_path_buf(),
755 &ctx,
756 &config,
757 )
758 .unwrap();
759
760 let xml = std::fs::read_to_string(service_config_file.path()).unwrap();
761
762 service_config_file.assert(predicates::path::is_file());
763 assert_eq!("org.example.my_service", get_element_value(&xml, "id"));
764 assert_eq!("org.example.my_service", get_element_value(&xml, "name"));
765 assert_eq!(
766 "C:\\Program Files\\org.example\\my_service.exe",
767 get_element_value(&xml, "executable")
768 );
769 assert_eq!(
770 "Service for org.example.my_service",
771 get_element_value(&xml, "description")
772 );
773 assert_eq!(
774 "--arg value --another-arg",
775 get_element_value(&xml, "arguments")
776 );
777 assert_eq!("Boot", get_element_value(&xml, "startmode"));
778 }
779
780 #[test]
781 fn test_service_configuration_with_full_options() {
782 let temp_dir = assert_fs::TempDir::new().unwrap();
783 let service_config_file = temp_dir.child("service_config.xml");
784
785 let ctx = ServiceInstallCtx {
786 label: "org.example.my_service".parse().unwrap(),
787 program: PathBuf::from("C:\\Program Files\\org.example\\my_service.exe"),
788 args: vec![
789 OsString::from("--arg"),
790 OsString::from("value"),
791 OsString::from("--another-arg"),
792 ],
793 contents: None,
794 username: None,
795 working_directory: Some(PathBuf::from("C:\\Program Files\\org.example")),
796 environment: Some(vec![
797 ("ENV1".to_string(), "val1".to_string()),
798 ("ENV2".to_string(), "val2".to_string()),
799 ]),
800 autostart: true,
801 restart_policy: RestartPolicy::OnFailure {
802 delay_secs: Some(10),
803 },
804 };
805
806 let config = WinSwConfig {
807 install: WinSwInstallConfig {
808 failure_action: Some(WinSwOnFailureAction::Restart(Some("10 sec".to_string()))),
809 reset_failure_time: Some("1 hour".to_string()),
810 security_descriptor: Some(
811 "O:AOG:DAD:(A;;RPWPCCDCLCSWRCWDWOGA;;;S-1-0-0)".to_string(),
812 ),
813 },
814 options: WinSwOptionsConfig {
815 priority: Some(WinSwPriority::High),
816 stop_timeout: Some("15 sec".to_string()),
817 stop_executable: Some(PathBuf::from("C:\\Temp\\stop.exe")),
818 stop_args: Some(vec![
819 OsString::from("--stop-arg1"),
820 OsString::from("arg1val"),
821 OsString::from("--stop-arg2-flag"),
822 ]),
823 start_mode: Some(WinSwStartType::Manual),
824 delayed_autostart: Some(true),
825 dependent_services: Some(vec!["service1".to_string(), "service2".to_string()]),
826 interactive: Some(true),
827 beep_on_shutdown: Some(true),
828 },
829 service_definition_dir_path: PathBuf::from("C:\\Temp\\service-definitions"),
830 };
831
832 WinSwServiceManager::write_service_configuration(
833 &service_config_file.to_path_buf(),
834 &ctx,
835 &config,
836 )
837 .unwrap();
838
839 let xml = std::fs::read_to_string(service_config_file.path()).unwrap();
840 println!("{xml}");
841
842 service_config_file.assert(predicates::path::is_file());
843 assert_eq!("org.example.my_service", get_element_value(&xml, "id"));
844 assert_eq!("org.example.my_service", get_element_value(&xml, "name"));
845 assert_eq!(
846 "C:\\Program Files\\org.example\\my_service.exe",
847 get_element_value(&xml, "executable")
848 );
849 assert_eq!(
850 "Service for org.example.my_service",
851 get_element_value(&xml, "description")
852 );
853 assert_eq!(
854 "--arg value --another-arg",
855 get_element_value(&xml, "arguments")
856 );
857 assert_eq!(
858 "C:\\Program Files\\org.example",
859 get_element_value(&xml, "workingdirectory")
860 );
861
862 let attributes = get_environment_variables(&xml);
863 assert_eq!(attributes[0].0, "ENV1");
864 assert_eq!(attributes[0].1, "val1");
865 assert_eq!(attributes[1].0, "ENV2");
866 assert_eq!(attributes[1].1, "val2");
867
868 assert_eq!(
870 "restart",
871 get_element_attribute_value(&xml, "onfailure", "action")
872 );
873 assert_eq!(
874 "10 sec",
875 get_element_attribute_value(&xml, "onfailure", "delay")
876 );
877 assert_eq!("1 hour", get_element_value(&xml, "resetfailure"));
878 assert_eq!(
879 "O:AOG:DAD:(A;;RPWPCCDCLCSWRCWDWOGA;;;S-1-0-0)",
880 get_element_value(&xml, "securityDescriptor")
881 );
882
883 assert_eq!("High", get_element_value(&xml, "priority"));
885 assert_eq!("15 sec", get_element_value(&xml, "stoptimeout"));
886 assert_eq!(
887 "C:\\Temp\\stop.exe",
888 get_element_value(&xml, "stopexecutable")
889 );
890 assert_eq!(
891 "--stop-arg1 arg1val --stop-arg2-flag",
892 get_element_value(&xml, "stoparguments")
893 );
894 assert_eq!("Manual", get_element_value(&xml, "startmode"));
895 assert_eq!("true", get_element_value(&xml, "delayedAutoStart"));
896
897 let dependent_services = get_element_values(&xml, "depend");
898 assert_eq!("service1", dependent_services[0]);
899 assert_eq!("service2", dependent_services[1]);
900
901 assert_eq!("true", get_element_value(&xml, "interactive"));
902 assert_eq!("true", get_element_value(&xml, "beeponshutdown"));
903 }
904
905 #[test]
906 fn test_service_configuration_with_contents() {
907 let temp_dir = assert_fs::TempDir::new().unwrap();
908 let service_config_file = temp_dir.child("service_config.xml");
909
910 let contents = indoc! {r#"
911 <service>
912 <id>jenkins</id>
913 <name>Jenkins</name>
914 <description>This service runs Jenkins continuous integration system.</description>
915 <executable>java</executable>
916 <arguments>-Xrs -Xmx256m -jar "%BASE%\jenkins.war" --httpPort=8080</arguments>
917 <startmode>Automatic</startmode>
918 </service>
919 "#};
920 let ctx = ServiceInstallCtx {
921 label: "org.example.my_service".parse().unwrap(),
922 program: PathBuf::from("C:\\Program Files\\org.example\\my_service.exe"),
923 args: vec![
924 OsString::from("--arg"),
925 OsString::from("value"),
926 OsString::from("--another-arg"),
927 ],
928 contents: Some(contents.to_string()),
929 username: None,
930 working_directory: None,
931 environment: None,
932 autostart: true,
933 restart_policy: RestartPolicy::default(),
934 };
935
936 WinSwServiceManager::write_service_configuration(
937 &service_config_file.to_path_buf(),
938 &ctx,
939 &WinSwConfig::default(),
940 )
941 .unwrap();
942
943 let xml = std::fs::read_to_string(service_config_file.path()).unwrap();
944
945 service_config_file.assert(predicates::path::is_file());
946 assert_eq!("jenkins", get_element_value(&xml, "id"));
947 assert_eq!("Jenkins", get_element_value(&xml, "name"));
948 assert_eq!("java", get_element_value(&xml, "executable"));
949 assert_eq!(
950 "This service runs Jenkins continuous integration system.",
951 get_element_value(&xml, "description")
952 );
953 assert_eq!(
954 "-Xrs -Xmx256m -jar \"%BASE%\\jenkins.war\" --httpPort=8080",
955 get_element_value(&xml, "arguments")
956 );
957 }
958
959 #[test]
960 fn test_service_configuration_with_invalid_contents() {
961 let temp_dir = assert_fs::TempDir::new().unwrap();
962 let service_config_file = temp_dir.child("service_config.xml");
963
964 let ctx = ServiceInstallCtx {
965 label: "org.example.my_service".parse().unwrap(),
966 program: PathBuf::from("C:\\Program Files\\org.example\\my_service.exe"),
967 args: vec![
968 OsString::from("--arg"),
969 OsString::from("value"),
970 OsString::from("--another-arg"),
971 ],
972 contents: Some("this is not an XML document".to_string()),
973 username: None,
974 working_directory: None,
975 environment: None,
976 autostart: true,
977 restart_policy: RestartPolicy::default(),
978 };
979
980 let result = WinSwServiceManager::write_service_configuration(
981 &service_config_file.to_path_buf(),
982 &ctx,
983 &WinSwConfig::default(),
984 );
985
986 match result {
987 Ok(()) => panic!("This test should result in a failure"),
988 Err(e) => assert_eq!(
989 "The contents override was not a valid XML document",
990 e.to_string()
991 ),
992 }
993 }
994}