Skip to content

Commit

Permalink
Fix WSL Hot Reload (#2721)
Browse files Browse the repository at this point in the history
* feat: poll watcher
* progress: wsl hot reload setting
* feat: wsl poll setting
  • Loading branch information
DogeDark authored Jul 29, 2024
1 parent 4e338ac commit b7127ad
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 62 deletions.
63 changes: 33 additions & 30 deletions packages/cli/src/cli/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,51 +26,47 @@ pub enum Config {
/// Create a custom html file.
CustomHtml {},

/// Set global cli settings.
SetGlobal { setting: Setting, value: Value },
/// Set CLI settings.
#[command(subcommand)]
Set(Setting),
}

#[derive(Debug, Clone, Copy, Deserialize, clap::ValueEnum)]
#[derive(Debug, Clone, Copy, Deserialize, Subcommand)]
pub enum Setting {
/// Set the value of the always-hot-reload setting.
AlwaysHotReload,
AlwaysHotReload { value: BoolValue },
/// Set the value of the always-open-browser setting.
AlwaysOpenBrowser,
AlwaysOpenBrowser { value: BoolValue },
/// Set the value of the always-on-top desktop setting.
AlwaysOnTop,
AlwaysOnTop { value: BoolValue },
/// Set the interval that file changes are polled on WSL for hot reloading.
WSLFilePollInterval { value: u16 },
}

impl Display for Setting {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::AlwaysHotReload => write!(f, "always_hot_reload"),
Self::AlwaysOpenBrowser => write!(f, "always_open_browser"),
Self::AlwaysOnTop => write!(f, "always_on_top"),
Self::AlwaysHotReload { value: _ } => write!(f, "always-hot-reload"),
Self::AlwaysOpenBrowser { value: _ } => write!(f, "always-open-browser"),
Self::AlwaysOnTop { value: _ } => write!(f, "always-on-top"),
Self::WSLFilePollInterval { value: _ } => write!(f, "wsl-file-poll-interval"),
}
}
}

// NOTE: Unsure of an alternative to get the desired behavior with clap, if it exists.
// Clap complains if we use a bool directly and I can't find much info about it.
// "Argument 'value` is positional and it must take a value but action is SetTrue"
#[derive(Debug, Clone, Copy, Deserialize, clap::ValueEnum)]
pub enum Value {
pub enum BoolValue {
True,
False,
}

impl Display for Value {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::True => write!(f, "true"),
Self::False => write!(f, "false"),
}
}
}

