Skip to content

Commit 763b82e

Browse files
committed
feat!: provide generic RestartPolicy enum
I found myself in a situation where I need to use the 'always' and 'on success' restart policies on Linux Systemd and macOS Launchd. It was not possible to do this while using the generic `ServiceManager` trait; I would have to use specific service managers, which would necessitate the introduction of conditional compilation. Now we introduce a generic `RestartPolicy` enum to work with the generic `ServiceManager` trait. This should cover the most important cases supported by most service managers: 'never', 'always', and 'on failure', plus the option for 'on success'. Users can still instantiate a particular service manager if they want to use specific restart options that are only supported by that manager. In some cases these variants don't map directly to some service manager options, so an approximation is used and a warning is emitted; however, it should still be very acceptable to configure the service in this way since the behaviour should not be too surprising. See the changelog entry for more details. BREAKING CHANGE: the `disable_restart_on_failure` field was removed in favour of the restart policy and the fields on specific service managers were made optional.
1 parent 0294d3b commit 763b82e

File tree

11 files changed

+368
-57
lines changed

11 files changed

+368
-57
lines changed

CHANGELOG.md

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,46 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
## [Unreleased]
8+
## [0.9.0] - 2025-11-22
9+
10+
### Changed
11+
12+
- **BREAKING CHANGE**: Replaced `disable_restart_on_failure: bool` field with `restart_policy: RestartPolicy` in `ServiceInstallCtx`.
13+
- The new `RestartPolicy` enum provides a cross-platform abstraction for service-restart behavior with four variants:
14+
- `RestartPolicy::Never` - Service never restarts
15+
- `RestartPolicy::Always { delay_secs: Option<u32> }` - Service always restarts regardless of exit status
16+
- `RestartPolicy::OnFailure { delay_secs: Option<u32> }` - Service restarts only on non-zero exit (default)
17+
- `RestartPolicy::OnSuccess { delay_secs: Option<u32> }` - Service restarts only on successful exit (exit code 0)
18+
- Different platforms support different levels of granularity:
19+
- **systemd** (Linux): Supports all restart policies natively (including `OnSuccess` via `Restart=on-success`)
20+
- **launchd** (macOS): Supports Never, Always, and OnSuccess; OnFailure is approximated using `KeepAlive=true`; OnSuccess uses `KeepAlive` dictionary with `SuccessfulExit=false`
21+
- **WinSW** (Windows): Supports Never, Always, and OnFailure with optional delays; OnSuccess falls back to Always with a warning
22+
- **OpenRC/rc.d/sc.exe**: Limited or no restart support; logs warnings for unsupported policies
23+
- Migration guide for `ServiceInstallCtx`:
24+
- `disable_restart_on_failure: false``restart_policy: RestartPolicy::OnFailure { delay_secs: None }`
25+
- `disable_restart_on_failure: true``restart_policy: RestartPolicy::Never`
26+
27+
- **BREAKING CHANGE**: Platform-specific restart configuration fields are now `Option` types,
28+
allowing the generic `RestartPolicy` to be used by default while still supporting platform-specific
29+
features when needed:
30+
- `SystemdInstallConfig.restart`: Changed from `SystemdServiceRestartType` to `Option<SystemdServiceRestartType>`
31+
- When `Some`, the systemd-specific restart type takes precedence over the generic `RestartPolicy`
32+
- When `None` (default), falls back to the generic `RestartPolicy`
33+
- Migration: `restart: SystemdServiceRestartType::OnFailure`
34+
`restart: Some(SystemdServiceRestartType::OnFailure)` or `restart: None` to use generic policy
35+
- `LaunchdInstallConfig.keep_alive`: Changed from `bool` to `Option<bool>`
36+
- When `Some`, the launchd-specific keep-alive setting takes precedence
37+
- When `None` (default), falls back to the generic `RestartPolicy`
38+
- Migration: `keep_alive: true``keep_alive: Some(true)` or `keep_alive: None` to use generic policy
39+
- `WinSwInstallConfig.failure_action`: Changed from `WinSwOnFailureAction` to `Option<WinSwOnFailureAction>`
40+
- When `Some`, the WinSW-specific failure action takes precedence
41+
- When `None` (default), falls back to the generic `RestartPolicy`
42+
- Migration: `failure_action: WinSwOnFailureAction::Restart(...)`
43+
`failure_action: Some(WinSwOnFailureAction::Restart(...))` or `failure_action: None` to use generic policy
44+
45+
### Added
46+
47+
- Support for the `log` crate to emit warnings when platform-specific restart features are not supported
948

1049
## [0.8.0] - 2025-02-21
1150

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ members = ["system-tests"]
2525
cfg-if = "1.0"
2626
clap = { version = "4", features = ["derive"], optional = true }
2727
dirs = "4.0"
28+
log = "0.4"
2829
plist = "1.1"
2930
serde = { version = "1", features = ["derive"], optional = true }
3031
which = "4.0"

