use super::step_execution::{ABSENT, OK};
use super::target_resolution::StartupPlan;
use super::unit::UnitSpec;
use crate::cli::Init;
use crate::output::StartupStep;
#[cfg(target_os = "linux")]
use std::path::Path;
use std::path::PathBuf;
pub(super) const DEFAULT_HOME_DIR: &str = ".shep";
pub(crate) const fn unit_mode(init: Init) -> u32 {
match init {
Init::Systemd | Init::Launchd => 0o644,
Init::Openrc | Init::FreebsdRc | Init::OpenbsdRc => 0o755,
}
}
pub(crate) fn unit_path_for(init: Init, user: &str) -> PathBuf {
match init {
Init::Systemd => super::unit::systemd_unit_path(user),
Init::Launchd => super::unit::launchd_plist_path(user),
Init::Openrc => PathBuf::from(format!("/etc/init.d/shep-{user}")),
Init::FreebsdRc => PathBuf::from(format!("/usr/local/etc/rc.d/shep_{user}")),
Init::OpenbsdRc => PathBuf::from(format!("/etc/rc.d/shep_{user}")),
}
}
pub(super) fn secure_path_warning(sudo_user: Option<&str>, spec: &UnitSpec) -> Option<String> {
let sudo_user = sudo_user?;
Some(format!(
"sudo may have replaced PATH with its own secure_path before shep ever saw it \
(SUDO_USER={sudo_user}); the unit now carries PATH={}; compare it against \
{sudo_user}'s login PATH, and if a directory such as ~/.bun/bin or ~/.cargo/bin \
is missing, run shep unstartup then sudo --preserve-env=PATH shep startup to \
carry it through instead",
spec.path.to_string_lossy(),
))
}
pub(super) fn write_unit(plan: &StartupPlan) -> StartupStep {
use std::io::Write as _;
use std::os::unix::fs::{OpenOptionsExt as _, PermissionsExt as _};
let rendered = match plan.init {
Init::Systemd => super::unit::systemd_unit(&plan.spec),
Init::Launchd => super::unit::launchd_plist(&plan.spec),
Init::Openrc => super::unit::openrc_script(&plan.spec),
Init::FreebsdRc => super::unit::freebsd_rc_script(&plan.spec),
Init::OpenbsdRc => super::unit::openbsd_rc_script(&plan.spec),
};
let mode = unit_mode(plan.init);
let written = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(mode)
.open(&plan.unit_path)
.and_then(|mut file| {
file.write_all(rendered.as_bytes())?;
file.set_permissions(std::fs::Permissions::from_mode(mode))
});
StartupStep {
action: "wrote",
target: plan.unit_path.display().to_string(),
result: match written {
Ok(()) => OK.to_string(),
Err(err) => err.to_string(),
},
}
}
pub(super) fn remove_unit(plan: &StartupPlan) -> StartupStep {
StartupStep {
action: "removed",
target: plan.unit_path.display().to_string(),
result: match std::fs::remove_file(&plan.unit_path) {
Ok(()) => OK.to_string(),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => ABSENT.to_string(),
Err(err) => err.to_string(),
},
}
}
pub(super) fn unit_file_name(plan: &StartupPlan) -> String {
plan.unit_path
.file_name()
.unwrap_or(plan.unit_path.as_os_str())
.to_string_lossy()
.into_owned()
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct SystemdBooted(bool);
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct OpenrcRunning(bool);
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
const fn linux_init(systemd: SystemdBooted, openrc: OpenrcRunning) -> Option<Init> {
if systemd.0 {
Some(Init::Systemd)
} else if openrc.0 {
Some(Init::Openrc)
} else {
None
}
}
pub(super) fn current_init() -> Option<Init> {
#[cfg(target_os = "linux")]
{
linux_init(
SystemdBooted(Path::new("/run/systemd/system").is_dir()),
OpenrcRunning(
Path::new("/run/openrc/softlevel").exists() || Path::new("/run/openrc").is_dir(),
),
)
}
#[cfg(target_os = "macos")]
{
Some(Init::Launchd)
}
#[cfg(target_os = "freebsd")]
{
Some(Init::FreebsdRc)
}
#[cfg(target_os = "openbsd")]
{
Some(Init::OpenbsdRc)
}
#[cfg(not(any(
target_os = "linux",
target_os = "macos",
target_os = "freebsd",
target_os = "openbsd"
)))]
{
None
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::super::unit::UnitSpec;
use crate::cli::Init;
use super::*;
use std::ffi::OsString;
#[test]
fn secure_path_warning_names_the_sudo_user_and_the_full_path_only_under_sudo() {
let spec = UnitSpec {
user: "deploy".to_string(),
exec: PathBuf::from("/usr/local/bin/shep"),
home: PathBuf::from("/home/deploy/.shep"),
path: OsString::from("/usr/local/sbin:/usr/local/bin:/usr/bin:/bin"),
working_dir: PathBuf::from("/home/deploy"),
};
assert_eq!(
secure_path_warning(None, &spec),
None,
"no $SUDO_USER means shep was never run through sudo at all"
);
let message =
secure_path_warning(Some("ada"), &spec).expect("$SUDO_USER was set, so this warns");
assert!(message.contains("SUDO_USER=ada"), "{message}");
assert!(
message.contains("/usr/local/sbin:/usr/local/bin:/usr/bin:/bin"),
"the full captured PATH must be readable without a second lookup: {message}"
);
assert!(
message.contains("--preserve-env=PATH"),
"the operator needs a way to get the PATH they meant: {message}"
);
}
#[test]
fn the_mode_is_read_only_for_units_and_executable_for_scripts() {
assert_eq!(unit_mode(Init::Systemd), 0o644);
assert_eq!(unit_mode(Init::Launchd), 0o644);
assert_eq!(unit_mode(Init::Openrc), 0o755);
assert_eq!(unit_mode(Init::FreebsdRc), 0o755);
assert_eq!(unit_mode(Init::OpenbsdRc), 0o755);
}
#[test]
fn systemd_wins_when_both_linux_probes_are_true() {
assert_eq!(
linux_init(SystemdBooted(true), OpenrcRunning(true)),
Some(Init::Systemd)
);
assert_eq!(
linux_init(SystemdBooted(true), OpenrcRunning(false)),
Some(Init::Systemd)
);
assert_eq!(
linux_init(SystemdBooted(false), OpenrcRunning(true)),
Some(Init::Openrc)
);
assert_eq!(linux_init(SystemdBooted(false), OpenrcRunning(false)), None);
}
#[test]
fn each_init_names_its_own_unit_path() {
assert_eq!(
unit_path_for(Init::Openrc, "deploy"),
PathBuf::from("/etc/init.d/shep-deploy")
);
assert_eq!(
unit_path_for(Init::FreebsdRc, "deploy"),
PathBuf::from("/usr/local/etc/rc.d/shep_deploy")
);
assert_eq!(
unit_path_for(Init::OpenbsdRc, "deploy"),
PathBuf::from("/etc/rc.d/shep_deploy")
);
assert_eq!(
unit_path_for(Init::Systemd, "deploy"),
super::super::unit::systemd_unit_path("deploy")
);
assert_eq!(
unit_path_for(Init::Launchd, "deploy"),
super::super::unit::launchd_plist_path("deploy")
);
}
}