impl From<Value> for bool {
fn from(value: Value) -> Self {
impl From<BoolValue> for bool {
fn from(value: BoolValue) -> Self {
match value {
Value::True => true,
Value::False => false,
BoolValue::True => true,
BoolValue::False => false,
}
}
}
Expand Down Expand Up @@ -111,14 +107,21 @@ impl Config {
file.write_all(content.as_bytes())?;
tracing::info!("🚩 Create custom html file done.");
}
// Handle configuration of global CLI settings.
Config::SetGlobal { setting, value } => {
// Handle CLI settings.
Config::Set(setting) => {
CliSettings::modify_settings(|settings| match setting {
Setting::AlwaysHotReload => settings.always_hot_reload = Some(value.into()),
Setting::AlwaysOpenBrowser => settings.always_open_browser = Some(value.into()),
Setting::AlwaysOnTop => settings.always_on_top = Some(value.into()),
Setting::AlwaysOnTop { value } => settings.always_on_top = Some(value.into()),
Setting::AlwaysHotReload { value } => {
settings.always_hot_reload = Some(value.into())
}
Setting::AlwaysOpenBrowser { value } => {
settings.always_open_browser = Some(value.into())
}
Setting::WSLFilePollInterval { value } => {
settings.wsl_file_poll_interval = Some(value)
}
})?;
tracing::info!("🚩 CLI setting `{setting}` has been set to `{value}`")
tracing::info!("🚩 CLI setting `{setting}` has been set.");
}
}
Ok(())
Expand Down
17 changes: 16 additions & 1 deletion packages/cli/src/cli/serve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ pub struct ServeArguments {
/// Additional arguments to pass to the executable
#[clap(long)]
pub args: Vec<String>,

/// Sets the interval in seconds that the CLI will poll for file changes on WSL.
#[clap(long, default_missing_value = "2")]
pub wsl_file_poll_interval: Option<u16>,
}

/// Run the WASM project on dev-server
Expand All @@ -59,15 +63,26 @@ pub struct Serve {
impl Serve {
/// Resolve the serve arguments from the arguments or the config
fn resolve(&mut self, crate_config: &mut DioxusCrate) -> Result<()> {
// Set config settings
// Set config settings.
let settings = settings::CliSettings::load();

// Enable hot reload.
if self.server_arguments.hot_reload.is_none() {
self.server_arguments.hot_reload = Some(settings.always_hot_reload.unwrap_or(true));
}

// Open browser.
if self.server_arguments.open.is_none() {
self.server_arguments.open = Some(settings.always_open_browser.unwrap_or_default());
}

// Set WSL file poll interval.
if self.server_arguments.wsl_file_poll_interval.is_none() {
self.server_arguments.wsl_file_poll_interval =
Some(settings.wsl_file_poll_interval.unwrap_or(2));
}

// Set always-on-top for desktop.
if self.server_arguments.always_on_top.is_none() {
self.server_arguments.always_on_top = Some(settings.always_on_top.unwrap_or(true))
}
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/serve/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ pub async fn serve_all(serve: Serve, dioxus_crate: DioxusCrate) -> Result<()> {
builder.build();

let mut server = Server::start(&serve, &dioxus_crate);
let mut watcher = Watcher::start(&dioxus_crate);
let mut watcher = Watcher::start(&serve, &dioxus_crate);
let mut screen = Output::start(&serve).expect("Failed to open terminal logger");

loop {
Expand Down
118 changes: 89 additions & 29 deletions packages/cli/src/serve/watcher.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
use std::path::PathBuf;
use std::{fs, path::PathBuf, time::Duration};

use crate::dioxus_crate::DioxusCrate;
use crate::serve::hot_reloading_file_map::FileMap;
use crate::{cli::serve::Serve, dioxus_crate::DioxusCrate};
use dioxus_hot_reload::HotReloadMsg;
use dioxus_html::HtmlCtx;
use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender};
use futures_util::StreamExt;
use ignore::gitignore::Gitignore;
use notify::{event::ModifyKind, EventKind, RecommendedWatcher};
use notify::{
event::{MetadataKind, ModifyKind},
Config, EventKind,
};

/// This struct stores the file watcher and the filemap for the project.
///
Expand All @@ -17,14 +20,14 @@ pub struct Watcher {
_tx: UnboundedSender<notify::Event>,
rx: UnboundedReceiver<notify::Event>,
_last_update_time: i64,
_watcher: RecommendedWatcher,
_watcher: Box<dyn notify::Watcher>,
queued_events: Vec<notify::Event>,
file_map: FileMap,
ignore: Gitignore,
}

impl Watcher {
pub fn start(config: &DioxusCrate) -> Self {
pub fn start(serve: &Serve, config: &DioxusCrate) -> Self {
let (tx, rx) = futures_channel::mpsc::unbounded();

// Extend the watch path to include:
Expand Down Expand Up @@ -61,27 +64,41 @@ impl Watcher {
}
let ignore = builder.build().unwrap();

// Create the file watcher
let mut watcher = notify::recommended_watcher({
// Build the event handler for notify.
let notify_event_handler = {
let tx = tx.clone();
move |info: notify::Result<notify::Event>| {
if let Ok(e) = info {
match e.kind {

// An event emitted when the metadata of a file or folder is changed.
EventKind::Modify(ModifyKind::Data(_) | ModifyKind::Any) |
EventKind::Create(_) |
EventKind::Remove(_) => {
_ = tx.unbounded_send(e);
},
_ => {}
if is_allowed_notify_event(&e) {
_ = tx.unbounded_send(e);
}


}
}
})
.expect("Failed to create file watcher.\nEnsure you have the required permissions to watch the specified directories.");
};

// If we are in WSL, we must use Notify's poll watcher due to an event propagation issue.
let is_wsl = is_wsl();
const NOTIFY_ERROR_MSG: &str = "Failed to create file watcher.\nEnsure you have the required permissions to watch the specified directories.";

// Create the file watcher.
let mut watcher: Box<dyn notify::Watcher> = match is_wsl {
true => {
let poll_interval = Duration::from_secs(
serve.server_arguments.wsl_file_poll_interval.unwrap_or(2) as u64,
);

Box::new(
notify::PollWatcher::new(
notify_event_handler,
Config::default().with_poll_interval(poll_interval),
)
.expect(NOTIFY_ERROR_MSG),
)
}
false => {
Box::new(notify::recommended_watcher(notify_event_handler).expect(NOTIFY_ERROR_MSG))
}
};

// Watch the specified paths
// todo: make sure we don't double-watch paths if they're nested
Expand All @@ -95,7 +112,6 @@ impl Watcher {

let mode = notify::RecursiveMode::Recursive;

use notify::Watcher;
if let Err(err) = watcher.watch(path, mode) {
tracing::warn!("Failed to watch path: {}", err);
}
Expand Down Expand Up @@ -144,14 +160,9 @@ impl Watcher {

// Decompose the events into a list of all the files that have changed
for event in self.queued_events.drain(..) {
// We only care about modify/crate/delete events
match event.kind {
EventKind::Modify(ModifyKind::Any) => {}
EventKind::Modify(ModifyKind::Data(_)) => {}
EventKind::Modify(ModifyKind::Name(_)) => {}
EventKind::Create(_) => {}
EventKind::Remove(_) => {}
_ => continue,
// We only care about certain events.
if !is_allowed_notify_event(&event) {
continue;
}

for path in event.paths {
Expand Down Expand Up @@ -276,6 +287,55 @@ fn is_backup_file(path: PathBuf) -> bool {
false
}

/// Tests if the provided [`notify::Event`] is something we listen to so we can avoid unescessary hot reloads.
fn is_allowed_notify_event(event: &notify::Event) -> bool {
match event.kind {
EventKind::Modify(ModifyKind::Data(_)) => true,
EventKind::Modify(ModifyKind::Name(_)) => true,
EventKind::Create(_) => true,
EventKind::Remove(_) => true,
// The primary modification event on WSL's poll watcher.
EventKind::Modify(ModifyKind::Metadata(MetadataKind::WriteTime)) => true,
// Catch-all for unknown event types.
EventKind::Modify(ModifyKind::Any) => true,
// Don't care about anything else.
_ => false,
}
}

const WSL_1: &str = "/proc/sys/kernel/osrelease";
const WSL_2: &str = "/proc/version";
const WSL_KEYWORDS: [&str; 2] = ["microsoft", "wsl"];

/// Detects if `dx` is being ran in a WSL environment.
///
/// We determine this based on whether the keyword `microsoft` or `wsl` is contained within the [`WSL_1`] or [`WSL_2`] files.
/// This may fail in the future as it isn't guaranteed by Microsoft.
/// See https://github.com/microsoft/WSL/issues/423#issuecomment-221627364
fn is_wsl() -> bool {
// Test 1st File
if let Ok(content) = fs::read_to_string(WSL_1) {
let lowercase = content.to_lowercase();
for keyword in WSL_KEYWORDS {
if lowercase.contains(keyword) {
return true;
}
}
}

// Test 2nd File
if let Ok(content) = fs::read_to_string(WSL_2) {
let lowercase = content.to_lowercase();
for keyword in WSL_KEYWORDS {
if lowercase.contains(keyword) {
return true;
}
}
}

false
}

#[test]
fn test_is_backup_file() {
assert!(is_backup_file(PathBuf::from("examples/test.rs~")));
Expand Down
21 changes: 20 additions & 1 deletion packages/cli/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ pub struct CliSettings {
pub always_open_browser: Option<bool>,
/// Describes whether desktop apps in development will be pinned always-on-top.
pub always_on_top: Option<bool>,
/// Describes the interval in seconds that the CLI should poll for file changes on WSL.
#[serde(default = "default_wsl_file_poll_interval")]
pub wsl_file_poll_interval: Option<u16>,
}

impl CliSettings {
Expand Down Expand Up @@ -74,7 +77,19 @@ impl CliSettings {
CrateConfigError::Io(Error::new(ErrorKind::Other, e.to_string()))
})?;

let result = fs::write(path.clone(), data.clone());
// Create the directory structure if it doesn't exist.
let parent_path = path.parent().unwrap();
if let Err(e) = fs::create_dir_all(parent_path) {
error!(
?data,
?path,
"failed to create directories for settings file"
);
return Err(CrateConfigError::Io(e));
}

// Write the data.
let result = fs::write(&path, data.clone());
if let Err(e) = result {
error!(?data, ?path, "failed to save global cli settings");
return Err(CrateConfigError::Io(e));
Expand Down Expand Up @@ -102,3 +117,7 @@ impl CliSettings {
Ok(())
}
}

fn default_wsl_file_poll_interval() -> Option<u16> {
Some(2)
}

0 comments on commit b7127ad

Please sign in to comment.