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::{
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;
@ -57,16 +57,16 @@ pub fn activate(store_path: &StorePath, ephemeral: bool) -> Result<()> {
DirBuilder::new().recursive(true).create(&etc_dir)?;
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(
static_dir_name,
SYSTEM_MANAGER_STATIC_NAME,
&config.static_env,
&etc_dir,
EtcTree::new(PathBuf::new()),
initial_state,
);
status?;
let new_state = create_etc_links(config.entries.values(), &etc_dir, state).update_state(
old_state,
&|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");
Ok(())
@ -85,17 +85,15 @@ pub fn deactivate() -> Result<()> {
let state = read_created_files()?;
log::debug!("{:?}", state);
serialise_state(&state.deactivate_managed_entry(
Path::new("/"),
&|path| match try_delete_path(path) {
Ok(()) => true,
Err(e) => {
serialise_state(state.deactivate(&|path| {
try_delete_path(path)
.map_err(|e| {
log::error!("Error deleting path: {}", path.display());
log::error!("{e}");
false
}
},
))?;
e
})
.is_ok()
}))?;
log::info!("Done");
Ok(())
@ -154,7 +152,7 @@ fn create_etc_link(link_target: &OsStr, etc_dir: &Path, state: EtcTree) -> (EtcT
match status.and_then(|_| {
create_link(
Path::new(".")
.join(".system-manager-static")
.join(SYSTEM_MANAGER_STATIC_NAME)
.join("etc")
.join(link_target)
.as_path(),
@ -207,7 +205,7 @@ fn create_dir_recursively(dir: &Path, state: EtcTree) -> (EtcTree, Result<()>) {
let (new_state, _, status) = dir
.components()
.fold_while(
(state, PathBuf::from("/"), Ok(())),
(state, PathBuf::from(path::MAIN_SEPARATOR_STR), Ok(())),
|(state, path, _), component| match component {
Component::RootDir => Continue((state, path, Ok(()))),
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<()> {
fs::copy(source, target)?;
let mode_int = u32::from_str_radix(mode, 8).map_err(anyhow::Error::from)?;
fs::set_permissions(target, Permissions::from_mode(mode_int))?;
Ok(())
let exists = target.try_exists()?;
if !exists {
fs::copy(source, target)?;
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 {
@ -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);
DirBuilder::new()
.recursive(true)
@ -262,7 +268,12 @@ fn serialise_state(created_files: &EtcTree) -> 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, 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(())
}
@ -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 serde::{Deserialize, Serialize};
use std::cmp::Eq;
use std::ffi::OsString;
use std::iter::Peekable;
use std::path;
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)]
#[serde(rename_all = "camelCase")]
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.
///
/// This data will be serialised to disk and read on the next run.
///
/// We need these basic operations:
/// 1. Create a new, empty structure
/// 1. Create a new root structure
/// 2. Persist to a file
/// 3. Import from a file
/// 4. Add a path to the tree, that will from then on be considered as managed
/// 5.
impl EtcTree {
pub fn new(path: PathBuf) -> Self {
fn new(path: PathBuf) -> Self {
Self::with_status(path, EtcFileStatus::Unmanaged)
}
pub fn with_status(path: PathBuf, status: EtcFileStatus) -> Self {
fn with_status(path: PathBuf, status: EtcFileStatus) -> Self {
Self {
status,
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?
// Should we convert to CPS and use a crate like tramp to TCO this?
pub fn register_managed_entry(self, path: &Path) -> Self {
@ -75,26 +90,24 @@ impl EtcTree {
maybe_subtree.unwrap_or_else(|| {
EtcTree::with_status(
new_path.to_owned(),
if components.peek().is_some() {
EtcFileStatus::Unmanaged
} else {
EtcFileStatus::Managed
},
components
.peek()
.map_or(EtcFileStatus::Managed, |_| {
EtcFileStatus::Unmanaged
}),
)
}),
components,
new_path,
))
},
name.to_owned(),
name.to_string_lossy().to_string(),
);
tree
}
path::Component::RootDir => go(
tree,
components,
path.join(path::MAIN_SEPARATOR.to_string()),
),
path::Component::RootDir => {
go(tree, components, path.join(path::MAIN_SEPARATOR_STR))
}
_ => panic!(
"Unsupported path provided! At path 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>
where
F: Fn(&Path) -> bool,
@ -237,29 +193,86 @@ impl EtcTree {
mod tests {
use super::*;
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]
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("foo2").join("baz").join("bar"))
.register_managed_entry(&PathBuf::from("/").join("foo2").join("baz2").join("bar"));
dbg!(&tree1);
dbg!(&tree);
assert_eq!(
tree1.nested.keys().sorted().collect::<Vec<_>>(),
tree.nested.keys().sorted().collect::<Vec<_>>(),
["foo", "foo2"]
);
assert_eq!(
tree1
.nested
.get(OsStr::new("foo2"))
tree.nested
.get("foo2")
.unwrap()
.nested
.get(OsStr::new("baz"))
.get("baz")
.unwrap()
.nested
.get(OsStr::new("bar"))
.get("bar")
.unwrap()
.path,
PathBuf::from("/").join("foo2").join("baz").join("bar")
@ -268,7 +281,7 @@ mod tests {
#[test]
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("foo2"))
.register_managed_entry(&PathBuf::from("/").join("foo2").join("baz"))
@ -301,10 +314,10 @@ mod tests {
);
assert!(tree2
.nested
.get(OsStr::new("foo3"))
.get("foo3")
.unwrap()
.nested
.get(OsStr::new("baz2"))
.get("baz2")
.unwrap()
.nested
.keys()
@ -319,7 +332,7 @@ mod tests {
#[test]
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("foo2"))
.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").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("foo3").join("bar"))
.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 SERVICES_STATE_FILE_NAME: &str = "services.json";
const ETC_STATE_FILE_NAME: &str = "etc-files.json";
const SYSTEM_MANAGER_STATIC_NAME: &str = ".system-manager-static";
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(from = "String", into = "String", rename_all = "camelCase")]