implement hot reloading for desktop

This commit is contained in:
Evan Almloff 2023-01-11 13:40:02 -06:00
parent 0e40619b52
commit 1073574896
9 changed files with 155 additions and 32 deletions

View file

@ -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" }

View file

@ -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]

View file

@ -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,

View file

@ -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 wry::application::{event_loop::EventLoopProxy, window::WindowId};
use std::{sync::Arc, sync::Mutex};
fn handle_error(connection: std::io::Result<LocalSocketStream>) -> Option<LocalSocketStream> { use crate::desktop_context::{EventData, UserWindowEvent};
connection
.map_err(|error| eprintln!("Incoming connection failed: {}", error))
.ok()
}
pub(crate) fn init(proxy: futures_channel::mpsc::UnboundedSender<Template<'static>>) { pub(crate) fn init(proxy: EventLoopProxy<UserWindowEvent>) {
let latest_in_connection: Arc<Mutex<Option<BufReader<LocalSocketStream>>>> =
Arc::new(Mutex::new(None));
let latest_in_connection_handle = latest_in_connection.clone();
// connect to processes for incoming data
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(socket) = LocalSocketStream::connect(temp_file.as_path()) {
if let Ok(listener) = LocalSocketListener::bind(temp_file) { let mut buf_reader = BufReader::new(socket);
for conn in listener.incoming().filter_map(handle_error) {
*latest_in_connection_handle.lock().unwrap() = Some(BufReader::new(conn));
}
}
});
std::thread::spawn(move || {
loop { loop {
if let Some(conn) = &mut *latest_in_connection.lock().unwrap() {
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));
} }
}); });
} }

View file

@ -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);

View file

@ -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

View file

@ -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()
}

View file

@ -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"

View file

@ -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();
}
}
});
}
}
}