From b3d119b8150d862d22aeb1149f7f43780746fd8e Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 13 Jan 2023 16:13:07 -0600 Subject: [PATCH] Provide option to do full rebuilds when nessisary --- packages/desktop/src/desktop_context.rs | 4 +- packages/desktop/src/lib.rs | 17 +- packages/hot-reload/Cargo.toml | 3 + packages/hot-reload/src/lib.rs | 251 +++++++++++++++++------- packages/liveview/src/pool.rs | 13 +- packages/tui/src/lib.rs | 24 ++- 6 files changed, 217 insertions(+), 95 deletions(-) diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 720223e6c..baa658b28 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -8,8 +8,8 @@ use crate::events::IpcMessage; use crate::Config; use crate::WebviewHandler; use dioxus_core::ScopeState; -use dioxus_core::Template; use dioxus_core::VirtualDom; +use dioxus_hot_reload::HotReloadMsg; use serde_json::Value; use wry::application::event_loop::EventLoopProxy; use wry::application::event_loop::EventLoopWindowTarget; @@ -263,7 +263,7 @@ pub enum EventData { Ipc(IpcMessage), - TemplateUpdated(Template<'static>), + HotReloadEvent(HotReloadMsg), NewWindow, diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 0ec1f922c..9e1bf79db 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -112,7 +112,7 @@ pub fn launch_with_props(root: Component

, props: P, cfg: Config) let proxy = proxy.clone(); dioxus_hot_reload::connect(move |template| { let _ = proxy.send_event(UserWindowEvent( - EventData::TemplateUpdated(template), + EventData::HotReloadEvent(template), unsafe { WindowId::dummy() }, )); }); @@ -177,13 +177,18 @@ pub fn launch_with_props(root: Component

, props: P, cfg: Config) } Event::UserEvent(event) => match event.0 { - EventData::TemplateUpdated(template) => { - for webview in webviews.values_mut() { - webview.dom.replace_template(template); + EventData::HotReloadEvent(msg) => match msg { + dioxus_hot_reload::HotReloadMsg::UpdateTemplate(template) => { + for webview in webviews.values_mut() { + webview.dom.replace_template(template); - poll_vdom(webview); + poll_vdom(webview); + } } - } + dioxus_hot_reload::HotReloadMsg::Shutdown => { + *control_flow = ControlFlow::Exit; + } + }, EventData::CloseWindow => { webviews.remove(&event.1); diff --git a/packages/hot-reload/Cargo.toml b/packages/hot-reload/Cargo.toml index d6088a109..7dda13a5d 100644 --- a/packages/hot-reload/Cargo.toml +++ b/packages/hot-reload/Cargo.toml @@ -14,3 +14,6 @@ interprocess = { version = "1.2.1" } notify = "5.0.0" chrono = "0.4.23" serde_json = "1.0.91" +serde = { version = "1", features = ["derive"] } +execute = "0.2.11" +once_cell = "1.17.0" \ No newline at end of file diff --git a/packages/hot-reload/src/lib.rs b/packages/hot-reload/src/lib.rs index 9543911b2..face5edc1 100644 --- a/packages/hot-reload/src/lib.rs +++ b/packages/hot-reload/src/lib.rs @@ -15,42 +15,141 @@ use notify::{RecommendedWatcher, RecursiveMode, Watcher}; #[cfg(debug_assertions)] pub use dioxus_html::HtmlCtx; +use serde::{Deserialize, Serialize}; -/// Initialize the hot reloading listener on the given path -pub fn init( +/// A message the hot reloading server sends to the client +#[derive(Debug, Serialize, Deserialize, Clone, Copy)] +pub enum HotReloadMsg { + /// A template has been updated + #[serde(borrow = "'static")] + UpdateTemplate(Template<'static>), + /// The program needs to be recompiled, and the client should shut down + Shutdown, +} + +#[derive(Debug, Clone, Copy)] +pub struct Config { root_path: &'static str, listening_paths: &'static [&'static str], + excluded_paths: &'static [&'static str], log: bool, -) { + rebuild_with: Option<&'static str>, + phantom: std::marker::PhantomData, +} + +impl Default for Config { + fn default() -> Self { + Self { + root_path: "", + listening_paths: &[""], + excluded_paths: &["./target"], + log: true, + rebuild_with: None, + phantom: std::marker::PhantomData, + } + } +} + +impl Config { + pub const fn new() -> Self { + Self { + root_path: "", + listening_paths: &[""], + excluded_paths: &["./target"], + log: true, + rebuild_with: None, + phantom: std::marker::PhantomData, + } + } +} + +impl Config { + pub const fn root(self, path: &'static str) -> Self { + Self { + root_path: path, + ..self + } + } + + pub const fn listening_paths(self, paths: &'static [&'static str]) -> Self { + Self { + listening_paths: paths, + ..self + } + } + + pub const fn log(self, log: bool) -> Self { + Self { log, ..self } + } + + pub const fn rebuild_with(self, rebuild_with: &'static str) -> Self { + Self { + rebuild_with: Some(rebuild_with), + ..self + } + } + + pub const fn excluded_paths(self, paths: &'static [&'static str]) -> Self { + Self { + excluded_paths: paths, + ..self + } + } +} + +/// Initialize the hot reloading listener +pub fn init(cfg: Config) { + let Config { + root_path, + listening_paths, + log, + rebuild_with, + excluded_paths, + phantom: _, + } = cfg; + if let Ok(crate_dir) = PathBuf::from_str(root_path) { let temp_file = std::env::temp_dir().join("@dioxusin"); let channels = Arc::new(Mutex::new(Vec::new())); let file_map = Arc::new(Mutex::new(FileMap::::new(crate_dir.clone()))); if let Ok(local_socket_stream) = LocalSocketListener::bind(temp_file.as_path()) { + let aborted = Arc::new(Mutex::new(false)); + // listen for connections std::thread::spawn({ let file_map = file_map.clone(); let channels = channels.clone(); + let aborted = aborted.clone(); + let _ = local_socket_stream.set_nonblocking(true); move || { - for mut connection in local_socket_stream.incoming().flatten() { - // send any templates than have changed before the socket connected - let templates: Vec<_> = { - file_map - .lock() - .unwrap() - .map - .values() - .filter_map(|(_, template_slot)| *template_slot) - .collect() - }; - for template in templates { - if !send_template(template, &mut connection) { - continue; + loop { + if let Ok(mut connection) = local_socket_stream.accept() { + // send any templates than have changed before the socket connected + let templates: Vec<_> = { + file_map + .lock() + .unwrap() + .map + .values() + .filter_map(|(_, template_slot)| *template_slot) + .collect() + }; + for template in templates { + if !send_msg( + HotReloadMsg::UpdateTemplate(template), + &mut connection, + ) { + continue; + } + } + channels.lock().unwrap().push(connection); + if log { + println!("Connected to hot reloading 🚀"); } } - channels.lock().unwrap().push(connection); - if log { - println!("Connected to hot reloading 🚀"); + std::thread::sleep(std::time::Duration::from_millis(10)); + if aborted.lock().unwrap().clone() { + break; } } } @@ -65,37 +164,42 @@ pub fn init( let mut watcher = RecommendedWatcher::new(tx, notify::Config::default()).unwrap(); for path in listening_paths { - match PathBuf::from_str(path) { - Ok(path) => { - let full_path = crate_dir.join(path); - if let Err(err) = watcher.watch(&full_path, RecursiveMode::Recursive) { - if log { - println!( - "hot reloading failed to start watching {full_path:?}:\n{err:?}", - ); - } - } - } - Err(err) => { - if log { - println!("hot reloading failed to create path:\n{:?}", err); - } + let full_path = crate_dir.join(path); + if let Err(err) = watcher.watch(&full_path, RecursiveMode::Recursive) { + if log { + println!( + "hot reloading failed to start watching {full_path:?}:\n{err:?}", + ); } } } + let excluded_paths = excluded_paths + .iter() + .map(|path| crate_dir.join(PathBuf::from(path))) + .collect::>(); + for evt in rx { - // Give time for the change to take effect before reading the file - std::thread::sleep(std::time::Duration::from_millis(100)); if chrono::Local::now().timestamp() > last_update_time { if let Ok(evt) = evt { - let mut channels = channels.lock().unwrap(); - for path in &evt.paths { - // skip non rust files - if path.extension().and_then(|p| p.to_str()) != Some("rs") { - continue; - } + let real_paths = evt + .paths + .iter() + .filter(|path| { + // skip non rust files + path.extension().and_then(|p| p.to_str()) == Some("rs") && + // skip excluded paths + !excluded_paths.iter().any(|p| path.starts_with(p)) + }) + .collect::>(); + // Give time for the change to take effect before reading the file + if !real_paths.is_empty() { + std::thread::sleep(std::time::Duration::from_millis(100)); + } + + let mut channels = channels.lock().unwrap(); + for path in real_paths { // find changes to the rsx in the file match file_map .lock() @@ -107,7 +211,10 @@ pub fn init( let mut i = 0; while i < channels.len() { let channel = &mut channels[i]; - if send_template(msg, channel) { + if send_msg( + HotReloadMsg::UpdateTemplate(msg), + channel, + ) { i += 1; } else { channels.remove(i); @@ -116,11 +223,23 @@ pub fn init( } } UpdateResult::NeedsRebuild => { - if log { - println!( - "Rebuild needed... shutting down hot reloading.\nManually rebuild the application to view futher changes." - ); + if let Some(rebuild_command) = rebuild_with { + *aborted.lock().unwrap() = true; + if log { + println!("Rebuilding the application..."); + } + execute::shell(rebuild_command).spawn().expect("Failed to rebuild the application. Is cargo installed?"); + for channel in &mut *channels { + send_msg(HotReloadMsg::Shutdown, channel); + } + } else { + if log { + println!( + "Rebuild needed... shutting down hot reloading.\nManually rebuild the application to view futher changes." + ); + } } + return; } } @@ -134,8 +253,8 @@ pub fn init( } } -fn send_template(template: Template<'static>, channel: &mut impl Write) -> bool { - if let Ok(msg) = serde_json::to_string(&template) { +fn send_msg(msg: HotReloadMsg, channel: &mut impl Write) -> bool { + if let Ok(msg) = serde_json::to_string(&msg) { if channel.write_all(msg.as_bytes()).is_err() { return false; } @@ -149,7 +268,7 @@ fn send_template(template: Template<'static>, channel: &mut impl Write) -> bool } /// Connect to the hot reloading listener. The callback provided will be called every time a template change is detected -pub fn connect(mut f: impl FnMut(Template<'static>) + Send + 'static) { +pub fn connect(mut f: impl FnMut(HotReloadMsg) + Send + 'static) { std::thread::spawn(move || { let temp_file = std::env::temp_dir().join("@dioxusin"); if let Ok(socket) = LocalSocketStream::connect(temp_file.as_path()) { @@ -158,7 +277,7 @@ pub fn connect(mut f: impl FnMut(Template<'static>) + Send + 'static) { let mut buf = String::new(); match buf_reader.read_line(&mut buf) { Ok(_) => { - let template: Template<'static> = + let template: HotReloadMsg = serde_json::from_str(Box::leak(buf.into_boxed_str())).unwrap(); f(template); } @@ -179,33 +298,13 @@ pub fn connect(mut f: impl FnMut(Template<'static>) + Send + 'static) { /// If no paths are passed, it will listen on the src and examples folders. #[macro_export] macro_rules! hot_reload_init { - ($(@ $ctx:ident)? $($t: ident)*) => { + () => { #[cfg(debug_assertions)] - dioxus_hot_reload::init::(core::env!("CARGO_MANIFEST_DIR"), &["src", "examples"], hot_reload_init!(@log: $($t)*)) + dioxus_hot_reload::init(dioxus_hot_reload::Config::new().root(env!("CARGO_MANIFEST_DIR"))); }; - ($(@ $ctx:ident)? $($paths: literal),* $(,)? $($t: ident)*) => { + ($cfg: expr) => { #[cfg(debug_assertions)] - dioxus_hot_reload::init::(core::env!("CARGO_MANIFEST_DIR"), &[$($paths),*], hot_reload_init!(@log: $($t)*)) - }; - - (@log:) => { - false - }; - - (@log: enable logging) => { - true - }; - - (@log: disable logging) => { - false - }; - - (@ctx: $ctx: ident) => { - $ctx - }; - - (@ctx: ) => { - dioxus_hot_reload::HtmlCtx + dioxus_hot_reload::init($cfg.root(env!("CARGO_MANIFEST_DIR"))); }; } diff --git a/packages/liveview/src/pool.rs b/packages/liveview/src/pool.rs index 281ada480..b3ed6bef5 100644 --- a/packages/liveview/src/pool.rs +++ b/packages/liveview/src/pool.rs @@ -157,9 +157,16 @@ where } } - new_template = hot_reload_wait => { - if let Some(new_template) = new_template { - vdom.replace_template(new_template); + msg = hot_reload_wait => { + if let Some(msg) = msg { + match msg{ + dioxus_hot_reload::HotReloadMsg::UpdateTemplate(new_template) => { + vdom.replace_template(new_template); + } + dioxus_hot_reload::HotReloadMsg::Shutdown => { + std::process::exit(0); + }, + } } } } diff --git a/packages/tui/src/lib.rs b/packages/tui/src/lib.rs index 8950a7245..6961aa6d6 100644 --- a/packages/tui/src/lib.rs +++ b/packages/tui/src/lib.rs @@ -6,6 +6,7 @@ use crossterm::{ terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; use dioxus_core::*; +use dioxus_hot_reload::HotReloadMsg; use dioxus_native_core::{real_dom::RealDom, FxDashSet, NodeId, NodeMask, SendAnyMap}; use focus::FocusState; use futures::{ @@ -147,9 +148,9 @@ fn render_vdom( .block_on(async { #[cfg(all(feature = "hot-reload", debug_assertions))] let mut hot_reload_rx = { - let (hot_reload_tx, hot_reload_rx) = unbounded_channel::>(); - dioxus_hot_reload::connect(move |template| { - let _ = hot_reload_tx.send(template); + let (hot_reload_tx, hot_reload_rx) = unbounded_channel::(); + dioxus_hot_reload::connect(move |msg| { + let _ = hot_reload_tx.send(msg); }); hot_reload_rx }; @@ -232,7 +233,7 @@ fn render_vdom( } } - let mut new_templete = None; + let mut hot_reload_msg = None; { let wait = vdom.wait_for_work(); #[cfg(all(feature = "hot-reload", debug_assertions))] @@ -267,15 +268,22 @@ fn render_vdom( register_event(evt); } }, - Some(template) = hot_reload_wait => { - new_templete = Some(template); + Some(msg) = hot_reload_wait => { + hot_reload_msg = Some(msg); } } } // if we have a new template, replace the old one - if let Some(template) = new_templete { - vdom.replace_template(template); + if let Some(msg) = hot_reload_msg { + match msg { + HotReloadMsg::UpdateTemplate(template) => { + vdom.replace_template(template); + } + HotReloadMsg::Shutdown => { + break; + } + } } {