#[cfg(any(target_os = "macos", target_os = "linux", windows))]
mod imp {
use crate::{Result, env};
#[cfg(target_os = "linux")]
use auto_launcher::LinuxLaunchMode;
#[cfg(target_os = "macos")]
use auto_launcher::MacOSLaunchMode;
use auto_launcher::{AutoLaunch, AutoLaunchBuilder};
use miette::IntoDiagnostic;
#[cfg(any(target_os = "macos", target_os = "linux"))]
pub(crate) fn service_args(invoking_user: Option<&str>) -> Vec<String> {
let mut args: Vec<String> = ["supervisor", "run", "--boot"]
.into_iter()
.map(String::from)
.collect();
if let Some(user) = invoking_user {
args.push(env::INVOKING_USER_FLAG.to_string());
args.push(user.to_string());
}
args
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
fn build_launcher(
app_path: &str,
args: &[String],
#[cfg(target_os = "macos")] macos_mode: MacOSLaunchMode,
#[cfg(target_os = "linux")] linux_mode: LinuxLaunchMode,
) -> Result<AutoLaunch> {
let mut builder = AutoLaunchBuilder::new();
builder
.set_app_name("pitchfork")
.set_app_path(app_path)
.set_args(args);
#[cfg(target_os = "macos")]
builder.set_macos_launch_mode(macos_mode);
#[cfg(target_os = "linux")]
builder.set_linux_launch_mode(linux_mode);
builder.build().into_diagnostic()
}
pub struct BootManager {
invoking_user: Option<String>,
current: AutoLaunch,
other: AutoLaunch,
#[cfg(target_os = "macos")]
legacy: AutoLaunch,
}
#[cfg(target_os = "macos")]
const SYSTEM_REGISTRATION: &str = "/Library/LaunchDaemons/pitchfork.plist";
#[cfg(target_os = "linux")]
const SYSTEM_REGISTRATION: &str = "/etc/systemd/system/pitchfork.service";
#[cfg(any(target_os = "macos", target_os = "linux"))]
fn registered_system_invoking_user() -> Option<String> {
let contents = match std::fs::read(SYSTEM_REGISTRATION) {
Ok(contents) => contents,
Err(err) => {
if err.kind() != std::io::ErrorKind::NotFound {
warn!("failed to read {SYSTEM_REGISTRATION}: {err}");
}
return None;
}
};
#[cfg(target_os = "macos")]
let argv = super::launchd_program_arguments(&contents);
#[cfg(target_os = "linux")]
let argv = super::systemd_exec_start(&String::from_utf8_lossy(&contents));
env::invoking_user_arg(argv?.into_iter().map(Into::into))
}
impl BootManager {
pub fn new() -> Result<Self> {
#[cfg(any(target_os = "macos", target_os = "linux"))]
return Self::with_invoking_user(env::boot_service_invoking_user()?);
#[cfg(windows)]
Self::with_invoking_user(None)
}
fn with_invoking_user(invoking_user: Option<String>) -> Result<Self> {
let app_path = env::PITCHFORK_BIN.to_string_lossy().to_string();
#[cfg(any(target_os = "macos", target_os = "linux"))]
let (current_args, other_args) =
(service_args(invoking_user.as_deref()), service_args(None));
#[cfg(target_os = "macos")]
let (current, other, legacy) = {
let is_root = nix::unistd::Uid::effective().is_root();
let (current_mode, other_mode) = if is_root {
(
MacOSLaunchMode::LaunchDaemonSystem,
MacOSLaunchMode::LaunchAgentUser,
)
} else {
(
MacOSLaunchMode::LaunchAgentUser,
MacOSLaunchMode::LaunchDaemonSystem,
)
};
(
build_launcher(&app_path, ¤t_args, current_mode)?,
build_launcher(&app_path, &other_args, other_mode)?,
build_launcher(&app_path, &other_args, MacOSLaunchMode::LaunchAgentSystem)?,
)
};
#[cfg(target_os = "linux")]
let (current, other) = {
let is_root = nix::unistd::Uid::effective().is_root();
let (current_mode, other_mode) = if is_root {
(LinuxLaunchMode::SystemdSystem, LinuxLaunchMode::SystemdUser)
} else {
(LinuxLaunchMode::SystemdUser, LinuxLaunchMode::SystemdSystem)
};
(
build_launcher(&app_path, ¤t_args, current_mode)?,
build_launcher(&app_path, &other_args, other_mode)?,
)
};
#[cfg(windows)]
let (current, other) = (
AutoLaunchBuilder::new()
.set_app_name("pitchfork")
.set_app_path(&app_path)
.set_args(&["supervisor", "run", "--boot"])
.build()
.into_diagnostic()?,
AutoLaunchBuilder::new()
.set_app_name("pitchfork")
.set_app_path(&app_path)
.set_args(&["supervisor", "run", "--boot"])
.build()
.into_diagnostic()?,
);
#[cfg(target_os = "macos")]
return Ok(Self {
invoking_user,
current,
other,
legacy,
});
#[cfg(not(target_os = "macos"))]
Ok(Self {
invoking_user,
current,
other,
})
}
pub fn invoking_user(&self) -> Option<&str> {
self.invoking_user.as_deref()
}
pub fn is_system_level_enabled(&self) -> Result<bool> {
#[cfg(any(target_os = "macos", target_os = "linux"))]
let system = if nix::unistd::Uid::effective().is_root() {
&self.current
} else {
&self.other
};
#[cfg(any(target_os = "macos", target_os = "linux"))]
return system.is_enabled().into_diagnostic();
#[cfg(windows)]
Ok(false)
}
pub fn system_invoking_user(&self) -> Option<String> {
#[cfg(any(target_os = "macos", target_os = "linux"))]
return registered_system_invoking_user();
#[cfg(windows)]
None
}
pub fn is_current_level_up_to_date(&self) -> Result<bool> {
let current_bin = env::PITCHFORK_BIN.to_string_lossy();
let registered = self.current.get_registered_app_path().into_diagnostic()?;
if registered.as_deref() != Some(current_bin.as_ref()) {
return Ok(false);
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
if nix::unistd::Uid::effective().is_root() {
return Ok(registered_system_invoking_user() == self.invoking_user);
}
Ok(true)
}
pub fn is_enabled(&self) -> Result<bool> {
#[cfg(target_os = "macos")]
return Ok(self.current.is_enabled().into_diagnostic()?
|| self.other.is_enabled().into_diagnostic()?
|| self.legacy.is_enabled().into_diagnostic()?);
#[cfg(not(target_os = "macos"))]
Ok(self.current.is_enabled().into_diagnostic()?
|| self.other.is_enabled().into_diagnostic()?)
}
pub fn is_current_level_enabled(&self) -> Result<bool> {
self.current.is_enabled().into_diagnostic()
}
pub fn is_other_level_enabled(&self) -> Result<bool> {
#[cfg(target_os = "macos")]
return Ok(self.other.is_enabled().into_diagnostic()?
|| (!nix::unistd::Uid::effective().is_root()
&& self.legacy.is_enabled().into_diagnostic()?));
#[cfg(not(target_os = "macos"))]
self.other.is_enabled().into_diagnostic()
}
#[cfg(target_os = "macos")]
pub fn cleanup_legacy(&self, migrated: bool) -> Result<()> {
if nix::unistd::Uid::effective().is_root()
&& self.legacy.is_enabled().into_diagnostic()?
{
self.legacy.disable().into_diagnostic()?;
if migrated {
info!(
"migrated legacy system-level launch entry from /Library/LaunchAgents/ to /Library/LaunchDaemons/"
);
} else {
info!("removed legacy system-level launch entry from /Library/LaunchAgents/");
}
}
Ok(())
}
pub fn enable(&self) -> Result<()> {
#[cfg(target_os = "macos")]
let other_conflict = if nix::unistd::Uid::effective().is_root() {
self.other.is_enabled().into_diagnostic()?
} else {
self.is_other_level_enabled()?
};
#[cfg(not(target_os = "macos"))]
let other_conflict = self.other.is_enabled().into_diagnostic()?;
if other_conflict {
miette::bail!(
"boot start is already registered at the other privilege level; \
run `pitchfork boot disable` (with appropriate privileges) to remove \
it first"
);
}
self.current.enable().into_diagnostic()?;
#[cfg(target_os = "macos")]
self.cleanup_legacy(true)?;
Ok(())
}
pub fn refresh(&self) -> Result<()> {
self.current.enable().into_diagnostic()?;
#[cfg(target_os = "macos")]
self.cleanup_legacy(false)?;
Ok(())
}
pub fn disable(&self) -> Result<()> {
if self.current.is_enabled().into_diagnostic()? {
self.current.disable().into_diagnostic()?;
}
if self.other.is_enabled().into_diagnostic()? {
self.other.disable().into_diagnostic()?;
}
#[cfg(target_os = "macos")]
if nix::unistd::Uid::effective().is_root()
&& self.legacy.is_enabled().into_diagnostic()?
{
self.legacy.disable().into_diagnostic()?;
}
Ok(())
}
pub fn check_and_reregister_if_stale(&self) {
let current_bin = env::PITCHFORK_BIN.to_string_lossy().to_string();
let registered = match self.current.get_registered_app_path() {
Ok(Some(path)) => path,
Ok(None) => return, Err(e) => {
warn!("failed to read registered boot path: {e}");
return;
}
};
if registered == current_bin {
return; }
info!(
"boot registration points to stale binary path '{registered}', \
re-registering with current path '{current_bin}'"
);
#[cfg(any(target_os = "macos", target_os = "linux"))]
let preserved = if nix::unistd::Uid::effective().is_root() {
let registered = registered_system_invoking_user();
if registered == self.invoking_user {
None
} else {
match Self::with_invoking_user(registered) {
Ok(manager) => Some(manager),
Err(e) => {
warn!("failed to prepare boot re-registration: {e}");
return;
}
}
}
} else {
None
};
#[cfg(windows)]
let preserved: Option<Self> = None;
let launcher = preserved.as_ref().map_or(&self.current, |m| &m.current);
if let Err(e) = launcher.enable() {
warn!("failed to re-register boot start with current path: {e}");
return;
}
info!("boot registration updated to current binary path");
}
}
}
#[cfg(not(any(target_os = "macos", target_os = "linux", windows)))]
mod imp {
use crate::Result;
pub struct BootManager;
impl BootManager {
pub fn new() -> Result<Self> {
miette::bail!(
"boot management is not supported on this platform; \
only macOS, Linux, and Windows are supported"
)
}
pub fn is_enabled(&self) -> Result<bool> {
miette::bail!(
"boot management is not supported on this platform; \
only macOS, Linux, and Windows are supported"
)
}
pub fn is_current_level_enabled(&self) -> Result<bool> {
miette::bail!(
"boot management is not supported on this platform; \
only macOS, Linux, and Windows are supported"
)
}
pub fn is_other_level_enabled(&self) -> Result<bool> {
miette::bail!(
"boot management is not supported on this platform; \
only macOS, Linux, and Windows are supported"
)
}
pub fn enable(&self) -> Result<()> {
miette::bail!(
"boot management is not supported on this platform; \
only macOS, Linux, and Windows are supported"
)
}
pub fn refresh(&self) -> Result<()> {
miette::bail!(
"boot management is not supported on this platform; \
only macOS, Linux, and Windows are supported"
)
}
pub fn invoking_user(&self) -> Option<&str> {
None
}
pub fn is_system_level_enabled(&self) -> Result<bool> {
Ok(false)
}
pub fn system_invoking_user(&self) -> Option<String> {
None
}
pub fn is_current_level_up_to_date(&self) -> Result<bool> {
Ok(false)
}
pub fn disable(&self) -> Result<()> {
miette::bail!(
"boot management is not supported on this platform; \
only macOS, Linux, and Windows are supported"
)
}
}
}
pub use imp::BootManager;
#[cfg(any(target_os = "linux", all(test, target_os = "macos")))]
fn systemd_exec_start(unit: &str) -> Option<Vec<String>> {
unit.lines()
.find_map(|line| line.trim().strip_prefix("ExecStart="))
.map(|command| command.split_whitespace().map(String::from).collect())
}
#[cfg(any(target_os = "macos", all(test, target_os = "linux")))]
fn launchd_program_arguments(plist: &[u8]) -> Option<Vec<String>> {
let value = plist::Value::from_reader(std::io::Cursor::new(plist)).ok()?;
value
.as_dictionary()?
.get("ProgramArguments")?
.as_array()?
.iter()
.map(|arg| arg.as_string().map(String::from))
.collect()
}
#[cfg(all(test, any(target_os = "macos", target_os = "linux")))]
mod tests {
use super::imp::service_args;
use super::{launchd_program_arguments, systemd_exec_start};
use crate::env::invoking_user_arg;
fn argv(args: Option<Vec<String>>) -> Vec<std::ffi::OsString> {
args.unwrap().into_iter().map(Into::into).collect()
}
#[test]
fn invoking_user_is_read_back_from_systemd_unit() {
let unit = "[Unit]\nDescription=pitchfork\nAfter=multi-user.target\n\n\
[Service]\nType=simple\n\
ExecStart=/usr/local/bin/pitchfork supervisor run --boot --invoking-user alice\n\
Restart=on-failure\n";
let args = systemd_exec_start(unit);
assert_eq!(invoking_user_arg(argv(args)).as_deref(), Some("alice"));
let legacy = "[Service]\nExecStart=/usr/local/bin/pitchfork supervisor run --boot\n";
assert_eq!(invoking_user_arg(argv(systemd_exec_start(legacy))), None);
assert_eq!(systemd_exec_start("[Service]\n"), None);
}
#[test]
fn invoking_user_is_read_back_from_launchd_plist() {
let plist = br#"<?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>pitchfork</string>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/bin/pitchfork</string>
<string>supervisor</string>
<string>run</string>
<string>--boot</string>
<string>--invoking-user</string>
<string>alice</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>SessionCreate</key>
<true/>
</dict>
</plist>"#;
let args = launchd_program_arguments(plist);
assert_eq!(invoking_user_arg(argv(args)).as_deref(), Some("alice"));
assert_eq!(launchd_program_arguments(b"not a plist"), None);
}
#[test]
fn service_args_without_invoking_user_keep_plain_boot_command() {
assert_eq!(service_args(None), ["supervisor", "run", "--boot"]);
}
#[test]
fn service_args_record_invoking_user() {
let args = service_args(Some("alice"));
assert_eq!(
args,
["supervisor", "run", "--boot", "--invoking-user", "alice"]
);
let argv = std::iter::once("pitchfork".to_string())
.chain(args)
.map(std::ffi::OsString::from);
assert_eq!(
crate::env::invoking_user_arg(argv).as_deref(),
Some("alice")
);
}
}