use anyhow::{Context, Result, bail};
use async_trait::async_trait;
use futures::future::join_all;
use zbus::{Connection, proxy, zvariant::OwnedObjectPath};
use crate::desktop_file::DesktopFile;
use crate::model::{AutostartEntry, Scope};
use crate::provider::AutostartProvider;
const ENABLEABLE_STATES: &[&str] = &[
"enabled",
"enabled-runtime",
"disabled",
"linked",
"linked-runtime",
"masked",
"masked-runtime",
"indirect",
];
#[derive(Debug, Clone, Copy)]
enum Bus {
Session,
System,
}
type UnitFileChange = (String, String, String);
#[proxy(
interface = "org.freedesktop.systemd1.Manager",
default_service = "org.freedesktop.systemd1",
default_path = "/org/freedesktop/systemd1"
)]
trait Manager {
fn list_unit_files(&self) -> zbus::Result<Vec<(String, String)>>;
fn enable_unit_files(
&self,
files: &[&str],
runtime: bool,
force: bool,
) -> zbus::Result<(bool, Vec<UnitFileChange>)>;
fn disable_unit_files(
&self,
files: &[&str],
runtime: bool,
) -> zbus::Result<Vec<UnitFileChange>>;
fn start_unit(&self, name: &str, mode: &str) -> zbus::Result<OwnedObjectPath>;
}
pub struct SystemdProvider {
bus: Bus,
}
impl SystemdProvider {
pub fn user() -> Self {
Self { bus: Bus::Session }
}
pub fn system() -> Self {
Self { bus: Bus::System }
}
async fn connect(&self) -> Result<Connection> {
match self.bus {
Bus::Session => Connection::session()
.await
.context("connecting to the session bus"),
Bus::System => Connection::system()
.await
.context("connecting to the system bus"),
}
}
async fn manager(&self) -> Result<ManagerProxy<'static>> {
let conn = self.connect().await?;
ManagerProxy::new(&conn)
.await
.context("building systemd1 manager proxy")
}
}
#[async_trait]
impl AutostartProvider for SystemdProvider {
fn id(&self) -> &'static str {
match self.bus {
Bus::Session => "systemd-user",
Bus::System => "systemd-system",
}
}
fn scope(&self) -> Scope {
match self.bus {
Bus::Session => Scope::User,
Bus::System => Scope::System,
}
}
async fn is_available(&self) -> bool {
self.manager().await.is_ok()
}
async fn entries(&self) -> Result<Vec<AutostartEntry>> {
let manager = self.manager().await?;
let unit_files = manager
.list_unit_files()
.await
.context("listing systemd unit files")?;
let enableable: Vec<(String, String)> = unit_files
.into_iter()
.filter(|(_, state)| ENABLEABLE_STATES.contains(&state.as_str()))
.collect();
let reads = enableable
.iter()
.map(|(path, _)| async move { tokio::fs::read_to_string(path).await.ok() });
let file_texts = join_all(reads).await;
let source = self.id().to_string();
let scope = self.scope();
let entries = enableable
.iter()
.zip(file_texts)
.map(|((path, state), text)| {
let name = unit_name(path);
let enabled = matches!(state.as_str(), "enabled" | "enabled-runtime");
let (description, command) =
text.as_deref().map(unit_file_meta).unwrap_or_default();
AutostartEntry {
id: name.clone(),
display_name: description.clone().unwrap_or_else(|| name.clone()),
description,
command: command.unwrap_or_default(),
icon: None,
enabled,
source: source.clone(),
scope,
}
})
.collect();
Ok(entries)
}
async fn enable(&self, id: &str) -> Result<()> {
self.manager()
.await?
.enable_unit_files(&[id], false, true)
.await
.with_context(|| format!("enabling unit '{id}'"))?;
Ok(())
}
async fn disable(&self, id: &str) -> Result<()> {
self.manager()
.await?
.disable_unit_files(&[id], false)
.await
.with_context(|| format!("disabling unit '{id}'"))?;
Ok(())
}
async fn add(&self, _entry: &AutostartEntry) -> Result<()> {
bail!(
"adding units is not supported for {} in this pass",
self.id()
);
}
async fn remove(&self, _id: &str) -> Result<()> {
bail!(
"removing units is not supported for {} in this pass",
self.id()
);
}
}
fn unit_name(path: &str) -> String {
path.rsplit('/').next().unwrap_or(path).to_string()
}
fn unit_file_meta(text: &str) -> (Option<String>, Option<String>) {
let file = DesktopFile::parse(text);
let description = file
.get("Unit", "Description")
.filter(|s| !s.is_empty())
.map(str::to_string);
let command = file
.get("Service", "ExecStart")
.or_else(|| file.get("Timer", "OnCalendar"))
.or_else(|| file.get("Timer", "OnBootSec"))
.or_else(|| file.get("Timer", "OnStartupSec"))
.or_else(|| file.get("Timer", "OnUnitActiveSec"))
.or_else(|| file.get("Timer", "OnActiveSec"))
.or_else(|| file.get("Socket", "ListenStream"))
.or_else(|| file.get("Socket", "ListenDatagram"))
.or_else(|| file.get("Socket", "ListenSequentialPacket"))
.or_else(|| file.get("Socket", "ListenFIFO"))
.filter(|s| !s.is_empty())
.map(str::to_string);
(description, command)
}