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
+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(())
|
||||
}
|
||||
Reference in new issue
Block a user