diff --git a/crates/fnmatch/src/lib.rs b/crates/fnmatch/src/lib.rs index 8f7d66dc8..2bedc1073 100644 --- a/crates/fnmatch/src/lib.rs +++ b/crates/fnmatch/src/lib.rs @@ -27,6 +27,9 @@ enum Fragment { MatchOne, /// `*` + MatchAnyExceptForwardSlash, + + /// `**` MatchAny, /// `\` @@ -79,6 +82,10 @@ impl<'a> StringWalker<'a> { self.index += much; } + pub fn peek(&self) -> Option { + self.data.get(self.index..self.index + 1).and_then(|s| s.chars().nth(0)) + } + /// Find next occurrence of the character, and substring up to it pub fn substring_to(&self, c: char) -> Option<&'a str> { // Clone ourselves and search that iterator. @@ -161,7 +168,11 @@ fn fragments_from_string(s: &str) -> Result, Error> { while let Some(ch) = walker.next() { let next_token = match ch { '?' => Some(Fragment::MatchOne), - '*' => Some(Fragment::MatchAny), + '*' if walker.peek() == Some('*') => { + walker.eat(1); + Some(Fragment::MatchAny) + } + '*' => Some(Fragment::MatchAnyExceptForwardSlash), '\\' => Some(Fragment::BackSlash), '/' => Some(Fragment::ForwardSlash), '.' => Some(Fragment::Dot), @@ -211,7 +222,8 @@ fn fragment_to_regex_str(fragment: &Fragment) -> (String, Vec) { let mut groups = vec![]; let string = match fragment { Fragment::MatchOne => ".".into(), - Fragment::MatchAny => "[^\\/]*".into(), + Fragment::MatchAnyExceptForwardSlash => "[^\\/]*".into(), + Fragment::MatchAny => ".*".into(), Fragment::BackSlash => "\\".into(), Fragment::ForwardSlash => "\\/".into(), Fragment::Dot => "\\.".into(), @@ -311,4 +323,10 @@ pub mod path_tests { let wide = k.match_path("/usr/lib/modules/6.6.67-51.kvm/kernel/net/netfilter/nft_hash.ko.zst"); assert!(wide.is_none()); } + + #[test] + fn test_match_any_regex() { + let pattern = "/usr/share/fonts/**/*.ttf".parse::().unwrap(); + assert_eq!(pattern.regex.as_str(), r#"^\/usr\/share\/fonts\/.*\/[^\/]*\.ttf$"#); + } } diff --git a/crates/triggers/src/lib.rs b/crates/triggers/src/lib.rs index 1aa563582..351ccc833 100644 --- a/crates/triggers/src/lib.rs +++ b/crates/triggers/src/lib.rs @@ -12,6 +12,7 @@ use thiserror::Error; pub mod format; /// Grouped management of a set of triggers +#[derive(Debug)] pub struct Collection<'a> { handlers: Vec, triggers: BTreeMap, @@ -62,11 +63,13 @@ impl<'a> Collection<'a> { } /// Process a batch set of paths and record the "hit" - pub fn process_paths(&mut self, paths: impl Iterator) { + pub fn process_paths>(&mut self, paths: impl Iterator) { let results = paths.into_iter().flat_map(|p| { - self.handlers - .iter() - .filter_map(move |h| h.pattern.match_path(&p).map(|m| (h.id.clone(), h.handler.compiled(&m)))) + self.handlers.iter().filter_map(move |h| { + h.pattern + .match_path(p.as_ref()) + .map(|m| (h.id.clone(), h.handler.compiled(&m))) + }) }); for (id, handler) in results { diff --git a/moss/src/client/mod.rs b/moss/src/client/mod.rs index 845837a7c..1fda477e8 100644 --- a/moss/src/client/mod.rs +++ b/moss/src/client/mod.rs @@ -259,11 +259,12 @@ impl Client { let new = self.state_db.get(id).map_err(|_| Error::StateDoesntExist(id))?; // Get old (current) state - let Some(old) = self.installation.active_state else { + let Some(old_id) = self.installation.active_state else { return Err(Error::NoActiveState); }; + let old = self.state_db.get(old_id).map_err(|_| Error::StateDoesntExist(old_id))?; - if new.id == old { + if new.id == old.id { return Err(Error::StateAlreadyActive(id)); } @@ -281,23 +282,29 @@ impl Client { self.promote_staging()?; // Archive old state - self.archive_state(old)?; - - // Build VFS from new state selections - // to build triggers from - let fstree = self.vfs(new.selections.iter().map(|selection| &selection.package))?; + self.archive_state(old.id)?; if skip_triggers { - return Ok(old); + return Ok(old.id); } + // Build VFS from new state selections + // to build triggers from + let old_vfs = self.vfs(old.selections.iter().map(|selection| &selection.package))?; + let new_vfs = self.vfs(new.selections.iter().map(|selection| &selection.package))?; + // Run system triggers - let sys_triggers = postblit::triggers(TriggerScope::System(&self.installation, &self.scope), &fstree)?; + let sys_triggers = postblit::triggers( + TriggerScope::System(&self.installation, &self.scope), + Some(&old_vfs), + &new_vfs, + )?; + for trigger in sys_triggers { trigger.execute()?; } - Ok(old) + Ok(old.id) } /// Create a new recorded state from the provided packages @@ -336,21 +343,25 @@ impl Client { event_type = "progress_start", ); - let old_state = self.installation.active_state; + let old_state = self + .installation + .active_state + .map(|id| self.state_db.get(id).map_err(|_| Error::StateDoesntExist(id))) + .transpose()?; - let fstree = self.blit_root(selections.iter().map(|s| &s.package))?; + let new_vfs = self.blit_root(selections.iter().map(|s| &s.package))?; let result = match &self.scope { Scope::Stateful => { // Add to db let state = self.state_db.add(selections, Some(&summary.to_string()), None)?; - self.apply_stateful_blit(fstree, &state, old_state, system_model)?; + self.apply_stateful_blit(new_vfs, &state, old_state, system_model)?; Ok(Some(state)) } Scope::Ephemeral { blit_root } => { - self.apply_ephemeral_blit(fstree, blit_root, system_model)?; + self.apply_ephemeral_blit(new_vfs, blit_root, system_model)?; Ok(None) } @@ -367,8 +378,12 @@ impl Client { } /// Apply all triggers with the given scope, wrapping with a progressbar. - fn apply_triggers(scope: TriggerScope<'_>, fstree: &vfs::Tree) -> Result<(), postblit::Error> { - let triggers = postblit::triggers(scope, fstree)?; + fn apply_triggers( + scope: TriggerScope<'_>, + prev_vfs: Option<&vfs::Tree>, + curr_vfs: &vfs::Tree, + ) -> Result<(), postblit::Error> { + let triggers = postblit::triggers(scope, prev_vfs, curr_vfs)?; let progress = ProgressBar::new(triggers.len() as u64).with_style( ProgressStyle::with_template("\n|{bar:20.green/blue}| {pos}/{len} {msg}") @@ -428,9 +443,9 @@ impl Client { pub fn apply_stateful_blit( &self, - fstree: vfs::Tree, + new_vfs: vfs::Tree, state: &State, - old_state: Option, + old_state: Option, system_model: SystemModel, ) -> Result<(), Error> { record_state_id(&self.installation.staging_dir(), state.id)?; @@ -438,7 +453,17 @@ impl Client { record_system_model(&self.installation.staging_dir(), system_model)?; create_root_links(&self.installation.isolation_dir())?; - Self::apply_triggers(TriggerScope::Transaction(&self.installation, &self.scope), &fstree)?; + + let prev_vfs = old_state + .as_ref() + .map(|old| self.vfs(old.selections.iter().map(|selection| &selection.package))) + .transpose()?; + + Self::apply_triggers( + TriggerScope::Transaction(&self.installation, &self.scope), + prev_vfs.as_ref(), + &new_vfs, + )?; // Staging is only used with [`Scope::Stateful`] self.promote_staging()?; @@ -446,12 +471,16 @@ impl Client { // Now we got it staged, we need working rootfs create_root_links(&self.installation.root)?; - if let Some(id) = old_state { - self.archive_state(id)?; + if let Some(old_state) = old_state { + self.archive_state(old_state.id)?; } // At this point we're allowed to run system triggers - Self::apply_triggers(TriggerScope::System(&self.installation, &self.scope), &fstree)?; + Self::apply_triggers( + TriggerScope::System(&self.installation, &self.scope), + prev_vfs.as_ref(), + &new_vfs, + )?; boot::synchronize(self, state)?; @@ -460,7 +489,7 @@ impl Client { pub fn apply_ephemeral_blit( &self, - fstree: vfs::Tree, + new_vfs: vfs::Tree, blit_root: &Path, system_model: SystemModel, ) -> Result<(), Error> { @@ -474,9 +503,13 @@ impl Client { fs::create_dir_all(etc)?; // ephemeral tx triggers - Self::apply_triggers(TriggerScope::Transaction(&self.installation, &self.scope), &fstree)?; + Self::apply_triggers( + TriggerScope::Transaction(&self.installation, &self.scope), + None, + &new_vfs, + )?; // ephemeral system triggers - Self::apply_triggers(TriggerScope::System(&self.installation, &self.scope), &fstree)?; + Self::apply_triggers(TriggerScope::System(&self.installation, &self.scope), None, &new_vfs)?; Ok(()) } diff --git a/moss/src/client/postblit.rs b/moss/src/client/postblit.rs index 48dd4565f..e17fc9e48 100644 --- a/moss/src/client/postblit.rs +++ b/moss/src/client/postblit.rs @@ -9,6 +9,7 @@ //! Note that currently we only load from `/usr/share/moss/triggers/{tx,sys.d}/*.yaml` //! and do not yet support local triggers use std::{ + collections::HashSet, path::{Path, PathBuf}, process, }; @@ -20,6 +21,7 @@ use serde::Deserialize; use thiserror::Error; use tracing::{error, warn}; use triggers::format::{CompiledHandler, Handler, Trigger}; +use vfs::tree::BlitFile; use super::PendingFile; @@ -106,15 +108,15 @@ pub(super) struct TriggerRunner<'a> { trigger: CompiledHandler, } -/// Load all triggers matching the given scope and staging filesystem +/// Load all triggers matching the given scope and the difference +/// between the previous [`vfs::Tree`] and the current [`vfs::Tree`]. /// -/// # Arguments -/// -/// * `scope` - Trigger execution scope -/// * `fstree` - Virtual filesystem tree populated with records of the staging filesystem +/// Triggers are only run where a matching path has been added or +/// removed in the `curr_vfs` compared to the `prev_vfs` pub(super) fn triggers<'a>( scope: TriggerScope<'a>, - fstree: &vfs::tree::Tree, + prev_vfs: Option<&vfs::tree::Tree>, + curr_vfs: &vfs::tree::Tree, ) -> Result>, Error> { // Pre-calculate trigger root path once let trigger_root = { @@ -142,14 +144,25 @@ pub(super) fn triggers<'a>( .collect_vec(), }; - // Load trigger collection, process all the paths, convert to scoped TriggerRunner vec + // Get full set of paths that have changed (added or removed) between + // previous and current vfs + let prev_paths = prev_vfs + .map(|tree| tree.iter().map(|p| p.path()).collect::>()) + .unwrap_or_default(); + let curr_paths = curr_vfs.iter().map(|p| p.path()).collect::>(); + let diff_paths = curr_paths.symmetric_difference(&prev_paths); + + // Load trigger collection, process all the paths that have changed from previous state let mut collection = triggers::Collection::new(triggers.iter())?; - collection.process_paths(fstree.iter().map(|m| m.to_string())); + collection.process_paths(diff_paths); + + // Convert to scoped TriggerRunner vec let computed_commands = collection .bake()? .into_iter() .map(|trigger| TriggerRunner { scope, trigger }) .collect_vec(); + Ok(computed_commands) }