Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

snap: add support for kernel-gpu-2404 #20

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open

Conversation

Saviq
Copy link
Collaborator

@Saviq Saviq commented Oct 12, 2024

No description provided.

Add a component monitor that will manage content in the target path
based on the contents of a sentinel file under the source path.
Using hooks and the component monitor, ensure the content is managed in
${SNAP_DATA}/kernel-gpu-2404
@Saviq Saviq requested a review from a team as a code owner October 12, 2024 13:29
@Saviq Saviq added the no-merge Not to be merged label Oct 12, 2024
Check for sentinel existence in the second content location and run
through the wrapper within.
Since the component is likely to be more appropriate for the hardware,
make it go first.
Copy link
Member

@TheSignPainter98 TheSignPainter98 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking nice! I think this PR could use a little extra error handling, for which I provide some suggestions :)

Edit: apparently my suggestions on error handling got dropped from my review... thanks, GitHub! Anyway, my suggestion would be to use anyhow


[dependencies]
clap = { version = "4", default-features = false, features = ["derive", "env", "help", "std", "usage"] }
exitcode = { version = "1", default-features = false }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this dependency can be safely dropped, it looks like it's just being used to get exitcode::OK, which I think can be equivalently expressed using std::process::ExitCode::SUCCESS

Comment on lines +213 to +215
for _sig in signals.forever() {
std::process::exit(exitcode::OK);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use of a loop here feels slightly odd as this as signals.forever() is actually being used as a fence and the body of the loop is only expected to be run once. Perhaps something like this could be clearer?

signals.forever().next(); // Await signal.
process::exit(ExitCode::SUCCESS);

};

let sentinel_tgt = target.join(sentinel);
if sentinel_data == fs::read(&sentinel_tgt).unwrap_or_default() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use of .unwrap_or_default() feels a little unclean here as it's being used to ignore the error case and create a new string. This check also relies on the condition that valid sentinel_data != "" in order to work, and although this may always be the case, it's an extra precondition which must be kept in mind. I think instead, it could be clearer to use something like this to explicitly ignore the error and then check the content if successfully read

fs::read(&sentinel_tgt).is_ok_and(|content| content == sentinel_data)

edition = "2021"

[dependencies]
clap = { version = "4", default-features = false, features = ["derive", "env", "help", "std", "usage"] }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little surprised that the color, error-context and suggestions features are absent, is this for compatibility? Personally I'm a sucker for pretty terminal applications :D (have you also considered the wrap_help feature?) I see the rest of the dependencies deliberately don't have their default features enabled so I suspect this is all intentional

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really it's just for keeping it tight. It's not really a CLI application, just an internal service.

I dropped default_features across the board b/c I dislike things creeping up on me, is all ;)

And Innes warned me about nix being heavy, so that's probably why ;)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, this sounds really good, thanks for the clarification!

#[arg(env = "COMPONENT_SOURCE")]
source: PathBuf,
/// The sentinel file name
#[arg(env = "COMPONENT_SENTINEL", value_parser = clap::builder::NonEmptyStringValueParser::new())]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are many ways the user can get a sentinel incorrect and given that it's quite difficult to pass an empty string here, I think it's probably okay not to check this explicitly.

However, if there is a reasonable finite number of inotify events we wish to support here, we could use an enum and implicitly get better validation:

use clap::ValueEnum;

#[derive(ValueEnum)]
// #[value(rename_all = "SCREAMING_SNAKE_CASE")] // Default is kebab-case, not sure
                                                 // if this is more appropriate?
enum Sentinel {
    InAccess,
    InModify,
    ..
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sentinel here is actually a file name that we're monitoring for… I'm not reading in inotify events as arguments?

Or did I get my streams crossed here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oooooh, right, when I first read this, I thought sentinel here related to the inotify events! I think my confusion came from the use of the String type here---it looked like this specificially isn't a file given that target has type PathBuf below. I think using PathBuf for both would clean this up nicely.

For reference, String should be avoided for buffers as Rust's String and &str types both require valid UTF-8, so to drop this requrement, PathBuf is more idiomatic

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose I thought that Path* would not be appropriate for just a file name… but of course a file name is still a path…

Copy link
Member

@TheSignPainter98 TheSignPainter98 Oct 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, but the string could still contain a / making it more like a path :) I think the intention is to use this with the file system, so signalling that could be beneficial (hence Path*).

This actually makes me wonder, if sentinel is expected not to contain any path separators, rather than having separate target and sentinel parameters, could we take one and extract the name of the directory? Otherwise, I think sentinel feels more like a path to me

