Cleanup of EtcTree.

This commit is contained in:
R-VdP 2023-03-14 16:39:47 +01:00
parent 0e5a2c4bd7
commit 95f9897766
No known key found for this signature in database
3 changed files with 146 additions and 121 deletions

View file

@ -13,7 +13,7 @@ use std::{fs, io};
use crate::{ use crate::{
create_link, create_store_link, remove_dir, remove_file, remove_link, StorePath, create_link, create_store_link, remove_dir, remove_file, remove_link, StorePath,
ETC_STATE_FILE_NAME, SYSTEM_MANAGER_STATE_DIR, ETC_STATE_FILE_NAME, SYSTEM_MANAGER_STATE_DIR, SYSTEM_MANAGER_STATIC_NAME,
}; };
use etc_tree::EtcTree; use etc_tree::EtcTree;
@ -57,16 +57,16 @@ pub fn activate(store_path: &StorePath, ephemeral: bool) -> Result<()> {
DirBuilder::new().recursive(true).create(&etc_dir)?; DirBuilder::new().recursive(true).create(&etc_dir)?;
let old_state = read_created_files()?; let old_state = read_created_files()?;
let initial_state = EtcTree::root_node();
// TODO: constant?
let static_dir_name = ".system-manager-static";
let (state, status) = create_etc_static_link( let (state, status) = create_etc_static_link(
static_dir_name, SYSTEM_MANAGER_STATIC_NAME,
&config.static_env, &config.static_env,
&etc_dir, &etc_dir,
EtcTree::new(PathBuf::new()), initial_state,
); );
status?; status?;
let new_state = create_etc_links(config.entries.values(), &etc_dir, state).update_state( let new_state = create_etc_links(config.entries.values(), &etc_dir, state).update_state(
old_state, old_state,
&|path| { &|path| {
@ -75,7 +75,7 @@ pub fn activate(store_path: &StorePath, ephemeral: bool) -> Result<()> {
}, },
); );
serialise_state(&new_state.unwrap_or_else(|| EtcTree::new(PathBuf::from("/"))))?; serialise_state(new_state)?;
log::info!("Done"); log::info!("Done");
Ok(()) Ok(())
@ -85,17 +85,15 @@ pub fn deactivate() -> Result<()> {
let state = read_created_files()?; let state = read_created_files()?;
log::debug!("{:?}", state); log::debug!("{:?}", state);
serialise_state(&state.deactivate_managed_entry( serialise_state(state.deactivate(&|path| {
Path::new("/"), try_delete_path(path)
&|path| match try_delete_path(path) { .map_err(|e| {
Ok(()) => true,
Err(e) => {
log::error!("Error deleting path: {}", path.display()); log::error!("Error deleting path: {}", path.display());
log::error!("{e}"); log::error!("{e}");
false e
} })
}, .is_ok()
))?; }))?;
log::info!("Done"); log::info!("Done");
Ok(()) Ok(())
@ -154,7 +152,7 @@ fn create_etc_link(link_target: &OsStr, etc_dir: &Path, state: EtcTree) -> (EtcT
match status.and_then(|_| { match status.and_then(|_| {
create_link( create_link(
Path::new(".") Path::new(".")
.join(".system-manager-static") .join(SYSTEM_MANAGER_STATIC_NAME)
.join("etc") .join("etc")
.join(link_target) .join(link_target)
.as_path(), .as_path(),
@ -207,7 +205,7 @@ fn create_dir_recursively(dir: &Path, state: EtcTree) -> (EtcTree, Result<()>) {
let (new_state, _, status) = dir let (new_state, _, status) = dir
.components() .components()
.fold_while( .fold_while(
(state, PathBuf::from("/"), Ok(())), (state, PathBuf::from(path::MAIN_SEPARATOR_STR), Ok(())),
|(state, path, _), component| match component { |(state, path, _), component| match component {
Component::RootDir => Continue((state, path, Ok(()))), Component::RootDir => Continue((state, path, Ok(()))),
Component::Normal(dir) => { Component::Normal(dir) => {
@ -240,10 +238,15 @@ fn create_dir_recursively(dir: &Path, state: EtcTree) -> (EtcTree, Result<()>) {
} }
fn copy_file(source: &Path, target: &Path, mode: &str) -> Result<()> { fn copy_file(source: &Path, target: &Path, mode: &str) -> Result<()> {
fs::copy(source, target)?; let exists = target.try_exists()?;
let mode_int = u32::from_str_radix(mode, 8).map_err(anyhow::Error::from)?; if !exists {
fs::set_permissions(target, Permissions::from_mode(mode_int))?; fs::copy(source, target)?;
Ok(()) let mode_int = u32::from_str_radix(mode, 8)?;
fs::set_permissions(target, Permissions::from_mode(mode_int))?;
Ok(())
} else {
anyhow::bail!("File {} already exists, ignoring.", target.display());
}
} }
fn etc_dir(ephemeral: bool) -> PathBuf { fn etc_dir(ephemeral: bool) -> PathBuf {
@ -254,7 +257,10 @@ fn etc_dir(ephemeral: bool) -> PathBuf {
} }
} }
fn serialise_state(created_files: &EtcTree) -> Result<()> { fn serialise_state<E>(created_files: Option<E>) -> Result<()>
where
E: AsRef<EtcTree>,
{
let state_file = Path::new(SYSTEM_MANAGER_STATE_DIR).join(ETC_STATE_FILE_NAME); let state_file = Path::new(SYSTEM_MANAGER_STATE_DIR).join(ETC_STATE_FILE_NAME);
DirBuilder::new() DirBuilder::new()
.recursive(true) .recursive(true)
@ -262,7 +268,12 @@ fn serialise_state(created_files: &EtcTree) -> Result<()> {
log::info!("Writing state info into file: {}", state_file.display()); log::info!("Writing state info into file: {}", state_file.display());
let writer = io::BufWriter::new(fs::File::create(state_file)?); let writer = io::BufWriter::new(fs::File::create(state_file)?);
serde_json::to_writer(writer, created_files)?;
if let Some(e) = created_files {
serde_json::to_writer(writer, e.as_ref())?;
} else {
serde_json::to_writer(writer, &EtcTree::default())?;
}
Ok(()) Ok(())
} }
@ -283,5 +294,5 @@ fn read_created_files() -> Result<EtcTree> {
} }
} }
} }
Ok(EtcTree::new(PathBuf::from("/"))) Ok(EtcTree::default())
} }

View file

@ -1,22 +1,10 @@
use im::HashMap; use im::HashMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::cmp::Eq; use std::cmp::Eq;
use std::ffi::OsString;
use std::iter::Peekable; use std::iter::Peekable;
use std::path; use std::path;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EtcTree {
status: EtcFileStatus,
path: PathBuf,
// TODO directories and files are now both represented as a string associated with a nested
// map. For files the nested map is simple empty.
// We could potentially optimise this.
nested: HashMap<OsString, EtcTree>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub enum EtcFileStatus { pub enum EtcFileStatus {
@ -35,22 +23,45 @@ impl EtcFileStatus {
} }
} }
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EtcTree {
status: EtcFileStatus,
path: PathBuf,
// TODO directories and files are now both represented as a string associated with a nested
// map. For files the nested map is simple empty.
// We could potentially optimise this.
nested: HashMap<String, EtcTree>,
}
impl AsRef<EtcTree> for EtcTree {
fn as_ref(&self) -> &EtcTree {
self
}
}
impl Default for EtcTree {
fn default() -> Self {
Self::root_node()
}
}
/// Data structure to represent files that are managed by system-manager. /// Data structure to represent files that are managed by system-manager.
/// ///
/// This data will be serialised to disk and read on the next run. /// This data will be serialised to disk and read on the next run.
/// ///
/// We need these basic operations: /// We need these basic operations:
/// 1. Create a new, empty structure /// 1. Create a new root structure
/// 2. Persist to a file /// 2. Persist to a file
/// 3. Import from a file /// 3. Import from a file
/// 4. Add a path to the tree, that will from then on be considered as managed /// 4. Add a path to the tree, that will from then on be considered as managed
/// 5. /// 5.
impl EtcTree { impl EtcTree {
pub fn new(path: PathBuf) -> Self { fn new(path: PathBuf) -> Self {
Self::with_status(path, EtcFileStatus::Unmanaged) Self::with_status(path, EtcFileStatus::Unmanaged)
} }
pub fn with_status(path: PathBuf, status: EtcFileStatus) -> Self { fn with_status(path: PathBuf, status: EtcFileStatus) -> Self {
Self { Self {
status, status,
path, path,
@ -58,6 +69,10 @@ impl EtcTree {
} }
} }
pub fn root_node() -> Self {
Self::new(PathBuf::from(path::MAIN_SEPARATOR_STR))
}
// TODO is recursion OK here? // TODO is recursion OK here?
// Should we convert to CPS and use a crate like tramp to TCO this? // Should we convert to CPS and use a crate like tramp to TCO this?
pub fn register_managed_entry(self, path: &Path) -> Self { pub fn register_managed_entry(self, path: &Path) -> Self {
@ -75,26 +90,24 @@ impl EtcTree {
maybe_subtree.unwrap_or_else(|| { maybe_subtree.unwrap_or_else(|| {
EtcTree::with_status( EtcTree::with_status(
new_path.to_owned(), new_path.to_owned(),
if components.peek().is_some() { components
EtcFileStatus::Unmanaged .peek()
} else { .map_or(EtcFileStatus::Managed, |_| {
EtcFileStatus::Managed EtcFileStatus::Unmanaged
}, }),
) )
}), }),
components, components,
new_path, new_path,
)) ))
}, },
name.to_owned(), name.to_string_lossy().to_string(),
); );
tree tree
} }
path::Component::RootDir => go( path::Component::RootDir => {
tree, go(tree, components, path.join(path::MAIN_SEPARATOR_STR))
components, }
path.join(path::MAIN_SEPARATOR.to_string()),
),
_ => panic!( _ => panic!(
"Unsupported path provided! At path component: {:?}", "Unsupported path provided! At path component: {:?}",
component component
@ -131,63 +144,6 @@ impl EtcTree {
} }
} }
pub fn deactivate_managed_entry<F>(self, path: &Path, delete_action: &F) -> Self
where
F: Fn(&Path) -> bool,
{
fn go<'a, C, F>(
mut tree: EtcTree,
path: PathBuf,
mut components: Peekable<C>,
delete_action: &F,
) -> EtcTree
where
C: Iterator<Item = path::Component<'a>>,
F: Fn(&Path) -> bool,
{
log::debug!("Deactivating {}", path.display());
if let Some(component) = components.next() {
match component {
path::Component::Normal(name) => {
let new_path = path.join(name);
tree.nested = tree.nested.alter(
|maybe_subtree| {
maybe_subtree.and_then(|subtree| {
if components.peek().is_some() {
Some(go(subtree, new_path, components, delete_action))
} else {
subtree.deactivate(delete_action)
}
})
},
name.to_owned(),
);
tree
}
path::Component::RootDir => go(
tree,
path.join(path::MAIN_SEPARATOR.to_string()),
components,
delete_action,
),
_ => panic!(
"Unsupported path provided! At path component: {:?}",
component
),
}
} else {
tree
}
}
go(
self,
PathBuf::new(),
path.components().peekable(),
delete_action,
)
}
pub fn update_state<F>(self, other: Self, delete_action: &F) -> Option<Self> pub fn update_state<F>(self, other: Self, delete_action: &F) -> Option<Self>
where where
F: Fn(&Path) -> bool, F: Fn(&Path) -> bool,
@ -237,29 +193,86 @@ impl EtcTree {
mod tests { mod tests {
use super::*; use super::*;
use itertools::Itertools; use itertools::Itertools;
use std::ffi::OsStr;
impl EtcTree {
pub fn deactivate_managed_entry<F>(self, path: &Path, delete_action: &F) -> Self
where
F: Fn(&Path) -> bool,
{
fn go<'a, C, F>(
mut tree: EtcTree,
path: PathBuf,
mut components: Peekable<C>,
delete_action: &F,
) -> EtcTree
where
C: Iterator<Item = path::Component<'a>>,
F: Fn(&Path) -> bool,
{
log::debug!("Deactivating {}", path.display());
if let Some(component) = components.next() {
match component {
path::Component::Normal(name) => {
let new_path = path.join(name);
tree.nested = tree.nested.alter(
|maybe_subtree| {
maybe_subtree.and_then(|subtree| {
if components.peek().is_some() {
Some(go(subtree, new_path, components, delete_action))
} else {
subtree.deactivate(delete_action)
}
})
},
name.to_string_lossy().to_string(),
);
tree
}
path::Component::RootDir => go(
tree,
path.join(path::MAIN_SEPARATOR.to_string()),
components,
delete_action,
),
_ => panic!(
"Unsupported path provided! At path component: {:?}",
component
),
}
} else {
tree
}
}
go(
self,
PathBuf::new(),
path.components().peekable(),
delete_action,
)
}
}
#[test] #[test]
fn etc_tree_register() { fn etc_tree_register() {
let tree1 = EtcTree::new(PathBuf::from("/")) let tree = EtcTree::root_node()
.register_managed_entry(&PathBuf::from("/").join("foo").join("bar")) .register_managed_entry(&PathBuf::from("/").join("foo").join("bar"))
.register_managed_entry(&PathBuf::from("/").join("foo2").join("baz").join("bar")) .register_managed_entry(&PathBuf::from("/").join("foo2").join("baz").join("bar"))
.register_managed_entry(&PathBuf::from("/").join("foo2").join("baz2").join("bar")); .register_managed_entry(&PathBuf::from("/").join("foo2").join("baz2").join("bar"));
dbg!(&tree1); dbg!(&tree);
assert_eq!( assert_eq!(
tree1.nested.keys().sorted().collect::<Vec<_>>(), tree.nested.keys().sorted().collect::<Vec<_>>(),
["foo", "foo2"] ["foo", "foo2"]
); );
assert_eq!( assert_eq!(
tree1 tree.nested
.nested .get("foo2")
.get(OsStr::new("foo2"))
.unwrap() .unwrap()
.nested .nested
.get(OsStr::new("baz")) .get("baz")
.unwrap() .unwrap()
.nested .nested
.get(OsStr::new("bar")) .get("bar")
.unwrap() .unwrap()
.path, .path,
PathBuf::from("/").join("foo2").join("baz").join("bar") PathBuf::from("/").join("foo2").join("baz").join("bar")
@ -268,7 +281,7 @@ mod tests {
#[test] #[test]
fn etc_tree_deactivate() { fn etc_tree_deactivate() {
let tree1 = EtcTree::new(PathBuf::from("/")) let tree1 = EtcTree::root_node()
.register_managed_entry(&PathBuf::from("/").join("foo").join("bar")) .register_managed_entry(&PathBuf::from("/").join("foo").join("bar"))
.register_managed_entry(&PathBuf::from("/").join("foo2")) .register_managed_entry(&PathBuf::from("/").join("foo2"))
.register_managed_entry(&PathBuf::from("/").join("foo2").join("baz")) .register_managed_entry(&PathBuf::from("/").join("foo2").join("baz"))
@ -301,10 +314,10 @@ mod tests {
); );
assert!(tree2 assert!(tree2
.nested .nested
.get(OsStr::new("foo3")) .get("foo3")
.unwrap() .unwrap()
.nested .nested
.get(OsStr::new("baz2")) .get("baz2")
.unwrap() .unwrap()
.nested .nested
.keys() .keys()
@ -319,7 +332,7 @@ mod tests {
#[test] #[test]
fn etc_tree_update_state() { fn etc_tree_update_state() {
let tree1 = EtcTree::new(PathBuf::from("/")) let tree1 = EtcTree::root_node()
.register_managed_entry(&PathBuf::from("/").join("foo").join("bar")) .register_managed_entry(&PathBuf::from("/").join("foo").join("bar"))
.register_managed_entry(&PathBuf::from("/").join("foo2")) .register_managed_entry(&PathBuf::from("/").join("foo2"))
.register_managed_entry(&PathBuf::from("/").join("foo2").join("baz")) .register_managed_entry(&PathBuf::from("/").join("foo2").join("baz"))
@ -327,7 +340,7 @@ mod tests {
.register_managed_entry(&PathBuf::from("/").join("foo2").join("baz2")) .register_managed_entry(&PathBuf::from("/").join("foo2").join("baz2"))
.register_managed_entry(&PathBuf::from("/").join("foo2").join("baz2").join("bar")) .register_managed_entry(&PathBuf::from("/").join("foo2").join("baz2").join("bar"))
.register_managed_entry(&PathBuf::from("/").join("foo3").join("baz2").join("bar")); .register_managed_entry(&PathBuf::from("/").join("foo3").join("baz2").join("bar"));
let tree2 = EtcTree::new(PathBuf::from("/")) let tree2 = EtcTree::root_node()
.register_managed_entry(&PathBuf::from("/").join("foo").join("bar")) .register_managed_entry(&PathBuf::from("/").join("foo").join("bar"))
.register_managed_entry(&PathBuf::from("/").join("foo3").join("bar")) .register_managed_entry(&PathBuf::from("/").join("foo3").join("bar"))
.register_managed_entry(&PathBuf::from("/").join("foo4")) .register_managed_entry(&PathBuf::from("/").join("foo4"))

View file

@ -15,6 +15,7 @@ const GCROOT_PATH: &str = "/nix/var/nix/gcroots/system-manager-current";
const SYSTEM_MANAGER_STATE_DIR: &str = "/var/lib/system-manager/state"; const SYSTEM_MANAGER_STATE_DIR: &str = "/var/lib/system-manager/state";
const SERVICES_STATE_FILE_NAME: &str = "services.json"; const SERVICES_STATE_FILE_NAME: &str = "services.json";
const ETC_STATE_FILE_NAME: &str = "etc-files.json"; const ETC_STATE_FILE_NAME: &str = "etc-files.json";
const SYSTEM_MANAGER_STATIC_NAME: &str = ".system-manager-static";
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(from = "String", into = "String", rename_all = "camelCase")] #[serde(from = "String", into = "String", rename_all = "camelCase")]