-
Notifications
You must be signed in to change notification settings - Fork 4
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
base: main
Are you sure you want to change the base?
Conversation
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
Check for sentinel existence in the second content location and run through the wrapper within.
7155cbf
to
e35f73c
Compare
Since the component is likely to be more appropriate for the hardware, make it go first.
There was a problem hiding this 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 } |
There was a problem hiding this comment.
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
for _sig in signals.forever() { | ||
std::process::exit(exitcode::OK); | ||
} |
There was a problem hiding this comment.
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() { |
There was a problem hiding this comment.
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"] } |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 ;)
There was a problem hiding this comment.
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())] |
There was a problem hiding this comment.
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,
..
}
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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…
There was a problem hiding this comment.
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}"); | ||
} | ||
} |
There was a problem hiding this comment.
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 continue
s 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) { |
There was a problem hiding this comment.
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"), |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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
There was a problem hiding this 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
There was a problem hiding this 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())] |
There was a problem hiding this comment.
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"] } |
There was a problem hiding this comment.
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}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just a suggestion
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) => { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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
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 { |
There was a problem hiding this comment.
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 {
...
}
No description provided.