@@ -131,6 +131,9 @@
/// Stop the service
#[command(name = "svc-stop")]
Stop,
/// Restart the service (for picking up an upgraded binary)
#[command(name = "svc-restart")]
Restart,
/// Show service status
#[command(name = "svc-status")]
SvcStatus,
@@ -360,6 +363,7 @@
ServiceCommand::Uninstall => service_uninstall(),
ServiceCommand::Start => service_cmd("start"),
ServiceCommand::Stop => service_cmd("stop"),
ServiceCommand::Restart => service_cmd("restart"),
ServiceCommand::SvcStatus => service_cmd("status"),
}
}
@@ -511,9 +515,14 @@
let plist = dirs::home_dir()
.unwrap()
.join("Library/LaunchAgents/com.anvil.runner.plist");
// launchctl has no restart verb — emulate with unload + load. We
// ignore unload failures because the service may already be
let launchctl_action = match action {
"start" => "load",
"stop" => "unload",
// stopped; only load failures should surface as errors.
let launchctl_actions: &[&str] = match action {
"start" => &["load"],
"stop" => &["unload"],
"restart" => &["unload", "load"],
"status" => {
let output = std::process::Command::new("launchctl")
.args(["list", "com.anvil.runner"])
@@ -523,15 +532,28 @@
}
_ => return Err(format!("unknown action: {action}").into()),
};
let mut last_failure: Option<String> = None;
for (idx, launchctl_action) in launchctl_actions.iter().enumerate() {
let output = std::process::Command::new("launchctl")
.args([launchctl_action, plist.to_str().unwrap()])
.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let is_first_of_pair = action == "restart" && idx == 0;
if is_first_of_pair {
// unload-before-load failure is non-fatal
let output = std::process::Command::new("launchctl")
.args([launchctl_action, plist.to_str().unwrap()])
.output()?;
if output.status.success() {
continue;
}
last_failure = Some(format!("launchctl {launchctl_action} failed: {stderr}"));
break;
}
}
if let Some(err) = last_failure {
return Err(err.into());
output::success(&format!("Service {action}ed"));
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("launchctl {launchctl_action} failed: {stderr}").into());
}
output::success(&format!("Service {action}ed"));
} else {
let is_root = unsafe { libc::getuid() } == 0;
let mut args: Vec<&str> = Vec::new();
@@ -728,4 +750,61 @@
output::info("Use with: anvil runner configure --url <URL> --token <token>");
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
/// Wrapper so we can drive `try_parse_from` against the runner subcommand
/// in isolation without needing the whole top-level CLI.
#[derive(Parser)]
#[command(no_binary_name = true)]
struct RunnerCli {
#[command(subcommand)]
command: RunnerCommand,
}
fn parse(args: &[&str]) -> ServiceCommand {
match RunnerCli::try_parse_from(args).expect("parse").command {
RunnerCommand::Service(svc) => svc.command,
other => panic!(
"expected Service variant, got {:?}",
std::mem::discriminant(&other)
),
}
}
#[test]
fn svc_start_parses_to_start_variant() {
assert!(matches!(
parse(&["service", "svc-start"]),
ServiceCommand::Start
));
}
#[test]
fn svc_stop_parses_to_stop_variant() {
assert!(matches!(
parse(&["service", "svc-stop"]),
ServiceCommand::Stop
));
}
#[test]
fn svc_restart_parses_to_restart_variant() {
assert!(matches!(
parse(&["service", "svc-restart"]),
ServiceCommand::Restart
));
}
#[test]
fn svc_status_parses_to_status_variant() {
assert!(matches!(
parse(&["service", "svc-status"]),
ServiceCommand::SvcStatus
));
}
}