Err(e) => {
error!(target: "files", "{e}");
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section is quite deeply nested, I think it would be cleaner to flatten this with by assigning out of the outermost match, adding a few continues and then cleaning things up a little with if-let where we only care about the data in one branch of the match (this last change is a little more subjective than the others). How about something like this?

for entry in ... {
    let entry = match entry {
        Ok(e) => e,
        Err(err) => {
            error!(target: "files", "{err}");
            continue;
        },
    };
    
    let entry_path = entry.path();
    if entry_path.is_dir() {
        if let Err(err) = fs::remove_dir_all(&entry_path) {
            error!(..., "failed to remove ...");
            continue;
        }
        debug!(..., "removed ... recursively");
        continue;
    }

    if let Err(err) = fs::remove_file(&path) {
        error!(..., "failed to remove ...");
        continue;
    }
    debug!(..., "removed ...")
}


/// Check if the sentinel file is current, else clean the target directory and populate again,
/// sentinel file being last
fn populate(source: &PathBuf, sentinel: &String, target: &PathBuf) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The types here are slightly non-idiomatic, specifically, we're passing immutable references to the owned versions of each of these types (the owned versions have enough information to perform mutation). This is a little awkward as if, for example, we wanted to pass a string literal here---it would force that &'static str to be copied into a String before it could be used! Instead, it's better practice to use to take references to the non-owned versions of each of these, i.e. &Path and &str. The compiler will automatically coerce these &Owned types into their &Borrowed equivalents, so this change will make this signature simpler without any real cost

InotifyEvent {
mask: AddWatchFlags::IN_DELETE_SELF | AddWatchFlags::IN_MOVE_SELF,
..
} => panic!("ERROR: Monitored folder disappeared"),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like quite an easy panic to hit! Panicking is a very aggressive action and there is no guarantee that resources are nicely cleaned up (if this program were compiled with panic = "abort" set in Cargo.toml e.g. to reduce binary size). I think it would be nicer just to log an error and return here

if path == source {
continue;
}
if *(relative_path.to_str().unwrap()) == **sentinel {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be a way to get this to read relative_path == sentinel.into() and then compare them as PathBuf

match entry {
Ok(e) => {
let path = e.path();
let relative_path = path.strip_prefix(source).unwrap();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should try to avoid unwrap and prefer ? instead

Copy link
Collaborator Author

@Saviq Saviq left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks all for the comments!

Will reply for now to just the ones I have any excuse for :X

Copy link
Collaborator Author

@Saviq Saviq left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm how did these not get in…

#[arg(env = "COMPONENT_SOURCE")]
source: PathBuf,
/// The sentinel file name
#[arg(env = "COMPONENT_SENTINEL", value_parser = clap::builder::NonEmptyStringValueParser::new())]
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sentinel here is actually a file name that we're monitoring for… I'm not reading in inotify events as arguments?

Or did I get my streams crossed here?

edition = "2021"

[dependencies]
clap = { version = "4", default-features = false, features = ["derive", "env", "help", "std", "usage"] }
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really it's just for keeping it tight. It's not really a CLI application, just an internal service.

I dropped default_features across the board b/c I dislike things creeping up on me, is all ;)

And Innes warned me about nix being heavy, so that's probably why ;)

@@ -0,0 +1,238 @@
use log::{debug, error, info, warn, LevelFilter};
use nix::sys::inotify::{AddWatchFlags, InitFlags, Inotify, InotifyEvent};
use signal_hook::{consts::SIGINT, consts::SIGTERM, iterator::Signals};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just a suggestion

Suggested change
use signal_hook::{consts::SIGINT, consts::SIGTERM, iterator::Signals};
use signal_hook::{consts::{SIGINT, SIGTERM}, iterator::Signals};

info!(target: "files", "copying from {source:?} to {target:?}");
for entry in WalkDir::new(&source) {
match entry {
Ok(e) => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(from @TheSignPainter98) Instead of having the whole logic nested, we can use here something like this:

for entry in ... {
    let entry = match entry {
        Ok(e) => e,
        Err(err) => {
            error!(target: "files", "{err}");
            continue;
        },
    };

    let entry_path = entry.path();
    if entry_path.is_dir() {
        if let Err(err) = fs::remove_dir_all(&entry_path) {
            error!(..., "failed to remove ...");
            continue;
        }
        debug!(..., "removed ... recursively");
        continue;
    }

    if let Err(err) = fs::remove_file(&path) {
        error!(..., "failed to remove ...");
        continue;
    }
    debug!(..., "removed ...")
}

Then you can use the value in the code, avoiding additional nesting

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh weird, apparently my comment above did make it into the review?! Amazing, thanks for making sure this is seen anyway Nadzeya!


cleanup(target, sentinel);
info!(target: "files", "copying from {source:?} to {target:?}");
for entry in WalkDir::new(&source) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion from cargo clippy: change this to: source

Comment on lines +164 to +176
for event in events {
debug!(
target: "inotify",
"handling {0:?} event for {1:?}",
event.mask,
event.name
);
match event {
InotifyEvent {
name: Some(name),
mask,
..
} if *name == *args.sentinel => match mask {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(from @TheSignPainter98 and @sminez)
Since InotifyEvent is not an enum but a struct, using match here is unexpected. It's better to extract the values and match them separately:

let (event_name, mask) = match event { InotifyEvent { name, mask .. } => (name, mask),
    _ => panic!("..."),
};

Moreover, since it seems that you need only the fields from this struct, you can simplify it the following way:

for InotifyEvent { name, mask, .. } in events {
    ...
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
no-merge Not to be merged
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants