Skip to content

Commit

Permalink
Introduce autostart field on ServiceInstallCtx (#25)
Browse files Browse the repository at this point in the history
* Obtain WinSW location from environment variable

If WinSW is not at any of the locations specified by the system or user `Path` variable, check the
`WINSW_PATH` variable for a potential location.

This is useful because it enables putting the WinSW binary in a location that is not on `Path`. On
Windows, by default there are no locations on `Path` the user can write to without administrative
privileges. Callers of the crate can obtain the WinSW binary and put it in any location they wish,
then set `WINSW_PATH` in the same process, and not have to worry about their users not having the
binary in a `Path` location.

* Introduce `autostart` field on `ServiceInstallCtx`

BREAKING CHANGE: users who upgrade will need to explicitly add an `autostart` field into their
`ServiceInstallCtx` definition.

This controls whether a service should automatically start upon rebooting the OS. It's an option
common to all service managers and it's useful for developers to think about whether their services
should automatically start up, which I think justifies the breaking change. If the service is
resource intensive or uses a lot of bandwidth, some users actually don't want automatic start
because it can potentially render their machine unusable.

It could be that rc.d needs a little bit of additional work here. I am not very familiar with this
system.
  • Loading branch information
jacderida committed May 31, 2024
1 parent bc95878 commit 743a911
Show file tree
Hide file tree
Showing 10 changed files with 199 additions and 15 deletions.
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.7.0] - 2024-05-30

### Added

- The WinSW service manager can read the location of the WinSW binary from the `WINSW_PATH`
environment variable. This is useful to avoid the necessity of having it in a location that is on
the `Path` variable, which can be a bit more awkward on Windows. There are a lack of standard
locations that can be written to without administrative privileges.
- Introduce the `autostart` field on `ServiceInstallCtx`. This controls whether a service should
automatically start upon rebooting the OS. It's an option common to all service managers and it's
useful for developers to think about whether their services should automatically start up. If the
service is resource intensive or uses a lot of bandwidth, some users actually don't want automatic
start because it can potentially render their machine unusable.

## [0.6.2] - 2024-05-27

