Skip to content

Commit

Permalink
Add --removal-debounce for "remove" FS events, with a higher default
Browse files Browse the repository at this point in the history
This should fix one pain point of mine when writing docs, running
`cargo watch -x doc` and running `penguin serve target/doc`. Cargo first
wipes the output directory clean and seem to only then start building
the docs. There is quite a bit of time between the wipe and writing all
the new files. This used to make Penguin already reload the sessions,
resulting in 404 pages. Now that "remove" events are treated as less
important, this should be fixed. I think it's generally a valid
assumption that a quick reload after only a deletion event is rarely
the intended behavior.
  • Loading branch information
LukasKalbertodt committed Nov 26, 2023
1 parent 3104171 commit 385b639
Show file tree
Hide file tree
Showing 2 changed files with 46 additions and 5 deletions.
17 changes: 16 additions & 1 deletion app/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ pub(crate) enum Command {
Reload,
}

#[derive(Debug, StructOpt)]
#[derive(Debug, Clone, StructOpt)]
pub(crate) struct ServeOptions {
/// Mount a directory on an URI path: '--mount <uri>:<path>'.
///
Expand Down Expand Up @@ -126,6 +126,21 @@ pub(crate) struct ServeOptions {
parse(try_from_str = parse_duration)
)]
pub(crate) debounce_duration: Duration,

/// The debounce duration (in ms) for when a file in a watched path was
/// removed.
///
/// Like `--debounce`, but for "remove" file system events. This is treated
/// separately as usually reloading quickly on deletion is not useful: the
/// reload would result in a 404 page. And in many situation (e.g. cargo
/// doc) the watched directory is first wiped by a build process and then
/// populated again after a while. So this default is much higher.
#[structopt(
long = "--removal-debounce",
default_value = "3000",
parse(try_from_str = parse_duration)
)]
pub(crate) removal_debounce_duration: Duration,
}

fn parse_mount(s: &str) -> Result<Mount, &'static str> {
Expand Down
34 changes: 30 additions & 4 deletions app/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::{env, ops::Deref, path::Path, thread, time::Duration};

use anyhow::{Context, Result};
use log::{debug, info, trace, LevelFilter};
use notify::Op;
use penguin::{Config, Controller, Mount, ProxyTarget, Server};

use crate::args::{Args, DEFAULT_PORT, ServeOptions};
Expand Down Expand Up @@ -44,7 +45,7 @@ pub(crate) async fn run(
};

if !watched_paths.is_empty() {
watch(controller, options.debounce_duration, &watched_paths)?;
watch(controller, options, &watched_paths)?;
}

// Nice output of what is being done
Expand Down Expand Up @@ -89,7 +90,7 @@ pub(crate) async fn run(

fn watch<'a>(
controller: Controller,
debounce_duration: Duration,
options: &ServeOptions,
paths: &[&Path],
) -> Result<()> {
use std::sync::mpsc::{channel, RecvTimeoutError};
Expand All @@ -114,13 +115,24 @@ fn watch<'a>(

// We create a new thread that will react to incoming events and trigger a
// page reload.
let options = options.clone();
thread::spawn(move || {
// Move it to the thread to avoid dropping it early.
let _watcher = watcher;
let debounce_duration_of = |event: &RawEvent| {
if event.op.as_ref().is_ok_and(|&op| op == Op::REMOVE) {
options.removal_debounce_duration
} else {
options.debounce_duration
}
};

while let Ok(event) = rx.recv() {
let mut debounce_duration = debounce_duration_of(&event);

debug!(
"Received watch-event for '{}'. Debouncing now for {:?}.",
"Received watch-event '{:?}' for '{}'. Debouncing now for {:?}.",
event.op,
pretty_path(&event),
debounce_duration,
);
Expand All @@ -129,7 +141,21 @@ fn watch<'a>(
// `debounce_duration`.
loop {
match rx.recv_timeout(debounce_duration) {
Ok(event) => trace!("Debounce interrupted by '{}'", pretty_path(&event)),
Ok(event) => {
trace!(
"Debounce interrupted by '{:?}' of '{}'",
event.op,
pretty_path(&event),
);

// We reset the waiting duration to the minimum of both
// events' durations. So if any non-remove event is
// involved, the shorter duration is used.
debounce_duration = std::cmp::min(
debounce_duration_of(&event),
debounce_duration,
);
},
Err(RecvTimeoutError::Timeout) => break,
Err(RecvTimeoutError::Disconnected) => return,
}
Expand Down

0 comments on commit 385b639

Please sign in to comment.