Browse Source

implement hot reloading for desktop

Evan Almloff 2 years ago
parent
commit
1073574896

+ 3 - 0
Cargo.toml

@@ -20,6 +20,8 @@ members = [
     "packages/native-core-macro",
     "packages/native-core-macro",
     "packages/rsx-rosetta",
     "packages/rsx-rosetta",
     "packages/signals",
     "packages/signals",
+    "packages/hot-reload",
+    "packages/hot-reload-macro",
     "docs/guide",
     "docs/guide",
 ]
 ]
 
 
@@ -43,6 +45,7 @@ publish = false
 dioxus = { path = "./packages/dioxus" }
 dioxus = { path = "./packages/dioxus" }
 dioxus-desktop = { path = "./packages/desktop", features = ["transparent"] }
 dioxus-desktop = { path = "./packages/desktop", features = ["transparent"] }
 dioxus-ssr = { path = "./packages/ssr" }
 dioxus-ssr = { path = "./packages/ssr" }
+dioxus-hot-reload = { path = "./packages/hot-reload" }
 dioxus-router = { path = "./packages/router" }
 dioxus-router = { path = "./packages/router" }
 dioxus-signals = { path = "./packages/signals" }
 dioxus-signals = { path = "./packages/signals" }
 fermi = { path = "./packages/fermi" }
 fermi = { path = "./packages/fermi" }

+ 1 - 1
packages/desktop/Cargo.toml

@@ -33,7 +33,7 @@ webbrowser = "0.8.0"
 infer = "0.11.0"
 infer = "0.11.0"
 dunce = "1.0.2"
 dunce = "1.0.2"
 
 
-interprocess = { version = "1.1.1", optional = true }
+interprocess = { version = "1.2.1", optional = true }
 futures-util = "0.3.25"
 futures-util = "0.3.25"
 
 
 [target.'cfg(target_os = "ios")'.dependencies]
 [target.'cfg(target_os = "ios")'.dependencies]

+ 3 - 0
packages/desktop/src/desktop_context.rs

@@ -8,6 +8,7 @@ use crate::events::IpcMessage;
 use crate::Config;
 use crate::Config;
 use crate::WebviewHandler;
 use crate::WebviewHandler;
 use dioxus_core::ScopeState;
 use dioxus_core::ScopeState;
+use dioxus_core::Template;
 use dioxus_core::VirtualDom;
 use dioxus_core::VirtualDom;
 use serde_json::Value;
 use serde_json::Value;
 use wry::application::event_loop::EventLoopProxy;
 use wry::application::event_loop::EventLoopProxy;
