From 955ed6aec6420d6a98337e2a3f33590d9d8fbdcf Mon Sep 17 00:00:00 2001 From: Fabian-Lars Date: Tue, 2 Jul 2024 07:22:33 +0200 Subject: [PATCH] fix(updater): Start app after update via msi (#1498) --- .changes/fix-updater-msi-autostart.md | 5 + plugins/updater/src/lib.rs | 4 +- plugins/updater/src/updater.rs | 131 +++++++++++++++++++++----- 3 files changed, 112 insertions(+), 28 deletions(-) create mode 100644 .changes/fix-updater-msi-autostart.md diff --git a/.changes/fix-updater-msi-autostart.md b/.changes/fix-updater-msi-autostart.md new file mode 100644 index 00000000..ffacb0ff --- /dev/null +++ b/.changes/fix-updater-msi-autostart.md @@ -0,0 +1,5 @@ +--- +updater: patch +--- + +Automatically launch app after updates using `.msi`, to match NSIS `.exe` installer behaviour. diff --git a/plugins/updater/src/lib.rs b/plugins/updater/src/lib.rs index 0a205958..19bcbb72 100644 --- a/plugins/updater/src/lib.rs +++ b/plugins/updater/src/lib.rs @@ -81,9 +81,7 @@ impl> UpdaterExt for T { let args = self.env().args_os; if !args.is_empty() { - builder = builder - .nsis_installer_arg("/ARGS") - .nsis_installer_args(args); + builder = builder.current_exe_args(args); } #[cfg(any( diff --git a/plugins/updater/src/updater.rs b/plugins/updater/src/updater.rs index d8cc48a3..e29a2da8 100644 --- a/plugins/updater/src/updater.rs +++ b/plugins/updater/src/updater.rs @@ -104,7 +104,7 @@ pub struct UpdaterBuilder { timeout: Option, proxy: Option, installer_args: Vec, - nsis_installer_args: Vec, + current_exe_args: Vec, on_before_exit: Option, } @@ -116,7 +116,7 @@ impl UpdaterBuilder { .as_ref() .map(|w| w.installer_args.clone()) .unwrap_or_default(), - nsis_installer_args: Vec::new(), + current_exe_args: Vec::new(), current_version, config, version_comparator: None, @@ -245,7 +245,7 @@ impl UpdaterBuilder { proxy: self.proxy, endpoints, installer_args: self.installer_args, - nsis_installer_args: self.nsis_installer_args, + current_exe_args: self.current_exe_args, arch, target, json_target, @@ -257,21 +257,13 @@ impl UpdaterBuilder { } impl UpdaterBuilder { - pub(crate) fn nsis_installer_arg(mut self, arg: S) -> Self - where - S: Into, - { - self.nsis_installer_args.push(arg.into()); - self - } - - pub(crate) fn nsis_installer_args(mut self, args: I) -> Self + pub(crate) fn current_exe_args(mut self, args: I) -> Self where I: IntoIterator, S: Into, { let args = args.into_iter().map(|a| a.into()).collect::>(); - self.nsis_installer_args.extend_from_slice(&args); + self.current_exe_args.extend_from_slice(&args); self } } @@ -294,7 +286,7 @@ pub struct Updater { #[allow(unused)] installer_args: Vec, #[allow(unused)] - nsis_installer_args: Vec, + current_exe_args: Vec, } impl Updater { @@ -406,7 +398,7 @@ impl Updater { proxy: self.proxy.clone(), headers: self.headers.clone(), installer_args: self.installer_args.clone(), - nsis_installer_args: self.nsis_installer_args.clone(), + current_exe_args: self.current_exe_args.clone(), }) } else { None @@ -447,7 +439,7 @@ pub struct Update { #[allow(unused)] installer_args: Vec, #[allow(unused)] - nsis_installer_args: Vec, + current_exe_args: Vec, } impl Resource for Update {} @@ -595,21 +587,36 @@ impl Update { let updater_type = Self::extract(bytes)?; let install_mode = self.config.install_mode(); + let current_args = &self.current_exe_args()[1..]; + let msi_args; + let installer_args: Vec<&OsStr> = match &updater_type { WindowsUpdaterType::Nsis { .. } => install_mode .nsis_args() .iter() .map(OsStr::new) .chain(once(OsStr::new("/UPDATE"))) - .chain(self.nsis_installer_args()) - .chain(self.installer_args()) - .collect(), - WindowsUpdaterType::Msi { path, .. } => [OsStr::new("/i"), path.as_os_str()] - .into_iter() - .chain(install_mode.msiexec_args().iter().map(OsStr::new)) - .chain(once(OsStr::new("/promptrestart"))) + .chain(once(OsStr::new("/ARGS"))) + .chain(current_args.to_vec()) .chain(self.installer_args()) .collect(), + WindowsUpdaterType::Msi { path, .. } => { + let escaped_args = current_args + .iter() + .map(escape_msi_property_arg) + .collect::>() + .join(" "); + msi_args = OsString::from(format!("LAUNCHAPPARGS=\"{escaped_args}\"")); + + [OsStr::new("/i"), path.as_os_str()] + .into_iter() + .chain(install_mode.msiexec_args().iter().map(OsStr::new)) + .chain(once(OsStr::new("/promptrestart"))) + .chain(self.installer_args()) + .chain(once(OsStr::new("AUTOLAUNCHAPP=True"))) + .chain(once(msi_args.as_os_str())) + .collect() + } }; if let Some(on_before_exit) = self.on_before_exit.as_ref() { @@ -649,8 +656,8 @@ impl Update { .collect::>() } - fn nsis_installer_args(&self) -> Vec<&OsStr> { - self.nsis_installer_args + fn current_exe_args(&self) -> Vec<&OsStr> { + self.current_exe_args .iter() .map(OsStr::new) .collect::>() @@ -1026,6 +1033,32 @@ impl PathExt for PathBuf { } } +#[cfg(windows)] +fn escape_msi_property_arg(arg: impl AsRef) -> 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}\"\"") + } +} + #[cfg(test)] mod tests { @@ -1040,4 +1073,52 @@ mod tests { PathBuf::from("\"C:\\Users\\Some User\\AppData\\tauri-example.exe\"") ) } + + #[test] + #[cfg(windows)] + fn it_escapes_correctly() { + use crate::updater::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); + } + } }