Binary patching rust hot-reloading, sub-second rebuilds, independent server/client hot-reload (#3797)
Enable runtime rust hot patching. * roll back wry to 0.45 * cleanup rustc * wip * wip: fix client/server * get the subsecond cli thing working again * back to list of builds but with accessors * migrate some methods to workspace, clean up impl a bit * pass mode through build context * use a build context, similar to cargo/rustc * merge BuildUpdate and HandleUpdate into BuilderUpdate * Move more resolution of args into runner * migrate some methods to runner * hoist out fullstack * fix build request resolver * yay request is cleaned up * fixup resolution a bit * spawn off the build! * re-wire android autodetection * re-wire android autodetection * wire back up a few more things * re-wire tooling verification * I think it's mostly in the right condition for regular app * add depinfo parser * yay okay works with regular apps again * full rebuilds are back * rewire file wtcher * wire patch! * yayyyyyy patching works and is really fast * yay android works * clean up stdout piping * yayyy memap on android so we don't need root * create a static wasm jump table * wip: global offset table for wasm * wip... thinking about making the patches relocatable * wip: customized relocation * YESSSSSS RELOCATABLE WASM * clean up impl a bit * lil bit more cleanup * lil bit more * lil bit more cleanup * lil bit more cleanup, support 32 bit platforms * sick, wasm is completely pic * hmmmm not quite working yet * woooo, patches loading from rust+wasm * integrated into the cli 😎 * condense a bit * cleaned up request a bit * bust fingerprints * Make the file diffing logic a bit more sensible * still working through a few issues, server launches but is acting weird * fix merge conflict * more merge issues * remove stuff we don't want anymore * revert name change * wip: server not working anymore :( * split apart "dioxus fullstack" into "dioxus server" and "dioxus web" * fixup a few more compile errors * grrr opening server * simultaneous frontend and backend patching * use rustc wrapper * wip: passing assets to jumptable * migrate project-like examples * patchy patchy server fns! * rollback some random changes * unwind the js files * rollback hash * no need for patch * more cleanups * lil bit more cleanup, remove some old cruft * allow patching when wasm webpage is closed * tiny bit more robust * lil bit of clean up * clean up ws code * bit more clean ups * condense * undo file moves * move back other project * move other project * migrate out harness and janky CLI * fix compile * anonymize some files, heavily document others * lots more documentation, for posterity! * ton more docs * clean up the cli a bit more * more ws cleanup * more cleanup * migrate build id to cli-config * add command file handling * random small cleanups * wip.... * wip.... * fix: use workspace for krate discovery * fix panic logging on serve by placing it *after* the logs * reorder logging to be sensible on success * bring back swc * ws cruft * small patches to relocs in loader * bump krates * hoist main funcs into the ifunc table, fixing wasm * fix tui output for long lines * add more linker logging * wow, incredible, we build fat binaries, holy heckkkkk * fix args for frontend/backend * small cleanups * properly send along build IDs to clients * fix workspace compiles * clean up jump table work a bit * clean up logging a bit * open existing * fixup some commands * open existing browser integration * wire up the new dx serve client/server syntax * fixup `dx run` command * bring back some old functionality * fix serverfn feature sets * remove toast animation and simplify its code a bit * less intrusive toast * dont change build status when patching on web * add proper cache busting to vcomponent * clean up the patch file a bit * more lints/checks cleaned up * go back to TargetArgs but a BuildTargets resolved struct * clean up more nits * use an atomicptr for jumptable * fix interaction with initial web load + suspense * don't run asset system on wasm/bindgen js in dev * reduce blast radius of PR * use profile to determine if we're in release * cleanup prod html template * cleanup profiles * fix feature resolution of fullstack * if fullstack is explicitly false, don't use it * light cleanups * drop carg config2 * pass along linker args properly * make workspace examples compile with dx again * fewer unwraps and better error loggign * bit more error handlign * small cleanup * drive-by cleanups * use queries instead of initialize for aslr stuff * fix hotpatch with higher opt level * fix aslr bug from multiple clients * fix merge conflict * fix typos * clippy * fix miscompile * fix doctest * properly rollback wry to 0.45 * fix markdown path issues, other test issues * fix test in router * fix release mode test * fix some more tests, clean up a few more items * use fnptr instead of typeid * clean up memozation via subsecond in core * use "main" as sentinel * fix imports and re-exports * get off __rust_alloc for aslr offset detection * wip * wip... fixing unoptimized web * hmmmm mmmm * close,ish, still missing the wasmbindgen dynaimc imports * aha! full wasm works now - disable externref when compilng patch modules - manually link wasm instrinsics - displace the __wbindgen_placeholder__ table with the synthesized wbg imports what an awful pain. There might be a few items missing, so will test against the docsite shortly * remove failing test * hmmmm things aren't perfect * from what I can tell, relocatable wasm just doesn't work with wasm-bindgen sad https://github.com/rustwasm/wasm-bindgen/issues/1420 * AI IS LITERALLY THE WORST * preserve wasm snippets across compiles * IT HOTPATCHES THE DOCSITE, WE ARE LIVING IN THE FUTURE * remove the macos cfg-out * fix: need to allocate 1 more page * fix url issue * properly span props * properly fill in extra symbols * light cleanups and good docs * delete cruft, better docs * More cleanups and simplifications * clippy, typos * remove harnesses * wasm-bindgen error logging * clean up deps * remove cruft * proper priv/pub * use ifuncs for all env imports * cruft * use par_iter, better output * implement a cache for the base module * delete custom sections * cache the main native module too * add par iter in a few more places * clippy, map ident * better logging and status bars * wip: some small windows support * wip more windowss * fix weird regression * implement windows jump table * small fixes for windows * better windows support * windows support pt2 * rust lld is not msvc * whole archive the rlibs * ignore windows and sys crates * logging * ohhhhhh, pop out dlls * dedupe * thANK GOD FAT LINKING WORKS ON WINDOWS * hoist caching * implement patch based on cache * fix location for pdb * pass data symbols, correct object * add all symbol for windows * add log * treat none rva as undefined * whoops * handle windows calling convention * dont use relocated arm64 stub windows * not all are text? * WINDOWS FUNCTION PATCHING WORKS but issue with statics? * use our own entry * handle windows imp symbols * add log * jump to the real symbol not the layout * whoops * hotpatching windows works completely holy heck yay * drop linker args crate * disable ssg playwright for now * light cleanups * some cleanups / preps: - bump to serverfn 0.8.2 - fix playwright test - fix clippy * use fix from #4042 * bring in other fix for liveview * hoist some configuration * use __aslr_reference instead of aslr_reference * add better errors to patcherror * fix android * panic if patching fails * clean up tomls * wip: fixing wasm compile for server * fullstack wasm again * fix cfg * fixup feature detection * fix issue with features * fix playwright! * add barebones template test harness * fix compatibility between the two hotreload engines * fix preldue of server * fix check for wasm-opt * small fixes
This commit is contained in:
1 parent
71d85e6dd5
commit
d976ca184a
142 files changed
+17982
-7156
No files matched your search
@@ -1,32 +0,0 @@
|
||||
/// Detects if `dx` is being ran in a WSL environment.
|
||||
///
|
||||
/// We determine this based on whether the keyword `microsoft` or `wsl` is contained within the [`WSL_1`] or [`WSL_2`] files.
|
||||
/// This may fail in the future as it isn't guaranteed by Microsoft.
|
||||
/// See https://github.com/microsoft/WSL/issues/423#issuecomment-221627364
|
||||
pub(crate) fn is_wsl() -> bool {
|
||||
const WSL_1: &str = "/proc/sys/kernel/osrelease";
|
||||
const WSL_2: &str = "/proc/version";
|
||||
const WSL_KEYWORDS: [&str; 2] = ["microsoft", "wsl"];
|
||||
|
||||
// Test 1st File
|
||||
if let Ok(content) = std::fs::read_to_string(WSL_1) {
|
||||
let lowercase = content.to_lowercase();
|
||||
for keyword in WSL_KEYWORDS {
|
||||
if lowercase.contains(keyword) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test 2nd File
|
||||
if let Ok(content) = std::fs::read_to_string(WSL_2) {
|
||||
let lowercase = content.to_lowercase();
|
||||
for keyword in WSL_KEYWORDS {
|
||||
if lowercase.contains(keyword) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
@@ -1,856 +0,0 @@
|
||||
use crate::{AppBundle, DioxusCrate, Platform, Result};
|
||||
use anyhow::Context;
|
||||
use dioxus_cli_opt::process_file_to;
|
||||
use std::{
|
||||
env,
|
||||
net::SocketAddr,
|
||||
path::{Path, PathBuf},
|
||||
process::Stdio,
|
||||
};
|
||||
use tokio::{
|
||||
io::{AsyncBufReadExt, BufReader, Lines},
|
||||
process::{Child, ChildStderr, ChildStdout, Command},
|
||||
};
|
||||
|
||||
/// A handle to a running app.
|
||||
///
|
||||
/// Also includes a handle to its server if it exists.
|
||||
/// The actual child processes might not be present (web) or running (died/killed).
|
||||
///
|
||||
/// The purpose of this struct is to accumulate state about the running app and its server, like
|
||||
/// any runtime information needed to hotreload the app or send it messages.
|
||||
///
|
||||
/// We might want to bring in websockets here too, so we know the exact channels the app is using to
|
||||
/// communicate with the devserver. Currently that's a broadcast-type system, so this struct isn't super
|
||||
/// duper useful.
|
||||
///
|
||||
/// todo: restructure this such that "open" is a running task instead of blocking the main thread
|
||||
pub(crate) struct AppHandle {
|
||||
pub(crate) app: AppBundle,
|
||||
|
||||
// These might be None if the app died or the user did not specify a server
|
||||
pub(crate) app_child: Option<Child>,
|
||||
pub(crate) server_child: Option<Child>,
|
||||
|
||||
// stdio for the app so we can read its stdout/stderr
|
||||
// we don't map stdin today (todo) but most apps don't need it
|
||||
pub(crate) app_stdout: Option<Lines<BufReader<ChildStdout>>>,
|
||||
pub(crate) app_stderr: Option<Lines<BufReader<ChildStderr>>>,
|
||||
pub(crate) server_stdout: Option<Lines<BufReader<ChildStdout>>>,
|
||||
pub(crate) server_stderr: Option<Lines<BufReader<ChildStderr>>>,
|
||||
|
||||
/// The executables but with some extra entropy in their name so we can run two instances of the
|
||||
/// same app without causing collisions on the filesystem.
|
||||
pub(crate) entropy_app_exe: Option<PathBuf>,
|
||||
pub(crate) entropy_server_exe: Option<PathBuf>,
|
||||
|
||||
/// The virtual directory that assets will be served from
|
||||
/// Used mostly for apk/ipa builds since they live in simulator
|
||||
pub(crate) runtime_asst_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl AppHandle {
|
||||
pub async fn new(app: AppBundle) -> Result<Self> {
|
||||
Ok(AppHandle {
|
||||
app,
|
||||
runtime_asst_dir: None,
|
||||
app_child: None,
|
||||
app_stderr: None,
|
||||
app_stdout: None,
|
||||
server_child: None,
|
||||
server_stdout: None,
|
||||
server_stderr: None,
|
||||
entropy_app_exe: None,
|
||||
entropy_server_exe: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn open(
|
||||
&mut self,
|
||||
devserver_ip: SocketAddr,
|
||||
open_address: Option<SocketAddr>,
|
||||
start_fullstack_on_address: Option<SocketAddr>,
|
||||
open_browser: bool,
|
||||
) -> Result<()> {
|
||||
let krate = &self.app.build.krate;
|
||||
|
||||
// Set the env vars that the clients will expect
|
||||
// These need to be stable within a release version (ie 0.6.0)
|
||||
let mut envs = vec![
|
||||
(dioxus_cli_config::CLI_ENABLED_ENV, "true".to_string()),
|
||||
(
|
||||
dioxus_cli_config::ALWAYS_ON_TOP_ENV,
|
||||
krate.settings.always_on_top.unwrap_or(true).to_string(),
|
||||
),
|
||||
(
|
||||
dioxus_cli_config::APP_TITLE_ENV,
|
||||
krate.config.web.app.title.clone(),
|
||||
),
|
||||
("RUST_BACKTRACE", "1".to_string()),
|
||||
(
|
||||
dioxus_cli_config::DEVSERVER_IP_ENV,
|
||||
devserver_ip.ip().to_string(),
|
||||
),
|
||||
(
|
||||
dioxus_cli_config::DEVSERVER_PORT_ENV,
|
||||
devserver_ip.port().to_string(),
|
||||
),
|
||||
// unset the cargo dirs in the event we're running `dx` locally
|
||||
// since the child process will inherit the env vars, we don't want to confuse the downstream process
|
||||
("CARGO_MANIFEST_DIR", "".to_string()),
|
||||
(
|
||||
dioxus_cli_config::SESSION_CACHE_DIR,
|
||||
self.app
|
||||
.build
|
||||
.krate
|
||||
.session_cache_dir()
|
||||
.display()
|
||||
.to_string(),
|
||||
),
|
||||
];
|
||||
|
||||
if let Some(base_path) = &krate.config.web.app.base_path {
|
||||
envs.push((dioxus_cli_config::ASSET_ROOT_ENV, base_path.clone()));
|
||||
}
|
||||
|
||||
if let Some(env_filter) = env::var_os("RUST_LOG").and_then(|e| e.into_string().ok()) {
|
||||
envs.push(("RUST_LOG", env_filter));
|
||||
}
|
||||
|
||||
// Launch the server if we were given an address to start it on, and the build includes a server. After we
|
||||
// start the server, consume its stdout/stderr.
|
||||
if let (Some(addr), Some(server)) = (start_fullstack_on_address, self.server_exe()) {
|
||||
tracing::debug!("Proxying fullstack server from port {:?}", addr);
|
||||
envs.push((dioxus_cli_config::SERVER_IP_ENV, addr.ip().to_string()));
|
||||
envs.push((dioxus_cli_config::SERVER_PORT_ENV, addr.port().to_string()));
|
||||
tracing::debug!("Launching server from path: {server:?}");
|
||||
let mut child = Command::new(server)
|
||||
.envs(envs.clone())
|
||||
.stderr(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.kill_on_drop(true)
|
||||
.spawn()?;
|
||||
let stdout = BufReader::new(child.stdout.take().unwrap());
|
||||
let stderr = BufReader::new(child.stderr.take().unwrap());
|
||||
self.server_stdout = Some(stdout.lines());
|
||||
self.server_stderr = Some(stderr.lines());
|
||||
self.server_child = Some(child);
|
||||
}
|
||||
|
||||
// We try to use stdin/stdout to communicate with the app
|
||||
let running_process = match self.app.build.build.platform() {
|
||||
// Unfortunately web won't let us get a proc handle to it (to read its stdout/stderr) so instead
|
||||
// use use the websocket to communicate with it. I wish we could merge the concepts here,
|
||||
// like say, opening the socket as a subprocess, but alas, it's simpler to do that somewhere else.
|
||||
Platform::Web => {
|
||||
// Only the first build we open the web app, after that the user knows it's running
|
||||
if open_browser {
|
||||
self.open_web(open_address.unwrap_or(devserver_ip));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
Platform::Ios => Some(self.open_ios_sim(envs).await?),
|
||||
|
||||
// https://developer.android.com/studio/run/emulator-commandline
|
||||
Platform::Android => {
|
||||
self.open_android_sim(devserver_ip, envs).await;
|
||||
None
|
||||
}
|
||||
|
||||
// These are all just basically running the main exe, but with slightly different resource dir paths
|
||||
Platform::Server
|
||||
| Platform::MacOS
|
||||
| Platform::Windows
|
||||
| Platform::Linux
|
||||
| Platform::Liveview => Some(self.open_with_main_exe(envs)?),
|
||||
};
|
||||
|
||||
// If we have a running process, we need to attach to it and wait for its outputs
|
||||
if let Some(mut child) = running_process {
|
||||
let stdout = BufReader::new(child.stdout.take().unwrap());
|
||||
let stderr = BufReader::new(child.stderr.take().unwrap());
|
||||
self.app_stdout = Some(stdout.lines());
|
||||
self.app_stderr = Some(stderr.lines());
|
||||
self.app_child = Some(child);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gracefully kill the process and all of its children
|
||||
///
|
||||
/// Uses the `SIGTERM` signal on unix and `taskkill` on windows.
|
||||
/// This complex logic is necessary for things like window state preservation to work properly.
|
||||
///
|
||||
/// Also wipes away the entropy executables if they exist.
|
||||
pub(crate) async fn cleanup(&mut self) {
|
||||
// Soft-kill the process by sending a sigkill, allowing the process to clean up
|
||||
self.soft_kill().await;
|
||||
|
||||
// Wipe out the entropy executables if they exist
|
||||
if let Some(entropy_app_exe) = self.entropy_app_exe.take() {
|
||||
_ = std::fs::remove_file(entropy_app_exe);
|
||||
}
|
||||
|
||||
if let Some(entropy_server_exe) = self.entropy_server_exe.take() {
|
||||
_ = std::fs::remove_file(entropy_server_exe);
|
||||
}
|
||||
}
|
||||
|
||||
/// Kill the app and server exes
|
||||
pub(crate) async fn soft_kill(&mut self) {
|
||||
use futures_util::FutureExt;
|
||||
|
||||
// Kill any running executables on Windows
|
||||
let server_process = self.server_child.take();
|
||||
let client_process = self.app_child.take();
|
||||
let processes = [server_process, client_process]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for mut process in processes {
|
||||
let Some(pid) = process.id() else {
|
||||
_ = process.kill().await;
|
||||
continue;
|
||||
};
|
||||
|
||||
// on unix, we can send a signal to the process to shut down
|
||||
#[cfg(unix)]
|
||||
{
|
||||
_ = Command::new("kill")
|
||||
.args(["-s", "TERM", &pid.to_string()])
|
||||
.spawn();
|
||||
}
|
||||
|
||||
// on windows, use the `taskkill` command
|
||||
#[cfg(windows)]
|
||||
{
|
||||
_ = Command::new("taskkill")
|
||||
.args(["/F", "/PID", &pid.to_string()])
|
||||
.spawn();
|
||||
}
|
||||
|
||||
// join the wait with a 100ms timeout
|
||||
futures_util::select! {
|
||||
_ = process.wait().fuse() => {}
|
||||
_ = tokio::time::sleep(std::time::Duration::from_millis(1000)).fuse() => {}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Hotreload an asset in the running app.
|
||||
///
|
||||
/// This will modify the build dir in place! Be careful! We generally assume you want all bundles
|
||||
/// to reflect the latest changes, so we will modify the bundle.
|
||||
///
|
||||
/// However, not all platforms work like this, so we might also need to update a separate asset
|
||||
/// dir that the system simulator might be providing. We know this is the case for ios simulators
|
||||
/// and haven't yet checked for android.
|
||||
///
|
||||
/// This will return the bundled name of the asset such that we can send it to the clients letting
|
||||
/// them know what to reload. It's not super important that this is robust since most clients will
|
||||
/// kick all stylsheets without necessarily checking the name.
|
||||
pub(crate) async fn hotreload_bundled_asset(&self, changed_file: &PathBuf) -> Option<PathBuf> {
|
||||
let mut bundled_name = None;
|
||||
|
||||
// Use the build dir if there's no runtime asset dir as the override. For the case of ios apps,
|
||||
// we won't actually be using the build dir.
|
||||
let asset_dir = match self.runtime_asst_dir.as_ref() {
|
||||
Some(dir) => dir.to_path_buf().join("assets/"),
|
||||
None => self.app.build.asset_dir(),
|
||||
};
|
||||
|
||||
tracing::debug!("Hotreloading asset {changed_file:?} in target {asset_dir:?}");
|
||||
|
||||
// If the asset shares the same name in the bundle, reload that
|
||||
if let Some(legacy_asset_dir) = self.app.build.krate.legacy_asset_dir() {
|
||||
if changed_file.starts_with(&legacy_asset_dir) {
|
||||
tracing::debug!("Hotreloading legacy asset {changed_file:?}");
|
||||
let trimmed = changed_file.strip_prefix(legacy_asset_dir).unwrap();
|
||||
let res = std::fs::copy(changed_file, asset_dir.join(trimmed));
|
||||
bundled_name = Some(trimmed.to_path_buf());
|
||||
if let Err(e) = res {
|
||||
tracing::debug!("Failed to hotreload legacy asset {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Canonicalize the path as Windows may use long-form paths "\\\\?\\C:\\".
|
||||
let changed_file = dunce::canonicalize(changed_file)
|
||||
.inspect_err(|e| tracing::debug!("Failed to canonicalize hotreloaded asset: {e}"))
|
||||
.ok()?;
|
||||
|
||||
// The asset might've been renamed thanks to the manifest, let's attempt to reload that too
|
||||
if let Some(resource) = self.app.app.assets.assets.get(&changed_file).as_ref() {
|
||||
let output_path = asset_dir.join(resource.bundled_path());
|
||||
// Remove the old asset if it exists
|
||||
_ = std::fs::remove_file(&output_path);
|
||||
// And then process the asset with the options into the **old** asset location. If we recompiled,
|
||||
// the asset would be in a new location because the contents and hash have changed. Since we are
|
||||
// hotreloading, we need to use the old asset location it was originally written to.
|
||||
let options = *resource.options();
|
||||
let res = process_file_to(&options, &changed_file, &output_path);
|
||||
bundled_name = Some(PathBuf::from(resource.bundled_path()));
|
||||
if let Err(e) = res {
|
||||
tracing::debug!("Failed to hotreload asset {e}");
|
||||
}
|
||||
}
|
||||
|
||||
// If the emulator is android, we need to copy the asset to the device with `adb push asset /data/local/tmp/dx/assets/filename.ext`
|
||||
if self.app.build.build.platform() == Platform::Android {
|
||||
if let Some(bundled_name) = bundled_name.as_ref() {
|
||||
let target = dioxus_cli_config::android_session_cache_dir().join(bundled_name);
|
||||
tracing::debug!("Pushing asset to device: {target:?}");
|
||||
let res = tokio::process::Command::new(DioxusCrate::android_adb())
|
||||
.arg("push")
|
||||
.arg(&changed_file)
|
||||
.arg(target)
|
||||
.output()
|
||||
.await
|
||||
.context("Failed to push asset to device");
|
||||
|
||||
if let Err(e) = res {
|
||||
tracing::debug!("Failed to push asset to device: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now we can return the bundled asset name to send to the hotreload engine
|
||||
bundled_name
|
||||
}
|
||||
|
||||
/// Open the native app simply by running its main exe
|
||||
///
|
||||
/// Eventually, for mac, we want to run the `.app` with `open` to fix issues with `dylib` paths,
|
||||
/// but for now, we just run the exe directly. Very few users should be caring about `dylib` search
|
||||
/// paths right now, but they will when we start to enable things like swift integration.
|
||||
///
|
||||
/// Server/liveview/desktop are all basically the same, though
|
||||
fn open_with_main_exe(&mut self, envs: Vec<(&str, String)>) -> Result<Child> {
|
||||
// Create a new entropy app exe if we need to
|
||||
let main_exe = self.app_exe();
|
||||
let child = Command::new(main_exe)
|
||||
.envs(envs)
|
||||
.stderr(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.kill_on_drop(true)
|
||||
.spawn()?;
|
||||
|
||||
Ok(child)
|
||||
}
|
||||
|
||||
/// Open the web app by opening the browser to the given address.
|
||||
/// Check if we need to use https or not, and if so, add the protocol.
|
||||
/// Go to the basepath if that's set too.
|
||||
fn open_web(&self, address: SocketAddr) {
|
||||
let base_path = self.app.build.krate.config.web.app.base_path.clone();
|
||||
let https = self
|
||||
.app
|
||||
.build
|
||||
.krate
|
||||
.config
|
||||
.web
|
||||
.https
|
||||
.enabled
|
||||
.unwrap_or_default();
|
||||
let protocol = if https { "https" } else { "http" };
|
||||
let base_path = match base_path.as_deref() {
|
||||
Some(base_path) => format!("/{}", base_path.trim_matches('/')),
|
||||
None => "".to_owned(),
|
||||
};
|
||||
_ = open::that_detached(format!("{protocol}://{address}{base_path}"));
|
||||
}
|
||||
|
||||
/// Use `xcrun` to install the app to the simulator
|
||||
/// With simulators, we're free to basically do anything, so we don't need to do any fancy codesigning
|
||||
/// or entitlements, or anything like that.
|
||||
///
|
||||
/// However, if there's no simulator running, this *might* fail.
|
||||
///
|
||||
/// TODO(jon): we should probably check if there's a simulator running before trying to install,
|
||||
/// and open the simulator if we have to.
|
||||
async fn open_ios_sim(&mut self, envs: Vec<(&str, String)>) -> Result<Child> {
|
||||
tracing::debug!(
|
||||
"Installing app to simulator {:?}",
|
||||
self.app.build.root_dir()
|
||||
);
|
||||
|
||||
let res = Command::new("xcrun")
|
||||
.arg("simctl")
|
||||
.arg("install")
|
||||
.arg("booted")
|
||||
.arg(self.app.build.root_dir())
|
||||
.stderr(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
tracing::debug!("Installed app to simulator with exit code: {res:?}");
|
||||
|
||||
// Remap the envs to the correct simctl env vars
|
||||
// iOS sim lets you pass env vars but they need to be in the format "SIMCTL_CHILD_XXX=XXX"
|
||||
let ios_envs = envs
|
||||
.iter()
|
||||
.map(|(k, v)| (format!("SIMCTL_CHILD_{k}"), v.clone()));
|
||||
|
||||
let child = Command::new("xcrun")
|
||||
.arg("simctl")
|
||||
.arg("launch")
|
||||
.arg("--console")
|
||||
.arg("booted")
|
||||
.arg(self.app.build.krate.bundle_identifier())
|
||||
.envs(ios_envs)
|
||||
.stderr(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.kill_on_drop(true)
|
||||
.spawn()?;
|
||||
|
||||
Ok(child)
|
||||
}
|
||||
|
||||
/// We have this whole thing figured out, but we don't actually use it yet.
|
||||
///
|
||||
/// Launching on devices is more complicated and requires us to codesign the app, which we don't
|
||||
/// currently do.
|
||||
///
|
||||
/// Converting these commands shouldn't be too hard, but device support would imply we need
|
||||
/// better support for codesigning and entitlements.
|
||||
#[allow(unused)]
|
||||
async fn open_ios_device(&self) -> Result<()> {
|
||||
use serde_json::Value;
|
||||
let app_path = self.app.build.root_dir();
|
||||
|
||||
install_app(&app_path).await?;
|
||||
|
||||
// 2. Determine which device the app was installed to
|
||||
let device_uuid = get_device_uuid().await?;
|
||||
|
||||
// 3. Get the installation URL of the app
|
||||
let installation_url = get_installation_url(&device_uuid, &app_path).await?;
|
||||
|
||||
// 4. Launch the app into the background, paused
|
||||
launch_app_paused(&device_uuid, &installation_url).await?;
|
||||
|
||||
// 5. Pick up the paused app and resume it
|
||||
resume_app(&device_uuid).await?;
|
||||
|
||||
async fn install_app(app_path: &PathBuf) -> Result<()> {
|
||||
let output = Command::new("xcrun")
|
||||
.args(["simctl", "install", "booted"])
|
||||
.arg(app_path)
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(format!("Failed to install app: {:?}", output).into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_device_uuid() -> Result<String> {
|
||||
let output = Command::new("xcrun")
|
||||
.args([
|
||||
"devicectl",
|
||||
"list",
|
||||
"devices",
|
||||
"--json-output",
|
||||
"target/deviceid.json",
|
||||
])
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
let json: Value =
|
||||
serde_json::from_str(&std::fs::read_to_string("target/deviceid.json")?)
|
||||
.context("Failed to parse xcrun output")?;
|
||||
let device_uuid = json["result"]["devices"][0]["identifier"]
|
||||
.as_str()
|
||||
.ok_or("Failed to extract device UUID")?
|
||||
.to_string();
|
||||
|
||||
Ok(device_uuid)
|
||||
}
|
||||
|
||||
async fn get_installation_url(device_uuid: &str, app_path: &Path) -> Result<String> {
|
||||
// xcrun devicectl device install app --device <uuid> --path <path> --json-output
|
||||
let output = Command::new("xcrun")
|
||||
.args([
|
||||
"devicectl",
|
||||
"device",
|
||||
"install",
|
||||
"app",
|
||||
"--device",
|
||||
device_uuid,
|
||||
&app_path.display().to_string(),
|
||||
"--json-output",
|
||||
"target/xcrun.json",
|
||||
])
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(format!("Failed to install app: {:?}", output).into());
|
||||
}
|
||||
|
||||
let json: Value = serde_json::from_str(&std::fs::read_to_string("target/xcrun.json")?)
|
||||
.context("Failed to parse xcrun output")?;
|
||||
let installation_url = json["result"]["installedApplications"][0]["installationURL"]
|
||||
.as_str()
|
||||
.ok_or("Failed to extract installation URL")?
|
||||
.to_string();
|
||||
|
||||
Ok(installation_url)
|
||||
}
|
||||
|
||||
async fn launch_app_paused(device_uuid: &str, installation_url: &str) -> Result<()> {
|
||||
let output = Command::new("xcrun")
|
||||
.args([
|
||||
"devicectl",
|
||||
"device",
|
||||
"process",
|
||||
"launch",
|
||||
"--no-activate",
|
||||
"--verbose",
|
||||
"--device",
|
||||
device_uuid,
|
||||
installation_url,
|
||||
"--json-output",
|
||||
"target/launch.json",
|
||||
])
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(format!("Failed to launch app: {:?}", output).into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn resume_app(device_uuid: &str) -> Result<()> {
|
||||
let json: Value = serde_json::from_str(&std::fs::read_to_string("target/launch.json")?)
|
||||
.context("Failed to parse xcrun output")?;
|
||||
|
||||
let status_pid = json["result"]["process"]["processIdentifier"]
|
||||
.as_u64()
|
||||
.ok_or("Failed to extract process identifier")?;
|
||||
|
||||
let output = Command::new("xcrun")
|
||||
.args([
|
||||
"devicectl",
|
||||
"device",
|
||||
"process",
|
||||
"resume",
|
||||
"--device",
|
||||
device_uuid,
|
||||
"--pid",
|
||||
&status_pid.to_string(),
|
||||
])
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(format!("Failed to resume app: {:?}", output).into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
unimplemented!("dioxus-cli doesn't support ios devices yet.")
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
async fn codesign_ios(&self) -> Result<()> {
|
||||
const CODESIGN_ERROR: &str = r#"This is likely because you haven't
|
||||
- Created a provisioning profile before
|
||||
- Accepted the Apple Developer Program License Agreement
|
||||
|
||||
The agreement changes frequently and might need to be accepted again.
|
||||
To accept the agreement, go to https://developer.apple.com/account
|
||||
|
||||
To create a provisioning profile, follow the instructions here:
|
||||
https://developer.apple.com/documentation/xcode/sharing-your-teams-signing-certificates"#;
|
||||
|
||||
let profiles_folder = dirs::home_dir()
|
||||
.context("Your machine has no home-dir")?
|
||||
.join("Library/MobileDevice/Provisioning Profiles");
|
||||
|
||||
if !profiles_folder.exists() || profiles_folder.read_dir()?.next().is_none() {
|
||||
tracing::error!(
|
||||
r#"No provisioning profiles found when trying to codesign the app.
|
||||
We checked the folder: {}
|
||||
|
||||
{CODESIGN_ERROR}
|
||||
"#,
|
||||
profiles_folder.display()
|
||||
)
|
||||
}
|
||||
|
||||
let identities = Command::new("security")
|
||||
.args(["find-identity", "-v", "-p", "codesigning"])
|
||||
.output()
|
||||
.await
|
||||
.context("Failed to run `security find-identity -v -p codesigning`")
|
||||
.map(|e| {
|
||||
String::from_utf8(e.stdout)
|
||||
.context("Failed to parse `security find-identity -v -p codesigning`")
|
||||
})??;
|
||||
|
||||
// Parsing this:
|
||||
// 51ADE4986E0033A5DB1C794E0D1473D74FD6F871 "Apple Development: jkelleyrtp@gmail.com (XYZYZY)"
|
||||
let app_dev_name = regex::Regex::new(r#""Apple Development: (.+)""#)
|
||||
.unwrap()
|
||||
.captures(&identities)
|
||||
.and_then(|caps| caps.get(1))
|
||||
.map(|m| m.as_str())
|
||||
.context(
|
||||
"Failed to find Apple Development in `security find-identity -v -p codesigning`",
|
||||
)?;
|
||||
|
||||
// Acquire the provision file
|
||||
let provision_file = profiles_folder
|
||||
.read_dir()?
|
||||
.flatten()
|
||||
.find(|entry| {
|
||||
entry
|
||||
.file_name()
|
||||
.to_str()
|
||||
.map(|s| s.contains("mobileprovision"))
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.context("Failed to find a provisioning profile. \n\n{CODESIGN_ERROR}")?;
|
||||
|
||||
// The .mobileprovision file has some random binary thrown into into, but it's still basically a plist
|
||||
// Let's use the plist markers to find the start and end of the plist
|
||||
fn cut_plist(bytes: &[u8], byte_match: &[u8]) -> Option<usize> {
|
||||
bytes
|
||||
.windows(byte_match.len())
|
||||
.enumerate()
|
||||
.rev()
|
||||
.find(|(_, slice)| *slice == byte_match)
|
||||
.map(|(i, _)| i + byte_match.len())
|
||||
}
|
||||
let bytes = std::fs::read(provision_file.path())?;
|
||||
let cut1 = cut_plist(&bytes, b"<plist").context("Failed to parse .mobileprovision file")?;
|
||||
let cut2 = cut_plist(&bytes, r#"</dict>"#.as_bytes())
|
||||
.context("Failed to parse .mobileprovision file")?;
|
||||
let sub_bytes = &bytes[(cut1 - 6)..cut2];
|
||||
let mbfile: ProvisioningProfile =
|
||||
plist::from_bytes(sub_bytes).context("Failed to parse .mobileprovision file")?;
|
||||
|
||||
#[derive(serde::Deserialize, Debug)]
|
||||
struct ProvisioningProfile {
|
||||
#[serde(rename = "TeamIdentifier")]
|
||||
team_identifier: Vec<String>,
|
||||
#[serde(rename = "ApplicationIdentifierPrefix")]
|
||||
application_identifier_prefix: Vec<String>,
|
||||
#[serde(rename = "Entitlements")]
|
||||
entitlements: Entitlements,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug)]
|
||||
struct Entitlements {
|
||||
#[serde(rename = "application-identifier")]
|
||||
application_identifier: String,
|
||||
#[serde(rename = "keychain-access-groups")]
|
||||
keychain_access_groups: Vec<String>,
|
||||
}
|
||||
|
||||
let entielements_xml = format!(
|
||||
r#"
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0"><dict>
|
||||
<key>application-identifier</key>
|
||||
<string>{APPLICATION_IDENTIFIER}</string>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>{APP_ID_ACCESS_GROUP}.*</string>
|
||||
</array>
|
||||
<key>get-task-allow</key>
|
||||
<true/>
|
||||
<key>com.apple.developer.team-identifier</key>
|
||||
<string>{TEAM_IDENTIFIER}</string>
|
||||
</dict></plist>
|
||||
"#,
|
||||
APPLICATION_IDENTIFIER = mbfile.entitlements.application_identifier,
|
||||
APP_ID_ACCESS_GROUP = mbfile.entitlements.keychain_access_groups[0],
|
||||
TEAM_IDENTIFIER = mbfile.team_identifier[0],
|
||||
);
|
||||
|
||||
// write to a temp file
|
||||
let temp_file = tempfile::NamedTempFile::new()?;
|
||||
std::fs::write(temp_file.path(), entielements_xml)?;
|
||||
|
||||
// codesign the app
|
||||
let output = Command::new("codesign")
|
||||
.args([
|
||||
"--force",
|
||||
"--entitlements",
|
||||
temp_file.path().to_str().unwrap(),
|
||||
"--sign",
|
||||
app_dev_name,
|
||||
])
|
||||
.arg(self.app.build.root_dir())
|
||||
.output()
|
||||
.await
|
||||
.context("Failed to codesign the app")?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8(output.stderr).unwrap_or_default();
|
||||
return Err(format!("Failed to codesign the app: {stderr}").into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn open_android_sim(
|
||||
&self,
|
||||
devserver_socket: SocketAddr,
|
||||
envs: Vec<(&'static str, String)>,
|
||||
) {
|
||||
let apk_path = self.app.apk_path();
|
||||
let session_cache = self.app.build.krate.session_cache_dir();
|
||||
let full_mobile_app_name = self.app.build.krate.full_mobile_app_name();
|
||||
|
||||
// Start backgrounded since .open() is called while in the arm of the top-level match
|
||||
tokio::task::spawn(async move {
|
||||
let port = devserver_socket.port();
|
||||
if let Err(e) = Command::new("adb")
|
||||
.arg("reverse")
|
||||
.arg(format!("tcp:{}", port))
|
||||
.arg(format!("tcp:{}", port))
|
||||
.stderr(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.output()
|
||||
.await
|
||||
{
|
||||
tracing::error!("failed to forward port {port}: {e}");
|
||||
}
|
||||
|
||||
// Install
|
||||
// adb install -r app-debug.apk
|
||||
if let Err(e) = Command::new(DioxusCrate::android_adb())
|
||||
.arg("install")
|
||||
.arg("-r")
|
||||
.arg(apk_path)
|
||||
.stderr(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.output()
|
||||
.await
|
||||
{
|
||||
tracing::error!("Failed to install apk with `adb`: {e}");
|
||||
};
|
||||
|
||||
// Write the env vars to a .env file in our session cache
|
||||
let env_file = session_cache.join(".env");
|
||||
let contents: String = envs
|
||||
.iter()
|
||||
.map(|(key, value)| format!("{key}={value}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
_ = std::fs::write(&env_file, contents);
|
||||
|
||||
// Push the env file to the device
|
||||
if let Err(e) = tokio::process::Command::new(DioxusCrate::android_adb())
|
||||
.arg("push")
|
||||
.arg(env_file)
|
||||
.arg(dioxus_cli_config::android_session_cache_dir().join(".env"))
|
||||
.output()
|
||||
.await
|
||||
.context("Failed to push asset to device")
|
||||
{
|
||||
tracing::error!("Failed to push .env file to device: {e}");
|
||||
}
|
||||
|
||||
// eventually, use the user's MainActivity, not our MainActivity
|
||||
// adb shell am start -n dev.dioxus.main/dev.dioxus.main.MainActivity
|
||||
let activity_name = format!("{}/dev.dioxus.main.MainActivity", full_mobile_app_name,);
|
||||
|
||||
if let Err(e) = Command::new(DioxusCrate::android_adb())
|
||||
.arg("shell")
|
||||
.arg("am")
|
||||
.arg("start")
|
||||
.arg("-n")
|
||||
.arg(activity_name)
|
||||
.stderr(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.output()
|
||||
.await
|
||||
{
|
||||
tracing::error!("Failed to start app with `adb`: {e}");
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
fn make_entropy_path(exe: &PathBuf) -> PathBuf {
|
||||
let id = uuid::Uuid::new_v4();
|
||||
let name = id.to_string();
|
||||
let some_entropy = name.split('-').next().unwrap();
|
||||
|
||||
// Make a copy of the server exe with a new name
|
||||
let entropy_server_exe = exe.with_file_name(format!(
|
||||
"{}-{}",
|
||||
exe.file_name().unwrap().to_str().unwrap(),
|
||||
some_entropy
|
||||
));
|
||||
|
||||
std::fs::copy(exe, &entropy_server_exe).unwrap();
|
||||
|
||||
entropy_server_exe
|
||||
}
|
||||
|
||||
fn server_exe(&mut self) -> Option<PathBuf> {
|
||||
let mut server = self.app.server_exe()?;
|
||||
|
||||
// Create a new entropy server exe if we need to
|
||||
if cfg!(target_os = "windows") || cfg!(target_os = "linux") {
|
||||
// If we already have an entropy server exe, return it - this is useful for re-opening the same app
|
||||
if let Some(existing_server) = self.entropy_server_exe.clone() {
|
||||
return Some(existing_server);
|
||||
}
|
||||
|
||||
// Otherwise, create a new entropy server exe and save it for re-opning
|
||||
let entropy_server_exe = Self::make_entropy_path(&server);
|
||||
self.entropy_server_exe = Some(entropy_server_exe.clone());
|
||||
server = entropy_server_exe;
|
||||
}
|
||||
|
||||
Some(server)
|
||||
}
|
||||
|
||||
fn app_exe(&mut self) -> PathBuf {
|
||||
let mut main_exe = self.app.main_exe();
|
||||
|
||||
// The requirement here is based on the platform, not necessarily our current architecture.
|
||||
let requires_entropy = match self.app.build.build.platform() {
|
||||
// When running "bundled", we don't need entropy
|
||||
Platform::Web => false,
|
||||
Platform::MacOS => false,
|
||||
Platform::Ios => false,
|
||||
Platform::Android => false,
|
||||
|
||||
// But on platforms that aren't running as "bundled", we do.
|
||||
Platform::Windows => true,
|
||||
Platform::Linux => true,
|
||||
Platform::Server => true,
|
||||
Platform::Liveview => true,
|
||||
};
|
||||
|
||||
if requires_entropy || std::env::var("DIOXUS_ENTROPY").is_ok() {
|
||||
// If we already have an entropy app exe, return it - this is useful for re-opening the same app
|
||||
if let Some(existing_app_exe) = self.entropy_app_exe.clone() {
|
||||
return existing_app_exe;
|
||||
}
|
||||
|
||||
let entropy_app_exe = Self::make_entropy_path(&main_exe);
|
||||
self.entropy_app_exe = Some(entropy_app_exe.clone());
|
||||
main_exe = entropy_app_exe;
|
||||
}
|
||||
|
||||
main_exe
|
||||
}
|
||||
}
|
||||
+127
-174
@@ -1,21 +1,16 @@
|
||||
use crate::{BuildUpdate, Builder, Error, Platform, Result, ServeArgs, TraceController, TraceSrc};
|
||||
use crate::{AppBuilder, BuildId, BuildMode, BuilderUpdate, Result, ServeArgs, TraceController};
|
||||
|
||||
mod ansi_buffer;
|
||||
mod detect;
|
||||
mod handle;
|
||||
mod output;
|
||||
mod proxy;
|
||||
mod runner;
|
||||
mod server;
|
||||
mod update;
|
||||
mod watcher;
|
||||
|
||||
pub(crate) use handle::*;
|
||||
pub(crate) use output::*;
|
||||
pub(crate) use runner::*;
|
||||
pub(crate) use server::*;
|
||||
pub(crate) use update::*;
|
||||
pub(crate) use watcher::*;
|
||||
|
||||
/// For *all* builds, the CLI spins up a dedicated webserver, file watcher, and build infrastructure to serve the project.
|
||||
///
|
||||
@@ -23,11 +18,11 @@ pub(crate) use watcher::*;
|
||||
///
|
||||
/// Platform specifics:
|
||||
/// -------------------
|
||||
/// - Web: we need to attach a filesystem server to our devtools webserver to serve the project. We
|
||||
/// want to emulate GithubPages here since most folks are deploying there and expect things like
|
||||
/// basepath to match.
|
||||
/// - Desktop: We spin up the dev server but without a filesystem server.
|
||||
/// - Mobile: Basically the same as desktop.
|
||||
/// - Web: We need to attach a filesystem server to our devtools webserver to serve the project. We
|
||||
/// want to emulate GithubPages here since most folks are deploying there and expect things like
|
||||
/// basepath to match.
|
||||
/// - Desktop: We spin up the dev server but without a filesystem server.
|
||||
/// - Mobile: Basically the same as desktop.
|
||||
///
|
||||
/// When fullstack is enabled, we'll also build for the `server` target and then hotreload the server.
|
||||
/// The "server" is special here since "fullstack" is functionally just an addition to the regular client
|
||||
@@ -37,19 +32,11 @@ pub(crate) use watcher::*;
|
||||
/// - I'd love to be able to configure the CLI while it's running so we can change settings on the fly.
|
||||
/// - I want us to be able to detect a `server_fn` in the project and then upgrade from a static server
|
||||
/// to a dynamic one on the fly.
|
||||
pub(crate) async fn serve_all(mut args: ServeArgs) -> Result<()> {
|
||||
// Redirect all logging the cli logger
|
||||
let mut tracer = TraceController::redirect(args.is_interactive_tty());
|
||||
|
||||
// Load the krate and resolve the server args against it - this might log so do it after we turn on the tracer first
|
||||
let krate = args.load_krate().await?;
|
||||
|
||||
// Note that starting the builder will queue up a build immediately
|
||||
let mut screen = Output::start(&args).await?;
|
||||
let mut builder = Builder::start(&krate, args.build_args())?;
|
||||
let mut devserver = WebServer::start(&krate, &args)?;
|
||||
let mut watcher = Watcher::start(&krate, &args);
|
||||
let mut runner = AppRunner::start(&krate);
|
||||
pub(crate) async fn serve_all(args: ServeArgs, tracer: &mut TraceController) -> Result<()> {
|
||||
// Load the args into a plan, resolving all tooling, build dirs, arguments, decoding the multi-target, etc
|
||||
let mut builder = AppServer::start(args).await?;
|
||||
let mut devserver = WebServer::start(&builder)?;
|
||||
let mut screen = Output::start(builder.interactive).await?;
|
||||
|
||||
// This is our default splash screen. We might want to make this a fancier splash screen in the future
|
||||
// Also, these commands might not be the most important, but it's all we've got enabled right now
|
||||
@@ -63,166 +50,137 @@ pub(crate) async fn serve_all(mut args: ServeArgs) -> Result<()> {
|
||||
• Press `/` for more commands and shortcuts
|
||||
Learn more at https://dioxuslabs.com/learn/0.6/getting_started
|
||||
----------------------------------------------------------------"#,
|
||||
krate.executable_name()
|
||||
builder.app_name()
|
||||
);
|
||||
|
||||
let err: Result<(), Error> = loop {
|
||||
loop {
|
||||
// Draw the state of the server to the screen
|
||||
screen.render(&args, &krate, &builder, &devserver, &watcher);
|
||||
screen.render(&builder, &devserver);
|
||||
|
||||
// And then wait for any updates before redrawing
|
||||
let msg = tokio::select! {
|
||||
msg = builder.wait() => ServeUpdate::BuildUpdate(msg),
|
||||
msg = watcher.wait() => msg,
|
||||
msg = builder.wait() => msg,
|
||||
msg = devserver.wait() => msg,
|
||||
msg = screen.wait() => msg,
|
||||
msg = runner.wait() => msg,
|
||||
msg = tracer.wait() => msg,
|
||||
};
|
||||
|
||||
match msg {
|
||||
ServeUpdate::FilesChanged { files } => {
|
||||
if files.is_empty() || !args.should_hotreload() {
|
||||
if files.is_empty() || !builder.hot_reload {
|
||||
continue;
|
||||
}
|
||||
|
||||
let file = files[0].display().to_string();
|
||||
let file = file.trim_start_matches(&krate.crate_dir().display().to_string());
|
||||
|
||||
// if change is hotreloadable, hotreload it
|
||||
// and then send that update to all connected clients
|
||||
if let Some(hr) = runner.attempt_hot_reload(files).await {
|
||||
// Only send a hotreload message for templates and assets - otherwise we'll just get a full rebuild
|
||||
//
|
||||
// Also make sure the builder isn't busy since that might cause issues with hotreloads
|
||||
// https://github.com/DioxusLabs/dioxus/issues/3361
|
||||
if hr.is_empty() || !builder.can_receive_hotreloads() {
|
||||
tracing::debug!(dx_src = ?TraceSrc::Dev, "Ignoring file change: {}", file);
|
||||
continue;
|
||||
}
|
||||
|
||||
tracing::info!(dx_src = ?TraceSrc::Dev, "Hotreloading: {}", file);
|
||||
|
||||
devserver.send_hotreload(hr).await;
|
||||
} else if runner.should_full_rebuild {
|
||||
tracing::info!(dx_src = ?TraceSrc::Dev, "Full rebuild: {}", file);
|
||||
|
||||
// We're going to kick off a new build, interrupting the current build if it's ongoing
|
||||
builder.rebuild(args.build_arguments.clone());
|
||||
|
||||
// Clear the hot reload changes so we don't have out-of-sync issues with changed UI
|
||||
runner.clear_hot_reload_changes();
|
||||
runner.file_map.force_rebuild();
|
||||
|
||||
// Tell the server to show a loading page for any new requests
|
||||
devserver.send_reload_start().await;
|
||||
devserver.start_build().await;
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"Rebuild required but is currently paused - press `r` to rebuild manually"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Run the server in the background
|
||||
// Waiting for updates here lets us tap into when clients are added/removed
|
||||
ServeUpdate::NewConnection => {
|
||||
devserver
|
||||
.send_hotreload(runner.applied_hot_reload_changes())
|
||||
.await;
|
||||
|
||||
runner.client_connected().await;
|
||||
}
|
||||
|
||||
// Received a message from the devtools server - currently we only use this for
|
||||
// logging, so we just forward it the tui
|
||||
ServeUpdate::WsMessage(msg) => {
|
||||
screen.push_ws_message(Platform::Web, msg);
|
||||
}
|
||||
|
||||
// Wait for logs from the build engine
|
||||
// These will cause us to update the screen
|
||||
// We also can check the status of the builds here in case we have multiple ongoing builds
|
||||
ServeUpdate::BuildUpdate(update) => {
|
||||
// Queue any logs to be printed if need be
|
||||
screen.new_build_update(&update);
|
||||
|
||||
// And then update the websocketed clients with the new build status in case they want it
|
||||
devserver.new_build_update(&update, &builder).await;
|
||||
|
||||
// And then open the app if it's ready
|
||||
// todo: there might be more things to do here that require coordination with other pieces of the CLI
|
||||
// todo: maybe we want to shuffle the runner around to send an "open" command instead of doing that
|
||||
match update {
|
||||
BuildUpdate::Progress { .. } => {}
|
||||
BuildUpdate::CompilerMessage { message } => {
|
||||
screen.push_cargo_log(message);
|
||||
}
|
||||
BuildUpdate::BuildFailed { err } => {
|
||||
tracing::error!("Build failed: {:?}", err);
|
||||
}
|
||||
BuildUpdate::BuildReady { bundle } => {
|
||||
let handle = runner
|
||||
.open(
|
||||
bundle,
|
||||
devserver.devserver_address(),
|
||||
devserver.displayed_address(),
|
||||
devserver.proxied_server_address(),
|
||||
args.open.unwrap_or(false),
|
||||
)
|
||||
.await;
|
||||
|
||||
match handle {
|
||||
// Update the screen + devserver with the new handle info
|
||||
Ok(_handle) => {
|
||||
devserver.send_reload_command().await;
|
||||
}
|
||||
|
||||
Err(e) => tracing::error!("Failed to open app: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the process exited *cleanly*, we can exit
|
||||
ServeUpdate::ProcessExited { status, platform } => {
|
||||
if !status.success() {
|
||||
tracing::error!("Application [{platform}] exited with error: {status}");
|
||||
} else {
|
||||
tracing::info!(
|
||||
r#"Application [{platform}] exited gracefully.
|
||||
- To restart the app, press `r` to rebuild or `o` to open
|
||||
- To exit the server, press `ctrl+c`"#
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ServeUpdate::StdoutReceived { platform, msg } => {
|
||||
screen.push_stdio(platform, msg, tracing::Level::INFO);
|
||||
}
|
||||
|
||||
ServeUpdate::StderrReceived { platform, msg } => {
|
||||
screen.push_stdio(platform, msg, tracing::Level::ERROR);
|
||||
}
|
||||
|
||||
ServeUpdate::TracingLog { log } => {
|
||||
screen.push_log(log);
|
||||
tracing::debug!("Starting hotpatching: {:?}", files);
|
||||
builder.handle_file_change(&files, &mut devserver).await;
|
||||
}
|
||||
|
||||
ServeUpdate::RequestRebuild => {
|
||||
// The spacing here is important-ish: we want
|
||||
// `Full rebuild:` to line up with
|
||||
// `Hotreloading:` to keep the alignment during long edit sessions
|
||||
// `Hot-patching:` to keep the alignment during long edit sessions
|
||||
tracing::info!("Full rebuild: triggered manually");
|
||||
|
||||
builder.rebuild(args.build_arguments.clone());
|
||||
runner.file_map.force_rebuild();
|
||||
builder.full_rebuild().await;
|
||||
devserver.send_reload_start().await;
|
||||
devserver.start_build().await
|
||||
devserver.start_build().await;
|
||||
}
|
||||
|
||||
// Run the server in the background
|
||||
// Waiting for updates here lets us tap into when clients are added/removed
|
||||
ServeUpdate::NewConnection { id, aslr_reference } => {
|
||||
// Send the client
|
||||
devserver
|
||||
.send_hotreload(builder.applied_hot_reload_changes(BuildId::CLIENT))
|
||||
.await;
|
||||
|
||||
if builder.server.is_some() {
|
||||
devserver
|
||||
.send_hotreload(builder.applied_hot_reload_changes(BuildId::SERVER))
|
||||
.await;
|
||||
}
|
||||
|
||||
builder.client_connected(id, aslr_reference).await;
|
||||
}
|
||||
|
||||
// Received a message from the devtools server - currently we only use this for
|
||||
// logging, so we just forward it the tui
|
||||
ServeUpdate::WsMessage { msg, platform } => {
|
||||
screen.push_ws_message(platform, &msg);
|
||||
}
|
||||
|
||||
// Wait for logs from the build engine
|
||||
// These will cause us to update the screen
|
||||
// We also can check the status of the builds here in case we have multiple ongoing builds
|
||||
ServeUpdate::BuilderUpdate { id, update } => {
|
||||
let platform = builder.get_build(id).unwrap().build.platform;
|
||||
|
||||
// Queue any logs to be printed if need be
|
||||
screen.new_build_update(&update);
|
||||
|
||||
// And then update the websocketed clients with the new build status in case they want it
|
||||
devserver.new_build_update(&update).await;
|
||||
|
||||
// And then open the app if it's ready
|
||||
match update {
|
||||
BuilderUpdate::Progress { .. } => {}
|
||||
BuilderUpdate::CompilerMessage { message } => {
|
||||
screen.push_cargo_log(message);
|
||||
}
|
||||
BuilderUpdate::BuildFailed { err } => {
|
||||
tracing::error!("Build failed: {:#?}", err);
|
||||
}
|
||||
BuilderUpdate::BuildReady { bundle } => match bundle.mode {
|
||||
BuildMode::Thin { ref cache, .. } => {
|
||||
let elapsed =
|
||||
bundle.time_end.duration_since(bundle.time_start).unwrap();
|
||||
match builder.hotpatch(&bundle, id, cache).await {
|
||||
Ok(jumptable) => devserver.send_patch(jumptable, elapsed, id).await,
|
||||
Err(err) => {
|
||||
tracing::error!("Failed to hot-patch app: {err}");
|
||||
|
||||
if matches!(err, crate::Error::PatchingFailed(_)) {
|
||||
tracing::info!("Starting full rebuild: {err}");
|
||||
builder.full_rebuild().await;
|
||||
devserver.send_reload_start().await;
|
||||
devserver.start_build().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
BuildMode::Base | BuildMode::Fat => {
|
||||
_ = builder
|
||||
.open(bundle, &mut devserver)
|
||||
.await
|
||||
.inspect_err(|e| tracing::error!("Failed to open app: {}", e));
|
||||
}
|
||||
},
|
||||
BuilderUpdate::StdoutReceived { msg } => {
|
||||
screen.push_stdio(platform, msg, tracing::Level::INFO);
|
||||
}
|
||||
BuilderUpdate::StderrReceived { msg } => {
|
||||
screen.push_stdio(platform, msg, tracing::Level::ERROR);
|
||||
}
|
||||
BuilderUpdate::ProcessExited { status } => {
|
||||
if status.success() {
|
||||
tracing::info!(
|
||||
r#"Application [{platform}] exited gracefully.
|
||||
• To restart the app, press `r` to rebuild or `o` to open
|
||||
• To exit the server, press `ctrl+c`"#
|
||||
);
|
||||
} else {
|
||||
tracing::error!("Application [{platform}] exited with error: {status}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ServeUpdate::TracingLog { log } => {
|
||||
screen.push_log(log);
|
||||
}
|
||||
|
||||
ServeUpdate::OpenApp => {
|
||||
if let Err(err) = runner.open_existing(&devserver).await {
|
||||
if let Err(err) = builder.open_all(&devserver, true).await {
|
||||
tracing::error!("Failed to open app: {err}")
|
||||
}
|
||||
}
|
||||
@@ -232,10 +190,10 @@ pub(crate) async fn serve_all(mut args: ServeArgs) -> Result<()> {
|
||||
}
|
||||
|
||||
ServeUpdate::ToggleShouldRebuild => {
|
||||
runner.should_full_rebuild = !runner.should_full_rebuild;
|
||||
builder.automatic_rebuilds = !builder.automatic_rebuilds;
|
||||
tracing::info!(
|
||||
"Automatic rebuilds are currently: {}",
|
||||
if runner.should_full_rebuild {
|
||||
if builder.automatic_rebuilds {
|
||||
"enabled"
|
||||
} else {
|
||||
"disabled"
|
||||
@@ -243,21 +201,16 @@ pub(crate) async fn serve_all(mut args: ServeArgs) -> Result<()> {
|
||||
)
|
||||
}
|
||||
|
||||
ServeUpdate::Exit { error } => match error {
|
||||
Some(err) => break Err(anyhow::anyhow!("{}", err).into()),
|
||||
None => break Ok(()),
|
||||
},
|
||||
ServeUpdate::Exit { error } => {
|
||||
_ = builder.cleanup_all().await;
|
||||
_ = devserver.shutdown().await;
|
||||
_ = screen.shutdown();
|
||||
|
||||
match error {
|
||||
Some(err) => return Err(anyhow::anyhow!("{}", err).into()),
|
||||
None => return Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_ = runner.cleanup().await;
|
||||
_ = devserver.shutdown().await;
|
||||
builder.abort_all();
|
||||
_ = screen.shutdown();
|
||||
|
||||
if let Err(err) = err {
|
||||
eprintln!("Exiting with error: {}", err);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
use crate::{
|
||||
serve::{ansi_buffer::AnsiStringLine, Builder, ServeUpdate, Watcher, WebServer},
|
||||
BuildStage, BuildUpdate, DioxusCrate, Platform, RustcDetails, ServeArgs, TraceContent,
|
||||
TraceMsg, TraceSrc,
|
||||
serve::{ansi_buffer::AnsiStringLine, ServeUpdate, WebServer},
|
||||
BuildStage, BuilderUpdate, Platform, TraceContent, TraceMsg, TraceSrc,
|
||||
};
|
||||
use crossterm::{
|
||||
cursor::{Hide, Show},
|
||||
@@ -26,6 +25,8 @@ use std::{
|
||||
};
|
||||
use tracing::Level;
|
||||
|
||||
use super::AppServer;
|
||||
|
||||
const TICK_RATE_MS: u64 = 100;
|
||||
const VIEWPORT_MAX_WIDTH: u16 = 100;
|
||||
const VIEWPORT_HEIGHT_SMALL: u16 = 5;
|
||||
@@ -46,7 +47,6 @@ pub struct Output {
|
||||
// A list of all messages from build, dev, app, and more.
|
||||
more_modal_open: bool,
|
||||
interactive: bool,
|
||||
platform: Platform,
|
||||
|
||||
// Whether to show verbose logs or not
|
||||
// We automatically hide "debug" logs if verbose is false (only showing "info" / "warn" / "error")
|
||||
@@ -64,31 +64,24 @@ pub struct Output {
|
||||
// ! needs to be wrapped in an &mut since `render stateful widget` requires &mut... but our
|
||||
// "render" method only borrows &self (for no particular reason at all...)
|
||||
throbber: RefCell<throbber_widgets_tui::ThrobberState>,
|
||||
|
||||
rustc_details: RustcDetails,
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
#[derive(Clone, Copy)]
|
||||
struct RenderState<'a> {
|
||||
opts: &'a ServeArgs,
|
||||
krate: &'a DioxusCrate,
|
||||
build_engine: &'a Builder,
|
||||
runner: &'a AppServer,
|
||||
server: &'a WebServer,
|
||||
watcher: &'a Watcher,
|
||||
}
|
||||
|
||||
impl Output {
|
||||
pub(crate) async fn start(cfg: &ServeArgs) -> crate::Result<Self> {
|
||||
pub(crate) async fn start(interactive: bool) -> crate::Result<Self> {
|
||||
let mut output = Self {
|
||||
interactive,
|
||||
term: Rc::new(RefCell::new(None)),
|
||||
interactive: cfg.is_interactive_tty(),
|
||||
dx_version: format!(
|
||||
"{}-{}",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
crate::dx_build_info::GIT_COMMIT_HASH_SHORT.unwrap_or("main")
|
||||
),
|
||||
platform: cfg.build_arguments.platform.expect("To be resolved by now"),
|
||||
events: None,
|
||||
more_modal_open: false,
|
||||
pending_logs: VecDeque::new(),
|
||||
@@ -101,7 +94,6 @@ impl Output {
|
||||
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
|
||||
interval
|
||||
},
|
||||
rustc_details: RustcDetails::from_cli().await?,
|
||||
};
|
||||
|
||||
output.startup()?;
|
||||
@@ -113,15 +105,6 @@ impl Output {
|
||||
/// This is meant to be paired with "shutdown" to restore the terminal to its original state.
|
||||
fn startup(&mut self) -> io::Result<()> {
|
||||
if self.interactive {
|
||||
// set the panic hook to fix the terminal in the event of a panic
|
||||
// The terminal might be left in a wonky state if a panic occurs, and we don't want it to be completely broken
|
||||
let original_hook = std::panic::take_hook();
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
_ = disable_raw_mode();
|
||||
_ = stdout().execute(Show);
|
||||
original_hook(info);
|
||||
}));
|
||||
|
||||
// Check if writing the terminal is going to block infinitely.
|
||||
// If it does, we should disable interactive mode. This ensures we work with programs like `bg`
|
||||
// which suspend the process and cause us to block when writing output.
|
||||
@@ -183,7 +166,12 @@ impl Output {
|
||||
/// Call the shutdown functions that might mess with the terminal settings - see the related code
|
||||
/// in "startup" for more details about what we need to unset
|
||||
pub(crate) fn shutdown(&self) -> io::Result<()> {
|
||||
if self.interactive {
|
||||
Self::remote_shutdown(self.interactive)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn remote_shutdown(interactive: bool) -> io::Result<()> {
|
||||
if interactive {
|
||||
stdout()
|
||||
.execute(Show)?
|
||||
.execute(DisableFocusChange)?
|
||||
@@ -329,7 +317,7 @@ impl Output {
|
||||
}
|
||||
|
||||
/// Push a message from the websocket to the logs
|
||||
pub fn push_ws_message(&mut self, platform: Platform, message: axum::extract::ws::Message) {
|
||||
pub fn push_ws_message(&mut self, platform: Platform, message: &axum::extract::ws::Message) {
|
||||
use dioxus_devtools_types::ClientMsg;
|
||||
|
||||
// We can only handle text messages from the websocket...
|
||||
@@ -341,14 +329,18 @@ impl Output {
|
||||
let res = serde_json::from_str::<ClientMsg>(text.as_str());
|
||||
|
||||
// Client logs being errors aren't fatal, but we should still report them them
|
||||
let ClientMsg::Log { level, messages } = match res {
|
||||
let msg = match res {
|
||||
Ok(msg) => msg,
|
||||
Err(err) => {
|
||||
tracing::error!(dx_src = ?TraceSrc::Dev, "Error parsing message from {}: {}", platform, err);
|
||||
tracing::error!(dx_src = ?TraceSrc::Dev, "Error parsing message from {}: {} -> {:?}", platform, err, text.as_str());
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let ClientMsg::Log { level, messages } = msg else {
|
||||
return;
|
||||
};
|
||||
|
||||
// FIXME(jon): why are we pulling only the first message here?
|
||||
let content = messages.first().unwrap_or(&String::new()).clone();
|
||||
|
||||
@@ -373,26 +365,19 @@ impl Output {
|
||||
/// approach, but then we'd need to do that *everywhere* instead of simply performing a react-like
|
||||
/// re-render when external state changes. Ratatui will diff the intermediate buffer, so we at least
|
||||
/// we won't be drawing it.
|
||||
pub(crate) fn new_build_update(&mut self, update: &BuildUpdate) {
|
||||
pub(crate) fn new_build_update(&mut self, update: &BuilderUpdate) {
|
||||
match update {
|
||||
BuildUpdate::Progress {
|
||||
BuilderUpdate::Progress {
|
||||
stage: BuildStage::Starting { .. },
|
||||
} => self.tick_animation = true,
|
||||
BuildUpdate::BuildReady { .. } => self.tick_animation = false,
|
||||
BuildUpdate::BuildFailed { .. } => self.tick_animation = false,
|
||||
BuilderUpdate::BuildReady { .. } => self.tick_animation = false,
|
||||
BuilderUpdate::BuildFailed { .. } => self.tick_animation = false,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Render the current state of everything to the console screen
|
||||
pub fn render(
|
||||
&mut self,
|
||||
opts: &ServeArgs,
|
||||
config: &DioxusCrate,
|
||||
build_engine: &Builder,
|
||||
server: &WebServer,
|
||||
watcher: &Watcher,
|
||||
) {
|
||||
pub fn render(&mut self, runner: &AppServer, server: &WebServer) {
|
||||
if !self.interactive {
|
||||
return;
|
||||
}
|
||||
@@ -409,16 +394,7 @@ impl Output {
|
||||
|
||||
// Then, draw the frame, passing along all the state of the TUI so we can render it properly
|
||||
_ = term.draw(|frame| {
|
||||
self.render_frame(
|
||||
frame,
|
||||
RenderState {
|
||||
opts,
|
||||
krate: config,
|
||||
build_engine,
|
||||
server,
|
||||
watcher,
|
||||
},
|
||||
);
|
||||
self.render_frame(frame, RenderState { runner, server });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -488,39 +464,46 @@ impl Output {
|
||||
])
|
||||
.areas(gauge_area);
|
||||
|
||||
let client = &state.runner.client();
|
||||
self.render_single_gauge(
|
||||
frame,
|
||||
app_progress,
|
||||
state.build_engine.compile_progress(),
|
||||
client.compile_progress(),
|
||||
"App: ",
|
||||
state,
|
||||
state.build_engine.compile_duration(),
|
||||
client.compile_duration(),
|
||||
);
|
||||
|
||||
if state.build_engine.request.build.fullstack() {
|
||||
if state.runner.is_fullstack() {
|
||||
self.render_single_gauge(
|
||||
frame,
|
||||
second_progress,
|
||||
state.build_engine.server_compile_progress(),
|
||||
state.runner.server_compile_progress(),
|
||||
"Server: ",
|
||||
state,
|
||||
state.build_engine.compile_duration(),
|
||||
client.compile_duration(),
|
||||
);
|
||||
} else {
|
||||
self.render_single_gauge(
|
||||
frame,
|
||||
second_progress,
|
||||
state.build_engine.bundle_progress(),
|
||||
client.bundle_progress(),
|
||||
"Bundle: ",
|
||||
state,
|
||||
state.build_engine.bundle_duration(),
|
||||
client.bundle_duration(),
|
||||
);
|
||||
}
|
||||
|
||||
let mut lines = vec!["Status: ".white()];
|
||||
match &state.build_engine.stage {
|
||||
match &client.stage {
|
||||
BuildStage::Initializing => lines.push("Initializing".yellow()),
|
||||
BuildStage::Starting { .. } => lines.push("Starting build".yellow()),
|
||||
BuildStage::Starting { patch, .. } => {
|
||||
if *patch {
|
||||
lines.push("Hot-patching...".yellow())
|
||||
} else {
|
||||
lines.push("Starting build".yellow())
|
||||
}
|
||||
}
|
||||
BuildStage::InstallingTooling => lines.push("Installing tooling".yellow()),
|
||||
BuildStage::Compiling {
|
||||
current,
|
||||
@@ -535,7 +518,6 @@ impl Output {
|
||||
BuildStage::OptimizingWasm => lines.push("Optimizing wasm".yellow()),
|
||||
BuildStage::SplittingBundle => lines.push("Splitting bundle".yellow()),
|
||||
BuildStage::CompressingAssets => lines.push("Compressing assets".yellow()),
|
||||
BuildStage::PrerenderingRoutes => lines.push("Prerendering static routes".yellow()),
|
||||
BuildStage::RunningBindgen => lines.push("Running wasm-bindgen".yellow()),
|
||||
BuildStage::RunningGradle => lines.push("Running gradle assemble".yellow()),
|
||||
BuildStage::Bundling => lines.push("Bundling app".yellow()),
|
||||
@@ -552,15 +534,18 @@ impl Output {
|
||||
}
|
||||
BuildStage::Success => {
|
||||
lines.push("Serving ".yellow());
|
||||
lines.push(state.krate.executable_name().white());
|
||||
lines.push(client.build.executable_name().white());
|
||||
lines.push(" 🚀 ".green());
|
||||
if let Some(comp_time) = state.build_engine.total_build_time() {
|
||||
if let Some(comp_time) = client.total_build_time() {
|
||||
lines.push(format!("{:.1}s", comp_time.as_secs_f32()).dark_gray());
|
||||
}
|
||||
}
|
||||
BuildStage::Failed => lines.push("Failed".red()),
|
||||
BuildStage::Aborted => lines.push("Aborted".red()),
|
||||
BuildStage::Restarting => lines.push("Restarting".yellow()),
|
||||
BuildStage::Linking => lines.push("Linking".yellow()),
|
||||
BuildStage::Hotpatching => lines.push("Hot-patching...".yellow()),
|
||||
BuildStage::ExtractingAssets => lines.push("Extracting assets".yellow()),
|
||||
_ => {}
|
||||
};
|
||||
|
||||
@@ -576,7 +561,7 @@ impl Output {
|
||||
state: RenderState,
|
||||
time_taken: Option<Duration>,
|
||||
) {
|
||||
let failed = state.build_engine.stage == BuildStage::Failed;
|
||||
let failed = state.runner.client.stage == BuildStage::Failed;
|
||||
let value = if failed { 1.0 } else { value.clamp(0.0, 1.0) };
|
||||
|
||||
let [gauge_row, _, icon] = Layout::horizontal([
|
||||
@@ -647,11 +632,12 @@ impl Output {
|
||||
])
|
||||
.areas(area);
|
||||
|
||||
let client = &state.runner.client();
|
||||
frame.render_widget(
|
||||
Paragraph::new(Line::from(vec![
|
||||
"Platform: ".gray(),
|
||||
self.platform.expected_name().yellow(),
|
||||
if state.opts.build_arguments.fullstack() {
|
||||
client.build.platform.expected_name().yellow(),
|
||||
if state.runner.is_fullstack() {
|
||||
" + fullstack".yellow()
|
||||
} else {
|
||||
" ".dark_gray()
|
||||
@@ -671,7 +657,7 @@ impl Output {
|
||||
|
||||
frame.render_widget_ref(
|
||||
Paragraph::new(Line::from(vec![
|
||||
if self.platform == Platform::Web {
|
||||
if client.build.platform == Platform::Web {
|
||||
"Serving at: ".gray()
|
||||
} else {
|
||||
"ServerFns at: ".gray()
|
||||
@@ -688,7 +674,7 @@ impl Output {
|
||||
Paragraph::new(Line::from({
|
||||
let mut lines = vec!["App features: ".gray(), "[".yellow()];
|
||||
|
||||
let feature_list: Vec<String> = state.build_engine.request.all_target_features();
|
||||
let feature_list: Vec<String> = state.runner.client().build.all_target_features();
|
||||
let num_features = feature_list.len();
|
||||
|
||||
for (idx, feature) in feature_list.into_iter().enumerate() {
|
||||
@@ -737,7 +723,7 @@ impl Output {
|
||||
frame.render_widget(
|
||||
Paragraph::new(Line::from(vec![
|
||||
"rustc: ".gray(),
|
||||
self.rustc_details.version.as_str().yellow(),
|
||||
state.runner.workspace.rustc_version.as_str().yellow(),
|
||||
])),
|
||||
meta_list[2],
|
||||
);
|
||||
@@ -869,8 +855,8 @@ impl Output {
|
||||
.iter()
|
||||
.map(|line| {
|
||||
// Very important to strip ansi codes before counting graphemes - the ansi codes count as multiple graphemes!
|
||||
let grapheme_count = console::strip_ansi_codes(line).graphemes(true).count() as u16;
|
||||
grapheme_count.max(1).div_ceil(term_size.width)
|
||||
let grapheme_count = console::strip_ansi_codes(line).graphemes(true).count();
|
||||
grapheme_count.max(1).div_ceil(term_size.width as usize) as u16
|
||||
})
|
||||
.sum::<u16>();
|
||||
|
||||
@@ -1005,7 +991,11 @@ impl Output {
|
||||
|
||||
// Create the ansi -> raw string line with a width of either the viewport width or the max width
|
||||
let line_length = line.styled_graphemes(Style::default()).count();
|
||||
lines.push(AnsiStringLine::new(line_length as _).render(&line));
|
||||
if line_length < u16::MAX as usize {
|
||||
lines.push(AnsiStringLine::new(line_length as _).render(&line));
|
||||
} else {
|
||||
lines.push(line.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+947
-236
File diff suppressed because it is too large.
Load diff
+119
-123
@@ -1,14 +1,13 @@
|
||||
use crate::{
|
||||
config::WebHttpsConfig,
|
||||
serve::{ServeArgs, ServeUpdate},
|
||||
BuildStage, BuildUpdate, DioxusCrate, Platform, Result, TraceSrc,
|
||||
config::WebHttpsConfig, serve::ServeUpdate, BuildId, BuildStage, BuilderUpdate, Platform,
|
||||
Result, TraceSrc,
|
||||
};
|
||||
use anyhow::Context;
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::{
|
||||
ws::{Message, WebSocket},
|
||||
Request, State, WebSocketUpgrade,
|
||||
Query, Request, State, WebSocketUpgrade,
|
||||
},
|
||||
http::{
|
||||
header::{HeaderName, HeaderValue, CACHE_CONTROL, EXPIRES, PRAGMA},
|
||||
@@ -34,13 +33,18 @@ use std::{
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener},
|
||||
path::Path,
|
||||
sync::{Arc, RwLock},
|
||||
time::Duration,
|
||||
};
|
||||
use subsecond_types::JumpTable;
|
||||
use tokio::process::Command;
|
||||
use tower_http::{
|
||||
cors::Any,
|
||||
services::fs::{ServeDir, ServeFileSystemResponseBody},
|
||||
ServiceBuilderExt,
|
||||
};
|
||||
|
||||
use super::AppServer;
|
||||
|
||||
/// The webserver that serves statics assets (if fullstack isn't already doing that) and the websocket
|
||||
/// communication layer that we use to send status updates and hotreloads to the client.
|
||||
///
|
||||
@@ -52,46 +56,40 @@ pub(crate) struct WebServer {
|
||||
devserver_bind_ip: IpAddr,
|
||||
devserver_port: u16,
|
||||
proxied_port: Option<u16>,
|
||||
hot_reload_sockets: Vec<WebSocket>,
|
||||
build_status_sockets: Vec<WebSocket>,
|
||||
new_hot_reload_sockets: UnboundedReceiver<WebSocket>,
|
||||
new_build_status_sockets: UnboundedReceiver<WebSocket>,
|
||||
hot_reload_sockets: Vec<ConnectedWsClient>,
|
||||
build_status_sockets: Vec<ConnectedWsClient>,
|
||||
new_hot_reload_sockets: UnboundedReceiver<ConnectedWsClient>,
|
||||
new_build_status_sockets: UnboundedReceiver<ConnectedWsClient>,
|
||||
build_status: SharedStatus,
|
||||
application_name: String,
|
||||
platform: Platform,
|
||||
}
|
||||
|
||||
pub(crate) struct ConnectedWsClient {
|
||||
socket: WebSocket,
|
||||
build_id: Option<BuildId>,
|
||||
aslr_reference: Option<u64>,
|
||||
}
|
||||
|
||||
impl WebServer {
|
||||
pub const SELF_IP: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
|
||||
|
||||
/// Start the development server.
|
||||
/// This will set up the default http server if there's no server specified (usually via fullstack).
|
||||
///
|
||||
/// This will also start the websocket server that powers the devtools. If you want to communicate
|
||||
/// with connected devtools clients, this is the place to do it.
|
||||
pub(crate) fn start(krate: &DioxusCrate, args: &ServeArgs) -> Result<Self> {
|
||||
pub(crate) fn start(runner: &AppServer) -> Result<Self> {
|
||||
let (hot_reload_sockets_tx, hot_reload_sockets_rx) = futures_channel::mpsc::unbounded();
|
||||
let (build_status_sockets_tx, build_status_sockets_rx) = futures_channel::mpsc::unbounded();
|
||||
|
||||
const SELF_IP: IpAddr = IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0));
|
||||
|
||||
// Use 0.0.0.0 as the default address if none is specified - this will let us expose the
|
||||
// devserver to the network (for other devices like phones/embedded)
|
||||
let devserver_bind_ip = args.address.addr.unwrap_or(SELF_IP);
|
||||
|
||||
// If the user specified a port, use that, otherwise use any available port, preferring 8080
|
||||
let devserver_port = args
|
||||
.address
|
||||
.port
|
||||
.unwrap_or_else(|| get_available_port(devserver_bind_ip, Some(8080)).unwrap_or(8080));
|
||||
|
||||
// All servers will end up behind us (the devserver) but on a different port
|
||||
// This is so we can serve a loading screen as well as devtools without anything particularly fancy
|
||||
let proxied_port = args
|
||||
.should_proxy_build()
|
||||
.then(|| get_available_port(devserver_bind_ip, None))
|
||||
.flatten();
|
||||
|
||||
// Create the listener that we'll pass into the devserver, but save its IP here so
|
||||
// we can display it to the user in the tui
|
||||
let devserver_bind_ip = runner.devserver_bind_ip;
|
||||
let devserver_port = runner.devserver_port;
|
||||
let proxied_port = runner.proxied_port;
|
||||
let devserver_exposed_ip = devserver_bind_ip;
|
||||
|
||||
let devserver_bind_address = SocketAddr::new(devserver_bind_ip, devserver_port);
|
||||
let listener = std::net::TcpListener::bind(devserver_bind_address).with_context(|| {
|
||||
anyhow::anyhow!(
|
||||
@@ -99,21 +97,12 @@ impl WebServer {
|
||||
)
|
||||
})?;
|
||||
|
||||
// If the IP is 0.0.0.0, we need to get the actual IP of the machine
|
||||
// This will let ios/android/network clients connect to the devserver
|
||||
let devserver_exposed_ip = if devserver_bind_ip == SELF_IP {
|
||||
local_ip_address::local_ip().unwrap_or(devserver_bind_ip)
|
||||
} else {
|
||||
devserver_bind_ip
|
||||
};
|
||||
|
||||
let proxied_address = proxied_port.map(|port| SocketAddr::new(devserver_exposed_ip, port));
|
||||
|
||||
// Set up the router with some shared state that we'll update later to reflect the current state of the build
|
||||
let build_status = SharedStatus::new_with_starting_build();
|
||||
let router = build_devserver_router(
|
||||
args,
|
||||
krate,
|
||||
runner,
|
||||
hot_reload_sockets_tx,
|
||||
build_status_sockets_tx,
|
||||
proxied_address,
|
||||
@@ -122,7 +111,7 @@ impl WebServer {
|
||||
|
||||
// And finally, start the server mainloop
|
||||
tokio::spawn(devserver_mainloop(
|
||||
krate.config.web.https.clone(),
|
||||
runner.client().build.config.web.https.clone(),
|
||||
listener,
|
||||
router,
|
||||
));
|
||||
@@ -137,8 +126,8 @@ impl WebServer {
|
||||
build_status_sockets: Default::default(),
|
||||
new_hot_reload_sockets: hot_reload_sockets_rx,
|
||||
new_build_status_sockets: build_status_sockets_rx,
|
||||
application_name: krate.executable_name().to_string(),
|
||||
platform: args.build_arguments.platform(),
|
||||
application_name: runner.app_name().to_string(),
|
||||
platform: runner.client.build.platform,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -150,15 +139,19 @@ impl WebServer {
|
||||
.hot_reload_sockets
|
||||
.iter_mut()
|
||||
.enumerate()
|
||||
.map(|(idx, socket)| async move { (idx, socket.next().await) })
|
||||
.map(|(idx, socket)| async move { (idx, socket.socket.next().await) })
|
||||
.collect::<FuturesUnordered<_>>();
|
||||
|
||||
tokio::select! {
|
||||
new_hot_reload_socket = &mut new_hot_reload_socket => {
|
||||
if let Some(new_socket) = new_hot_reload_socket {
|
||||
let aslr_reference = new_socket.aslr_reference;
|
||||
let id = new_socket.build_id.unwrap_or(BuildId::CLIENT);
|
||||
|
||||
drop(new_message);
|
||||
self.hot_reload_sockets.push(new_socket);
|
||||
return ServeUpdate::NewConnection;
|
||||
|
||||
return ServeUpdate::NewConnection { aslr_reference, id };
|
||||
} else {
|
||||
panic!("Could not receive a socket - the devtools could not boot - the port is likely already in use");
|
||||
}
|
||||
@@ -169,8 +162,8 @@ impl WebServer {
|
||||
|
||||
// Update the socket with project info and current build status
|
||||
let project_info = SharedStatus::new(Status::ClientInit { application_name: self.application_name.clone(), platform: self.platform });
|
||||
if project_info.send_to(&mut new_socket).await.is_ok() {
|
||||
_ = self.build_status.send_to(&mut new_socket).await;
|
||||
if project_info.send_to(&mut new_socket.socket).await.is_ok() {
|
||||
_ = self.build_status.send_to(&mut new_socket.socket).await;
|
||||
self.build_status_sockets.push(new_socket);
|
||||
}
|
||||
return future::pending::<ServeUpdate>().await;
|
||||
@@ -180,7 +173,7 @@ impl WebServer {
|
||||
}
|
||||
Some((idx, message)) = new_message.next() => {
|
||||
match message {
|
||||
Some(Ok(message)) => return ServeUpdate::WsMessage(message),
|
||||
Some(Ok(msg)) => return ServeUpdate::WsMessage { msg, platform: Platform::Web },
|
||||
_ => {
|
||||
drop(new_message);
|
||||
_ = self.hot_reload_sockets.remove(idx);
|
||||
@@ -193,26 +186,9 @@ impl WebServer {
|
||||
}
|
||||
|
||||
pub(crate) async fn shutdown(&mut self) {
|
||||
if matches!(self.platform, Platform::Android) {
|
||||
use std::process::{Command, Stdio};
|
||||
if let Err(err) = Command::new("adb")
|
||||
.arg("reverse")
|
||||
.arg("--remove")
|
||||
.arg(format!("tcp:{}", self.devserver_port))
|
||||
.stderr(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.output()
|
||||
{
|
||||
tracing::error!(
|
||||
"failed to remove forwarded port {}: {err}",
|
||||
self.devserver_port
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
self.send_shutdown().await;
|
||||
for mut socket in self.hot_reload_sockets.drain(..) {
|
||||
_ = socket.send(Message::Close(None)).await;
|
||||
_ = socket.socket.send(Message::Close(None)).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,7 +197,7 @@ impl WebServer {
|
||||
let mut i = 0;
|
||||
while i < self.build_status_sockets.len() {
|
||||
let socket = &mut self.build_status_sockets[i];
|
||||
if self.build_status.send_to(socket).await.is_err() {
|
||||
if self.build_status.send_to(&mut socket.socket).await.is_err() {
|
||||
self.build_status_sockets.remove(i);
|
||||
} else {
|
||||
i += 1;
|
||||
@@ -239,13 +215,9 @@ impl WebServer {
|
||||
}
|
||||
|
||||
/// Sends an updated build status to all clients.
|
||||
pub(crate) async fn new_build_update(
|
||||
&mut self,
|
||||
update: &BuildUpdate,
|
||||
builder: &super::Builder,
|
||||
) {
|
||||
pub(crate) async fn new_build_update(&mut self, update: &BuilderUpdate) {
|
||||
match update {
|
||||
BuildUpdate::Progress { stage } => {
|
||||
BuilderUpdate::Progress { stage } => {
|
||||
// Todo(miles): wire up more messages into the splash screen UI
|
||||
match stage {
|
||||
BuildStage::Success => {}
|
||||
@@ -259,7 +231,10 @@ impl WebServer {
|
||||
krate,
|
||||
..
|
||||
} => {
|
||||
if !builder.is_finished() {
|
||||
if !matches!(
|
||||
self.build_status.get(),
|
||||
Status::Ready | Status::BuildError { .. }
|
||||
) {
|
||||
self.build_status.set(Status::Building {
|
||||
progress: (*current as f64 / *total as f64).clamp(0.0, 1.0),
|
||||
build_message: format!("{krate} compiling"),
|
||||
@@ -273,9 +248,9 @@ impl WebServer {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
BuildUpdate::CompilerMessage { .. } => {}
|
||||
BuildUpdate::BuildReady { .. } => {}
|
||||
BuildUpdate::BuildFailed { err } => {
|
||||
BuilderUpdate::CompilerMessage { .. } => {}
|
||||
BuilderUpdate::BuildReady { .. } => {}
|
||||
BuilderUpdate::BuildFailed { err } => {
|
||||
let error = err.to_string();
|
||||
self.build_status.set(Status::BuildError {
|
||||
error: ansi_to_html::convert(&error).unwrap_or(error),
|
||||
@@ -283,6 +258,9 @@ impl WebServer {
|
||||
self.send_reload_failed().await;
|
||||
self.send_build_status().await;
|
||||
}
|
||||
BuilderUpdate::StdoutReceived { .. } => {}
|
||||
BuilderUpdate::StderrReceived { .. } => {}
|
||||
BuilderUpdate::ProcessExited { .. } => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,7 +270,6 @@ impl WebServer {
|
||||
return;
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
tracing::trace!("Sending hotreload to clients {:?}", reload);
|
||||
|
||||
let msg = DevserverMsg::HotReload(reload);
|
||||
@@ -303,6 +280,7 @@ impl WebServer {
|
||||
while i < self.hot_reload_sockets.len() {
|
||||
let socket = &mut self.hot_reload_sockets[i];
|
||||
if socket
|
||||
.socket
|
||||
.send(Message::Text(msg.clone().into()))
|
||||
.await
|
||||
.is_err()
|
||||
@@ -314,15 +292,37 @@ impl WebServer {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn send_patch(
|
||||
&mut self,
|
||||
jump_table: JumpTable,
|
||||
time_taken: Duration,
|
||||
build: BuildId,
|
||||
) {
|
||||
let msg = DevserverMsg::HotReload(HotReloadMsg {
|
||||
jump_table: Some(jump_table),
|
||||
ms_elapsed: time_taken.as_millis() as u64,
|
||||
templates: vec![],
|
||||
assets: vec![],
|
||||
for_build_id: Some(build.0 as _),
|
||||
});
|
||||
self.send_devserver_message_to_all(msg).await;
|
||||
}
|
||||
|
||||
/// Tells all clients that a hot patch has started.
|
||||
pub(crate) async fn send_patch_start(&mut self) {
|
||||
self.send_devserver_message_to_all(DevserverMsg::HotPatchStart)
|
||||
.await;
|
||||
}
|
||||
|
||||
/// Tells all clients that a full rebuild has started.
|
||||
pub(crate) async fn send_reload_start(&mut self) {
|
||||
self.send_devserver_message(DevserverMsg::FullReloadStart)
|
||||
self.send_devserver_message_to_all(DevserverMsg::FullReloadStart)
|
||||
.await;
|
||||
}
|
||||
|
||||
/// Tells all clients that a full rebuild has failed.
|
||||
pub(crate) async fn send_reload_failed(&mut self) {
|
||||
self.send_devserver_message(DevserverMsg::FullReloadFailed)
|
||||
self.send_devserver_message_to_all(DevserverMsg::FullReloadFailed)
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -334,19 +334,21 @@ impl WebServer {
|
||||
|
||||
self.build_status.set(Status::Ready);
|
||||
self.send_build_status().await;
|
||||
self.send_devserver_message(DevserverMsg::FullReloadCommand)
|
||||
self.send_devserver_message_to_all(DevserverMsg::FullReloadCommand)
|
||||
.await;
|
||||
}
|
||||
|
||||
/// Send a shutdown message to all connected clients.
|
||||
pub(crate) async fn send_shutdown(&mut self) {
|
||||
self.send_devserver_message(DevserverMsg::Shutdown).await;
|
||||
self.send_devserver_message_to_all(DevserverMsg::Shutdown)
|
||||
.await;
|
||||
}
|
||||
|
||||
/// Sends a devserver message to all connected clients.
|
||||
async fn send_devserver_message(&mut self, msg: DevserverMsg) {
|
||||
async fn send_devserver_message_to_all(&mut self, msg: DevserverMsg) {
|
||||
for socket in self.hot_reload_sockets.iter_mut() {
|
||||
_ = socket
|
||||
.socket
|
||||
.send(Message::Text(serde_json::to_string(&msg).unwrap().into()))
|
||||
.await;
|
||||
}
|
||||
@@ -424,23 +426,24 @@ async fn devserver_mainloop(
|
||||
/// - Setting up the file serve service
|
||||
/// - Setting up the websocket endpoint for devtools
|
||||
fn build_devserver_router(
|
||||
args: &ServeArgs,
|
||||
krate: &DioxusCrate,
|
||||
hot_reload_sockets: UnboundedSender<WebSocket>,
|
||||
build_status_sockets: UnboundedSender<WebSocket>,
|
||||
runner: &AppServer,
|
||||
hot_reload_sockets: UnboundedSender<ConnectedWsClient>,
|
||||
build_status_sockets: UnboundedSender<ConnectedWsClient>,
|
||||
fullstack_address: Option<SocketAddr>,
|
||||
build_status: SharedStatus,
|
||||
) -> Result<Router> {
|
||||
let mut router = Router::new();
|
||||
let build = runner.client();
|
||||
|
||||
// Setup proxy for the endpoint specified in the config
|
||||
for proxy_config in krate.config.web.proxy.iter() {
|
||||
for proxy_config in build.build.config.web.proxy.iter() {
|
||||
router = super::proxy::add_proxy(router, proxy_config)?;
|
||||
}
|
||||
|
||||
if args.should_proxy_build() {
|
||||
// For fullstack, liveview, and server, forward all requests to the inner server
|
||||
let address = fullstack_address.unwrap();
|
||||
// For fullstack, liveview, and server, forward all requests to the inner server
|
||||
if runner.proxied_port.is_some() {
|
||||
tracing::debug!("Proxying requests to fullstack server at {fullstack_address:?}");
|
||||
let address = fullstack_address.context("No fullstack address specified")?;
|
||||
tracing::debug!("Proxying requests to fullstack server at {address}");
|
||||
router = router.fallback_service(super::proxy::proxy_to(
|
||||
format!("http://{address}").parse().unwrap(),
|
||||
@@ -460,7 +463,9 @@ fn build_devserver_router(
|
||||
// Route file service to output the .wasm and assets if this is a web build
|
||||
let base_path = format!(
|
||||
"/{}",
|
||||
krate
|
||||
runner
|
||||
.client()
|
||||
.build
|
||||
.config
|
||||
.web
|
||||
.app
|
||||
@@ -470,9 +475,9 @@ fn build_devserver_router(
|
||||
.trim_matches('/')
|
||||
);
|
||||
if base_path == "/" {
|
||||
router = router.fallback_service(build_serve_dir(args, krate));
|
||||
router = router.fallback_service(build_serve_dir(runner));
|
||||
} else {
|
||||
router = router.nest_service(&base_path, build_serve_dir(args, krate));
|
||||
router = router.nest_service(&base_path, build_serve_dir(runner));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -482,6 +487,12 @@ fn build_devserver_router(
|
||||
build_status_middleware,
|
||||
));
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct ConnectionQuery {
|
||||
aslr_reference: Option<u64>,
|
||||
build_id: Option<BuildId>,
|
||||
}
|
||||
|
||||
// Setup websocket endpoint - and pass in the extension layer immediately after
|
||||
router = router.nest(
|
||||
"/_dioxus",
|
||||
@@ -489,9 +500,9 @@ fn build_devserver_router(
|
||||
.route(
|
||||
"/",
|
||||
get(
|
||||
|ws: WebSocketUpgrade, ext: Extension<UnboundedSender<WebSocket>>| async move {
|
||||
tracing::debug!("New devtool websocket connection");
|
||||
ws.on_upgrade(move |socket| async move { _ = ext.0.unbounded_send(socket) })
|
||||
|ws: WebSocketUpgrade, ext: Extension<UnboundedSender<ConnectedWsClient>>, query: Query<ConnectionQuery>| async move {
|
||||
tracing::debug!("New devtool websocket connection: {:?}", query);
|
||||
ws.on_upgrade(move |socket| async move { _ = ext.0.unbounded_send(ConnectedWsClient { socket, aslr_reference: query.aslr_reference, build_id: query.build_id }) })
|
||||
},
|
||||
),
|
||||
)
|
||||
@@ -499,8 +510,8 @@ fn build_devserver_router(
|
||||
.route(
|
||||
"/build_status",
|
||||
get(
|
||||
|ws: WebSocketUpgrade, ext: Extension<UnboundedSender<WebSocket>>| async move {
|
||||
ws.on_upgrade(move |socket| async move { _ = ext.0.unbounded_send(socket) })
|
||||
|ws: WebSocketUpgrade, ext: Extension<UnboundedSender<ConnectedWsClient>>| async move {
|
||||
ws.on_upgrade(move |socket| async move { _ = ext.0.unbounded_send(ConnectedWsClient { socket, aslr_reference: None, build_id: None }) })
|
||||
},
|
||||
),
|
||||
)
|
||||
@@ -520,7 +531,7 @@ fn build_devserver_router(
|
||||
Ok(router)
|
||||
}
|
||||
|
||||
fn build_serve_dir(args: &ServeArgs, cfg: &DioxusCrate) -> axum::routing::MethodRouter {
|
||||
fn build_serve_dir(runner: &AppServer) -> axum::routing::MethodRouter {
|
||||
use tower::ServiceBuilder;
|
||||
|
||||
static CORS_UNSAFE: (HeaderValue, HeaderValue) = (
|
||||
@@ -533,15 +544,19 @@ fn build_serve_dir(args: &ServeArgs, cfg: &DioxusCrate) -> axum::routing::Method
|
||||
HeaderValue::from_static("same-origin"),
|
||||
);
|
||||
|
||||
let (coep, coop) = match args.cross_origin_policy {
|
||||
let (coep, coop) = match runner.cross_origin_policy {
|
||||
true => CORS_REQUIRE.clone(),
|
||||
false => CORS_UNSAFE.clone(),
|
||||
};
|
||||
|
||||
let out_dir = cfg
|
||||
.build_dir(Platform::Web, args.build_arguments.release)
|
||||
let app = &runner.client;
|
||||
let cfg = &runner.client.build.config;
|
||||
|
||||
let out_dir = app
|
||||
.build
|
||||
.build_dir(Platform::Web, app.build.release)
|
||||
.join("public");
|
||||
let index_on_404 = cfg.config.web.watcher.index_on_404;
|
||||
let index_on_404: bool = cfg.web.watcher.index_on_404;
|
||||
|
||||
get_service(
|
||||
ServiceBuilder::new()
|
||||
@@ -641,7 +656,7 @@ async fn get_rustls(web_config: &WebHttpsConfig) -> Result<(String, String)> {
|
||||
_ = fs::create_dir("ssl");
|
||||
}
|
||||
|
||||
let cmd = tokio::process::Command::new("mkcert")
|
||||
let cmd = Command::new("mkcert")
|
||||
.args([
|
||||
"-install",
|
||||
"-key-file",
|
||||
@@ -673,25 +688,6 @@ async fn get_rustls(web_config: &WebHttpsConfig) -> Result<(String, String)> {
|
||||
|
||||
Ok((cert_path, key_path))
|
||||
}
|
||||
/// Bind a listener to any point and return it
|
||||
/// When the listener is dropped, the socket will be closed, but we'll still have a port that we
|
||||
/// can bind our proxy to.
|
||||
///
|
||||
/// Todo: we might want to do this on every new build in case the OS tries to bind things to this port
|
||||
/// and we don't already have something bound to it. There's no great way of "reserving" a port.
|
||||
fn get_available_port(address: IpAddr, prefer: Option<u16>) -> Option<u16> {
|
||||
// First, try to bind to the preferred port
|
||||
if let Some(port) = prefer {
|
||||
if let Ok(_listener) = TcpListener::bind((address, port)) {
|
||||
return Some(port);
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, try to bind to any port and return the first one we can
|
||||
TcpListener::bind((address, 0))
|
||||
.map(|listener| listener.local_addr().unwrap().port())
|
||||
.ok()
|
||||
}
|
||||
|
||||
/// Middleware that intercepts html requests if the status is "Building" and returns a loading page instead
|
||||
async fn build_status_middleware(
|
||||
@@ -708,7 +704,7 @@ async fn build_status_middleware(
|
||||
if let Some(true) = accepts_html {
|
||||
let status = state.get();
|
||||
if status != Status::Ready {
|
||||
let html = include_str!("../../assets/web/loading.html");
|
||||
let html = include_str!("../../assets/web/dev.loading.html");
|
||||
return axum::response::Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
// Load the html loader then keep loading forever
|
||||
|
||||
@@ -1,46 +1,31 @@
|
||||
use crate::{BuildUpdate, Platform, TraceMsg};
|
||||
use crate::{BuildId, BuilderUpdate, Platform, TraceMsg};
|
||||
use axum::extract::ws::Message as WsMessage;
|
||||
use std::{path::PathBuf, process::ExitStatus};
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// One fat enum to rule them all....
|
||||
///
|
||||
/// Thanks to libraries like winit for the inspiration
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub(crate) enum ServeUpdate {
|
||||
NewConnection,
|
||||
WsMessage(WsMessage),
|
||||
|
||||
/// A build update from the build engine
|
||||
BuildUpdate(BuildUpdate),
|
||||
|
||||
/// A running process has received a stdout.
|
||||
/// May or may not be a complete line - do not treat it as a line. It will include a line if it is a complete line.
|
||||
///
|
||||
/// We will poll lines and any content in a 50ms interval
|
||||
StdoutReceived {
|
||||
NewConnection {
|
||||
id: BuildId,
|
||||
aslr_reference: Option<u64>,
|
||||
},
|
||||
WsMessage {
|
||||
platform: Platform,
|
||||
msg: String,
|
||||
msg: WsMessage,
|
||||
},
|
||||
|
||||
/// A running process has received a stderr.
|
||||
/// May or may not be a complete line - do not treat it as a line. It will include a line if it is a complete line.
|
||||
///
|
||||
/// We will poll lines and any content in a 50ms interval
|
||||
StderrReceived {
|
||||
platform: Platform,
|
||||
msg: String,
|
||||
},
|
||||
|
||||
ProcessExited {
|
||||
platform: Platform,
|
||||
status: ExitStatus,
|
||||
/// An update regarding the state of the build and running app from an AppBuilder
|
||||
BuilderUpdate {
|
||||
id: BuildId,
|
||||
update: BuilderUpdate,
|
||||
},
|
||||
|
||||
FilesChanged {
|
||||
files: Vec<PathBuf>,
|
||||
},
|
||||
|
||||
/// Open an existing app bundle, if it exists
|
||||
OpenApp,
|
||||
|
||||
RequestRebuild,
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
use super::detect::is_wsl;
|
||||
use super::update::ServeUpdate;
|
||||
use crate::{cli::serve::ServeArgs, dioxus_crate::DioxusCrate};
|
||||
use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender};
|
||||
use futures_util::StreamExt;
|
||||
use notify::{
|
||||
event::{MetadataKind, ModifyKind},
|
||||
Config, EventKind, RecursiveMode, Watcher as NotifyWatcher,
|
||||
};
|
||||
use std::{path::PathBuf, time::Duration};
|
||||
|
||||
/// This struct stores the file watcher and the filemap for the project.
|
||||
///
|
||||
/// This is where we do workspace discovery and recursively listen for changes in Rust files and asset
|
||||
/// directories.
|
||||
pub(crate) struct Watcher {
|
||||
rx: UnboundedReceiver<notify::Event>,
|
||||
krate: DioxusCrate,
|
||||
_tx: UnboundedSender<notify::Event>,
|
||||
watcher: Box<dyn notify::Watcher>,
|
||||
}
|
||||
|
||||
impl Watcher {
|
||||
pub(crate) fn start(krate: &DioxusCrate, serve: &ServeArgs) -> Self {
|
||||
let (tx, rx) = futures_channel::mpsc::unbounded();
|
||||
|
||||
let mut watcher = Self {
|
||||
watcher: create_notify_watcher(serve, tx.clone()),
|
||||
_tx: tx,
|
||||
krate: krate.clone(),
|
||||
rx,
|
||||
};
|
||||
|
||||
watcher.watch_filesystem();
|
||||
|
||||
watcher
|
||||
}
|
||||
|
||||
/// Wait for changed files to be detected
|
||||
pub(crate) async fn wait(&mut self) -> ServeUpdate {
|
||||
// Wait for the next file to change
|
||||
let mut changes: Vec<_> = self.rx.next().await.into_iter().collect();
|
||||
|
||||
// Dequeue in bulk if we can, we might've received a lot of events in one go
|
||||
while let Some(event) = self.rx.try_next().ok().flatten() {
|
||||
changes.push(event);
|
||||
}
|
||||
|
||||
// Filter the changes
|
||||
let mut files: Vec<PathBuf> = vec![];
|
||||
|
||||
// Decompose the events into a list of all the files that have changed
|
||||
for event in changes.drain(..) {
|
||||
// Make sure we add new folders to the watch list, provided they're not matched by the ignore list
|
||||
// We'll only watch new folders that are found under the crate, and then update our watcher to watch them
|
||||
// This unfortunately won't pick up new krates added "at a distance" - IE krates not within the workspace.
|
||||
if let EventKind::Create(_create_kind) = event.kind {
|
||||
// If it's a new folder, watch it
|
||||
// If it's a new cargo.toml (ie dep on the fly),
|
||||
// todo(jon) support new folders on the fly
|
||||
}
|
||||
|
||||
for path in event.paths {
|
||||
// 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) = std::fs::metadata(&path) {
|
||||
if metadata.len() == 0 {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
files.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
tracing::debug!("Files changed: {files:?}");
|
||||
|
||||
ServeUpdate::FilesChanged { files }
|
||||
}
|
||||
|
||||
fn watch_filesystem(&mut self) {
|
||||
// Watch the folders of the crates that we're interested in
|
||||
for path in self.krate.watch_paths() {
|
||||
tracing::debug!("Watching path {path:?}");
|
||||
|
||||
if let Err(err) = self.watcher.watch(&path, RecursiveMode::Recursive) {
|
||||
handle_notify_error(err);
|
||||
}
|
||||
}
|
||||
|
||||
// Also watch the crates themselves, but not recursively, such that we can pick up new folders
|
||||
for krate in self.krate.all_watched_crates() {
|
||||
tracing::debug!("Watching path {krate:?}");
|
||||
if let Err(err) = self.watcher.watch(&krate, RecursiveMode::NonRecursive) {
|
||||
handle_notify_error(err);
|
||||
}
|
||||
}
|
||||
|
||||
// Also watch the workspace dir, non recursively, such that we can pick up new folders there too
|
||||
if let Err(err) = self
|
||||
.watcher
|
||||
.watch(&self.krate.workspace_dir(), RecursiveMode::NonRecursive)
|
||||
{
|
||||
handle_notify_error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_notify_error(err: notify::Error) {
|
||||
tracing::debug!("Failed to watch path: {}", err);
|
||||
match err.kind {
|
||||
notify::ErrorKind::Io(error) if error.kind() == std::io::ErrorKind::PermissionDenied => {
|
||||
tracing::error!("Failed to watch path: permission denied. {:?}", err.paths)
|
||||
}
|
||||
notify::ErrorKind::MaxFilesWatch => {
|
||||
tracing::error!("Failed to set up file watcher: too many files to watch")
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_notify_watcher(
|
||||
serve: &ServeArgs,
|
||||
tx: UnboundedSender<notify::Event>,
|
||||
) -> Box<dyn NotifyWatcher> {
|
||||
// Build the event handler for notify.
|
||||
let handler = move |info: notify::Result<notify::Event>| {
|
||||
let Ok(event) = info else {
|
||||
return;
|
||||
};
|
||||
|
||||
let is_allowed_notify_event = match event.kind {
|
||||
EventKind::Modify(ModifyKind::Data(_)) => true,
|
||||
EventKind::Modify(ModifyKind::Name(_)) => true,
|
||||
// The primary modification event on WSL's poll watcher.
|
||||
EventKind::Modify(ModifyKind::Metadata(MetadataKind::WriteTime)) => true,
|
||||
// Catch-all for unknown event types (windows)
|
||||
EventKind::Modify(ModifyKind::Any) => true,
|
||||
EventKind::Modify(ModifyKind::Metadata(_)) => false,
|
||||
// Don't care about anything else.
|
||||
EventKind::Create(_) => true,
|
||||
EventKind::Remove(_) => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if is_allowed_notify_event {
|
||||
_ = tx.unbounded_send(event);
|
||||
}
|
||||
};
|
||||
|
||||
const NOTIFY_ERROR_MSG: &str = "Failed to create file watcher.\nEnsure you have the required permissions to watch the specified directories.";
|
||||
|
||||
if !is_wsl() {
|
||||
return Box::new(notify::recommended_watcher(handler).expect(NOTIFY_ERROR_MSG));
|
||||
}
|
||||
|
||||
let poll_interval = Duration::from_secs(serve.wsl_file_poll_interval.unwrap_or(2) as u64);
|
||||
|
||||
Box::new(
|
||||
notify::PollWatcher::new(handler, Config::default().with_poll_interval(poll_interval))
|
||||
.expect(NOTIFY_ERROR_MSG),
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user