fix(updater): Start app after update via msi (#1498)

pull/1479/head
Fabian-Lars 11 months ago committed by GitHub
parent 28bc5c2e3e
commit 955ed6aec6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
updater: patch
---
Automatically launch app after updates using `.msi`, to match NSIS `.exe` installer behaviour.

@ -81,9 +81,7 @@ impl<R: Runtime, T: Manager<R>> UpdaterExt<R> for T {
let args = self.env().args_os; let args = self.env().args_os;
if !args.is_empty() { if !args.is_empty() {
builder = builder builder = builder.current_exe_args(args);
.nsis_installer_arg("/ARGS")
.nsis_installer_args(args);
} }
#[cfg(any( #[cfg(any(

@ -104,7 +104,7 @@ pub struct UpdaterBuilder {
timeout: Option<Duration>, timeout: Option<Duration>,
proxy: Option<Url>, proxy: Option<Url>,
installer_args: Vec<OsString>, installer_args: Vec<OsString>,
nsis_installer_args: Vec<OsString>, current_exe_args: Vec<OsString>,
on_before_exit: Option<OnBeforeExit>, on_before_exit: Option<OnBeforeExit>,
} }
@ -116,7 +116,7 @@ impl UpdaterBuilder {
.as_ref() .as_ref()
.map(|w| w.installer_args.clone()) .map(|w| w.installer_args.clone())
.unwrap_or_default(), .unwrap_or_default(),
nsis_installer_args: Vec::new(), current_exe_args: Vec::new(),
current_version, current_version,
config, config,
version_comparator: None, version_comparator: None,
@ -245,7 +245,7 @@ impl UpdaterBuilder {
proxy: self.proxy, proxy: self.proxy,
endpoints, endpoints,
installer_args: self.installer_args, installer_args: self.installer_args,
nsis_installer_args: self.nsis_installer_args, current_exe_args: self.current_exe_args,
arch, arch,
target, target,
json_target, json_target,
@ -257,21 +257,13 @@ impl UpdaterBuilder {
} }
impl UpdaterBuilder { impl UpdaterBuilder {
pub(crate) fn nsis_installer_arg<S>(mut self, arg: S) -> Self pub(crate) fn current_exe_args<I, S>(mut self, args: I) -> Self
where
S: Into<OsString>,
{
self.nsis_installer_args.push(arg.into());
self
}
pub(crate) fn nsis_installer_args<I, S>(mut self, args: I) -> Self
where where
I: IntoIterator<Item = S>, I: IntoIterator<Item = S>,
S: Into<OsString>, S: Into<OsString>,
{ {
let args = args.into_iter().map(|a| a.into()).collect::<Vec<_>>(); let args = args.into_iter().map(|a| a.into()).collect::<Vec<_>>();
self.nsis_installer_args.extend_from_slice(&args); self.current_exe_args.extend_from_slice(&args);
self self
} }
} }
@ -294,7 +286,7 @@ pub struct Updater {
#[allow(unused)] #[allow(unused)]
installer_args: Vec<OsString>, installer_args: Vec<OsString>,
#[allow(unused)] #[allow(unused)]
nsis_installer_args: Vec<OsString>, current_exe_args: Vec<OsString>,
} }
impl Updater { impl Updater {
@ -406,7 +398,7 @@ impl Updater {
proxy: self.proxy.clone(), proxy: self.proxy.clone(),
headers: self.headers.clone(), headers: self.headers.clone(),
installer_args: self.installer_args.clone(), installer_args: self.installer_args.clone(),
nsis_installer_args: self.nsis_installer_args.clone(), current_exe_args: self.current_exe_args.clone(),
}) })
} else { } else {
None None
@ -447,7 +439,7 @@ pub struct Update {
#[allow(unused)] #[allow(unused)]
installer_args: Vec<OsString>, installer_args: Vec<OsString>,
#[allow(unused)] #[allow(unused)]
nsis_installer_args: Vec<OsString>, current_exe_args: Vec<OsString>,
} }
impl Resource for Update {} impl Resource for Update {}
@ -595,21 +587,36 @@ impl Update {
let updater_type = Self::extract(bytes)?; let updater_type = Self::extract(bytes)?;
let install_mode = self.config.install_mode(); 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 { let installer_args: Vec<&OsStr> = match &updater_type {
WindowsUpdaterType::Nsis { .. } => install_mode WindowsUpdaterType::Nsis { .. } => install_mode
.nsis_args() .nsis_args()
.iter() .iter()
.map(OsStr::new) .map(OsStr::new)
.chain(once(OsStr::new("/UPDATE"))) .chain(once(OsStr::new("/UPDATE")))
.chain(self.nsis_installer_args()) .chain(once(OsStr::new("/ARGS")))
.chain(current_args.to_vec())
.chain(self.installer_args()) .chain(self.installer_args())
.collect(), .collect(),
WindowsUpdaterType::Msi { path, .. } => [OsStr::new("/i"), path.as_os_str()] WindowsUpdaterType::Msi { path, .. } => {
let escaped_args = current_args
.iter()
.map(escape_msi_property_arg)
.collect::<Vec<_>>()
.join(" ");
msi_args = OsString::from(format!("LAUNCHAPPARGS=\"{escaped_args}\""));
[OsStr::new("/i"), path.as_os_str()]
.into_iter() .into_iter()
.chain(install_mode.msiexec_args().iter().map(OsStr::new)) .chain(install_mode.msiexec_args().iter().map(OsStr::new))
.chain(once(OsStr::new("/promptrestart"))) .chain(once(OsStr::new("/promptrestart")))
.chain(self.installer_args()) .chain(self.installer_args())
.collect(), .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() { if let Some(on_before_exit) = self.on_before_exit.as_ref() {
@ -649,8 +656,8 @@ impl Update {
.collect::<Vec<_>>() .collect::<Vec<_>>()
} }
fn nsis_installer_args(&self) -> Vec<&OsStr> { fn current_exe_args(&self) -> Vec<&OsStr> {
self.nsis_installer_args self.current_exe_args
.iter() .iter()
.map(OsStr::new) .map(OsStr::new)
.collect::<Vec<_>>() .collect::<Vec<_>>()
@ -1026,6 +1033,32 @@ impl PathExt for PathBuf {
} }
} }
#[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}\"\"")
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
@ -1040,4 +1073,52 @@ mod tests {
PathBuf::from("\"C:\\Users\\Some User\\AppData\\tauri-example.exe\"") 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);
}
}
} }

Loading…
Cancel
Save