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

Adds a watch flag to watch the elf file and reloads the file if it changed #807

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

Conversation

JomerDev
Copy link
Contributor

@JomerDev JomerDev commented Jan 28, 2024

This PR adds a --watch_elf/-w flag that watches the elf file for changes and reloads defmt-print on file modify or create.

To make this work I had to add tokio as dependency, since stdin.read otherwise blocked until some more serial data is received at which point it is too late to make the reload (if you do you end up with broken frames for the first few messages).

I also had to add alterable_logger to defmt-decoder to allow for the global logger to be overwritten after it was already set.

I'm open to making the flag and/or the logger changes depending on a feature.

This PR would fix issue #794

In verbose mode the logger also catches some logs from the notify crate used for the file watcher. This can probably be worked around if need be

@Urhengulas
Copy link
Member

Could this not be achieved with cargo watch?

@JomerDev
Copy link
Contributor Author

In theory yes, however afaik you'd usually run defmt-print by piping data from a serial port into it and I doubt cargo watch sends any through stdin received data to the inner command

Copy link
Contributor

@jonathanpallant jonathanpallant left a comment

Choose a reason for hiding this comment

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

This generally looks OK to me, modulo a few comments.

I am a little bit worried about adding tokio to the defat-print dependency tree, but maybe someone can persuade me that this is better than running the TCP stream read in a thread with a 500ms timeout that checks some shared shutdown flag.

