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:
Jonathan Kelley authored and GitHub committed 2025-05-06 23:08:59 -07:00
1 parent 71d85e6dd5
commit d976ca184a
142 files changed
+17982 -7156

No files matched your search

+127 -174
View File
@@ -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(())
}