Complete rework of the systemd logic.
This commit is contained in:
parent
806b1f23fd
commit
58353436c2
9 changed files with 316 additions and 164 deletions
|
|
@ -12,7 +12,7 @@ use std::path::{Path, PathBuf};
|
|||
use std::{fs, io};
|
||||
|
||||
use crate::{
|
||||
create_link, create_store_link, remove_dir, remove_file, remove_link, StorePath,
|
||||
create_link, create_store_link, etc_dir, remove_dir, remove_file, remove_link, StorePath,
|
||||
ETC_STATE_FILE_NAME, SYSTEM_MANAGER_STATE_DIR, SYSTEM_MANAGER_STATIC_NAME,
|
||||
};
|
||||
use etc_tree::EtcTree;
|
||||
|
|
@ -42,16 +42,19 @@ struct EtcFilesConfig {
|
|||
|
||||
impl std::fmt::Display for EtcFilesConfig {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
writeln!(f, "Files in config:")?;
|
||||
self.entries.values().try_for_each(|entry| {
|
||||
writeln!(
|
||||
f,
|
||||
"target: {}, source:{}, mode:{}",
|
||||
entry.target.display(),
|
||||
entry.source,
|
||||
entry.mode
|
||||
)
|
||||
})
|
||||
let out: String = itertools::intersperse(
|
||||
self.entries.values().map(|entry| {
|
||||
format!(
|
||||
"target: {}, source:{}, mode:{}",
|
||||
entry.target.display(),
|
||||
entry.source,
|
||||
entry.mode
|
||||
)
|
||||
}),
|
||||
"\n".to_owned(),
|
||||
)
|
||||
.collect();
|
||||
write!(f, "{out}")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -284,14 +287,6 @@ fn copy_file(source: &Path, target: &Path, mode: &str, old_state: &EtcTree) -> R
|
|||
}
|
||||
}
|
||||
|
||||
fn etc_dir(ephemeral: bool) -> PathBuf {
|
||||
if ephemeral {
|
||||
Path::new("/run").join("etc")
|
||||
} else {
|
||||
PathBuf::from("/etc")
|
||||
}
|
||||
}
|
||||
|
||||
fn serialise_state<E>(created_files: Option<E>) -> Result<()>
|
||||
where
|
||||
E: AsRef<EtcTree>,
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
use anyhow::Result;
|
||||
use anyhow::{Context, Result};
|
||||
use im::{HashMap, HashSet};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs::DirBuilder;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::{self, Path};
|
||||
use std::time::Duration;
|
||||
use std::{fs, io, str};
|
||||
|
||||
use crate::{
|
||||
create_store_link, remove_link, systemd, StorePath, SERVICES_STATE_FILE_NAME,
|
||||
SYSTEM_MANAGER_STATE_DIR,
|
||||
create_link, etc_dir, systemd, StorePath, SERVICES_STATE_FILE_NAME, SYSTEM_MANAGER_STATE_DIR,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
|
@ -20,44 +19,22 @@ struct ServiceConfig {
|
|||
type Services = HashMap<String, ServiceConfig>;
|
||||
|
||||
fn print_services(services: &Services) -> Result<String> {
|
||||
use std::fmt::Write as _;
|
||||
|
||||
let mut out = String::new();
|
||||
writeln!(out, "Services in config:")?;
|
||||
services
|
||||
.iter()
|
||||
.try_for_each(|(name, entry)| writeln!(out, "name: {name}, source:{}", entry.store_path))?;
|
||||
let out = itertools::intersperse(
|
||||
services
|
||||
.iter()
|
||||
.map(|(name, entry)| format!("name: {name}, source:{}", entry.store_path)),
|
||||
"\n".to_owned(),
|
||||
)
|
||||
.collect();
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct LinkedServiceConfig {
|
||||
#[serde(flatten)]
|
||||
service_config: ServiceConfig,
|
||||
#[serde(rename = "linkedPath")]
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl LinkedServiceConfig {
|
||||
fn linked_path(&self) -> PathBuf {
|
||||
PathBuf::from(&self.path)
|
||||
}
|
||||
|
||||
fn new(service_config: ServiceConfig, path: PathBuf) -> Self {
|
||||
LinkedServiceConfig {
|
||||
service_config,
|
||||
path,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type LinkedServices = HashMap<String, LinkedServiceConfig>;
|
||||
|
||||
pub fn activate(store_path: &StorePath, ephemeral: bool) -> Result<()> {
|
||||
let old_linked_services = read_linked_services()?;
|
||||
verify_systemd_dir(ephemeral)?;
|
||||
|
||||
log::info!("Reading service definitions...");
|
||||
let old_services = read_saved_services()?;
|
||||
|
||||
log::info!("Reading new service definitions...");
|
||||
let file = fs::File::open(
|
||||
Path::new(&store_path.store_path)
|
||||
.join("services")
|
||||
|
|
@ -67,94 +44,158 @@ pub fn activate(store_path: &StorePath, ephemeral: bool) -> Result<()> {
|
|||
let services: Services = serde_json::from_reader(reader)?;
|
||||
log::debug!("{}", print_services(&services)?);
|
||||
|
||||
let linked_services = link_services(services, ephemeral)?;
|
||||
serialise_linked_services(&linked_services)?;
|
||||
serialise_saved_services(&services)?;
|
||||
|
||||
let services_to_stop = old_linked_services.relative_complement(linked_services.clone());
|
||||
let services_to_stop = old_services.clone().relative_complement(services.clone());
|
||||
|
||||
let service_manager = systemd::ServiceManager::new_session()?;
|
||||
let job_monitor = service_manager.monitor_jobs_init()?;
|
||||
let timeout = Some(Duration::from_secs(30));
|
||||
|
||||
// We need to do this before we reload the systemd daemon, so that the daemon
|
||||
// still knows about these units.
|
||||
stop_services(&service_manager, &services_to_stop, &timeout)?;
|
||||
unlink_services(&services_to_stop)?;
|
||||
wait_for_jobs(
|
||||
&service_manager,
|
||||
job_monitor,
|
||||
stop_services(&service_manager, &services_to_stop)?,
|
||||
&timeout,
|
||||
)?;
|
||||
|
||||
// We added all new services and removed old ones, so let's reload the units
|
||||
// to tell systemd about them.
|
||||
log::info!("Reloading the systemd daemon...");
|
||||
service_manager.daemon_reload()?;
|
||||
|
||||
start_services(&service_manager, &linked_services, &timeout)?;
|
||||
let active_targets = get_active_targets(&service_manager);
|
||||
let services_to_reload = get_services_to_reload(services, old_services);
|
||||
|
||||
let job_monitor = service_manager.monitor_jobs_init()?;
|
||||
wait_for_jobs(
|
||||
&service_manager,
|
||||
job_monitor,
|
||||
reload_services(&service_manager, &services_to_reload)?
|
||||
+ start_units(&service_manager, active_targets?)?,
|
||||
&timeout,
|
||||
)?;
|
||||
|
||||
log::info!("Done");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_active_targets(
|
||||
service_manager: &systemd::ServiceManager,
|
||||
) -> Result<Vec<systemd::UnitStatus>> {
|
||||
// We exclude some targets that we do not want to start
|
||||
let excluded_targets: HashSet<String> =
|
||||
["suspend.target", "hibernate.target", "hybrid-sleep.target"]
|
||||
.iter()
|
||||
.map(ToOwned::to_owned)
|
||||
.collect();
|
||||
Ok(service_manager
|
||||
.list_units_by_patterns(&["active", "activating"], &[])?
|
||||
.into_iter()
|
||||
.filter(|unit| {
|
||||
unit.name.ends_with(".target")
|
||||
&& !excluded_targets.contains(&unit.name)
|
||||
&& !service_manager
|
||||
.unit_manager(unit)
|
||||
.refuse_manual_start()
|
||||
.unwrap_or_else(|e| {
|
||||
log::error!("Error communicating with DBus: {}", e);
|
||||
true
|
||||
})
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn get_services_to_reload(services: Services, old_services: Services) -> Services {
|
||||
let mut services_to_reload = services.intersection(old_services.clone());
|
||||
services_to_reload.retain(|name, service| {
|
||||
if let Some(old_service) = old_services.get(name) {
|
||||
service.store_path != old_service.store_path
|
||||
} else {
|
||||
// Since we run this on the intersection, this should never happen
|
||||
panic!("Something went terribly wrong!");
|
||||
}
|
||||
});
|
||||
services_to_reload
|
||||
}
|
||||
|
||||
fn verify_systemd_dir(ephemeral: bool) -> Result<()> {
|
||||
if ephemeral {
|
||||
let system_dir = Path::new(path::MAIN_SEPARATOR_STR)
|
||||
.join("run")
|
||||
.join("systemd")
|
||||
.join("system");
|
||||
if system_dir.exists()
|
||||
&& !system_dir.is_symlink()
|
||||
&& system_dir.is_dir()
|
||||
&& system_dir.read_dir()?.next().is_some()
|
||||
{
|
||||
anyhow::bail!(
|
||||
"The directory {} exists and is not empty, we cannot symlink it.",
|
||||
system_dir.display()
|
||||
);
|
||||
} else if system_dir.exists() {
|
||||
if !system_dir.is_symlink() && system_dir.is_dir() {
|
||||
fs::remove_dir(&system_dir).with_context(|| {
|
||||
format!(
|
||||
"Error while removing the empty dir at {}",
|
||||
system_dir.display()
|
||||
)
|
||||
})?;
|
||||
} else {
|
||||
fs::remove_file(&system_dir).with_context(|| {
|
||||
format!(
|
||||
"Error while removing the symlink at {}",
|
||||
system_dir.display()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
let target = etc_dir(ephemeral).join("systemd").join("system");
|
||||
create_link(&target, &system_dir).with_context(|| {
|
||||
format!(
|
||||
"Error while creating symlink: {} -> {}",
|
||||
system_dir.display(),
|
||||
target.display(),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn deactivate() -> Result<()> {
|
||||
let old_linked_services = read_linked_services()?;
|
||||
log::debug!("{:?}", old_linked_services);
|
||||
let old_services = read_saved_services()?;
|
||||
log::debug!("{:?}", old_services);
|
||||
|
||||
let service_manager = systemd::ServiceManager::new_session()?;
|
||||
let job_monitor = service_manager.monitor_jobs_init()?;
|
||||
let timeout = Some(Duration::from_secs(30));
|
||||
|
||||
// We need to do this before we reload the systemd daemon, so that the daemon
|
||||
// still knows about these units.
|
||||
stop_services(&service_manager, &old_linked_services, &timeout)?;
|
||||
unlink_services(&old_linked_services)?;
|
||||
wait_for_jobs(
|
||||
&service_manager,
|
||||
job_monitor,
|
||||
stop_services(&service_manager, &old_services)?,
|
||||
&timeout,
|
||||
)?;
|
||||
|
||||
// We added all new services and removed old ones, so let's reload the units
|
||||
// to tell systemd about them.
|
||||
// We removed all old services, so let's reload the units so that
|
||||
// the systemd daemon is up-to-date
|
||||
log::info!("Reloading the systemd daemon...");
|
||||
service_manager.daemon_reload()?;
|
||||
|
||||
serialise_linked_services(&HashMap::new())?;
|
||||
serialise_saved_services(&HashMap::new())?;
|
||||
|
||||
log::info!("Done");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn unlink_services(services: &LinkedServices) -> Result<()> {
|
||||
services
|
||||
.values()
|
||||
.try_for_each(|linked_service| remove_link(&linked_service.linked_path()))
|
||||
}
|
||||
|
||||
fn link_services(services: Services, ephemeral: bool) -> Result<LinkedServices> {
|
||||
let systemd_system_dir = systemd_system_dir(ephemeral);
|
||||
services.iter().try_fold(
|
||||
HashMap::new(),
|
||||
|mut linked_services, (name, service_config)| {
|
||||
let linked_path = systemd_system_dir.join(name);
|
||||
match create_store_link(&service_config.store_path, &linked_path) {
|
||||
Ok(_) => {
|
||||
linked_services.insert(
|
||||
name.to_owned(),
|
||||
LinkedServiceConfig::new(service_config.to_owned(), linked_path),
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Error linking service {name}, skipping.");
|
||||
log::error!("{:?}", e);
|
||||
}
|
||||
};
|
||||
Ok(linked_services)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn systemd_system_dir(ephemeral: bool) -> PathBuf {
|
||||
(if ephemeral {
|
||||
Path::new("/run")
|
||||
} else {
|
||||
Path::new("/etc")
|
||||
})
|
||||
.join("systemd")
|
||||
.join("system")
|
||||
}
|
||||
|
||||
// FIXME: we should probably lock this file to avoid concurrent writes
|
||||
fn serialise_linked_services(linked_services: &LinkedServices) -> Result<()> {
|
||||
fn serialise_saved_services(services: &Services) -> Result<()> {
|
||||
let state_file = Path::new(SYSTEM_MANAGER_STATE_DIR).join(SERVICES_STATE_FILE_NAME);
|
||||
DirBuilder::new()
|
||||
.recursive(true)
|
||||
|
|
@ -162,11 +203,11 @@ fn serialise_linked_services(linked_services: &LinkedServices) -> Result<()> {
|
|||
|
||||
log::info!("Writing state info into file: {}", state_file.display());
|
||||
let writer = io::BufWriter::new(fs::File::create(state_file)?);
|
||||
serde_json::to_writer(writer, linked_services)?;
|
||||
serde_json::to_writer(writer, services)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_linked_services() -> Result<LinkedServices> {
|
||||
fn read_saved_services() -> Result<Services> {
|
||||
let state_file = Path::new(SYSTEM_MANAGER_STATE_DIR).join(SERVICES_STATE_FILE_NAME);
|
||||
DirBuilder::new()
|
||||
.recursive(true)
|
||||
|
|
@ -186,47 +227,29 @@ fn read_linked_services() -> Result<LinkedServices> {
|
|||
Ok(HashMap::default())
|
||||
}
|
||||
|
||||
fn start_services(
|
||||
service_manager: &systemd::ServiceManager,
|
||||
services: &LinkedServices,
|
||||
timeout: &Option<Duration>,
|
||||
) -> Result<()> {
|
||||
for_each_service(
|
||||
|s| service_manager.start_unit(s),
|
||||
service_manager,
|
||||
services,
|
||||
timeout,
|
||||
"restarting",
|
||||
)
|
||||
}
|
||||
|
||||
fn stop_services(
|
||||
service_manager: &systemd::ServiceManager,
|
||||
services: &LinkedServices,
|
||||
timeout: &Option<Duration>,
|
||||
) -> Result<()> {
|
||||
for_each_service(
|
||||
|s| service_manager.stop_unit(s),
|
||||
service_manager,
|
||||
services,
|
||||
timeout,
|
||||
"stopping",
|
||||
)
|
||||
services: &Services,
|
||||
) -> Result<HashSet<JobId>> {
|
||||
for_each_service(|s| service_manager.stop_unit(s), services, "stopping")
|
||||
}
|
||||
|
||||
fn reload_services(
|
||||
service_manager: &systemd::ServiceManager,
|
||||
services: &Services,
|
||||
) -> Result<HashSet<JobId>> {
|
||||
for_each_service(|s| service_manager.reload_unit(s), services, "reloading")
|
||||
}
|
||||
|
||||
fn for_each_service<F, R>(
|
||||
action: F,
|
||||
service_manager: &systemd::ServiceManager,
|
||||
services: &LinkedServices,
|
||||
timeout: &Option<Duration>,
|
||||
services: &Services,
|
||||
log_action: &str,
|
||||
) -> Result<()>
|
||||
) -> Result<HashSet<JobId>>
|
||||
where
|
||||
F: Fn(&str) -> Result<R>,
|
||||
{
|
||||
let job_monitor = service_manager.monitor_jobs_init()?;
|
||||
|
||||
let successful_services: HashSet<String> =
|
||||
let successful_services: HashSet<JobId> =
|
||||
services
|
||||
.clone()
|
||||
.into_iter()
|
||||
|
|
@ -234,7 +257,7 @@ where
|
|||
match action(&service) {
|
||||
Ok(_) => {
|
||||
log::info!("Service {}: {}...", service, log_action);
|
||||
set.insert(service);
|
||||
set.insert(JobId { id: service });
|
||||
set
|
||||
}
|
||||
Err(e) => {
|
||||
|
|
@ -246,11 +269,67 @@ where
|
|||
}
|
||||
}
|
||||
});
|
||||
// TODO: do we want to propagate unit failures here in some way?
|
||||
Ok(successful_services)
|
||||
}
|
||||
|
||||
if !service_manager.monitor_jobs_finish(job_monitor, timeout, successful_services)? {
|
||||
anyhow::bail!("Timeout waiting for systemd jobs");
|
||||
}
|
||||
fn start_units(
|
||||
service_manager: &systemd::ServiceManager,
|
||||
units: Vec<systemd::UnitStatus>,
|
||||
) -> Result<HashSet<JobId>> {
|
||||
for_each_unit(|s| service_manager.start_unit(&s.name), units, "restarting")
|
||||
}
|
||||
|
||||
fn for_each_unit<F, R>(
|
||||
action: F,
|
||||
units: Vec<systemd::UnitStatus>,
|
||||
log_action: &str,
|
||||
) -> Result<HashSet<JobId>>
|
||||
where
|
||||
F: Fn(&systemd::UnitStatus) -> Result<R>,
|
||||
{
|
||||
let successful_services: HashSet<JobId> =
|
||||
units
|
||||
.into_iter()
|
||||
.fold(HashSet::new(), |mut set, unit| match action(&unit) {
|
||||
Ok(_) => {
|
||||
log::info!("Unit {}: {}...", unit.name, log_action);
|
||||
set.insert(JobId { id: unit.name });
|
||||
set
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
"Service {}: error {log_action}, please consult the logs",
|
||||
unit.name
|
||||
);
|
||||
log::error!("{e}");
|
||||
set
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: do we want to propagate unit failures here in some way?
|
||||
Ok(successful_services)
|
||||
}
|
||||
|
||||
fn wait_for_jobs(
|
||||
service_manager: &systemd::ServiceManager,
|
||||
job_monitor: systemd::JobMonitor,
|
||||
jobs: HashSet<JobId>,
|
||||
timeout: &Option<Duration>,
|
||||
) -> Result<()> {
|
||||
if !service_manager.monitor_jobs_finish(job_monitor, timeout, jobs)? {
|
||||
anyhow::bail!("Timeout waiting for systemd jobs");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Hash, Clone)]
|
||||
struct JobId {
|
||||
id: String,
|
||||
}
|
||||
|
||||
impl From<JobId> for String {
|
||||
fn from(value: JobId) -> Self {
|
||||
value.id
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue