use super::step_execution::shell_quote;
use super::target_resolution::StartupPlan;
use super::unit_layout::unit_file_name;
use crate::cli::Init;
use shep_core::paths::ShepPaths;
use shep_daemon::boot::{self, BootError, Shepherd};
use std::path::Path;
pub(super) type InitCommand = (&'static str, Vec<String>);
pub(super) fn install_commands(plan: &StartupPlan, start: bool) -> Vec<InitCommand> {
let unit = unit_file_name(plan);
let mut commands = match plan.init {
Init::Systemd => {
let enable = if start {
words(&["enable", "--now", &unit])
} else {
words(&["enable", &unit])
};
return vec![
("systemctl", words(&["daemon-reload"])),
("systemctl", enable),
];
}
Init::Launchd => Vec::new(),
Init::Openrc => vec![("rc-update", words(&["add", &unit, "default"]))],
Init::FreebsdRc => vec![("sysrc", vec![format!("{unit}_enable=YES")])],
Init::OpenbsdRc => vec![("rcctl", words(&["enable", &unit]))],
};
if start {
commands.push(start_command(plan));
}
commands
}
fn start_command(plan: &StartupPlan) -> InitCommand {
let unit = unit_file_name(plan);
match plan.init {
Init::Systemd => ("systemctl", words(&["start", &unit])),
Init::Launchd => (
"launchctl",
words(&["bootstrap", "system", &plan.unit_path.display().to_string()]),
),
Init::Openrc => ("rc-service", words(&[&unit, "start"])),
Init::FreebsdRc => ("service", words(&[&unit, "start"])),
Init::OpenbsdRc => ("rcctl", words(&["start", &unit])),
}
}
pub(super) fn running_shepherd(home: &Path) -> Result<Shepherd, BootError> {
let paths = ShepPaths::at(home.to_path_buf());
if !boot::pidfile(&paths).exists() {
return Ok(Shepherd::Absent);
}
boot::daemon_liveness(&paths)
}
pub(super) fn not_started_notice(plan: &StartupPlan, shepherd: Shepherd) -> String {
let holder = match shepherd {
Shepherd::Running(pid) => format!("a shepherd (pid {pid})"),
Shepherd::Absent | Shepherd::Booting => "a shepherd that is still starting".to_string(),
};
let home = shell_quote(&plan.spec.home.display().to_string());
let (program, args) = start_command(plan);
format!(
"{holder} already runs on {home}, so the unit is enabled but not started; it takes over \
at the next boot, or now: {exec} --home {home} kill && {program} {args}",
exec = shell_quote(&plan.spec.exec.display().to_string()),
args = args.join(" "),
)
}
fn words(args: &[&str]) -> Vec<String> {
args.iter().map(ToString::to_string).collect()
}
#[cfg(test)]
mod tests {
use super::super::testing::plan_for_test;
use super::*;
use std::io::Write as _;
use std::path::PathBuf;
fn plan(init: Init) -> StartupPlan {
StartupPlan {
init,
..plan_for_test(Path::new("/home/deploy/.shep"))
}
}
fn lines(commands: &[InitCommand]) -> Vec<String> {
commands
.iter()
.map(|(program, args)| format!("{program} {}", args.join(" ")))
.collect()
}
#[test]
fn an_empty_home_enables_and_starts_the_unit() {
let cases = [
(
Init::Systemd,
vec![
"systemctl daemon-reload",
"systemctl enable --now shep-deploy.service",
],
),
(
Init::Launchd,
vec!["launchctl bootstrap system /home/deploy/shep-deploy.service"],
),
(
Init::Openrc,
vec![
"rc-update add shep-deploy.service default",
"rc-service shep-deploy.service start",
],
),
(
Init::FreebsdRc,
vec![
"sysrc shep-deploy.service_enable=YES",
"service shep-deploy.service start",
],
),
(
Init::OpenbsdRc,
vec![
"rcctl enable shep-deploy.service",
"rcctl start shep-deploy.service",
],
),
];
for (init, expected) in cases {
assert_eq!(
lines(&install_commands(&plan(init), true)),
expected,
"{init:?}"
);
}
}
#[test]
fn a_held_home_enables_the_unit_without_starting_it() {
let cases = [
(
Init::Systemd,
vec![
"systemctl daemon-reload",
"systemctl enable shep-deploy.service",
],
),
(Init::Launchd, vec![]),
(
Init::Openrc,
vec!["rc-update add shep-deploy.service default"],
),
(
Init::FreebsdRc,
vec!["sysrc shep-deploy.service_enable=YES"],
),
(Init::OpenbsdRc, vec!["rcctl enable shep-deploy.service"]),
];
for (init, expected) in cases {
assert_eq!(
lines(&install_commands(&plan(init), false)),
expected,
"{init:?}"
);
}
}
#[test]
fn the_notice_names_the_pid_and_a_pasteable_handover() {
let notice = not_started_notice(&plan(Init::Systemd), Shepherd::Running(205_270));
assert_eq!(
notice,
"a shepherd (pid 205270) already runs on /home/deploy/.shep, so the unit is enabled \
but not started; it takes over at the next boot, or now: /usr/local/bin/shep \
--home /home/deploy/.shep kill && systemctl start shep-deploy.service"
);
}
#[test]
fn a_home_with_no_pidfile_is_absent_and_stays_without_one() {
let dir = tempfile::tempdir().unwrap();
let paths = ShepPaths::at(dir.path().to_path_buf());
std::fs::create_dir_all(&paths.pids).unwrap();
assert_eq!(running_shepherd(&paths.home).unwrap(), Shepherd::Absent);
assert!(
!boot::pidfile(&paths).exists(),
"the probe created a pidfile"
);
}
#[test]
fn a_held_pidfile_lock_is_a_running_shepherd() {
let dir = tempfile::tempdir().unwrap();
let paths = ShepPaths::at(dir.path().to_path_buf());
std::fs::create_dir_all(&paths.pids).unwrap();
let path: PathBuf = boot::pidfile(&paths);
let mut file = std::fs::File::create(&path).unwrap();
file.write_all(b"4242").unwrap();
let _held =
nix::fcntl::Flock::lock(file, nix::fcntl::FlockArg::LockExclusiveNonblock).unwrap();
assert_eq!(
running_shepherd(&paths.home).unwrap(),
Shepherd::Running(4242)
);
}
}