use std::ffi::OsStr;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::{Context as _, Result, bail};
use super::{Manager, State, unit};
use crate::home::{Home, user_home, write_private};
use crate::install::{SANDBOX_BACKUP, backup_path};
pub const DISPLAY: &str = "systemd user unit";
pub fn manager(_home: &Home) -> Result<Box<dyn Manager>> {
Ok(Box::new(SystemdManager { home: user_home()? }))
}
struct SystemdManager {
home: PathBuf,
}
impl SystemdManager {
fn unit_dir(&self) -> PathBuf {
self.home.join(".config/systemd/user")
}
fn unit_path(&self) -> PathBuf {
self.unit_dir().join(unit::NAME)
}
}
fn systemctl<I, S>(args: I) -> Result<std::process::Output>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
Command::new("systemctl")
.arg("--user")
.args(args)
.output()
.with_context(|| "running systemctl --user; install systemd or run `moat proxy` yourself")
}
impl Manager for SystemdManager {
fn file_path(&self) -> Result<PathBuf> {
Ok(self.unit_path())
}
fn install(&self, binary: &Path, moat_home: &Path) -> Result<PathBuf> {
let dir = self.unit_dir();
fs::create_dir_all(&dir).with_context(|| format!("creating {}", dir.display()))?;
let text = unit::render(&unit::Spec { binary, moat_home });
let path = self.unit_path();
if path.exists() {
let backup = backup_path(&path, SANDBOX_BACKUP);
fs::copy(&path, &backup).with_context(|| format!("backing up {}", path.display()))?;
}
write_private(&path, text.as_bytes())?;
if super::skip_exec() {
return Ok(path);
}
let reload = systemctl(["daemon-reload"])?;
if !reload.status.success() {
bail!(
"systemctl --user daemon-reload refused: {}; the unit is at {} and \
`systemctl --user enable --now moat-proxy` runs it by hand",
String::from_utf8_lossy(&reload.stderr).trim(),
path.display(),
);
}
let start = systemctl(["enable", "--now", unit::NAME])?;
if !start.status.success() {
bail!(
"systemctl --user enable --now refused: {}; the unit is at {}",
String::from_utf8_lossy(&start.stderr).trim(),
path.display(),
);
}
Ok(path)
}
fn uninstall(&self) -> Result<Option<PathBuf>> {
let path = self.unit_path();
if !path.exists() {
return Ok(None);
}
if !super::skip_exec() {
let _ = systemctl(["disable", "--now", unit::NAME]);
}
fs::remove_file(&path).with_context(|| format!("removing {}", path.display()))?;
if !super::skip_exec() {
let _ = systemctl(["daemon-reload"]);
}
Ok(Some(path))
}
fn state(&self) -> Result<State> {
let path = self.unit_path();
if !path.exists() {
return Ok(State::NotInstalled);
}
if super::skip_exec() {
return Ok(State::Stopped);
}
let active = match systemctl(["is-active", unit::NAME]) {
Ok(out) => out,
Err(e) => {
return Ok(State::Unknown {
details: format!("{e:#}"),
});
}
};
let text = String::from_utf8_lossy(&active.stdout);
match text.trim() {
"active" | "activating" | "reloading" => Ok(State::Running),
"failed" => classify_failure(),
"inactive" | "deactivating" => classify_inactive(),
other => Ok(State::Unknown {
details: format!("systemctl is-active returned {other:?}"),
}),
}
}
fn restart(&self) -> Result<()> {
let path = self.unit_path();
if !path.exists() || super::skip_exec() {
return Ok(());
}
let out = systemctl(["restart", unit::NAME])?;
if !out.status.success() {
bail!(
"systemctl --user restart refused: {}",
String::from_utf8_lossy(&out.stderr).trim(),
);
}
Ok(())
}
}
fn classify_failure() -> Result<State> {
let out = systemctl(["show", unit::NAME, "-p", "ExecMainStatus", "--value"])?;
let text = String::from_utf8_lossy(&out.stdout);
if text.trim() == "64" {
return Ok(State::DriftBlocked);
}
Ok(State::Crashlooping)
}
fn classify_inactive() -> Result<State> {
let out = systemctl(["show", unit::NAME, "-p", "ExecMainStatus", "--value"])?;
let text = String::from_utf8_lossy(&out.stdout);
if text.trim() == "64" {
return Ok(State::DriftBlocked);
}
Ok(State::Stopped)
}