use super::install::{
elevated_command, ensure_config_preserved, is_privileged, manager_error_is_permission,
repair_system_config_permissions, write_atomic_text, InstallError,
};
use super::method::{
is_systemd_environment, standard_systemd_binary, standard_systemd_config,
standard_systemd_config_dir, standard_systemd_unit_path, StartupMethodArg,
};
use super::process::{run_bounded_command, MANAGER_COMMAND_TIMEOUT};
use super::ArtifactOwnership;
use std::fs;
use std::io::{self};
use std::path::{Path, PathBuf};
pub fn systemd_unit_content() -> String {
const TEMPLATE: &str = r"[Unit]
Description=Gregg metrics daemon
Documentation=https://github.com/eggstack/gregg
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=greggd
Group=greggd
RuntimeDirectory=gregg
ExecStart=/usr/local/bin/greggd run --config /etc/gregg/greggd.toml
Restart=on-failure
RestartSec=5
StartLimitIntervalSec=60
StartLimitBurst=5
# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadOnlyPaths=/proc /sys
ReadWritePaths=/etc/gregg
PrivateTmp=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictNamespaces=true
RestrictSUIDSGID=true
MemoryDenyWriteExecute=true
RestrictRealtime=true
LockPersonality=true
SystemCallFilter=@system-service
SystemCallArchitectures=native
# Network access
IPAddressAllow=any
IPAddressDeny=
# Capabilities
CapabilityBoundingSet=
AmbientCapabilities=
[Install]
WantedBy=multi-user.target
";
TEMPLATE.to_string()
}
#[allow(dead_code)]
pub(crate) fn systemd_unit_exists() -> bool {
standard_systemd_unit_path().exists()
}
#[allow(dead_code)]
pub(crate) fn systemd_is_active() -> bool {
matches!(
run_bounded_command(
"systemctl",
&["is-active", "--quiet", "greggd"],
MANAGER_COMMAND_TIMEOUT,
),
Ok(output) if output.status.success()
)
}
pub fn parse_exec_start_target(text: &str) -> Option<(PathBuf, Option<PathBuf>)> {
let value = text
.lines()
.find_map(|line| line.trim().strip_prefix("ExecStart="))
.or_else(|| {
let trimmed = text.trim();
trimmed.starts_with('{').then_some(trimmed)
})?
.trim();
parse_command_target(value)
}
fn parse_command_target(value: &str) -> Option<(PathBuf, Option<PathBuf>)> {
let command = value
.strip_prefix("{ path=")
.or_else(|| value.strip_prefix("{path="))
.unwrap_or(value);
let target = command
.split(|c: char| c.is_whitespace() || c == ';' || c == '}')
.next()?
.trim_matches('"')
.strip_prefix('-')
.unwrap_or(command.split_whitespace().next()?)
.trim_matches('"');
if target.is_empty() || target.contains('=') || !target.starts_with('/') {
return None;
}
let tokens: Vec<&str> = value
.split_whitespace()
.map(|token| token.trim_matches('"').trim_matches(';').trim_matches('}'))
.collect();
let config = tokens
.windows(2)
.find(|pair| pair[0] == "--config")
.and_then(|pair| pair.get(1))
.filter(|path| !path.is_empty())
.map(PathBuf::from);
Some((PathBuf::from(target), config))
}
pub fn systemd_artifact_ownership(exe: &Path) -> (ArtifactOwnership, bool, Option<PathBuf>) {
let unit_path = standard_systemd_unit_path();
let unit_exists = unit_path.exists();
let active = if unit_exists || is_systemd_environment() {
systemd_is_active()
} else {
false
};
let command = if unit_exists {
fs::read_to_string(&unit_path).ok()
} else if active {
run_bounded_command(
"systemctl",
&["show", "greggd", "--property=ExecStart", "--value"],
MANAGER_COMMAND_TIMEOUT,
)
.ok()
.filter(|output| output.status.success())
.map(|output| String::from_utf8_lossy(&output.stdout).into_owned())
} else {
None
};
let Some(command) = command else {
return (
if unit_exists || active {
ArtifactOwnership::Unknown
} else {
ArtifactOwnership::Absent
},
active,
None,
);
};
let Some((target, config)) = parse_exec_start_target(&command) else {
return (ArtifactOwnership::Unknown, active, None);
};
let ownership = if gregg_update::uninstall::paths_equivalent(&target, exe) {
ArtifactOwnership::Owned
} else {
ArtifactOwnership::Foreign
};
(ownership, active, config)
}
fn ensure_greggd_user() -> io::Result<()> {
let exists = matches!(
run_bounded_command("id", &["-u", "greggd"], MANAGER_COMMAND_TIMEOUT),
Ok(output) if output.status.success()
);
if exists {
return Ok(());
}
let output = run_bounded_command(
"useradd",
&[
"--system",
"--no-create-home",
"--shell",
"/usr/sbin/nologin",
"greggd",
],
MANAGER_COMMAND_TIMEOUT,
)?;
if output.status.success() {
Ok(())
} else {
Err(io::Error::other(format!(
"useradd greggd failed with status {}",
output.status
)))
}
}
fn set_config_ownership() -> io::Result<()> {
let dir = standard_systemd_config_dir();
if !dir.exists() {
return Ok(());
}
let dir_str = dir.to_string_lossy().to_string();
let output = run_bounded_command(
"chown",
&["-R", "greggd:greggd", &dir_str],
MANAGER_COMMAND_TIMEOUT,
)?;
if output.status.success() {
Ok(())
} else {
eprintln!(
"warning: chown greggd:greggd {} failed: {}",
dir.display(),
output.status
);
Ok(())
}
}
fn run_systemctl(args: &[&str]) -> io::Result<()> {
let output = run_bounded_command("systemctl", args, MANAGER_COMMAND_TIMEOUT)?;
if output.status.success() {
Ok(())
} else {
Err(io::Error::other(format!(
"systemctl {} failed with status {:?}: {}",
args.join(" "),
output.status.code(),
String::from_utf8_lossy(&output.stderr).trim()
)))
}
}
fn systemd_manager_error(exe: &Path, args: &[&str], error: io::Error) -> InstallError {
if error.kind() == io::ErrorKind::PermissionDenied
|| manager_error_is_permission(&error.to_string())
{
InstallError::Permission {
message: format!(
"systemctl {} was denied; rerun as root: {}",
args.join(" "),
elevated_command(exe, StartupMethodArg::Systemd)
),
}
} else {
InstallError::Io {
path: PathBuf::from(format!("systemctl {}", args.join(" "))),
source: error,
}
}
}
pub fn install_systemd(exe: &Path, config_path: &Path) -> Result<(), InstallError> {
if !is_systemd_environment() {
return Err(InstallError::SystemdNotDetected {
message: "systemd not detected: /run/systemd/system missing or PID 1 is not systemd"
.into(),
});
}
let bin_path = standard_systemd_binary();
if !bin_path.exists() {
return Err(InstallError::BinaryMissing { path: bin_path });
}
if !is_privileged() {
let cmd = elevated_command(exe, StartupMethodArg::Systemd);
return Err(InstallError::Permission {
message: format!("permission denied: rerun as root: {cmd}"),
});
}
ensure_greggd_user().map_err(|e| InstallError::Io {
path: PathBuf::from("useradd greggd"),
source: e,
})?;
fs::create_dir_all(standard_systemd_config_dir()).map_err(|e| InstallError::Io {
path: standard_systemd_config_dir(),
source: e,
})?;
ensure_config_preserved(&standard_systemd_config()).map_err(|e| InstallError::Io {
path: standard_systemd_config(),
source: e,
})?;
set_config_ownership().map_err(|e| InstallError::Io {
path: standard_systemd_config_dir(),
source: e,
})?;
repair_system_config_permissions(&standard_systemd_config()).map_err(|e| InstallError::Io {
path: standard_systemd_config(),
source: e,
})?;
let unit_content = systemd_unit_content();
let unit_path = standard_systemd_unit_path();
write_atomic_text(&unit_path, &unit_content).map_err(|e| {
if e.kind() == io::ErrorKind::PermissionDenied {
InstallError::Permission {
message: format!(
"permission denied writing {}: rerun as root: {}",
unit_path.display(),
elevated_command(exe, StartupMethodArg::Systemd)
),
}
} else {
InstallError::Io {
path: unit_path.clone(),
source: e,
}
}
})?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = fs::set_permissions(&unit_path, fs::Permissions::from_mode(0o644));
}
let daemon_reload = ["daemon-reload"];
run_systemctl(&daemon_reload).map_err(|e| systemd_manager_error(exe, &daemon_reload, e))?;
let enable = ["enable", "greggd"];
run_systemctl(&enable).map_err(|e| systemd_manager_error(exe, &enable, e))?;
if systemd_is_active() {
let restart = ["restart", "greggd"];
run_systemctl(&restart).map_err(|e| systemd_manager_error(exe, &restart, e))?;
} else {
if let Err(e) = run_systemctl(&["start", "greggd"]) {
eprintln!("systemctl start failed ({e}), trying restart...");
let restart = ["restart", "greggd"];
run_systemctl(&restart).map_err(|e2| systemd_manager_error(exe, &restart, e2))?;
}
}
println!("greggd systemd service installed: {}", unit_path.display());
println!("config: {}", standard_systemd_config().display());
println!("status: systemctl status greggd");
println!("logs: journalctl -u greggd -f");
let _ = config_path;
Ok(())
}
pub(crate) fn restart_systemd(exe: &Path) -> Result<(), InstallError> {
if !systemd_unit_exists() {
return Err(InstallError::Other(
"systemd unit not installed: run `sudo greggd startup install --method systemd`".into(),
));
}
match run_systemctl(&["restart", "greggd"]) {
Ok(()) => {
println!("greggd restarted via systemd");
Ok(())
}
Err(e) if e.kind() == io::ErrorKind::PermissionDenied => Err(InstallError::Permission {
message: format!(
"permission denied: rerun as root: sudo systemctl restart greggd (original exe: {})",
exe.display()
),
}),
Err(e) => {
let msg = e.to_string();
if manager_error_is_permission(&msg) {
Err(InstallError::Permission {
message: format!(
"permission denied: rerun as root: sudo systemctl restart greggd (exe: {})",
exe.display()
),
})
} else {
Err(InstallError::Io {
path: PathBuf::from("systemctl restart greggd"),
source: e,
})
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SystemdUninstallStep {
Stop,
Disable,
RemoveUnit,
DaemonReload,
}
#[must_use]
pub fn systemd_uninstall_steps(unit_exists: bool, active: bool) -> Vec<SystemdUninstallStep> {
use SystemdUninstallStep::{DaemonReload, Disable, RemoveUnit, Stop};
if !unit_exists && !active {
return Vec::new();
}
let mut steps = Vec::with_capacity(4);
if active {
steps.push(Stop);
}
steps.push(Disable);
if unit_exists {
steps.push(RemoveUnit);
steps.push(DaemonReload);
}
steps
}
pub fn uninstall_systemd(exe: &Path) -> Result<(), InstallError> {
let unit_path = standard_systemd_unit_path();
let (ownership, active, _) = systemd_artifact_ownership(exe);
if !ownership.is_owned() {
return Ok(());
}
let unit_exists = unit_path.exists();
let steps = systemd_uninstall_steps(unit_exists, active);
if steps.is_empty() {
return Ok(());
}
for step in steps {
match step {
SystemdUninstallStep::Stop => {
let args = ["stop", "greggd"];
run_systemctl(&args).map_err(|e| systemd_manager_error_for(exe, &args, e))?;
}
SystemdUninstallStep::Disable => {
let args = ["disable", "greggd"];
run_systemctl(&args).map_err(|e| systemd_manager_error_for(exe, &args, e))?;
}
SystemdUninstallStep::RemoveUnit => {
match fs::remove_file(&unit_path) {
Ok(()) => {}
Err(e) if e.kind() == io::ErrorKind::NotFound => {}
Err(e) if e.kind() == io::ErrorKind::PermissionDenied => {
return Err(InstallError::Permission {
message: format!(
"permission denied removing {}: rerun as root: sudo {} uninstall",
unit_path.display(),
exe.display()
),
});
}
Err(e) => {
return Err(InstallError::Io {
path: unit_path.clone(),
source: e,
});
}
}
}
SystemdUninstallStep::DaemonReload => {
let args = ["daemon-reload"];
run_systemctl(&args).map_err(|e| systemd_manager_error_for(exe, &args, e))?;
}
}
}
println!("greggd systemd integration removed");
Ok(())
}
fn systemd_manager_error_for(exe: &Path, args: &[&str], error: io::Error) -> InstallError {
if error.kind() == io::ErrorKind::PermissionDenied
|| manager_error_is_permission(&error.to_string())
{
InstallError::Permission {
message: format!(
"systemctl {} was denied; rerun as root: sudo {} uninstall",
args.join(" "),
exe.display()
),
}
} else {
InstallError::Io {
path: PathBuf::from(format!("systemctl {}", args.join(" "))),
source: error,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn systemd_unit_content_contains_hardening() {
let content = systemd_unit_content();
assert!(content.contains("ExecStart=/usr/local/bin/greggd"));
assert!(content.contains("NoNewPrivileges"));
assert!(content.contains("ProtectSystem"));
assert!(content.contains("[Service]"));
assert!(content.contains("[Unit]"));
}
#[test]
fn embedded_systemd_unit_matches_packaging_file_when_present() {
let packaging =
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../packaging/systemd/greggd.service");
if let Ok(file) = std::fs::read_to_string(&packaging) {
let mut file_norm = file.replace("\r\n", "\n");
if !file_norm.ends_with('\n') {
file_norm.push('\n');
}
assert_eq!(
systemd_unit_content(),
file_norm,
"embedded systemd unit must stay synchronized with packaging/systemd/greggd.service"
);
}
}
#[test]
fn systemd_uninstall_orders_stop_disable_remove_reload() {
use SystemdUninstallStep::{DaemonReload, Disable, RemoveUnit, Stop};
assert_eq!(
systemd_uninstall_steps(true, true),
vec![Stop, Disable, RemoveUnit, DaemonReload]
);
}
#[test]
fn systemd_uninstall_skips_stop_when_inactive() {
use SystemdUninstallStep::{DaemonReload, Disable, RemoveUnit};
assert_eq!(
systemd_uninstall_steps(true, false),
vec![Disable, RemoveUnit, DaemonReload]
);
}
#[test]
fn systemd_uninstall_missing_state_is_idempotent_noop() {
assert!(systemd_uninstall_steps(false, false).is_empty());
}
#[test]
fn systemd_uninstall_active_without_unit_still_stops_and_disables() {
use SystemdUninstallStep::{Disable, Stop};
assert_eq!(systemd_uninstall_steps(false, true), vec![Stop, Disable]);
}
#[test]
fn parses_canonical_exec_start_and_config() {
let parsed = parse_exec_start_target(
"ExecStart=/usr/local/bin/greggd run --config /etc/gregg/greggd.toml\n",
)
.unwrap();
assert_eq!(parsed.0, PathBuf::from("/usr/local/bin/greggd"));
assert_eq!(parsed.1, Some(PathBuf::from("/etc/gregg/greggd.toml")));
}
#[test]
fn parses_systemctl_exec_start_shape() {
let parsed = parse_exec_start_target(
"{ path=/usr/local/bin/greggd ; argv[]=/usr/local/bin/greggd run --config /etc/gregg/greggd.toml ; }",
)
.unwrap();
assert_eq!(parsed.0, PathBuf::from("/usr/local/bin/greggd"));
}
#[test]
fn malformed_exec_start_is_not_owned() {
assert!(parse_exec_start_target("ExecStart=\n").is_none());
assert!(parse_exec_start_target("ExecStart=not a command =").is_none());
}
}