Repository: DioxusLabs/dioxus (language: rust)

Issue:
Android bundle
**Problem**

Command: dx bundle --platform android --outdir ./bundle_output
When i ran this command I took this error: Android bundles are not yet supported

**Steps To Reproduce**

Steps to reproduce the behavior:

- dx bundle --platform android --outdir ./bundle_output

**Expected behavior**

Should bundle to Android.

**Screenshots**

![image](https://github.com/user-attachments/assets/5037e9bd-4944-4c27-ba1e-f578f515fe86)

**Environment:**
 - Dioxus version:  v0.6
 - Rust version:    1.83
 - OS info:         MacOS 15
 - App platform:    mobile

**Questionnaire**
<!-- If you feel up to the challenge, please uncomment one of the lines below: -->

<!-- I'm interested in fixing this myself but don't know where to start -->
<!-- I would like to fix and I have a solution -->
<!-- I don't have time to fix this right now, but maybe later -->



Discussion / hints:

Fix patch:
diff --git a/packages/cli-config/src/lib.rs b/packages/cli-config/src/lib.rs
index 389438de34..11d4afbfd6 100644
--- a/packages/cli-config/src/lib.rs
+++ b/packages/cli-config/src/lib.rs
@@ -67,6 +67,7 @@ pub const APP_TITLE_ENV: &str = "DIOXUS_APP_TITLE";
 #[deprecated(since = "0.6.0", note = "The CLI currently does not set this.")]
 #[doc(hidden)]
 pub const OUT_DIR: &str = "DIOXUS_OUT_DIR";
+pub const SESSION_CACHE_DIR: &str = "DIOXUS_SESSION_CACHE_DIR";
 
 /// Reads an environment variable at runtime in debug mode or at compile time in
 /// release mode. When bundling in release mode, we will not be running under the
@@ -277,3 +278,15 @@ pub fn out_dir() -> Option<PathBuf> {
         std::env::var(OUT_DIR).ok().map(PathBuf::from)
     }
 }
+
+/// Get the directory where this app can write to for this session that's guaranteed to be stable
+/// between reloads of the same app. This is useful for emitting state like window position and size
+/// so the app can restore it when it's next opened.
+///
+/// Note that this cache dir is really only useful for platforms that can access it. Web/Android
+/// don't have access to this directory, so it's not useful for them.
+///
+/// This is designed with desktop executables in mind.
+pub fn session_cache_dir() -> Option<PathBuf> {
+    std::env::var(SESSION_CACHE_DIR).ok().map(PathBuf::from)
+}
diff --git a/packages/cli/src/build/bundle.rs b/packages/cli/src/build/bundle.rs
index 21373fb575..4aea947577 100644
--- a/packages/cli/src/build/bundle.rs
+++ b/packages/cli/src/build/bundle.rs
@@ -730,23 +730,7 @@ impl AppBundle {
         if let Platform::Android = self.build.build.platform() {
             self.build.status_running_gradle();
 
-            // make sure we can execute the gradlew script
-            #[cfg(unix)]
-            {
-                use std::os::unix::prelude::PermissionsExt;
-                std::fs::set_permissions(
-                    self.build.root_dir().join("gradlew"),
-                    std::fs::Permissions::from_mode(0o755),
-                )?;
-            }
-
-            let gradle_exec_name = match cfg!(windows) {
-                true => "gradlew.bat",
-                false => "gradlew",
-            };
-            let gradle_exec = self.build.root_dir().join(gradle_exec_name);
-
-            let output = Command::new(gradle_exec)
+            let output = Command::new(self.gradle_exe()?)
                 .arg("assembleDebug")
                 .current_dir(self.build.root_dir())
                 .stderr(std::process::Stdio::piped())
@@ -762,6 +746,62 @@ impl AppBundle {
         Ok(())
     }
 
+    /// Run bundleRelease and return the path to the `.aab` file
+    ///
+    /// https://stackoverflow.com/questions/57072558/whats-the-difference-between-gradlewassemblerelease-gradlewinstallrelease-and
+    pub(crate) async fn android_gradle_bundle(&self) -> Result<PathBuf> {
+        let output = Command::new(self.gradle_exe()?)
+            .arg("bundleRelease")
+            .current_dir(self.build.root_dir())
+            .output()
+            .await
+            .context("Failed to run gradle bundleRelease")?;
+
+        if !output.status.success() {
+            return Err(anyhow::anyhow!("Failed to bundleRelease: {output:?}").into());
+        }
+
+        let app_release = self
+            .build
+            .root_dir()
+            .join("app")
+            .join("build")
+            .join("outputs")
+            .join("bundle")
+            .join("release");
+
+        // Rename it to Name-arch.aab
+        let from = app_release.join("app-release.aab");
+        let to = app_release.join(format!(
+            "{}-{}.aab",
+            self.build.krate.bundled_app_name(),
+            self.build.build.target_args.arch()
+        ));
+
+        std::fs::rename(from, &to).context("Failed to rename aab")?;
+
+        Ok(to)
+    }
+
+    fn gradle_exe(&self) -> Result<PathBuf> {
+        // make sure we can execute the gradlew script
+        #[cfg(unix)]
+        {
+            use std::os::unix::prelude::PermissionsExt;
+            std::fs::set_permissions(
+                self.build.root_dir().join("gradlew"),
+                std::fs::Permissions::from_mode(0o755),
+            )?;
+        }
+
+        let gradle_exec_name = match cfg!(windows) {
+            true => "gradlew.bat",
+            false => "gradlew",
+        };
+
+        Ok(self.build.root_dir().join(gradle_exec_name))
+    }
+
     pub(crate) fn apk_path(&self) -> PathBuf {
         self.build
             .root_dir()
diff --git a/packages/cli/src/cli/bundle.rs b/packages/cli/src/cli/bundle.rs
index 09587ddd57..751fc9bceb 100644
--- a/packages/cli/src/cli/bundle.rs
+++ b/packages/cli/src/cli/bundle.rs
@@ -76,11 +76,12 @@ impl Bundle {
             Platform::Server => bundles.push(bundle.build.root_dir()),
             Platform::Liveview => bundles.push(bundle.build.root_dir()),
 
-            // todo(jon): we can technically create apks (already do...) just need to expose it
             Platform::Android => {
-                return Err(Error::UnsupportedFeature(
-                    "Android bundles are not yet supported".into(),
-                ));
+                let aab = bundle
+                    .android_gradle_bundle()
+                    .await
+                    .context("Failed to run gradle bundleRelease")?;
+                bundles.push(aab);
             }
         };
 
diff --git a/packages/cli/src/cli/run.rs b/packages/cli/src/cli/run.rs
index 2c0e83d5be..b9cd100275 100644
--- a/packages/cli/src/cli/run.rs
+++ b/packages/cli/src/cli/run.rs
@@ -46,7 +46,7 @@ impl RunArgs {
                     tracing::info!("[{platform}]: {msg}")
                 }
                 ServeUpdate::ProcessExited { platform, status } => {
-                    runner.kill(platform).await;
+                    runner.cleanup().await;
                     tracing::info!("[{platform}]: process exited with status: {status:?}");
                     break;
                 }
diff --git a/packages/cli/src/cli/target.rs b/packages/cli/src/cli/target.rs
index e5febd656d..b0099fb2da 100644
--- a/packages/cli/src/cli/target.rs
+++ b/packages/cli/src/cli/target.rs
@@ -217,3 +217,15 @@ impl TryFrom<String> for Arch {
         }
     }
 }
+
+impl std::fmt::Display for Arch {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Arch::Arm => "armv7l",
+            Arch::Arm64 => "aarch64",
+            Arch::X86 => "i386",
+            Arch::X64 => "x86_64",
+        }
+        .fmt(f)
+    }
+}
diff --git a/packages/cli/src/dioxus_crate.rs b/packages/cli/src/dioxus_crate.rs
index ba5d4066c7..ad62f250d6 100644
--- a/packages/cli/src/dioxus_crate.rs
+++ b/packages/cli/src/dioxus_crate.rs
@@ -149,6 +149,16 @@ impl DioxusCrate {
         files
     }
 
