runner.rs 43 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141
  1. use super::{AppBuilder, ServeUpdate, WebServer};
  2. use crate::{
  3. BuildArtifacts, BuildId, BuildMode, BuildTargets, Error, HotpatchModuleCache, Platform, Result,
  4. ServeArgs, TailwindCli, TraceSrc, Workspace,
  5. };
  6. use anyhow::Context;
  7. use dioxus_core::internal::{
  8. HotReloadTemplateWithLocation, HotReloadedTemplate, TemplateGlobalKey,
  9. };
  10. use dioxus_devtools_types::HotReloadMsg;
  11. use dioxus_dx_wire_format::BuildStage;
  12. use dioxus_html::HtmlCtx;
  13. use dioxus_rsx::CallBody;
  14. use dioxus_rsx_hotreload::{ChangedRsx, HotReloadResult};
  15. use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender};
  16. use futures_util::future::OptionFuture;
  17. use futures_util::StreamExt;
  18. use krates::NodeId;
  19. use notify::{
  20. event::{MetadataKind, ModifyKind},
  21. Config, EventKind, RecursiveMode, Watcher as NotifyWatcher,
  22. };
  23. use std::{
  24. collections::{HashMap, HashSet},
  25. net::{IpAddr, TcpListener},
  26. path::PathBuf,
  27. sync::Arc,
  28. time::Duration,
  29. };
  30. use subsecond_types::JumpTable;
  31. use syn::spanned::Spanned;
  32. use tokio::process::Command;
  33. /// This is the primary "state" object that holds the builds and handles for the running apps.
  34. ///
  35. /// It also holds the watcher which is used to watch for changes in the filesystem and trigger rebuilds,
  36. /// hotreloads, asset updates, etc.
  37. ///
  38. /// Since we resolve the build request before initializing the CLI, it also serves as a place to store
  39. /// resolved "serve" arguments, which is why it takes ServeArgs instead of BuildArgs. Simply wrap the
  40. /// BuildArgs in a default ServeArgs and pass it in.
  41. pub(crate) struct AppServer {
  42. /// the platform of the "primary" crate (ie the first)
  43. pub(crate) workspace: Arc<Workspace>,
  44. pub(crate) client: AppBuilder,
  45. pub(crate) server: Option<AppBuilder>,
  46. // Related to to the filesystem watcher
  47. pub(crate) watcher: Box<dyn notify::Watcher>,
  48. pub(crate) _watcher_tx: UnboundedSender<notify::Event>,
  49. pub(crate) watcher_rx: UnboundedReceiver<notify::Event>,
  50. // Tracked state related to open builds and hot reloading
  51. pub(crate) applied_hot_reload_message: HotReloadMsg,
  52. pub(crate) file_map: HashMap<PathBuf, CachedFile>,
  53. // Resolved args related to how we go about processing the rebuilds and logging
  54. pub(crate) use_hotpatch_engine: bool,
  55. pub(crate) automatic_rebuilds: bool,
  56. pub(crate) interactive: bool,
  57. pub(crate) _force_sequential: bool,
  58. pub(crate) hot_reload: bool,
  59. pub(crate) open_browser: bool,
  60. pub(crate) _wsl_file_poll_interval: u16,
  61. pub(crate) always_on_top: bool,
  62. pub(crate) fullstack: bool,
  63. pub(crate) watch_fs: bool,
  64. // resolve args related to the webserver
  65. pub(crate) devserver_port: u16,
  66. pub(crate) devserver_bind_ip: IpAddr,
  67. pub(crate) proxied_port: Option<u16>,
  68. pub(crate) cross_origin_policy: bool,
  69. // Additional plugin-type tools
  70. pub(crate) tw_watcher: tokio::task::JoinHandle<Result<()>>,
  71. }
  72. pub(crate) struct CachedFile {
  73. contents: String,
  74. most_recent: Option<String>,
  75. templates: HashMap<TemplateGlobalKey, HotReloadedTemplate>,
  76. }
  77. impl AppServer {
  78. /// Create the AppRunner and then initialize the filemap with the crate directory.
  79. pub(crate) async fn start(args: ServeArgs) -> Result<Self> {
  80. let workspace = Workspace::current().await?;
  81. // Resolve the simpler args
  82. let interactive = args.is_interactive_tty();
  83. let force_sequential = args.force_sequential;
  84. let cross_origin_policy = args.cross_origin_policy;
  85. // These come from the args but also might come from the workspace settings
  86. // We opt to use the manually specified args over the workspace settings
  87. let hot_reload = args
  88. .hot_reload
  89. .unwrap_or_else(|| workspace.settings.always_hot_reload.unwrap_or(true));
  90. let open_browser = args
  91. .open
  92. .unwrap_or_else(|| workspace.settings.always_open_browser.unwrap_or_default());
  93. let wsl_file_poll_interval = args
  94. .wsl_file_poll_interval
  95. .unwrap_or_else(|| workspace.settings.wsl_file_poll_interval.unwrap_or(2));
  96. let always_on_top = args
  97. .always_on_top
  98. .unwrap_or_else(|| workspace.settings.always_on_top.unwrap_or(true));
  99. // Use 127.0.0.1 as the default address if none is specified.
  100. // If the user wants to export on the network, they can use `0.0.0.0` instead.
  101. let devserver_bind_ip = args.address.addr.unwrap_or(WebServer::SELF_IP);
  102. // If the user specified a port, use that, otherwise use any available port, preferring 8080
  103. let devserver_port = args
  104. .address
  105. .port
  106. .unwrap_or_else(|| get_available_port(devserver_bind_ip, Some(8080)).unwrap_or(8080));
  107. // Spin up the file watcher
  108. let (watcher_tx, watcher_rx) = futures_channel::mpsc::unbounded();
  109. let watcher = create_notify_watcher(watcher_tx.clone(), wsl_file_poll_interval as u64);
  110. let BuildTargets { client, server } = args.targets.into_targets().await?;
  111. // All servers will end up behind us (the devserver) but on a different port
  112. // This is so we can serve a loading screen as well as devtools without anything particularly fancy
  113. let fullstack = server.is_some();
  114. let should_proxy_port = match client.platform {
  115. Platform::Server => true,
  116. _ => fullstack,
  117. };
  118. let proxied_port = should_proxy_port
  119. .then(|| get_available_port(devserver_bind_ip, None))
  120. .flatten();
  121. let watch_fs = args.watch.unwrap_or(true);
  122. let use_hotpatch_engine = args.hot_patch;
  123. let build_mode = match use_hotpatch_engine {
  124. true => BuildMode::Fat,
  125. false => BuildMode::Base,
  126. };
  127. let client = AppBuilder::start(&client, build_mode.clone())?;
  128. let server = server
  129. .map(|server| AppBuilder::start(&server, build_mode))
  130. .transpose()?;
  131. let tw_watcher = TailwindCli::serve(
  132. client.build.package_manifest_dir(),
  133. client.build.config.application.tailwind_input.clone(),
  134. client.build.config.application.tailwind_output.clone(),
  135. );
  136. _ = client.build.start_simulators().await;
  137. // Encourage the user to update to a new dx version
  138. crate::update::log_if_cli_could_update();
  139. // Create the runner
  140. let mut runner = Self {
  141. file_map: Default::default(),
  142. applied_hot_reload_message: Default::default(),
  143. automatic_rebuilds: true,
  144. watch_fs,
  145. use_hotpatch_engine,
  146. client,
  147. server,
  148. hot_reload,
  149. open_browser,
  150. _wsl_file_poll_interval: wsl_file_poll_interval,
  151. always_on_top,
  152. workspace,
  153. devserver_port,
  154. devserver_bind_ip,
  155. proxied_port,
  156. watcher,
  157. watcher_rx,
  158. _watcher_tx: watcher_tx,
  159. interactive,
  160. _force_sequential: force_sequential,
  161. cross_origin_policy,
  162. fullstack,
  163. tw_watcher,
  164. };
  165. // Only register the hot-reload stuff if we're watching the filesystem
  166. if runner.watch_fs {
  167. // Spin up the notify watcher
  168. // When builds load though, we're going to parse their depinfo and add the paths to the watcher
  169. runner.watch_filesystem();
  170. // todo(jon): this might take a while so we should try and background it, or make it lazy somehow
  171. // we could spawn a thread to search the FS and then when it returns we can fill the filemap
  172. // in testing, if this hits a massive directory, it might take several seconds with no feedback.
  173. // really, we should be using depinfo to get the files that are actually used, but the depinfo file might not be around yet
  174. // todo(jon): see if we can just guess the depinfo file before it generates. might be stale but at least it catches most of the files
  175. runner.load_rsx_filemap();
  176. }
  177. Ok(runner)
  178. }
  179. pub(crate) async fn wait(&mut self) -> ServeUpdate {
  180. let client = &mut self.client;
  181. let server = self.server.as_mut();
  182. let client_wait = client.wait();
  183. let server_wait = OptionFuture::from(server.map(|s| s.wait()));
  184. let watcher_wait = self.watcher_rx.next();
  185. tokio::select! {
  186. // Wait for the client to finish
  187. client_update = client_wait => {
  188. ServeUpdate::BuilderUpdate {
  189. id: BuildId::CLIENT,
  190. update: client_update,
  191. }
  192. }
  193. Some(server_update) = server_wait => {
  194. ServeUpdate::BuilderUpdate {
  195. id: BuildId::SERVER,
  196. update: server_update,
  197. }
  198. }
  199. // Wait for the watcher to send us an event
  200. event = watcher_wait => {
  201. let mut changes: Vec<_> = event.into_iter().collect();
  202. // Dequeue in bulk if we can, we might've received a lot of events in one go
  203. while let Some(event) = self.watcher_rx.try_next().ok().flatten() {
  204. changes.push(event);
  205. }
  206. // Filter the changes
  207. let mut files: Vec<PathBuf> = vec![];
  208. // Decompose the events into a list of all the files that have changed
  209. for event in changes.drain(..) {
  210. // Make sure we add new folders to the watch list, provided they're not matched by the ignore list
  211. // We'll only watch new folders that are found under the crate, and then update our watcher to watch them
  212. // This unfortunately won't pick up new krates added "at a distance" - IE krates not within the workspace.
  213. if let EventKind::Create(_create_kind) = event.kind {
  214. // If it's a new folder, watch it
  215. // If it's a new cargo.toml (ie dep on the fly),
  216. // todo(jon) support new folders on the fly
  217. }
  218. for path in event.paths {
  219. // Workaround for notify and vscode-like editor:
  220. // - when edit & save a file in vscode, there will be two notifications,
  221. // - the first one is a file with empty content.
  222. // - filter the empty file notification to avoid false rebuild during hot-reload
  223. if let Ok(metadata) = std::fs::metadata(&path) {
  224. if metadata.len() == 0 {
  225. continue;
  226. }
  227. }
  228. files.push(path);
  229. }
  230. }
  231. ServeUpdate::FilesChanged { files }
  232. }
  233. }
  234. }
  235. /// Handle the list of changed files from the file watcher, attempting to aggressively prevent
  236. /// full rebuilds by hot-reloading RSX and hot-patching Rust code.
  237. ///
  238. /// This will also handle any assets that are linked in the files, and copy them to the bundle
  239. /// and send them to the client.
  240. pub(crate) async fn handle_file_change(&mut self, files: &[PathBuf], server: &mut WebServer) {
  241. // We can attempt to hotpatch if the build is in a bad state, since this patch might be a recovery.
  242. if !matches!(
  243. self.client.stage,
  244. BuildStage::Failed | BuildStage::Aborted | BuildStage::Success
  245. ) {
  246. tracing::debug!(
  247. "Ignoring file change: client is not ready to receive hotreloads. Files: {:#?}",
  248. files
  249. );
  250. return;
  251. }
  252. // If we have any changes to the rust files, we need to update the file map
  253. let mut templates = vec![];
  254. // Prepare the hotreload message we need to send
  255. let mut assets = Vec::new();
  256. let mut needs_full_rebuild = false;
  257. // We attempt to hotreload rsx blocks without a full rebuild
  258. for path in files {
  259. // for various assets that might be linked in, we just try to hotreloading them forcefully
  260. // That is, unless they appear in an include! macro, in which case we need to a full rebuild....
  261. let Some(ext) = path.extension().and_then(|v| v.to_str()) else {
  262. continue;
  263. };
  264. // If it's an asset, we want to hotreload it
  265. // todo(jon): don't hardcode this here
  266. if let Some(bundled_name) = self.client.hotreload_bundled_asset(path).await {
  267. assets.push(PathBuf::from("/assets/").join(bundled_name));
  268. }
  269. // If it's a rust file, we want to hotreload it using the filemap
  270. if ext == "rs" {
  271. // And grabout the contents
  272. let Ok(new_contents) = std::fs::read_to_string(path) else {
  273. tracing::debug!("Failed to read rust file while hotreloading: {:?}", path);
  274. continue;
  275. };
  276. // Get the cached file if it exists - ignoring if it doesn't exist
  277. let Some(cached_file) = self.file_map.get_mut(path) else {
  278. tracing::debug!("No entry for file in filemap: {:?}", path);
  279. tracing::debug!("Filemap: {:#?}", self.file_map.keys());
  280. continue;
  281. };
  282. let Ok(local_path) = path.strip_prefix(self.workspace.workspace_root()) else {
  283. tracing::debug!("Skipping file outside workspace dir: {:?}", path);
  284. continue;
  285. };
  286. // We assume we can parse the old file and the new file, ignoring untracked rust files
  287. let old_syn = syn::parse_file(&cached_file.contents);
  288. let new_syn = syn::parse_file(&new_contents);
  289. let (Ok(old_file), Ok(new_file)) = (old_syn, new_syn) else {
  290. tracing::debug!("Diff rsx returned not parseable");
  291. continue;
  292. };
  293. // Update the most recent version of the file, so when we force a rebuild, we keep operating on the most recent version
  294. cached_file.most_recent = Some(new_contents);
  295. // This assumes the two files are structured similarly. If they're not, we can't diff them
  296. let Some(changed_rsx) = dioxus_rsx_hotreload::diff_rsx(&new_file, &old_file) else {
  297. needs_full_rebuild = true;
  298. break;
  299. };
  300. for ChangedRsx { old, new } in changed_rsx {
  301. let old_start = old.span().start();
  302. let old_parsed = syn::parse2::<CallBody>(old.tokens);
  303. let new_parsed = syn::parse2::<CallBody>(new.tokens);
  304. let (Ok(old_call_body), Ok(new_call_body)) = (old_parsed, new_parsed) else {
  305. continue;
  306. };
  307. // Format the template location, normalizing the path
  308. let file_name: String = local_path
  309. .components()
  310. .map(|c| c.as_os_str().to_string_lossy())
  311. .collect::<Vec<_>>()
  312. .join("/");
  313. // Returns a list of templates that are hotreloadable
  314. let results = HotReloadResult::new::<HtmlCtx>(
  315. &old_call_body.body,
  316. &new_call_body.body,
  317. file_name.clone(),
  318. );
  319. // If no result is returned, we can't hotreload this file and need to keep the old file
  320. let Some(results) = results else {
  321. needs_full_rebuild = true;
  322. break;
  323. };
  324. // Only send down templates that have roots, and ideally ones that have changed
  325. // todo(jon): maybe cache these and don't send them down if they're the same
  326. for (index, template) in results.templates {
  327. if template.roots.is_empty() {
  328. continue;
  329. }
  330. // Create the key we're going to use to identify this template
  331. let key = TemplateGlobalKey {
  332. file: file_name.clone(),
  333. line: old_start.line,
  334. column: old_start.column + 1,
  335. index,
  336. };
  337. // if the template is the same, don't send its
  338. if cached_file.templates.get(&key) == Some(&template) {
  339. continue;
  340. };
  341. cached_file.templates.insert(key.clone(), template.clone());
  342. templates.push(HotReloadTemplateWithLocation { template, key });
  343. }
  344. }
  345. }
  346. }
  347. // todo - we need to distinguish between hotpatchable rebuilds and true full rebuilds.
  348. // A full rebuild is required when the user modifies static initializers which we haven't wired up yet.
  349. if needs_full_rebuild {
  350. if self.use_hotpatch_engine {
  351. self.client.patch_rebuild(files.to_vec());
  352. if let Some(server) = self.server.as_mut() {
  353. server.patch_rebuild(files.to_vec());
  354. }
  355. self.clear_hot_reload_changes();
  356. self.clear_cached_rsx();
  357. server.send_patch_start().await;
  358. } else {
  359. self.client.start_rebuild(BuildMode::Base);
  360. if let Some(server) = self.server.as_mut() {
  361. server.start_rebuild(BuildMode::Base);
  362. }
  363. self.clear_hot_reload_changes();
  364. self.clear_cached_rsx();
  365. server.send_reload_start().await;
  366. }
  367. } else {
  368. let msg = HotReloadMsg {
  369. templates,
  370. assets,
  371. ms_elapsed: 0,
  372. jump_table: Default::default(),
  373. for_build_id: None,
  374. for_pid: None,
  375. };
  376. self.add_hot_reload_message(&msg);
  377. let file = files[0].display().to_string();
  378. let file =
  379. file.trim_start_matches(&self.client.build.crate_dir().display().to_string());
  380. // Only send a hotreload message for templates and assets - otherwise we'll just get a full rebuild
  381. //
  382. // todo: move the android file uploading out of hotreload_bundled_asset and
  383. //
  384. // Also make sure the builder isn't busy since that might cause issues with hotreloads
  385. // https://github.com/DioxusLabs/dioxus/issues/3361
  386. if !msg.is_empty() && self.client.can_receive_hotreloads() {
  387. use crate::styles::NOTE_STYLE;
  388. tracing::info!(dx_src = ?TraceSrc::Dev, "Hotreloading: {NOTE_STYLE}{}{NOTE_STYLE:#}", file);
  389. if !server.has_hotreload_sockets() && self.client.build.platform != Platform::Web {
  390. tracing::warn!("No clients to hotreload - try reloading the app!");
  391. }
  392. server.send_hotreload(msg).await;
  393. } else {
  394. tracing::debug!(dx_src = ?TraceSrc::Dev, "Ignoring file change: {}", file);
  395. }
  396. }
  397. }
  398. /// Finally "bundle" this app and return a handle to it
  399. pub(crate) async fn open(
  400. &mut self,
  401. artifacts: BuildArtifacts,
  402. devserver: &mut WebServer,
  403. ) -> Result<()> {
  404. // Make sure to save artifacts regardless of if we're opening the app or not
  405. match artifacts.platform {
  406. Platform::Server => {
  407. if let Some(server) = self.server.as_mut() {
  408. server.artifacts = Some(artifacts.clone());
  409. }
  410. }
  411. _ => self.client.artifacts = Some(artifacts.clone()),
  412. }
  413. let should_open = self.client.stage == BuildStage::Success
  414. && (self.server.as_ref().map(|s| s.stage == BuildStage::Success)).unwrap_or(true);
  415. use crate::cli::styles::GLOW_STYLE;
  416. if should_open {
  417. let time_taken = artifacts
  418. .time_end
  419. .duration_since(artifacts.time_start)
  420. .unwrap();
  421. if self.client.builds_opened == 0 {
  422. tracing::info!(
  423. "Build completed successfully in {GLOW_STYLE}{:?}ms{GLOW_STYLE:#}, launching app! 💫",
  424. time_taken.as_millis()
  425. );
  426. } else {
  427. tracing::info!(
  428. "Build completed in {GLOW_STYLE}{:?}ms{GLOW_STYLE:#}",
  429. time_taken.as_millis()
  430. );
  431. }
  432. let open_browser = self.client.builds_opened == 0 && self.open_browser;
  433. self.open_all(devserver, open_browser).await?;
  434. // Give a second for the server to boot
  435. tokio::time::sleep(Duration::from_millis(300)).await;
  436. // Update the screen + devserver with the new handle info
  437. devserver.send_reload_command().await
  438. }
  439. Ok(())
  440. }
  441. /// Open an existing app bundle, if it exists
  442. ///
  443. /// Will attempt to open the server and client together, in a coordinated way such that the server
  444. /// opens first, initializes, and then the client opens.
  445. ///
  446. /// There's a number of issues we need to be careful to work around:
  447. /// - The server failing to boot or crashing on startup (and entering a boot loop)
  448. /// -
  449. pub(crate) async fn open_all(
  450. &mut self,
  451. devserver: &WebServer,
  452. open_browser: bool,
  453. ) -> Result<()> {
  454. let devserver_ip = devserver.devserver_address();
  455. let fullstack_address = devserver.proxied_server_address();
  456. let displayed_address = devserver.displayed_address();
  457. // Always open the server first after the client has been built
  458. if let Some(server) = self.server.as_mut() {
  459. tracing::debug!("Opening server build");
  460. server.soft_kill().await;
  461. server
  462. .open(
  463. devserver_ip,
  464. displayed_address,
  465. fullstack_address,
  466. false,
  467. false,
  468. BuildId::SERVER,
  469. )
  470. .await?;
  471. }
  472. // Start the new app before we kill the old one to give it a little bit of time
  473. self.client.soft_kill().await;
  474. self.client
  475. .open(
  476. devserver_ip,
  477. displayed_address,
  478. fullstack_address,
  479. open_browser,
  480. self.always_on_top,
  481. BuildId::CLIENT,
  482. )
  483. .await?;
  484. Ok(())
  485. }
  486. /// Shutdown all the running processes
  487. pub(crate) async fn shutdown(&mut self) -> Result<()> {
  488. self.client.soft_kill().await;
  489. if let Some(server) = self.server.as_mut() {
  490. server.soft_kill().await;
  491. }
  492. // If the client is running on Android, we need to remove the port forwarding
  493. // todo: use the android tools "adb"
  494. if matches!(self.client.build.platform, Platform::Android) {
  495. if let Err(err) = Command::new(&self.workspace.android_tools()?.adb)
  496. .arg("reverse")
  497. .arg("--remove")
  498. .arg(format!("tcp:{}", self.devserver_port))
  499. .output()
  500. .await
  501. {
  502. tracing::error!(
  503. "failed to remove forwarded port {}: {err}",
  504. self.devserver_port
  505. );
  506. }
  507. }
  508. // force the tailwind watcher to stop - if we don't, it eats our stdin
  509. self.tw_watcher.abort();
  510. Ok(())
  511. }
  512. /// Perform a full rebuild of the app, equivalent to `cargo rustc` from scratch with no incremental
  513. /// hot-patch engine integration.
  514. pub(crate) async fn full_rebuild(&mut self) {
  515. let build_mode = match self.use_hotpatch_engine {
  516. true => BuildMode::Fat,
  517. false => BuildMode::Base,
  518. };
  519. self.client.start_rebuild(build_mode.clone());
  520. if let Some(s) = self.server.as_mut() {
  521. s.start_rebuild(build_mode)
  522. }
  523. self.clear_hot_reload_changes();
  524. self.clear_cached_rsx();
  525. self.clear_patches();
  526. }
  527. pub(crate) async fn hotpatch(
  528. &mut self,
  529. res: &BuildArtifacts,
  530. id: BuildId,
  531. cache: &HotpatchModuleCache,
  532. ) -> Result<JumpTable> {
  533. let jump_table = match id {
  534. BuildId::CLIENT => self.client.hotpatch(res, cache).await,
  535. BuildId::SERVER => {
  536. self.server
  537. .as_mut()
  538. .context("Server not found")?
  539. .hotpatch(res, cache)
  540. .await
  541. }
  542. _ => return Err(Error::Runtime("Invalid build id".into())),
  543. }?;
  544. if id == BuildId::CLIENT {
  545. self.applied_hot_reload_message.jump_table = self.client.patches.last().cloned();
  546. }
  547. Ok(jump_table)
  548. }
  549. pub(crate) fn get_build(&self, id: BuildId) -> Option<&AppBuilder> {
  550. match id {
  551. BuildId::CLIENT => Some(&self.client),
  552. BuildId::SERVER => self.server.as_ref(),
  553. _ => None,
  554. }
  555. }
  556. pub(crate) fn client(&self) -> &AppBuilder {
  557. &self.client
  558. }
  559. /// The name of the app being served, to display
  560. pub(crate) fn app_name(&self) -> &str {
  561. self.client.build.executable_name()
  562. }
  563. /// Get any hot reload changes that have been applied since the last full rebuild
  564. pub(crate) fn applied_hot_reload_changes(&mut self, build: BuildId) -> HotReloadMsg {
  565. let mut msg = self.applied_hot_reload_message.clone();
  566. if build == BuildId::CLIENT {
  567. msg.jump_table = self.client.patches.last().cloned();
  568. msg.for_build_id = Some(BuildId::CLIENT.0 as _);
  569. if let Some(lib) = msg.jump_table.as_mut() {
  570. lib.lib = PathBuf::from("/").join(lib.lib.clone());
  571. }
  572. }
  573. if build == BuildId::SERVER {
  574. if let Some(server) = self.server.as_mut() {
  575. msg.jump_table = server.patches.last().cloned();
  576. msg.for_build_id = Some(BuildId::SERVER.0 as _);
  577. }
  578. }
  579. msg
  580. }
  581. /// Clear the hot reload changes. This should be called any time a new build is starting
  582. pub(crate) fn clear_hot_reload_changes(&mut self) {
  583. self.applied_hot_reload_message = Default::default();
  584. }
  585. pub(crate) fn clear_patches(&mut self) {
  586. self.client.patches.clear();
  587. if let Some(server) = self.server.as_mut() {
  588. server.patches.clear();
  589. }
  590. }
  591. pub(crate) async fn client_connected(
  592. &mut self,
  593. build_id: BuildId,
  594. aslr_reference: Option<u64>,
  595. pid: Option<u32>,
  596. ) {
  597. match build_id {
  598. BuildId::CLIENT => {
  599. // multiple tabs on web can cause this to be called incorrectly, and it doesn't
  600. // make any sense anyways
  601. if self.client.build.platform != Platform::Web {
  602. if let Some(aslr_reference) = aslr_reference {
  603. self.client.aslr_reference = Some(aslr_reference);
  604. }
  605. if let Some(pid) = pid {
  606. self.client.pid = Some(pid);
  607. }
  608. }
  609. }
  610. BuildId::SERVER => {
  611. if let Some(server) = self.server.as_mut() {
  612. server.aslr_reference = aslr_reference;
  613. }
  614. }
  615. _ => {}
  616. }
  617. // Assign the runtime asset dir to the runner
  618. if self.client.build.platform == Platform::Ios {
  619. // xcrun simctl get_app_container booted com.dioxuslabs
  620. let res = Command::new("xcrun")
  621. .arg("simctl")
  622. .arg("get_app_container")
  623. .arg("booted")
  624. .arg(self.client.build.bundle_identifier())
  625. .output()
  626. .await;
  627. if let Ok(res) = res {
  628. tracing::trace!("Using runtime asset dir: {:?}", res);
  629. if let Ok(out) = String::from_utf8(res.stdout) {
  630. let out = out.trim();
  631. tracing::trace!("Setting Runtime asset dir: {out:?}");
  632. self.client.runtime_asset_dir = Some(PathBuf::from(out));
  633. }
  634. }
  635. }
  636. }
  637. /// Store the hot reload changes for any future clients that connect
  638. fn add_hot_reload_message(&mut self, msg: &HotReloadMsg) {
  639. let applied = &mut self.applied_hot_reload_message;
  640. // Merge the assets, unknown files, and templates
  641. // We keep the newer change if there is both a old and new change
  642. let mut templates: HashMap<TemplateGlobalKey, _> = std::mem::take(&mut applied.templates)
  643. .into_iter()
  644. .map(|template| (template.key.clone(), template))
  645. .collect();
  646. let mut assets: HashSet<PathBuf> =
  647. std::mem::take(&mut applied.assets).into_iter().collect();
  648. for template in &msg.templates {
  649. templates.insert(template.key.clone(), template.clone());
  650. }
  651. assets.extend(msg.assets.iter().cloned());
  652. applied.templates = templates.into_values().collect();
  653. applied.assets = assets.into_iter().collect();
  654. applied.jump_table = self.client.patches.last().cloned();
  655. }
  656. /// Register the files from the workspace into our file watcher.
  657. ///
  658. /// This very simply looks for all Rust files in the workspace and adds them to the filemap.
  659. ///
  660. /// Once the builds complete we'll use the depinfo files to get the actual files that are used,
  661. /// making our watcher more accurate. Filling the filemap here is intended to catch any file changes
  662. /// in between the first build and the depinfo file being generated.
  663. ///
  664. /// We don't want watch any registry files since that generally causes a huge performance hit -
  665. /// we mostly just care about workspace files and local dependencies.
  666. ///
  667. /// Dep-info file background:
  668. /// https://doc.rust-lang.org/stable/nightly-rustc/cargo/core/compiler/fingerprint/index.html#dep-info-files
  669. fn load_rsx_filemap(&mut self) {
  670. self.fill_filemap_from_krate(self.client.build.crate_dir());
  671. if let Some(server) = self.server.as_ref() {
  672. self.fill_filemap_from_krate(server.build.crate_dir());
  673. }
  674. for krate in self.all_watched_crates() {
  675. self.fill_filemap_from_krate(krate);
  676. }
  677. }
  678. /// Fill the filemap with files from the filesystem, using the given filter to determine which files to include.
  679. ///
  680. /// You can use the filter with something like a gitignore to only include files that are relevant to your project.
  681. /// We'll walk the filesystem from the given path and recursively search for all files that match the filter.
  682. ///
  683. /// The filter function takes a path and returns true if the file should be included in the filemap.
  684. /// Generally this will only be .rs files
  685. ///
  686. /// If a file couldn't be parsed, we don't fail. Instead, we save the error.
  687. ///
  688. /// todo: There are known bugs here when handling gitignores.
  689. fn fill_filemap_from_krate(&mut self, crate_dir: PathBuf) {
  690. for entry in walkdir::WalkDir::new(crate_dir).into_iter().flatten() {
  691. if self
  692. .workspace
  693. .ignore
  694. .matched(entry.path(), entry.file_type().is_dir())
  695. .is_ignore()
  696. {
  697. continue;
  698. }
  699. let path = entry.path();
  700. if path.extension().and_then(|s| s.to_str()) == Some("rs") {
  701. if let Ok(contents) = std::fs::read_to_string(path) {
  702. self.file_map.insert(
  703. path.to_path_buf(),
  704. CachedFile {
  705. contents,
  706. most_recent: None,
  707. templates: Default::default(),
  708. },
  709. );
  710. }
  711. }
  712. }
  713. }
  714. /// Commit the changes to the filemap, overwriting the contents of the files
  715. ///
  716. /// Removes any cached templates and replaces the contents of the files with the most recent
  717. ///
  718. /// todo: we should-reparse the contents so we never send a new version, ever
  719. fn clear_cached_rsx(&mut self) {
  720. for cached_file in self.file_map.values_mut() {
  721. if let Some(most_recent) = cached_file.most_recent.take() {
  722. cached_file.contents = most_recent;
  723. }
  724. cached_file.templates.clear();
  725. }
  726. }
  727. fn watch_filesystem(&mut self) {
  728. // Watch the folders of the crates that we're interested in
  729. for path in self.watch_paths(
  730. self.client.build.crate_dir(),
  731. self.client.build.crate_package,
  732. ) {
  733. tracing::trace!("Watching path {path:?}");
  734. if let Err(err) = self.watcher.watch(&path, RecursiveMode::Recursive) {
  735. handle_notify_error(err);
  736. }
  737. }
  738. if let Some(server) = self.server.as_ref() {
  739. // Watch the server's crate directory as well
  740. for path in self.watch_paths(server.build.crate_dir(), server.build.crate_package) {
  741. tracing::trace!("Watching path {path:?}");
  742. if let Err(err) = self.watcher.watch(&path, RecursiveMode::Recursive) {
  743. handle_notify_error(err);
  744. }
  745. }
  746. }
  747. // Also watch the crates themselves, but not recursively, such that we can pick up new folders
  748. for krate in self.all_watched_crates() {
  749. tracing::trace!("Watching path {krate:?}");
  750. if let Err(err) = self.watcher.watch(&krate, RecursiveMode::NonRecursive) {
  751. handle_notify_error(err);
  752. }
  753. }
  754. // Also watch the workspace dir, non recursively, such that we can pick up new folders there too
  755. if let Err(err) = self.watcher.watch(
  756. self.workspace.krates.workspace_root().as_std_path(),
  757. RecursiveMode::NonRecursive,
  758. ) {
  759. handle_notify_error(err);
  760. }
  761. }
  762. /// Return the list of paths that we should watch for changes.
  763. fn watch_paths(&self, crate_dir: PathBuf, crate_package: NodeId) -> Vec<PathBuf> {
  764. let mut watched_paths = vec![];
  765. // Get a list of *all* the crates with Rust code that we need to watch.
  766. // This will end up being dependencies in the workspace and non-workspace dependencies on the user's computer.
  767. let mut watched_crates = self.local_dependencies(crate_package);
  768. watched_crates.push(crate_dir);
  769. // Now, watch all the folders in the crates, but respecting their respective ignore files
  770. for krate_root in watched_crates {
  771. // Build the ignore builder for this crate, but with our default ignore list as well
  772. let ignore = self.workspace.ignore_for_krate(&krate_root);
  773. for entry in krate_root.read_dir().into_iter().flatten() {
  774. let Ok(entry) = entry else {
  775. continue;
  776. };
  777. if ignore
  778. .matched(entry.path(), entry.path().is_dir())
  779. .is_ignore()
  780. {
  781. continue;
  782. }
  783. watched_paths.push(entry.path().to_path_buf());
  784. }
  785. }
  786. watched_paths.dedup();
  787. watched_paths
  788. }
  789. /// Get all the Manifest paths for dependencies that we should watch. Will not return anything
  790. /// in the `.cargo` folder - only local dependencies will be watched.
  791. ///
  792. /// This returns a list of manifest paths
  793. ///
  794. /// Extend the watch path to include:
  795. ///
  796. /// - the assets directory - this is so we can hotreload CSS and other assets by default
  797. /// - the Cargo.toml file - this is so we can hotreload the project if the user changes dependencies
  798. /// - the Dioxus.toml file - this is so we can hotreload the project if the user changes the Dioxus config
  799. fn local_dependencies(&self, crate_package: NodeId) -> Vec<PathBuf> {
  800. let mut paths = vec![];
  801. for (dependency, _edge) in self.workspace.krates.get_deps(crate_package) {
  802. let krate = match dependency {
  803. krates::Node::Krate { krate, .. } => krate,
  804. krates::Node::Feature { krate_index, .. } => {
  805. &self.workspace.krates[krate_index.index()]
  806. }
  807. };
  808. if krate
  809. .manifest_path
  810. .components()
  811. .any(|c| c.as_str() == ".cargo")
  812. {
  813. continue;
  814. }
  815. paths.push(
  816. krate
  817. .manifest_path
  818. .parent()
  819. .unwrap()
  820. .to_path_buf()
  821. .into_std_path_buf(),
  822. );
  823. }
  824. paths
  825. }
  826. // todo: we need to make sure we merge this for all the running packages
  827. fn all_watched_crates(&self) -> Vec<PathBuf> {
  828. let crate_package = self.client().build.crate_package;
  829. let crate_dir = self.client().build.crate_dir();
  830. let mut krates: Vec<PathBuf> = self
  831. .local_dependencies(crate_package)
  832. .into_iter()
  833. .map(|p| {
  834. p.parent()
  835. .expect("Local manifest to exist and have a parent")
  836. .to_path_buf()
  837. })
  838. .chain(Some(crate_dir))
  839. .collect();
  840. if let Some(server) = self.server.as_ref() {
  841. let server_crate_package = server.build.crate_package;
  842. let server_crate_dir = server.build.crate_dir();
  843. let server_krates: Vec<PathBuf> = self
  844. .local_dependencies(server_crate_package)
  845. .into_iter()
  846. .map(|p| {
  847. p.parent()
  848. .expect("Server manifest to exist and have a parent")
  849. .to_path_buf()
  850. })
  851. .chain(Some(server_crate_dir))
  852. .collect();
  853. krates.extend(server_krates);
  854. }
  855. krates.dedup();
  856. krates
  857. }
  858. /// Check if this is a fullstack build. This means that there is an additional build with the `server` platform.
  859. pub(crate) fn is_fullstack(&self) -> bool {
  860. self.fullstack
  861. }
  862. /// Return a number between 0 and 1 representing the progress of the server build
  863. pub(crate) fn server_compile_progress(&self) -> f64 {
  864. let Some(server) = self.server.as_ref() else {
  865. return 0.0;
  866. };
  867. server.compiled_crates as f64 / server.expected_crates as f64
  868. }
  869. pub(crate) async fn open_debugger(&mut self, dev: &WebServer, build: BuildId) {
  870. if self.use_hotpatch_engine {
  871. tracing::warn!("Debugging symbols might not work properly with hotpatching enabled. Consider disabling hotpatching for debugging.");
  872. }
  873. match build {
  874. BuildId::CLIENT => {
  875. _ = self.client.open_debugger(dev).await;
  876. }
  877. BuildId::SERVER => {
  878. if let Some(server) = self.server.as_mut() {
  879. _ = server.open_debugger(dev).await;
  880. }
  881. }
  882. _ => {}
  883. }
  884. }
  885. }
  886. /// Bind a listener to any point and return it
  887. /// When the listener is dropped, the socket will be closed, but we'll still have a port that we
  888. /// can bind our proxy to.
  889. ///
  890. /// Todo: we might want to do this on every new build in case the OS tries to bind things to this port
  891. /// and we don't already have something bound to it. There's no great way of "reserving" a port.
  892. fn get_available_port(address: IpAddr, prefer: Option<u16>) -> Option<u16> {
  893. // First, try to bind to the preferred port
  894. if let Some(port) = prefer {
  895. if let Ok(_listener) = TcpListener::bind((address, port)) {
  896. return Some(port);
  897. }
  898. }
  899. // Otherwise, try to bind to any port and return the first one we can
  900. TcpListener::bind((address, 0))
  901. .and_then(|listener| listener.local_addr().map(|f| f.port()))
  902. .ok()
  903. }
  904. fn create_notify_watcher(
  905. tx: UnboundedSender<notify::Event>,
  906. wsl_poll_interval: u64,
  907. ) -> Box<dyn NotifyWatcher> {
  908. // Build the event handler for notify.
  909. // This has been known to be a source of many problems, unfortunately, since notify handling seems to be flakey across platforms
  910. let handler = move |info: notify::Result<notify::Event>| {
  911. let Ok(event) = info else {
  912. return;
  913. };
  914. let is_allowed_notify_event = match event.kind {
  915. EventKind::Modify(ModifyKind::Data(_)) => true,
  916. EventKind::Modify(ModifyKind::Name(_)) => true,
  917. // The primary modification event on WSL's poll watcher.
  918. EventKind::Modify(ModifyKind::Metadata(MetadataKind::WriteTime)) => true,
  919. // Catch-all for unknown event types (windows)
  920. EventKind::Modify(ModifyKind::Any) => true,
  921. EventKind::Modify(ModifyKind::Metadata(_)) => false,
  922. // Don't care about anything else.
  923. EventKind::Create(_) => true,
  924. EventKind::Remove(_) => true,
  925. _ => false,
  926. };
  927. if is_allowed_notify_event {
  928. _ = tx.unbounded_send(event);
  929. }
  930. };
  931. const NOTIFY_ERROR_MSG: &str = "Failed to create file watcher.\nEnsure you have the required permissions to watch the specified directories.";
  932. // On wsl, we need to poll the filesystem for changes
  933. if is_wsl() {
  934. return Box::new(
  935. notify::PollWatcher::new(
  936. handler,
  937. Config::default().with_poll_interval(Duration::from_secs(wsl_poll_interval)),
  938. )
  939. .expect(NOTIFY_ERROR_MSG),
  940. );
  941. }
  942. // Otherwise we can use the recommended watcher
  943. Box::new(notify::recommended_watcher(handler).expect(NOTIFY_ERROR_MSG))
  944. }
  945. fn handle_notify_error(err: notify::Error) {
  946. tracing::debug!("Failed to watch path: {}", err);
  947. match err.kind {
  948. notify::ErrorKind::Io(error) if error.kind() == std::io::ErrorKind::PermissionDenied => {
  949. tracing::error!("Failed to watch path: permission denied. {:?}", err.paths)
  950. }
  951. notify::ErrorKind::MaxFilesWatch => {
  952. tracing::error!("Failed to set up file watcher: too many files to watch")
  953. }
  954. _ => {}
  955. }
  956. }
  957. /// Detects if `dx` is being ran in a WSL environment.
  958. ///
  959. /// We determine this based on whether the keyword `microsoft` or `wsl` is contained within the [`WSL_1`] or [`WSL_2`] files.
  960. /// This may fail in the future as it isn't guaranteed by Microsoft.
  961. /// See https://github.com/microsoft/WSL/issues/423#issuecomment-221627364
  962. fn is_wsl() -> bool {
  963. const WSL_1: &str = "/proc/sys/kernel/osrelease";
  964. const WSL_2: &str = "/proc/version";
  965. const WSL_KEYWORDS: [&str; 2] = ["microsoft", "wsl"];
  966. // Test 1st File
  967. if let Ok(content) = std::fs::read_to_string(WSL_1) {
  968. let lowercase = content.to_lowercase();
  969. for keyword in WSL_KEYWORDS {
  970. if lowercase.contains(keyword) {
  971. return true;
  972. }
  973. }
  974. }
  975. // Test 2nd File
  976. if let Ok(content) = std::fs::read_to_string(WSL_2) {
  977. let lowercase = content.to_lowercase();
  978. for keyword in WSL_KEYWORDS {
  979. if lowercase.contains(keyword) {
  980. return true;
  981. }
  982. }
  983. }
  984. false
  985. }