use crate::context::Context;
use crate::error::{Error, Result};
use std::path::PathBuf;
#[cfg(target_os = "macos")]
const LABEL: &str = "com.datlechin.pitboard.renew";
const EVERY_SECONDS: u32 = 86_400;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Installed {
Yes {
path: PathBuf,
every_seconds: u32,
},
No,
Unsupported,
}
#[cfg(target_os = "macos")]
fn agent_path(ctx: &Context) -> PathBuf {
ctx.home()
.join("Library/LaunchAgents")
.join(format!("{LABEL}.plist"))
}
#[cfg(target_os = "linux")]
fn unit_dir(ctx: &Context) -> PathBuf {
ctx.home().join(".config/systemd/user")
}
pub fn path(ctx: &Context) -> Option<PathBuf> {
#[cfg(target_os = "macos")]
{
Some(agent_path(ctx))
}
#[cfg(target_os = "linux")]
{
Some(unit_dir(ctx).join("pitboard-renew.timer"))
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
{
let _ = ctx;
None
}
}
pub fn status(ctx: &Context) -> Installed {
match path(ctx) {
None => Installed::Unsupported,
Some(path) if path.is_file() => Installed::Yes {
path,
every_seconds: EVERY_SECONDS,
},
Some(_) => Installed::No,
}
}
pub(crate) fn serves(ctx: &Context) -> bool {
crate::home::dir(ctx) == ctx.home().join(".pitboard")
}
fn program(ctx: &Context) -> Result<PathBuf> {
if let Some(program) = ctx.schedule_program() {
return lasting(program).map(std::path::Path::to_path_buf);
}
let running = std::env::current_exe().map_err(|source| Error::HomeUnwritable {
path: PathBuf::from("the running pitboard"),
source,
})?;
#[cfg(target_os = "linux")]
let running = started_as(
std::env::args_os().next().as_deref(),
&std::env::var_os("PATH").unwrap_or_default(),
running,
);
Ok(running)
}
#[cfg(any(target_os = "linux", test))]
fn started_as(
argv0: Option<&std::ffi::OsStr>,
search: &std::ffi::OsStr,
running: PathBuf,
) -> PathBuf {
let resolved = std::fs::canonicalize(&running).ok();
argv0
.and_then(|named| crate::provider::find_program(std::path::Path::new(named), search))
.filter(|found| resolved.is_some() && std::fs::canonicalize(found).ok() == resolved)
.unwrap_or(running)
}
fn lasting(program: &std::path::Path) -> Result<&std::path::Path> {
if program
.components()
.any(|part| part.as_os_str() == "AppTranslocation")
{
return Err(Error::ScheduleProgramTemporary {
path: program.to_path_buf(),
});
}
if !program.is_file() {
return Err(Error::ScheduleProgramMissing {
path: program.to_path_buf(),
});
}
Ok(program)
}
pub fn installed_program(ctx: &Context) -> Option<PathBuf> {
#[cfg(target_os = "macos")]
{
let body = std::fs::read_to_string(agent_path(ctx)).ok()?;
let (_, after) = body.split_once("<key>ProgramArguments</key>")?;
let (_, after) = after.split_once("<string>")?;
let (program, _) = after.split_once("</string>")?;
Some(PathBuf::from(unescape(program)))
}
#[cfg(target_os = "linux")]
{
if !unit_dir(ctx).join("pitboard-renew.timer").is_file() {
return None;
}
let body = std::fs::read_to_string(unit_dir(ctx).join("pitboard-renew.service")).ok()?;
body.lines()
.find_map(|line| line.strip_prefix("ExecStart=")?.strip_suffix(" renew"))
.map(PathBuf::from)
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
{
let _ = ctx;
None
}
}
fn write(path: &std::path::Path, body: &str) -> Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|source| Error::HomeUnwritable {
path: parent.to_path_buf(),
source,
})?;
}
crate::atomic::write(path, body.as_bytes(), crate::atomic::Perms::MatchExisting).map_err(
|source| Error::HomeUnwritable {
path: path.to_path_buf(),
source,
},
)
}
pub fn install(ctx: &Context) -> Result<PathBuf> {
let path = put(ctx, &program(ctx)?)?;
crate::audit::record(ctx, "schedule", "install", "ok");
Ok(path)
}
pub fn repair(ctx: &Context) -> Result<bool> {
#[cfg(target_os = "macos")]
if ctx.launchd_job().as_deref() == Some(LABEL) {
return Ok(false);
}
let Some(named) = ctx.schedule_program() else {
return Ok(false);
};
if !serves(ctx)
|| lasting(named).is_err()
|| !installed_program(ctx).is_some_and(|program| an_apps_own_program(&program))
{
return Ok(false);
}
let repaired = put(ctx, named);
crate::audit::record(
ctx,
"schedule",
"repair",
match &repaired {
Ok(_) => "ok",
Err(e) => e.code(),
},
);
repaired.map(|_| true)
}
pub(crate) fn an_apps_own_program(program: &std::path::Path) -> bool {
let mut dirs = program
.ancestors()
.skip(1)
.map(|dir| dir.file_name().and_then(|n| n.to_str()));
dirs.next() == Some(Some("MacOS")) && dirs.next() == Some(Some("Contents"))
}
fn put(ctx: &Context, program: &std::path::Path) -> Result<PathBuf> {
let Some(path) = path(ctx) else {
return Err(Error::ScheduleUnsupported);
};
#[cfg(target_os = "macos")]
{
let before = std::fs::read_to_string(&path).ok();
write(&path, &plist(program, ctx.argv_fallback()))?;
let uid = unsafe { libc::getuid() };
let domain = format!("gui/{uid}");
let target = path.to_string_lossy();
let _ = run("/bin/launchctl", &["bootout", &domain, &target]);
if let Err(refused) = run("/bin/launchctl", &["bootstrap", &domain, &target]) {
restore(&path, before.as_deref());
if before.is_some() {
let _ = run("/bin/launchctl", &["bootstrap", &domain, &target]);
}
return Err(refused);
}
}
#[cfg(target_os = "linux")]
{
let unit = unit_dir(ctx).join("pitboard-renew.service");
let before = (
std::fs::read_to_string(&unit).ok(),
std::fs::read_to_string(&path).ok(),
);
write(&unit, &service(program, ctx.argv_fallback()))?;
write(&path, &timer())?;
let _ = run("systemctl", &["--user", "daemon-reload"]);
let start = ["--user", "enable", "--now", "pitboard-renew.timer"];
if let Err(refused) = run("systemctl", &start) {
restore(&unit, before.0.as_deref());
restore(&path, before.1.as_deref());
let _ = run("systemctl", &["--user", "daemon-reload"]);
if before.1.is_some() {
let _ = run("systemctl", &start);
}
return Err(refused);
}
}
Ok(path)
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
fn restore(path: &std::path::Path, before: Option<&str>) {
let _ = match before {
Some(body) => write(path, body),
None => remove(path),
};
}
pub fn uninstall(ctx: &Context) -> Result<bool> {
let Some(path) = path(ctx) else {
return Err(Error::ScheduleUnsupported);
};
if !path.is_file() {
return Ok(false);
}
#[cfg(target_os = "macos")]
{
let uid = unsafe { libc::getuid() };
let domain = format!("gui/{uid}");
let _ = run(
"/bin/launchctl",
&["bootout", &domain, &path.to_string_lossy()],
);
remove(&path)?;
}
#[cfg(target_os = "linux")]
{
let _ = run(
"systemctl",
&["--user", "disable", "--now", "pitboard-renew.timer"],
);
remove(&path)?;
remove(&unit_dir(ctx).join("pitboard-renew.service"))?;
let _ = run("systemctl", &["--user", "daemon-reload"]);
}
crate::audit::record(ctx, "schedule", "uninstall", "ok");
Ok(true)
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
fn remove(path: &std::path::Path) -> Result<()> {
match std::fs::remove_file(path) {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(source) => Err(Error::HomeUnwritable {
path: path.to_path_buf(),
source,
}),
}
}
#[cfg(all(any(target_os = "macos", target_os = "linux"), test))]
fn run(program: &str, args: &[&str]) -> Result<()> {
let refused = REFUSED.with(|refused| {
let mut refused = refused.borrow_mut();
refused
.filter(|verb| args.contains(verb))
.inspect(|_| *refused = None)
});
match refused {
Some(verb) => Err(Error::ScheduleRefused {
detail: format!("{program} refused {verb} in a test"),
}),
None => Ok(()),
}
}
#[cfg(all(any(target_os = "macos", target_os = "linux"), test))]
thread_local! {
static REFUSED: std::cell::RefCell<Option<&'static str>> =
const { std::cell::RefCell::new(None) };
}
#[cfg(all(any(target_os = "macos", target_os = "linux"), not(test)))]
fn run(program: &str, args: &[&str]) -> Result<()> {
let out = std::process::Command::new(program)
.args(args)
.output()
.map_err(|source| Error::HomeUnwritable {
path: PathBuf::from(program),
source,
})?;
if out.status.success() {
return Ok(());
}
Err(Error::ScheduleRefused {
detail: format!(
"{program} exited {}: {}",
out.status
.code()
.map_or_else(|| "on a signal".into(), |c| c.to_string()),
String::from_utf8_lossy(&out.stderr).trim()
),
})
}
#[cfg(target_os = "macos")]
fn plist(program: &std::path::Path, argument_line: bool) -> String {
let environment = if argument_line {
""
} else {
" <key>EnvironmentVariables</key>\n <dict>\n <key>PITBOARD_NO_ARGV</key><string>1</string>\n </dict>\n"
};
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>
<string>{}</string>
<string>renew</string>
</array>
<key>StartInterval</key><integer>{EVERY_SECONDS}</integer>
<key>RunAtLoad</key><false/>
<key>LowPriorityIO</key><true/>
<key>ProcessType</key><string>Background</string>
{environment}</dict>
</plist>
"#,
escape(&program.to_string_lossy())
)
}
#[cfg(target_os = "macos")]
fn escape(text: &str) -> String {
text.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
}
#[cfg(target_os = "macos")]
fn unescape(text: &str) -> String {
text.replace("<", "<")
.replace(">", ">")
.replace("&", "&")
}
#[cfg(target_os = "linux")]
fn service(program: &std::path::Path, argument_line: bool) -> String {
format!(
"[Unit]\n\
Description=Renew pitboard's parked logins\n\
Documentation=https://docs.usepitboard.com\n\
\n\
[Service]\n\
Type=oneshot\n\
{}\
ExecStart={} renew\n",
if argument_line {
""
} else {
"Environment=PITBOARD_NO_ARGV=1\n"
},
program.display()
)
}
#[cfg(target_os = "linux")]
fn timer() -> String {
format!(
"[Unit]\n\
Description=Renew pitboard's parked logins daily\n\
\n\
[Timer]\n\
OnUnitActiveSec={EVERY_SECONDS}\n\
OnStartupSec=900\n\
Persistent=true\n\
\n\
[Install]\n\
WantedBy=timers.target\n"
)
}
#[cfg(test)]
mod tests {
use super::*;
struct Scratch(PathBuf);
impl Scratch {
fn new(name: &str) -> Scratch {
let root = std::env::temp_dir().join(format!(
"pitboard-schedule-{name}-{}-{:?}",
std::process::id(),
std::thread::current().id()
));
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(&root).expect("a scratch home");
Scratch(root)
}
}
impl Drop for Scratch {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
fn a_program_at(path: &std::path::Path) {
std::fs::create_dir_all(path.parent().expect("a directory")).expect("its directory");
std::fs::write(path, "").expect("a program");
}
#[cfg(target_os = "macos")]
const START: &str = "bootstrap";
#[cfg(target_os = "linux")]
const START: &str = "enable";
#[cfg(any(target_os = "macos", target_os = "linux"))]
fn refusing<T>(verb: &'static str, change: impl FnOnce() -> T) -> T {
REFUSED.with(|refused| *refused.borrow_mut() = Some(verb));
let outcome = change();
REFUSED.with(|refused| *refused.borrow_mut() = None);
outcome
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
#[test]
fn a_schedule_the_scheduler_will_not_start_is_not_left_on_disk() {
let home = Scratch::new("refused");
let program = home.0.join("bin/pitboard");
a_program_at(&program);
let ctx = Context::new(home.0.clone()).with_schedule_program(program);
let refused = refusing(START, || install(&ctx)).expect_err("refused");
assert_eq!(refused.code(), "schedule_refused");
assert_eq!(status(&ctx), Installed::No);
assert_eq!(installed_program(&ctx), None);
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
#[test]
fn a_repair_the_scheduler_will_not_start_leaves_the_schedule_as_it_was() {
let home = Scratch::new("repair-refused");
std::fs::create_dir_all(home.0.join(".pitboard")).expect("a pitboard home");
let app = home
.0
.join("Applications/Pitboard.app/Contents/MacOS/Pitboard");
let bundled = home
.0
.join("Applications/Pitboard.app/Contents/Helpers/pitboard");
a_program_at(&app);
a_program_at(&bundled);
let ctx = Context::new(home.0.clone());
install(&ctx.clone().with_schedule_program(app.clone())).expect("0.3.0's schedule");
let refused = refusing(START, || {
repair(&ctx.clone().with_schedule_program(bundled))
})
.expect_err("refused");
assert_eq!(refused.code(), "schedule_refused");
assert!(matches!(status(&ctx), Installed::Yes { .. }));
assert_eq!(installed_program(&ctx), Some(app));
}
#[test]
fn nothing_is_installed_on_a_machine_where_nothing_was_installed() {
let home = Scratch::new("none");
let ctx = Context::new(home.0.clone());
assert!(matches!(
status(&ctx),
Installed::No | Installed::Unsupported
));
assert_eq!(installed_program(&ctx), None);
}
#[test]
fn the_schedule_runs_the_pitboard_the_context_names_and_otherwise_this_one() {
let home = Scratch::new("program");
let ctx = Context::new(home.0.clone());
let scheduled = program(&ctx).expect("this program");
let running = std::env::current_exe().expect("this test's own program");
assert_eq!(
std::fs::canonicalize(&scheduled).expect("there"),
std::fs::canonicalize(&running).expect("there"),
"the command line schedules itself"
);
#[cfg(target_os = "macos")]
assert_eq!(
scheduled, running,
"by the path macOS says it was started by"
);
let bundled = home
.0
.join("Applications/Pitboard.app/Contents/Helpers/pitboard");
a_program_at(&bundled);
assert_eq!(
program(&ctx.with_schedule_program(bundled.clone())).expect("the named one"),
bundled,
"an app schedules the command line it comes with"
);
}
#[test]
fn the_command_line_schedules_itself_by_the_path_it_was_started_by() {
use std::os::unix::fs::PermissionsExt;
let home = Scratch::new("started");
let runnable = |path: &std::path::Path| {
a_program_at(path);
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o755))
.expect("runnable");
};
let running = home.0.join("Caskroom/pitboard/0.4.0/pitboard");
runnable(&running);
let bin = home.0.join("bin");
std::fs::create_dir_all(&bin).expect("a bin");
let link = bin.join("pitboard");
std::os::unix::fs::symlink(&running, &link).expect("a link");
let another = home.0.join("elsewhere/pitboard");
runnable(&another);
let started = |argv0: Option<&std::path::Path>, search: &std::path::Path| {
started_as(
argv0.map(std::path::Path::as_os_str),
search.as_os_str(),
running.clone(),
)
};
let name = std::path::Path::new("pitboard");
let nowhere = std::path::Path::new("");
assert_eq!(started(Some(name), &bin), link, "a name found on PATH");
assert_eq!(started(Some(&link), nowhere), link, "a path");
assert_eq!(
started(Some(name), another.parent().expect("its directory")),
running,
"another pitboard on PATH is not the one that is running"
);
assert_eq!(
started(Some(name), nowhere),
running,
"a name found nowhere"
);
assert_eq!(started(None, &bin), running, "no name at all");
}
#[test]
fn a_named_pitboard_that_will_not_be_there_is_refused() {
let home = Scratch::new("refused");
let ctx = Context::new(home.0.clone());
let missing = home.0.join("Pitboard.app/Contents/Helpers/pitboard");
let refused = install(&ctx.clone().with_schedule_program(missing.clone()))
.expect_err("nothing there to run");
assert_eq!(refused.code(), "schedule_program_missing");
assert!(
refused.to_string().contains(&missing.display().to_string()),
"{refused}"
);
let temporary = home
.0
.join("AppTranslocation/6A1C/d/Pitboard.app/Contents/Helpers/pitboard");
a_program_at(&temporary);
let refused = install(&ctx.clone().with_schedule_program(temporary.clone()))
.expect_err("a copy that goes away");
assert_eq!(refused.code(), "schedule_program_temporary");
assert!(
refused
.to_string()
.contains(&temporary.display().to_string())
&& refused.to_string().contains("Applications folder"),
"{refused}"
);
assert!(
matches!(status(&ctx), Installed::No | Installed::Unsupported),
"nothing was written"
);
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
#[test]
fn an_installed_schedule_says_which_pitboard_it_runs() {
let home = Scratch::new("installed");
let bundled = home
.0
.join("Tools&Apps/Pitboard.app/Contents/Helpers/pitboard");
a_program_at(&bundled);
let ctx = Context::new(home.0.clone()).with_schedule_program(bundled.clone());
install(&ctx).expect("installed");
assert!(matches!(status(&ctx), Installed::Yes { .. }));
#[cfg(target_os = "macos")]
{
let written = std::fs::read_to_string(agent_path(&ctx)).expect("the agent");
assert!(written.contains("/Tools&Apps/"), "{written}");
assert!(!written.contains("/Tools&Apps/"), "{written}");
}
assert_eq!(installed_program(&ctx), Some(bundled));
assert!(uninstall(&ctx).expect("taken away"));
assert_eq!(status(&ctx), Installed::No);
assert_eq!(installed_program(&ctx), None);
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
#[test]
fn a_schedule_that_runs_an_app_is_pointed_at_its_command_line() {
let home = Scratch::new("repair");
std::fs::create_dir_all(home.0.join(".pitboard")).expect("a pitboard home");
let app = home
.0
.join("Applications/Pitboard.app/Contents/MacOS/Pitboard");
let bundled = home
.0
.join("Applications/Pitboard.app/Contents/Helpers/pitboard");
a_program_at(&app);
a_program_at(&bundled);
let ctx = Context::new(home.0.clone());
let the_app = ctx.clone().with_schedule_program(bundled.clone());
assert!(
!repair(&the_app).expect("nothing to do"),
"nothing installed"
);
install(&ctx.clone().with_schedule_program(app.clone())).expect("0.3.0's schedule");
assert!(
!repair(&ctx).expect("nothing to do"),
"no command line named"
);
let gone = home.0.join("Old.app/Contents/Helpers/pitboard");
assert!(
!repair(&ctx.clone().with_schedule_program(gone)).expect("nothing to do"),
"a command line that is not there"
);
assert!(
!repair(&the_app.clone().with_pitboard_home(home.0.join("elsewhere")))
.expect("nothing to do"),
"a schedule another home's pitboard looks after"
);
assert_eq!(installed_program(&ctx), Some(app.clone()));
assert!(repair(&the_app).expect("repaired"));
assert_eq!(installed_program(&ctx), Some(bundled));
let logged = crate::audit::read(&ctx, 1);
assert_eq!(
logged
.iter()
.map(|e| (e.verb.as_str(), e.subject.as_str(), e.outcome.as_str()))
.collect::<Vec<_>>(),
[("schedule", "repair", "ok")]
);
assert!(
!repair(&the_app).expect("nothing to do"),
"a schedule that runs a command line already"
);
install(&ctx.clone().with_schedule_program(app.clone())).expect("the app again");
let temporary = home
.0
.join("AppTranslocation/6A1C/d/Pitboard.app/Contents/Helpers/pitboard");
a_program_at(&temporary);
assert!(
!repair(&ctx.clone().with_schedule_program(temporary)).expect("nothing to do"),
"a command line that is gone once the app quits"
);
assert_eq!(installed_program(&ctx), Some(app));
}
#[cfg(target_os = "macos")]
#[test]
fn a_schedule_is_not_repaired_from_inside_its_own_job() {
let home = Scratch::new("inside");
std::fs::create_dir_all(home.0.join(".pitboard")).expect("a pitboard home");
let app = home
.0
.join("Applications/Pitboard.app/Contents/MacOS/Pitboard");
let bundled = home
.0
.join("Applications/Pitboard.app/Contents/Helpers/pitboard");
a_program_at(&app);
a_program_at(&bundled);
let ctx = Context::new(home.0.clone());
install(&ctx.clone().with_schedule_program(app.clone())).expect("0.3.0's schedule");
let the_app = ctx.clone().with_schedule_program(bundled.clone());
assert!(
!repair(&the_app.clone().with_launchd_job(LABEL.into())).expect("nothing to do"),
"started by the schedule"
);
assert_eq!(installed_program(&ctx), Some(app));
assert_eq!(
crate::audit::read(&ctx, 1)
.iter()
.map(|e| e.subject.as_str())
.collect::<Vec<_>>(),
["install"],
"and nothing recorded"
);
let opened = the_app.with_launchd_job("application.com.datlechin.pitboard.1.2".into());
assert!(repair(&opened).expect("repaired"), "opened from Finder");
assert_eq!(installed_program(&ctx), Some(bundled));
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
#[test]
fn a_schedule_file_pitboard_did_not_write_names_no_program() {
let home = Scratch::new("foreign");
let ctx = Context::new(home.0.clone());
let installed = path(&ctx).expect("a scheduler here");
write(&installed, "not what install writes\n").expect("written");
#[cfg(target_os = "linux")]
write(
&unit_dir(&ctx).join("pitboard-renew.service"),
"[Service]\nExecStart=/bin/true\n",
)
.expect("written");
assert!(matches!(status(&ctx), Installed::Yes { .. }));
assert_eq!(installed_program(&ctx), None);
}
#[test]
fn the_schedule_belongs_to_the_default_home_alone() {
let ctx = Context::new(PathBuf::from("/home/x"));
assert!(serves(&ctx));
assert!(serves(
&ctx.clone()
.with_pitboard_home(PathBuf::from("/home/x/.pitboard/"))
));
assert!(!serves(
&ctx.with_pitboard_home(PathBuf::from("/tmp/elsewhere"))
));
}
#[cfg(target_os = "macos")]
#[test]
fn the_agent_runs_one_verb_and_does_not_run_at_load() {
let body = plist(std::path::Path::new("/usr/local/bin/pitboard"), true);
assert!(body.contains("<string>/usr/local/bin/pitboard</string>"));
assert!(body.contains("<string>renew</string>"));
assert!(!body.contains("status"), "it never asks for usage");
assert!(!body.contains("use"), "and it never switches");
assert!(
body.contains("<key>RunAtLoad</key><false/>"),
"installing it is not a reason to talk to Anthropic that second"
);
assert!(body.contains(&format!("<integer>{EVERY_SECONDS}</integer>")));
}
#[cfg(target_os = "macos")]
#[test]
fn the_agent_writes_a_path_as_xml_text() {
let body = plist(
std::path::Path::new("/Users/x/A&B <old>/Pitboard.app"),
true,
);
assert!(
body.contains("<string>/Users/x/A&B <old>/Pitboard.app</string>"),
"{body}"
);
}
#[cfg(target_os = "macos")]
#[test]
fn the_agent_keeps_a_refusal_of_the_argument_line() {
let program = std::path::Path::new("/usr/local/bin/pitboard");
let refusing = plist(program, false);
assert!(
refusing.contains("<key>PITBOARD_NO_ARGV</key><string>1</string>"),
"{refusing}"
);
assert!(!plist(program, true).contains("EnvironmentVariables"));
let (_, after) = refusing
.split_once("<key>ProgramArguments</key>")
.expect("its arguments");
assert!(
after
.trim_start()
.starts_with("<array>\n <string>/usr/local/bin/pitboard")
);
}
#[cfg(target_os = "linux")]
#[test]
fn the_unit_keeps_a_refusal_of_the_argument_line() {
let program = std::path::Path::new("/usr/local/bin/pitboard");
let refusing = service(program, false);
assert!(
refusing.contains("Environment=PITBOARD_NO_ARGV=1\n"),
"{refusing}"
);
assert!(refusing.contains("ExecStart=/usr/local/bin/pitboard renew"));
assert!(!service(program, true).contains("Environment="));
}
#[cfg(target_os = "linux")]
#[test]
fn the_unit_runs_one_verb_and_the_timer_survives_a_machine_being_off() {
let unit = service(std::path::Path::new("/usr/local/bin/pitboard"), true);
assert!(unit.contains("ExecStart=/usr/local/bin/pitboard renew"));
assert!(!unit.contains("status"));
let timer = timer();
assert!(timer.contains("Persistent=true"));
assert!(timer.contains("WantedBy=timers.target"));
}
}