lib.rs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. use std::{
  2. io::{BufRead, BufReader, Write},
  3. path::PathBuf,
  4. str::FromStr,
  5. sync::{Arc, Mutex},
  6. };
  7. use dioxus_core::Template;
  8. use dioxus_rsx::{
  9. hot_reload::{FileMap, UpdateResult},
  10. HotReloadingContext,
  11. };
  12. use interprocess::local_socket::{LocalSocketListener, LocalSocketStream};
  13. use notify::{RecommendedWatcher, RecursiveMode, Watcher};
  14. #[cfg(debug_assertions)]
  15. pub use dioxus_html::HtmlCtx;
  16. use serde::{Deserialize, Serialize};
  17. /// A message the hot reloading server sends to the client
  18. #[derive(Debug, Serialize, Deserialize, Clone, Copy)]
  19. pub enum HotReloadMsg {
  20. /// A template has been updated
  21. #[serde(borrow = "'static")]
  22. UpdateTemplate(Template<'static>),
  23. /// The program needs to be recompiled, and the client should shut down
  24. Shutdown,
  25. }
  26. #[derive(Debug, Clone, Copy)]
  27. pub struct Config<Ctx: HotReloadingContext = HtmlCtx> {
  28. root_path: &'static str,
  29. listening_paths: &'static [&'static str],
  30. excluded_paths: &'static [&'static str],
  31. log: bool,
  32. rebuild_with: Option<&'static str>,
  33. phantom: std::marker::PhantomData<Ctx>,
  34. }
  35. impl<Ctx: HotReloadingContext> Default for Config<Ctx> {
  36. fn default() -> Self {
  37. Self {
  38. root_path: "",
  39. listening_paths: &[""],
  40. excluded_paths: &["./target"],
  41. log: true,
  42. rebuild_with: None,
  43. phantom: std::marker::PhantomData,
  44. }
  45. }
  46. }
  47. impl Config<HtmlCtx> {
  48. pub const fn new() -> Self {
  49. Self {
  50. root_path: "",
  51. listening_paths: &[""],
  52. excluded_paths: &["./target"],
  53. log: true,
  54. rebuild_with: None,
  55. phantom: std::marker::PhantomData,
  56. }
  57. }
  58. }
  59. impl<Ctx: HotReloadingContext> Config<Ctx> {
  60. /// Set the root path of the project (where the Cargo.toml file is). This is automatically set by the [`hot_reload_init`] macro.
  61. pub const fn root(self, path: &'static str) -> Self {
  62. Self {
  63. root_path: path,
  64. ..self
  65. }
  66. }
  67. /// Set whether to enable logs
  68. pub const fn with_logging(self, log: bool) -> Self {
  69. Self { log, ..self }
  70. }
  71. /// Set the command to run to rebuild the project
  72. ///
  73. /// For example to restart the application after a change is made, you could use `cargo run`
  74. pub const fn with_rebuild_command(self, rebuild_with: &'static str) -> Self {
  75. Self {
  76. rebuild_with: Some(rebuild_with),
  77. ..self
  78. }
  79. }
  80. /// 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.
  81. pub const fn with_paths(self, paths: &'static [&'static str]) -> Self {
  82. Self {
  83. listening_paths: paths,
  84. ..self
  85. }
  86. }
  87. /// Sets paths to ignore changes on. This will override any paths set in the [`Config::with_paths`] method in the case of conflicts.
  88. pub const fn excluded_paths(self, paths: &'static [&'static str]) -> Self {
  89. Self {
  90. excluded_paths: paths,
  91. ..self
  92. }
  93. }
  94. }
  95. /// Initialize the hot reloading listener
  96. pub fn init<Ctx: HotReloadingContext + Send + 'static>(cfg: Config<Ctx>) {
  97. let Config {
  98. root_path,
  99. listening_paths,
  100. log,
  101. rebuild_with,
  102. excluded_paths,
  103. phantom: _,
  104. } = cfg;
  105. if let Ok(crate_dir) = PathBuf::from_str(root_path) {
  106. let temp_file = std::env::temp_dir().join("@dioxusin");
  107. let channels = Arc::new(Mutex::new(Vec::new()));
  108. let file_map = Arc::new(Mutex::new(FileMap::<Ctx>::new(crate_dir.clone())));
  109. if let Ok(local_socket_stream) = LocalSocketListener::bind(temp_file.as_path()) {
  110. let aborted = Arc::new(Mutex::new(false));
  111. // listen for connections
  112. std::thread::spawn({
  113. let file_map = file_map.clone();
  114. let channels = channels.clone();
  115. let aborted = aborted.clone();
  116. let _ = local_socket_stream.set_nonblocking(true);
  117. move || {
  118. loop {
  119. if let Ok(mut connection) = local_socket_stream.accept() {
  120. // send any templates than have changed before the socket connected
  121. let templates: Vec<_> = {
  122. file_map
  123. .lock()
  124. .unwrap()
  125. .map
  126. .values()
  127. .filter_map(|(_, template_slot)| *template_slot)
  128. .collect()
  129. };
  130. for template in templates {
  131. if !send_msg(
  132. HotReloadMsg::UpdateTemplate(template),
  133. &mut connection,
  134. ) {
  135. continue;
  136. }
  137. }
  138. channels.lock().unwrap().push(connection);
  139. if log {
  140. println!("Connected to hot reloading 🚀");
  141. }
  142. }
  143. std::thread::sleep(std::time::Duration::from_millis(10));
  144. if aborted.lock().unwrap().clone() {
  145. break;
  146. }
  147. }
  148. }
  149. });
  150. // watch for changes
  151. std::thread::spawn(move || {
  152. // try to find the gitingore file
  153. let gitignore_file_path = crate_dir.join(".gitignore");
  154. let (gitignore, _) = ignore::gitignore::Gitignore::new(gitignore_file_path);
  155. let mut last_update_time = chrono::Local::now().timestamp();
  156. let (tx, rx) = std::sync::mpsc::channel();
  157. let mut watcher = RecommendedWatcher::new(tx, notify::Config::default()).unwrap();
  158. for path in listening_paths {
  159. let full_path = crate_dir.join(path);
  160. if let Err(err) = watcher.watch(&full_path, RecursiveMode::Recursive) {
  161. if log {
  162. println!(
  163. "hot reloading failed to start watching {full_path:?}:\n{err:?}",
  164. );
  165. }
  166. }
  167. }
  168. let excluded_paths = excluded_paths
  169. .iter()
  170. .map(|path| crate_dir.join(PathBuf::from(path)))
  171. .collect::<Vec<_>>();
  172. let rebuild = || {
  173. if let Some(rebuild_command) = rebuild_with {
  174. *aborted.lock().unwrap() = true;
  175. if log {
  176. println!("Rebuilding the application...");
  177. }
  178. execute::shell(rebuild_command)
  179. .spawn()
  180. .expect("Failed to spawn the rebuild command");
  181. for channel in &mut *channels.lock().unwrap() {
  182. send_msg(HotReloadMsg::Shutdown, channel);
  183. }
  184. } else {
  185. if log {
  186. println!(
  187. "Rebuild needed... shutting down hot reloading.\nManually rebuild the application to view futher changes."
  188. );
  189. }
  190. }
  191. };
  192. for evt in rx {
  193. if chrono::Local::now().timestamp() > last_update_time {
  194. if let Ok(evt) = evt {
  195. let real_paths = evt
  196. .paths
  197. .iter()
  198. .filter(|path| {
  199. // skip non rust files
  200. matches!(
  201. path.extension().and_then(|p| p.to_str()),
  202. Some("rs" | "toml" | "css" | "html" | "js")
  203. )&&
  204. // skip excluded paths
  205. !excluded_paths.iter().any(|p| path.starts_with(p)) &&
  206. // respect .gitignore
  207. !gitignore
  208. .matched_path_or_any_parents(path, false)
  209. .is_ignore()
  210. })
  211. .collect::<Vec<_>>();
  212. // Give time for the change to take effect before reading the file
  213. if !real_paths.is_empty() {
  214. std::thread::sleep(std::time::Duration::from_millis(10));
  215. }
  216. let mut channels = channels.lock().unwrap();
  217. for path in real_paths {
  218. println!("File changed: {:?}", path);
  219. // if this file type cannot be hot reloaded, rebuild the application
  220. if path.extension().and_then(|p| p.to_str()) != Some("rs") {
  221. rebuild();
  222. }
  223. // find changes to the rsx in the file
  224. match file_map
  225. .lock()
  226. .unwrap()
  227. .update_rsx(path, crate_dir.as_path())
  228. {
  229. UpdateResult::UpdatedRsx(msgs) => {
  230. for msg in msgs {
  231. let mut i = 0;
  232. while i < channels.len() {
  233. let channel = &mut channels[i];
  234. if send_msg(
  235. HotReloadMsg::UpdateTemplate(msg),
  236. channel,
  237. ) {
  238. i += 1;
  239. } else {
  240. channels.remove(i);
  241. }
  242. }
  243. }
  244. }
  245. UpdateResult::NeedsRebuild => {
  246. drop(channels);
  247. rebuild();
  248. return;
  249. }
  250. }
  251. }
  252. }
  253. last_update_time = chrono::Local::now().timestamp();
  254. }
  255. }
  256. });
  257. }
  258. }
  259. }
  260. fn send_msg(msg: HotReloadMsg, channel: &mut impl Write) -> bool {
  261. if let Ok(msg) = serde_json::to_string(&msg) {
  262. if channel.write_all(msg.as_bytes()).is_err() {
  263. return false;
  264. }
  265. if channel.write_all(&[b'\n']).is_err() {
  266. return false;
  267. }
  268. true
  269. } else {
  270. false
  271. }
  272. }
  273. /// Connect to the hot reloading listener. The callback provided will be called every time a template change is detected
  274. pub fn connect(mut f: impl FnMut(HotReloadMsg) + Send + 'static) {
  275. std::thread::spawn(move || {
  276. let temp_file = std::env::temp_dir().join("@dioxusin");
  277. if let Ok(socket) = LocalSocketStream::connect(temp_file.as_path()) {
  278. let mut buf_reader = BufReader::new(socket);
  279. loop {
  280. let mut buf = String::new();
  281. match buf_reader.read_line(&mut buf) {
  282. Ok(_) => {
  283. let template: HotReloadMsg =
  284. serde_json::from_str(Box::leak(buf.into_boxed_str())).unwrap();
  285. f(template);
  286. }
  287. Err(err) => {
  288. if err.kind() != std::io::ErrorKind::WouldBlock {
  289. break;
  290. }
  291. }
  292. }
  293. }
  294. }
  295. });
  296. }
  297. /// Start the hot reloading server with the current directory as the root
  298. #[macro_export]
  299. macro_rules! hot_reload_init {
  300. () => {
  301. #[cfg(debug_assertions)]
  302. dioxus_hot_reload::init(dioxus_hot_reload::Config::new().root(env!("CARGO_MANIFEST_DIR")));
  303. };
  304. ($cfg: expr) => {
  305. #[cfg(debug_assertions)]
  306. dioxus_hot_reload::init($cfg.root(env!("CARGO_MANIFEST_DIR")));
  307. };
  308. }