use std::process::Command;
use anyhow::{bail, Result};
use log::error;
use tokio_util::sync::CancellationToken;
use tracing::info;
use zbus::{proxy, zvariant, Connection};
#[derive(Debug, Clone)]
pub struct UnitWithStatus {
pub name: String, pub scope: UnitScope, pub description: String, pub file_path: Option<String>, pub load_state: String, pub active_state: String, pub sub_state: String, }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum UnitScope {
Global,
User,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct UnitId {
pub name: String,
pub scope: UnitScope,
}
impl UnitWithStatus {
pub fn is_active(&self) -> bool {
self.active_state == "active"
}
pub fn is_failed(&self) -> bool {
self.active_state == "failed"
}
pub fn is_not_found(&self) -> bool {
self.load_state == "not-found"
}
pub fn is_enabled(&self) -> bool {
self.load_state == "loaded" && self.active_state == "active"
}
pub fn short_name(&self) -> &str {
if self.name.ends_with(".service") {
&self.name[..self.name.len() - 8]
} else {
&self.name
}
}
pub fn id(&self) -> UnitId {
UnitId { name: self.name.clone(), scope: self.scope }
}
pub fn update(&mut self, other: UnitWithStatus) {
self.description = other.description;
self.load_state = other.load_state;
self.active_state = other.active_state;
self.sub_state = other.sub_state;
}
}
type RawUnit =
(String, String, String, String, String, String, zvariant::OwnedObjectPath, u32, String, zvariant::OwnedObjectPath);
fn to_unit_status(raw_unit: RawUnit, scope: UnitScope) -> UnitWithStatus {
let (name, description, load_state, active_state, sub_state, _followed, _path, _job_id, _job_type, _job_path) =
raw_unit;
UnitWithStatus { name, scope, description, file_path: None, load_state, active_state, sub_state }
}
#[derive(Clone, Copy, Default, Debug)]
pub enum Scope {
Global,
User,
#[default]
All,
}
pub async fn get_all_services(scope: Scope, services: &[String]) -> Result<Vec<UnitWithStatus>> {
let start = std::time::Instant::now();
let mut units = vec![];
let is_root = nix::unistd::geteuid().is_root();
match scope {
Scope::Global => {
let system_units = get_services(UnitScope::Global, services).await?;
units.extend(system_units);
},
Scope::User => {
let user_units = get_services(UnitScope::User, services).await?;
units.extend(user_units);
},
Scope::All => {
let (system_units, user_units) =
tokio::join!(get_services(UnitScope::Global, services), get_services(UnitScope::User, services));
units.extend(system_units?);
if let Ok(user_units) = user_units {
units.extend(user_units);
} else if is_root {
error!("Failed to get user units, ignoring because we're running as root")
} else {
user_units?;
}
},
}
units.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
info!("Loaded systemd services in {:?}", start.elapsed());
Ok(units)
}
async fn get_services(scope: UnitScope, services: &[String]) -> Result<Vec<UnitWithStatus>, anyhow::Error> {
let connection = get_connection(scope).await?;
let manager_proxy = ManagerProxy::new(&connection).await?;
let units = manager_proxy.list_units_by_patterns(vec![], services.to_vec()).await?;
let units: Vec<_> = units.into_iter().map(|u| to_unit_status(u, scope)).collect();
Ok(units)
}
pub fn get_unit_file_location(service: &UnitId) -> Result<String> {
let mut args = vec!["--quiet", "show", "-P", "FragmentPath"];
args.push(&service.name);
if service.scope == UnitScope::User {
args.insert(0, "--user");
}
let output = Command::new("systemctl").args(&args).output()?;
if output.status.success() {
let path = String::from_utf8(output.stdout)?;
Ok(path.trim().to_string())
} else {
let stderr = String::from_utf8(output.stderr)?;
bail!(stderr);
}
}
pub async fn start_service(service: UnitId, cancel_token: CancellationToken) -> Result<()> {
async fn start_service(service: UnitId) -> Result<()> {
let connection = get_connection(service.scope).await?;
let manager_proxy = ManagerProxy::new(&connection).await?;
manager_proxy.start_unit(service.name.clone(), "replace".into()).await?;
Ok(())
}
tokio::select! {
_ = cancel_token.cancelled() => {
anyhow::bail!("cancelled");
}
result = start_service(service) => {
result
}
}
}
pub async fn stop_service(service: UnitId, cancel_token: CancellationToken) -> Result<()> {
async fn stop_service(service: UnitId) -> Result<()> {
let connection = get_connection(service.scope).await?;
let manager_proxy = ManagerProxy::new(&connection).await?;
manager_proxy.stop_unit(service.name, "replace".into()).await?;
Ok(())
}
tokio::select! {
_ = cancel_token.cancelled() => {
anyhow::bail!("cancelled");
}
result = stop_service(service) => {
result
}
}
}
async fn get_connection(scope: UnitScope) -> Result<Connection, anyhow::Error> {
match scope {
UnitScope::Global => Ok(Connection::system().await?),
UnitScope::User => Ok(Connection::session().await?),
}
}
pub async fn restart_service(service: UnitId, cancel_token: CancellationToken) -> Result<()> {
async fn restart(service: UnitId) -> Result<()> {
let connection = get_connection(service.scope).await?;
let manager_proxy = ManagerProxy::new(&connection).await?;
manager_proxy.restart_unit(service.name, "replace".into()).await?;
Ok(())
}
tokio::select! {
_ = cancel_token.cancelled() => {
anyhow::bail!("cancelled");
}
result = restart(service) => {
result
}
}
}
pub async fn sleep_test(_service: String, cancel_token: CancellationToken) -> Result<()> {
tokio::select! {
_ = cancel_token.cancelled() => {
anyhow::bail!("cancelled");
}
_ = tokio::time::sleep(std::time::Duration::from_secs(2)) => {
Ok(())
}
}
}
#[proxy(
interface = "org.freedesktop.systemd1.Manager",
default_service = "org.freedesktop.systemd1",
default_path = "/org/freedesktop/systemd1",
gen_blocking = false
)]
pub trait Manager {
#[dbus_proxy(name = "StartUnit")]
fn start_unit(&self, name: String, mode: String) -> zbus::Result<zvariant::OwnedObjectPath>;
#[dbus_proxy(name = "StopUnit")]
fn stop_unit(&self, name: String, mode: String) -> zbus::Result<zvariant::OwnedObjectPath>;
#[dbus_proxy(name = "RestartUnit")]
fn restart_unit(&self, name: String, mode: String) -> zbus::Result<zvariant::OwnedObjectPath>;
#[dbus_proxy(name = "EnableUnitFiles")]
fn enable_unit_files(
&self,
files: Vec<String>,
runtime: bool,
force: bool,
) -> zbus::Result<(bool, Vec<(String, String, String)>)>;
#[dbus_proxy(name = "DisableUnitFiles")]
fn disable_unit_files(&self, files: Vec<String>, runtime: bool) -> zbus::Result<Vec<(String, String, String)>>;
#[dbus_proxy(name = "ListUnits")]
fn list_units(
&self,
) -> zbus::Result<
Vec<(
String,
String,
String,
String,
String,
String,
zvariant::OwnedObjectPath,
u32,
String,
zvariant::OwnedObjectPath,
)>,
>;
#[dbus_proxy(name = "ListUnitsByPatterns")]
fn list_units_by_patterns(
&self,
states: Vec<String>,
patterns: Vec<String>,
) -> zbus::Result<
Vec<(
String,
String,
String,
String,
String,
String,
zvariant::OwnedObjectPath,
u32,
String,
zvariant::OwnedObjectPath,
)>,
>;
#[dbus_proxy(name = "Reload")]
fn reload(&self) -> zbus::Result<()>;
}
#[proxy(
interface = "org.freedesktop.systemd1.Unit",
default_service = "org.freedesktop.systemd1",
assume_defaults = false,
gen_blocking = false
)]
pub trait Unit {
#[dbus_proxy(property)]
fn active_state(&self) -> zbus::Result<String>;
#[dbus_proxy(property)]
fn load_state(&self) -> zbus::Result<String>;
#[dbus_proxy(property)]
fn unit_file_state(&self) -> zbus::Result<String>;
}
#[proxy(
interface = "org.freedesktop.systemd1.Service",
default_service = "org.freedesktop.systemd1",
assume_defaults = false,
gen_blocking = false
)]
trait Service {
#[dbus_proxy(property, name = "MainPID")]
fn main_pid(&self) -> zbus::Result<u32>;
}
pub async fn get_active_state(connection: &Connection, full_service_name: &str) -> String {
let object_path = get_unit_path(full_service_name);
match zvariant::ObjectPath::try_from(object_path) {
Ok(path) => {
let unit_proxy = UnitProxy::new(connection, path).await.unwrap();
unit_proxy.active_state().await.unwrap_or("invalid-unit-path".into())
},
Err(_) => "invalid-unit-path".to_string(),
}
}
pub async fn get_unit_file_state(connection: &Connection, full_service_name: &str) -> String {
let object_path = get_unit_path(full_service_name);
match zvariant::ObjectPath::try_from(object_path) {
Ok(path) => {
let unit_proxy = UnitProxy::new(connection, path).await.unwrap();
unit_proxy.unit_file_state().await.unwrap_or("invalid-unit-path".into())
},
Err(_) => "invalid-unit-path".to_string(),
}
}
pub async fn get_main_pid(connection: &Connection, full_service_name: &str) -> Result<u32, zbus::Error> {
let object_path = get_unit_path(full_service_name);
let validated_object_path = zvariant::ObjectPath::try_from(object_path).unwrap();
let service_proxy = ServiceProxy::new(connection, validated_object_path).await.unwrap();
service_proxy.main_pid().await
}
fn encode_as_dbus_object_path(input_string: &str) -> String {
input_string
.chars()
.map(|c| if c.is_ascii_alphanumeric() || c == '/' || c == '_' { c.to_string() } else { format!("_{:x}", c as u32) })
.collect()
}
pub fn get_unit_path(full_service_name: &str) -> String {
format!("/org/freedesktop/systemd1/unit/{}", encode_as_dbus_object_path(full_service_name))
}