Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions crates/fnmatch/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ enum Fragment {
MatchOne,

/// `*`
MatchAnyExceptForwardSlash,

/// `**`
MatchAny,

/// `\`
Expand Down Expand Up @@ -79,6 +82,10 @@ impl<'a> StringWalker<'a> {
self.index += much;
}

pub fn peek(&self) -> Option<char> {
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.
Expand Down Expand Up @@ -161,7 +168,11 @@ fn fragments_from_string(s: &str) -> Result<Vec<Fragment>, 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),
Expand Down Expand Up @@ -211,7 +222,8 @@ fn fragment_to_regex_str(fragment: &Fragment) -> (String, Vec<String>) {
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(),
Expand Down Expand Up @@ -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::<Pattern>().unwrap();
assert_eq!(pattern.regex.as_str(), r#"^\/usr\/share\/fonts\/.*\/[^\/]*\.ttf$"#);
}
}
11 changes: 7 additions & 4 deletions crates/triggers/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ExtractedHandler>,
triggers: BTreeMap<String, &'a Trigger>,
Expand Down Expand Up @@ -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<Item = String>) {
pub fn process_paths<T: AsRef<str>>(&mut self, paths: impl Iterator<Item = T>) {
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 {
Expand Down
83 changes: 58 additions & 25 deletions moss/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

Expand All @@ -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
Expand Down Expand Up @@ -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)
}
Expand All @@ -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<PendingFile>) -> Result<(), postblit::Error> {
let triggers = postblit::triggers(scope, fstree)?;
fn apply_triggers(
scope: TriggerScope<'_>,
prev_vfs: Option<&vfs::Tree<PendingFile>>,
curr_vfs: &vfs::Tree<PendingFile>,
) -> 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}")
Expand Down Expand Up @@ -428,30 +443,44 @@ impl Client {

pub fn apply_stateful_blit(
&self,
fstree: vfs::Tree<PendingFile>,
new_vfs: vfs::Tree<PendingFile>,
state: &State,
old_state: Option<state::Id>,
old_state: Option<State>,
system_model: SystemModel,
) -> Result<(), Error> {
record_state_id(&self.installation.staging_dir(), state.id)?;
record_os_release(&self.installation.staging_dir())?;
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()?;

// 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)?;

Expand All @@ -460,7 +489,7 @@ impl Client {

pub fn apply_ephemeral_blit(
&self,
fstree: vfs::Tree<PendingFile>,
new_vfs: vfs::Tree<PendingFile>,
blit_root: &Path,
system_model: SystemModel,
) -> Result<(), Error> {
Expand All @@ -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(())
}
Expand Down
29 changes: 21 additions & 8 deletions moss/src/client/postblit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand All @@ -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;

Expand Down Expand Up @@ -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<PendingFile>,
prev_vfs: Option<&vfs::tree::Tree<PendingFile>>,
curr_vfs: &vfs::tree::Tree<PendingFile>,
) -> Result<Vec<TriggerRunner<'a>>, Error> {
// Pre-calculate trigger root path once
let trigger_root = {
Expand Down Expand Up @@ -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::<HashSet<_>>())
.unwrap_or_default();
let curr_paths = curr_vfs.iter().map(|p| p.path()).collect::<HashSet<_>>();
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)
}

Expand Down
Loading