pub use crate::systemd_dbus_wire_format::{CallError, Reply, Target, decode_reply, encode_call};
use crate::systemd_dbus_wire_format::{ERROR, METHOD_RETURN, aligned, read_u32};
use std::io::{Read, Write};
use std::os::unix::net::UnixStream;
use std::path::Path;
use std::time::{Duration, Instant};
pub const MANAGER_PATH: &str = "/org/freedesktop/systemd1";
pub const MANAGER_INTERFACE: &str = "org.freedesktop.systemd1.Manager";
pub const EXCHANGE_WAIT: Duration = Duration::from_secs(20);
const BUS_NAME: &str = "org.freedesktop.DBus";
const BUS_PATH: &str = "/org/freedesktop/DBus";
pub const MANAGER_NAME: &str = "org.freedesktop.systemd1";
const MAX_REPLY_BYTES: usize = 1 << 20;
pub const HELLO: Target<'static> = Target { destination: BUS_NAME, path: BUS_PATH, interface: BUS_NAME, member: "Hello" };
pub const RELOAD: Target<'static> = Target { destination: MANAGER_NAME, path: MANAGER_PATH, interface: MANAGER_INTERFACE, member: "Reload" };
pub const START_UNIT: Target<'static> = Target { destination: MANAGER_NAME, path: MANAGER_PATH, interface: MANAGER_INTERFACE, member: "StartUnit" };
pub const STOP_UNIT: Target<'static> = Target { destination: MANAGER_NAME, path: MANAGER_PATH, interface: MANAGER_INTERFACE, member: "StopUnit" };
pub const RESET_FAILED_UNIT: Target<'static> =
Target { destination: MANAGER_NAME, path: MANAGER_PATH, interface: MANAGER_INTERFACE, member: "ResetFailedUnit" };
pub const GET_UNIT: Target<'static> = Target { destination: MANAGER_NAME, path: MANAGER_PATH, interface: MANAGER_INTERFACE, member: "GetUnit" };
pub const STOP_ALONE_MODE: &str = "ignore-dependencies";
const PROPERTIES_INTERFACE: &str = "org.freedesktop.DBus.Properties";
const UNIT_INTERFACE: &str = "org.freedesktop.systemd1.Unit";
pub const NO_SUCH_UNIT: &str = "org.freedesktop.systemd1.NoSuchUnit";
const UNKNOWN_OBJECT: &str = "org.freedesktop.DBus.Error.UnknownObject";
fn hex_of_byte(byte: u8) -> String {
return format!("{byte:02x}");
}
fn connection_failure(error: std::io::Error) -> CallError {
return CallError::Connection(error.to_string());
}
fn authentication_failure(error: std::io::Error) -> CallError {
return CallError::Authentication(error.to_string());
}
fn authentication_read_failure(error: std::io::Error) -> CallError {
return match socket_failure(&error) {
CallError::TimedOut => CallError::TimedOut,
_ => CallError::Authentication(error.to_string()),
};
}
fn socket_failure_of(error: std::io::Error) -> CallError {
return socket_failure(&error);
}
fn external_identity() -> String {
let user: u32 = unsafe { libc::geteuid() };
return user.to_string().bytes().map(hex_of_byte).collect();
}
fn remaining(deadline: Instant) -> Result<Duration, CallError> {
let left: Duration = deadline.saturating_duration_since(Instant::now());
return if left.is_zero() { Err(CallError::TimedOut) } else { Ok(left) };
}
fn socket_failure(error: &std::io::Error) -> CallError {
return match error.kind() {
std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut => CallError::TimedOut,
_ => CallError::Connection(error.to_string()),
};
}
fn read_line(stream: &mut UnixStream, deadline: Instant) -> Result<String, CallError> {
let mut line: Vec<u8> = Vec::new();
let mut one: [u8; 1] = [0];
while line.len() < 4096 {
stream.set_read_timeout(Some(remaining(deadline)?)).map_err(connection_failure)?;
stream.read_exact(&mut one).map_err(authentication_read_failure)?;
if one[0] == b'\n' {
return Ok(String::from_utf8_lossy(line.as_slice()).trim_end_matches('\r').to_string());
}
line.push(one[0]);
}
return Err(CallError::Authentication(String::from("the answer is too long")));
}
fn read_message(stream: &mut UnixStream, deadline: Instant) -> Result<Vec<u8>, CallError> {
let mut header: Vec<u8> = vec![0; 16];
stream.set_read_timeout(Some(remaining(deadline)?)).map_err(connection_failure)?;
stream.read_exact(header.as_mut_slice()).map_err(socket_failure_of)?;
let body_length: usize = usize::try_from(read_u32(header.as_slice(), 4).unwrap_or(0)).unwrap_or(usize::MAX);
let fields_length: usize = usize::try_from(read_u32(header.as_slice(), 12).unwrap_or(0)).unwrap_or(usize::MAX);
let rest: usize = aligned(16usize.saturating_add(fields_length), 8).saturating_add(body_length).saturating_sub(16);
if rest > MAX_REPLY_BYTES {
return Err(CallError::Malformed(String::from("the reply is larger than this client reads")));
}
let mut remainder: Vec<u8> = vec![0; rest];
stream.set_read_timeout(Some(remaining(deadline)?)).map_err(connection_failure)?;
stream.read_exact(remainder.as_mut_slice()).map_err(socket_failure_of)?;
header.extend(remainder);
return Ok(header);
}
fn bus_in_directory(directory: &Path) -> std::path::PathBuf {
return directory.join("bus");
}
pub fn session_bus_path(address: Option<&str>, runtime_dir: Option<&Path>) -> Option<std::path::PathBuf> {
let mut from_address: Option<std::path::PathBuf> = None;
if let Some(list) = address {
for entry in list.split(';') {
let Some(rest) = entry.strip_prefix("unix:") else {
continue;
};
let mut named: Option<&str> = None;
for option in rest.split(',') {
if let Some(found) = option.strip_prefix("path=") {
named = Some(found);
break;
}
}
let Some(path) = named else {
continue;
};
if path.contains('%') || path.is_empty() {
continue;
}
from_address = Some(std::path::PathBuf::from(path));
break;
}
}
if from_address.is_some() {
return from_address;
}
return runtime_dir.map(bus_in_directory);
}
pub struct Bus {
stream: UnixStream,
deadline: Instant,
serial: u32,
}
impl Bus {
pub fn connect(socket: &Path, wait: Duration) -> Result<Bus, CallError> {
let deadline: Instant = Instant::now() + wait;
let mut stream: UnixStream = UnixStream::connect(socket).map_err(connection_failure)?;
stream.set_write_timeout(Some(wait)).map_err(connection_failure)?;
let mut hello: Vec<u8> = vec![0];
hello.extend(format!("AUTH EXTERNAL {}\r\n", external_identity()).as_bytes());
stream.write_all(hello.as_slice()).map_err(authentication_failure)?;
let answer: String = read_line(&mut stream, deadline)?;
if !answer.starts_with("OK ") {
return Err(CallError::Authentication(answer));
}
stream.write_all(b"BEGIN\r\n").map_err(authentication_failure)?;
let mut bus: Bus = Bus { stream, deadline, serial: 0 };
bus.call(&HELLO, &[])?;
return Ok(bus);
}
pub fn within(&mut self, wait: Duration) {
self.deadline = Instant::now() + wait;
}
pub fn call(&mut self, target: &Target<'_>, arguments: &[&str]) -> Result<Reply, CallError> {
self.serial += 1;
let serial: u32 = self.serial;
self.stream.write_all(encode_call(serial, target, arguments).as_slice()).map_err(socket_failure_of)?;
loop {
let reply: Reply = decode_reply(read_message(&mut self.stream, self.deadline)?.as_slice())?;
if reply.reply_serial != Some(serial) || !matches!(reply.kind, METHOD_RETURN | ERROR) {
continue;
}
return match (reply.kind, reply.error_name.clone()) {
(METHOD_RETURN, _) => Ok(reply),
(_, Some(name)) => Err(CallError::Manager { name, text: reply.first_text.unwrap_or_default() }),
_ => Err(CallError::Malformed(String::from("an error reply without a name"))),
};
}
}
pub fn reload(&mut self) -> Result<(), CallError> {
return self.call(&RELOAD, &[]).map(drop);
}
pub fn reset_failed(&mut self, name: &str) -> Result<(), CallError> {
return self.call(&RESET_FAILED_UNIT, &[name]).map(drop);
}
pub fn stop_unit(&mut self, name: &str) -> Result<(), CallError> {
return self.call(&STOP_UNIT, &[name, "replace"]).map(drop);
}
pub fn stop_unit_alone(&mut self, name: &str) -> Result<(), CallError> {
return self.call(&STOP_UNIT, &[name, STOP_ALONE_MODE]).map(drop);
}
pub fn active_state(&mut self, name: &str) -> Result<Option<String>, CallError> {
let path: String = match self.call(&GET_UNIT, &[name]) {
Ok(reply) => match reply.first_text {
Some(text) => text,
None => return Err(CallError::Malformed(String::from("GetUnit answered without a path"))),
},
Err(CallError::Manager { name: error, .. }) if error == NO_SUCH_UNIT => return Ok(None),
Err(error) => return Err(error),
};
let property: Target<'_> = Target { destination: MANAGER_NAME, path: path.as_str(), interface: PROPERTIES_INTERFACE, member: "Get" };
return match self.call(&property, &[UNIT_INTERFACE, "ActiveState"]) {
Ok(reply) => match reply.first_text {
Some(text) => Ok(Some(text)),
None => Err(CallError::Malformed(String::from("the state property answered without text"))),
},
Err(CallError::Manager { name: error, .. }) if error == NO_SUCH_UNIT || error == UNKNOWN_OBJECT => Ok(None),
Err(error) => Err(error),
};
}
pub fn start_unit(&mut self, name: &str) -> Result<(), CallError> {
return self.call(&START_UNIT, &[name, "replace"]).map(drop);
}
}
#[cfg(test)]
#[path = "systemd_dbus_tests.rs"]
mod tests;