#[derive(Clone, Copy, Debug)]
pub struct Services {
#[cfg(feature = "service")]
list: &'static [fn() -> corundum_framework_service::ServiceSpec],
}
impl Services {
pub const NONE: Services = Services {
#[cfg(feature = "service")]
list: &[],
};
#[cfg(feature = "service")]
pub const fn new(
services: &'static [fn() -> corundum_framework_service::ServiceSpec],
) -> Services {
Services { list: services }
}
#[cfg(feature = "service")]
pub(crate) fn is_empty(&self) -> bool {
self.list.is_empty()
}
#[cfg(feature = "service")]
pub(crate) fn specs(&self) -> impl Iterator<Item = corundum_framework_service::ServiceSpec> {
self.list.iter().map(|service| service())
}
}
pub(crate) trait ServiceDriver: Send + Sync {
fn stop_running(&self) -> Result<(), String>;
fn restart_stopped(&self);
}
impl ServiceDriver for () {
fn stop_running(&self) -> Result<(), String> {
Ok(())
}
fn restart_stopped(&self) {}
}
#[cfg(feature = "service")]
pub(crate) use with_services::*;
#[cfg(not(feature = "service"))]
pub(crate) fn driver(
_spec: &'static crate::start::PackageSpec,
) -> std::sync::Arc<dyn ServiceDriver> {
std::sync::Arc::new(())
}
#[cfg(feature = "service")]
mod with_services {
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use corundum_framework_platform::dirs::AppDirs;
use corundum_framework_service::{
Environment, ServiceManager, ServiceState, SystemManager, start, state, stop,
};
use super::ServiceDriver;
use crate::start::PackageSpec;
pub(crate) const RESTART_MARKER: &str = "services-to-restart";
pub(crate) fn driver(spec: &'static PackageSpec) -> Arc<dyn ServiceDriver> {
Arc::new(System {
spec,
data: AppDirs::new(spec.app_name).map(|dirs| dirs.data().to_owned()),
})
}
struct System {
spec: &'static PackageSpec,
data: Option<PathBuf>,
}
impl ServiceDriver for System {
fn stop_running(&self) -> Result<(), String> {
if self.spec.services.is_empty() {
return Ok(());
}
let data = self
.data
.as_deref()
.ok_or("no data directory to note the services to restart in")?;
stop_running(self.spec, data, &Environment::detect(), &SystemManager).map(|_| ())
}
fn restart_stopped(&self) {
if let Some(data) = &self.data {
restart_stopped(self.spec, data, &Environment::detect(), &SystemManager);
}
}
}
pub(crate) fn stop_running(
spec: &PackageSpec,
data: &Path,
env: &Environment,
manager: &dyn ServiceManager,
) -> Result<Vec<&'static str>, String> {
let mut running = Vec::new();
for service in spec.services.specs() {
match state(&service, env, manager) {
Ok(ServiceState::Running) => running.push(service),
Ok(_) => {}
Err(e) => {
tracing::warn!(
"could not tell whether {} runs, stopping it anyway: {e}",
service.app_name
);
running.push(service);
}
}
}
if running.is_empty() {
return Ok(Vec::new());
}
let names: String = running
.iter()
.map(|service| format!("{}\n", service.app_name))
.collect();
let marker = data.join(RESTART_MARKER);
fs::create_dir_all(data)
.and_then(|()| fs::write(&marker, names))
.map_err(|e| format!("could not write {}: {e}", marker.display()))?;
for service in &running {
let _ = stop(service, env, manager);
}
Ok(running.iter().map(|service| service.app_name).collect())
}
pub(crate) fn restart_stopped(
spec: &PackageSpec,
data: &Path,
env: &Environment,
manager: &dyn ServiceManager,
) {
let marker = data.join(RESTART_MARKER);
let names = match fs::read(&marker) {
Ok(bytes) => String::from_utf8_lossy(&bytes).into_owned(),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return,
Err(e) => {
tracing::warn!("could not read {}: {e}", marker.display());
String::new()
}
};
for name in names.lines().map(str::trim).filter(|name| !name.is_empty()) {
let Some(service) = spec
.services
.specs()
.find(|service| service.app_name == name)
else {
tracing::warn!(
"{name:?} was stopped for an update but is no service of the app now"
);
continue;
};
if let Err(e) = start(&service, env, manager) {
tracing::warn!("could not start {name} again after the update: {e}");
}
}
if let Err(e) = fs::remove_file(&marker) {
tracing::warn!("could not remove {}: {e}", marker.display());
}
}
#[cfg(any(windows, test))]
pub(crate) fn uninstall_all(
spec: &PackageSpec,
env: &Environment,
manager: &dyn ServiceManager,
) {
for service in spec.services.specs() {
if let Err(e) = corundum_framework_service::uninstall(&service, env, manager) {
tracing::warn!("could not uninstall {}: {e}", service.app_name);
}
}
}
#[cfg(test)]
mod tests {
use std::fs;
use std::path::PathBuf;
use corundum_framework_service::{
Environment, Os, RecordingManager, ServiceSpec, unit_path,
};
use super::*;
use crate::services::Services;
fn demo() -> ServiceSpec {
ServiceSpec {
app_name: "demo",
description: "Demo daemon".into(),
args: vec!["daemon".into()],
legacy_labels: vec![],
post_install: None,
}
}
fn idle() -> ServiceSpec {
ServiceSpec {
app_name: "idle",
..demo()
}
}
fn package(services: &'static [fn() -> ServiceSpec]) -> PackageSpec {
PackageSpec {
app_name: "demo",
on_installed: None,
on_uninstalling: None,
before_apply: None,
cli: None,
services: Services::new(services),
}
}
fn home() -> PathBuf {
PathBuf::from("/home/user")
}
fn linux() -> Environment {
Environment {
euid: 1000,
os: Os::Linux,
home: Some(home()),
}
}
fn command(parts: &[&str]) -> Vec<String> {
parts.iter().map(|part| (*part).to_owned()).collect()
}
fn installed() -> RecordingManager {
RecordingManager::new()
.with_unit(
unit_path("demo", &home()),
"[Service]\nExecStart=/usr/bin/demo daemon\n",
)
.with_unit(
unit_path("idle", &home()),
"[Service]\nExecStart=/usr/bin/idle daemon\n",
)
.with_file("/usr/bin/demo")
.with_file("/usr/bin/idle")
.answering("idle", "inactive")
.answering("is-active", "active")
}
fn marker(data: &Path) -> PathBuf {
data.join(RESTART_MARKER)
}
#[test]
fn a_running_service_is_stopped_and_marked() {
let data = tempfile::tempdir().unwrap();
let manager = installed();
let stopped = stop_running(&package(&[demo]), data.path(), &linux(), &manager).unwrap();
assert_eq!(stopped, ["demo"]);
assert_eq!(fs::read_to_string(marker(data.path())).unwrap(), "demo\n");
assert!(manager.calls().commands.contains(&command(&[
"systemctl",
"--user",
"stop",
"demo"
])));
}
#[test]
fn only_the_spec_s_running_services_are_stopped() {
let data = tempfile::tempdir().unwrap();
let manager = installed();
let stopped =
stop_running(&package(&[demo, idle]), data.path(), &linux(), &manager).unwrap();
assert_eq!(stopped, ["demo"]);
assert_eq!(fs::read_to_string(marker(data.path())).unwrap(), "demo\n");
let commands = manager.calls().commands;
assert!(!commands.contains(&command(&["systemctl", "--user", "stop", "idle"])));
let data = tempfile::tempdir().unwrap();
let manager = installed();
let stopped = stop_running(&package(&[]), data.path(), &linux(), &manager).unwrap();
assert!(stopped.is_empty());
assert!(manager.calls().commands.is_empty());
assert!(!marker(data.path()).exists());
}
#[test]
fn the_marked_services_start_and_the_marker_goes() {
let data = tempfile::tempdir().unwrap();
fs::write(marker(data.path()), "demo\n").unwrap();
let manager = RecordingManager::new();
restart_stopped(&package(&[demo, idle]), data.path(), &linux(), &manager);
assert_eq!(
manager.calls().commands,
[command(&["systemctl", "--user", "start", "demo"])]
);
assert!(!marker(data.path()).exists());
}
#[test]
fn a_marker_naming_an_unknown_service_is_dropped() {
let data = tempfile::tempdir().unwrap();
fs::write(marker(data.path()), b"gone\n\xff\n").unwrap();
let manager = RecordingManager::new();
restart_stopped(&package(&[demo]), data.path(), &linux(), &manager);
assert!(manager.calls().commands.is_empty());
assert!(!marker(data.path()).exists());
restart_stopped(&package(&[demo]), data.path(), &linux(), &manager);
assert!(manager.calls().commands.is_empty());
}
#[test]
fn a_failed_start_is_logged_and_the_marker_still_goes() {
let data = tempfile::tempdir().unwrap();
fs::write(marker(data.path()), "demo\nidle\n").unwrap();
let manager = RecordingManager::failing_on("start");
restart_stopped(&package(&[demo, idle]), data.path(), &linux(), &manager);
assert_eq!(manager.calls().commands.len(), 2);
assert!(!marker(data.path()).exists());
}
#[test]
fn a_service_whose_state_cannot_be_read_is_stopped_and_marked() {
let data = tempfile::tempdir().unwrap();
let env = Environment {
home: None,
..linux()
};
let manager = installed();
let stopped = stop_running(&package(&[demo]), data.path(), &env, &manager).unwrap();
assert_eq!(stopped, ["demo"]);
assert_eq!(fs::read_to_string(marker(data.path())).unwrap(), "demo\n");
assert!(manager.calls().commands.contains(&command(&[
"systemctl",
"--user",
"stop",
"demo"
])));
}
#[test]
fn a_marker_that_cannot_be_written_stops_nothing() {
let data = tempfile::tempdir().unwrap();
let unwritable = data.path().join("file");
fs::write(&unwritable, "").unwrap();
let manager = installed();
let err = stop_running(&package(&[demo]), &unwritable, &linux(), &manager).unwrap_err();
assert!(err.contains(RESTART_MARKER), "{err}");
assert!(!manager.calls().commands.contains(&command(&[
"systemctl",
"--user",
"stop",
"demo"
])));
}
#[test]
fn uninstall_all_uninstalls_each_service() {
let manager = RecordingManager::new();
uninstall_all(&package(&[demo, idle]), &linux(), &manager);
assert_eq!(
manager.calls().removed,
[unit_path("demo", &home()), unit_path("idle", &home())]
);
}
}
}