pub use gregg_update::{
asset_name, compare_versions, detect_target, detect_target_for, github_urls,
is_supported_binary_target, parse_stable_version, UpdateError,
};
use gregg_update::{UpdatePlan, UpdateSpec};
use std::fmt;
use std::path::Path;
use crate::startup::{startup_state, StartupState};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UpdateOutcome {
AlreadyCurrent {
version: String,
},
UpdatedBinary {
from: String,
to: String,
},
UpdatedFromCargo {
from: String,
to: String,
},
UpdatedButRestartFailed {
from: String,
to: String,
restart_error: String,
},
}
impl fmt::Display for UpdateOutcome {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::AlreadyCurrent { version } => {
write!(f, "greggd {version} is already the latest stable version")
}
Self::UpdatedBinary { from, to } => {
write!(f, "updated greggd {from} -> {to} (GitHub binary)")
}
Self::UpdatedFromCargo { from, to } => {
write!(f, "updated greggd {from} -> {to} (Cargo)")
}
Self::UpdatedButRestartFailed {
from,
to,
restart_error,
} => write!(
f,
"updated greggd {from} -> {to} on disk but restart failed: {restart_error}"
),
}
}
}
const CRATE_NAME: &str = "greggd";
const PROGRAM: &str = "greggd";
const CURR_VERSION: &str = env!("CARGO_PKG_VERSION");
#[must_use]
pub fn update_spec() -> UpdateSpec {
UpdateSpec::new(CRATE_NAME, PROGRAM, CURR_VERSION)
}
fn restart_after_update(
state: StartupState,
exe: &Path,
config_path: &Path,
explicit: bool,
) -> Result<(), UpdateError> {
match state {
StartupState::SystemdActive
| StartupState::LaunchdLoaded
| StartupState::WindowsServiceRunning => {
crate::startup::restart_with_state(state, exe, config_path, explicit)
.map_err(|e| UpdateError::RestartFailed(format!("{e}")))
}
StartupState::SystemdInstalledStopped
| StartupState::LaunchdInstalledUnloaded
| StartupState::WindowsServiceStopped => {
eprintln!("Service is installed but stopped; leaving it stopped after update.");
Ok(())
}
StartupState::UnmanagedOrCron => {
if is_unmanaged_daemon_running(config_path, explicit) {
eprintln!("Restarting direct/cron daemon...");
crate::startup::restart_with_state(state, exe, config_path, explicit)
.map_err(|e| UpdateError::RestartFailed(format!("{e}")))
} else {
eprintln!("No daemon running; leaving binary updated without starting.");
Ok(())
}
}
}
}
pub fn run_update(config_path: &Path, explicit: bool) -> Result<UpdateOutcome, UpdateError> {
let spec = update_spec();
let plan = gregg_update::resolve_plan(&spec)?;
let (current, latest, target) = match plan {
UpdatePlan::AlreadyCurrent { version } => {
return Ok(UpdateOutcome::AlreadyCurrent { version });
}
UpdatePlan::Available {
current,
latest,
target,
} => (current, latest, target),
};
let (_exe_path, original_exe) = gregg_update::preflight_exe_writable(PROGRAM)?;
let pre_state = startup_state();
eprintln!("Current greggd {current}, latest {latest}, pre-update state: {pre_state}");
let (from_cargo, staged) =
gregg_update::prepare_candidate(&spec, ¤t, &latest, target.as_deref())?;
#[cfg(target_os = "windows")]
stop_windows_service_if_needed(pre_state)?;
gregg_update::stage::replace_current_exe(staged.path(), PROGRAM)?;
eprintln!(
"Replaced {PROGRAM} binary {current} -> {latest} via {}",
if from_cargo { "Cargo" } else { "GitHub binary" }
);
let new_version = latest;
let restart_result = restart_after_update(pre_state, &original_exe, config_path, explicit);
match restart_result {
Ok(()) => {
if from_cargo {
Ok(UpdateOutcome::UpdatedFromCargo {
from: current,
to: new_version,
})
} else {
Ok(UpdateOutcome::UpdatedBinary {
from: current,
to: new_version,
})
}
}
Err(restart_err) => {
let msg = restart_err.to_string();
eprintln!(
"Updated {PROGRAM} {current} -> {new_version} on disk but restart failed: {msg}"
);
eprintln!("Rerun: {} restart", original_exe.display());
if matches!(pre_state, StartupState::SystemdActive) {
eprintln!("Or: sudo systemctl restart greggd");
} else if matches!(pre_state, StartupState::LaunchdLoaded) {
eprintln!("Or: sudo launchctl kickstart -k system/com.eggstack.greggd");
}
Ok(UpdateOutcome::UpdatedButRestartFailed {
from: current,
to: new_version,
restart_error: msg,
})
}
}
}
#[cfg(target_os = "windows")]
fn should_quiesce_running_service(candidate_prepared: bool, state: StartupState) -> bool {
candidate_prepared && matches!(state, StartupState::WindowsServiceRunning)
}
#[cfg(target_os = "windows")]
fn stop_windows_service_if_needed(state: StartupState) -> Result<(), UpdateError> {
if should_quiesce_running_service(true, state) {
eprintln!("Stopping Windows service after candidate verification...");
crate::service::platform_service_manager()
.stop()
.map_err(|error| {
let message = error.to_string();
if message.to_ascii_lowercase().contains("access denied")
|| message.to_ascii_lowercase().contains("permission")
{
UpdateError::PermissionDenied {
message: format!("failed to stop service: {message}"),
elevated: "run as Administrator: greggd update".to_string(),
}
} else {
UpdateError::RestartFailed(format!(
"failed to stop Windows service before replacement: {message}"
))
}
})?;
}
Ok(())
}
fn is_unmanaged_daemon_running(config_path: &Path, explicit: bool) -> bool {
let Ok(config) = crate::cli::load_config(config_path, explicit) else {
return false;
};
let target = crate::cli::croncheck_target(&config);
matches!(
crate::cli::probe_health(target),
crate::cli::HealthProbe::Ready
| crate::cli::HealthProbe::Warming
| crate::cli::HealthProbe::Failed
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn spec_binds_greggd_identity() {
let spec = update_spec();
assert_eq!(spec.crate_name, "greggd");
assert_eq!(spec.program_name, "greggd");
assert_eq!(spec.current_version, env!("CARGO_PKG_VERSION"));
}
#[test]
fn outcome_strings_keep_program_prefix() {
assert_eq!(
UpdateOutcome::AlreadyCurrent {
version: "1.0.12".to_string()
}
.to_string(),
"greggd 1.0.12 is already the latest stable version"
);
assert_eq!(
UpdateOutcome::UpdatedBinary {
from: "1.0.11".to_string(),
to: "1.0.12".to_string()
}
.to_string(),
"updated greggd 1.0.11 -> 1.0.12 (GitHub binary)"
);
}
#[test]
fn shared_version_target_helpers_match_contract() {
assert_eq!(parse_stable_version("1.0.12"), Some((1, 0, 12)));
assert_eq!(
detect_target_for("windows", "x86_64"),
Some("x86_64-pc-windows-msvc".to_string())
);
assert_eq!(
asset_name("greggd", "x86_64-unknown-linux-gnu"),
"greggd-x86_64-unknown-linux-gnu"
);
let (url, sha) = github_urls("greggd", "x86_64-unknown-linux-gnu", "1.0.12");
assert_eq!(
url,
"https://github.com/eggstack/gregg/releases/download/v1.0.12/greggd-x86_64-unknown-linux-gnu"
);
assert_eq!(
sha,
"https://github.com/eggstack/gregg/releases/download/v1.0.12/greggd-x86_64-unknown-linux-gnu.sha256"
);
assert!(is_supported_binary_target("aarch64-apple-darwin"));
assert!(!is_supported_binary_target("armv7-unknown-linux-gnueabihf"));
}
#[test]
fn startup_state_helpers() {
assert_eq!(
crate::startup::systemd_state_with(true, true),
StartupState::SystemdActive
);
assert_eq!(
crate::startup::systemd_state_with(false, false),
StartupState::UnmanagedOrCron
);
}
#[test]
fn restart_decision_for_stopped_service_is_no_restart() {
let state = StartupState::SystemdInstalledStopped;
assert_ne!(state, StartupState::SystemdActive);
}
#[test]
fn candidate_is_prepared_before_any_quiesce() {
#[cfg(target_os = "windows")]
{
assert!(!should_quiesce_running_service(
false,
StartupState::WindowsServiceRunning
));
assert!(!should_quiesce_running_service(
true,
StartupState::WindowsServiceStopped
));
assert!(should_quiesce_running_service(
true,
StartupState::WindowsServiceRunning
));
}
#[cfg(not(target_os = "windows"))]
{
assert_ne!(
StartupState::SystemdInstalledStopped,
StartupState::SystemdActive
);
}
}
#[cfg(target_os = "windows")]
#[test]
fn windows_service_quiescence_requires_prepared_candidate() {
assert!(!should_quiesce_running_service(
false,
StartupState::WindowsServiceRunning
));
assert!(!should_quiesce_running_service(
true,
StartupState::WindowsServiceStopped
));
assert!(should_quiesce_running_service(
true,
StartupState::WindowsServiceRunning
));
}
}