Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Run service as particular user for systemd and launchd #11

Merged
merged 1 commit into from
Oct 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ license = "MIT OR Apache-2.0"
members = ["system-tests"]

[dependencies]
dirs = "4.0"
which = "4.0"
cfg-if = "1.0"

clap = { version = "4", features = ["derive"], optional = true }
dirs = "4.0"
plist = "1.1"
serde = { version = "1", features = ["derive"], optional = true }
which = "4.0"
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ manager.install(ServiceInstallCtx {
program: PathBuf::from("path/to/my-service-executable"),
args: vec![OsString::from("--some-arg")],
contents: None, // Optional String for system-specific service content.
username: None, // Optionally specify running the service as a specific user.
}).expect("Failed to install");
// Start our service using the underlying service management platform
Expand Down
54 changes: 32 additions & 22 deletions src/launchd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ use super::{
utils, ServiceInstallCtx, ServiceLevel, ServiceManager, ServiceStartCtx, ServiceStopCtx,
ServiceUninstallCtx,
};
use plist::Dictionary;
use plist::Value;
use std::{
ffi::OsStr,
io,
Expand Down Expand Up @@ -99,7 +101,12 @@ impl ServiceManager for LaunchdServiceManager {
let plist_path = dir_path.join(format!("{}.plist", qualified_name));
let plist = match ctx.contents {
Some(contents) => contents,
_ => make_plist(&self.config.install, &qualified_name, ctx.cmd_iter()),
_ => make_plist(
&self.config.install,
&qualified_name,
ctx.cmd_iter(),
ctx.username.clone(),
),
};

utils::write_file(
Expand Down Expand Up @@ -192,26 +199,29 @@ fn make_plist<'a>(
config: &LaunchdInstallConfig,
label: &str,
args: impl Iterator<Item = &'a OsStr>,
username: Option<String>,
) -> String {
let LaunchdInstallConfig { keep_alive } = config;
let args = args
.map(|arg| format!("<string>{}</string>", arg.to_string_lossy()))
.collect::<Vec<String>>()
.join("");
format!(r#"
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>{label}</string>
<key>ProgramArguments</key>
<array>
{args}
</array>
<key>KeepAlive</key>
<{keep_alive}/>
</dict>
</plist>
"#).trim().to_string()
let mut dict = Dictionary::new();

dict.insert("Label".to_string(), Value::String(label.to_string()));

let program_arguments: Vec<Value> = args
.map(|arg| Value::String(arg.to_string_lossy().into_owned()))
.collect();
dict.insert(
"ProgramArguments".to_string(),
Value::Array(program_arguments),
);

dict.insert("KeepAlive".to_string(), Value::Boolean(config.keep_alive));

if let Some(username) = username {
dict.insert("UserName".to_string(), Value::String(username));
}

let plist = Value::Dictionary(dict);

let mut buffer = Vec::new();
plist.to_writer_xml(&mut buffer).unwrap();
String::from_utf8(buffer).unwrap()
}
5 changes: 5 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,11 @@ pub struct ServiceInstallCtx {
/// Optional contents of the service file for a given ServiceManager
/// to use instead of the default template.
pub contents: Option<String>,

/// Optionally supply the user the service will run as
///
/// If not specified, the service will run as the root or Administrator user.
pub username: Option<String>,
}

impl ServiceInstallCtx {
Expand Down
17 changes: 11 additions & 6 deletions src/systemd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,9 @@ impl ServiceManager for SystemdServiceManager {

fn install(&self, ctx: ServiceInstallCtx) -> io::Result<()> {
let dir_path = if self.user {
user_dir_path()?
systemd_user_dir_path()?
} else {
global_dir_path()
systemd_global_dir_path()
};

std::fs::create_dir_all(&dir_path)?;
Expand All @@ -143,6 +143,7 @@ impl ServiceManager for SystemdServiceManager {
ctx.program.into_os_string(),
ctx.args,
self.user,
ctx.username,
),
};

Expand All @@ -157,9 +158,9 @@ impl ServiceManager for SystemdServiceManager {

fn uninstall(&self, ctx: ServiceUninstallCtx) -> io::Result<()> {
let dir_path = if self.user {
user_dir_path()?
systemd_user_dir_path()?
} else {
global_dir_path()
systemd_global_dir_path()
};
let script_name = ctx.label.to_script_name();
let script_path = dir_path.join(format!("{script_name}.service"));
Expand Down Expand Up @@ -228,11 +229,11 @@ fn systemctl(cmd: &str, label: &str, user: bool) -> io::Result<()> {
}

#[inline]
fn global_dir_path() -> PathBuf {
pub fn systemd_global_dir_path() -> PathBuf {
PathBuf::from("/etc/systemd/system")
}

fn user_dir_path() -> io::Result<PathBuf> {
pub fn systemd_user_dir_path() -> io::Result<PathBuf> {
Ok(dirs::config_dir()
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Unable to locate home directory"))?
.join("systemd")
Expand All @@ -245,6 +246,7 @@ fn make_service(
program: OsString,
args: Vec<OsString>,
user: bool,
username: Option<String>,
) -> String {
use std::fmt::Write as _;
let SystemdInstallConfig {
Expand Down Expand Up @@ -283,6 +285,9 @@ fn make_service(
if let Some(x) = restart_sec {
let _ = writeln!(service, "RestartSec={x}");
}
if let Some(username) = username {
let _ = writeln!(service, "User={username}");
}

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

Expand Down
3 changes: 3 additions & 0 deletions system-tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,6 @@ windows-service = "0.5"

[dev-dependencies]
assert_cmd = "2.0"

[target.'cfg(target_os = "macos")'.dev-dependencies]
plist = "1.1"
119 changes: 118 additions & 1 deletion system-tests/tests/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ mod runner;
const TEST_ITER_CNT: usize = 3;

#[test]
#[cfg(target_os = "macos")]
// #[cfg(target_os = "macos")]
fn should_support_launchd_for_system_services() {
runner::run_test_n(LaunchdServiceManager::system(), TEST_ITER_CNT)
}
Expand All @@ -16,6 +16,22 @@ fn should_support_launchd_for_user_services() {
runner::run_test_n(LaunchdServiceManager::user(), TEST_ITER_CNT)
}

#[test]
#[cfg(target_os = "macos")]
fn should_support_launchd_for_system_services_running_as_specific_user() {
create_user_account("test_account");

let is_user_specified = runner::run_test_n_as_user(
LaunchdServiceManager::system(),
TEST_ITER_CNT,
"test_account",
);

remove_user_account("test_account");

assert!(is_user_specified);
}

#[test]
#[cfg(target_os = "linux")]
fn should_support_openrc_for_system_services() {
Expand Down Expand Up @@ -55,8 +71,109 @@ fn should_support_systemd_for_system_services() {
runner::run_test_n(SystemdServiceManager::system(), TEST_ITER_CNT)
}

#[test]
#[cfg(target_os = "linux")]
fn should_support_systemd_for_system_services_running_as_specific_user() {
create_user_account("test_account");

let is_user_specified = runner::run_test_n_as_user(
SystemdServiceManager::system(),
TEST_ITER_CNT,
"test_account",
);

remove_user_account("test_account");

assert!(is_user_specified);
}

#[test]
#[cfg(target_os = "linux")]
fn should_support_systemd_for_user_services() {
runner::run_test_n(SystemdServiceManager::user(), TEST_ITER_CNT)
}

#[cfg(target_os = "linux")]
fn create_user_account(username: &str) {
use std::process::Command;

let status = Command::new("useradd")
.arg("-m")
.arg("-s")
.arg("/bin/bash")
.arg(username)
.status()
.unwrap();
assert!(status.success(), "Failed to create user test_account");
}

#[cfg(target_os = "macos")]
fn create_user_account(username: &str) {
use std::process::Command;
use std::str;

let output = Command::new("dscl")
.arg(".")
.arg("-list")
.arg("/Users")
.arg("UniqueID")
.output()
.unwrap();
let output_str = str::from_utf8(&output.stdout).unwrap();
let mut max_id = 0;

for line in output_str.lines() {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() == 2 {
if let Ok(id) = parts[1].parse::<u32>() {
if id > max_id {
max_id = id;
}
}
}
}
let new_unique_id = max_id + 1;

let commands = vec![
format!("dscl . -create /Users/{}", username),
format!(
"dscl . -create /Users/{} UserShell /usr/bin/false",
username
),
format!(
"dscl . -create /Users/{} UniqueID {}",
username, new_unique_id
),
format!("dscl . -create /Users/{} PrimaryGroupID 20", username),
];
for cmd in commands {
let status = Command::new("sh").arg("-c").arg(&cmd).status().unwrap();
assert!(status.success(), "Failed to create user test_account");
}
}

#[cfg(target_os = "linux")]
fn remove_user_account(username: &str) {
use std::process::Command;

let status = Command::new("userdel")
.arg("-r")
.arg("-f")
.arg(username)
.status()
.unwrap();
assert!(status.success(), "Failed to delete user test_account");
}

#[cfg(target_os = "macos")]
fn remove_user_account(username: &str) {
use std::process::Command;

let status = Command::new("dscl")
.arg(".")
.arg("-delete")
.arg(format!("/Users/{username}"))
.status()
.unwrap();
assert!(status.success(), "Failed to delete user test_account");
}
Loading
Loading