- The WinSW service manager will delete service directories upon uninstall
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ manager.install(ServiceInstallCtx {
username: None, // Optional String for alternative user to run service.
working_directory: None, // Optional String for the working directory for the service process.
environment: None, // Optional list of environment variables to supply the service process.
autostart: true, // Specify whether the service should automatically start upon OS reboot.
}).expect("Failed to install");
// Start our service using the underlying service management platform
Expand Down Expand Up @@ -142,6 +143,7 @@ manager.install(ServiceInstallCtx {
username: None, // Optional String for alternative user to run service.
working_directory: None, // Optional String for the working directory for the service process.
environment: None, // Optional list of environment variables to supply the service process.
autostart: true, // Specify whether the service should automatically start upon OS reboot.
}).expect("Failed to install");
```

Expand Down
14 changes: 13 additions & 1 deletion src/launchd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ impl ServiceManager for LaunchdServiceManager {
ctx.username.clone(),
ctx.working_directory.clone(),
ctx.environment.clone(),
ctx.autostart,
),
};

Expand All @@ -126,7 +127,11 @@ impl ServiceManager for LaunchdServiceManager {
PLIST_FILE_PERMISSIONS,
)?;

launchctl("load", plist_path.to_string_lossy().as_ref())
if ctx.autostart {
launchctl("load", plist_path.to_string_lossy().as_ref())?;
}

Ok(())
}

fn uninstall(&self, ctx: ServiceUninstallCtx) -> io::Result<()> {
Expand Down Expand Up @@ -209,6 +214,7 @@ fn make_plist<'a>(
username: Option<String>,
working_directory: Option<PathBuf>,
environment: Option<Vec<(String, String)>>,
autostart: bool,
) -> String {
let mut dict = Dictionary::new();

Expand Down Expand Up @@ -246,6 +252,12 @@ fn make_plist<'a>(
);
}

if autostart {
dict.insert("RunAtLoad".to_string(), Value::Boolean(true));
} else {
dict.insert("RunAtLoad".to_string(), Value::Boolean(false));
}

let plist = Value::Dictionary(dict);

let mut buffer = Vec::new();
Expand Down
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,9 @@ pub struct ServiceInstallCtx {
/// Optionally specify a list of environment variables to be passed to the process launched by
/// the service
pub environment: Option<Vec<(String, String)>>,

/// Specify whether the service should automatically start on reboot
pub autostart: bool,
}

impl ServiceInstallCtx {
Expand Down
12 changes: 8 additions & 4 deletions src/openrc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,14 @@ impl ServiceManager for OpenRcServiceManager {
SCRIPT_FILE_PERMISSIONS,
)?;

// Add with default run level explicitly defined to prevent weird systems
// like alpine's docker container with openrc from setting a different
// run level than default
rc_update("add", &script_name, [OsStr::new("default")])
if ctx.autostart {
// Add with default run level explicitly defined to prevent weird systems
// like alpine's docker container with openrc from setting a different
// run level than default
rc_update("add", &script_name, [OsStr::new("default")])?;
}

Ok(())
}

fn uninstall(&self, ctx: ServiceUninstallCtx) -> io::Result<()> {
Expand Down
6 changes: 5 additions & 1 deletion src/rcd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,11 @@ impl ServiceManager for RcdServiceManager {
SCRIPT_FILE_PERMISSIONS,
)?;

rc_d_script("enable", &service)
if ctx.autostart {
rc_d_script("enable", &service)?;
}

Ok(())
}

fn uninstall(&self, ctx: ServiceUninstallCtx) -> io::Result<()> {
Expand Down
9 changes: 8 additions & 1 deletion src/sc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,15 @@ impl ServiceManager for ScServiceManager {
let service_name = ctx.label.to_qualified_name();

let service_type = OsString::from(self.config.install.service_type.to_string());
let start_type = OsString::from(self.config.install.start_type.to_string());
let error_severity = OsString::from(self.config.install.error_severity.to_string());
let start_type = if ctx.autostart {
OsString::from("Auto")
} else {
// TODO: Perhaps it could be useful to make `start_type` an `Option`? That way you
// could have `Auto`/`Demand` based on `autostart`, and if `start_type` is set, its
// special value will override `autostart`.
OsString::from(self.config.install.start_type.to_string())
};

// Build our binary including arguments, following similar approach as windows-service-rs
let mut binpath = OsString::new();
Expand Down
23 changes: 17 additions & 6 deletions src/systemd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,13 @@ impl ServiceManager for SystemdServiceManager {
let script_path = dir_path.join(format!("{script_name}.service"));
let service = match ctx.contents {
Some(contents) => contents,
_ => make_service(&self.config.install, &script_name, &ctx, self.user),
_ => make_service(
&self.config.install,
&script_name,
&ctx,
self.user,
ctx.autostart,
),
};

utils::write_file(
Expand All @@ -145,7 +151,11 @@ impl ServiceManager for SystemdServiceManager {
SERVICE_FILE_PERMISSIONS,
)?;

systemctl("enable", script_path.to_string_lossy().as_ref(), self.user)
if ctx.autostart {
systemctl("enable", script_path.to_string_lossy().as_ref(), self.user)?;
}

Ok(())
}

fn uninstall(&self, ctx: ServiceUninstallCtx) -> io::Result<()> {
Expand Down Expand Up @@ -237,6 +247,7 @@ fn make_service(
description: &str,
ctx: &ServiceInstallCtx,
user: bool,
autostart: bool,
) -> String {
use std::fmt::Write as _;
let SystemdInstallConfig {
Expand Down Expand Up @@ -301,11 +312,11 @@ fn make_service(
}
}

let _ = writeln!(service, "[Install]");

if user {
if user && autostart {
let _ = writeln!(service, "[Install]");
let _ = writeln!(service, "WantedBy=default.target");
} else {
} else if autostart {
let _ = writeln!(service, "[Install]");
let _ = writeln!(service, "WantedBy=multi-user.target");
}

Expand Down
130 changes: 128 additions & 2 deletions src/winsw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,9 +231,15 @@ impl WinSwServiceManager {
.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))?;
} else if ctx.autostart {
Self::write_element(&mut writer, "startmode", "Automatic")?;
} else {
Self::write_element(&mut writer, "startmode", "Manual")?;
}

if let Some(delayed_autostart) = config.options.delayed_autostart {
Self::write_element(
&mut writer,
Expand Down Expand Up @@ -342,7 +348,13 @@ 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(which::Error::CannotFindBinaryPath) => match std::env::var("WINSW_PATH") {
Ok(val) => {
let path = PathBuf::from(val);
Ok(path.exists())
}
Err(_) => Ok(false),
},
Err(x) => Err(io::Error::new(io::ErrorKind::Other, x)),
}
}
Expand Down Expand Up @@ -411,7 +423,19 @@ impl ServiceManager for WinSwServiceManager {
}

fn winsw_exe(cmd: &str, service_name: &str, working_dir_path: &Path) -> io::Result<()> {
let mut command = Command::new(WINSW_EXE);
let winsw_path = match std::env::var("WINSW_PATH") {
Ok(val) => {
let path = PathBuf::from(val);
if path.exists() {
path
} else {
PathBuf::from(WINSW_EXE)
}
}
Err(_) => PathBuf::from(WINSW_EXE),
};

let mut command = Command::new(winsw_path);
command
.stdin(Stdio::null())
.stdout(Stdio::piped())
Expand Down Expand Up @@ -563,6 +587,54 @@ mod tests {
username: None,
working_directory: None,
environment: None,
autostart: true,
};

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")
);
assert_eq!("Automatic", get_element_value(&xml, "startmode"));
}

#[test]
fn test_service_configuration_with_autostart_false() {
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,
autostart: false,
};

WinSwServiceManager::write_service_configuration(
Expand All @@ -589,6 +661,56 @@ mod tests {
"--arg value --another-arg",
get_element_value(&xml, "arguments")
);
assert_eq!("Manual", get_element_value(&xml, "startmode"));
}

#[test]
fn test_service_configuration_with_special_start_type_should_override_autostart() {
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,
autostart: false,
};

let mut config = WinSwConfig::default();
config.options.start_mode = Some(WinSwStartType::Boot);
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();

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!("Boot", get_element_value(&xml, "startmode"));
}

#[test]
Expand All @@ -611,6 +733,7 @@ mod tests {
("ENV1".to_string(), "val1".to_string()),
("ENV2".to_string(), "val2".to_string()),
]),
autostart: true,
};

let config = WinSwConfig {
Expand Down Expand Up @@ -724,6 +847,7 @@ mod tests {
<description>This service runs Jenkins continuous integration system.</description>
<executable>java</executable>
<arguments>-Xrs -Xmx256m -jar "%BASE%\jenkins.war" --httpPort=8080</arguments>
<startmode>Automatic</startmode>
</service>
"#};
let ctx = ServiceInstallCtx {
Expand All @@ -738,6 +862,7 @@ mod tests {
username: None,
working_directory: None,
environment: None,
autostart: true,
};

WinSwServiceManager::write_service_configuration(
Expand Down Expand Up @@ -780,6 +905,7 @@ mod tests {
username: None,
working_directory: None,
environment: None,
autostart: true,
};

let result = WinSwServiceManager::write_service_configuration(
Expand Down
1 change: 1 addition & 0 deletions system-tests/tests/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ pub fn run_test(manager: &TypedServiceManager, username: Option<String>) -> Opti
username: username.clone(),
working_directory: None,
environment: None,
autostart: true,
})
.unwrap();

Expand Down

0 comments on commit 743a911

Please sign in to comment.