mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +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:
51
.github/workflows/build-proxy.yml
vendored
51
.github/workflows/build-proxy.yml
vendored
@@ -30,6 +30,14 @@ jobs:
|
|||||||
target: aarch64-unknown-linux-gnu
|
target: aarch64-unknown-linux-gnu
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
use_cross: true
|
use_cross: true
|
||||||
|
- name: linux-musl-amd64
|
||||||
|
target: x86_64-unknown-linux-musl
|
||||||
|
os: ubuntu-latest
|
||||||
|
use_cross: true
|
||||||
|
- name: linux-musl-arm64
|
||||||
|
target: aarch64-unknown-linux-musl
|
||||||
|
os: ubuntu-latest
|
||||||
|
use_cross: true
|
||||||
- name: macos-amd64
|
- name: macos-amd64
|
||||||
target: x86_64-apple-darwin
|
target: x86_64-apple-darwin
|
||||||
os: macos-latest
|
os: macos-latest
|
||||||
@@ -54,7 +62,7 @@ jobs:
|
|||||||
- name: Rust cache
|
- name: Rust cache
|
||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
workspaces: aether-proxy -> target
|
workspaces: apps/aether-proxy -> target
|
||||||
key: ${{ matrix.target }}
|
key: ${{ matrix.target }}
|
||||||
|
|
||||||
- name: Install cross
|
- name: Install cross
|
||||||
@@ -62,7 +70,7 @@ jobs:
|
|||||||
uses: taiki-e/install-action@cross
|
uses: taiki-e/install-action@cross
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
working-directory: aether-proxy
|
working-directory: apps/aether-proxy
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
if [ "${{ matrix.use_cross }}" = "true" ]; then
|
if [ "${{ matrix.use_cross }}" = "true" ]; then
|
||||||
@@ -75,16 +83,16 @@ jobs:
|
|||||||
if: runner.os != 'Windows'
|
if: runner.os != 'Windows'
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
cd aether-proxy/target/${{ matrix.target }}/release
|
cd target/${{ matrix.target }}/release
|
||||||
chmod +x aether-proxy
|
chmod +x aether-proxy
|
||||||
tar czf ../../../../aether-proxy-${{ matrix.name }}.tar.gz aether-proxy
|
tar czf ../../../aether-proxy-${{ matrix.name }}.tar.gz aether-proxy
|
||||||
|
|
||||||
- name: Package (Windows)
|
- name: Package (Windows)
|
||||||
if: runner.os == 'Windows'
|
if: runner.os == 'Windows'
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
cd aether-proxy/target/${{ matrix.target }}/release
|
cd target/${{ matrix.target }}/release
|
||||||
7z a ../../../../aether-proxy-${{ matrix.name }}.zip aether-proxy.exe
|
7z a ../../../aether-proxy-${{ matrix.name }}.zip aether-proxy.exe
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v5
|
||||||
@@ -136,9 +144,9 @@ jobs:
|
|||||||
|
|
||||||
- name: Prepare binaries
|
- name: Prepare binaries
|
||||||
run: |
|
run: |
|
||||||
mkdir -p aether-proxy/build/linux-amd64 aether-proxy/build/linux-arm64
|
mkdir -p apps/aether-proxy/build/linux-amd64 apps/aether-proxy/build/linux-arm64
|
||||||
tar xzf artifacts/aether-proxy-linux-amd64.tar.gz -C aether-proxy/build/linux-amd64
|
tar xzf artifacts/aether-proxy-linux-amd64.tar.gz -C apps/aether-proxy/build/linux-amd64
|
||||||
tar xzf artifacts/aether-proxy-linux-arm64.tar.gz -C aether-proxy/build/linux-arm64
|
tar xzf artifacts/aether-proxy-linux-arm64.tar.gz -C apps/aether-proxy/build/linux-arm64
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
@@ -176,7 +184,7 @@ jobs:
|
|||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: ./aether-proxy
|
context: ./apps/aether-proxy
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
@@ -197,11 +205,20 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
VERSION="${TAG#proxy-v}"
|
VERSION="${TAG#proxy-v}"
|
||||||
BASE="https://github.com/fawney19/Aether/releases/download/${TAG}"
|
BASE="https://github.com/fawney19/Aether/releases/download/${TAG}"
|
||||||
cd aether-proxy
|
|
||||||
|
if [ -d apps/aether-proxy ]; then
|
||||||
|
PROXY_DIR="apps/aether-proxy"
|
||||||
|
else
|
||||||
|
PROXY_DIR="aether-proxy"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "$PROXY_DIR"
|
||||||
|
|
||||||
TABLE="| Platform | Download |\n|----------|----------|\n"
|
TABLE="| Platform | Download |\n|----------|----------|\n"
|
||||||
TABLE+="| Linux x86_64 | [aether-proxy-linux-amd64.tar.gz](${BASE}/aether-proxy-linux-amd64.tar.gz) |\n"
|
TABLE+="| Linux x86_64 (GNU) | [aether-proxy-linux-amd64.tar.gz](${BASE}/aether-proxy-linux-amd64.tar.gz) |\n"
|
||||||
TABLE+="| Linux ARM64 | [aether-proxy-linux-arm64.tar.gz](${BASE}/aether-proxy-linux-arm64.tar.gz) |\n"
|
TABLE+="| Linux ARM64 (GNU) | [aether-proxy-linux-arm64.tar.gz](${BASE}/aether-proxy-linux-arm64.tar.gz) |\n"
|
||||||
|
TABLE+="| Linux x86_64 (musl) | [aether-proxy-linux-musl-amd64.tar.gz](${BASE}/aether-proxy-linux-musl-amd64.tar.gz) |\n"
|
||||||
|
TABLE+="| Linux ARM64 (musl) | [aether-proxy-linux-musl-arm64.tar.gz](${BASE}/aether-proxy-linux-musl-arm64.tar.gz) |\n"
|
||||||
TABLE+="| macOS x86_64 | [aether-proxy-macos-amd64.tar.gz](${BASE}/aether-proxy-macos-amd64.tar.gz) |\n"
|
TABLE+="| macOS x86_64 | [aether-proxy-macos-amd64.tar.gz](${BASE}/aether-proxy-macos-amd64.tar.gz) |\n"
|
||||||
TABLE+="| macOS ARM64 | [aether-proxy-macos-arm64.tar.gz](${BASE}/aether-proxy-macos-arm64.tar.gz) |\n"
|
TABLE+="| macOS ARM64 | [aether-proxy-macos-arm64.tar.gz](${BASE}/aether-proxy-macos-arm64.tar.gz) |\n"
|
||||||
TABLE+="| Windows x86_64 | [aether-proxy-windows-amd64.zip](${BASE}/aether-proxy-windows-amd64.zip) |"
|
TABLE+="| Windows x86_64 | [aether-proxy-windows-amd64.zip](${BASE}/aether-proxy-windows-amd64.zip) |"
|
||||||
@@ -217,7 +234,13 @@ jobs:
|
|||||||
|
|
||||||
- name: Commit and push
|
- name: Commit and push
|
||||||
run: |
|
run: |
|
||||||
cd aether-proxy
|
if [ -d apps/aether-proxy ]; then
|
||||||
|
PROXY_DIR="apps/aether-proxy"
|
||||||
|
else
|
||||||
|
PROXY_DIR="aether-proxy"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "$PROXY_DIR"
|
||||||
git config user.name "github-actions[bot]"
|
git config user.name "github-actions[bot]"
|
||||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
git add README.md
|
git add README.md
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ Tunnel 模式下代理节点**无需对外监听端口**,仅需出站连接到
|
|||||||
|
|
||||||
## 安装
|
## 安装
|
||||||
|
|
||||||
|
`aether-proxy` 会根据宿主机自动选择服务管理器:
|
||||||
|
- 常规 Linux 发行版:`systemd`
|
||||||
|
- Alpine Linux:`OpenRC`
|
||||||
|
|
||||||
### Docker Compose 部署
|
### Docker Compose 部署
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -19,13 +23,15 @@ docker compose up -d
|
|||||||
<!-- DOWNLOAD_TABLE_START -->
|
<!-- DOWNLOAD_TABLE_START -->
|
||||||
| Platform | Download |
|
| 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 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 | [aether-proxy-linux-arm64.tar.gz](https://github.com/fawney19/Aether/releases/download/proxy-v0.2.5/aether-proxy-linux-arm64.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 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) |
|
| 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) |
|
| 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 -->
|
<!-- DOWNLOAD_TABLE_END -->
|
||||||
|
|
||||||
|
上表展示的是最新已发布版本的下载链接。从下一次 `proxy-v*` 发布开始,表格会自动补上 `Linux x86_64 (musl)` / `Linux ARM64 (musl)` 包,供 Alpine 等 musl 系统直接使用。
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -34,7 +40,7 @@ sudo ./aether-proxy setup
|
|||||||
|
|
||||||
# 2. 日常管理 (勾选 Install Service 作为系统服务的情况下)
|
# 2. 日常管理 (勾选 Install Service 作为系统服务的情况下)
|
||||||
aether-proxy status # 看状态
|
aether-proxy status # 看状态
|
||||||
aether-proxy logs # 看日志
|
sudo aether-proxy logs # 看日志
|
||||||
|
|
||||||
sudo aether-proxy start # 启动服务
|
sudo aether-proxy start # 启动服务
|
||||||
sudo aether-proxy stop # 停止服务
|
sudo aether-proxy stop # 停止服务
|
||||||
@@ -47,7 +53,7 @@ sudo aether-proxy setup
|
|||||||
sudo aether-proxy uninstall
|
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` 开关即可
|
- 需要落盘时改成 `file` 或 `both`,并设置 `AETHER_PROXY_LOG_DIR`;setup TUI 里用 `Save Logs to File` 开关即可
|
||||||
- 文件日志固定是普通 `.log` 文本;默认按天轮换,并保留 7 天
|
- 文件日志固定写普通文本,并支持 `hourly/daily` 轮转;默认按天轮换、保留 7 天,最多保留 30 个文件
|
||||||
- `docker compose` 默认保持 `stdout`,避免和容器自带日志重复;systemd 安装是否额外写文件日志由配置文件决定
|
- `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
|
```bash
|
||||||
git tag proxy-v0.2.0
|
git tag proxy-v0.2.0
|
||||||
git push origin proxy-v0.2.0
|
git push origin proxy-v0.2.0
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -36,12 +36,12 @@ fn build_command() -> clap::Command {
|
|||||||
.default_value(DEFAULT_CONFIG),
|
.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("status").about("Show service status"))
|
||||||
.subcommand(clap::Command::new("logs").about("Tail service logs"))
|
.subcommand(clap::Command::new("logs").about("Tail service logs"))
|
||||||
.subcommand(clap::Command::new("restart").about("Restart the systemd service"))
|
.subcommand(clap::Command::new("restart").about("Restart the installed service"))
|
||||||
.subcommand(clap::Command::new("stop").about("Stop the systemd service"))
|
.subcommand(clap::Command::new("stop").about("Stop the installed service"))
|
||||||
.subcommand(clap::Command::new("uninstall").about("Uninstall the systemd service"))
|
.subcommand(clap::Command::new("uninstall").about("Uninstall the installed service"))
|
||||||
.subcommand(
|
.subcommand(
|
||||||
clap::Command::new("upgrade")
|
clap::Command::new("upgrade")
|
||||||
.about("Self-upgrade from GitHub releases")
|
.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<()> {
|
async fn run_proxy(config: Config) -> anyhow::Result<()> {
|
||||||
// Warn if systemd service is already running (would cause port conflict).
|
// Warn if a managed service is already running (would cause conflicts).
|
||||||
// Skip this check when we ARE the systemd service (INVOCATION_ID is set by systemd).
|
if std::env::var_os("AETHER_PROXY_SERVICE_MANAGER").is_none()
|
||||||
if std::env::var_os("INVOCATION_ID").is_none() && setup::service::is_service_active() {
|
&& std::env::var_os("INVOCATION_ID").is_none()
|
||||||
eprintln!("Warning: systemd service is already running.");
|
&& 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!("Use `./aether-proxy stop` to stop it first, or manage via subcommands:");
|
||||||
eprintln!(" ./aether-proxy status / logs / restart / stop");
|
eprintln!(" ./aether-proxy status / logs / restart / stop");
|
||||||
std::process::exit(1);
|
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".
|
//! Supports the host-native service manager we currently target:
|
||||||
//! The unit file points to the binary and config at their current
|
//! `systemd` on most Linux distributions and `OpenRC` on Alpine.
|
||||||
//! absolute paths -- no files are copied.
|
|
||||||
|
|
||||||
|
use std::fs::OpenOptions;
|
||||||
|
use std::io::ErrorKind;
|
||||||
use std::path::Path;
|
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";
|
const SERVICE_NAME: &str = "aether-proxy";
|
||||||
|
|
||||||
/// Whether systemd service installation is possible (systemd present + root).
|
const SYSTEMD_UNIT_PATH: &str = "/etc/systemd/system/aether-proxy.service";
|
||||||
pub fn is_available() -> bool {
|
|
||||||
is_systemd_available() && is_root()
|
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.
|
impl ServiceManager {
|
||||||
pub fn install_service(config_path: &Path) -> anyhow::Result<()> {
|
fn display_name(self) -> &'static str {
|
||||||
if !is_systemd_available() {
|
match self {
|
||||||
anyhow::bail!("systemd not available");
|
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() {
|
if !is_root() {
|
||||||
anyhow::bail!("root required, use: sudo ./aether-proxy setup");
|
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_path = std::env::current_exe()?.canonicalize()?;
|
||||||
let exe_str = exe_path
|
let exe_str = exe_path
|
||||||
.to_str()
|
.to_str()
|
||||||
@@ -40,15 +281,13 @@ pub fn install_service(config_path: &Path) -> anyhow::Result<()> {
|
|||||||
.to_str()
|
.to_str()
|
||||||
.unwrap_or("/");
|
.unwrap_or("/");
|
||||||
|
|
||||||
// Stop existing service if running (ignore errors)
|
if Path::new(SYSTEMD_UNIT_PATH).exists() {
|
||||||
if Path::new(UNIT_PATH).exists() {
|
|
||||||
eprintln!(" Stopping existing service...");
|
eprintln!(" Stopping existing service...");
|
||||||
let _ = Command::new("systemctl")
|
let _ = Command::new("systemctl")
|
||||||
.args(["stop", SERVICE_NAME])
|
.args(["stop", SERVICE_NAME])
|
||||||
.status();
|
.status();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write unit file
|
|
||||||
eprintln!(" Generating systemd unit file...");
|
eprintln!(" Generating systemd unit file...");
|
||||||
eprintln!(" Binary: {}", exe_str);
|
eprintln!(" Binary: {}", exe_str);
|
||||||
eprintln!(" Config: {}", config_str);
|
eprintln!(" Config: {}", config_str);
|
||||||
@@ -63,6 +302,9 @@ pub fn install_service(config_path: &Path) -> anyhow::Result<()> {
|
|||||||
Type=simple\n\
|
Type=simple\n\
|
||||||
WorkingDirectory={working_dir}\n\
|
WorkingDirectory={working_dir}\n\
|
||||||
Environment=AETHER_PROXY_CONFIG={config_str}\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\
|
ExecStart={exe_str}\n\
|
||||||
Restart=on-failure\n\
|
Restart=on-failure\n\
|
||||||
RestartSec=5\n\
|
RestartSec=5\n\
|
||||||
@@ -74,185 +316,322 @@ pub fn install_service(config_path: &Path) -> anyhow::Result<()> {
|
|||||||
[Install]\n\
|
[Install]\n\
|
||||||
WantedBy=multi-user.target\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...");
|
eprintln!(" Enabling and starting service...");
|
||||||
run_cmd("systemctl", &["daemon-reload"])?;
|
run_cmd("systemctl", &["daemon-reload"])?;
|
||||||
run_cmd("systemctl", &["enable", "--now", SERVICE_NAME])?;
|
run_cmd("systemctl", &["enable", "--now", SERVICE_NAME])?;
|
||||||
|
|
||||||
// Verify
|
|
||||||
eprintln!();
|
eprintln!();
|
||||||
let output = Command::new("systemctl")
|
if manager_is_active(ServiceManager::Systemd) {
|
||||||
.args(["is-active", SERVICE_NAME])
|
|
||||||
.output()?;
|
|
||||||
let state = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
|
||||||
|
|
||||||
if state == "active" {
|
|
||||||
eprintln!(" Service started successfully!");
|
eprintln!(" Service started successfully!");
|
||||||
} else {
|
} else {
|
||||||
eprintln!(" Service state: {} (check logs)", state);
|
eprintln!(" Service state is not active yet. Check `sudo ./aether-proxy logs`.");
|
||||||
}
|
}
|
||||||
|
|
||||||
eprintln!();
|
print_post_install_commands();
|
||||||
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!();
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_systemd_available() -> bool {
|
fn install_openrc_service(config_path: &Path) -> anyhow::Result<()> {
|
||||||
Command::new("systemctl")
|
let exe_path = std::env::current_exe()?.canonicalize()?;
|
||||||
.arg("--version")
|
let exe_str = exe_path
|
||||||
.stdout(std::process::Stdio::null())
|
.to_str()
|
||||||
.stderr(std::process::Stdio::null())
|
.ok_or_else(|| anyhow::anyhow!("binary path contains invalid UTF-8"))?;
|
||||||
.status()
|
|
||||||
.map(|s| s.success())
|
|
||||||
.unwrap_or(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn is_root() -> bool {
|
let config_abs = std::fs::canonicalize(config_path)?;
|
||||||
#[cfg(unix)]
|
let config_str = config_abs
|
||||||
{
|
.to_str()
|
||||||
unsafe { libc::geteuid() == 0 }
|
.ok_or_else(|| anyhow::anyhow!("config path contains invalid UTF-8"))?;
|
||||||
}
|
|
||||||
#[cfg(not(unix))]
|
|
||||||
{
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether a systemd unit file is currently installed.
|
let working_dir = config_abs
|
||||||
pub fn is_installed() -> bool {
|
.parent()
|
||||||
Path::new(UNIT_PATH).exists()
|
.unwrap_or_else(|| Path::new("/"))
|
||||||
}
|
.to_str()
|
||||||
|
.unwrap_or("/");
|
||||||
|
|
||||||
/// Remove the systemd service (called from setup TUI when Install Service is toggled off).
|
if Path::new(OPENRC_INIT_PATH).exists() {
|
||||||
pub fn uninstall_service() -> anyhow::Result<()> {
|
eprintln!(" Stopping existing service...");
|
||||||
if !Path::new(UNIT_PATH).exists() {
|
let _ = Command::new(openrc_service_bin())
|
||||||
return Ok(());
|
.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...");
|
eprintln!(" Stopping and removing existing service...");
|
||||||
let _ = Command::new("systemctl")
|
let _ = Command::new("systemctl")
|
||||||
.args(["disable", "--now", SERVICE_NAME])
|
.args(["disable", "--now", SERVICE_NAME])
|
||||||
.status();
|
.status();
|
||||||
|
|
||||||
std::fs::remove_file(UNIT_PATH)?;
|
if Path::new(SYSTEMD_UNIT_PATH).exists() {
|
||||||
eprintln!(" Removed {}", UNIT_PATH);
|
std::fs::remove_file(SYSTEMD_UNIT_PATH)?;
|
||||||
|
eprintln!(" Removed {}", SYSTEMD_UNIT_PATH);
|
||||||
|
}
|
||||||
|
|
||||||
run_cmd("systemctl", &["daemon-reload"])?;
|
run_cmd("systemctl", &["daemon-reload"])?;
|
||||||
eprintln!(" Service uninstalled.");
|
eprintln!(" Service uninstalled.");
|
||||||
eprintln!();
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the systemd service is currently active.
|
fn uninstall_openrc_service() -> anyhow::Result<()> {
|
||||||
pub fn is_service_active() -> bool {
|
eprintln!(" Stopping and removing existing service...");
|
||||||
std::path::Path::new(UNIT_PATH).exists()
|
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")
|
&& Command::new("systemctl")
|
||||||
.args(["is-active", "--quiet", SERVICE_NAME])
|
.arg("--version")
|
||||||
.stdout(std::process::Stdio::null())
|
.stdout(Stdio::null())
|
||||||
.stderr(std::process::Stdio::null())
|
.stderr(Stdio::null())
|
||||||
.status()
|
.status()
|
||||||
.map(|s| s.success())
|
.map(|status| status.success())
|
||||||
.unwrap_or(false)
|
.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<()> {
|
fn has_absolute_candidate(candidates: &[&str]) -> bool {
|
||||||
if !std::path::Path::new(UNIT_PATH).exists() {
|
candidates
|
||||||
anyhow::bail!("service not installed, run `sudo ./aether-proxy setup` first");
|
.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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ensure_root_and_service() -> anyhow::Result<()> {
|
fn set_mode(path: &str, mode: u32) -> anyhow::Result<()> {
|
||||||
ensure_service_installed()?;
|
#[cfg(unix)]
|
||||||
if !is_root() {
|
{
|
||||||
anyhow::bail!("root required, use: sudo ./aether-proxy <command>");
|
use std::os::unix::fs::PermissionsExt;
|
||||||
}
|
let perms = std::fs::Permissions::from_mode(mode);
|
||||||
Ok(())
|
std::fs::set_permissions(path, perms)?;
|
||||||
}
|
|
||||||
|
|
||||||
/// `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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
run_cmd("systemctl", &["daemon-reload"])?;
|
#[cfg(not(unix))]
|
||||||
eprintln!(" Service uninstalled.");
|
let _ = (path, mode);
|
||||||
eprintln!();
|
|
||||||
eprintln!(" Config file and TLS certs are preserved. Remove manually if needed.");
|
|
||||||
|
|
||||||
Ok(())
|
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.
|
/// Outcome of the setup wizard, returned to the caller.
|
||||||
pub enum SetupOutcome {
|
pub enum SetupOutcome {
|
||||||
/// Config saved; systemd service installed and started.
|
/// Config saved and the selected host service was installed.
|
||||||
ServiceInstalled,
|
ServiceInstalled,
|
||||||
/// Config saved; no service -- caller should start the proxy directly.
|
/// Config saved; no service -- caller should start the proxy directly.
|
||||||
ReadyToRun(PathBuf),
|
ReadyToRun(PathBuf),
|
||||||
@@ -148,7 +148,7 @@ impl App {
|
|||||||
.into(),
|
.into(),
|
||||||
kind: FieldKind::Bool,
|
kind: FieldKind::Bool,
|
||||||
required: false,
|
required: false,
|
||||||
help: "Install as systemd service (requires root) -- Enter to toggle",
|
help: "Install as managed service (requires root) -- Enter to toggle",
|
||||||
},
|
},
|
||||||
Field {
|
Field {
|
||||||
label: "Log Level",
|
label: "Log Level",
|
||||||
@@ -532,11 +532,8 @@ impl App {
|
|||||||
&& toggled == "true"
|
&& toggled == "true"
|
||||||
&& !super::service::is_available()
|
&& !super::service::is_available()
|
||||||
{
|
{
|
||||||
self.message = Some((
|
self.message =
|
||||||
"requires root with systemd, use: sudo aether-proxy setup".into(),
|
Some((super::service::unavailable_hint(), Instant::now(), true));
|
||||||
Instant::now(),
|
|
||||||
true,
|
|
||||||
));
|
|
||||||
} else {
|
} else {
|
||||||
self.selected_field_mut().value = toggled.into();
|
self.selected_field_mut().value = toggled.into();
|
||||||
self.modified = true;
|
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
|
//! Downloads a release from GitHub, verifies the SHA256 checksum, replaces the
|
||||||
//! replaces the running binary. Restarts the systemd service if active.
|
//! running binary atomically, and restarts the active managed service when
|
||||||
|
//! applicable.
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
@@ -23,7 +24,14 @@ struct GithubRelease {
|
|||||||
// ── Platform detection ───────────────────────────────────────────────────────
|
// ── Platform detection ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn detect_platform() -> &'static str {
|
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"
|
"linux-amd64"
|
||||||
} else if cfg!(target_os = "linux") && cfg!(target_arch = "aarch64") {
|
} else if cfg!(target_os = "linux") && cfg!(target_arch = "aarch64") {
|
||||||
"linux-arm64"
|
"linux-arm64"
|
||||||
@@ -361,34 +369,33 @@ async fn execute_upgrade(
|
|||||||
|
|
||||||
match restart_mode {
|
match restart_mode {
|
||||||
RestartMode::BestEffort => {
|
RestartMode::BestEffort => {
|
||||||
// Restart systemd service if running.
|
|
||||||
// Use best-effort: binary is already replaced, so a restart failure should
|
// Use best-effort: binary is already replaced, so a restart failure should
|
||||||
// not abort the whole upgrade -- the user can restart manually.
|
// not abort the whole upgrade -- the user can restart manually.
|
||||||
if super::service::is_service_active() {
|
if super::service::is_service_active() {
|
||||||
if super::service::is_root() {
|
if super::service::is_root() {
|
||||||
eprintln!(" Restarting systemd service...");
|
eprintln!(" Restarting managed service...");
|
||||||
match super::service::run_cmd("systemctl", &["restart", "aether-proxy"]) {
|
match super::service::restart_active_service() {
|
||||||
Ok(()) => eprintln!(" Service restarted."),
|
Ok(()) => eprintln!(" Service restarted."),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!(" WARNING: failed to restart service: {}", e);
|
eprintln!(" WARNING: failed to restart service: {}", e);
|
||||||
eprintln!(" Run manually: sudo systemctl restart aether-proxy");
|
eprintln!(" Run manually: sudo aether-proxy restart");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
eprintln!(" Systemd service is active, but restart requires root.");
|
eprintln!(" Managed service is active, but restart requires root.");
|
||||||
eprintln!(" Run: sudo systemctl restart aether-proxy");
|
eprintln!(" Run: sudo aether-proxy restart");
|
||||||
eprintln!(" Skipping restart.");
|
eprintln!(" Skipping restart.");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
eprintln!(" No active systemd service detected, skipping restart.");
|
eprintln!(" No active service detected, skipping restart.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RestartMode::Required => {
|
RestartMode::Required => {
|
||||||
if !super::service::is_root() {
|
if !super::service::is_root() {
|
||||||
anyhow::bail!("automatic upgrade requires root privileges");
|
anyhow::bail!("automatic upgrade requires root privileges");
|
||||||
}
|
}
|
||||||
eprintln!(" Restarting systemd service...");
|
eprintln!(" Restarting managed service...");
|
||||||
super::service::run_cmd("systemctl", &["restart", "aether-proxy"])?;
|
super::service::restart_active_service()?;
|
||||||
eprintln!(" Service restarted.");
|
eprintln!(" Service restarted.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -409,8 +416,8 @@ pub async fn cmd_upgrade(version: Option<String>) -> anyhow::Result<()> {
|
|||||||
|
|
||||||
/// Perform automatic upgrade to a specific version.
|
/// Perform automatic upgrade to a specific version.
|
||||||
///
|
///
|
||||||
/// This path is designed for server-pushed upgrades in systemd/root scenarios:
|
/// This path is used for server-pushed upgrades: it requires root and expects
|
||||||
/// it requires root and requires a successful `systemctl restart aether-proxy`.
|
/// the currently active managed service to restart successfully.
|
||||||
pub async fn perform_upgrade(version: &str) -> anyhow::Result<()> {
|
pub async fn perform_upgrade(version: &str) -> anyhow::Result<()> {
|
||||||
execute_upgrade(Some(version), true, RestartMode::Required).await
|
execute_upgrade(Some(version), true, RestartMode::Required).await
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user