@@ -262,6 +263,8 @@ pub enum EventData {
 
 
     Ipc(IpcMessage),
     Ipc(IpcMessage),
 
 
+    TemplateUpdated(Template<'static>),
+
     NewWindow,
     NewWindow,
 
 
     CloseWindow,
     CloseWindow,

+ 15 - 31
packages/desktop/src/hot_reload.rs

@@ -2,43 +2,29 @@
 
 
 use dioxus_core::Template;
 use dioxus_core::Template;
 
 
-use interprocess::local_socket::{LocalSocketListener, LocalSocketStream};
+use interprocess::local_socket::LocalSocketStream;
 use std::io::{BufRead, BufReader};
 use std::io::{BufRead, BufReader};
-use std::time::Duration;
-use std::{sync::Arc, sync::Mutex};
+use wry::application::{event_loop::EventLoopProxy, window::WindowId};
 
 
-fn handle_error(connection: std::io::Result<LocalSocketStream>) -> Option<LocalSocketStream> {
-    connection
-        .map_err(|error| eprintln!("Incoming connection failed: {}", error))
-        .ok()
-}
-
-pub(crate) fn init(proxy: futures_channel::mpsc::UnboundedSender<Template<'static>>) {
-    let latest_in_connection: Arc<Mutex<Option<BufReader<LocalSocketStream>>>> =
-        Arc::new(Mutex::new(None));
-
-    let latest_in_connection_handle = latest_in_connection.clone();
+use crate::desktop_context::{EventData, UserWindowEvent};
 
 
-    // connect to processes for incoming data
+pub(crate) fn init(proxy: EventLoopProxy<UserWindowEvent>) {
     std::thread::spawn(move || {
     std::thread::spawn(move || {
         let temp_file = std::env::temp_dir().join("@dioxusin");
         let temp_file = std::env::temp_dir().join("@dioxusin");
-
-        if let Ok(listener) = LocalSocketListener::bind(temp_file) {
-            for conn in listener.incoming().filter_map(handle_error) {
-                *latest_in_connection_handle.lock().unwrap() = Some(BufReader::new(conn));
-            }
-        }
-    });
-
-    std::thread::spawn(move || {
-        loop {
-            if let Some(conn) = &mut *latest_in_connection.lock().unwrap() {
+        if let Ok(socket) = LocalSocketStream::connect(temp_file.as_path()) {
+            let mut buf_reader = BufReader::new(socket);
+            loop {
                 let mut buf = String::new();
                 let mut buf = String::new();
-                match conn.read_line(&mut buf) {
+                match buf_reader.read_line(&mut buf) {
                     Ok(_) => {
                     Ok(_) => {
-                        let msg: Template<'static> =
+                        let template: Template<'static> =
                             serde_json::from_str(Box::leak(buf.into_boxed_str())).unwrap();
                             serde_json::from_str(Box::leak(buf.into_boxed_str())).unwrap();
-                        proxy.unbounded_send(msg).unwrap();
+                        proxy
+                            .send_event(UserWindowEvent(
+                                EventData::TemplateUpdated(template),
+                                unsafe { WindowId::dummy() },
+                            ))
+                            .unwrap();
                     }
                     }
                     Err(err) => {
                     Err(err) => {
                         if err.kind() != std::io::ErrorKind::WouldBlock {
                         if err.kind() != std::io::ErrorKind::WouldBlock {
@@ -47,8 +33,6 @@ pub(crate) fn init(proxy: futures_channel::mpsc::UnboundedSender<Template<'stati
                     }
                     }
                 }
                 }
             }
             }
-            // give the error handler time to take the mutex
-            std::thread::sleep(Duration::from_millis(100));
         }
         }
     });
     });
 }
 }

+ 12 - 0
packages/desktop/src/lib.rs

@@ -109,6 +109,10 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
 
 
     let proxy = event_loop.create_proxy();
     let proxy = event_loop.create_proxy();
 
 
+    // Intialize hot reloading if it is enabled
+    #[cfg(all(feature = "hot-reload", debug_assertions))]
+    hot_reload::init(proxy.clone());
+
     // We start the tokio runtime *on this thread*
     // We start the tokio runtime *on this thread*
     // Any future we poll later will use this runtime to spawn tasks and for IO
     // Any future we poll later will use this runtime to spawn tasks and for IO
     let rt = tokio::runtime::Builder::new_multi_thread()
     let rt = tokio::runtime::Builder::new_multi_thread()
@@ -168,6 +172,14 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
             }
             }
 
 
             Event::UserEvent(event) => match event.0 {
             Event::UserEvent(event) => match event.0 {
+                EventData::TemplateUpdated(template) => {
+                    for webview in webviews.values_mut() {
+                        webview.dom.replace_template(template);
+
+                        poll_vdom(webview);
+                    }
+                }
+
                 EventData::CloseWindow => {
                 EventData::CloseWindow => {
                     webviews.remove(&event.1);
                     webviews.remove(&event.1);
 
 

+ 13 - 0
packages/hot-reload-macro/Cargo.toml

@@ -0,0 +1,13 @@
+[package]
+name = "dioxus-hot-reload-macro"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+syn = { version = "1.0.107", features = ["full"] }
+proc-macro2 = { version = "1.0" }
+
+[lib]
+proc-macro = true

+ 7 - 0
packages/hot-reload-macro/src/lib.rs

@@ -0,0 +1,7 @@
+use proc_macro::TokenStream;
+use syn::__private::quote::quote;
+
+#[proc_macro]
+pub fn hot_reload(_: TokenStream) -> TokenStream {
+    quote!(dioxus_hot_reload::init(core::env!("CARGO_MANIFEST_DIR"))).into()
+}

+ 17 - 0
packages/hot-reload/Cargo.toml

@@ -0,0 +1,17 @@
+[package]
+name = "dioxus-hot-reload"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+dioxus-hot-reload-macro = { path = "../hot-reload-macro" }
+dioxus-rsx = { path = "../rsx" }
+dioxus-core = { path = "../core", features = ["serialize"] }
+dioxus-html = { path = "../html", features = ["hot-reload-context"] }
+
+interprocess = { version = "1.2.1" }
+notify = "5.0.0"
+chrono = "0.4.23"
+serde_json = "1.0.91"

+ 84 - 0
packages/hot-reload/src/lib.rs

@@ -0,0 +1,84 @@
+use std::{
+    io::Write,
+    path::PathBuf,
+    str::FromStr,
+    sync::{Arc, Mutex},
+};
+
+pub use dioxus_hot_reload_macro::hot_reload;
+use dioxus_html::HtmlCtx;
+use dioxus_rsx::hot_reload::{FileMap, UpdateResult};
+use interprocess::local_socket::LocalSocketListener;
+use notify::{RecommendedWatcher, RecursiveMode, Watcher};
+
+pub fn init(path: &'static str) {
+    if let Ok(crate_dir) = PathBuf::from_str(path) {
+        let temp_file = std::env::temp_dir().join("@dioxusin");
+        let channels = Arc::new(Mutex::new(Vec::new()));
+        if let Ok(local_socket_stream) = LocalSocketListener::bind(temp_file.as_path()) {
+            // listen for connections
+            std::thread::spawn({
+                let channels = channels.clone();
+                move || {
+                    for connection in local_socket_stream.incoming() {
+                        if let Ok(connection) = connection {
+                            channels.lock().unwrap().push(connection);
+                            println!("Connected to hot reloading 🚀");
+                        }
+                    }
+                }
+            });
+
+            // watch for changes
+            std::thread::spawn(move || {
+                let mut last_update_time = chrono::Local::now().timestamp();
+                let mut file_map = FileMap::<HtmlCtx>::new(crate_dir.clone());
+
+                let (tx, rx) = std::sync::mpsc::channel();
+
+                let mut watcher = RecommendedWatcher::new(tx, notify::Config::default()).unwrap();
+
+                let mut examples_path = crate_dir.clone();
+                examples_path.push("examples");
+                let _ = watcher.watch(&examples_path, RecursiveMode::Recursive);
+                let mut src_path = crate_dir.clone();
+                src_path.push("src");
+                let _ = watcher.watch(&src_path, RecursiveMode::Recursive);
+
+                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;
+                                }
+
+                                // find changes to the rsx in the file
+                                match file_map.update_rsx(&path, crate_dir.as_path()) {
+                                    UpdateResult::UpdatedRsx(msgs) => {
+                                        for msg in msgs {
+                                            for channel in channels.iter_mut() {
+                                                let msg = serde_json::to_string(&msg).unwrap();
+                                                channel.write_all(msg.as_bytes()).unwrap();
+                                                channel.write_all(&[b'\n']).unwrap();
+                                            }
+                                        }
+                                    }
+                                    UpdateResult::NeedsRebuild => {
+                                        println!("Rebuild needed... shutting down hot reloading");
+                                        return;
+                                    }
+                                }
+                            }
+                        }
+                        last_update_time = chrono::Local::now().timestamp();
+                    }
+                }
+            });
+        }
+    }
+}