Ver código fonte

refactor(updater/wix): launch app after update, maintain args (#10966)

* refactor(updater/wix): launch app after update, maintain args

* change files
Amr Bashir 10 meses atrás
pai
commit
14443a1319

+ 5 - 0
.changes/wix-autolaunch-args.md

@@ -0,0 +1,5 @@
+---
+"tauri-bundler": "minor:feat"
+---
+
+Add `AUTOLAUNCHAPP` and `LAUNCHAPPARGS` properties to MSI installer, which can be used by updater to instruct launching the app after update and maintain the passed CLI arguments.

+ 6 - 0
.changes/wix-maintain-args.md

@@ -0,0 +1,6 @@
+---
+"tauri": "minor:enhance"
+---
+
+On Windows, maintain current CLI arguments when relaunching the app after updates using `.msi`.
+

+ 105 - 61
core/tauri/src/updater/core.rs

@@ -863,6 +863,32 @@ fn write_installer_in_temp(
   Ok((temp.to_path_buf(), Some(temp)))
 }
 
+#[cfg(windows)]
+fn escape_msi_property_arg(arg: impl AsRef<OsStr>) -> String {
+  let mut arg = arg.as_ref().to_string_lossy().to_string();
+
+  // Otherwise this argument will get lost in ShellExecute
+  if arg.is_empty() {
+    return "\"\"\"\"".to_string();
+  } else if !arg.contains(' ') && !arg.contains('"') {
+    return arg;
+  }
+
+  if arg.contains('"') {
+    arg = arg.replace('"', r#""""""#)
+  }
+
+  if arg.starts_with('-') {
+    if let Some((a1, a2)) = arg.split_once('=') {
+      format!("{a1}=\"\"{a2}\"\"")
+    } else {
+      format!("\"\"{arg}\"\"")
+    }
+  } else {
+    format!("\"\"{arg}\"\"")
+  }
+}
+
 // Windows
 //
 /// ### Expected one of:
@@ -921,7 +947,7 @@ fn copy_files_and_run(
     return Err(Error::InvalidUpdaterFormat);
   };
 
-  let system_root = std::env::var("SYSTEMROOT");
+  let msi_args;
 
   let installer_args: Vec<&OsStr> = match &updater_type {
     WindowsUpdaterType::Nsis { .. } => config
@@ -986,66 +1012,13 @@ fn copy_files_and_run(
           }
         }
       }
-
-      let powershell_path = system_root.as_ref().map_or_else(
-        |_| "powershell.exe".to_string(),
-        |p| format!("{p}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"),
-      );
-
-      // we need to wrap the current exe path in quotes for Start-Process
-      let mut current_executable = std::ffi::OsString::new();
-      current_executable.push("\"");
-      current_executable.push(dunce::simplified(&current_exe()?));
-      current_executable.push("\"");
-
-      let mut msi_path = std::ffi::OsString::new();
-      msi_path.push("\"\"\"");
-      msi_path.push(&path);
-      msi_path.push("\"\"\"");
-
-      let msi_installer_args = [
-        config.tauri.updater.windows.install_mode.msiexec_args(),
-        config
-          .tauri
-          .updater
-          .windows
-          .installer_args
-          .iter()
-          .map(AsRef::as_ref)
-          .collect::<Vec<_>>()
-          .as_slice(),
-      ]
-      .concat();
-
-      // run the installer and relaunch the application
-      let mut powershell_cmd = Command::new(powershell_path);
-
-      powershell_cmd
-        .args(["-NoProfile", "-WindowStyle", "Hidden"])
-        .args([
-          "Start-Process",
-          "-Wait",
-          "-FilePath",
-          "$Env:SYSTEMROOT\\System32\\msiexec.exe",
-          "-ArgumentList",
-        ])
-        .arg("/i,")
-        .arg(&msi_path)
-        .arg(format!(
-          ", {}, /promptrestart;",
-          msi_installer_args.join(", ")
-        ))
-        .arg("Start-Process")
-        .arg(current_executable);
-
-      if !env.args.is_empty() {
-        powershell_cmd.arg("-ArgumentList").arg(env.args.join(", "));
-      }
-
-      let powershell_install_res = powershell_cmd.spawn();
-      if powershell_install_res.is_ok() {
-        exit(0);
-      }
+      let escaped_args = env
+        .args
+        .iter()
+        .map(escape_msi_property_arg)
+        .collect::<Vec<_>>()
+        .join(" ");
+      msi_args = std::ffi::OsString::from(format!("LAUNCHAPPARGS=\"{escaped_args}\""));
 
       [OsStr::new("/i"), quoted_path.as_os_str()]
         .into_iter()
@@ -1060,6 +1033,17 @@ fn copy_files_and_run(
             .map(OsStr::new),
         )
         .chain(once(OsStr::new("/promptrestart")))
+        .chain(
+          config
+            .tauri
+            .updater
+            .windows
+            .installer_args
+            .iter()
+            .map(OsStr::new),
+        )
+        .chain(once(OsStr::new("AUTOLAUNCHAPP=True")))
+        .chain(once(msi_args.as_os_str()))
         .collect()
     }
   };
