web_dev_server.rs 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. // Copyright 2019-2023 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. use axum::{
  5. extract::{ws::WebSocket, WebSocketUpgrade},
  6. http::{header::CONTENT_TYPE, StatusCode, Uri},
  7. response::IntoResponse,
  8. routing::get,
  9. Router, Server,
  10. };
  11. use html5ever::{namespace_url, ns, LocalName, QualName};
  12. use kuchiki::{traits::TendrilSink, NodeRef};
  13. use notify::RecursiveMode;
  14. use notify_debouncer_mini::new_debouncer;
  15. use std::{
  16. net::{IpAddr, SocketAddr},
  17. path::{Path, PathBuf},
  18. sync::{mpsc::sync_channel, Arc},
  19. thread,
  20. time::Duration,
  21. };
  22. use tauri_utils::mime_type::MimeType;
  23. use tokio::sync::broadcast::{channel, Sender};
  24. const AUTO_RELOAD_SCRIPT: &str = include_str!("./auto-reload.js");
  25. struct State {
  26. serve_dir: PathBuf,
  27. address: SocketAddr,
  28. tx: Sender<()>,
  29. }
  30. pub fn start_dev_server<P: AsRef<Path>>(
  31. path: P,
  32. ip: IpAddr,
  33. port: Option<u16>,
  34. ) -> crate::Result<SocketAddr> {
  35. let serve_dir = path.as_ref().to_path_buf();
  36. let (server_url_tx, server_url_rx) = std::sync::mpsc::channel();
  37. std::thread::spawn(move || {
  38. tokio::runtime::Builder::new_current_thread()
  39. .enable_io()
  40. .build()
  41. .unwrap()
  42. .block_on(async move {
  43. let (tx, _) = channel(1);
  44. let tokio_tx = tx.clone();
  45. let serve_dir_ = serve_dir.clone();
  46. thread::spawn(move || {
  47. let (tx, rx) = sync_channel(1);
  48. let mut watcher = new_debouncer(Duration::from_secs(1), move |r| {
  49. if let Ok(events) = r {
  50. tx.send(events).unwrap()
  51. }
  52. })
  53. .unwrap();
  54. watcher
  55. .watcher()
  56. .watch(&serve_dir_, RecursiveMode::Recursive)
  57. .unwrap();
  58. loop {
  59. if rx.recv().is_ok() {
  60. let _ = tokio_tx.send(());
  61. }
  62. }
  63. });
  64. let mut auto_port = false;
  65. let mut port = port.unwrap_or_else(|| {
  66. auto_port = true;
  67. 1430
  68. });
  69. let (server, server_url) = loop {
  70. let server_url = SocketAddr::new(ip, port);
  71. let server = Server::try_bind(&server_url);
  72. if !auto_port {
  73. break (server, server_url);
  74. }
  75. if server.is_ok() {
  76. break (server, server_url);
  77. }
  78. port += 1;
  79. };
  80. let state = Arc::new(State {
  81. serve_dir,
  82. tx,
  83. address: server_url,
  84. });
  85. let state_ = state.clone();
  86. let router = Router::new()
  87. .fallback(move |uri| handler(uri, state_))
  88. .route(
  89. "/__tauri_cli",
  90. get(move |ws: WebSocketUpgrade| async move {
  91. ws.on_upgrade(|socket| async move { ws_handler(socket, state).await })
  92. }),
  93. );
  94. match server {
  95. Ok(server) => {
  96. server_url_tx.send(Ok(server_url)).unwrap();
  97. server.serve(router.into_make_service()).await.unwrap();
  98. }
  99. Err(e) => {
  100. server_url_tx
  101. .send(Err(anyhow::anyhow!(
  102. "failed to start development server on {server_url}: {e}"
  103. )))
  104. .unwrap();
  105. }
  106. }
  107. })
  108. });
  109. server_url_rx.recv().unwrap()
  110. }
  111. async fn handler(uri: Uri, state: Arc<State>) -> impl IntoResponse {
  112. let uri = uri.to_string();
  113. let uri = if uri == "/" {
  114. &uri
  115. } else {
  116. uri.strip_prefix('/').unwrap_or(&uri)
  117. };
  118. let file = std::fs::read(state.serve_dir.join(uri))
  119. .or_else(|_| std::fs::read(state.serve_dir.join(format!("{}.html", &uri))))
  120. .or_else(|_| std::fs::read(state.serve_dir.join(format!("{}/index.html", &uri))))
  121. .or_else(|_| std::fs::read(state.serve_dir.join("index.html")));
  122. file
  123. .map(|mut f| {
  124. let mime_type = MimeType::parse_with_fallback(&f, uri, MimeType::OctetStream);
  125. if mime_type == MimeType::Html.to_string() {
  126. let mut document = kuchiki::parse_html().one(String::from_utf8_lossy(&f).into_owned());
  127. fn with_html_head<F: FnOnce(&NodeRef)>(document: &mut NodeRef, f: F) {
  128. if let Ok(ref node) = document.select_first("head") {
  129. f(node.as_node())
  130. } else {
  131. let node = NodeRef::new_element(
  132. QualName::new(None, ns!(html), LocalName::from("head")),
  133. None,
  134. );
  135. f(&node);
  136. document.prepend(node)
  137. }
  138. }
  139. with_html_head(&mut document, |head| {
  140. let script_el =
  141. NodeRef::new_element(QualName::new(None, ns!(html), "script".into()), None);
  142. script_el.append(NodeRef::new_text(AUTO_RELOAD_SCRIPT.replace(
  143. "{{reload_url}}",
  144. &format!("ws://{}/__tauri_cli", state.address),
  145. )));
  146. head.prepend(script_el);
  147. });
  148. f = tauri_utils::html::serialize_node(&document);
  149. }
  150. (StatusCode::OK, [(CONTENT_TYPE, mime_type)], f)
  151. })
  152. .unwrap_or_else(|_| {
  153. (
  154. StatusCode::NOT_FOUND,
  155. [(CONTENT_TYPE, "text/plain".into())],
  156. vec![],
  157. )
  158. })
  159. }
  160. async fn ws_handler(mut ws: WebSocket, state: Arc<State>) {
  161. let mut rx = state.tx.subscribe();
  162. while tokio::select! {
  163. _ = ws.recv() => return,
  164. fs_reload_event = rx.recv() => fs_reload_event.is_ok(),
  165. } {
  166. let ws_send = ws.send(axum::extract::ws::Message::Text(
  167. r#"{"reload": true}"#.to_owned(),
  168. ));
  169. if ws_send.await.is_err() {
  170. break;
  171. }
  172. }
  173. }