README.md

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ manager.install(ServiceInstallCtx {
6969
working_directory: None, // Optional String for the working directory for the service process.
7070
environment: None, // Optional list of environment variables to supply the service process.
7171
autostart: true, // Specify whether the service should automatically start upon OS reboot.
72-
disable_restart_on_failure: false, // Services restart on crash by default.
72+
restart_policy: RestartPolicy::default(), // Restart on failure by default.
7373
}).expect("Failed to install");
7474
7575
// Start our service using the underlying service management platform
@@ -133,7 +133,7 @@ let mut manager = LaunchdServiceManager::system();
133133
134134
// Update an install configuration property where installing a service
135135
// will NOT add the KeepAlive flag
136-
manager.config.install.keep_alive = false;
136+
manager.config.install.keep_alive = Some(false);
137137
138138
// Install our service using the explicit service manager
139139
manager.install(ServiceInstallCtx {
@@ -145,10 +145,75 @@ manager.install(ServiceInstallCtx {
145145
working_directory: None, // Optional String for the working directory for the service process.
146146
environment: None, // Optional list of environment variables to supply the service process.
147147
autostart: true, // Specify whether the service should automatically start upon OS reboot.
148-
disable_restart_on_failure: false, // Services restart on crash by default.
148+
restart_policy: RestartPolicy::default(), // Restart on failure by default.
149149
}).expect("Failed to install");
150150
```
151151

152+
### Configuring restart policies
153+
154+
The crate provides a cross-platform `RestartPolicy` enum that allows you to control
155+
when and how services should be restarted. Different platforms support different levels
156+
of granularity, and the implementation will use the closest approximation when an exact
157+
match isn't available.
158+
159+
If you need options specific to any given service manager, you should use that specific
160+
service manager rather than the generic `ServiceManager` crate.
161+
162+
```rust,no_run
163+
use service_manager::*;
164+
use std::ffi::OsString;
165+
use std::path::PathBuf;
166+
167+
let label: ServiceLabel = "com.example.my-service".parse().unwrap();
168+
let manager = <dyn ServiceManager>::native()
169+
.expect("Failed to detect management platform");
170+
171+
// Example 1: Never restart the service
172+
manager.install(ServiceInstallCtx {
173+
label: label.clone(),
174+
program: PathBuf::from("path/to/my-service-executable"),
175+
args: vec![OsString::from("--some-arg")],
176+
contents: None,
177+
username: None,
178+
working_directory: None,
179+
environment: None,
180+
autostart: true,
181+
restart_policy: RestartPolicy::Never,
182+
}).expect("Failed to install");
183+
184+
// Example 2: Always restart regardless of exit status
185+
manager.install(ServiceInstallCtx {
186+
label: label.clone(),
187+
program: PathBuf::from("path/to/my-service-executable"),
188+
args: vec![OsString::from("--some-arg")],
189+
contents: None,
190+
username: None,
191+
working_directory: None,
192+
environment: None,
193+
autostart: true,
194+
restart_policy: RestartPolicy::Always { delay_secs: Some(10) },
195+
}).expect("Failed to install");
196+
197+
// Example 3: Restart only on failure (non-zero exit)
198+
manager.install(ServiceInstallCtx {
199+
label: label.clone(),
200+
program: PathBuf::from("path/to/my-service-executable"),
201+
args: vec![OsString::from("--some-arg")],
202+
contents: None,
203+
username: None,
204+
working_directory: None,
205+
environment: None,
206+
autostart: true,
207+
restart_policy: RestartPolicy::OnFailure { delay_secs: Some(5) },
208+
}).expect("Failed to install");
209+
```
210+
211+
**Platform support:**
212+
- **systemd (Linux)**: Supports all restart policies natively
213+
- **launchd (macOS)**: Only supports Never vs Always/OnFailure (uses KeepAlive boolean)
214+
- **WinSW (Windows)**: Supports all restart policies
215+
- **OpenRC/rc.d/sc.exe**: Limited or no restart support; warnings logged for unsupported policies
216+
152217
### Running tests
153218

154219
For testing purposes, we use a separate crate called `system-tests` and

src/launchd.rs

Lines changed: 65 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use crate::utils::wrap_output;
22

33
use super::{
4-
utils, ServiceInstallCtx, ServiceLevel, ServiceManager, ServiceStartCtx, ServiceStopCtx,
5-
ServiceUninstallCtx,
4+
utils, RestartPolicy, ServiceInstallCtx, ServiceLevel, ServiceManager, ServiceStartCtx,
5+
ServiceStopCtx, ServiceUninstallCtx,
66
};
77
use plist::{Dictionary, Value};
88
use std::{
@@ -25,13 +25,14 @@ pub struct LaunchdConfig {
2525
/// Configuration settings tied to launchd services during installation
2626
#[derive(Clone, Debug, PartialEq, Eq)]
2727
pub struct LaunchdInstallConfig {
28-
/// If true, will include `KeepAlive` flag set to true
29-
pub keep_alive: bool,
28+
/// Launchd-specific keep-alive setting. If `Some`, this takes precedence over the generic
29+
/// `RestartPolicy` in `ServiceInstallCtx`. If `None`, the generic policy is used.
30+
pub keep_alive: Option<bool>,
3031
}
3132

3233
impl Default for LaunchdInstallConfig {
3334
fn default() -> Self {
34-
Self { keep_alive: true }
35+
Self { keep_alive: None }
3536
}
3637
}
3738

@@ -121,7 +122,7 @@ impl ServiceManager for LaunchdServiceManager {
121122
ctx.working_directory.clone(),
122123
ctx.environment.clone(),
123124
ctx.autostart,
124-
ctx.disable_restart_on_failure
125+
ctx.restart_policy,
125126
),
126127
};
127128

@@ -276,7 +277,7 @@ fn make_plist<'a>(
276277
working_directory: Option<PathBuf>,
277278
environment: Option<Vec<(String, String)>>,
278279
autostart: bool,
279-
disable_restart_on_failure: bool,
280+
restart_policy: RestartPolicy,
280281
) -> String {
281282
let mut dict = Dictionary::new();
282283

@@ -290,8 +291,63 @@ fn make_plist<'a>(
290291
Value::Array(program_arguments),
291292
);
292293

293-
if !disable_restart_on_failure {
294-
dict.insert("KeepAlive".to_string(), Value::Boolean(config.keep_alive));
294+
// Handle restart configuration
295+
// Priority: launchd-specific config > generic RestartPolicy
296+
if let Some(keep_alive) = config.keep_alive {
297+
// Use launchd-specific keep_alive configuration
298+
if keep_alive {
299+
dict.insert("KeepAlive".to_string(), Value::Boolean(true));
300+
}
301+
} else {
302+
// Fall back to generic RestartPolicy
303+
// Convert generic `RestartPolicy` to Launchd `KeepAlive`.
304+
//
305+
// Right now we are only supporting binary restart for Launchd, with `KeepAlive` either set or
306+
// not.
307+
//
308+
// However, Launchd does support further options when `KeepAlive` is set, e.g.,
309+
// `SuccessfulExit`. These could be extensions for the future.
310+
match restart_policy {
311+
RestartPolicy::Never => {
312+
// Don't set KeepAlive
313+
}
314+
RestartPolicy::Always { delay_secs } => {
315+
dict.insert("KeepAlive".to_string(), Value::Boolean(true));
316+
if delay_secs.is_some() {
317+
log::warn!(
318+
"Launchd does not support restart delays; delay_secs will be ignored for service '{}'",
319+
label
320+
);
321+
}
322+
}
323+
RestartPolicy::OnFailure { delay_secs } => {
324+
dict.insert("KeepAlive".to_string(), Value::Boolean(true));
325+
log::warn!(
326+
"Right now we don't have more granular restart support for Launchd so the service will always restart; using KeepAlive=true for service '{}'",
327+
label
328+
);
329+
if delay_secs.is_some() {
330+
log::warn!(
331+
"Launchd does not support restart delays; delay_secs will be ignored for service '{}'",
332+
label
333+
);
334+
}
335+
}
336+
RestartPolicy::OnSuccess { delay_secs } => {
337+
// Create KeepAlive dictionary with SuccessfulExit=false
338+
// This means: restart when exit is successful (exit code 0)
339+
let mut keep_alive_dict = Dictionary::new();
340+
keep_alive_dict.insert("SuccessfulExit".to_string(), Value::Boolean(false));
341+
dict.insert("KeepAlive".to_string(), Value::Dictionary(keep_alive_dict));
342+
343+
if delay_secs.is_some() {
344+
log::warn!(
345+
"Launchd does not support restart delays; delay_secs will be ignored for service '{}'",
346+
label
347+
);
348+
}
349+
}
350+
}
295351
}
296352

297353
if let Some(username) = username {

src/lib.rs

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,62 @@ pub enum ServiceLevel {
111111
User,
112112
}
113113

114+
/// Represents the restart policy for a service.
115+
///
116+
/// This enum provides a cross-platform abstraction for service restart behavior with a set of
117+
/// simple options that cover most service managers.
118+
///
119+
/// For most service cases you likely want a restart-on-failure policy, so this is the default.
120+
///
121+
/// Each service manager supports different levels of granularity:
122+
///
123+
/// - **Systemd** (Linux): supports all variants natively
124+
/// - **Launchd** (macOS): supports Never, Always, OnFailure (approximated), and OnSuccess via KeepAlive dictionary
125+
/// - **WinSW** (Windows): supports Never, Always, and OnFailure; OnSuccess falls back to Always with a warning
126+
/// - **OpenRC/rc.d/sc.exe**: limited or no restart support as of yet
127+
///
128+
/// When a platform doesn't support a specific policy, the implementation will fall back
129+
/// to the closest approximation and log a warning.
130+
///
131+
/// In the case where you need a restart policy that is very specific to a particular service
132+
/// manager, you should instantiate that service manager directly, rather than using the generic
133+
/// `ServiceManager` trait.
134+
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
135+
pub enum RestartPolicy {
136+
/// Never restart the service
137+
Never,
138+
139+
/// Always restart the service regardless of exit status.
140+
///
141+
/// The optional delay specifies seconds to wait before restarting.
142+
Always {
143+
/// Delay in seconds before restarting
144+
delay_secs: Option<u32>,
145+
},
146+
147+
/// Restart the service only when it exits with a non-zero status.
148+
///
149+
/// The optional delay specifies seconds to wait before restarting.
150+
OnFailure {
151+
/// Delay in seconds before restarting
152+
delay_secs: Option<u32>,
153+
},
154+
155+
/// Restart the service only when it exits with a zero status (success).
156+
///
157+
/// The optional delay specifies seconds to wait before restarting.
158+
OnSuccess {
159+
/// Delay in seconds before restarting
160+
delay_secs: Option<u32>,
161+
},
162+
}
163+
164+
impl Default for RestartPolicy {
165+
fn default() -> Self {
166+
RestartPolicy::OnFailure { delay_secs: None }
167+
}
168+
}
169+
114170
/// Represents the status of a service
115171
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
116172
pub enum ServiceStatus {
@@ -245,10 +301,14 @@ pub struct ServiceInstallCtx {
245301
/// Specify whether the service should automatically start on reboot
246302
pub autostart: bool,
247303

248-
/// Optionally disable a service from restarting when it exits with a failure
304+
/// Specify the restart policy for the service
305+
///
306+
/// This controls when and how the service should be restarted if it exits.
307+
/// Different platforms support different levels of granularity - see [`RestartPolicy`]
308+
/// documentation for details.
249309
///
250-
/// This could overwrite the platform specific service manager config.
251-
pub disable_restart_on_failure: bool,
310+
/// Defaults to [`RestartPolicy::OnFailure`] if not specified.
311+
pub restart_policy: RestartPolicy,
252312
}
253313

254314
impl ServiceInstallCtx {

src/openrc.rs

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use crate::utils::wrap_output;
22

33
use super::{
4-
utils, ServiceInstallCtx, ServiceLevel, ServiceManager, ServiceStartCtx, ServiceStopCtx,
5-
ServiceUninstallCtx,
4+
utils, RestartPolicy, ServiceInstallCtx, ServiceLevel, ServiceManager, ServiceStartCtx,
5+
ServiceStopCtx, ServiceUninstallCtx,
66
};
77
use std::{
88
ffi::{OsStr, OsString},
@@ -50,6 +50,20 @@ impl ServiceManager for OpenRcServiceManager {
5050
}
5151

5252
fn install(&self, ctx: ServiceInstallCtx) -> io::Result<()> {
53+
// OpenRC doesn't support restart policies in the basic implementation.
54+
// Log a warning if user requested anything other than `Never`.
55+
match ctx.restart_policy {
56+
RestartPolicy::Never => {
57+
// This is fine, OpenRC services don't restart by default
58+
}
59+
RestartPolicy::Always { .. } | RestartPolicy::OnFailure { .. } | RestartPolicy::OnSuccess { .. } => {
60+
log::warn!(
61+
"OpenRC does not support automatic restart policies; service '{}' will not restart automatically",
62+
ctx.label.to_script_name()
63+
);
64+
}
65+
}
66+
5367
let dir_path = service_dir_path();
5468
std::fs::create_dir_all(&dir_path)?;
5569

@@ -84,11 +98,7 @@ impl ServiceManager for OpenRcServiceManager {
8498

8599
fn uninstall(&self, ctx: ServiceUninstallCtx) -> io::Result<()> {
86100
// If the script is configured to run at boot, remove it
87-
let _ = rc_update(
88-
"del",
89-
&ctx.label.to_script_name(),
90-
[OsStr::new("default")],
91-
);
101+
let _ = rc_update("del", &ctx.label.to_script_name(), [OsStr::new("default")]);
92102

93103
// Uninstall service by removing the script
94104
std::fs::remove_file(service_dir_path().join(&ctx.label.to_script_name()))

0 commit comments

Comments
 (0)