fix hotreloading issues in the CLI
This commit is contained in:
37 files changed
+818
-805
No files matched your search
@@ -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
@@ -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: ¬ify::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",
|
||||
@@ -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 {
|
||||
|
||||
@@ -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?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user