fix hotreloading issues in the CLI

This commit is contained in:
Jonathan Kelley committed 2024-03-12 13:39:42 -07:00
1 parent d180f569cf
commit ad7a350d2e
37 files changed
+818 -805

No files matched your search

+18 -15
View File
@@ -1,20 +1,18 @@
use crate::server::Platform;
use crate::{
cfg::ConfigOptsServe,
server::{
output::{print_console_info, PrettierOptions},
setup_file_watcher,
setup_file_watcher, Platform,
},
BuildResult, Result,
};
use dioxus_cli_config::CrateConfig;
use dioxus_cli_config::{CrateConfig, ExecutableType};
use dioxus_hot_reload::HotReloadMsg;
use dioxus_html::HtmlCtx;
use dioxus_rsx::hot_reload::*;
use interprocess_docfix::local_socket::LocalSocketListener;
use std::fs::create_dir_all;
use interprocess::local_socket::LocalSocketListener;
use std::{
fs::create_dir_all,
process::{Child, Command},
sync::{Arc, Mutex, RwLock},
};
@@ -33,13 +31,7 @@ pub(crate) async fn startup_with_platform<P: Platform + Send + 'static>(
config: CrateConfig,
serve_cfg: &ConfigOptsServe,
) -> Result<()> {
// ctrl-c shutdown checker
let _crate_config = config.clone();
let _ = ctrlc::set_handler(move || {
#[cfg(feature = "plugin")]
let _ = PluginManager::on_serve_shutdown(&_crate_config);
std::process::exit(0);
});
set_ctrl_c(&config);
let hot_reload_state = match config.hot_reload {
true => {
@@ -67,6 +59,16 @@ pub(crate) async fn startup_with_platform<P: Platform + Send + 'static>(
Ok(())
}
fn set_ctrl_c(config: &CrateConfig) {
// ctrl-c shutdown checker
let _crate_config = config.clone();
let _ = ctrlc::set_handler(move || {
#[cfg(feature = "plugin")]
let _ = PluginManager::on_serve_shutdown(&_crate_config);
std::process::exit(0);
});
}
/// Start the server without hot reload
async fn serve<P: Platform + Send + 'static>(
config: CrateConfig,
@@ -169,12 +171,13 @@ async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<()
let mut hot_reload_rx = hot_reload_state.messages.subscribe();
while let Ok(template) = hot_reload_rx.recv().await {
while let Ok(msg) = hot_reload_rx.recv().await {
let channels = &mut *channels.lock().unwrap();
let mut i = 0;
while i < channels.len() {
let channel = &mut channels[i];
if send_msg(HotReloadMsg::UpdateTemplate(template), channel) {
if send_msg(msg.clone(), channel) {
i += 1;
} else {
channels.remove(i);
+191 -112
View File
@@ -3,10 +3,14 @@ use dioxus_cli_config::CrateConfig;
use cargo_metadata::diagnostic::Diagnostic;
use dioxus_core::Template;
use dioxus_hot_reload::HotReloadMsg;
use dioxus_html::HtmlCtx;
use dioxus_rsx::hot_reload::*;
use notify::{RecommendedWatcher, Watcher};
use std::sync::{Arc, Mutex};
use std::{
path::PathBuf,
sync::{Arc, Mutex},
};
use tokio::sync::broadcast::{self};
mod output;
@@ -15,7 +19,15 @@ pub mod desktop;
pub mod fullstack;
pub mod web;
/// Sets up a file watcher
#[derive(Clone)]
pub struct HotReloadState {
pub messages: broadcast::Sender<HotReloadMsg>,
pub file_map: Arc<Mutex<FileMap<HtmlCtx>>>,
}
/// Sets up a file watcher.
///
/// Will attempt to hotreload HTML, RSX (.rs), and CSS
async fn setup_file_watcher<F: Fn() -> Result<BuildResult> + Send + 'static>(
build_with: F,
config: &CrateConfig,
@@ -25,124 +37,184 @@ async fn setup_file_watcher<F: Fn() -> Result<BuildResult> + Send + 'static>(
let mut last_update_time = chrono::Local::now().timestamp();
// file watcher: check file change
let allow_watch_path = config.dioxus_config.web.watcher.watch_path.clone();
let mut allow_watch_path = config.dioxus_config.web.watcher.watch_path.clone();
let watcher_config = config.clone();
let mut watcher = notify::recommended_watcher(move |info: notify::Result<notify::Event>| {
let config = watcher_config.clone();
if let Ok(e) = info {
match e.kind {
notify::EventKind::Create(_)
| notify::EventKind::Remove(_)
| notify::EventKind::Modify(_) => {
if chrono::Local::now().timestamp() > last_update_time {
let mut needs_full_rebuild;
if let Some(hot_reload) = &hot_reload {
// find changes to the rsx in the file
let mut rsx_file_map = hot_reload.file_map.lock().unwrap();
let mut messages: Vec<Template> = Vec::new();
// Extend the watch path to include the assets directory
allow_watch_path.push(config.dioxus_config.application.asset_dir.clone());
// In hot reload mode, we only need to rebuild if non-rsx code is changed
needs_full_rebuild = false;
// Create the file watcher
let mut watcher = notify::recommended_watcher({
let watcher_config = config.clone();
move |info: notify::Result<notify::Event>| {
let Ok(e) = info else {
return;
};
for path in &e.paths {
// if this is not a rust file, rebuild the whole project
let path_extension = path.extension().and_then(|p| p.to_str());
if path_extension != Some("rs") {
needs_full_rebuild = true;
// if backup file generated will impact normal hot-reload, so ignore it
if path_extension == Some("rs~") {
needs_full_rebuild = false;
}
break;
}
// Workaround for notify and vscode-like editor:
// when edit & save a file in vscode, there will be two notifications,
// the first one is a file with empty content.
// filter the empty file notification to avoid false rebuild during hot-reload
if let Ok(metadata) = fs::metadata(path) {
if metadata.len() == 0 {
continue;
}
}
match rsx_file_map.update_rsx(path, &config.crate_dir) {
Ok(UpdateResult::UpdatedRsx(msgs)) => {
messages.extend(msgs);
needs_full_rebuild = false;
}
Ok(UpdateResult::NeedsRebuild) => {
needs_full_rebuild = true;
}
Err(err) => {
log::error!("{}", err);
}
}
}
if needs_full_rebuild {
// Reset the file map to the new state of the project
let FileMapBuildResult {
map: new_file_map,
errors,
} = FileMap::<HtmlCtx>::create(config.crate_dir.clone()).unwrap();
for err in errors {
log::error!("{}", err);
}
*rsx_file_map = new_file_map;
} else {
for msg in messages {
let _ = hot_reload.messages.send(msg);
}
}
} else {
needs_full_rebuild = true;
}
if needs_full_rebuild {
match build_with() {
Ok(res) => {
last_update_time = chrono::Local::now().timestamp();
#[allow(clippy::redundant_clone)]
print_console_info(
&config,
PrettierOptions {
changed: e.paths.clone(),
warnings: res.warnings,
elapsed_time: res.elapsed_time,
},
web_info.clone(),
);
}
Err(e) => {
last_update_time = chrono::Local::now().timestamp();
log::error!("{:?}", e);
}
}
}
}
}
_ => {}
}
watch_event(
e,
&mut last_update_time,
&hot_reload,
&watcher_config,
&build_with,
&web_info,
);
}
})
.unwrap();
.expect("Failed to create file watcher - please ensure you have the required permissions to watch the specified directories.");
// Watch the specified paths
for sub_path in allow_watch_path {
if let Err(err) = watcher.watch(
&config.crate_dir.join(sub_path),
notify::RecursiveMode::Recursive,
) {
let path = &config.crate_dir.join(sub_path);
let mode = notify::RecursiveMode::Recursive;
if let Err(err) = watcher.watch(path, mode) {
log::warn!("Failed to watch path: {}", err);
}
}
Ok(watcher)
}
fn watch_event<F>(
event: notify::Event,
last_update_time: &mut i64,
hot_reload: &Option<HotReloadState>,
config: &CrateConfig,
build_with: &F,
web_info: &Option<WebServerInfo>,
) where
F: Fn() -> Result<BuildResult> + Send + 'static,
{
// Ensure that we're tracking only modifications
if !matches!(
event.kind,
notify::EventKind::Create(_) | notify::EventKind::Remove(_) | notify::EventKind::Modify(_)
) {
return;
}
// Ensure that we're not rebuilding too frequently
if chrono::Local::now().timestamp() <= *last_update_time {
return;
}
// By default we want to opt into a full rebuild, but hotreloading will actually set this force us
let mut needs_full_rebuild = true;
if let Some(hot_reload) = &hot_reload {
hotreload_files(hot_reload, &mut needs_full_rebuild, &event, &config);
}
if needs_full_rebuild {
full_rebuild(build_with, last_update_time, config, event, web_info);
}
}
fn full_rebuild<F>(
build_with: &F,
last_update_time: &mut i64,
config: &CrateConfig,
event: notify::Event,
web_info: &Option<WebServerInfo>,
) where
F: Fn() -> Result<BuildResult> + Send + 'static,
{
match build_with() {
Ok(res) => {
*last_update_time = chrono::Local::now().timestamp();
#[allow(clippy::redundant_clone)]
print_console_info(
&config,
PrettierOptions {
changed: event.paths.clone(),
warnings: res.warnings,
elapsed_time: res.elapsed_time,
},
web_info.clone(),
);
}
Err(e) => {
*last_update_time = chrono::Local::now().timestamp();
log::error!("{:?}", e);
}
}
}
fn hotreload_files(
hot_reload: &HotReloadState,
needs_full_rebuild: &mut bool,
event: &notify::Event,
config: &CrateConfig,
) {
// find changes to the rsx in the file
let mut rsx_file_map = hot_reload.file_map.lock().unwrap();
let mut messages: Vec<HotReloadMsg> = Vec::new();
// In hot reload mode, we only need to rebuild if non-rsx code is changed
*needs_full_rebuild = false;
for path in &event.paths {
// for various assets that might be linked in, we just try to hotreloading them forcefully
// That is, unless they appear in an include! macro, in which case we need to a full rebuild....
// if this is not a rust file, rebuild the whole project
let path_extension = path.extension().and_then(|p| p.to_str());
if path_extension != Some("rs") {
*needs_full_rebuild = true;
if path_extension == Some("rs~") {
*needs_full_rebuild = false;
}
continue;
}
// Workaround for notify and vscode-like editor:
// when edit & save a file in vscode, there will be two notifications,
// the first one is a file with empty content.
// filter the empty file notification to avoid false rebuild during hot-reload
if let Ok(metadata) = fs::metadata(path) {
if metadata.len() == 0 {
continue;
}
}
match rsx_file_map.update_rsx(path, &config.crate_dir) {
Ok(UpdateResult::UpdatedRsx(msgs)) => {
messages.extend(
msgs.into_iter()
.map(|msg| HotReloadMsg::UpdateTemplate(msg)),
);
*needs_full_rebuild = false;
}
Ok(UpdateResult::NeedsRebuild) => {
*needs_full_rebuild = true;
}
Err(err) => {
log::error!("{}", err);
}
}
}
if *needs_full_rebuild {
// Reset the file map to the new state of the project
let FileMapBuildResult {
map: new_file_map,
errors,
} = FileMap::<HtmlCtx>::create(config.crate_dir.clone()).unwrap();
for err in errors {
log::error!("{}", err);
}
*rsx_file_map = new_file_map;
} else {
for msg in messages {
let _ = hot_reload.messages.send(msg);
}
}
}
pub(crate) trait Platform {
fn start(config: &CrateConfig, serve: &ConfigOptsServe) -> Result<Self>
where
@@ -150,8 +222,15 @@ pub(crate) trait Platform {
fn rebuild(&mut self, config: &CrateConfig) -> Result<BuildResult>;
}
#[derive(Clone)]
pub struct HotReloadState {
pub messages: broadcast::Sender<Template>,
pub file_map: Arc<Mutex<FileMap<HtmlCtx>>>,
}
// Some("bin") => "application/octet-stream",
// Some("css") => "text/css",
// Some("csv") => "text/csv",
// Some("html") => "text/html",
// Some("ico") => "image/vnd.microsoft.icon",
// Some("js") => "text/javascript",
// Some("json") => "application/json",
// Some("jsonld") => "application/ld+json",
// Some("mjs") => "text/javascript",
// Some("rtf") => "application/rtf",
// Some("svg") => "image/svg+xml",
// Some("mp4") => "video/mp4",
+2 -4
View File
@@ -1,9 +1,7 @@
use crate::server::Diagnostic;
use colored::Colorize;
use dioxus_cli_config::crate_root;
use dioxus_cli_config::CrateConfig;
use std::path::PathBuf;
use std::process::Command;
use dioxus_cli_config::{crate_root, CrateConfig};
use std::{path::PathBuf, process::Command};
#[derive(Debug, Default)]
pub struct PrettierOptions {
+47 -40
View File
@@ -1,54 +1,61 @@
use crate::server::HotReloadState;
use axum::{
extract::{ws::Message, WebSocketUpgrade},
extract::{
ws::{Message, WebSocket},
WebSocketUpgrade,
},
response::IntoResponse,
Extension,
};
use dioxus_hot_reload::HotReloadMsg;
pub async fn hot_reload_handler(
ws: WebSocketUpgrade,
Extension(state): Extension<HotReloadState>,
) -> impl IntoResponse {
ws.on_upgrade(|mut socket| async move {
log::info!("🔥 Hot Reload WebSocket connected");
{
// update any rsx calls that changed before the websocket connected.
{
log::info!("🔮 Finding updates since last compile...");
let templates: Vec<_> = {
state
.file_map
.lock()
.unwrap()
.map
.values()
.filter_map(|(_, template_slot)| *template_slot)
.collect()
};
for template in templates {
if socket
.send(Message::Text(serde_json::to_string(&template).unwrap()))
.await
.is_err()
{
return;
}
}
}
log::info!("finished");
}
ws.on_upgrade(|socket| async move {
let err = hotreload_loop(socket, state).await;
let mut rx = state.messages.subscribe();
loop {
if let Ok(rsx) = rx.recv().await {
if socket
.send(Message::Text(serde_json::to_string(&rsx).unwrap()))
.await
.is_err()
{
break;
};
}
if let Err(err) = err {
log::error!("Hotreload receiver failed: {}", err);
}
})
}
async fn hotreload_loop(mut socket: WebSocket, state: HotReloadState) -> anyhow::Result<()> {
log::info!("🔥 Hot Reload WebSocket connected");
// update any rsx calls that changed before the websocket connected.
log::info!("🔮 Finding updates since last compile...");
let templates = state
.file_map
.lock()
.unwrap()
.map
.values()
.filter_map(|(_, template_slot)| *template_slot)
.collect::<Vec<_>>();
for template in templates {
socket
.send(Message::Text(serde_json::to_string(&template).unwrap()))
.await?;
}
let mut rx = state.messages.subscribe();
loop {
if let Ok(msg) = rx.recv().await {
let msg = match msg {
HotReloadMsg::UpdateTemplate(template) => {
Message::Text(serde_json::to_string(&template).unwrap())
}
HotReloadMsg::UpdateAsset(_) => todo!(),
HotReloadMsg::Shutdown => todo!(),
};
socket.send(msg).await?;
}
}
}
+72 -311
View File
@@ -1,5 +1,6 @@
use crate::{
builder,
cfg::ConfigOptsServe,
serve::Serve,
server::{
output::{print_console_info, PrettierOptions, WebServerInfo},
@@ -7,111 +8,51 @@ use crate::{
},
BuildResult, Result,
};
use axum::{
body::Body,
extract::{ws::Message, Extension, WebSocketUpgrade},
http::{
self,
header::{HeaderName, HeaderValue},
Method, Response, StatusCode,
},
response::IntoResponse,
routing::{get, get_service},
Router,
};
use axum_server::tls_rustls::RustlsConfig;
use dioxus_cli_config::CrateConfig;
use dioxus_cli_config::WebHttpsConfig;
use dioxus_html::HtmlCtx;
use dioxus_rsx::hot_reload::*;
use std::{
net::UdpSocket,
process::Command,
net::{SocketAddr, UdpSocket},
sync::{Arc, Mutex},
};
use tokio::sync::broadcast::{self, Sender};
use tower::ServiceBuilder;
use tower_http::services::fs::{ServeDir, ServeFileSystemResponseBody};
use tower_http::{
cors::{Any, CorsLayer},
ServiceBuilderExt,
};
#[cfg(feature = "plugin")]
use crate::plugin::PluginManager;
mod proxy;
use tokio::sync::broadcast;
mod hot_reload;
use hot_reload::*;
mod proxy;
mod server;
struct WsReloadState {
use server::*;
pub struct WsReloadState {
update: broadcast::Sender<()>,
}
pub async fn startup(
port: u16,
config: CrateConfig,
start_browser: bool,
skip_assets: bool,
) -> Result<()> {
// ctrl-c shutdown checker
let _crate_config = config.clone();
let _ = ctrlc::set_handler(move || {
#[cfg(feature = "plugin")]
let _ = PluginManager::on_serve_shutdown(&_crate_config);
std::process::exit(0);
});
pub async fn startup(config: CrateConfig, serve_cfg: &ConfigOptsServe) -> Result<()> {
set_ctrlc_handler(&config);
let ip = get_ip().unwrap_or(String::from("0.0.0.0"));
let hot_reload_state = match config.hot_reload {
true => {
let FileMapBuildResult { map, errors } =
FileMap::<HtmlCtx>::create(config.crate_dir.clone()).unwrap();
let mut hot_reload_state = None;
for err in errors {
log::error!("{}", err);
}
if config.hot_reload {
hot_reload_state = Some(build_hotreload_filemap(&config));
}
let file_map = Arc::new(Mutex::new(map));
let hot_reload_tx = broadcast::channel(100).0;
Some(HotReloadState {
messages: hot_reload_tx.clone(),
file_map: file_map.clone(),
})
}
false => None,
};
serve(
ip,
port,
config,
start_browser,
skip_assets,
hot_reload_state,
)
.await?;
Ok(())
serve(ip, config, hot_reload_state, serve_cfg).await
}
/// Start the server without hot reload
pub async fn serve(
ip: String,
port: u16,
config: CrateConfig,
start_browser: bool,
skip_assets: bool,
hot_reload_state: Option<HotReloadState>,
opts: &ConfigOptsServe,
) -> Result<()> {
let skip_assets = opts.skip_assets;
let port = opts.port;
// Since web platform doesn't use `rust_flags`, this argument is explicitly
// set to `None`.
let first_build_result = crate::builder::build(&config, false, skip_assets, None)?;
let first_build_result = crate::builder::build(&config, skip_assets, None)?;
// generate dev-index page
Serve::regen_dev_page(&config, first_build_result.assets.as_ref())?;
@@ -154,7 +95,7 @@ pub async fn serve(
warnings: first_build_result.warnings,
elapsed_time: first_build_result.elapsed_time,
},
Some(crate::server::output::WebServerInfo {
Some(WebServerInfo {
ip: ip.clone(),
port,
}),
@@ -164,230 +105,43 @@ pub async fn serve(
let router = setup_router(config.clone(), ws_reload_state, hot_reload_state).await?;
// Start server
start_server(port, router, start_browser, rustls_config, &config).await?;
start_server(port, router, opts.open, rustls_config, &config).await?;
Ok(())
}
const DEFAULT_KEY_PATH: &str = "ssl/key.pem";
const DEFAULT_CERT_PATH: &str = "ssl/cert.pem";
/// Returns an enum of rustls config and a bool if mkcert isn't installed
async fn get_rustls(config: &CrateConfig) -> Result<Option<RustlsConfig>> {
let web_config = &config.dioxus_config.web.https;
if web_config.enabled != Some(true) {
return Ok(None);
}
let (cert_path, key_path) = if let Some(true) = web_config.mkcert {
// mkcert, use it
get_rustls_with_mkcert(web_config)?
} else {
// if mkcert not specified or false, don't use it
get_rustls_without_mkcert(web_config)?
};
Ok(Some(
RustlsConfig::from_pem_file(cert_path, key_path).await?,
))
}
fn get_rustls_with_mkcert(web_config: &WebHttpsConfig) -> Result<(String, String)> {
// Get paths to store certs, otherwise use ssl/item.pem
let key_path = web_config
.key_path
.clone()
.unwrap_or(DEFAULT_KEY_PATH.to_string());
let cert_path = web_config
.cert_path
.clone()
.unwrap_or(DEFAULT_CERT_PATH.to_string());
// Create ssl directory if using defaults
if key_path == DEFAULT_KEY_PATH && cert_path == DEFAULT_CERT_PATH {
_ = fs::create_dir("ssl");
}
let cmd = Command::new("mkcert")
.args([
"-install",
"-key-file",
&key_path,
"-cert-file",
&cert_path,
"localhost",
"::1",
"127.0.0.1",
])
.spawn();
match cmd {
Err(e) => {
match e.kind() {
io::ErrorKind::NotFound => log::error!("mkcert is not installed. See https://github.com/FiloSottile/mkcert#installation for installation instructions."),
e => log::error!("an error occured while generating mkcert certificates: {}", e.to_string()),
};
return Err("failed to generate mkcert certificates".into());
}
Ok(mut cmd) => {
cmd.wait()?;
}
}
Ok((cert_path, key_path))
}
fn get_rustls_without_mkcert(web_config: &WebHttpsConfig) -> Result<(String, String)> {
// get paths to cert & key
if let (Some(key), Some(cert)) = (web_config.key_path.clone(), web_config.cert_path.clone()) {
Ok((cert, key))
} else {
// missing cert or key
Err("https is enabled but cert or key path is missing".into())
}
}
/// Sets up and returns a router
async fn setup_router(
config: CrateConfig,
ws_reload: Arc<WsReloadState>,
hot_reload: Option<HotReloadState>,
) -> Result<Router> {
// Setup cors
let cors = CorsLayer::new()
// allow `GET` and `POST` when accessing the resource
.allow_methods([Method::GET, Method::POST])
// allow requests from any origin
.allow_origin(Any)
.allow_headers(Any);
let (coep, coop) = if config.cross_origin_policy {
(
HeaderValue::from_static("require-corp"),
HeaderValue::from_static("same-origin"),
)
} else {
(
HeaderValue::from_static("unsafe-none"),
HeaderValue::from_static("unsafe-none"),
)
};
// Create file service
let file_service_config = config.clone();
let file_service = ServiceBuilder::new()
.override_response_header(
HeaderName::from_static("cross-origin-embedder-policy"),
coep,
)
.override_response_header(HeaderName::from_static("cross-origin-opener-policy"), coop)
.and_then(
move |response: Response<ServeFileSystemResponseBody>| async move {
let mut response = if file_service_config.dioxus_config.web.watcher.index_on_404
&& response.status() == StatusCode::NOT_FOUND
{
let body = Body::from(
// TODO: Cache/memoize this.
std::fs::read_to_string(file_service_config.out_dir().join("index.html"))
.ok()
.unwrap(),
);
Response::builder()
.status(StatusCode::OK)
.body(body)
.unwrap()
} else {
response.into_response()
};
let headers = response.headers_mut();
headers.insert(
http::header::CACHE_CONTROL,
HeaderValue::from_static("no-cache"),
);
headers.insert(http::header::PRAGMA, HeaderValue::from_static("no-cache"));
headers.insert(http::header::EXPIRES, HeaderValue::from_static("0"));
Ok(response)
},
)
.service(ServeDir::new(config.out_dir()));
// Setup websocket
let mut router = Router::new().route("/_dioxus/ws", get(ws_handler));
// Setup proxy
for proxy_config in config.dioxus_config.web.proxy {
router = proxy::add_proxy(router, &proxy_config)?;
}
// Route file service
router = router.fallback(get_service(file_service).handle_error(
|error: std::convert::Infallible| async move {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Unhandled internal error: {}", error),
)
},
));
router = if let Some(base_path) = config.dioxus_config.web.app.base_path.clone() {
let base_path = format!("/{}", base_path.trim_matches('/'));
Router::new()
.route(&base_path, axum::routing::any_service(router))
.fallback(get(move || {
let base_path = base_path.clone();
async move { format!("Outside of the base path: {}", base_path) }
}))
} else {
router
};
// Setup routes
router = router
.route("/_dioxus/hot_reload", get(hot_reload_handler))
.layer(cors)
.layer(Extension(ws_reload));
if let Some(hot_reload) = hot_reload {
router = router.layer(Extension(hot_reload))
}
Ok(router)
}
/// Starts dx serve with no hot reload
async fn start_server(
port: u16,
router: Router,
router: axum::Router,
start_browser: bool,
rustls: Option<RustlsConfig>,
rustls: Option<axum_server::tls_rustls::RustlsConfig>,
_config: &CrateConfig,
) -> Result<()> {
// If plugins, call on_serve_start event
#[cfg(feature = "plugin")]
PluginManager::on_serve_start(_config)?;
crate::plugin::PluginManager::on_serve_start(_config)?;
// Bind the server to `[::]` and it will LISTEN for both IPv4 and IPv6. (required IPv6 dual stack)
let addr = format!("[::]:{}", port).parse().unwrap();
let addr: SocketAddr = format!("0.0.0.0:{}", port).parse().unwrap();
// Open the browser
if start_browser {
match rustls {
Some(_) => _ = open::that(format!("https://{}", addr)),
None => _ = open::that(format!("http://{}", addr)),
Some(_) => _ = open::that(format!("https://localhost:{port}")),
None => _ = open::that(format!("http://localhost:{port}")),
}
}
let svc = router.into_make_service();
// Start the server with or without rustls
match rustls {
Some(rustls) => {
axum_server::bind_rustls(addr, rustls)
.serve(router.into_make_service())
.await?
}
Some(rustls) => axum_server::bind_rustls(addr, rustls).serve(svc).await?,
None => {
// Create a TCP listener bound to the address
let listener = tokio::net::TcpListener::bind(&addr).await?;
axum::serve(listener, router.into_make_service()).await?
axum::serve(listener, svc).await?
}
}
@@ -412,43 +166,50 @@ fn get_ip() -> Option<String> {
}
}
/// Handle websockets
async fn ws_handler(
ws: WebSocketUpgrade,
Extension(state): Extension<Arc<WsReloadState>>,
) -> impl IntoResponse {
ws.on_upgrade(|mut socket| async move {
let mut rx = state.update.subscribe();
let reload_watcher = tokio::spawn(async move {
loop {
rx.recv().await.unwrap();
// ignore the error
if socket
.send(Message::Text(String::from("reload")))
.await
.is_err()
{
break;
}
// flush the errors after recompling
rx = rx.resubscribe();
}
});
reload_watcher.await.unwrap();
})
}
fn build(config: &CrateConfig, reload_tx: &Sender<()>, skip_assets: bool) -> Result<BuildResult> {
fn build(
config: &CrateConfig,
reload_tx: &broadcast::Sender<()>,
skip_assets: bool,
) -> Result<BuildResult> {
// Since web platform doesn't use `rust_flags`, this argument is explicitly
// set to `None`.
let result = builder::build(config, true, skip_assets, None)?;
let result = std::panic::catch_unwind(|| builder::build(config, skip_assets, None))
.map_err(|e| anyhow::anyhow!("Build failed: {e:?}"))?;
// change the websocket reload state to true;
// the page will auto-reload.
if config.dioxus_config.web.watcher.reload_html {
let _ = Serve::regen_dev_page(config, result.assets.as_ref());
if let Ok(assets) = result.as_ref().map(|x| x.assets.as_ref()) {
let _ = Serve::regen_dev_page(config, assets);
}
}
let _ = reload_tx.send(());
Ok(result)
result
}
fn set_ctrlc_handler(config: &CrateConfig) {
// ctrl-c shutdown checker
let _crate_config = config.clone();
let _ = ctrlc::set_handler(move || {
#[cfg(feature = "plugin")]
let _ = crate::plugin::PluginManager::on_serve_shutdown(&_crate_config);
std::process::exit(0);
});
}
fn build_hotreload_filemap(config: &CrateConfig) -> HotReloadState {
let FileMapBuildResult { map, errors } = FileMap::create(config.crate_dir.clone()).unwrap();
for err in errors {
log::error!("{}", err);
}
HotReloadState {
messages: broadcast::channel(100).0.clone(),
file_map: Arc::new(Mutex::new(map)).clone(),
}
}
+254
View File
@@ -0,0 +1,254 @@
use std::{fs, io, process::Command, sync::Arc};
use crate::{
builder,
serve::Serve,
server::{
output::{print_console_info, PrettierOptions, WebServerInfo},
setup_file_watcher, HotReloadState,
},
BuildResult, Result,
};
use tower::ServiceBuilder;
use axum::{
body::Body,
extract::{
ws::{Message, WebSocket},
Extension, WebSocketUpgrade,
},
http::{
self,
header::{HeaderName, HeaderValue},
Method, Response, StatusCode,
},
response::IntoResponse,
routing::{get, get_service},
Router,
};
use axum_server::tls_rustls::RustlsConfig;
use dioxus_cli_config::{CrateConfig, WebHttpsConfig};
use super::{hot_reload::*, WsReloadState};
use tower_http::{
cors::{Any, CorsLayer},
services::fs::{ServeDir, ServeFileSystemResponseBody},
ServiceBuilderExt,
};
/// Sets up and returns a router
pub async fn setup_router(
config: CrateConfig,
ws_reload: Arc<WsReloadState>,
hot_reload: Option<HotReloadState>,
) -> Result<Router> {
// Setup cors
let cors = CorsLayer::new()
// allow `GET` and `POST` when accessing the resource
.allow_methods([Method::GET, Method::POST])
// allow requests from any origin
.allow_origin(Any)
.allow_headers(Any);
let (coep, coop) = if config.cross_origin_policy {
(
HeaderValue::from_static("require-corp"),
HeaderValue::from_static("same-origin"),
)
} else {
(
HeaderValue::from_static("unsafe-none"),
HeaderValue::from_static("unsafe-none"),
)
};
// Create file service
let file_service_config = config.clone();
let file_service = ServiceBuilder::new()
.override_response_header(
HeaderName::from_static("cross-origin-embedder-policy"),
coep,
)
.override_response_header(HeaderName::from_static("cross-origin-opener-policy"), coop)
.and_then(move |response| async move { Ok(no_cache(file_service_config, response)) })
.service(ServeDir::new(config.out_dir()));
// Setup websocket
let mut router = Router::new().route("/_dioxus/ws", get(ws_handler));
// Setup proxy
for proxy_config in config.dioxus_config.web.proxy {
router = super::proxy::add_proxy(router, &proxy_config)?;
}
// Route file service
router = router.fallback(get_service(file_service).handle_error(
|error: std::convert::Infallible| async move {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Unhandled internal error: {}", error),
)
},
));
router = if let Some(base_path) = config.dioxus_config.web.app.base_path.clone() {
let base_path = format!("/{}", base_path.trim_matches('/'));
Router::new()
.route(&base_path, axum::routing::any_service(router))
.fallback(get(move || {
let base_path = base_path.clone();
async move { format!("Outside of the base path: {}", base_path) }
}))
} else {
router
};
// Setup routes
router = router
.route("/_dioxus/hot_reload", get(hot_reload_handler))
.layer(cors)
.layer(Extension(ws_reload));
if let Some(hot_reload) = hot_reload {
router = router.layer(Extension(hot_reload))
}
Ok(router)
}
fn no_cache(
file_service_config: CrateConfig,
response: Response<ServeFileSystemResponseBody>,
) -> Response<Body> {
let mut response = if file_service_config.dioxus_config.web.watcher.index_on_404
&& response.status() == StatusCode::NOT_FOUND
{
let body = Body::from(
// TODO: Cache/memoize this.
std::fs::read_to_string(file_service_config.out_dir().join("index.html"))
.ok()
.unwrap(),
);
Response::builder()
.status(StatusCode::OK)
.body(body)
.unwrap()
} else {
response.into_response()
};
let headers = response.headers_mut();
headers.insert(
http::header::CACHE_CONTROL,
HeaderValue::from_static("no-cache"),
);
headers.insert(http::header::PRAGMA, HeaderValue::from_static("no-cache"));
headers.insert(http::header::EXPIRES, HeaderValue::from_static("0"));
response
}
/// Handle websockets
async fn ws_handler(
ws: WebSocketUpgrade,
Extension(state): Extension<Arc<WsReloadState>>,
) -> impl IntoResponse {
ws.on_upgrade(move |socket| ws_reload_handler(socket, state))
}
async fn ws_reload_handler(mut socket: WebSocket, state: Arc<WsReloadState>) {
let mut rx = state.update.subscribe();
let reload_watcher = tokio::spawn(async move {
loop {
rx.recv().await.unwrap();
let _ = socket.send(Message::Text(String::from("reload"))).await;
// ignore the error
println!("forcing reload");
// flush the errors after recompling
rx = rx.resubscribe();
}
});
reload_watcher.await.unwrap();
}
const DEFAULT_KEY_PATH: &str = "ssl/key.pem";
const DEFAULT_CERT_PATH: &str = "ssl/cert.pem";
/// Returns an enum of rustls config and a bool if mkcert isn't installed
pub async fn get_rustls(config: &CrateConfig) -> Result<Option<RustlsConfig>> {
let web_config = &config.dioxus_config.web.https;
if web_config.enabled != Some(true) {
return Ok(None);
}
let (cert_path, key_path) = if let Some(true) = web_config.mkcert {
// mkcert, use it
get_rustls_with_mkcert(web_config)?
} else {
// if mkcert not specified or false, don't use it
get_rustls_without_mkcert(web_config)?
};
Ok(Some(
RustlsConfig::from_pem_file(cert_path, key_path).await?,
))
}
pub fn get_rustls_with_mkcert(web_config: &WebHttpsConfig) -> Result<(String, String)> {
// Get paths to store certs, otherwise use ssl/item.pem
let key_path = web_config
.key_path
.clone()
.unwrap_or(DEFAULT_KEY_PATH.to_string());
let cert_path = web_config
.cert_path
.clone()
.unwrap_or(DEFAULT_CERT_PATH.to_string());
// Create ssl directory if using defaults
if key_path == DEFAULT_KEY_PATH && cert_path == DEFAULT_CERT_PATH {
_ = fs::create_dir("ssl");
}
let cmd = Command::new("mkcert")
.args([
"-install",
"-key-file",
&key_path,
"-cert-file",
&cert_path,
"localhost",
"::1",
"127.0.0.1",
])
.spawn();
match cmd {
Err(e) => {
match e.kind() {
io::ErrorKind::NotFound => log::error!("mkcert is not installed. See https://github.com/FiloSottile/mkcert#installation for installation instructions."),
e => log::error!("an error occured while generating mkcert certificates: {}", e.to_string()),
};
return Err("failed to generate mkcert certificates".into());
}
Ok(mut cmd) => {
cmd.wait()?;
}
}
Ok((cert_path, key_path))
}
pub fn get_rustls_without_mkcert(web_config: &WebHttpsConfig) -> Result<(String, String)> {
// get paths to cert & key
if let (Some(key), Some(cert)) = (web_config.key_path.clone(), web_config.cert_path.clone()) {
Ok((cert, key))
} else {
// missing cert or key
Err("https is enabled but cert or key path is missing".into())
}
}