CHANGELOG.md Outdated
@@ -11,11 +11,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- [#803]: `CI`: Disable nightly qemu-snapshot tests
- [#789]: `defmt`: Add support for new time-related display hints
- [#783]: `defmt-decoder`: Move formatting logic to `Formatter`
- [#807]: `defmt-print`: Add watch_elf flrag to allow elf reload without restarting `defmt-print`
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
- [#807]: `defmt-print`: Add watch_elf flrag to allow elf reload without restarting `defmt-print`
- [#807]: `defmt-print`: Add `watch_elf` flag to allow ELF file reload without restarting `defmt-print`

Copy link
Member

Choose a reason for hiding this comment

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

⬆️

decoder/Cargo.toml Outdated Show resolved Hide resolved
@jonathanpallant
Copy link
Contributor

jonathanpallant commented Jun 12, 2024

I tested this on macOS, and I the file watching didn't appear to work.

I also wonder if some extra logging is useful, to let people know the ELF has been changed and reloaded. The --verbose flag does that.

The problem here appears to be the notifier gave the full path, but I passed a relative path to defmt-print, and they don't match. So it never reloaded the file.

print/Cargo.toml Outdated Show resolved Hide resolved
print/src/main.rs Outdated Show resolved Hide resolved
@@ -40,11 +47,14 @@ struct Opts {
#[arg(short = 'V', long)]
version: bool,

#[arg[short, long]]
Copy link
Member

Choose a reason for hiding this comment

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

Let's be consistent

Suggested change
#[arg[short, long]]
#[arg(short, long)]

Copy link
Member

Choose a reason for hiding this comment

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

⬆️

@Urhengulas
Copy link
Member

Maybe it actually does make sense to use async. In the end defmt-print does a lot of waiting either on stdin or tcp. But I'd like if we first port the current behaviour of defmt-print to async in a separate PR and see if that makes sense. And then we add the watch flag after.

@JomerDev Would you be willing to do that work?

@JomerDev
Copy link
Contributor Author

Maybe it actually does make sense to use async. In the end defmt-print does a lot of waiting either on stdin or tcp. But I'd like if we first port the current behaviour of defmt-print to async in a separate PR and see if that makes sense. And then we add the watch flag after.

@JomerDev Would you be willing to do that work?

That would pretty much just mean that I'll split this PR apart, keeping the file watch stuff in here and move the async stuff into a separate PR, right? I can do that, sure

@JomerDev JomerDev mentioned this pull request Jun 15, 2024
@JomerDev
Copy link
Contributor Author

Done, see #855

@Urhengulas
Copy link
Member

Please rebase on main. I will review next week.

@Urhengulas
Copy link
Member

@JomerDev ping

@JomerDev
Copy link
Contributor Author

JomerDev commented Jul 9, 2024

Sorry, I didn't have much time last week,. I'll try and get it done tomorrow

Copy link
Member

@Urhengulas Urhengulas left a comment

Choose a reason for hiding this comment

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

First round of review, still need to actually test it

Comment on lines 15 to 17
use notify::{Config, Event, RecommendedWatcher, RecursiveMode, Watcher};

use tokio::{
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
use notify::{Config, Event, RecommendedWatcher, RecursiveMode, Watcher};
use tokio::{
use notify::{Config, Event, RecommendedWatcher, RecursiveMode, Watcher};
use tokio::{

print/Cargo.toml Outdated
Comment on lines 21 to 24
log = "0.4"
tokio = { version = "1.38", features = ["full"] }
notify = "6.1.1"
futures = "0.3"
Copy link
Member

Choose a reason for hiding this comment

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

Please keep alphabetic ordering

Suggested change
log = "0.4"
tokio = { version = "1.38", features = ["full"] }
notify = "6.1.1"
futures = "0.3"
futures = "0.3"
log = "0.4"
notify = "6.1"
tokio = { version = "1.38", features = ["full"] }

Comment on lines 116 to 137
let (tx, mut rx) = tokio::sync::mpsc::channel(1);

let mut watcher = RecommendedWatcher::new(
move |res| {
futures::executor::block_on(async {
tx.send(res).await.unwrap();
})
},
Config::default(),
)?;
let mut directory_path = opts.elf.clone().unwrap();
directory_path.pop(); // We want the elf directory instead of the elf, since some editors remove and recreate the file on save which will remove the notifier
watcher.watch(directory_path.as_ref(), RecursiveMode::NonRecursive)?;

let path = opts.elf.clone().unwrap();

loop {
select! {
r = run(opts.clone(), &mut source) => r?,
_ = has_file_changed(&mut rx, &path) => ()
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Please move this to a run_and_watch function

Comment on lines 127 to 128
directory_path.pop(); // We want the elf directory instead of the elf, since some editors remove and recreate the file on save which will remove the notifier
watcher.watch(directory_path.as_ref(), RecursiveMode::NonRecursive)?;
Copy link
Member

Choose a reason for hiding this comment

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

Could it be that we get notified about some unrelated changes? What if I am running two programs in parallel? Will changing one of the make both reload?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We will, yes. But since there is no way to filter them directly in the watcher, we filter them in has_file_changed instead. There we first check that the path matches the path of the elf file, and then we check the event kind and ignore everything that isn't a create or modify of that file
If you run two programs in parallel, as long as they not use the exact same elf file (with the exact same path) they wont both reload

@@ -43,6 +43,7 @@ object = { version = "0.35", default-features = false, features = [
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1", features = ["arbitrary_precision"] }
regex = "1"
alterable_logger = "1.0.0"
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
alterable_logger = "1.0.0"
alterable_logger = "1"

print/src/main.rs Outdated Show resolved Hide resolved
print/Cargo.toml Outdated
@@ -20,3 +20,5 @@ defmt-decoder = { version = "=0.3.11", path = "../decoder", features = [
] }
log = "0.4"
tokio = { version = "1.38", features = ["full"] }
notify = "6.1.1"
Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Seeing as nobody could reliably confirm the issue and since std has been using the crossbeam implementation since 1.67 anyway I don't see a reason to do so.
See also the last comment on the issue: notify-rs/notify#380 (comment)

Comment on lines 120 to 122
futures::executor::block_on(async {
tx.send(res).await.unwrap();
})
Copy link
Member

Choose a reason for hiding this comment

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

Would tokio::runtime::Handle.current() work here? Then we could get rid of the futures dependency.

Suggested change
futures::executor::block_on(async {
tx.send(res).await.unwrap();
})
tokio::runtime::Handle::current().block_on(async {
tx.send(res).await.unwrap();
})

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's probably even easier to use tx.blocking_send instead, that way there is no need for any async context. I'll try that later today

print/src/main.rs Outdated Show resolved Hide resolved
@JomerDev JomerDev requested a review from Urhengulas July 23, 2024 10:26
@JomerDev
Copy link
Contributor Author

So, I don't know why the lint is complaining, I tried to apply it's suggestion but the rust-analyzer complains. I also don't get the lint when I run clippy locally

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants