use std::str::FromStr;
use zbus_systemd::systemd1::{ManagerProxy, UnitProxy};
use zbus_systemd::zbus;
use crate as cindy;
use crate::Context;
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
#[crate::wire]
pub enum RuntimeAction {
Started,
Stopped,
Restarted,
Reloaded,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
#[crate::wire]
pub enum Enablement {
Masked,
Disabled,
Enabled,
}
#[derive(Clone, Default, PartialEq, Eq)]
#[crate::wire]
pub struct State {
pub name: String,
pub enablement: Option<Enablement>,
pub runtime: Option<RuntimeAction>,
}
impl crate::Diff for State {}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum UnitState {
Enabled,
EnabledRuntime,
Linked,
LinkedRuntime,
Alias,
Masked,
MaskedRuntime,
Static,
Disabled,
Generated,
Transient,
Indirect,
Bad,
}
impl FromStr for UnitState {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"enabled" => Self::Enabled,
"enabled-runtime" => Self::EnabledRuntime,
"linked" => Self::Linked,
"linked-runtime" => Self::LinkedRuntime,
"alias" => Self::Alias,
"masked" => Self::Masked,
"masked-runtime" => Self::MaskedRuntime,
"static" => Self::Static,
"disabled" => Self::Disabled,
"generated" => Self::Generated,
"transient" => Self::Transient,
"indirect" => Self::Indirect,
"bad" | "" => Self::Bad,
other => return Err(format!("unknown UnitFileState: {other:?}")),
})
}
}
impl UnitState {
fn is_masked(self) -> bool {
matches!(self, Self::Masked | Self::MaskedRuntime)
}
fn needs_enable_call(self) -> bool {
matches!(self, Self::Disabled)
}
fn needs_disable_call(self) -> bool {
matches!(
self,
Self::Enabled | Self::EnabledRuntime | Self::Linked | Self::LinkedRuntime | Self::Alias
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ActiveState {
Active,
Reloading,
Inactive,
Failed,
Activating,
Deactivating,
}
impl FromStr for ActiveState {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"active" => Self::Active,
"reloading" => Self::Reloading,
"inactive" => Self::Inactive,
"failed" => Self::Failed,
"activating" => Self::Activating,
"deactivating" => Self::Deactivating,
other => return Err(format!("unknown ActiveState: {other:?}")),
})
}
}
impl ActiveState {
fn is_running(self) -> bool {
matches!(self, Self::Active | Self::Reloading | Self::Activating)
}
}
fn is_no_such_unit(err: &zbus::Error) -> bool {
if let zbus::Error::MethodError(name, _, _) = err {
let n = name.to_string();
n.ends_with(".NoSuchUnit") || n.ends_with(".NoSuchUnitFile") || n.ends_with(".LoadFailed")
} else {
false
}
}
async fn read_file_state(
manager: &ManagerProxy<'_>,
name: &str,
) -> crate::Result<Option<UnitState>> {
match manager.get_unit_file_state(name.to_owned()).await {
Ok(s) => {
let parsed = s
.parse::<UnitState>()
.map_err(anyhow_serde::Error::msg)
.context("Parsing UnitFileState")?;
Ok(Some(parsed))
}
Err(e) if is_no_such_unit(&e) => Ok(None),
Err(e) => Err(e).context(format!("GetUnitFileState({name}) failed")),
}
}
async fn read_active_state(
manager: &ManagerProxy<'_>,
conn: &zbus::Connection,
name: &str,
) -> crate::Result<Option<ActiveState>> {
let path = match manager.load_unit(name.to_owned()).await {
Ok(p) => p,
Err(e) if is_no_such_unit(&e) => return Ok(None),
Err(e) => return Err(e).context(format!("LoadUnit({name}) failed")),
};
let unit = UnitProxy::builder(conn)
.path(path)
.context("Invalid unit object path")?
.build()
.await
.context("Couldn't construct Unit proxy")?;
let raw = unit
.active_state()
.await
.context("Reading ActiveState failed")?;
let parsed = raw
.parse::<ActiveState>()
.map_err(anyhow_serde::Error::msg)
.context("Parsing ActiveState")?;
Ok(Some(parsed))
}
#[crate::remote]
pub async fn daemon_reload() -> crate::Result<()> {
let conn = zbus::Connection::system()
.await
.context("Couldn't connect to the system bus")?;
let manager = ManagerProxy::new(&conn)
.await
.context("Couldn't construct Manager proxy")?;
manager.reload().await.context("Daemon-reload failed")?;
Ok(())
}
#[crate::remote]
pub async fn systemd(state: State) -> crate::Result<super::Return> {
apply(state).await
}
#[crate::action]
pub async fn enable(name: impl Into<String>) -> crate::Result<super::Return> {
apply(State {
name,
enablement: Some(Enablement::Enabled),
runtime: None,
})
.await
}
#[crate::action]
pub async fn disable(name: impl Into<String>) -> crate::Result<super::Return> {
apply(State {
name,
enablement: Some(Enablement::Disabled),
runtime: None,
})
.await
}
#[crate::action]
pub async fn mask(name: impl Into<String>) -> crate::Result<super::Return> {
apply(State {
name,
enablement: Some(Enablement::Masked),
runtime: None,
})
.await
}
#[crate::action]
pub async fn start(name: impl Into<String>) -> crate::Result<super::Return> {
apply(State {
name,
enablement: None,
runtime: Some(RuntimeAction::Started),
})
.await
}
#[crate::action]
pub async fn stop(name: impl Into<String>) -> crate::Result<super::Return> {
apply(State {
name,
enablement: None,
runtime: Some(RuntimeAction::Stopped),
})
.await
}
#[crate::action]
pub async fn restart(name: impl Into<String>) -> crate::Result<super::Return> {
apply(State {
name,
enablement: None,
runtime: Some(RuntimeAction::Restarted),
})
.await
}
#[crate::action]
pub async fn reload(name: impl Into<String>) -> crate::Result<super::Return> {
apply(State {
name,
enablement: None,
runtime: Some(RuntimeAction::Reloaded),
})
.await
}
#[crate::action]
pub async fn enable_and_start(name: impl Into<String>) -> crate::Result<super::Return> {
apply(State {
name,
enablement: Some(Enablement::Enabled),
runtime: Some(RuntimeAction::Started),
})
.await
}
#[crate::action]
pub async fn disable_and_stop(name: impl Into<String>) -> crate::Result<super::Return> {
apply(State {
name,
enablement: Some(Enablement::Disabled),
runtime: Some(RuntimeAction::Stopped),
})
.await
}
fn print_action(unit_name: &str, verb: &str) {
use std::io::Write as _;
let (color, reset) = if std::env::var_os("NO_COLOR").is_some_and(|v| !v.is_empty()) {
("", "")
} else {
("\x1b[36;1m", "\x1b[0m") };
let _ = writeln!(
std::io::stderr().lock(),
"{color}~ {unit_name} {verb}{reset}",
);
}
async fn apply(state: State) -> crate::Result<super::Return> {
let unit_name = {
let name = &state.name;
if name.contains('.') {
name.to_owned()
} else {
format!("{name}.service")
}
};
if state.enablement == Some(Enablement::Masked)
&& let Some(runtime) = state.runtime
{
crate::bail!(
"invalid systemd request for `{unit_name}`: a unit can't be masked and have a \
runtime action ({runtime:?}) at once — a masked unit cannot run. Mask it (which \
stops it), or pick Enabled/Disabled if you need it running.",
);
}
let conn = zbus::Connection::system()
.await
.context("Couldn't connect to the system bus")?;
let manager = ManagerProxy::new(&conn)
.await
.context("Couldn't construct Manager proxy")?;
let initial_unit_state = read_file_state(&manager, &unit_name).await?;
let initial_active_state = read_active_state(&manager, &conn, &unit_name).await?;
let observed_runtime = if initial_active_state
.map(ActiveState::is_running)
.unwrap_or(false)
{
RuntimeAction::Started
} else {
RuntimeAction::Stopped
};
let observed_enablement = initial_unit_state.map(|f| {
if f.is_masked() {
Enablement::Masked
} else if matches!(f, UnitState::Enabled | UnitState::EnabledRuntime) {
Enablement::Enabled
} else {
Enablement::Disabled
}
});
let old_view = State {
name: state.name.clone(),
enablement: observed_enablement,
runtime: Some(observed_runtime),
};
let new_view = State {
name: state.name.clone(),
enablement: state.enablement.or(observed_enablement),
runtime: Some(match state.runtime {
None => observed_runtime,
Some(RuntimeAction::Stopped) => RuntimeAction::Stopped,
Some(RuntimeAction::Started | RuntimeAction::Restarted | RuntimeAction::Reloaded) => {
RuntimeAction::Started
}
}),
};
if old_view != new_view {
let _ = <State as crate::Diff>::diff(&old_view, &new_view, &mut std::io::stderr().lock());
}
if state.enablement.is_none() && state.runtime.is_none() {
return Ok(super::Return::Unchanged);
}
let mut changed = false;
let unit_files = vec![unit_name.clone()];
let is_masked = initial_unit_state
.map(UnitState::is_masked)
.unwrap_or(false);
let mut current_file_state = initial_unit_state;
if let Some(wanted_enablement) = state.enablement {
match wanted_enablement {
Enablement::Masked => {
if !is_masked {
let unit_path = std::path::Path::new("/etc/systemd/system").join(&unit_name);
let meta = std::fs::symlink_metadata(&unit_path).ok();
if let Some(meta) = meta.as_ref()
&& !meta.file_type().is_symlink()
{
if meta.is_dir() {
std::fs::remove_dir_all(&unit_path)
.context("Failed to clear conflicting directory at mask target")?;
} else {
std::fs::remove_file(&unit_path)
.context("Failed to clear conflicting file at mask target")?;
}
}
manager
.mask_unit_files(unit_files, false, true) .await
.context(format!("MaskUnitFiles({unit_name}) failed"))?;
manager.reload().await.context("Daemon-reload failed")?;
changed = true;
}
}
Enablement::Disabled => {
if is_masked {
manager
.unmask_unit_files(unit_files.clone(), false)
.await
.context(format!("UnmaskUnitFiles({unit_name}) failed"))?;
changed = true;
current_file_state = read_file_state(&manager, &unit_name).await?;
}
if current_file_state
.map(UnitState::needs_disable_call)
.unwrap_or(false)
{
manager
.disable_unit_files(unit_files, false)
.await
.context(format!("DisableUnitFiles({unit_name}) failed"))?;
changed = true;
}
}
Enablement::Enabled => {
if is_masked {
manager
.unmask_unit_files(unit_files.clone(), false)
.await
.context(format!("UnmaskUnitFiles({unit_name}) failed"))?;
changed = true;
current_file_state = read_file_state(&manager, &unit_name).await?;
}
if current_file_state
.map(UnitState::needs_enable_call)
.unwrap_or(true)
{
manager
.enable_unit_files(unit_files, false, true) .await
.context(format!("EnableUnitFiles({unit_name}) failed"))?;
changed = true;
}
}
}
}
if let Some(runtime_action) = state.runtime {
let active = if changed {
read_active_state(&manager, &conn, &unit_name).await?
} else {
initial_active_state
};
let is_running = active.map(ActiveState::is_running).unwrap_or(false);
match runtime_action {
RuntimeAction::Started => {
if !is_running {
manager
.start_unit(unit_name.clone(), "replace".to_owned())
.await
.context(format!("StartUnit({unit_name}) failed"))?;
changed = true;
}
}
RuntimeAction::Stopped => {
if is_running {
manager
.stop_unit(unit_name.clone(), "replace".to_owned())
.await
.context(format!("StopUnit({unit_name}) failed"))?;
changed = true;
}
}
RuntimeAction::Restarted => {
manager
.restart_unit(unit_name.clone(), "replace".to_owned())
.await
.context(format!("RestartUnit({unit_name}) failed"))?;
print_action(&unit_name, "restarted");
changed = true;
}
RuntimeAction::Reloaded => {
manager
.reload_unit(unit_name.clone(), "replace".to_owned())
.await
.context(format!("ReloadUnit({unit_name}) failed"))?;
print_action(&unit_name, "reloaded");
changed = true;
}
}
}
Ok(super::Return::from_changed(changed))
}