systemd-transient 0.2.0

Start transient systemd services using dbus, just like systemd-run.
Documentation
//! Against the calling user's systemd manager. Ignored by default, as CI
//! usually has no session bus: `cargo test -- --ignored`.

use std::time::Duration;

use systemd_transient::{
  ExecCommand, ServiceType, SystemdTransientManager, UnitExit, UnitProperty, escape_unit_name,
};

fn name(test: &str) -> String {
  format!("systemd-transient-test-{test}-{}", std::process::id())
}

fn command(args: &[&str]) -> Vec<UnitProperty> {
  vec![
    UnitProperty::Type(ServiceType::Exec),
    UnitProperty::ExecStart(vec![ExecCommand::new("/usr/bin/env", args.iter().copied())]),
  ]
}

async fn manager() -> SystemdTransientManager {
  SystemdTransientManager::session().await.unwrap()
}

/// Waits until systemd no longer lists `unit`.
async fn wait_unloaded(manager: &SystemdTransientManager, unit: &str) {
  for _ in 0..50 {
    if manager.list_units(&[unit]).await.unwrap().is_empty() {
      return;
    }
    tokio::time::sleep(Duration::from_millis(100)).await;
  }
  panic!("{unit} is still loaded");
}

#[tokio::test]
#[ignore]
async fn adopted_unit_survives_the_starting_connection() {
  let name = name("adopt-running");
  let unit = format!("{name}.service");

  let first = manager().await;
  let started = first.start(&name, command(&["sleep", "30"])).await.unwrap();

  let second = manager().await;
  let adopted = second.adopt(&name).await.unwrap().expect("unit is loaded");

  // Closes the first connection, which drops its reference.
  drop(started);
  drop(first);
  tokio::time::sleep(Duration::from_millis(500)).await;

  let listed = second.list_units(&[&unit]).await.unwrap();
  assert_eq!(listed[0].active_state, "active");

  assert_eq!(adopted.stop().await.unwrap(), UnitExit::Inactive);
  adopted.release().await.unwrap();
  wait_unloaded(&second, &unit).await;
}

#[tokio::test]
#[ignore]
async fn failed_unit_stays_adoptable_without_references() {
  let name = name("adopt-failed");
  let unit = format!("{name}.service");

  let first = manager().await;
  let started = first.start(&name, command(&["false"])).await.unwrap();
  assert_eq!(started.wait_for_exit().await.unwrap(), UnitExit::Failed);
  started.release().await.unwrap();
  drop(first);

  let second = manager().await;
  let adopted = second
    .adopt(&name)
    .await
    .unwrap()
    .expect("failed units stay loaded");
  assert_eq!(adopted.wait_for_exit().await.unwrap(), UnitExit::Failed);
  adopted.release().await.unwrap();

  // start resets the failed unit and reuses the name.
  let again = second.start(&name, command(&["true"])).await.unwrap();
  assert_eq!(again.wait_for_exit().await.unwrap(), UnitExit::Inactive);
  again.release().await.unwrap();
  wait_unloaded(&second, &unit).await;
}

#[tokio::test]
#[ignore]
async fn cleanly_exited_unit_is_collected_and_not_adoptable() {
  let name = name("adopt-inactive");
  let unit = format!("{name}.service");

  let manager = manager().await;
  let started = manager.start(&name, command(&["true"])).await.unwrap();
  assert_eq!(started.wait_for_exit().await.unwrap(), UnitExit::Inactive);
  started.release().await.unwrap();
  wait_unloaded(&manager, &unit).await;

  assert!(manager.adopt(&name).await.unwrap().is_none());
  // The lookup must not leave a stub behind.
  wait_unloaded(&manager, &unit).await;
}

#[tokio::test]
#[ignore]
async fn transient_slice_with_escaped_child_name() {
  let parent = name("slice");
  let slice = format!("{parent}-{}.slice", escape_unit_name("a-b"));

  let manager = manager().await;
  let slice_handle = manager.start(&slice, []).await.unwrap();

  let service = name("in-slice");
  let mut props = command(&["sleep", "30"]);
  props.push(UnitProperty::Slice(slice.clone()));
  let service_handle = manager.start(&service, props).await.unwrap();

  let listed = manager.list_units(&[&slice]).await.unwrap();
  assert_eq!(listed[0].active_state, "active");

  // Stopping the slice stops what runs in it.
  assert_eq!(slice_handle.stop().await.unwrap(), UnitExit::Inactive);
  assert_eq!(
    service_handle.wait_for_exit().await.unwrap(),
    UnitExit::Inactive
  );

  service_handle.release().await.unwrap();
  slice_handle.release().await.unwrap();
}