mod.rs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. use std::future::{poll_fn, Future, IntoFuture};
  2. use std::task::Poll;
  3. use crate::builder::OpenArguments;
  4. use crate::cli::serve::Serve;
  5. use crate::dioxus_crate::DioxusCrate;
  6. use crate::tracer::CLILogControl;
  7. use crate::Result;
  8. use crate::{
  9. builder::{Stage, TargetPlatform, UpdateBuildProgress, UpdateStage},
  10. TraceSrc,
  11. };
  12. use futures_util::FutureExt;
  13. use tokio::task::yield_now;
  14. mod builder;
  15. mod hot_reloading_file_map;
  16. mod logs_tab;
  17. mod output;
  18. mod proxy;
  19. mod render;
  20. mod server;
  21. mod watcher;
  22. use builder::*;
  23. use output::*;
  24. use server::*;
  25. use watcher::*;
  26. /// For *all* builds the CLI spins up a dedicated webserver, file watcher, and build infrastructure to serve the project.
  27. ///
  28. /// This includes web, desktop, mobile, fullstack, etc.
  29. ///
  30. /// Platform specifics:
  31. /// - Web: we need to attach a filesystem server to our devtools webserver to serve the project. We
  32. /// want to emulate GithubPages here since most folks are deploying there and expect things like
  33. /// basepath to match.
  34. /// - Fullstack: We spin up the same dev server but in this case the fullstack server itself needs to
  35. /// proxy all dev requests to our dev server
  36. /// - Desktop: We spin up the dev server but without a filesystem server.
  37. /// - Mobile: Basically the same as desktop.
  38. ///
  39. /// Notes:
  40. /// - All filesystem changes are tracked here
  41. /// - We send all updates to connected websocket connections. Even desktop connects via the websocket
  42. /// - Right now desktop compiles tokio-tungstenite to do the connection but we could in theory reuse
  43. /// the websocket logic from the webview for thinner builds.
  44. ///
  45. /// Todos(Jon):
  46. /// - I'd love to be able to configure the CLI while it's running so we can change settingaon the fly.
  47. /// This would require some light refactoring and potentially pulling in something like ratatui.
  48. /// - Build a custom subscriber for logs by tools within this
  49. /// - Handle logs from the build engine separately?
  50. /// - Consume logs from the wasm for web/fullstack
  51. /// - I want us to be able to detect a `server_fn` in the project and then upgrade from a static server
  52. /// to a dynamic one on the fly.
  53. pub async fn serve_all(
  54. serve: Serve,
  55. dioxus_crate: DioxusCrate,
  56. log_control: CLILogControl,
  57. ) -> Result<()> {
  58. // Start the screen first so we collect build logs.
  59. let mut screen = Output::start(&serve, log_control).expect("Failed to open terminal logger");
  60. let mut builder = Builder::new(&dioxus_crate, &serve);
  61. // Start the first build
  62. builder.build()?;
  63. let mut server = Server::start(&serve, &dioxus_crate);
  64. let mut watcher = Watcher::start(&serve, &dioxus_crate);
  65. let is_hot_reload = serve.server_arguments.hot_reload.unwrap_or(true);
  66. loop {
  67. // Make sure we don't hog the CPU: these loop { select! {} } blocks can starve the executor
  68. yield_now().await;
  69. // Draw the state of the server to the screen
  70. screen.render(&serve, &dioxus_crate, &builder, &server, &watcher);
  71. // And then wait for any updates before redrawing
  72. tokio::select! {
  73. // rebuild the project or hotreload it
  74. _ = watcher.wait(), if is_hot_reload => {
  75. if !watcher.pending_changes() {
  76. continue
  77. }
  78. let changed_files = watcher.dequeue_changed_files(&dioxus_crate);
  79. let changed = changed_files.first().cloned();
  80. // if change is hotreloadable, hotreload it
  81. // and then send that update to all connected clients
  82. if let Some(hr) = watcher.attempt_hot_reload(&dioxus_crate, changed_files) {
  83. // Only send a hotreload message for templates and assets - otherwise we'll just get a full rebuild
  84. if hr.templates.is_empty() && hr.assets.is_empty() && hr.unknown_files.is_empty() {
  85. continue
  86. }
  87. if let Some(changed_path) = changed {
  88. let path_relative = changed_path.strip_prefix(dioxus_crate.crate_dir()).map(|p| p.display().to_string()).unwrap_or_else(|_| changed_path.display().to_string());
  89. tracing::info!(dx_src = ?TraceSrc::Dev, "Hotreloaded {}", path_relative);
  90. }
  91. server.send_hotreload(hr).await;
  92. } else {
  93. // If the change is not binary patchable, rebuild the project
  94. // We're going to kick off a new build, interrupting the current build if it's ongoing
  95. builder.build()?;
  96. // Clear the hot reload changes
  97. watcher.clear_hot_reload_changes();
  98. // Tell the server to show a loading page for any new requests
  99. server.start_build().await;
  100. }
  101. }
  102. // reload the page
  103. msg = server.wait() => {
  104. // Run the server in the background
  105. // Waiting for updates here lets us tap into when clients are added/removed
  106. match msg {
  107. Some(ServerUpdate::NewConnection) => {
  108. if let Some(msg) = watcher.applied_hot_reload_changes() {
  109. server.send_hotreload(msg).await;
  110. }
  111. }
  112. Some(ServerUpdate::Message(msg)) => {
  113. screen.new_ws_message(TargetPlatform::Web, msg);
  114. }
  115. None => {}
  116. }
  117. }
  118. // Handle updates from the build engine
  119. application = builder.wait() => {
  120. // Wait for logs from the build engine
  121. // These will cause us to update the screen
  122. // We also can check the status of the builds here in case we have multiple ongoing builds
  123. match application {
  124. Ok(BuilderUpdate::Progress { platform, update }) => {
  125. let update_clone = update.clone();
  126. screen.new_build_progress(platform, update_clone);
  127. server.update_build_status(screen.build_progress.progress(), update.stage.to_string()).await;
  128. match update {
  129. // Send rebuild start message.
  130. UpdateBuildProgress { stage: Stage::Compiling, update: UpdateStage::Start } => server.send_reload_start().await,
  131. // Send rebuild failed message.
  132. UpdateBuildProgress { stage: Stage::Finished, update: UpdateStage::Failed(_) } => server.send_reload_failed().await,
  133. _ => {},
  134. }
  135. }
  136. Ok(BuilderUpdate::Ready { results }) => {
  137. if !results.is_empty() {
  138. builder.children.clear();
  139. }
  140. // If we have a build result, open it
  141. for build_result in results.iter() {
  142. let child = build_result.open(
  143. OpenArguments::new(
  144. &serve.server_arguments,
  145. server.fullstack_address(),
  146. &dioxus_crate
  147. )
  148. );
  149. match child {
  150. Ok(Some(child_proc)) => builder.children.push((build_result.target_platform, child_proc)),
  151. Err(e) => {
  152. tracing::error!(dx_src = ?TraceSrc::Build, "Failed to open build result: {e}");
  153. break;
  154. },
  155. _ => {}
  156. }
  157. }
  158. // Make sure we immediately capture the stdout/stderr of the executable -
  159. // otherwise it'll clobber our terminal output
  160. screen.new_ready_app(&mut builder, results);
  161. // And then finally tell the server to reload
  162. server.send_reload_command().await;
  163. },
  164. // If the desktop process exited *cleanly*, we can exit
  165. Ok(BuilderUpdate::ProcessExited { status, target_platform }) => {
  166. // Then remove the child process
  167. builder.children.retain(|(platform, _)| *platform != target_platform);
  168. match (target_platform, status) {
  169. (TargetPlatform::Desktop, Ok(status)) => {
  170. if status.success() {
  171. break;
  172. }
  173. else {
  174. tracing::error!(dx_src = ?TraceSrc::Dev, "Application exited with status: {status}");
  175. }
  176. },
  177. // Ignore the static generation platform exiting
  178. (_ , Ok(_)) => {},
  179. (_, Err(e)) => {
  180. tracing::error!(dx_src = ?TraceSrc::Dev, "Application exited with error: {e}");
  181. }
  182. }
  183. }
  184. Err(err) => {
  185. server.send_build_error(err).await;
  186. }
  187. }
  188. }
  189. // Handle input from the user using our settings
  190. res = screen.wait() => {
  191. match res {
  192. Ok(false) => {}
  193. // Request a rebuild.
  194. Ok(true) => {
  195. builder.build()?;
  196. server.start_build().await
  197. },
  198. // Shutdown the server.
  199. Err(_) => break,
  200. }
  201. }
  202. }
  203. }
  204. // Run our cleanup logic here - maybe printing as we go?
  205. // todo: more printing, logging, error handling in this phase
  206. _ = screen.shutdown();
  207. _ = server.shutdown().await;
  208. builder.shutdown();
  209. Ok(())
  210. }
  211. // Grab the output of a future that returns an option or wait forever
  212. pub(crate) fn next_or_pending<F, T>(f: F) -> impl Future<Output = T>
  213. where
  214. F: IntoFuture<Output = Option<T>>,
  215. {
  216. let pinned = f.into_future().fuse();
  217. let mut pinned = Box::pin(pinned);
  218. poll_fn(move |cx| {
  219. let next = pinned.as_mut().poll(cx);
  220. match next {
  221. Poll::Ready(Some(next)) => Poll::Ready(next),
  222. _ => Poll::Pending,
  223. }
  224. })
  225. .fuse()
  226. }