warpgate/src/services/systemd.rs
grabbit d2b9f46b1a feat: add Apply Config progress modal and fix stale PENDING health after reload
- Add 4-step progress modal to config apply flow (validate, write, reload, services ready)
- Poll SSE-updated data-share-health attributes to detect when services finish restarting
- Fix stale health bug: recalculate health for affected shares based on actual mount
  success instead of preserving old health from before reload
- Add modal overlay/card/step CSS matching the dark theme
- Include connection refactor (multi-protocol support) and probe helpers from prior work

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 01:11:50 +08:00

143 lines
3.9 KiB
Rust

//! Generate systemd unit files for Warpgate services.
use std::fmt::Write as _;
use std::fs;
use std::path::Path;
use std::process::Command;
use anyhow::{Context, Result};
use crate::config::Config;
/// Target directory for systemd unit files.
pub const SYSTEMD_DIR: &str = "/etc/systemd/system";
/// Single unified service unit name.
pub const RUN_SERVICE: &str = "warpgate.service";
/// Generate the single `warpgate.service` unit that runs `warpgate run`.
///
/// This replaces the old multi-unit approach. The `warpgate run` supervisor
/// manages rclone mount, SMB, NFS, and WebDAV internally.
pub fn generate_run_unit(_config: &Config) -> Result<String> {
let mut unit = String::new();
writeln!(unit, "# Generated by Warpgate — do not edit manually.")?;
writeln!(unit, "[Unit]")?;
writeln!(unit, "Description=Warpgate NAS cache proxy")?;
writeln!(unit, "After=network-online.target")?;
writeln!(unit, "Wants=network-online.target")?;
writeln!(unit)?;
writeln!(unit, "[Service]")?;
writeln!(unit, "Type=simple")?;
writeln!(unit, "ExecStart=/usr/local/bin/warpgate run")?;
writeln!(unit, "Restart=on-failure")?;
writeln!(unit, "RestartSec=10")?;
writeln!(unit, "KillMode=mixed")?;
writeln!(unit, "TimeoutStopSec=30")?;
writeln!(unit)?;
writeln!(unit, "[Install]")?;
writeln!(unit, "WantedBy=multi-user.target")?;
Ok(unit)
}
/// Install the single `warpgate.service` unit and reload systemd.
pub fn install_run_unit(config: &Config) -> Result<()> {
let systemd_dir = Path::new(SYSTEMD_DIR);
let unit_content = generate_run_unit(config)?;
fs::write(systemd_dir.join(RUN_SERVICE), unit_content)
.with_context(|| format!("Failed to write {RUN_SERVICE}"))?;
// Reload systemd daemon
let status = Command::new("systemctl")
.arg("daemon-reload")
.status()
.context("Failed to run systemctl daemon-reload")?;
if !status.success() {
anyhow::bail!("systemctl daemon-reload failed with exit code: {}", status);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn test_config() -> Config {
toml::from_str(
r#"
[[connections]]
name = "nas"
host = "10.0.0.1"
protocol = "sftp"
user = "admin"
[cache]
dir = "/tmp/cache"
[read]
[bandwidth]
[writeback]
[directory_cache]
[protocols]
[[shares]]
name = "photos"
connection = "nas"
remote_path = "/photos"
mount_point = "/mnt/photos"
"#,
)
.unwrap()
}
#[test]
fn test_generate_run_unit_sections() {
let config = test_config();
let unit = generate_run_unit(&config).unwrap();
assert!(unit.contains("[Unit]"));
assert!(unit.contains("[Service]"));
assert!(unit.contains("[Install]"));
}
#[test]
fn test_generate_run_unit_content() {
let config = test_config();
let unit = generate_run_unit(&config).unwrap();
assert!(unit.contains("Description=Warpgate NAS cache proxy"));
assert!(unit.contains("After=network-online.target"));
assert!(unit.contains("Type=simple"));
assert!(unit.contains("ExecStart=/usr/local/bin/warpgate run"));
assert!(unit.contains("Restart=on-failure"));
assert!(unit.contains("RestartSec=10"));
assert!(unit.contains("KillMode=mixed"));
assert!(unit.contains("WantedBy=multi-user.target"));
}
#[test]
fn test_systemd_constants() {
assert_eq!(SYSTEMD_DIR, "/etc/systemd/system");
assert_eq!(RUN_SERVICE, "warpgate.service");
}
}
/// Enable and start the single `warpgate.service`.
pub fn enable_and_start_run() -> Result<()> {
let status = Command::new("systemctl")
.args(["enable", "--now", RUN_SERVICE])
.status()
.with_context(|| format!("Failed to run systemctl enable --now {RUN_SERVICE}"))?;
if !status.success() {
anyhow::bail!("systemctl enable --now {RUN_SERVICE} failed with exit code: {status}");
}
Ok(())
}