mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
feat(aether-proxy): 支持 Alpine 主机服务安装与 musl 发布 (#291)
* feat(aether-proxy): 支持 Alpine 主机服务安装与 musl 发布 * fix(aether-proxy): address alpine support review findings
This commit is contained in:
@@ -6,6 +6,10 @@ Tunnel 模式下代理节点**无需对外监听端口**,仅需出站连接到
|
||||
|
||||
## 安装
|
||||
|
||||
`aether-proxy` 会根据宿主机自动选择服务管理器:
|
||||
- 常规 Linux 发行版:`systemd`
|
||||
- Alpine Linux:`OpenRC`
|
||||
|
||||
### Docker Compose 部署
|
||||
|
||||
```bash
|
||||
@@ -19,13 +23,15 @@ docker compose up -d
|
||||
<!-- DOWNLOAD_TABLE_START -->
|
||||
| Platform | Download |
|
||||
|----------|----------|
|
||||
| Linux x86_64 | [aether-proxy-linux-amd64.tar.gz](https://github.com/fawney19/Aether/releases/download/proxy-v0.2.5/aether-proxy-linux-amd64.tar.gz) |
|
||||
| Linux ARM64 | [aether-proxy-linux-arm64.tar.gz](https://github.com/fawney19/Aether/releases/download/proxy-v0.2.5/aether-proxy-linux-arm64.tar.gz) |
|
||||
| Linux x86_64 (GNU) | [aether-proxy-linux-amd64.tar.gz](https://github.com/fawney19/Aether/releases/download/proxy-v0.2.5/aether-proxy-linux-amd64.tar.gz) |
|
||||
| Linux ARM64 (GNU) | [aether-proxy-linux-arm64.tar.gz](https://github.com/fawney19/Aether/releases/download/proxy-v0.2.5/aether-proxy-linux-arm64.tar.gz) |
|
||||
| macOS x86_64 | [aether-proxy-macos-amd64.tar.gz](https://github.com/fawney19/Aether/releases/download/proxy-v0.2.5/aether-proxy-macos-amd64.tar.gz) |
|
||||
| macOS ARM64 | [aether-proxy-macos-arm64.tar.gz](https://github.com/fawney19/Aether/releases/download/proxy-v0.2.5/aether-proxy-macos-arm64.tar.gz) |
|
||||
| Windows x86_64 | [aether-proxy-windows-amd64.zip](https://github.com/fawney19/Aether/releases/download/proxy-v0.2.5/aether-proxy-windows-amd64.zip) |
|
||||
<!-- DOWNLOAD_TABLE_END -->
|
||||
|
||||
上表展示的是最新已发布版本的下载链接。从下一次 `proxy-v*` 发布开始,表格会自动补上 `Linux x86_64 (musl)` / `Linux ARM64 (musl)` 包,供 Alpine 等 musl 系统直接使用。
|
||||
|
||||
## 快速开始
|
||||
|
||||
```bash
|
||||
@@ -34,7 +40,7 @@ sudo ./aether-proxy setup
|
||||
|
||||
# 2. 日常管理 (勾选 Install Service 作为系统服务的情况下)
|
||||
aether-proxy status # 看状态
|
||||
aether-proxy logs # 看日志
|
||||
sudo aether-proxy logs # 看日志
|
||||
|
||||
sudo aether-proxy start # 启动服务
|
||||
sudo aether-proxy stop # 停止服务
|
||||
@@ -47,7 +53,7 @@ sudo aether-proxy setup
|
||||
sudo aether-proxy uninstall
|
||||
```
|
||||
|
||||
完成向导后, 配置自动保存到 `aether-proxy.toml`,如果启用了 Install Service,将自动注册并启动 systemd 服务。
|
||||
完成向导后, 配置自动保存到 `aether-proxy.toml`,如果启用了 Install Service,将自动注册并启动当前系统支持的服务(`systemd` 或 `OpenRC`)。
|
||||
|
||||
### 直接运行
|
||||
|
||||
@@ -132,10 +138,11 @@ sudo aether-proxy uninstall
|
||||
|
||||
### 日志落点
|
||||
|
||||
- 默认 `AETHER_PROXY_LOG_DESTINATION=stdout`,日志交给容器日志驱动或 `journald`
|
||||
- 默认 `AETHER_PROXY_LOG_DESTINATION=stdout`,日志交给容器日志驱动或宿主机服务管理器
|
||||
- 需要落盘时改成 `file` 或 `both`,并设置 `AETHER_PROXY_LOG_DIR`;setup TUI 里用 `Save Logs to File` 开关即可
|
||||
- 文件日志固定是普通 `.log` 文本;默认按天轮换,并保留 7 天
|
||||
- `docker compose` 默认保持 `stdout`,避免和容器自带日志重复;systemd 安装是否额外写文件日志由配置文件决定
|
||||
- 文件日志固定写普通文本,并支持 `hourly/daily` 轮转;默认按天轮换、保留 7 天,最多保留 30 个文件
|
||||
- `docker compose` 默认保持 `stdout`,避免和容器自带日志重复;以 `systemd` 或 `OpenRC` 安装时默认会额外打开文件日志到 `/var/log/aether-proxy`
|
||||
- OpenRC 安装时,`aether-proxy logs` 实际读取 `/var/log/aether-proxy/current.log` 和 `/var/log/aether-proxy/error.log`;这些文件通常需要用 `sudo aether-proxy logs` 查看
|
||||
|
||||
### 多服务器配置
|
||||
|
||||
@@ -163,4 +170,4 @@ node_name = "jp-proxy-02"
|
||||
```bash
|
||||
git tag proxy-v0.2.0
|
||||
git push origin proxy-v0.2.0
|
||||
```
|
||||
```
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user