From bee89b795d2d3195e8c8142fbcdedb04e7dbe07c Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 6 Oct 2025 16:12:18 -0700 Subject: [PATCH] fix: hotpatching fullstack (#4730) * fix: hotpatching fullstack * add hot-patching fullstack test * add caching to playwright apt-get * only run playwright on beta * install binutils? * dont commit lockfile * ignore lockfiles * make sure ld is in path --- .github/workflows/main.yml | 7 +- .gitignore | 3 + packages/cli/src/build/builder.rs | 5 +- packages/cli/src/serve/mod.rs | 31 ++--- packages/cli/src/serve/runner.rs | 62 ++++++++-- .../playwright-tests/playwright.config.js | 8 ++ .../web-hot-patch-fullstack/.gitignore | 2 + .../web-hot-patch-fullstack/Cargo.toml | 16 +++ .../assets/alternative-style.css | 3 + .../web-hot-patch-fullstack/assets/style.css | 3 + .../web-hot-patch-fullstack/assets/toasts.png | Bin 0 -> 20366 bytes .../web-hot-patch-fullstack/src/main.rs | 36 ++++++ .../playwright-tests/web-hot-patch/Cargo.toml | 4 +- .../web-patch-fullstack.spec.js | 113 ++++++++++++++++++ packages/subsecond/subsecond/src/lib.rs | 10 ++ 15 files changed, 265 insertions(+), 38 deletions(-) create mode 100644 packages/playwright-tests/web-hot-patch-fullstack/.gitignore create mode 100644 packages/playwright-tests/web-hot-patch-fullstack/Cargo.toml create mode 100644 packages/playwright-tests/web-hot-patch-fullstack/assets/alternative-style.css create mode 100644 packages/playwright-tests/web-hot-patch-fullstack/assets/style.css create mode 100644 packages/playwright-tests/web-hot-patch-fullstack/assets/toasts.png create mode 100644 packages/playwright-tests/web-hot-patch-fullstack/src/main.rs create mode 100644 packages/playwright-tests/web-patch-fullstack.spec.js diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4a1b877e0..6c0691d6c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -224,14 +224,18 @@ jobs: strategy: matrix: platform: - - { toolchain: 1.86.0 } - { toolchain: beta } + # - { toolchain: 1.86.0 } steps: # Do our best to cache the toolchain and node install steps - uses: actions/checkout@v5 - name: Free Disk Space if: ${{ matrix.platform.os == 'ubuntu-24.04' || matrix.platform.os == 'ubuntu-24.04-arm' || matrix.platform.os == 'blacksmith-4vcpu-ubuntu-2404' || matrix.platform.os == 'blacksmith-4vcpu-ubuntu-2404-arm' }} uses: ./.github/actions/free-disk-space + - uses: awalsh128/cache-apt-pkgs-action@latest + with: + packages: glib-networking glib-networking-common glib-networking-services libaa1 libabsl20220623t64 libass9 libasyncns0 libavc1394-0 libavcodec60 libavfilter9 libavformat60 libavtp0 libavutil58 libblas3 libbluray2 libbs2b0 libcaca0 libcairo-script-interpreter2 libcdparanoia0 libchromaprint1 libcjson1 libcodec2-1.2 libdav1d7 libdc1394-25 libdca0 libdecor-0-0 libdirectfb-1.7-7t64 libdv4t64 libdvdnav4 libdvdread8t64 libfaad2 libflac12t64 libfluidsynth3 libfreeaptx0 libgav1-1 libgme0 libgraphene-1.0-0 libgsm1 libgssdp-1.6-0 libgstreamer-plugins-good1.0-0 libgtk-4-common libgupnp-1.6-0 libgupnp-igd-1.6-0 libhwy1t64 libiec61883-0 libimath-3-1-29t64 libinstpatch-1.0-2 libjack-jackd2-0 libjxl0.7 liblapack3 liblc3-1 libldacbt-enc2 liblilv-0-0 liblrdf0 libltc11 libmbedcrypto7t64 libmfx1 libmjpegutils-2.1-0t64 libmodplug1 libmp3lame0 libmpcdec6 libmpeg2encpp-2.1-0t64 libmpg123-0t64 libmplex2-2.1-0t64 libmysofa1 libneon27t64 libnice10 libogg0 libopenal-data libopenal1 libopenexr-3-1-30 libopenh264-7 libopenmpt0t64 libopenni2-0 liborc-0.4-0t64 libpipewire-0.3-0t64 libplacebo338 libpocketsphinx3 libpostproc57 libproxy1v5 libpulse0 libqrencode4 libraptor2-0 librav1e0 libraw1394-11 librist4 librubberband2 libsamplerate0 libsbc1 libsdl2-2.0-0 libsecret-common libserd-0-0 libshine3 libshout3 libsndfile1 libsndio7.0 libsord-0-0 libsoundtouch1 libsoup-3.0-0 libsoup-3.0-common libsoxr0 libspa-0.2-modules libspandsp2t64 libspeex1 libsphinxbase3t64 libsratom-0-0 libsrt1.5-gnutls libsrtp2-1 libssh-gcrypt-4 libsvtav1enc1d1 libswresample4 libswscale7 libtag1v5 libtag1v5-vanilla libtheora0 libtwolame0 libudfread0 libunibreak5 libv4l-0t64 libv4lconvert0t64 libva-drm2 libva-x11-2 libva2 libvdpau1 libvidstab1.1 libvisual-0.4-0 libvo-aacenc0 libvo-amrwbenc0 libvorbis0a libvorbisenc2 libvorbisfile3 libvpl2 libwavpack1 libwebrtc-audio-processing1 libwildmidi2 libx265-199 libxcb-xkb1 libxkbcommon-x11-0 libxml2-dev libxvidcore4 libyuv0 libzbar0t64 libzimg2 libzix-0-0 libzvbi-common libzvbi0t64 libzxing3 ocl-icd-libopencl1 timgm6mb-soundfont xfonts-encodings xfonts-utils binutils lld binutils-devel binutils-gold + version: 1.0 - uses: actions/setup-node@v4 with: node-version: 24 @@ -254,6 +258,7 @@ jobs: # The hot patch test requires incremental compilation CARGO_INCREMENTAL: 1 run: | + export PATH=$PATH:/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin npm ci npm install -D @playwright/test npx playwright install --with-deps diff --git a/.gitignore b/.gitignore index 2fbbcaf05..a95c90b44 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,9 @@ /packages/playwright-tests/fullstack/dist /packages/playwright-tests/test-results /packages/playwright-tests/web-hot-patch-temp +/packages/playwright-tests/web-hot-patch-fullstack-temp +/packages/playwright-tests/web-hot-patch/Cargo.lock +/packages/playwright-tests/web-hot-patch-fullstack/Cargo.lock /dist .DS_Store /examples/assets/test_video.mp4 diff --git a/packages/cli/src/build/builder.rs b/packages/cli/src/build/builder.rs index 0064b77d2..33c4fa83c 100644 --- a/packages/cli/src/build/builder.rs +++ b/packages/cli/src/build/builder.rs @@ -715,7 +715,7 @@ impl AppBuilder { let changed_file = changed_files.first().unwrap(); tracing::info!( - "Hot-patching: {NOTE_STYLE}{}{NOTE_STYLE:#} took {GLOW_STYLE}{:?}ms{GLOW_STYLE:#} ({})", + "Hot-patching: {NOTE_STYLE}{}{NOTE_STYLE:#} took {GLOW_STYLE}{:?}ms{GLOW_STYLE:#}", changed_file .display() .to_string() @@ -723,8 +723,7 @@ impl AppBuilder { SystemTime::now() .duration_since(res.time_start) .unwrap() - .as_millis(), - triple + .as_millis() ); self.patches.push(jump_table.clone()); diff --git a/packages/cli/src/serve/mod.rs b/packages/cli/src/serve/mod.rs index 93af8d979..7e54e8abf 100644 --- a/packages/cli/src/serve/mod.rs +++ b/packages/cli/src/serve/mod.rs @@ -175,27 +175,18 @@ pub(crate) async fn serve_all(args: ServeArgs, tracer: &TraceController) -> Resu } 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) => { - let pid = match id { - BuildId::CLIENT => builder.client.pid, - _ => builder.server.as_ref().and_then(|s| s.pid), - }; - devserver.send_patch(jumptable, elapsed, id, pid).await - } - Err(err) => { - tracing::error!("Failed to hot-patch app: {err}"); + if let Err(err) = + builder.hotpatch(&bundle, id, cache, &mut devserver).await + { + tracing::error!("Failed to hot-patch app: {err}"); - if let Some(_patching) = - err.downcast_ref::() - { - tracing::info!("Starting full rebuild: {err}"); - builder.full_rebuild().await; - devserver.send_reload_start().await; - devserver.start_build().await; - } + if let Some(_patching) = + err.downcast_ref::() + { + tracing::info!("Starting full rebuild: {err}"); + builder.full_rebuild().await; + devserver.send_reload_start().await; + devserver.start_build().await; } } } diff --git a/packages/cli/src/serve/runner.rs b/packages/cli/src/serve/runner.rs index ba19a3c52..211e5b0b1 100644 --- a/packages/cli/src/serve/runner.rs +++ b/packages/cli/src/serve/runner.rs @@ -28,7 +28,6 @@ use std::{ sync::Arc, time::Duration, }; -use subsecond_types::JumpTable; use syn::spanned::Spanned; use tokio::process::Command; @@ -53,7 +52,7 @@ pub(crate) struct AppServer { pub(crate) watcher_rx: UnboundedReceiver, // Tracked state related to open builds and hot reloading - pub(crate) applied_hot_reload_message: HotReloadMsg, + pub(crate) applied_client_hot_reload_message: HotReloadMsg, pub(crate) file_map: HashMap, // Resolved args related to how we go about processing the rebuilds and logging @@ -185,7 +184,7 @@ impl AppServer { // Create the runner let mut runner = Self { file_map: Default::default(), - applied_hot_reload_message: Default::default(), + applied_client_hot_reload_message: Default::default(), automatic_rebuilds: true, watch_fs, use_hotpatch_engine, @@ -709,27 +708,66 @@ impl AppServer { pub(crate) async fn hotpatch( &mut self, - res: &BuildArtifacts, + bundle: &BuildArtifacts, id: BuildId, cache: &HotpatchModuleCache, - ) -> Result { + devserver: &mut WebServer, + ) -> Result<()> { + let elapsed = bundle + .time_end + .duration_since(bundle.time_start) + .unwrap_or_default(); + let jump_table = match id { - BuildId::CLIENT => self.client.hotpatch(res, cache).await, + BuildId::CLIENT => self.client.hotpatch(bundle, cache).await, BuildId::SERVER => { self.server .as_mut() .context("Server not found")? - .hotpatch(res, cache) + .hotpatch(bundle, cache) .await } _ => bail!("Invalid build id"), }?; if id == BuildId::CLIENT { - self.applied_hot_reload_message.jump_table = self.client.patches.last().cloned(); + self.applied_client_hot_reload_message.jump_table = self.client.patches.last().cloned(); } - Ok(jump_table) + // If no server, just send the patch immediately + let Some(server) = self.server.as_mut() else { + devserver + .send_patch(jump_table, elapsed, id, self.client.pid) + .await; + return Ok(()); + }; + + // If we have a server, we need to wait until both the client and server are ready + // Otherwise we end up with an annoying race condition where the client can't actually load the patch + if self.client.stage == BuildStage::Success && server.stage == BuildStage::Success { + let client_jump_table = self + .client + .patches + .last() + .cloned() + .context("Missing client jump table")?; + + let server_jump_table = server + .patches + .last() + .cloned() + .context("Missing server jump table")?; + + devserver + .send_patch(server_jump_table, elapsed, BuildId::SERVER, server.pid) + .await; + + devserver + .send_patch(client_jump_table, elapsed, BuildId::CLIENT, self.client.pid) + .await; + } + + Ok(()) } pub(crate) fn get_build(&self, id: BuildId) -> Option<&AppBuilder> { @@ -751,7 +789,7 @@ impl AppServer { /// Get any hot reload changes that have been applied since the last full rebuild pub(crate) fn applied_hot_reload_changes(&mut self, build: BuildId) -> HotReloadMsg { - let mut msg = self.applied_hot_reload_message.clone(); + let mut msg = self.applied_client_hot_reload_message.clone(); if build == BuildId::CLIENT { msg.jump_table = self.client.patches.last().cloned(); @@ -773,7 +811,7 @@ impl AppServer { /// Clear the hot reload changes. This should be called any time a new build is starting pub(crate) fn clear_hot_reload_changes(&mut self) { - self.applied_hot_reload_message = Default::default(); + self.applied_client_hot_reload_message = Default::default(); } pub(crate) fn clear_patches(&mut self) { @@ -836,7 +874,7 @@ impl AppServer { /// Store the hot reload changes for any future clients that connect fn add_hot_reload_message(&mut self, msg: &HotReloadMsg) { - let applied = &mut self.applied_hot_reload_message; + let applied = &mut self.applied_client_hot_reload_message; // Merge the assets, unknown files, and templates // We keep the newer change if there is both a old and new change diff --git a/packages/playwright-tests/playwright.config.js b/packages/playwright-tests/playwright.config.js index 2f1c95723..733a75586 100644 --- a/packages/playwright-tests/playwright.config.js +++ b/packages/playwright-tests/playwright.config.js @@ -207,5 +207,13 @@ module.exports = defineConfig({ reuseExistingServer: !process.env.CI, stdout: "pipe", }, + { + command: + 'rm -rf ./web-hot-patch-fullstack-temp && cp -r ./web-hot-patch-fullstack ./web-hot-patch-fullstack-temp && cd web-hot-patch-fullstack-temp && cargo run --manifest-path ../../cli/Cargo.toml --release -- serve --verbose --force-sequential --web --addr "127.0.0.1" --port 9981 --hot-patch --exit-on-error', + port: 9981, + timeout: 50 * 60 * 1000, + reuseExistingServer: !process.env.CI, + stdout: "pipe", + }, ], }); diff --git a/packages/playwright-tests/web-hot-patch-fullstack/.gitignore b/packages/playwright-tests/web-hot-patch-fullstack/.gitignore new file mode 100644 index 000000000..283c01d61 --- /dev/null +++ b/packages/playwright-tests/web-hot-patch-fullstack/.gitignore @@ -0,0 +1,2 @@ +dist +target diff --git a/packages/playwright-tests/web-hot-patch-fullstack/Cargo.toml b/packages/playwright-tests/web-hot-patch-fullstack/Cargo.toml new file mode 100644 index 000000000..c03540e1a --- /dev/null +++ b/packages/playwright-tests/web-hot-patch-fullstack/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "dioxus-playwright-fullstack-patch-test" +version = "0.0.1" +edition = "2021" +description = "Playwright test for Dioxus Web" +license = "MIT OR Apache-2.0" +publish = false + +[dependencies] +dioxus = { path = "../../dioxus", features = ["fullstack"] } + +[features] +web = ["dioxus/web"] +server = ["dioxus/server"] + +[workspace] diff --git a/packages/playwright-tests/web-hot-patch-fullstack/assets/alternative-style.css b/packages/playwright-tests/web-hot-patch-fullstack/assets/alternative-style.css new file mode 100644 index 000000000..bd621de2d --- /dev/null +++ b/packages/playwright-tests/web-hot-patch-fullstack/assets/alternative-style.css @@ -0,0 +1,3 @@ +body { + background-color: rgb(100, 100, 100); +} diff --git a/packages/playwright-tests/web-hot-patch-fullstack/assets/style.css b/packages/playwright-tests/web-hot-patch-fullstack/assets/style.css new file mode 100644 index 000000000..8dcca08c7 --- /dev/null +++ b/packages/playwright-tests/web-hot-patch-fullstack/assets/style.css @@ -0,0 +1,3 @@ +#main { + background-color: red; +} diff --git a/packages/playwright-tests/web-hot-patch-fullstack/assets/toasts.png b/packages/playwright-tests/web-hot-patch-fullstack/assets/toasts.png new file mode 100644 index 0000000000000000000000000000000000000000..382298f9c7c658635ad6e2635e98ba265148cace GIT binary patch literal 20366 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 Element { + let mut num = use_signal(|| 0); + + rsx! { + document::Link { + href: CSS, + rel: "stylesheet", + } + img { + id: "toasts", + src: IMAGE, + } + button { + id: "increment-button", + onclick: move |_| async move { + let increment_amount = get_count().await.unwrap(); + *num.write() += increment_amount; + }, + "Click me! Count: {num}" + } + } +} + +#[post("/api/get_count")] +async fn get_count() -> Result { + Ok(1) +} + +fn main() { + dioxus::launch(app); +} diff --git a/packages/playwright-tests/web-hot-patch/Cargo.toml b/packages/playwright-tests/web-hot-patch/Cargo.toml index bcfeee4a5..e5adc75f1 100644 --- a/packages/playwright-tests/web-hot-patch/Cargo.toml +++ b/packages/playwright-tests/web-hot-patch/Cargo.toml @@ -7,6 +7,6 @@ license = "MIT OR Apache-2.0" publish = false [dependencies] -dioxus = { path = "../../dioxus", features = ["web"]} +dioxus = { path = "../../dioxus", features = ["web"] } -[workspace] \ No newline at end of file +[workspace] diff --git a/packages/playwright-tests/web-patch-fullstack.spec.js b/packages/playwright-tests/web-patch-fullstack.spec.js new file mode 100644 index 000000000..5061594ee --- /dev/null +++ b/packages/playwright-tests/web-patch-fullstack.spec.js @@ -0,0 +1,113 @@ +// @ts-check +const { test, expect } = require("@playwright/test"); + +const hotPatchTimeout = { + timeout: 1000 * 60 * 2, // 2 minute +}; + +test("button click", async ({ page }) => { + const fs = require("fs"); + const mainPath = "web-hot-patch-fullstack-temp/src/main.rs"; + var mainContent = fs.readFileSync(mainPath, "utf8"); + const stylePath = "web-hot-patch-fullstack-temp/assets/style.css"; + var styleContent = fs.readFileSync(stylePath, "utf8"); + + // Reset any changes made to the main.rs and style.css files. + mainContent = mainContent.replace(/Ok\(\s*2\s*\)/g, "Ok(1)"); + mainContent = mainContent.replace("Click button! Count:", "Click me! Count:"); + mainContent = mainContent.replace("asset!('/assets/alternative-style.css')", "asset!('/assets/style.css')"); + fs.writeFileSync(mainPath, mainContent); + styleContent = styleContent.replace( + "background-color: blue;", + "background-color: red;" + ); + fs.writeFileSync(stylePath, styleContent); + + await page.goto("http://localhost:9981"); + + + // wait a sec for the serverfn to process + // give the page time to finish loading resources + await page.waitForLoadState("networkidle"); + await page.waitForTimeout(2000); + + // ** First test make sure the initial fat build is working ** + // Expect the page to contain the counter text. + const main = page.locator("#main"); + await expect(main).toContainText("Click me! Count: 0"); + + // Click the increment button. + let button = page.locator("button#increment-button"); + await button.click(); + + await page.waitForTimeout(1000); + + // Expect the page to contain the updated counter text. + await expect(main).toContainText("Click me! Count: 1"); + + // Make sure the css is applied correctly. + await expect(main).toHaveCSS("background-color", "rgb(255, 0, 0)"); + + // Make sure the image is loaded. + const headerImage = page.locator("#toasts"); + // expect the attribute src to start with /assets/toasts- + await expect(headerImage).toHaveAttribute("src", /\/assets\/toasts-/); + + // ** Then make sure the hot patch is working ** + // Then change the file to increment by 2. + const updatedContent = mainContent.replace(/Ok\(\s*1\s*\)/g, "Ok(2)"); + // Change the click me text to reflect the new increment. + const updatedContentWithText = updatedContent.replace( + "Click me! Count:", + "Click button! Count:" + ); + fs.writeFileSync(mainPath, updatedContentWithText); + + // Wait for the page to update and show the new text. + await expect(main).toContainText("Click button! Count: 1", hotPatchTimeout); + + // Now click the button again. + await button.click(); + + // wait a sec for the serverfn to process + await page.waitForTimeout(1000); + + // Expect the count to update by 2. + await expect(main).toContainText("Click button! Count: 3"); + + // Next change just the css file to change the background color to blue. + const updatedStyleContent = styleContent.replace( + "background-color: red;", + "background-color: blue;" + ); + fs.writeFileSync(stylePath, updatedStyleContent); + + // Wait for the page to update the background color. + await expect(main).toHaveCSS( + "background-color", + "rgb(0, 0, 255)", + hotPatchTimeout + ); + + // Make sure the image is still loaded. + // expect the attribute src to start with /assets/toasts- + await expect(headerImage).toHaveAttribute("src", /\/assets\/toasts-/); + + // ** Then add a new asset to the page ** + // Switch from style to alternative-style + const updatedContentWithAlternativeStyle = updatedContentWithText.replace( + 'asset!("/assets/style.css")', + 'asset!("/assets/alternative-style.css")' + ); + fs.writeFileSync(mainPath, updatedContentWithAlternativeStyle); + + // Assert the page has the new alternative style applied. + const body = page.locator("body"); + // Log the page content to debug if needed. + console.log(await page.content()); + await expect(body).toHaveCSS( + "background-color", + "rgb(100, 100, 100)", + hotPatchTimeout + ); +}); diff --git a/packages/subsecond/subsecond/src/lib.rs b/packages/subsecond/subsecond/src/lib.rs index d43044150..2c857b276 100644 --- a/packages/subsecond/subsecond/src/lib.rs +++ b/packages/subsecond/subsecond/src/lib.rs @@ -573,6 +573,16 @@ pub unsafe fn apply_patch(mut table: JumpTable) -> Result<(), PatchError> { // Wait for the fetch to complete - we need the wasm module size in bytes to reserve in the memory let response: web_sys::Response = JsFuture::from(response).await.unwrap().unchecked_into(); + + // If the status is not success, we bail + if !response.ok() { + panic!( + "Failed to patch wasm module at {} - response failed with: {}", + path, + response.status_text() + ); + } + let dl_bytes: ArrayBuffer = JsFuture::from(response.array_buffer().unwrap()) .await .unwrap()