feat(aether-proxy): 支持 Alpine 主机服务安装与 musl 发布 (#291)

* feat(aether-proxy): 支持 Alpine 主机服务安装与 musl 发布

* fix(aether-proxy): address alpine support review findings
This commit is contained in:
RWDai
2026-04-13 17:20:22 +08:00
committed by GitHub
parent 4fd2b4a014
commit 37bb120d20
6 changed files with 627 additions and 209 deletions

View File

@@ -36,12 +36,12 @@ fn build_command() -> clap::Command {
.default_value(DEFAULT_CONFIG),
),
)
.subcommand(clap::Command::new("start").about("Start the systemd service"))
.subcommand(clap::Command::new("start").about("Start the installed service"))
.subcommand(clap::Command::new("status").about("Show service status"))
.subcommand(clap::Command::new("logs").about("Tail service logs"))
.subcommand(clap::Command::new("restart").about("Restart the systemd service"))
.subcommand(clap::Command::new("stop").about("Stop the systemd service"))
.subcommand(clap::Command::new("uninstall").about("Uninstall the systemd service"))
.subcommand(clap::Command::new("restart").about("Restart the installed service"))
.subcommand(clap::Command::new("stop").about("Stop the installed service"))
.subcommand(clap::Command::new("uninstall").about("Uninstall the installed service"))
.subcommand(
clap::Command::new("upgrade")
.about("Self-upgrade from GitHub releases")
@@ -132,12 +132,17 @@ async fn handle_setup_result(outcome: setup::SetupOutcome) -> anyhow::Result<()>
}
}
/// Start the proxy server, checking for systemd conflicts first.
/// Start the proxy server, checking for managed-service conflicts first.
async fn run_proxy(config: Config) -> anyhow::Result<()> {
// Warn if systemd service is already running (would cause port conflict).
// Skip this check when we ARE the systemd service (INVOCATION_ID is set by systemd).
if std::env::var_os("INVOCATION_ID").is_none() && setup::service::is_service_active() {
eprintln!("Warning: systemd service is already running.");
// Warn if a managed service is already running (would cause conflicts).
if std::env::var_os("AETHER_PROXY_SERVICE_MANAGER").is_none()
&& std::env::var_os("INVOCATION_ID").is_none()
&& setup::service::is_service_active()
{
eprintln!(
"Warning: {} service is already running.",
setup::service::preferred_manager_name()
);
eprintln!("Use `./aether-proxy stop` to stop it first, or manage via subcommands:");
eprintln!(" ./aether-proxy status / logs / restart / stop");
std::process::exit(1);

View File

@@ -1,29 +1,270 @@
//! Systemd service installation for aether-proxy.
//! Service installation and management for `aether-proxy`.
//!
//! Called from the setup TUI when the user enables "Install Service".
//! The unit file points to the binary and config at their current
//! absolute paths -- no files are copied.
//! Supports the host-native service manager we currently target:
//! `systemd` on most Linux distributions and `OpenRC` on Alpine.
use std::fs::OpenOptions;
use std::io::ErrorKind;
use std::path::Path;
use std::process::Command;
use std::process::{Command, ExitStatus, Stdio};
const UNIT_PATH: &str = "/etc/systemd/system/aether-proxy.service";
const SERVICE_NAME: &str = "aether-proxy";
/// Whether systemd service installation is possible (systemd present + root).
pub fn is_available() -> bool {
is_systemd_available() && is_root()
const SYSTEMD_UNIT_PATH: &str = "/etc/systemd/system/aether-proxy.service";
const OPENRC_INIT_PATH: &str = "/etc/init.d/aether-proxy";
const OPENRC_PID_PATH: &str = "/run/aether-proxy.pid";
const OPENRC_LOG_DIR: &str = "/var/log/aether-proxy";
const OPENRC_STDOUT_LOG: &str = "/var/log/aether-proxy/current.log";
const OPENRC_STDERR_LOG: &str = "/var/log/aether-proxy/error.log";
const OPENRC_RUN_BINS: &[&str] = &["/sbin/openrc-run", "/usr/sbin/openrc-run", "openrc-run"];
const OPENRC_SERVICE_BINS: &[&str] = &["/sbin/rc-service", "/usr/sbin/rc-service", "rc-service"];
const OPENRC_UPDATE_BINS: &[&str] = &["/sbin/rc-update", "/usr/sbin/rc-update", "rc-update"];
const OPENRC_SUPERVISE_BINS: &[&str] = &[
"/sbin/supervise-daemon",
"/usr/sbin/supervise-daemon",
"supervise-daemon",
];
const TAIL_BINS: &[&str] = &["/usr/bin/tail", "/bin/tail", "tail"];
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ServiceManager {
Systemd,
OpenRc,
}
/// Install aether-proxy as a systemd service. Must be run as root.
pub fn install_service(config_path: &Path) -> anyhow::Result<()> {
if !is_systemd_available() {
anyhow::bail!("systemd not available");
impl ServiceManager {
fn display_name(self) -> &'static str {
match self {
Self::Systemd => "systemd",
Self::OpenRc => "OpenRC",
}
}
fn unit_path(self) -> &'static str {
match self {
Self::Systemd => SYSTEMD_UNIT_PATH,
Self::OpenRc => OPENRC_INIT_PATH,
}
}
fn is_installed(self) -> bool {
Path::new(self.unit_path()).exists()
}
}
pub fn is_available() -> bool {
detect_service_manager().is_some() && is_root()
}
pub fn preferred_manager_name() -> &'static str {
installed_manager()
.or_else(detect_service_manager)
.map(ServiceManager::display_name)
.unwrap_or("service")
}
pub fn unavailable_hint() -> String {
match detect_service_manager() {
Some(manager) if !is_root() => {
format!(
"requires root with {}, use: sudo aether-proxy setup",
manager.display_name()
)
}
Some(manager) => format!(
"{} is available but service setup is not ready",
manager.display_name()
),
None => "no supported service manager detected (systemd/OpenRC)".into(),
}
}
pub fn install_service(config_path: &Path) -> anyhow::Result<()> {
let manager = detect_service_manager()
.ok_or_else(|| anyhow::anyhow!("no supported service manager detected (systemd/OpenRC)"))?;
if !is_root() {
anyhow::bail!("root required, use: sudo ./aether-proxy setup");
}
match manager {
ServiceManager::Systemd => install_systemd_service(config_path),
ServiceManager::OpenRc => install_openrc_service(config_path),
}
}
pub(crate) fn is_root() -> bool {
#[cfg(unix)]
{
unsafe { libc::geteuid() == 0 }
}
#[cfg(not(unix))]
{
false
}
}
pub fn is_installed() -> bool {
installed_manager().is_some()
}
pub fn is_service_active() -> bool {
active_service_manager().is_some()
}
pub fn restart_active_service() -> anyhow::Result<()> {
let manager =
active_service_manager().ok_or_else(|| anyhow::anyhow!("no active service detected"))?;
restart_manager(manager)
}
pub fn uninstall_service() -> anyhow::Result<()> {
let Some(manager) = installed_manager() else {
return Ok(());
};
match manager {
ServiceManager::Systemd => uninstall_systemd_service(),
ServiceManager::OpenRc => uninstall_openrc_service(),
}
}
pub fn cmd_status() -> anyhow::Result<()> {
let manager = ensure_service_installed()?;
let status = manager_status(manager)?;
std::process::exit(status.code().unwrap_or(1));
}
pub fn cmd_logs() -> anyhow::Result<()> {
let manager = ensure_service_installed()?;
if manager == ServiceManager::OpenRc {
ensure_openrc_logs_readable()?;
}
let status = match manager {
ServiceManager::Systemd => Command::new("journalctl")
.args(["-u", SERVICE_NAME, "-f", "--no-pager", "-n", "100"])
.status()?,
ServiceManager::OpenRc => Command::new(tail_bin())
.args(["-n", "100", "-f", OPENRC_STDOUT_LOG, OPENRC_STDERR_LOG])
.status()?,
};
std::process::exit(status.code().unwrap_or(1));
}
pub fn cmd_start() -> anyhow::Result<()> {
let manager = ensure_root_and_service()?;
start_manager(manager)?;
eprintln!(" Service started.");
Ok(())
}
pub fn cmd_restart() -> anyhow::Result<()> {
let manager = ensure_root_and_service()?;
restart_manager(manager)?;
eprintln!(" Service restarted.");
Ok(())
}
pub fn cmd_stop() -> anyhow::Result<()> {
let manager = ensure_root_and_service()?;
stop_manager(manager)?;
eprintln!(" Service stopped.");
Ok(())
}
pub fn cmd_uninstall() -> anyhow::Result<()> {
ensure_root_and_service()?;
uninstall_service()?;
eprintln!();
eprintln!(" Config file, TLS certs, and logs are preserved. Remove manually if needed.");
Ok(())
}
pub(crate) fn run_cmd(program: &str, args: &[&str]) -> anyhow::Result<()> {
let display = format!("{} {}", program, args.join(" "));
eprintln!(" > {}", display);
let status = Command::new(program).args(args).status()?;
if !status.success() {
anyhow::bail!("command failed: {}", display);
}
Ok(())
}
fn detect_service_manager() -> Option<ServiceManager> {
if is_systemd_available() {
Some(ServiceManager::Systemd)
} else if is_openrc_available() {
Some(ServiceManager::OpenRc)
} else {
None
}
}
fn installed_manager() -> Option<ServiceManager> {
if let Some(manager) = detect_service_manager() {
if manager.is_installed() {
return Some(manager);
}
}
[ServiceManager::Systemd, ServiceManager::OpenRc]
.into_iter()
.find(|manager| manager.is_installed())
}
fn ensure_openrc_logs_readable() -> anyhow::Result<()> {
for path in [OPENRC_STDOUT_LOG, OPENRC_STDERR_LOG] {
match std::fs::File::open(path) {
Ok(_) => {}
Err(err) if err.kind() == ErrorKind::PermissionDenied => {
anyhow::bail!(
"OpenRC logs are stored under {} and usually require root access. Try `sudo ./aether-proxy logs`.",
OPENRC_LOG_DIR
);
}
Err(err) if err.kind() == ErrorKind::NotFound => {
anyhow::bail!(
"OpenRC log file not found at {}. Start the service first or check `./aether-proxy status`.",
path
);
}
Err(err) => return Err(err.into()),
}
}
Ok(())
}
fn active_service_manager() -> Option<ServiceManager> {
if let Some(manager) = installed_manager() {
if manager_is_active(manager) {
return Some(manager);
}
}
[ServiceManager::Systemd, ServiceManager::OpenRc]
.into_iter()
.find(|manager| manager_is_active(*manager))
}
fn ensure_service_installed() -> anyhow::Result<ServiceManager> {
installed_manager().ok_or_else(|| {
anyhow::anyhow!("service not installed, run `sudo ./aether-proxy setup` first")
})
}
fn ensure_root_and_service() -> anyhow::Result<ServiceManager> {
let manager = ensure_service_installed()?;
if !is_root() {
anyhow::bail!("root required, use: sudo ./aether-proxy <command>");
}
Ok(manager)
}
fn install_systemd_service(config_path: &Path) -> anyhow::Result<()> {
let exe_path = std::env::current_exe()?.canonicalize()?;
let exe_str = exe_path
.to_str()
@@ -40,15 +281,13 @@ pub fn install_service(config_path: &Path) -> anyhow::Result<()> {
.to_str()
.unwrap_or("/");
// Stop existing service if running (ignore errors)
if Path::new(UNIT_PATH).exists() {
if Path::new(SYSTEMD_UNIT_PATH).exists() {
eprintln!(" Stopping existing service...");
let _ = Command::new("systemctl")
.args(["stop", SERVICE_NAME])
.status();
}
// Write unit file
eprintln!(" Generating systemd unit file...");
eprintln!(" Binary: {}", exe_str);
eprintln!(" Config: {}", config_str);
@@ -63,6 +302,9 @@ pub fn install_service(config_path: &Path) -> anyhow::Result<()> {
Type=simple\n\
WorkingDirectory={working_dir}\n\
Environment=AETHER_PROXY_CONFIG={config_str}\n\
Environment=AETHER_PROXY_SERVICE_MANAGER=systemd\n\
Environment=AETHER_PROXY_LOG_DESTINATION=both\n\
Environment=AETHER_PROXY_LOG_DIR=/var/log/aether-proxy\n\
ExecStart={exe_str}\n\
Restart=on-failure\n\
RestartSec=5\n\
@@ -74,185 +316,322 @@ pub fn install_service(config_path: &Path) -> anyhow::Result<()> {
[Install]\n\
WantedBy=multi-user.target\n",
);
std::fs::write(UNIT_PATH, &unit_content)?;
std::fs::write(SYSTEMD_UNIT_PATH, &unit_content)?;
// Reload and enable
eprintln!(" Enabling and starting service...");
run_cmd("systemctl", &["daemon-reload"])?;
run_cmd("systemctl", &["enable", "--now", SERVICE_NAME])?;
// Verify
eprintln!();
let output = Command::new("systemctl")
.args(["is-active", SERVICE_NAME])
.output()?;
let state = String::from_utf8_lossy(&output.stdout).trim().to_string();
if state == "active" {
if manager_is_active(ServiceManager::Systemd) {
eprintln!(" Service started successfully!");
} else {
eprintln!(" Service state: {} (check logs)", state);
eprintln!(" Service state is not active yet. Check `sudo ./aether-proxy logs`.");
}
eprintln!();
eprintln!(" Commands:");
eprintln!(" ./aether-proxy status # service status");
eprintln!(" ./aether-proxy logs # tail logs");
eprintln!(" sudo ./aether-proxy restart # restart");
eprintln!(" sudo ./aether-proxy stop # stop");
eprintln!(" sudo ./aether-proxy uninstall # remove service");
eprintln!();
print_post_install_commands();
Ok(())
}
fn is_systemd_available() -> bool {
Command::new("systemctl")
.arg("--version")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
fn install_openrc_service(config_path: &Path) -> anyhow::Result<()> {
let exe_path = std::env::current_exe()?.canonicalize()?;
let exe_str = exe_path
.to_str()
.ok_or_else(|| anyhow::anyhow!("binary path contains invalid UTF-8"))?;
pub(crate) fn is_root() -> bool {
#[cfg(unix)]
{
unsafe { libc::geteuid() == 0 }
}
#[cfg(not(unix))]
{
false
}
}
let config_abs = std::fs::canonicalize(config_path)?;
let config_str = config_abs
.to_str()
.ok_or_else(|| anyhow::anyhow!("config path contains invalid UTF-8"))?;
/// Whether a systemd unit file is currently installed.
pub fn is_installed() -> bool {
Path::new(UNIT_PATH).exists()
}
let working_dir = config_abs
.parent()
.unwrap_or_else(|| Path::new("/"))
.to_str()
.unwrap_or("/");
/// Remove the systemd service (called from setup TUI when Install Service is toggled off).
pub fn uninstall_service() -> anyhow::Result<()> {
if !Path::new(UNIT_PATH).exists() {
return Ok(());
if Path::new(OPENRC_INIT_PATH).exists() {
eprintln!(" Stopping existing service...");
let _ = Command::new(openrc_service_bin())
.args([SERVICE_NAME, "stop"])
.status();
}
std::fs::create_dir_all(OPENRC_LOG_DIR)?;
touch_log(OPENRC_STDOUT_LOG)?;
touch_log(OPENRC_STDERR_LOG)?;
set_mode(OPENRC_LOG_DIR, 0o750)?;
set_mode(OPENRC_STDOUT_LOG, 0o640)?;
set_mode(OPENRC_STDERR_LOG, 0o640)?;
eprintln!(" Generating OpenRC init script...");
eprintln!(" Binary: {}", exe_str);
eprintln!(" Config: {}", config_str);
eprintln!(" WorkDir: {}", working_dir);
let init_content = format!(
r#"#!{}
name={}
description={}
supervisor=supervise-daemon
command={}
directory={}
pidfile={}
output_log_dir={}
output_log={}
error_log={}
supervise_daemon={}
config_env={}
service_manager_env={}
log_destination_env={}
log_dir_env={}
respawn_delay=5
respawn_max=10
respawn_period=60
depend() {{
after net
}}
start_pre() {{
checkpath --directory --mode 0750 "$output_log_dir"
checkpath --file --mode 0640 "$output_log"
checkpath --file --mode 0640 "$error_log"
}}
start() {{
ebegin "Starting ${{RC_SVCNAME}}"
"$supervise_daemon" "${{RC_SVCNAME}}" \
--start "$command" \
--pidfile "$pidfile" \
--chdir "$directory" \
--stdout "$output_log" \
--stderr "$error_log" \
--respawn-delay "$respawn_delay" \
--respawn-max "$respawn_max" \
--respawn-period "$respawn_period" \
--umask 0077 \
--env "$config_env" \
--env "$service_manager_env" \
--env "$log_destination_env" \
--env "$log_dir_env"
eend $?
}}
stop() {{
ebegin "Stopping ${{RC_SVCNAME}}"
"$supervise_daemon" "${{RC_SVCNAME}}" --stop "$command" --pidfile "$pidfile"
eend $?
}}
"#,
openrc_run_bin(),
shell_quote(SERVICE_NAME),
shell_quote("Aether Proxy"),
shell_quote(exe_str),
shell_quote(working_dir),
shell_quote(OPENRC_PID_PATH),
shell_quote(OPENRC_LOG_DIR),
shell_quote(OPENRC_STDOUT_LOG),
shell_quote(OPENRC_STDERR_LOG),
shell_quote(supervise_daemon_bin()),
shell_quote(&format!("AETHER_PROXY_CONFIG={config_str}")),
shell_quote("AETHER_PROXY_SERVICE_MANAGER=openrc"),
shell_quote("AETHER_PROXY_LOG_DESTINATION=both"),
shell_quote(&format!("AETHER_PROXY_LOG_DIR={OPENRC_LOG_DIR}")),
);
std::fs::write(OPENRC_INIT_PATH, &init_content)?;
set_mode(OPENRC_INIT_PATH, 0o755)?;
eprintln!(" Enabling and starting service...");
run_cmd(openrc_update_bin(), &["add", SERVICE_NAME, "default"])?;
run_cmd(openrc_service_bin(), &[SERVICE_NAME, "start"])?;
eprintln!();
if manager_is_active(ServiceManager::OpenRc) {
eprintln!(" Service started successfully!");
} else {
eprintln!(" Service state is not active yet. Check `sudo ./aether-proxy logs`.");
}
print_post_install_commands();
Ok(())
}
fn uninstall_systemd_service() -> anyhow::Result<()> {
eprintln!(" Stopping and removing existing service...");
let _ = Command::new("systemctl")
.args(["disable", "--now", SERVICE_NAME])
.status();
std::fs::remove_file(UNIT_PATH)?;
eprintln!(" Removed {}", UNIT_PATH);
if Path::new(SYSTEMD_UNIT_PATH).exists() {
std::fs::remove_file(SYSTEMD_UNIT_PATH)?;
eprintln!(" Removed {}", SYSTEMD_UNIT_PATH);
}
run_cmd("systemctl", &["daemon-reload"])?;
eprintln!(" Service uninstalled.");
eprintln!();
Ok(())
}
/// Check if the systemd service is currently active.
pub fn is_service_active() -> bool {
std::path::Path::new(UNIT_PATH).exists()
fn uninstall_openrc_service() -> anyhow::Result<()> {
eprintln!(" Stopping and removing existing service...");
let _ = Command::new(openrc_service_bin())
.args([SERVICE_NAME, "stop"])
.status();
let _ = Command::new(openrc_update_bin())
.args(["del", SERVICE_NAME, "default"])
.status();
if Path::new(OPENRC_INIT_PATH).exists() {
std::fs::remove_file(OPENRC_INIT_PATH)?;
eprintln!(" Removed {}", OPENRC_INIT_PATH);
}
eprintln!(" Service uninstalled.");
Ok(())
}
fn start_manager(manager: ServiceManager) -> anyhow::Result<()> {
match manager {
ServiceManager::Systemd => run_cmd("systemctl", &["start", SERVICE_NAME]),
ServiceManager::OpenRc => run_cmd(openrc_service_bin(), &[SERVICE_NAME, "start"]),
}
}
fn stop_manager(manager: ServiceManager) -> anyhow::Result<()> {
match manager {
ServiceManager::Systemd => run_cmd("systemctl", &["stop", SERVICE_NAME]),
ServiceManager::OpenRc => run_cmd(openrc_service_bin(), &[SERVICE_NAME, "stop"]),
}
}
fn restart_manager(manager: ServiceManager) -> anyhow::Result<()> {
match manager {
ServiceManager::Systemd => run_cmd("systemctl", &["restart", SERVICE_NAME]),
ServiceManager::OpenRc => run_cmd(openrc_service_bin(), &[SERVICE_NAME, "restart"]),
}
}
fn manager_status(manager: ServiceManager) -> anyhow::Result<ExitStatus> {
let status = match manager {
ServiceManager::Systemd => Command::new("systemctl")
.args(["status", SERVICE_NAME])
.status()?,
ServiceManager::OpenRc => Command::new(openrc_service_bin())
.args([SERVICE_NAME, "status"])
.status()?,
};
Ok(status)
}
fn manager_is_active(manager: ServiceManager) -> bool {
match manager {
ServiceManager::Systemd => {
Path::new(SYSTEMD_UNIT_PATH).exists()
&& Command::new("systemctl")
.args(["is-active", "--quiet", SERVICE_NAME])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|status| status.success())
.unwrap_or(false)
}
ServiceManager::OpenRc => {
Path::new(OPENRC_INIT_PATH).exists()
&& Command::new(openrc_service_bin())
.args([SERVICE_NAME, "status"])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|status| status.success())
.unwrap_or(false)
}
}
}
fn print_post_install_commands() {
eprintln!();
eprintln!(" Commands:");
eprintln!(" ./aether-proxy status # service status");
eprintln!(" sudo ./aether-proxy logs # tail logs");
eprintln!(" sudo ./aether-proxy restart # restart");
eprintln!(" sudo ./aether-proxy stop # stop");
eprintln!(" sudo ./aether-proxy uninstall # remove service");
eprintln!();
}
fn is_systemd_available() -> bool {
Path::new("/run/systemd/system").exists()
&& Command::new("systemctl")
.args(["is-active", "--quiet", SERVICE_NAME])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.map(|status| status.success())
.unwrap_or(false)
}
// ── CLI subcommands (systemd wrappers) ──────────────────────────────────────
fn is_openrc_available() -> bool {
(Path::new("/run/openrc").exists() || Path::new("/run/openrc/softlevel").exists())
&& has_absolute_candidate(OPENRC_RUN_BINS)
&& has_absolute_candidate(OPENRC_SERVICE_BINS)
&& has_absolute_candidate(OPENRC_UPDATE_BINS)
&& has_absolute_candidate(OPENRC_SUPERVISE_BINS)
}
fn ensure_service_installed() -> anyhow::Result<()> {
if !std::path::Path::new(UNIT_PATH).exists() {
anyhow::bail!("service not installed, run `sudo ./aether-proxy setup` first");
}
fn has_absolute_candidate(candidates: &[&str]) -> bool {
candidates
.iter()
.any(|candidate| candidate.starts_with('/') && Path::new(candidate).exists())
}
fn openrc_run_bin() -> &'static str {
pick_bin(OPENRC_RUN_BINS)
}
fn openrc_service_bin() -> &'static str {
pick_bin(OPENRC_SERVICE_BINS)
}
fn openrc_update_bin() -> &'static str {
pick_bin(OPENRC_UPDATE_BINS)
}
fn supervise_daemon_bin() -> &'static str {
pick_bin(OPENRC_SUPERVISE_BINS)
}
fn tail_bin() -> &'static str {
pick_bin(TAIL_BINS)
}
fn pick_bin(candidates: &[&'static str]) -> &'static str {
candidates
.iter()
.copied()
.find(|candidate| candidate.starts_with('/') && Path::new(candidate).exists())
.unwrap_or_else(|| candidates[candidates.len() - 1])
}
fn shell_quote(value: &str) -> String {
format!("'{}'", value.replace('\'', "'\"'\"'"))
}
fn touch_log(path: &str) -> anyhow::Result<()> {
OpenOptions::new().create(true).append(true).open(path)?;
Ok(())
}
fn ensure_root_and_service() -> anyhow::Result<()> {
ensure_service_installed()?;
if !is_root() {
anyhow::bail!("root required, use: sudo ./aether-proxy <command>");
}
Ok(())
}
/// `aether-proxy status` -- show service status.
pub fn cmd_status() -> anyhow::Result<()> {
ensure_service_installed()?;
let status = Command::new("systemctl")
.args(["status", SERVICE_NAME])
.status()?;
// systemctl status returns non-zero when inactive; that's fine
std::process::exit(status.code().unwrap_or(1));
}
/// `aether-proxy logs` -- tail service logs.
pub fn cmd_logs() -> anyhow::Result<()> {
ensure_service_installed()?;
let status = Command::new("journalctl")
.args(["-u", SERVICE_NAME, "-f", "--no-pager", "-n", "100"])
.status()?;
std::process::exit(status.code().unwrap_or(1));
}
/// `aether-proxy start` -- start the service.
pub fn cmd_start() -> anyhow::Result<()> {
ensure_root_and_service()?;
run_cmd("systemctl", &["start", SERVICE_NAME])?;
eprintln!(" Service started.");
Ok(())
}
/// `aether-proxy restart` -- restart the service.
pub fn cmd_restart() -> anyhow::Result<()> {
ensure_root_and_service()?;
run_cmd("systemctl", &["restart", SERVICE_NAME])?;
eprintln!(" Service restarted.");
Ok(())
}
/// `aether-proxy stop` -- stop the service.
pub fn cmd_stop() -> anyhow::Result<()> {
ensure_root_and_service()?;
run_cmd("systemctl", &["stop", SERVICE_NAME])?;
eprintln!(" Service stopped.");
Ok(())
}
/// `aether-proxy uninstall` -- disable and remove the systemd service.
pub fn cmd_uninstall() -> anyhow::Result<()> {
ensure_root_and_service()?;
eprintln!(" Stopping and disabling service...");
let _ = Command::new("systemctl")
.args(["disable", "--now", SERVICE_NAME])
.status();
if std::path::Path::new(UNIT_PATH).exists() {
std::fs::remove_file(UNIT_PATH)?;
eprintln!(" Removed {}", UNIT_PATH);
fn set_mode(path: &str, mode: u32) -> anyhow::Result<()> {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = std::fs::Permissions::from_mode(mode);
std::fs::set_permissions(path, perms)?;
}
run_cmd("systemctl", &["daemon-reload"])?;
eprintln!(" Service uninstalled.");
eprintln!();
eprintln!(" Config file and TLS certs are preserved. Remove manually if needed.");
#[cfg(not(unix))]
let _ = (path, mode);
Ok(())
}
pub(crate) fn run_cmd(program: &str, args: &[&str]) -> anyhow::Result<()> {
let display = format!("{} {}", program, args.join(" "));
eprintln!(" > {}", display);
let status = Command::new(program).args(args).status()?;
if !status.success() {
anyhow::bail!("command failed: {}", display);
}
Ok(())
}

View File

@@ -28,7 +28,7 @@ use crate::config::{
/// Outcome of the setup wizard, returned to the caller.
pub enum SetupOutcome {
/// Config saved; systemd service installed and started.
/// Config saved and the selected host service was installed.
ServiceInstalled,
/// Config saved; no service -- caller should start the proxy directly.
ReadyToRun(PathBuf),
@@ -148,7 +148,7 @@ impl App {
.into(),
kind: FieldKind::Bool,
required: false,
help: "Install as systemd service (requires root) -- Enter to toggle",
help: "Install as managed service (requires root) -- Enter to toggle",
},
Field {
label: "Log Level",
@@ -532,11 +532,8 @@ impl App {
&& toggled == "true"
&& !super::service::is_available()
{
self.message = Some((
"requires root with systemd, use: sudo aether-proxy setup".into(),
Instant::now(),
true,
));
self.message =
Some((super::service::unavailable_hint(), Instant::now(), true));
} else {
self.selected_field_mut().value = toggled.into();
self.modified = true;

View File

@@ -1,7 +1,8 @@
//! Self-upgrade for aether-proxy.
//! Self-upgrade support for `aether-proxy`.
//!
//! Downloads a release from GitHub, verifies SHA256 checksum, and atomically
//! replaces the running binary. Restarts the systemd service if active.
//! Downloads a release from GitHub, verifies the SHA256 checksum, replaces the
//! running binary atomically, and restarts the active managed service when
//! applicable.
use std::path::{Path, PathBuf};
@@ -23,7 +24,14 @@ struct GithubRelease {
// ── Platform detection ───────────────────────────────────────────────────────
fn detect_platform() -> &'static str {
if cfg!(target_os = "linux") && cfg!(target_arch = "x86_64") {
if cfg!(target_os = "linux") && cfg!(target_arch = "x86_64") && cfg!(target_env = "musl") {
"linux-musl-amd64"
} else if cfg!(target_os = "linux")
&& cfg!(target_arch = "aarch64")
&& cfg!(target_env = "musl")
{
"linux-musl-arm64"
} else if cfg!(target_os = "linux") && cfg!(target_arch = "x86_64") {
"linux-amd64"
} else if cfg!(target_os = "linux") && cfg!(target_arch = "aarch64") {
"linux-arm64"
@@ -361,34 +369,33 @@ async fn execute_upgrade(
match restart_mode {
RestartMode::BestEffort => {
// Restart systemd service if running.
// Use best-effort: binary is already replaced, so a restart failure should
// not abort the whole upgrade -- the user can restart manually.
if super::service::is_service_active() {
if super::service::is_root() {
eprintln!(" Restarting systemd service...");
match super::service::run_cmd("systemctl", &["restart", "aether-proxy"]) {
eprintln!(" Restarting managed service...");
match super::service::restart_active_service() {
Ok(()) => eprintln!(" Service restarted."),
Err(e) => {
eprintln!(" WARNING: failed to restart service: {}", e);
eprintln!(" Run manually: sudo systemctl restart aether-proxy");
eprintln!(" Run manually: sudo aether-proxy restart");
}
}
} else {
eprintln!(" Systemd service is active, but restart requires root.");
eprintln!(" Run: sudo systemctl restart aether-proxy");
eprintln!(" Managed service is active, but restart requires root.");
eprintln!(" Run: sudo aether-proxy restart");
eprintln!(" Skipping restart.");
}
} else {
eprintln!(" No active systemd service detected, skipping restart.");
eprintln!(" No active service detected, skipping restart.");
}
}
RestartMode::Required => {
if !super::service::is_root() {
anyhow::bail!("automatic upgrade requires root privileges");
}
eprintln!(" Restarting systemd service...");
super::service::run_cmd("systemctl", &["restart", "aether-proxy"])?;
eprintln!(" Restarting managed service...");
super::service::restart_active_service()?;
eprintln!(" Service restarted.");
}
}
@@ -409,8 +416,8 @@ pub async fn cmd_upgrade(version: Option<String>) -> anyhow::Result<()> {
/// Perform automatic upgrade to a specific version.
///
/// This path is designed for server-pushed upgrades in systemd/root scenarios:
/// it requires root and requires a successful `systemctl restart aether-proxy`.
/// This path is used for server-pushed upgrades: it requires root and expects
/// the currently active managed service to restart successfully.
pub async fn perform_upgrade(version: &str) -> anyhow::Result<()> {
execute_upgrade(Some(version), true, RestartMode::Required).await
}