+    /// Get the directory where this app can write to for this session that's guaranteed to be stable
+    /// for the same app. This is useful for emitting state like window position and size.
+    ///
+    /// The directory is specific for this app and might be
+    pub(crate) fn session_cache_dir(&self) -> PathBuf {
+        self.internal_out_dir()
+            .join(self.executable_name())
+            .join("session-cache")
+    }
+
     /// Get the outdir specified by the Dioxus.toml, relative to the crate directory.
     /// We don't support workspaces yet since that would cause a collision of bundles per project.
     pub(crate) fn crate_out_dir(&self) -> Option<PathBuf> {
diff --git a/packages/cli/src/error.rs b/packages/cli/src/error.rs
index 9a7c5942db..69e0096abc 100644
--- a/packages/cli/src/error.rs
+++ b/packages/cli/src/error.rs
@@ -33,6 +33,7 @@ pub(crate) enum Error {
     #[error("Failed to bundle project: {0}")]
     BundleFailed(#[from] tauri_bundler::Error),
 
+    #[allow(unused)]
     #[error("Unsupported feature: {0}")]
     UnsupportedFeature(String),
 
diff --git a/packages/cli/src/serve/handle.rs b/packages/cli/src/serve/handle.rs
index 2901455aa7..bab073a6d6 100644
--- a/packages/cli/src/serve/handle.rs
+++ b/packages/cli/src/serve/handle.rs
@@ -38,6 +38,11 @@ pub(crate) struct AppHandle {
     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>,
@@ -54,6 +59,8 @@ impl AppHandle {
             server_child: None,
             server_stdout: None,
             server_stderr: None,
+            entropy_app_exe: None,
+            entropy_server_exe: None,
         })
     }
 
@@ -79,6 +86,15 @@ impl AppHandle {
             // 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) = &self.app.build.krate.config.web.app.base_path {
@@ -87,7 +103,7 @@ impl AppHandle {
 
         // 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.app.server_exe()) {
+        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()));
@@ -147,6 +163,70 @@ impl AppHandle {
         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) {
+        tracing::debug!("Cleaning up process");
+
+        // 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
@@ -236,12 +316,15 @@ impl AppHandle {
     ///
     /// Server/liveview/desktop are all basically the same, though
     fn open_with_main_exe(&mut self, envs: Vec<(&str, String)>) -> Result<Child> {
-        let child = Command::new(self.app.main_exe())
+        // 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)
     }
 
@@ -650,4 +733,72 @@ We checked the folder: {}
             };
         });
     }
+
+    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
+    }
 }
diff --git a/packages/cli/src/serve/mod.rs b/packages/cli/src/serve/mod.rs
index 5f960bc987..f19eff965d 100644
--- a/packages/cli/src/serve/mod.rs
+++ b/packages/cli/src/serve/mod.rs
@@ -107,11 +107,6 @@ pub(crate) async fn serve_all(mut args: ServeArgs) -> Result<()> {
                 } else if runner.should_full_rebuild {
                     tracing::info!(dx_src = ?TraceSrc::Dev, "Full rebuild: {}", file);
 
-                    // Kill any running executables on Windows
-                    if cfg!(windows) {
-                        runner.kill_all().await;
-                    }
-
                     // We're going to kick off a new build, interrupting the current build if it's ongoing
                     builder.rebuild(args.build_arguments.clone());
 
@@ -199,8 +194,6 @@ pub(crate) async fn serve_all(mut args: ServeArgs) -> Result<()> {
                - To exit the server, press `ctrl+c`"#
                     );
                 }