@@ -1916,4 +1900,64 @@ mod test {
 
     assert!(bin_file.exists());
   }
+
+  #[test]
+  #[cfg(windows)]
+  fn it_wraps_correctly() {
+    use super::PathExt;
+    use std::path::PathBuf;
+
+    assert_eq!(
+      PathBuf::from("C:\\Users\\Some User\\AppData\\tauri-example.exe").wrap_in_quotes(),
+      PathBuf::from("\"C:\\Users\\Some User\\AppData\\tauri-example.exe\"")
+    )
+  }
+
+  #[test]
+  #[cfg(windows)]
+  fn it_escapes_correctly() {
+    use super::escape_msi_property_arg;
+
+    // Explanation for quotes:
+    // The output of escape_msi_property_args() will be used in `LAUNCHAPPARGS=\"{HERE}\"`. This is the first quote level.
+    // To escape a quotation mark we use a second quotation mark, so "" is interpreted as " later.
+    // This means that the escaped strings can't ever have a single quotation mark!
+    // Now there are 3 major things to look out for to not break the msiexec call:
+    //   1) Wrap spaces in quotation marks, otherwise it will be interpreted as the end of the msiexec argument.
+    //   2) Escape escaping quotation marks, otherwise they will either end the msiexec argument or be ignored.
+    //   3) Escape emtpy args in quotation marks, otherwise the argument will get lost.
+    let cases = [
+      "something",
+      "--flag",
+      "--empty=",
+      "--arg=value",
+      "some space",                     // This simulates `./my-app "some string"`.
+      "--arg value", // -> This simulates `./my-app "--arg value"`. Same as above but it triggers the startsWith(`-`) logic.
+      "--arg=unwrapped space", // `./my-app --arg="unwrapped space"`
+      "--arg=\"wrapped\"", // `./my-app --args=""wrapped""`
+      "--arg=\"wrapped space\"", // `./my-app --args=""wrapped space""`
+      "--arg=midword\"wrapped space\"", // `./my-app --args=midword""wrapped""`
+      "",            // `./my-app '""'`
+    ];
+    let cases_escaped = [
+      "something",
+      "--flag",
+      "--empty=",
+      "--arg=value",
+      "\"\"some space\"\"",
+      "\"\"--arg value\"\"",
+      "--arg=\"\"unwrapped space\"\"",
+      r#"--arg=""""""wrapped"""""""#,
+      r#"--arg=""""""wrapped space"""""""#,
+      r#"--arg=""midword""""wrapped space"""""""#,
+      "\"\"\"\"",
+    ];
+
+    // Just to be sure we didn't mess that up
+    assert_eq!(cases.len(), cases_escaped.len());
+
+    for (orig, escaped) in cases.iter().zip(cases_escaped) {
+      assert_eq!(escape_msi_property_arg(orig), escaped);
+    }
+  }
 }

+ 11 - 2
tooling/bundler/src/bundle/windows/templates/main.wxs

@@ -32,6 +32,11 @@
         <!-- reinstall all files; rewrite all registry entries; reinstall all shortcuts -->
         <Property Id="REINSTALLMODE" Value="amus" />
 
+        <!-- Auto launch app after installation, useful for passive mode which usually used in updates -->
+        <Property Id="AUTOLAUNCHAPP" Secure="yes" />
+        <!-- Property to forward cli args to the launched app to not lose those of the pre-update instance -->
+        <Property Id="LAUNCHAPPARGS" Secure="yes" />
+
         {{#if allow_downgrades}}
             <MajorUpgrade Schedule="afterInstallInitialize" AllowDowngrades="yes" />
         {{else}}
@@ -67,8 +72,7 @@
         <!-- launch app checkbox -->
         <Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOXTEXT" Value="!(loc.LaunchApp)" />
         <Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOX" Value="1"/>
-        <Property Id="WixShellExecTarget" Value="[!Path]" />
-        <CustomAction Id="LaunchApplication" BinaryKey="WixCA" DllEntry="WixShellExec" Impersonate="yes" />
+        <CustomAction Id="LaunchApplication" Impersonate="yes" FileKey="Path" ExeCommand="[LAUNCHAPPARGS]" Return="asyncNoWait" />
 
         <UI>
             <!-- launch app checkbox -->
@@ -309,6 +313,11 @@
         </InstallExecuteSequence>
         {{/if}}
 
+
+        <InstallExecuteSequence>
+            <Custom Action="LaunchApplication" After="InstallFinalize">AUTOLAUNCHAPP AND NOT Installed</Custom>
+        </InstallExecuteSequence>
+
         <SetProperty Id="ARPINSTALLLOCATION" Value="[INSTALLDIR]" After="CostFinalize"/>
     </Product>
 </Wix>