123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338 |
- use std::{
- io::{BufRead, BufReader, Write},
- path::PathBuf,
- str::FromStr,
- sync::{Arc, Mutex},
- };
- use dioxus_core::Template;
- use dioxus_rsx::{
- hot_reload::{FileMap, UpdateResult},
- HotReloadingContext,
- };
- use interprocess::local_socket::{LocalSocketListener, LocalSocketStream};
- use notify::{RecommendedWatcher, RecursiveMode, Watcher};
- #[cfg(debug_assertions)]
- pub use dioxus_html::HtmlCtx;
- use serde::{Deserialize, Serialize};
- /// 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<Ctx: HotReloadingContext = HtmlCtx> {
- 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<Ctx>,
- }
- impl<Ctx: HotReloadingContext> Default for Config<Ctx> {
- fn default() -> Self {
- Self {
- root_path: "",
- listening_paths: &[""],
- excluded_paths: &["./target"],
- log: true,
- rebuild_with: None,
- phantom: std::marker::PhantomData,
- }
- }
- }
- impl Config<HtmlCtx> {
- pub const fn new() -> Self {
- Self {
- root_path: "",
- listening_paths: &[""],
- excluded_paths: &["./target"],
- log: true,
- rebuild_with: None,
- phantom: std::marker::PhantomData,
- }
- }
- }
- impl<Ctx: HotReloadingContext> Config<Ctx> {
- /// Set the root path of the project (where the Cargo.toml file is). This is automatically set by the [`hot_reload_init`] macro.
- pub const fn root(self, path: &'static str) -> Self {
- Self {
- root_path: path,
- ..self
- }
- }
- /// Set whether to enable logs
- pub const fn with_logging(self, log: bool) -> Self {
- Self { log, ..self }
- }
- /// Set the command to run to rebuild the project
- ///
- /// For example to restart the application after a change is made, you could use `cargo run`
- pub const fn with_rebuild_command(self, rebuild_with: &'static str) -> Self {
- Self {
- rebuild_with: Some(rebuild_with),
- ..self
- }
- }
- /// Set the paths to listen for changes in to trigger hot reloading. If this is a directory it will listen for changes in all files in that directory recursively.
- pub const fn with_paths(self, paths: &'static [&'static str]) -> Self {
- Self {
- listening_paths: paths,
- ..self
- }
- }
- /// Sets paths to ignore changes on. This will override any paths set in the [`Config::with_paths`] method in the case of conflicts.
- pub const fn excluded_paths(self, paths: &'static [&'static str]) -> Self {
- Self {
- excluded_paths: paths,
- ..self
- }
- }
- }
- /// Initialize the hot reloading listener
- pub fn init<Ctx: HotReloadingContext + Send + 'static>(cfg: Config<Ctx>) {
- 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::<Ctx>::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 || {
- 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 🚀");
- }
- }
- std::thread::sleep(std::time::Duration::from_millis(10));
- if aborted.lock().unwrap().clone() {
- break;
- }
- }
- }
- });
- // watch for changes
- std::thread::spawn(move || {
- // try to find the gitingore file
- let gitignore_file_path = crate_dir.join(".gitignore");
- let (gitignore, _) = ignore::gitignore::Gitignore::new(gitignore_file_path);
- let mut last_update_time = chrono::Local::now().timestamp();
- let (tx, rx) = std::sync::mpsc::channel();
- let mut watcher = RecommendedWatcher::new(tx, notify::Config::default()).unwrap();
- for path in listening_paths {
- 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::<Vec<_>>();
- let rebuild = || {
- 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 spawn the rebuild command");
- for channel in &mut *channels.lock().unwrap() {
- send_msg(HotReloadMsg::Shutdown, channel);
- }
- } else {
- if log {
- println!(
- "Rebuild needed... shutting down hot reloading.\nManually rebuild the application to view futher changes."
- );
- }
- }
- };
- for evt in rx {
- if chrono::Local::now().timestamp() > last_update_time {
- if let Ok(evt) = evt {
- let real_paths = evt
- .paths
- .iter()
- .filter(|path| {
- // skip non rust files
- matches!(
- path.extension().and_then(|p| p.to_str()),
- Some("rs" | "toml" | "css" | "html" | "js")
- )&&
- // skip excluded paths
- !excluded_paths.iter().any(|p| path.starts_with(p)) &&
- // respect .gitignore
- !gitignore
- .matched_path_or_any_parents(path, false)
- .is_ignore()
- })
- .collect::<Vec<_>>();
- // 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(10));
- }
- let mut channels = channels.lock().unwrap();
- for path in real_paths {
- println!("File changed: {:?}", path);
- // if this file type cannot be hot reloaded, rebuild the application
- if path.extension().and_then(|p| p.to_str()) != Some("rs") {
- rebuild();
- }
- // find changes to the rsx in the file
- match file_map
- .lock()
- .unwrap()
- .update_rsx(path, crate_dir.as_path())
- {
- UpdateResult::UpdatedRsx(msgs) => {
- for msg in msgs {
- let mut i = 0;
- while i < channels.len() {
- let channel = &mut channels[i];
- if send_msg(
- HotReloadMsg::UpdateTemplate(msg),
- channel,
- ) {
- i += 1;
- } else {
- channels.remove(i);
- }
- }
- }
- }
- UpdateResult::NeedsRebuild => {
- drop(channels);
- rebuild();
- return;
- }
- }
- }
- }
- last_update_time = chrono::Local::now().timestamp();
- }
- }
- });
- }
- }
- }
- 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;
- }
- if channel.write_all(&[b'\n']).is_err() {
- return false;
- }
- true
- } else {
- false
- }
- }
- /// 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(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()) {
- let mut buf_reader = BufReader::new(socket);
- loop {
- let mut buf = String::new();
- match buf_reader.read_line(&mut buf) {
- Ok(_) => {
- let template: HotReloadMsg =
- serde_json::from_str(Box::leak(buf.into_boxed_str())).unwrap();
- f(template);
- }
- Err(err) => {
- if err.kind() != std::io::ErrorKind::WouldBlock {
- break;
- }
- }
- }
- }
- }
- });
- }
- /// Start the hot reloading server with the current directory as the root
- #[macro_export]
- macro_rules! hot_reload_init {
- () => {
- #[cfg(debug_assertions)]
- dioxus_hot_reload::init(dioxus_hot_reload::Config::new().root(env!("CARGO_MANIFEST_DIR")));
- };
- ($cfg: expr) => {
- #[cfg(debug_assertions)]
- dioxus_hot_reload::init($cfg.root(env!("CARGO_MANIFEST_DIR")));
- };
- }
|