浏览代码

fix: window state preservation by soft-killing the process (#3579)

Jonathan Kelley 5 月之前
父节点
当前提交
a729968ee4
共有 3 个文件被更改,包括 57 次插入10 次删除
  1. 1 1
      packages/cli/src/cli/run.rs
  2. 3 3
      packages/cli/src/serve/mod.rs
  3. 53 6
      packages/cli/src/serve/runner.rs

+ 1 - 1
packages/cli/src/cli/run.rs

@@ -46,7 +46,7 @@ impl RunArgs {
                     tracing::info!("[{platform}]: {msg}")
                 }
                 ServeUpdate::ProcessExited { platform, status } => {
-                    runner.kill(platform);
+                    runner.kill(platform).await;
                     tracing::info!("[{platform}]: process exited with status: {status:?}");
                     break;
                 }

+ 3 - 3
packages/cli/src/serve/mod.rs

@@ -109,7 +109,7 @@ pub(crate) async fn serve_all(mut args: ServeArgs) -> Result<()> {
 
                     // Kill any running executables on Windows
                     if cfg!(windows) {
-                        runner.kill_all();
+                        runner.kill_all().await;
                     }
 
                     // We're going to kick off a new build, interrupting the current build if it's ongoing
@@ -200,7 +200,7 @@ pub(crate) async fn serve_all(mut args: ServeArgs) -> Result<()> {
                     );
                 }
 
-                runner.kill(platform);
+                runner.kill(platform).await;
             }
 
             ServeUpdate::StdoutReceived { platform, msg } => {
@@ -223,7 +223,7 @@ pub(crate) async fn serve_all(mut args: ServeArgs) -> Result<()> {
 
                 // Kill any running executables on Windows
                 if cfg!(windows) {
-                    runner.kill_all();
+                    runner.kill_all().await;
                 }
 
                 builder.rebuild(args.build_arguments.clone());

+ 53 - 6
packages/cli/src/serve/runner.rs

@@ -5,7 +5,7 @@ use crate::{
 use dioxus_core::internal::TemplateGlobalKey;
 use dioxus_devtools_types::HotReloadMsg;
 use dioxus_html::HtmlCtx;
-use futures_util::{future::OptionFuture, stream::FuturesUnordered};
+use futures_util::{future::OptionFuture, stream::FuturesUnordered, FutureExt};
 use ignore::gitignore::Gitignore;
 use std::{
     collections::{HashMap, HashSet},
@@ -105,7 +105,7 @@ impl AppRunner {
         // 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);
+        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
@@ -137,12 +137,59 @@ impl AppRunner {
         Ok(self.running.get(&platform).unwrap())
     }
 
-    pub(crate) fn kill(&mut self, platform: Platform) {
-        self.running.remove(&platform);
+    /// 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) fn kill_all(&mut self) {
-        self.running.clear();
+    pub(crate) async fn kill_all(&mut self) {
+        let keys = self.running.keys().cloned().collect::<Vec<_>>();
+        for platform in keys {
+            self.kill(platform).await;
+        }
     }
 
     /// Open an existing app bundle, if it exists