use crate::{
builder,
serve::Serve,
server::{
output::{print_console_info, PrettierOptions, WebServerInfo},
setup_file_watcher, setup_file_watcher_hot_reload,
},
BuildResult, CrateConfig, Result,
};
use axum::{
body::{Full, HttpBody},
extract::{ws::Message, Extension, TypedHeader, WebSocketUpgrade},
http::{
header::{HeaderName, HeaderValue},
Method, Response, StatusCode,
},
response::IntoResponse,
routing::{get, get_service},
Router,
};
use axum_server::tls_rustls::RustlsConfig;
use dioxus_html::HtmlCtx;
use dioxus_rsx::hot_reload::*;
use std::{
net::UdpSocket,
process::Command,
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 plugin::PluginManager;
mod proxy;
mod hot_reload;
use hot_reload::*;
struct WsReloadState {
update: broadcast::Sender<()>,
}
pub async fn startup(port: u16, config: CrateConfig, start_browser: bool) -> Result<()> {
let _crate_config = config.clone();
let _ = ctrlc::set_handler(move || {
#[cfg(feature = "plugin")]
let _ = PluginManager::on_serve_shutdown(&_crate_config);
std::process::exit(0);
});
let ip = get_ip().unwrap_or(String::from("0.0.0.0"));
match config.hot_reload {
true => serve_hot_reload(ip, port, config, start_browser).await?,
false => serve_default(ip, port, config, start_browser).await?,
}
Ok(())
}
pub async fn serve_default(
ip: String,
port: u16,
config: CrateConfig,
start_browser: bool,
) -> Result<()> {
let first_build_result = crate::builder::build(&config, false)?;
log::info!("🚀 Starting development server...");
let (reload_tx, _) = broadcast::channel(100);
let _watcher = setup_file_watcher(
{
let config = config.clone();
let reload_tx = reload_tx.clone();
move || build(&config, &reload_tx)
},
&config,
Some(WebServerInfo {
ip: ip.clone(),
port,
}),
)
.await?;
let ws_reload_state = Arc::new(WsReloadState {
update: reload_tx.clone(),
});
let rustls_config = get_rustls(&config).await?;
print_console_info(
&config,
PrettierOptions {
changed: vec![],
warnings: first_build_result.warnings,
elapsed_time: first_build_result.elapsed_time,
},
Some(crate::server::output::WebServerInfo {
ip: ip.clone(),
port,
}),
);
let router = setup_router(config, ws_reload_state, None).await?;
start_server(port, router, start_browser, rustls_config).await?;
Ok(())
}
pub async fn serve_hot_reload(
ip: String,
port: u16,
config: CrateConfig,
start_browser: bool,
) -> Result<()> {
let first_build_result = crate::builder::build(&config, false)?;
log::info!("🚀 Starting development server...");
let (reload_tx, _) = broadcast::channel(100);
let FileMapBuildResult { map, errors } =
FileMap::<HtmlCtx>::create(config.crate_dir.clone()).unwrap();
for err in errors {
log::error!("{}", err);
}
let file_map = Arc::new(Mutex::new(map));
let hot_reload_tx = broadcast::channel(100).0;
let hot_reload_state = Arc::new(HotReloadState {
messages: hot_reload_tx.clone(),
file_map: file_map.clone(),
watcher_config: config.clone(),
});
let ws_reload_state = Arc::new(WsReloadState {
update: reload_tx.clone(),
});
let _watcher = setup_file_watcher_hot_reload(
&config,
hot_reload_tx,
file_map,
{
let config = config.clone();
let reload_tx = reload_tx.clone();
move || build(&config, &reload_tx)
},
Some(WebServerInfo {
ip: ip.clone(),
port,
}),
)
.await?;
let rustls_config = get_rustls(&config).await?;
print_console_info(
&config,
PrettierOptions {
changed: vec![],
warnings: first_build_result.warnings,
elapsed_time: first_build_result.elapsed_time,
},
Some(WebServerInfo {
ip: ip.clone(),
port,
}),
);
let router = setup_router(config, ws_reload_state, Some(hot_reload_state)).await?;
start_server(port, router, start_browser, rustls_config).await?;
Ok(())
}
const DEFAULT_KEY_PATH: &str = "ssl/key.pem";
const DEFAULT_CERT_PATH: &str = "ssl/cert.pem";
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) = match web_config.mkcert {
Some(true) => {
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());
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()?;
}
}
(cert_path, key_path)
}
Some(false) => {
if let (Some(key), Some(cert)) =
(web_config.key_path.clone(), web_config.cert_path.clone())
{
(cert, key)
} else {
return Err("https is enabled but cert or key path is missing".into());
}
}
_ => return Ok(None),
};
Ok(Some(
RustlsConfig::from_pem_file(cert_path, key_path).await?,
))
}
async fn setup_router(
config: CrateConfig,
ws_reload: Arc<WsReloadState>,
hot_reload: Option<Arc<HotReloadState>>,
) -> Result<Router> {
let cors = CorsLayer::new()
.allow_methods([Method::GET, Method::POST])
.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"),
)
};
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 response = if file_service_config
.dioxus_config
.web
.watcher
.index_on_404
.unwrap_or(false)
&& response.status() == StatusCode::NOT_FOUND
{
let body = Full::from(
std::fs::read_to_string(
file_service_config
.crate_dir
.join(file_service_config.out_dir)
.join("index.html"),
)
.ok()
.unwrap(),
)
.map_err(|err| match err {})
.boxed();
Response::builder()
.status(StatusCode::OK)
.body(body)
.unwrap()
} else {
response.map(|body| body.boxed())
};
Ok(response)
},
)
.service(ServeDir::new(config.crate_dir.join(&config.out_dir)));
let mut router = Router::new().route("/_dioxus/ws", get(ws_handler));
for proxy_config in config.dioxus_config.web.proxy.unwrap_or_default() {
router = proxy::add_proxy(router, &proxy_config)?;
}
router = router.fallback(get_service(file_service).handle_error(
|error: std::io::Error| async move {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Unhandled internal error: {}", error),
)
},
));
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)
}
async fn start_server(
port: u16,
router: Router,
start_browser: bool,
rustls: Option<RustlsConfig>,
) -> Result<()> {
#[cfg(feature = "plugin")]
PluginManager::on_serve_start(&config)?;
let addr = format!("0.0.0.0:{}", port).parse().unwrap();
if start_browser {
match rustls {
Some(_) => _ = open::that(format!("https://{}", addr)),
None => _ = open::that(format!("http://{}", addr)),
}
}
match rustls {
Some(rustls) => {
axum_server::bind_rustls(addr, rustls)
.serve(router.into_make_service())
.await?
}
None => {
axum::Server::bind(&addr)
.serve(router.into_make_service())
.await?
}
}
Ok(())
}
fn get_ip() -> Option<String> {
let socket = match UdpSocket::bind("0.0.0.0:0") {
Ok(s) => s,
Err(_) => return None,
};
match socket.connect("8.8.8.8:80") {
Ok(()) => (),
Err(_) => return None,
};
match socket.local_addr() {
Ok(addr) => Some(addr.ip().to_string()),
Err(_) => None,
}
}
async fn ws_handler(
ws: WebSocketUpgrade,
_: Option<TypedHeader<headers::UserAgent>>,
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();
if socket
.send(Message::Text(String::from("reload")))
.await
.is_err()
{
break;
}
rx = rx.resubscribe();
}
});
reload_watcher.await.unwrap();
})
}
fn build(config: &CrateConfig, reload_tx: &Sender<()>) -> Result<BuildResult> {
let result = builder::build(config, true)?;
if config
.dioxus_config
.web
.watcher
.reload_html
.unwrap_or(false)
{
let _ = Serve::regen_dev_page(config);
}
let _ = reload_tx.send(());
Ok(result)
}