file_watcher.rs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  1. use std::{
  2. io::Write,
  3. path::PathBuf,
  4. str::FromStr,
  5. sync::{Arc, Mutex},
  6. };
  7. use crate::HotReloadMsg;
  8. use dioxus_rsx::{
  9. hot_reload::{FileMap, FileMapBuildResult, UpdateResult},
  10. HotReloadingContext,
  11. };
  12. use interprocess::local_socket::LocalSocketListener;
  13. use notify::{RecommendedWatcher, RecursiveMode, Watcher};
  14. #[cfg(feature = "file_watcher")]
  15. use dioxus_html::HtmlCtx;
  16. pub struct Config<Ctx: HotReloadingContext> {
  17. root_path: &'static str,
  18. listening_paths: &'static [&'static str],
  19. excluded_paths: &'static [&'static str],
  20. log: bool,
  21. rebuild_with: Option<Box<dyn FnMut() -> bool + Send + 'static>>,
  22. phantom: std::marker::PhantomData<Ctx>,
  23. }
  24. impl<Ctx: HotReloadingContext> Default for Config<Ctx> {
  25. fn default() -> Self {
  26. Self {
  27. root_path: "",
  28. listening_paths: &[""],
  29. excluded_paths: &["./target"],
  30. log: true,
  31. rebuild_with: None,
  32. phantom: std::marker::PhantomData,
  33. }
  34. }
  35. }
  36. #[cfg(feature = "file_watcher")]
  37. impl Config<HtmlCtx> {
  38. pub const fn new() -> Self {
  39. Self {
  40. root_path: "",
  41. listening_paths: &[""],
  42. excluded_paths: &["./target"],
  43. log: true,
  44. rebuild_with: None,
  45. phantom: std::marker::PhantomData,
  46. }
  47. }
  48. }
  49. impl<Ctx: HotReloadingContext> Config<Ctx> {
  50. /// Set the root path of the project (where the Cargo.toml file is). This is automatically set by the [`hot_reload_init`] macro.
  51. pub fn root(self, path: &'static str) -> Self {
  52. Self {
  53. root_path: path,
  54. ..self
  55. }
  56. }
  57. /// Set whether to enable logs
  58. pub fn with_logging(self, log: bool) -> Self {
  59. Self { log, ..self }
  60. }
  61. /// Set the command to run to rebuild the project
  62. ///
  63. /// For example to restart the application after a change is made, you could use `cargo run`
  64. pub fn with_rebuild_command(self, rebuild_command: &'static str) -> Self {
  65. self.with_rebuild_callback(move || {
  66. execute::shell(rebuild_command)
  67. .spawn()
  68. .expect("Failed to spawn the rebuild command");
  69. true
  70. })
  71. }
  72. /// Set a callback to run to when the project needs to be rebuilt and returns if the server should shut down
  73. ///
  74. /// For example a CLI application could rebuild the application when a change is made
  75. pub fn with_rebuild_callback(
  76. self,
  77. rebuild_callback: impl FnMut() -> bool + Send + 'static,
  78. ) -> Self {
  79. Self {
  80. rebuild_with: Some(Box::new(rebuild_callback)),
  81. ..self
  82. }
  83. }
  84. /// 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.
  85. pub fn with_paths(self, paths: &'static [&'static str]) -> Self {
  86. Self {
  87. listening_paths: paths,
  88. ..self
  89. }
  90. }
  91. /// Sets paths to ignore changes on. This will override any paths set in the [`Config::with_paths`] method in the case of conflicts.
  92. pub fn excluded_paths(self, paths: &'static [&'static str]) -> Self {
  93. Self {
  94. excluded_paths: paths,
  95. ..self
  96. }
  97. }
  98. }
  99. /// Initialize the hot reloading listener
  100. ///
  101. /// This is designed to be called by hot_reload_Init!() which will pass in information about the project
  102. ///
  103. /// Notes:
  104. /// - We don't wannt to watch the
  105. pub fn init<Ctx: HotReloadingContext + Send + 'static>(cfg: Config<Ctx>) {
  106. let Config {
  107. mut rebuild_with,
  108. root_path,
  109. listening_paths,
  110. log,
  111. excluded_paths,
  112. ..
  113. } = cfg;
  114. let Ok(crate_dir) = PathBuf::from_str(root_path) else {
  115. return;
  116. };
  117. // try to find the gitignore file
  118. let gitignore_file_path = crate_dir.join(".gitignore");
  119. let (gitignore, _) = ignore::gitignore::Gitignore::new(gitignore_file_path);
  120. // convert the excluded paths to absolute paths
  121. let excluded_paths = excluded_paths
  122. .iter()
  123. .map(|path| crate_dir.join(PathBuf::from(path)))
  124. .collect::<Vec<_>>();
  125. let channels = Arc::new(Mutex::new(Vec::new()));
  126. let FileMapBuildResult {
  127. map: file_map,
  128. errors,
  129. } = FileMap::<Ctx>::create_with_filter(crate_dir.clone(), |path| {
  130. // skip excluded paths
  131. excluded_paths.iter().any(|p| path.starts_with(p)) ||
  132. // respect .gitignore
  133. gitignore
  134. .matched_path_or_any_parents(path, path.is_dir())
  135. .is_ignore()
  136. })
  137. .unwrap();
  138. for err in errors {
  139. if log {
  140. println!("hot reloading failed to initialize:\n{err:?}");
  141. }
  142. }
  143. let file_map = Arc::new(Mutex::new(file_map));
  144. let target_dir = crate_dir.join("target");
  145. let hot_reload_socket_path = target_dir.join("dioxusin");
  146. #[cfg(unix)]
  147. {
  148. // On unix, if you force quit the application, it can leave the file socket open
  149. // This will cause the local socket listener to fail to open
  150. // We check if the file socket is already open from an old session and then delete it
  151. if hot_reload_socket_path.exists() {
  152. let _ = std::fs::remove_file(hot_reload_socket_path.clone());
  153. }
  154. }
  155. let local_socket_stream = match LocalSocketListener::bind(hot_reload_socket_path) {
  156. Ok(local_socket_stream) => local_socket_stream,
  157. Err(err) => {
  158. println!("failed to connect to hot reloading\n{err}");
  159. return;
  160. }
  161. };
  162. let aborted = Arc::new(Mutex::new(false));
  163. // listen for connections
  164. std::thread::spawn({
  165. let file_map = file_map.clone();
  166. let channels = channels.clone();
  167. let aborted = aborted.clone();
  168. let _ = local_socket_stream.set_nonblocking(true);
  169. move || {
  170. loop {
  171. if let Ok(mut connection) = local_socket_stream.accept() {
  172. // send any templates than have changed before the socket connected
  173. let templates: Vec<_> = {
  174. file_map
  175. .lock()
  176. .unwrap()
  177. .map
  178. .values()
  179. .flat_map(|v| v.templates.values().copied())
  180. .collect()
  181. };
  182. for template in templates {
  183. if !send_msg(HotReloadMsg::UpdateTemplate(template), &mut connection) {
  184. continue;
  185. }
  186. }
  187. channels.lock().unwrap().push(connection);
  188. if log {
  189. println!("Connected to hot reloading 🚀");
  190. }
  191. }
  192. if *aborted.lock().unwrap() {
  193. break;
  194. }
  195. }
  196. }
  197. });
  198. // watch for changes
  199. std::thread::spawn(move || {
  200. let mut last_update_time = chrono::Local::now().timestamp();
  201. let (tx, rx) = std::sync::mpsc::channel();
  202. let mut watcher = RecommendedWatcher::new(tx, notify::Config::default()).unwrap();
  203. let mut listening_pathbufs = vec![];
  204. // We're attempting to watch the root path... which contains a target directory...
  205. // And on some platforms the target directory is really really large and can cause the watcher to crash
  206. // since it runs out of file handles
  207. // So we're going to iterate through its children and watch them instead of the root path, skipping the target
  208. // directory.
  209. //
  210. // In reality, this whole approach of doing embedded file watching is kinda hairy since you want full knowledge
  211. // of where rust code is. We could just use the filemap we generated above as an indication of where the rust
  212. // code is in this project and deduce the subfolders under the root path from that.
  213. //
  214. // FIXME: use a more robust system here for embedded discovery
  215. //
  216. // https://github.com/DioxusLabs/dioxus/issues/1914
  217. if listening_paths == &[""] {
  218. for entry in std::fs::read_dir(&crate_dir)
  219. .expect("failed to read rust crate directory. Are you running with cargo?")
  220. {
  221. let entry = entry.expect("failed to read directory entry");
  222. let path = entry.path();
  223. if path.is_dir() {
  224. if path == target_dir {
  225. continue;
  226. }
  227. listening_pathbufs.push(path);
  228. }
  229. }
  230. } else {
  231. for path in listening_paths {
  232. let full_path = crate_dir.join(path);
  233. listening_pathbufs.push(full_path);
  234. }
  235. }
  236. for full_path in listening_pathbufs {
  237. if let Err(err) = watcher.watch(&full_path, RecursiveMode::Recursive) {
  238. if log {
  239. println!("hot reloading failed to start watching {full_path:?}:\n{err:?}",);
  240. }
  241. }
  242. }
  243. let mut rebuild = {
  244. let aborted = aborted.clone();
  245. let channels = channels.clone();
  246. move || {
  247. if let Some(rebuild_callback) = &mut rebuild_with {
  248. if log {
  249. println!("Rebuilding the application...");
  250. }
  251. let shutdown = rebuild_callback();
  252. if shutdown {
  253. *aborted.lock().unwrap() = true;
  254. }
  255. for channel in &mut *channels.lock().unwrap() {
  256. send_msg(HotReloadMsg::Shutdown, channel);
  257. }
  258. return shutdown;
  259. } else if log {
  260. println!("Rebuild needed... shutting down hot reloading.\nManually rebuild the application to view further changes.");
  261. }
  262. true
  263. }
  264. };
  265. for evt in rx {
  266. if chrono::Local::now().timestamp_millis() < last_update_time {
  267. continue;
  268. }
  269. let Ok(evt) = evt else {
  270. continue;
  271. };
  272. let real_paths = evt
  273. .paths
  274. .iter()
  275. .filter(|path| {
  276. // skip non rust files
  277. matches!(
  278. path.extension().and_then(|p| p.to_str()),
  279. Some("rs" | "toml" | "css" | "html" | "js")
  280. ) &&
  281. // skip excluded paths
  282. !excluded_paths.iter().any(|p| path.starts_with(p)) &&
  283. // respect .gitignore
  284. !gitignore
  285. .matched_path_or_any_parents(path, false)
  286. .is_ignore()
  287. })
  288. .collect::<Vec<_>>();
  289. // Give time for the change to take effect before reading the file
  290. if !real_paths.is_empty() {
  291. std::thread::sleep(std::time::Duration::from_millis(10));
  292. }
  293. let mut channels = channels.lock().unwrap();
  294. for path in real_paths {
  295. // if this file type cannot be hot reloaded, rebuild the application
  296. if path.extension().and_then(|p| p.to_str()) != Some("rs") && rebuild() {
  297. return;
  298. }
  299. // find changes to the rsx in the file
  300. let changes = file_map
  301. .lock()
  302. .unwrap()
  303. .update_rsx(path, crate_dir.as_path());
  304. match changes {
  305. Ok(UpdateResult::UpdatedRsx(msgs)) => {
  306. for msg in msgs {
  307. let mut i = 0;
  308. while i < channels.len() {
  309. let channel = &mut channels[i];
  310. if send_msg(HotReloadMsg::UpdateTemplate(msg), channel) {
  311. i += 1;
  312. } else {
  313. channels.remove(i);
  314. }
  315. }
  316. }
  317. }
  318. Ok(UpdateResult::NeedsRebuild) => {
  319. drop(channels);
  320. if rebuild() {
  321. return;
  322. }
  323. break;
  324. }
  325. Err(err) => {
  326. if log {
  327. println!("hot reloading failed to update rsx:\n{err:?}");
  328. }
  329. }
  330. }
  331. }
  332. last_update_time = chrono::Local::now().timestamp_millis();
  333. }
  334. });
  335. }
  336. fn send_msg(msg: HotReloadMsg, channel: &mut impl Write) -> bool {
  337. if let Ok(msg) = serde_json::to_string(&msg) {
  338. if channel.write_all(msg.as_bytes()).is_err() {
  339. return false;
  340. }
  341. if channel.write_all(&[b'\n']).is_err() {
  342. return false;
  343. }
  344. true
  345. } else {
  346. false
  347. }
  348. }