use super::{
ServiceInstallCtx, ServiceLevel, ServiceManager, ServiceStartCtx, ServiceStopCtx,
ServiceUninstallCtx,
};
use std::ffi::OsString;
use std::fs::File;
use std::io::{self, BufWriter, Cursor, Write};
use std::path::PathBuf;
use std::process::{Command, Stdio};
use xml::common::XmlVersion;
use xml::reader::EventReader;
use xml::writer::{EmitterConfig, EventWriter, XmlEvent};
static WINSW_EXE: &str = "winsw.exe";
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct WinSwConfig {
pub install: WinSwInstallConfig,
pub options: WinSwOptionsConfig,
pub service_definition_dir_path: PathBuf,
}
impl Default for WinSwConfig {
fn default() -> Self {
WinSwConfig {
install: WinSwInstallConfig::default(),
options: WinSwOptionsConfig::default(),
service_definition_dir_path: PathBuf::from("C:\\ProgramData\\service-manager"),
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct WinSwInstallConfig {
pub failure_action: WinSwOnFailureAction,
pub reset_failure_time: Option<String>,
pub security_descriptor: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct WinSwOptionsConfig {
pub priority: Option<WinSwPriority>,
pub stop_timeout: Option<String>,
pub stop_executable: Option<PathBuf>,
pub stop_args: Option<Vec<OsString>>,
pub start_mode: Option<WinSwStartType>,
pub delayed_autostart: Option<bool>,
pub dependent_services: Option<Vec<String>>,
pub interactive: Option<bool>,
pub beep_on_shutdown: Option<bool>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub enum WinSwOnFailureAction {
Restart(Option<String>),
Reboot,
#[default]
None,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum WinSwStartType {
Automatic,
Boot,
Manual,
System,
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub enum WinSwPriority {
#[default]
Normal,
Idle,
High,
RealTime,
BelowNormal,
AboveNormal,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct WinSwServiceManager {
pub config: WinSwConfig,
}
impl WinSwServiceManager {
pub fn system() -> Self {
let config = WinSwConfig {
install: WinSwInstallConfig::default(),
options: WinSwOptionsConfig::default(),
service_definition_dir_path: PathBuf::from("C:\\ProgramData\\service-manager"),
};
Self { config }
}
pub fn with_config(self, config: WinSwConfig) -> Self {
Self { config }
}
pub fn write_service_configuration(
path: &PathBuf,
ctx: &ServiceInstallCtx,
config: &WinSwConfig,
) -> io::Result<()> {
let mut file = File::create(path).unwrap();
if let Some(contents) = &ctx.contents {
if Self::is_valid_xml(contents) {
file.write_all(contents.as_bytes())?;
return Ok(());
}
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"The contents override was not a valid XML document",
));
}
let file = BufWriter::new(file);
let mut writer = EmitterConfig::new()
.perform_indent(true)
.create_writer(file);
writer
.write(XmlEvent::StartDocument {
version: XmlVersion::Version10,
encoding: Some("UTF-8"),
standalone: None,
})
.map_err(|e| {
io::Error::new(
io::ErrorKind::Other,
format!("Writing service config failed: {}", e),
)
})?;
writer
.write(XmlEvent::start_element("service"))
.map_err(|e| {
io::Error::new(
io::ErrorKind::Other,
format!("Writing service config failed: {}", e),
)
})?;
Self::write_element(&mut writer, "id", &ctx.label.to_qualified_name())?;
Self::write_element(&mut writer, "name", &ctx.label.to_qualified_name())?;
Self::write_element(&mut writer, "executable", &ctx.program.to_string_lossy())?;
Self::write_element(
&mut writer,
"description",
&format!("Service for {}", ctx.label.to_qualified_name()),
)?;
let args = ctx
.args
.clone()
.into_iter()
.map(|s| s.into_string().unwrap_or_default())
.collect::<Vec<String>>()
.join(" ");
Self::write_element(&mut writer, "arguments", &args)?;
if let Some(working_directory) = &ctx.working_directory {
Self::write_element(
&mut writer,
"workingdirectory",
&working_directory.to_string_lossy(),
)?;
}
if let Some(env_vars) = &ctx.environment {
for var in env_vars.iter() {
Self::write_element_with_attributes(
&mut writer,
"env",
&[("name", &var.0), ("value", &var.1)],
None,
)?;
}
}
let (action, delay) = match &config.install.failure_action {
WinSwOnFailureAction::Restart(delay) => ("restart", delay.as_deref()),
WinSwOnFailureAction::Reboot => ("reboot", None),
WinSwOnFailureAction::None => ("none", None),
};
let attributes = delay.map_or_else(
|| vec![("action", action)],
|d| vec![("action", action), ("delay", d)],
);
Self::write_element_with_attributes(&mut writer, "onfailure", &attributes, None)?;
if let Some(reset_time) = &config.install.reset_failure_time {
Self::write_element(&mut writer, "resetfailure", reset_time)?;
}
if let Some(security_descriptor) = &config.install.security_descriptor {
Self::write_element(&mut writer, "securityDescriptor", security_descriptor)?;
}
if let Some(priority) = &config.options.priority {
Self::write_element(&mut writer, "priority", &format!("{:?}", priority))?;
}
if let Some(stop_timeout) = &config.options.stop_timeout {
Self::write_element(&mut writer, "stoptimeout", stop_timeout)?;
}
if let Some(stop_executable) = &config.options.stop_executable {
Self::write_element(
&mut writer,
"stopexecutable",
&stop_executable.to_string_lossy(),
)?;
}
if let Some(stop_args) = &config.options.stop_args {
let stop_args = stop_args
.iter()
.map(|s| s.to_string_lossy().into_owned())
.collect::<Vec<String>>()
.join(" ");
Self::write_element(&mut writer, "stoparguments", &stop_args)?;
}
if let Some(start_mode) = &config.options.start_mode {
Self::write_element(&mut writer, "startmode", &format!("{:?}", start_mode))?;
}
if let Some(delayed_autostart) = config.options.delayed_autostart {
Self::write_element(
&mut writer,
"delayedAutoStart",
&delayed_autostart.to_string(),
)?;
}
if let Some(dependent_services) = &config.options.dependent_services {
for service in dependent_services {
Self::write_element(&mut writer, "depend", service)?;
}
}
if let Some(interactive) = config.options.interactive {
Self::write_element(&mut writer, "interactive", &interactive.to_string())?;
}
if let Some(beep_on_shutdown) = config.options.beep_on_shutdown {
Self::write_element(&mut writer, "beeponshutdown", &beep_on_shutdown.to_string())?;
}
writer.write(XmlEvent::end_element()).map_err(|e| {
io::Error::new(
io::ErrorKind::Other,
format!("Writing service config failed: {}", e),
)
})?;
Ok(())
}
fn write_element<W: Write>(
writer: &mut EventWriter<W>,
name: &str,
value: &str,
) -> io::Result<()> {
writer.write(XmlEvent::start_element(name)).map_err(|e| {
io::Error::new(
io::ErrorKind::Other,
format!("Failed to write element '{}': {}", name, e),
)
})?;
writer.write(XmlEvent::characters(value)).map_err(|e| {
io::Error::new(
io::ErrorKind::Other,
format!("Failed to write value for element '{}': {}", name, e),
)
})?;
writer.write(XmlEvent::end_element()).map_err(|e| {
io::Error::new(
io::ErrorKind::Other,
format!("Failed to end element '{}': {}", name, e),
)
})?;
Ok(())
}
fn write_element_with_attributes<W: Write>(
writer: &mut EventWriter<W>,
name: &str,
attributes: &[(&str, &str)],
value: Option<&str>,
) -> io::Result<()> {
let mut start_element = XmlEvent::start_element(name);
for &(attr_name, attr_value) in attributes {
start_element = start_element.attr(attr_name, attr_value);
}
writer.write(start_element).map_err(|e| {
io::Error::new(
io::ErrorKind::Other,
format!("Failed to write value for element '{}': {}", name, e),
)
})?;
if let Some(val) = value {
writer.write(XmlEvent::characters(val)).map_err(|e| {
io::Error::new(
io::ErrorKind::Other,
format!("Failed to write value for element '{}': {}", name, e),
)
})?;
}
writer.write(XmlEvent::end_element()).map_err(|e| {
io::Error::new(
io::ErrorKind::Other,
format!("Failed to end element '{}': {}", name, e),
)
})?;
Ok(())
}
fn is_valid_xml(xml_string: &str) -> bool {
let cursor = Cursor::new(xml_string);
let parser = EventReader::new(cursor);
for e in parser {
if e.is_err() {
return false;
}
}
true
}
}
impl ServiceManager for WinSwServiceManager {
fn available(&self) -> io::Result<bool> {
match which::which(WINSW_EXE) {
Ok(_) => Ok(true),
Err(which::Error::CannotFindBinaryPath) => Ok(false),
Err(x) => Err(io::Error::new(io::ErrorKind::Other, x)),
}
}
fn install(&self, ctx: ServiceInstallCtx) -> io::Result<()> {
let service_name = ctx.label.to_qualified_name();
let service_instance_path = self
.config
.service_definition_dir_path
.join(service_name.clone());
std::fs::create_dir_all(&service_instance_path)?;
let service_config_path = service_instance_path.join(format!("{service_name}.xml"));
Self::write_service_configuration(&service_config_path, &ctx, &self.config)?;
winsw_exe("install", &service_name, service_instance_path)
}
fn uninstall(&self, ctx: ServiceUninstallCtx) -> io::Result<()> {
let service_name = ctx.label.to_qualified_name();
let service_instance_path = self
.config
.service_definition_dir_path
.join(service_name.clone());
winsw_exe("uninstall", &service_name, service_instance_path)
}
fn start(&self, ctx: ServiceStartCtx) -> io::Result<()> {
let service_name = ctx.label.to_qualified_name();
let service_instance_path = self
.config
.service_definition_dir_path
.join(service_name.clone());
winsw_exe("start", &service_name, service_instance_path)
}
fn stop(&self, ctx: ServiceStopCtx) -> io::Result<()> {
let service_name = ctx.label.to_qualified_name();
let service_instance_path = self
.config
.service_definition_dir_path
.join(service_name.clone());
winsw_exe("stop", &service_name, service_instance_path)
}
fn level(&self) -> ServiceLevel {
ServiceLevel::System
}
fn set_level(&mut self, level: ServiceLevel) -> io::Result<()> {
match level {
ServiceLevel::System => Ok(()),
ServiceLevel::User => Err(io::Error::new(
io::ErrorKind::Unsupported,
"Windows does not support user-level services",
)),
}
}
}
fn winsw_exe(cmd: &str, service_name: &str, working_dir_path: PathBuf) -> io::Result<()> {
let mut command = Command::new(WINSW_EXE);
command
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
command.current_dir(working_dir_path);
command.arg(cmd).arg(&format!("{}.xml", service_name));
let output = command.output()?;
if output.status.success() {
Ok(())
} else {
let msg = String::from_utf8(output.stderr)
.ok()
.filter(|s| !s.trim().is_empty())
.or_else(|| {
String::from_utf8(output.stdout)
.ok()
.filter(|s| !s.trim().is_empty())
})
.unwrap_or_else(|| format!("Failed to {cmd}"));
Err(io::Error::new(io::ErrorKind::Other, msg))
}
}
#[cfg(test)]
mod tests {
use super::*;
use assert_fs::prelude::*;
use indoc::indoc;
use std::ffi::OsString;
use std::io::Cursor;
use xml::reader::{EventReader, XmlEvent};
fn get_element_value(xml: &str, element_name: &str) -> String {
let cursor = Cursor::new(xml);
let parser = EventReader::new(cursor);
let mut inside_target_element = false;
for e in parser {
match e {
Ok(XmlEvent::StartElement { name, .. }) if name.local_name == element_name => {
inside_target_element = true;
}
Ok(XmlEvent::Characters(text)) if inside_target_element => {
return text;
}
Ok(XmlEvent::EndElement { name }) if name.local_name == element_name => {
inside_target_element = false;
}
Err(e) => panic!("Error while parsing XML: {}", e),
_ => {}
}
}
panic!("Element {} not found", element_name);
}
fn get_element_attribute_value(xml: &str, element_name: &str, attribute_name: &str) -> String {
let cursor = Cursor::new(xml);
let parser = EventReader::new(cursor);
for e in parser {
match e {
Ok(XmlEvent::StartElement {
name, attributes, ..
}) if name.local_name == element_name => {
for attr in attributes {
if attr.name.local_name == attribute_name {
return attr.value;
}
}
}
Err(e) => panic!("Error while parsing XML: {}", e),
_ => {}
}
}
panic!("Attribute {} not found", attribute_name);
}
fn get_element_values(xml: &str, element_name: &str) -> Vec<String> {
let cursor = Cursor::new(xml);
let parser = EventReader::new(cursor);
let mut values = Vec::new();
let mut inside_target_element = false;
for e in parser {
match e {
Ok(XmlEvent::StartElement { name, .. }) if name.local_name == element_name => {
inside_target_element = true;
}
Ok(XmlEvent::Characters(text)) if inside_target_element => {
values.push(text);
}
Ok(XmlEvent::EndElement { name }) if name.local_name == element_name => {
inside_target_element = false;
}
Err(e) => panic!("Error while parsing XML: {}", e),
_ => {}
}
}
values
}
fn get_environment_variables(xml: &str) -> Vec<(String, String)> {
let cursor = Cursor::new(xml);
let parser = EventReader::new(cursor);
let mut env_vars = Vec::new();
for e in parser.into_iter().flatten() {
if let XmlEvent::StartElement {
name, attributes, ..
} = e
{
if name.local_name == "env" {
let mut name_value_pair = (String::new(), String::new());
for attr in attributes {
match attr.name.local_name.as_str() {
"name" => name_value_pair.0 = attr.value,
"value" => name_value_pair.1 = attr.value,
_ => {}
}
}
if !name_value_pair.0.is_empty() && !name_value_pair.1.is_empty() {
env_vars.push(name_value_pair);
}
}
}
}
env_vars
}
#[test]
fn test_service_configuration_with_mandatory_elements() {
let temp_dir = assert_fs::TempDir::new().unwrap();
let service_config_file = temp_dir.child("service_config.xml");
let ctx = ServiceInstallCtx {
label: "org.example.my_service".parse().unwrap(),
program: PathBuf::from("C:\\Program Files\\org.example\\my_service.exe"),
args: vec![
OsString::from("--arg"),
OsString::from("value"),
OsString::from("--another-arg"),
],
contents: None,
username: None,
working_directory: None,
environment: None,
};
WinSwServiceManager::write_service_configuration(
&service_config_file.to_path_buf(),
&ctx,
&WinSwConfig::default(),
)
.unwrap();
let xml = std::fs::read_to_string(service_config_file.path()).unwrap();
service_config_file.assert(predicates::path::is_file());
assert_eq!("org.example.my_service", get_element_value(&xml, "id"));
assert_eq!("org.example.my_service", get_element_value(&xml, "name"));
assert_eq!(
"C:\\Program Files\\org.example\\my_service.exe",
get_element_value(&xml, "executable")
);
assert_eq!(
"Service for org.example.my_service",
get_element_value(&xml, "description")
);
assert_eq!(
"--arg value --another-arg",
get_element_value(&xml, "arguments")
);
}
#[test]
fn test_service_configuration_with_full_options() {
let temp_dir = assert_fs::TempDir::new().unwrap();
let service_config_file = temp_dir.child("service_config.xml");
let ctx = ServiceInstallCtx {
label: "org.example.my_service".parse().unwrap(),
program: PathBuf::from("C:\\Program Files\\org.example\\my_service.exe"),
args: vec![
OsString::from("--arg"),
OsString::from("value"),
OsString::from("--another-arg"),
],
contents: None,
username: None,
working_directory: Some(PathBuf::from("C:\\Program Files\\org.example")),
environment: Some(vec![
("ENV1".to_string(), "val1".to_string()),
("ENV2".to_string(), "val2".to_string()),
]),
};
let config = WinSwConfig {
install: WinSwInstallConfig {
failure_action: WinSwOnFailureAction::Restart(Some("10 sec".to_string())),
reset_failure_time: Some("1 hour".to_string()),
security_descriptor: Some(
"O:AOG:DAD:(A;;RPWPCCDCLCSWRCWDWOGA;;;S-1-0-0)".to_string(),
),
},
options: WinSwOptionsConfig {
priority: Some(WinSwPriority::High),
stop_timeout: Some("15 sec".to_string()),
stop_executable: Some(PathBuf::from("C:\\Temp\\stop.exe")),
stop_args: Some(vec![
OsString::from("--stop-arg1"),
OsString::from("arg1val"),
OsString::from("--stop-arg2-flag"),
]),
start_mode: Some(WinSwStartType::Manual),
delayed_autostart: Some(true),
dependent_services: Some(vec!["service1".to_string(), "service2".to_string()]),
interactive: Some(true),
beep_on_shutdown: Some(true),
},
service_definition_dir_path: PathBuf::from("C:\\Temp\\service-definitions"),
};
WinSwServiceManager::write_service_configuration(
&service_config_file.to_path_buf(),
&ctx,
&config,
)
.unwrap();
let xml = std::fs::read_to_string(service_config_file.path()).unwrap();
println!("{xml}");
service_config_file.assert(predicates::path::is_file());
assert_eq!("org.example.my_service", get_element_value(&xml, "id"));
assert_eq!("org.example.my_service", get_element_value(&xml, "name"));
assert_eq!(
"C:\\Program Files\\org.example\\my_service.exe",
get_element_value(&xml, "executable")
);
assert_eq!(
"Service for org.example.my_service",
get_element_value(&xml, "description")
);
assert_eq!(
"--arg value --another-arg",
get_element_value(&xml, "arguments")
);
assert_eq!(
"C:\\Program Files\\org.example",
get_element_value(&xml, "workingdirectory")
);
let attributes = get_environment_variables(&xml);
assert_eq!(attributes[0].0, "ENV1");
assert_eq!(attributes[0].1, "val1");
assert_eq!(attributes[1].0, "ENV2");
assert_eq!(attributes[1].1, "val2");
assert_eq!(
"restart",
get_element_attribute_value(&xml, "onfailure", "action")
);
assert_eq!(
"10 sec",
get_element_attribute_value(&xml, "onfailure", "delay")
);
assert_eq!("1 hour", get_element_value(&xml, "resetfailure"));
assert_eq!(
"O:AOG:DAD:(A;;RPWPCCDCLCSWRCWDWOGA;;;S-1-0-0)",
get_element_value(&xml, "securityDescriptor")
);
assert_eq!("High", get_element_value(&xml, "priority"));
assert_eq!("15 sec", get_element_value(&xml, "stoptimeout"));
assert_eq!(
"C:\\Temp\\stop.exe",
get_element_value(&xml, "stopexecutable")
);
assert_eq!(
"--stop-arg1 arg1val --stop-arg2-flag",
get_element_value(&xml, "stoparguments")
);
assert_eq!("Manual", get_element_value(&xml, "startmode"));
assert_eq!("true", get_element_value(&xml, "delayedAutoStart"));
let dependent_services = get_element_values(&xml, "depend");
assert_eq!("service1", dependent_services[0]);
assert_eq!("service2", dependent_services[1]);
assert_eq!("true", get_element_value(&xml, "interactive"));
assert_eq!("true", get_element_value(&xml, "beeponshutdown"));
}
#[test]
fn test_service_configuration_with_contents() {
let temp_dir = assert_fs::TempDir::new().unwrap();
let service_config_file = temp_dir.child("service_config.xml");
let contents = indoc! {r#"
<service>
<id>jenkins</id>
<name>Jenkins</name>
<description>This service runs Jenkins continuous integration system.</description>
<executable>java</executable>
<arguments>-Xrs -Xmx256m -jar "%BASE%\jenkins.war" --httpPort=8080</arguments>
</service>
"#};
let ctx = ServiceInstallCtx {
label: "org.example.my_service".parse().unwrap(),
program: PathBuf::from("C:\\Program Files\\org.example\\my_service.exe"),
args: vec![
OsString::from("--arg"),
OsString::from("value"),
OsString::from("--another-arg"),
],
contents: Some(contents.to_string()),
username: None,
working_directory: None,
environment: None,
};
WinSwServiceManager::write_service_configuration(
&service_config_file.to_path_buf(),
&ctx,
&WinSwConfig::default(),
)
.unwrap();
let xml = std::fs::read_to_string(service_config_file.path()).unwrap();
service_config_file.assert(predicates::path::is_file());
assert_eq!("jenkins", get_element_value(&xml, "id"));
assert_eq!("Jenkins", get_element_value(&xml, "name"));
assert_eq!("java", get_element_value(&xml, "executable"));
assert_eq!(
"This service runs Jenkins continuous integration system.",
get_element_value(&xml, "description")
);
assert_eq!(
"-Xrs -Xmx256m -jar \"%BASE%\\jenkins.war\" --httpPort=8080",
get_element_value(&xml, "arguments")
);
}
#[test]
fn test_service_configuration_with_invalid_contents() {
let temp_dir = assert_fs::TempDir::new().unwrap();
let service_config_file = temp_dir.child("service_config.xml");
let ctx = ServiceInstallCtx {
label: "org.example.my_service".parse().unwrap(),
program: PathBuf::from("C:\\Program Files\\org.example\\my_service.exe"),
args: vec![
OsString::from("--arg"),
OsString::from("value"),
OsString::from("--another-arg"),
],
contents: Some("this is not an XML document".to_string()),
username: None,
working_directory: None,
environment: None,
};
let result = WinSwServiceManager::write_service_configuration(
&service_config_file.to_path_buf(),
&ctx,
&WinSwConfig::default(),
);
match result {
Ok(()) => panic!("This test should result in a failure"),
Err(e) => assert_eq!(
"The contents override was not a valid XML document",
e.to_string()
),
}
}
}