Improve CLI and daemon lifecycle usability

This commit is contained in:
Eric Wendland 2026-07-18 15:23:20 +02:00
commit f61cb44dad
14 changed files with 1028 additions and 204 deletions

View file

@ -92,7 +92,7 @@ async fn serve_local_control(node: LocalNode, listener: UnixListener) -> Result<
}
});
}
signal = tokio::signal::ctrl_c() => {
signal = shutdown_signal() => {
signal?;
tracing::info!("shutdown signal received");
return Ok(());
@ -101,6 +101,22 @@ async fn serve_local_control(node: LocalNode, listener: UnixListener) -> Result<
}
}
async fn shutdown_signal() -> Result<(), std::io::Error> {
#[cfg(unix)]
{
let mut terminate =
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())?;
tokio::select! {
result = tokio::signal::ctrl_c() => result,
_ = terminate.recv() => Ok(()),
}
}
#[cfg(not(unix))]
{
tokio::signal::ctrl_c().await
}
}
pub(crate) fn spawn_iroh_control_accept_loop(
node: LocalNode,
endpoint: GethIrohEndpoint,

View file

@ -73,6 +73,7 @@ pub struct ServiceReport {
pub manager: ServiceManager,
pub action: ServiceAction,
pub service_name: String,
pub state: Option<String>,
pub definition_path: Option<PathBuf>,
pub definition: Option<String>,
pub commands: Vec<Vec<String>>,
@ -223,39 +224,37 @@ pub fn status_user_service(manager: ServiceManager) -> Result<ServiceReport, Ser
let manager = manager.resolve()?;
match manager {
ServiceManager::SystemdUser => {
let command = run_command(
let (command, code, detail) = inspect_command(
"systemctl",
&["--user", "status", SYSTEMD_UNIT, "--no-pager"],
)?;
Ok(report(
Ok(status_report(
manager,
ServiceAction::Status,
Some(systemd_unit_path()?),
None,
vec![command],
"queried systemd user service",
command,
systemd_state(code),
&detail,
))
}
ServiceManager::LaunchdUser => {
let command = run_command("launchctl", &["list", LAUNCHD_LABEL])?;
Ok(report(
let (command, code, detail) = inspect_command("launchctl", &["list", LAUNCHD_LABEL])?;
Ok(status_report(
manager,
ServiceAction::Status,
Some(launchd_plist_path()?),
None,
vec![command],
"queried launchd user agent",
command,
launchd_state(code, &detail),
&detail,
))
}
ServiceManager::WindowsTask => {
let command = run_command("schtasks", &["/Query", "/TN", WINDOWS_TASK_NAME])?;
Ok(report(
let (command, code, detail) =
inspect_command("schtasks", &["/Query", "/TN", WINDOWS_TASK_NAME])?;
Ok(status_report(
manager,
ServiceAction::Status,
None,
None,
vec![command],
"queried Windows per-user scheduled task",
command,
windows_task_state(code, &detail),
&detail,
))
}
ServiceManager::Auto => unreachable!("auto is resolved above"),
@ -358,12 +357,12 @@ fn install_launchd_user(
}
let definition = launchd_plist(paths, executable);
std::fs::write(&plist_path, &definition)?;
let mut commands = vec![run_command(
"launchctl",
&["load", "-w", &plist_path.display().to_string()],
)?];
let mut commands = Vec::new();
if start {
commands.push(run_command("launchctl", &["start", LAUNCHD_LABEL])?);
commands.push(run_command(
"launchctl",
&["load", "-w", &plist_path.display().to_string()],
)?);
}
Ok(report(
ServiceManager::LaunchdUser,
@ -379,10 +378,9 @@ fn uninstall_launchd_user() -> Result<ServiceReport, ServiceError> {
let plist_path = launchd_plist_path()?;
let mut commands = Vec::new();
if plist_path.exists() {
commands.push(run_command(
"launchctl",
&["unload", &plist_path.display().to_string()],
)?);
let (command, _, _) =
inspect_command("launchctl", &["unload", &plist_path.display().to_string()])?;
commands.push(command);
std::fs::remove_file(&plist_path)?;
}
Ok(report(
@ -546,6 +544,83 @@ fn run_command(program: &str, args: &[&str]) -> Result<Vec<String>, ServiceError
}
}
fn inspect_command(
program: &str,
args: &[&str],
) -> Result<(Vec<String>, Option<i32>, String), ServiceError> {
let output = Command::new(program).args(args).output()?;
let command = std::iter::once(program.to_owned())
.chain(args.iter().map(|arg| (*arg).to_owned()))
.collect::<Vec<_>>();
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_owned();
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_owned();
let detail = if stdout.is_empty() { stderr } else { stdout }
.lines()
.next()
.unwrap_or_default()
.to_owned();
Ok((command, output.status.code(), detail))
}
fn systemd_state(code: Option<i32>) -> &'static str {
match code {
Some(0) => "running",
Some(3) => "not-running",
Some(4) => "not-installed",
_ => "unknown",
}
}
fn launchd_state(code: Option<i32>, detail: &str) -> &'static str {
if code == Some(0) {
"loaded"
} else if detail
.to_ascii_lowercase()
.contains("could not find service")
{
"not-loaded"
} else {
"unknown"
}
}
fn windows_task_state(code: Option<i32>, detail: &str) -> &'static str {
if code == Some(0) {
"installed"
} else {
let detail = detail.to_ascii_lowercase();
if detail.contains("cannot find") || detail.contains("does not exist") {
"not-installed"
} else {
"unknown"
}
}
}
fn status_report(
manager: ServiceManager,
definition_path: Option<PathBuf>,
command: Vec<String>,
state: &str,
detail: &str,
) -> ServiceReport {
let note = if detail.is_empty() {
format!("user service is {state}")
} else {
format!("user service is {state}: {detail}")
};
let mut report = report(
manager,
ServiceAction::Status,
definition_path,
None,
vec![command],
&note,
);
report.state = Some(state.to_owned());
report
}
fn report(
manager: ServiceManager,
action: ServiceAction,
@ -563,6 +638,7 @@ fn report(
},
manager,
action,
state: None,
definition_path,
definition,
commands,
@ -619,4 +695,35 @@ mod tests {
assert!(command.contains("GETH_HOME="));
assert!(command.contains("daemon run"));
}
#[test]
fn service_status_reports_non_running_state_without_an_action_error() {
let report = status_report(
ServiceManager::SystemdUser,
Some(PathBuf::from("/tmp/geth.service")),
vec!["systemctl".to_owned(), "status".to_owned()],
"not-running",
"Unit geth.service could not be found.",
);
assert_eq!(report.action, ServiceAction::Status);
assert_eq!(report.state.as_deref(), Some("not-running"));
assert!(report.note.contains("could not be found"));
}
#[test]
fn service_status_classification_preserves_manager_errors_as_unknown() {
assert_eq!(systemd_state(Some(0)), "running");
assert_eq!(systemd_state(Some(3)), "not-running");
assert_eq!(systemd_state(Some(4)), "not-installed");
assert_eq!(systemd_state(Some(1)), "unknown");
assert_eq!(
launchd_state(Some(1), "Could not find service local.geth.daemon"),
"not-loaded"
);
assert_eq!(launchd_state(Some(1), "operation not permitted"), "unknown");
assert_eq!(
windows_task_state(Some(1), "ERROR: The system cannot find the file specified."),
"not-installed"
);
}
}