Skip to content

Commit

Permalink
Provide WinSW Service Manager (#13)
Browse files Browse the repository at this point in the history
* Provide the WinSW service manager

WinSW is a useful tool that can wrap any application as a Windows service; it provides the
boilerplate code for interacting with the service infrastructure on Windows. You define a
configuration for your application service in XML, then use WinSW, along with your configuration, as
a service controller.

The full specification for the service configuration can be found here:
https://github.com/winsw/winsw/blob/v3/samples/complete.xml

In this initial implementation, we provide support for most of the available elements, with the
exception of:

* serviceaccount
* startarguments (I don't know what the difference is between that and 'arguments')
* workingdirectory
* logpath and log
* env
* download
* extensions

Support for workingdirectory and env will follow in subsequent commits; these fields can be added to
the generic `ServiceInstallCtx` since they are common options on all platforms.

Most of the code here deals with building the configuration file. This code was written as a
separate function so that the different options could be unit tested. There is an integration/system
test in a similar fashion to the sc manager.

* Extend `ServiceInstallCtx` with new options

It is very common to define a working directory for a service process and a list of environment
variables to pass to the process when it runs. The `ServiceInstallCtx` is extended for this because
it applies to most service managers.

In this particular case, three of the managers have been extended to support the variables: Systemd,
Launchd and WinSW. The Windows `sc.exe` service manager does not support specifying either. The
other two unix-based service managers might, but I am not familiar with those at the present time.
It should be easy enough to extend them with a further PR.

* Update CHANGELOG for WinSW and `ServiceInstallCtx` changes
  • Loading branch information
jacderida committed Nov 5, 2023
1 parent 2dde1a5 commit bf30b30
Show file tree
Hide file tree
Showing 13 changed files with 934 additions and 35 deletions.
12 changes: 12 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ jobs:
tests:
name: "Test Rust ${{ matrix.rust }} for ${{ matrix.test }} w/ ${{ matrix.manager }} (${{ matrix.os }})"
runs-on: ${{ matrix.os }}
env:
WINSW_URL: https://github.com/winsw/winsw/releases/download/v3.0.0-alpha.11/WinSW-x64.exe
strategy:
fail-fast: false
matrix:
Expand All @@ -53,13 +55,23 @@ jobs:
- { rust: stable, os: macos-latest, manager: launchd, test: launchd_for_user }
- { rust: stable, os: macos-latest, manager: launchd, test: launchd_for_system, elevated: sudo }
- { rust: stable, os: windows-latest, manager: sc, test: sc_for_system }
- { rust: stable, os: windows-latest, manager: winsw, test: winsw_for_system }
steps:
- uses: actions/checkout@v2
- name: Install Rust ${{ matrix.rust }}
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: ${{ matrix.rust }}

- name: Download WinSW for Windows
if: matrix.os == 'windows-latest' && matrix.manager == 'winsw'
run: |
$winsw_dir = "$env:GITHUB_WORKSPACE\winsw"
New-Item -ItemType directory -Path $winsw_dir -Force
Invoke-WebRequest -Uri ${{ env.WINSW_URL }} -OutFile "$winsw_dir\WinSW.exe"
echo "$winsw_dir" | Out-File -Append -Encoding utf8 -FilePath $env:GITHUB_PATH
- uses: Swatinem/rust-cache@v1
- name: Run ${{ matrix.test }} for ${{ matrix.manager }}
run: |
Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
### Removed

## [0.5.0] - 2023-11-03

- Support for the WinSW service manager was added. WinSW can run any
application as a Windows service by providing the boilerplate code for
interacting with the Windows service infrastructure. Most, but not all,
configuration options are supported in this initial setup.
- The `ServiceInstallCtx` is extended with optional `working_directory` and
`environment` fields, which assign a working directory and pass a list of
environment variables to the process launched by the service. Most service
managers support these. This is a backwards incompatible change.

## [0.4.0] - 2023-10-19

### Added
Expand Down
6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,9 @@ dirs = "4.0"
plist = "1.1"
serde = { version = "1", features = ["derive"], optional = true }
which = "4.0"
xml-rs = "0.8.19"

[dev-dependencies]
assert_fs = "1.0.13"
indoc = "2.0.4"
predicates = "3.0.4"
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ manager.install(ServiceInstallCtx {
args: vec![OsString::from("--some-arg")],
contents: None, // Optional String for system-specific service content.
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.
}).expect("Failed to install");
// Start our service using the underlying service management platform
Expand Down Expand Up @@ -137,6 +139,8 @@ manager.install(ServiceInstallCtx {
args: vec![OsString::from("--some-arg")],
contents: None, // Optional String for system-specific service content.
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.
}).expect("Failed to install");
```

Expand Down
24 changes: 16 additions & 8 deletions src/kind.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::io;
use cfg_if::cfg_if;
use std::io;

/// Represents the kind of service manager
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
Expand All @@ -22,6 +22,9 @@ pub enum ServiceManagerKind {

/// Use systemd to manage the service
Systemd,

/// Use WinSW to manage the service
WinSw,
}

impl ServiceManagerKind {
Expand All @@ -31,6 +34,15 @@ impl ServiceManagerKind {
if #[cfg(target_os = "macos")] {
Ok(ServiceManagerKind::Launchd)
} else if #[cfg(target_os = "windows")] {
use super::{ServiceManager, TypedServiceManager};

// Prefer WinSW over sc.exe, because if it's present, it's likely been explicitly
// installed as an alternative to sc.exe.
let manager = TypedServiceManager::target(ServiceManagerKind::WinSw);
if let Ok(true) = manager.available() {
return Ok(ServiceManagerKind::WinSw);
}

Ok(ServiceManagerKind::Sc)
} else if #[cfg(any(
target_os = "freebsd",
Expand All @@ -41,17 +53,17 @@ impl ServiceManagerKind {
Ok(ServiceManagerKind::Rcd)
} else if #[cfg(target_os = "linux")] {
use super::{ServiceManager, TypedServiceManager};

let manager = TypedServiceManager::target(ServiceManagerKind::Systemd);
if let Ok(true) = manager.available() {
return Ok(ServiceManagerKind::Systemd);
}

let manager = TypedServiceManager::target(ServiceManagerKind::OpenRc);
if let Ok(true) = manager.available() {
return Ok(ServiceManagerKind::OpenRc);
}

Err(io::Error::new(
io::ErrorKind::Unsupported,
"Only systemd and openrc are supported on Linux",
Expand All @@ -64,8 +76,4 @@ impl ServiceManagerKind {
}
}
}

}



22 changes: 22 additions & 0 deletions src/launchd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ impl ServiceManager for LaunchdServiceManager {
&qualified_name,
ctx.cmd_iter(),
ctx.username.clone(),
ctx.working_directory.clone(),
ctx.environment.clone(),
),
};

Expand Down Expand Up @@ -200,6 +202,8 @@ fn make_plist<'a>(
label: &str,
args: impl Iterator<Item = &'a OsStr>,
username: Option<String>,
working_directory: Option<PathBuf>,
environment: Option<Vec<(String, String)>>,
) -> String {
let mut dict = Dictionary::new();

Expand All @@ -219,6 +223,24 @@ fn make_plist<'a>(
dict.insert("UserName".to_string(), Value::String(username));
}

if let Some(working_dir) = working_directory {
dict.insert(
"WorkingDirectory".to_string(),
Value::String(working_dir.to_string_lossy().to_string()),
);
}

if let Some(env_vars) = environment {
let env_dict: Dictionary = env_vars
.into_iter()
.map(|(k, v)| (k, Value::String(v)))
.collect();
dict.insert(
"EnvironmentVariables".to_string(),
Value::Dictionary(env_dict),
);
}

let plist = Value::Dictionary(dict);

let mut buffer = Vec::new();
Expand Down
9 changes: 9 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ mod sc;
mod systemd;
mod typed;
mod utils;
mod winsw;

pub use kind::*;
pub use launchd::*;
Expand All @@ -27,6 +28,7 @@ pub use rcd::*;
pub use sc::*;
pub use systemd::*;
pub use typed::*;
pub use winsw::*;

/// Interface for a service manager
pub trait ServiceManager {
Expand Down Expand Up @@ -220,6 +222,13 @@ pub struct ServiceInstallCtx {
///
/// If not specified, the service will run as the root or Administrator user.
pub username: Option<String>,

/// Optionally specify a working directory for the process launched by the service
pub working_directory: Option<PathBuf>,

/// Optionally specify a list of environment variables to be passed to the process launched by
/// the service
pub environment: Option<Vec<(String, String)>>,
}

impl ServiceInstallCtx {
Expand Down
29 changes: 14 additions & 15 deletions src/systemd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ use super::{
ServiceUninstallCtx,
};
use std::{
ffi::OsString,
fmt, io,
path::PathBuf,
process::{Command, Stdio},
Expand Down Expand Up @@ -137,14 +136,7 @@ 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.program.into_os_string(),
ctx.args,
self.user,
ctx.username,
),
_ => make_service(&self.config.install, &script_name, &ctx, self.user),
};

utils::write_file(
Expand Down Expand Up @@ -243,10 +235,8 @@ pub fn systemd_user_dir_path() -> io::Result<PathBuf> {
fn make_service(
config: &SystemdInstallConfig,
description: &str,
program: OsString,
args: Vec<OsString>,
ctx: &ServiceInstallCtx,
user: bool,
username: Option<String>,
) -> String {
use std::fmt::Write as _;
let SystemdInstallConfig {
Expand All @@ -269,9 +259,18 @@ fn make_service(
}

let _ = writeln!(service, "[Service]");
if let Some(working_directory) = &ctx.working_directory {
let _ = writeln!(
service,
"WorkingDirectory={}",
working_directory.to_string_lossy()
);
}

let program = program.to_string_lossy();
let args = args
let program = ctx.program.to_string_lossy();
let args = ctx
.args
.clone()
.into_iter()
.map(|a| a.to_string_lossy().to_string())
.collect::<Vec<String>>()
Expand All @@ -285,7 +284,7 @@ fn make_service(
if let Some(x) = restart_sec {
let _ = writeln!(service, "RestartSec={x}");
}
if let Some(username) = username {
if let Some(username) = &ctx.username {
let _ = writeln!(service, "User={username}");
}

Expand Down
16 changes: 15 additions & 1 deletion src/typed.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use super::{
LaunchdServiceManager, OpenRcServiceManager, RcdServiceManager, ScServiceManager,
ServiceInstallCtx, ServiceLevel, ServiceManager, ServiceManagerKind, ServiceStartCtx,
ServiceStopCtx, ServiceUninstallCtx, SystemdServiceManager,
ServiceStopCtx, ServiceUninstallCtx, SystemdServiceManager, WinSwServiceManager,
};
use std::io;

Expand All @@ -13,6 +13,7 @@ pub enum TypedServiceManager {
Rcd(RcdServiceManager),
Sc(ScServiceManager),
Systemd(SystemdServiceManager),
WinSw(WinSwServiceManager),
}

macro_rules! using {
Expand All @@ -23,6 +24,7 @@ macro_rules! using {
TypedServiceManager::Rcd($this) => $expr,
TypedServiceManager::Sc($this) => $expr,
TypedServiceManager::Systemd($this) => $expr,
TypedServiceManager::WinSw($this) => $expr,
}
}};
}
Expand Down Expand Up @@ -76,6 +78,7 @@ impl TypedServiceManager {
ServiceManagerKind::Rcd => Self::Rcd(RcdServiceManager::default()),
ServiceManagerKind::Sc => Self::Sc(ScServiceManager::default()),
ServiceManagerKind::Systemd => Self::Systemd(SystemdServiceManager::default()),
ServiceManagerKind::WinSw => Self::WinSw(WinSwServiceManager::default()),
}
}

Expand Down Expand Up @@ -118,6 +121,11 @@ impl TypedServiceManager {
pub fn is_systemd(&self) -> bool {
matches!(self, Self::Systemd(_))
}

/// Returns true if [`ServiceManager`] instance is for `winsw`
pub fn is_winsw(&self) -> bool {
matches!(self, Self::WinSw(_))
}
}

impl From<super::LaunchdServiceManager> for TypedServiceManager {
Expand Down Expand Up @@ -149,3 +157,9 @@ impl From<super::SystemdServiceManager> for TypedServiceManager {
Self::Systemd(manager)
}
}

impl From<super::WinSwServiceManager> for TypedServiceManager {
fn from(manager: super::WinSwServiceManager) -> Self {
Self::WinSw(manager)
}
}

0 comments on commit bf30b30

Please sign in to comment.