runner.rs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. use super::{AppHandle, ServeUpdate, WebServer};
  2. use crate::{
  3. AppBundle, DioxusCrate, HotreloadFilemap, HotreloadResult, Platform, Result, TraceSrc,
  4. };
  5. use dioxus_core::internal::TemplateGlobalKey;
  6. use dioxus_devtools_types::HotReloadMsg;
  7. use dioxus_html::HtmlCtx;
  8. use futures_util::{future::OptionFuture, stream::FuturesUnordered};
  9. use ignore::gitignore::Gitignore;
  10. use std::{
  11. collections::{HashMap, HashSet},
  12. net::SocketAddr,
  13. path::PathBuf,
  14. };
  15. use tokio_stream::StreamExt;
  16. pub(crate) struct AppRunner {
  17. pub(crate) running: HashMap<Platform, AppHandle>,
  18. pub(crate) krate: DioxusCrate,
  19. pub(crate) file_map: HotreloadFilemap,
  20. pub(crate) ignore: Gitignore,
  21. pub(crate) applied_hot_reload_message: HotReloadMsg,
  22. pub(crate) builds_opened: usize,
  23. pub(crate) should_full_rebuild: bool,
  24. }
  25. impl AppRunner {
  26. /// Create the AppRunner and then initialize the filemap with the crate directory.
  27. pub(crate) fn start(krate: &DioxusCrate) -> Self {
  28. let mut runner = Self {
  29. running: Default::default(),
  30. file_map: HotreloadFilemap::new(),
  31. applied_hot_reload_message: Default::default(),
  32. ignore: krate.workspace_gitignore(),
  33. krate: krate.clone(),
  34. builds_opened: 0,
  35. should_full_rebuild: true,
  36. };
  37. // todo(jon): this might take a while so we should try and background it, or make it lazy somehow
  38. // we could spawn a thread to search the FS and then when it returns we can fill the filemap
  39. // in testing, if this hits a massive directory, it might take several seconds with no feedback.
  40. for krate in krate.all_watched_crates() {
  41. runner.fill_filemap(krate);
  42. }
  43. runner
  44. }
  45. pub(crate) async fn wait(&mut self) -> ServeUpdate {
  46. // If there are no running apps, we can just return pending to avoid deadlocking
  47. if self.running.is_empty() {
  48. return futures_util::future::pending().await;
  49. }
  50. self.running
  51. .iter_mut()
  52. .map(|(platform, handle)| async {
  53. use ServeUpdate::*;
  54. let platform = *platform;
  55. tokio::select! {
  56. Some(Ok(Some(msg))) = OptionFuture::from(handle.app_stdout.as_mut().map(|f| f.next_line())) => {
  57. StdoutReceived { platform, msg }
  58. },
  59. Some(Ok(Some(msg))) = OptionFuture::from(handle.app_stderr.as_mut().map(|f| f.next_line())) => {
  60. StderrReceived { platform, msg }
  61. },
  62. Some(status) = OptionFuture::from(handle.app_child.as_mut().map(|f| f.wait())) => {
  63. match status {
  64. Ok(status) => ProcessExited { status, platform },
  65. Err(_err) => todo!("handle error in process joining?"),
  66. }
  67. }
  68. Some(Ok(Some(msg))) = OptionFuture::from(handle.server_stdout.as_mut().map(|f| f.next_line())) => {
  69. StdoutReceived { platform: Platform::Server, msg }
  70. },
  71. Some(Ok(Some(msg))) = OptionFuture::from(handle.server_stderr.as_mut().map(|f| f.next_line())) => {
  72. StderrReceived { platform: Platform::Server, msg }
  73. },
  74. Some(status) = OptionFuture::from(handle.server_child.as_mut().map(|f| f.wait())) => {
  75. match status {
  76. Ok(status) => ProcessExited { status, platform: Platform::Server },
  77. Err(_err) => todo!("handle error in process joining?"),
  78. }
  79. }
  80. else => futures_util::future::pending().await
  81. }
  82. })
  83. .collect::<FuturesUnordered<_>>()
  84. .next()
  85. .await
  86. .expect("Stream to pending if not empty")
  87. }
  88. /// Finally "bundle" this app and return a handle to it
  89. pub(crate) async fn open(
  90. &mut self,
  91. app: AppBundle,
  92. devserver_ip: SocketAddr,
  93. fullstack_address: Option<SocketAddr>,
  94. should_open_web: bool,
  95. ) -> Result<&AppHandle> {
  96. let platform = app.build.build.platform();
  97. // Drop the old handle
  98. // todo(jon): we should instead be sending the kill signal rather than dropping the process
  99. // This would allow a more graceful shutdown and fix bugs like desktop not retaining its size
  100. self.kill(platform);
  101. // wait a tiny sec for the processes to die so we don't have fullstack servers on top of each other
  102. // todo(jon): we should allow rebinding to the same port in fullstack itself
  103. tokio::time::sleep(std::time::Duration::from_millis(100)).await;
  104. // Add some cute logging
  105. if self.builds_opened == 0 {
  106. tracing::info!(
  107. "Build completed successfully in {:?}ms, launching app! 💫",
  108. app.app.time_taken.as_millis()
  109. );
  110. } else {
  111. tracing::info!("Build completed in {:?}ms", app.app.time_taken.as_millis());
  112. }
  113. // Start the new app before we kill the old one to give it a little bit of time
  114. let mut handle = AppHandle::new(app).await?;
  115. handle
  116. .open(
  117. devserver_ip,
  118. fullstack_address,
  119. self.builds_opened == 0 && should_open_web,
  120. )
  121. .await?;
  122. self.builds_opened += 1;
  123. self.running.insert(platform, handle);
  124. Ok(self.running.get(&platform).unwrap())
  125. }
  126. pub(crate) fn kill(&mut self, platform: Platform) {
  127. self.running.remove(&platform);
  128. }
  129. /// Open an existing app bundle, if it exists
  130. pub(crate) async fn open_existing(&self, devserver: &WebServer) {
  131. if let Some(address) = devserver.server_address() {
  132. let url = format!("http://{address}");
  133. tracing::debug!("opening url: {url}");
  134. _ = open::that(url);
  135. }
  136. }
  137. pub(crate) fn attempt_hot_reload(
  138. &mut self,
  139. modified_files: Vec<PathBuf>,
  140. ) -> Option<HotReloadMsg> {
  141. // If we have any changes to the rust files, we need to update the file map
  142. let mut templates = vec![];
  143. // Prepare the hotreload message we need to send
  144. let mut edited_rust_files = Vec::new();
  145. let mut assets = Vec::new();
  146. for path in modified_files {
  147. // for various assets that might be linked in, we just try to hotreloading them forcefully
  148. // That is, unless they appear in an include! macro, in which case we need to a full rebuild....
  149. let Some(ext) = path.extension().and_then(|v| v.to_str()) else {
  150. continue;
  151. };
  152. // If it's a rust file, we want to hotreload it using the filemap
  153. if ext == "rs" {
  154. edited_rust_files.push(path);
  155. continue;
  156. }
  157. // Otherwise, it might be an asset and we should look for it in all the running apps
  158. for runner in self.running.values() {
  159. if let Some(bundled_name) = runner.hotreload_bundled_asset(&path) {
  160. // todo(jon): don't hardcode this here
  161. let asset_relative = PathBuf::from("/assets/").join(bundled_name);
  162. assets.push(asset_relative);
  163. }
  164. }
  165. }
  166. // Multiple runners might have queued the same asset, so dedup them
  167. assets.dedup();
  168. // Process the rust files
  169. for rust_file in edited_rust_files {
  170. // Strip the prefix before sending it to the filemap
  171. let Ok(path) = rust_file.strip_prefix(self.krate.workspace_dir()) else {
  172. tracing::error!(
  173. "Hotreloading file outside of the crate directory: {:?}",
  174. rust_file
  175. );
  176. continue;
  177. };
  178. // And grabout the contents
  179. let Ok(contents) = std::fs::read_to_string(&rust_file) else {
  180. tracing::debug!(
  181. "Failed to read rust file while hotreloading: {:?}",
  182. rust_file
  183. );
  184. continue;
  185. };
  186. match self.file_map.update_rsx::<HtmlCtx>(path, contents) {
  187. HotreloadResult::Rsx(new) => templates.extend(new),
  188. // The rust file may have failed to parse, but that is most likely
  189. // because the user is in the middle of adding new code
  190. // We just ignore the error and let Rust analyzer warn about the problem
  191. HotreloadResult::Notreloadable => return None,
  192. HotreloadResult::NotParseable => {
  193. tracing::debug!(dx_src = ?TraceSrc::Dev, "Error hotreloading file - not parseable {rust_file:?}")
  194. }
  195. }
  196. }
  197. let msg = HotReloadMsg {
  198. templates,
  199. assets,
  200. unknown_files: vec![],
  201. };
  202. self.add_hot_reload_message(&msg);
  203. Some(msg)
  204. }
  205. /// Get any hot reload changes that have been applied since the last full rebuild
  206. pub(crate) fn applied_hot_reload_changes(&mut self) -> HotReloadMsg {
  207. self.applied_hot_reload_message.clone()
  208. }
  209. /// Clear the hot reload changes. This should be called any time a new build is starting
  210. pub(crate) fn clear_hot_reload_changes(&mut self) {
  211. self.applied_hot_reload_message = Default::default();
  212. }
  213. /// Store the hot reload changes for any future clients that connect
  214. fn add_hot_reload_message(&mut self, msg: &HotReloadMsg) {
  215. let applied = &mut self.applied_hot_reload_message;
  216. // Merge the assets, unknown files, and templates
  217. // We keep the newer change if there is both a old and new change
  218. let mut templates: HashMap<TemplateGlobalKey, _> = std::mem::take(&mut applied.templates)
  219. .into_iter()
  220. .map(|template| (template.key.clone(), template))
  221. .collect();
  222. let mut assets: HashSet<PathBuf> =
  223. std::mem::take(&mut applied.assets).into_iter().collect();
  224. let mut unknown_files: HashSet<PathBuf> = std::mem::take(&mut applied.unknown_files)
  225. .into_iter()
  226. .collect();
  227. for template in &msg.templates {
  228. templates.insert(template.key.clone(), template.clone());
  229. }
  230. assets.extend(msg.assets.iter().cloned());
  231. unknown_files.extend(msg.unknown_files.iter().cloned());
  232. applied.templates = templates.into_values().collect();
  233. applied.assets = assets.into_iter().collect();
  234. applied.unknown_files = unknown_files.into_iter().collect();
  235. }
  236. pub(crate) async fn client_connected(&mut self) {
  237. for (platform, runner) in self.running.iter_mut() {
  238. // Assign the runtime asset dir to the runner
  239. if *platform == Platform::Ios {
  240. // xcrun simctl get_app_container booted com.dioxuslabs
  241. let res = tokio::process::Command::new("xcrun")
  242. .arg("simctl")
  243. .arg("get_app_container")
  244. .arg("booted")
  245. .arg(runner.app.build.krate.bundle_identifier())
  246. .output()
  247. .await;
  248. if let Ok(res) = res {
  249. tracing::trace!("Using runtime asset dir: {:?}", res);
  250. if let Ok(out) = String::from_utf8(res.stdout) {
  251. let out = out.trim();
  252. tracing::trace!("Setting Runtime asset dir: {out:?}");
  253. runner.runtime_asst_dir = Some(PathBuf::from(out));
  254. }
  255. }
  256. }
  257. }
  258. }
  259. /// Fill the filemap with files from the filesystem, using the given filter to determine which files to include.
  260. ///
  261. /// You can use the filter with something like a gitignore to only include files that are relevant to your project.
  262. /// We'll walk the filesystem from the given path and recursively search for all files that match the filter.
  263. ///
  264. /// The filter function takes a path and returns true if the file should be included in the filemap.
  265. /// Generally this will only be .rs files
  266. ///
  267. /// If a file couldn't be parsed, we don't fail. Instead, we save the error.
  268. pub fn fill_filemap(&mut self, path: PathBuf) {
  269. if self.ignore.matched(&path, path.is_dir()).is_ignore() {
  270. return;
  271. }
  272. // If the file is a .rs file, add it to the filemap
  273. if path.extension().and_then(|s| s.to_str()) == Some("rs") {
  274. if let Ok(contents) = std::fs::read_to_string(&path) {
  275. if let Ok(path) = path.strip_prefix(self.krate.workspace_dir()) {
  276. self.file_map.add_file(path.to_path_buf(), contents);
  277. }
  278. }
  279. return;
  280. }
  281. // If it's not, we'll try to read the directory
  282. if path.is_dir() {
  283. if let Ok(read_dir) = std::fs::read_dir(&path) {
  284. for entry in read_dir.flatten() {
  285. self.fill_filemap(entry.path());
  286. }
  287. }
  288. }
  289. }
  290. }