-
-                runner.kill(platform).await;
             }
 
             ServeUpdate::StdoutReceived { platform, msg } => {
@@ -221,11 +214,6 @@ pub(crate) async fn serve_all(mut args: ServeArgs) -> Result<()> {
                 // `Hotreloading:` to keep the alignment during long edit sessions
                 tracing::info!("Full rebuild: triggered manually");
 
-                // Kill any running executables on Windows
-                if cfg!(windows) {
-                    runner.kill_all().await;
-                }
-
                 builder.rebuild(args.build_arguments.clone());
                 runner.file_map.force_rebuild();
                 devserver.send_reload_start().await;
@@ -261,9 +249,10 @@ pub(crate) async fn serve_all(mut args: ServeArgs) -> Result<()> {
         }
     };
 
+    _ = runner.cleanup().await;
     _ = devserver.shutdown().await;
-    _ = screen.shutdown();
     builder.abort_all();
+    _ = screen.shutdown();
 
     if let Err(err) = err {
         eprintln!("Exiting with error: {}", err);
diff --git a/packages/cli/src/serve/runner.rs b/packages/cli/src/serve/runner.rs
index 5c0a744eb2..058e465561 100644
--- a/packages/cli/src/serve/runner.rs
+++ b/packages/cli/src/serve/runner.rs
@@ -5,17 +5,16 @@ use crate::{
 use dioxus_core::internal::TemplateGlobalKey;
 use dioxus_devtools_types::HotReloadMsg;
 use dioxus_html::HtmlCtx;
-use futures_util::{future::OptionFuture, stream::FuturesUnordered, FutureExt};
+use futures_util::future::OptionFuture;
 use ignore::gitignore::Gitignore;
 use std::{
     collections::{HashMap, HashSet},
     net::SocketAddr,
     path::PathBuf,
 };
-use tokio_stream::StreamExt;
 
 pub(crate) struct AppRunner {
-    pub(crate) running: HashMap<Platform, AppHandle>,
+    pub(crate) running: Option<AppHandle>,
     pub(crate) krate: DioxusCrate,
     pub(crate) file_map: HotreloadFilemap,
     pub(crate) ignore: Gitignore,
@@ -44,52 +43,47 @@ impl AppRunner {
             runner.fill_filemap(krate);
         }
 
+        // Ensure the session cache dir exists and is empty
+        runner.flush_session_cache();
+
         runner
     }
 
     pub(crate) async fn wait(&mut self) -> ServeUpdate {
         // If there are no running apps, we can just return pending to avoid deadlocking
-        if self.running.is_empty() {
+        let Some(handle) = self.running.as_mut() else {
             return futures_util::future::pending().await;
-        }
+        };
 
-        self.running
-            .iter_mut()
-            .map(|(platform, handle)| async {
-                use ServeUpdate::*;
-                let platform = *platform;
-                tokio::select! {
-                    Some(Ok(Some(msg))) = OptionFuture::from(handle.app_stdout.as_mut().map(|f| f.next_line())) => {
-                        StdoutReceived { platform, msg }
-                    },
-                    Some(Ok(Some(msg))) = OptionFuture::from(handle.app_stderr.as_mut().map(|f| f.next_line())) => {
-                        StderrReceived { platform, msg }
-                    },
-                    Some(status) = OptionFuture::from(handle.app_child.as_mut().map(|f| f.wait())) => {
-                        match status {
-                            Ok(status) => ProcessExited { status, platform },
-                            Err(_err) => todo!("handle error in process joining?"),
-                        }
-                    }
-                    Some(Ok(Some(msg))) = OptionFuture::from(handle.server_stdout.as_mut().map(|f| f.next_line())) => {
-                        StdoutReceived { platform: Platform::Server, msg }
-                    },
-                    Some(Ok(Some(msg))) = OptionFuture::from(handle.server_stderr.as_mut().map(|f| f.next_line())) => {
-                        StderrReceived { platform: Platform::Server, msg }
-                    },
-                    Some(status) = OptionFuture::from(handle.server_child.as_mut().map(|f| f.wait())) => {
-                        match status {
-                            Ok(status) => ProcessExited { status, platform },
-                            Err(_err) => todo!("handle error in process joining?"),
-                        }
-                    }
-                    else => futures_util::future::pending().await
+        use ServeUpdate::*;
+        let platform = handle.app.build.build.platform();
+        tokio::select! {
+            Some(Ok(Some(msg))) = OptionFuture::from(handle.app_stdout.as_mut().map(|f| f.next_line())) => {
+                StdoutReceived { platform, msg }
+            },
+            Some(Ok(Some(msg))) = OptionFuture::from(handle.app_stderr.as_mut().map(|f| f.next_line())) => {
+                StderrReceived { platform, msg }
+            },
+            Some(status) = OptionFuture::from(handle.app_child.as_mut().map(|f| f.wait())) => {
+                match status {
+                    Ok(status) => ProcessExited { status, platform },
+                    Err(_err) => todo!("handle error in process joining?"),
+                }
+            }
+            Some(Ok(Some(msg))) = OptionFuture::from(handle.server_stdout.as_mut().map(|f| f.next_line())) => {
+                StdoutReceived { platform: Platform::Server, msg }
+            },
+            Some(Ok(Some(msg))) = OptionFuture::from(handle.server_stderr.as_mut().map(|f| f.next_line())) => {
+                StderrReceived { platform: Platform::Server, msg }
+            },
+            Some(status) = OptionFuture::from(handle.server_child.as_mut().map(|f| f.wait())) => {
+                match status {
+                    Ok(status) => ProcessExited { status, platform },
+                    Err(_err) => todo!("handle error in process joining?"),
                 }
-            })
-            .collect::<FuturesUnordered<_>>()
-            .next()
-            .await
-            .expect("Stream to pending if not empty")
+            }
+            else => futures_util::future::pending().await
+        }
     }
 
     /// Finally "bundle" this app and return a handle to it
@@ -100,16 +94,9 @@ impl AppRunner {
         fullstack_address: Option<SocketAddr>,
         should_open_web: bool,
     ) -> Result<&AppHandle> {
-        let platform = app.build.build.platform();
-
         // Drop the old handle
-        // todo(jon): we should instead be sending the kill signal rather than dropping the process
-        // This would allow a more graceful shutdown and fix bugs like desktop not retaining its size
-        self.kill(platform).await;
-
-        // wait a tiny sec for the processes to die so we don't have fullstack servers on top of each other
-        // todo(jon): we should allow rebinding to the same port in fullstack itself
-        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
+        // This is a more forceful kill than soft_kill since the app entropy will be wiped
+        self.cleanup().await;
 
         // Add some cute logging
         if self.builds_opened == 0 {
@@ -132,81 +119,32 @@ impl AppRunner {
             .await?;
 
         self.builds_opened += 1;
-        self.running.insert(platform, handle);
+        self.running = Some(handle);
 
-        Ok(self.running.get(&platform).unwrap())
-    }
-
-    /// 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.
-    pub(crate) async fn kill(&mut self, platform: Platform) {
-        use tokio::process::Command;
-
-        let Some(mut process) = self.running.remove(&platform) else {
-            return;
-        };
-
-        let server_process = process.server_child.take();
-        let client_process = process.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() => {}
-            };
-        }
-    }
-
-    pub(crate) async fn kill_all(&mut self) {
-        let keys = self.running.keys().cloned().collect::<Vec<_>>();
-        for platform in keys {
-            self.kill(platform).await;
-        }
+        Ok(self.running.as_ref().unwrap())
     }
 
     /// Open an existing app bundle, if it exists
     pub(crate) async fn open_existing(&mut self, devserver: &WebServer) -> Result<()> {
         let fullstack_address = devserver.proxied_server_address();
 
-        if let Some((_, app)) = self
-            .running
-            .iter_mut()
-            .find(|(platform, _)| **platform != Platform::Server)
-        {
-            app.open(devserver.devserver_address(), fullstack_address, true)
+        if let Some(runner) = self.running.as_mut() {
+            runner.soft_kill().await;
+            runner
+                .open(devserver.devserver_address(), fullstack_address, true)
                 .await?;
         }
+
         Ok(())
     }
 
+    /// Shutdown all the running processes
+    pub(crate) async fn cleanup(&mut self) {
+        if let Some(mut process) = self.running.take() {
+            process.cleanup().await;
+        }
+    }
+
     pub(crate) async fn attempt_hot_reload(
         &mut self,
         modified_files: Vec<PathBuf>,
@@ -237,7 +175,7 @@ impl AppRunner {
             }
 
             // Otherwise, it might be an asset and we should look for it in all the running apps
-            for runner in self.running.values() {
+            if let Some(runner) = self.running.as_mut() {
                 if let Some(bundled_name) = runner.hotreload_bundled_asset(&path).await {
                     // todo(jon): don't hardcode this here
                     let asset_relative = PathBuf::from("/assets/").join(bundled_name);
@@ -329,27 +267,29 @@ impl AppRunner {
     }
 
     pub(crate) async fn client_connected(&mut self) {
-        for (platform, runner) in self.running.iter_mut() {
-            // Assign the runtime asset dir to the runner
-            if *platform == Platform::Ios {
-                // xcrun simctl get_app_container booted com.dioxuslabs
-                let res = tokio::process::Command::new("xcrun")
-                    .arg("simctl")
-                    .arg("get_app_container")
-                    .arg("booted")
-                    .arg(runner.app.build.krate.bundle_identifier())
-                    .output()
-                    .await;
-
-                if let Ok(res) = res {
-                    tracing::trace!("Using runtime asset dir: {:?}", res);
-
-                    if let Ok(out) = String::from_utf8(res.stdout) {
-                        let out = out.trim();
-
-                        tracing::trace!("Setting Runtime asset dir: {out:?}");
-                        runner.runtime_asst_dir = Some(PathBuf::from(out));
-                    }
+        let Some(handle) = self.running.as_mut() else {
+            return;
+        };
+
+        // Assign the runtime asset dir to the runner
+        if handle.app.build.build.platform() == Platform::Ios {
+            // xcrun simctl get_app_container booted com.dioxuslabs
+            let res = tokio::process::Command::new("xcrun")
+                .arg("simctl")
+                .arg("get_app_container")
+                .arg("booted")
+                .arg(handle.app.build.krate.bundle_identifier())
+                .output()
+                .await;
+
+            if let Ok(res) = res {
+                tracing::trace!("Using runtime asset dir: {:?}", res);
+
+                if let Ok(out) = String::from_utf8(res.stdout) {
+                    let out = out.trim();
+
+                    tracing::trace!("Setting Runtime asset dir: {out:?}");
+                    handle.runtime_asst_dir = Some(PathBuf::from(out));
                 }
             }
         }
@@ -388,4 +328,10 @@ impl AppRunner {
             }
         }
     }
+
+    fn flush_session_cache(&self) {
+        let cache_dir = self.krate.session_cache_dir();
+        _ = std::fs::remove_dir_all(&cache_dir);
+        _ = std::fs::create_dir_all(&cache_dir);
+    }
 }
diff --git a/packages/desktop/src/app.rs b/packages/desktop/src/app.rs
index 555399906d..a23672793d 100644
--- a/packages/desktop/src/app.rs
+++ b/packages/desktop/src/app.rs
@@ -593,20 +593,6 @@ fn hide_last_window(window: &Window) {
 
 /// Return the location of a tempfile with our window state in it such that we can restore it later
 fn restore_file() -> std::path::PathBuf {
-    /// Get the name of the program or default to "dioxus" so we can hash it
-    fn get_prog_name_or_default() -> Option<String> {
-        Some(
-            std::env::current_exe()
-                .ok()?
-                .file_name()?
-                .to_str()?
-                .to_string(),
-        )
-    }
-
-    let name = get_prog_name_or_default().unwrap_or_else(|| "dioxus".to_string());
-    let hashed_id = name.chars().map(|c| c as usize).sum::<usize>();
-    let mut path = std::env::temp_dir();
-    path.push(format!("{}-window-state.json", hashed_id));
-    path
+    let dir = dioxus_cli_config::session_cache_dir().unwrap_or_else(std::env::temp_dir);
+    dir.join("window-state.json")
 }


Test patch: