Skip to content

Commit 955ed6a

Browse files
authored
fix(updater): Start app after update via msi (#1498)
1 parent 28bc5c2 commit 955ed6a

3 files changed

Lines changed: 112 additions & 28 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
updater: patch
3+
---
4+
5+
Automatically launch app after updates using `.msi`, to match NSIS `.exe` installer behaviour.

plugins/updater/src/lib.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,7 @@ impl<R: Runtime, T: Manager<R>> UpdaterExt<R> for T {
8181

8282
let args = self.env().args_os;
8383
if !args.is_empty() {
84-
builder = builder
85-
.nsis_installer_arg("/ARGS")
86-
.nsis_installer_args(args);
84+
builder = builder.current_exe_args(args);
8785
}
8886

8987
#[cfg(any(

plugins/updater/src/updater.rs

Lines changed: 106 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ pub struct UpdaterBuilder {
104104
timeout: Option<Duration>,
105105
proxy: Option<Url>,
106106
installer_args: Vec<OsString>,
107-
nsis_installer_args: Vec<OsString>,
107+
current_exe_args: Vec<OsString>,
108108
on_before_exit: Option<OnBeforeExit>,
109109
}
110110

@@ -116,7 +116,7 @@ impl UpdaterBuilder {
116116
.as_ref()
117117
.map(|w| w.installer_args.clone())
118118
.unwrap_or_default(),
119-
nsis_installer_args: Vec::new(),
119+
current_exe_args: Vec::new(),
120120
current_version,
121121
config,
122122
version_comparator: None,
@@ -245,7 +245,7 @@ impl UpdaterBuilder {
245245
proxy: self.proxy,
246246
endpoints,
247247
installer_args: self.installer_args,
248-
nsis_installer_args: self.nsis_installer_args,
248+
current_exe_args: self.current_exe_args,
249249
arch,
250250
target,
251251
json_target,
@@ -257,21 +257,13 @@ impl UpdaterBuilder {
257257
}
258258

259259
impl UpdaterBuilder {
260-
pub(crate) fn nsis_installer_arg<S>(mut self, arg: S) -> Self
261-
where
262-
S: Into<OsString>,
263-
{
264-
self.nsis_installer_args.push(arg.into());
265-
self
266-
}
267-
268-
pub(crate) fn nsis_installer_args<I, S>(mut self, args: I) -> Self
260+
pub(crate) fn current_exe_args<I, S>(mut self, args: I) -> Self
269261
where
270262
I: IntoIterator<Item = S>,
271263
S: Into<OsString>,
272264
{
273265
let args = args.into_iter().map(|a| a.into()).collect::<Vec<_>>();
274-
self.nsis_installer_args.extend_from_slice(&args);
266+
self.current_exe_args.extend_from_slice(&args);
275267
self
276268
}
277269
}
@@ -294,7 +286,7 @@ pub struct Updater {
294286
#[allow(unused)]
295287
installer_args: Vec<OsString>,
296288
#[allow(unused)]
297-
nsis_installer_args: Vec<OsString>,
289+
current_exe_args: Vec<OsString>,
298290
}
299291

300292
impl Updater {
@@ -406,7 +398,7 @@ impl Updater {
406398
proxy: self.proxy.clone(),
407399
headers: self.headers.clone(),
408400
installer_args: self.installer_args.clone(),
409-
nsis_installer_args: self.nsis_installer_args.clone(),
401+
current_exe_args: self.current_exe_args.clone(),
410402
})
411403
} else {
412404
None
@@ -447,7 +439,7 @@ pub struct Update {
447439
#[allow(unused)]
448440
installer_args: Vec<OsString>,
449441
#[allow(unused)]
450-
nsis_installer_args: Vec<OsString>,
442+
current_exe_args: Vec<OsString>,
451443
}
452444

453445
impl Resource for Update {}
@@ -595,21 +587,36 @@ impl Update {
595587
let updater_type = Self::extract(bytes)?;
596588

597589
let install_mode = self.config.install_mode();
590+
let current_args = &self.current_exe_args()[1..];
591+
let msi_args;
592+
598593
let installer_args: Vec<&OsStr> = match &updater_type {
599594
WindowsUpdaterType::Nsis { .. } => install_mode
600595
.nsis_args()
601596
.iter()
602597
.map(OsStr::new)
603598
.chain(once(OsStr::new("/UPDATE")))
604-
.chain(self.nsis_installer_args())
605-
.chain(self.installer_args())
606-
.collect(),
607-
WindowsUpdaterType::Msi { path, .. } => [OsStr::new("/i"), path.as_os_str()]
608-
.into_iter()
609-
.chain(install_mode.msiexec_args().iter().map(OsStr::new))
610-
.chain(once(OsStr::new("/promptrestart")))
599+
.chain(once(OsStr::new("/ARGS")))
600+
.chain(current_args.to_vec())
611601
.chain(self.installer_args())
612602
.collect(),
603+
WindowsUpdaterType::Msi { path, .. } => {
604+
let escaped_args = current_args
605+
.iter()
606+
.map(escape_msi_property_arg)
607+
.collect::<Vec<_>>()
608+
.join(" ");
609+
msi_args = OsString::from(format!("LAUNCHAPPARGS=\"{escaped_args}\""));
610+
611+
[OsStr::new("/i"), path.as_os_str()]
612+
.into_iter()
613+
.chain(install_mode.msiexec_args().iter().map(OsStr::new))
614+
.chain(once(OsStr::new("/promptrestart")))
615+
.chain(self.installer_args())
616+
.chain(once(OsStr::new("AUTOLAUNCHAPP=True")))
617+
.chain(once(msi_args.as_os_str()))
618+
.collect()
619+
}
613620
};
614621

615622
if let Some(on_before_exit) = self.on_before_exit.as_ref() {
@@ -649,8 +656,8 @@ impl Update {
649656
.collect::<Vec<_>>()
650657
}
651658

652-
fn nsis_installer_args(&self) -> Vec<&OsStr> {
653-
self.nsis_installer_args
659+
fn current_exe_args(&self) -> Vec<&OsStr> {
660+
self.current_exe_args
654661
.iter()
655662
.map(OsStr::new)
656663
.collect::<Vec<_>>()
@@ -1026,6 +1033,32 @@ impl PathExt for PathBuf {
10261033
}
10271034
}
10281035

1036+
#[cfg(windows)]
1037+
fn escape_msi_property_arg(arg: impl AsRef<OsStr>) -> String {
1038+
let mut arg = arg.as_ref().to_string_lossy().to_string();
1039+
1040+
// Otherwise this argument will get lost in ShellExecute
1041+
if arg.is_empty() {
1042+
return "\"\"\"\"".to_string();
1043+
} else if !arg.contains(' ') && !arg.contains('"') {
1044+
return arg;
1045+
}
1046+
1047+
if arg.contains('"') {
1048+
arg = arg.replace('"', r#""""""#)
1049+
}
1050+
1051+
if arg.starts_with('-') {
1052+
if let Some((a1, a2)) = arg.split_once('=') {
1053+
format!("{a1}=\"\"{a2}\"\"")
1054+
} else {
1055+
format!("\"\"{arg}\"\"")
1056+
}
1057+
} else {
1058+
format!("\"\"{arg}\"\"")
1059+
}
1060+
}
1061+
10291062
#[cfg(test)]
10301063
mod tests {
10311064

@@ -1040,4 +1073,52 @@ mod tests {
10401073
PathBuf::from("\"C:\\Users\\Some User\\AppData\\tauri-example.exe\"")
10411074
)
10421075
}
1076+
1077+
#[test]
1078+
#[cfg(windows)]
1079+
fn it_escapes_correctly() {
1080+
use crate::updater::escape_msi_property_arg;
1081+
1082+
// Explanation for quotes:
1083+
// The output of escape_msi_property_args() will be used in `LAUNCHAPPARGS=\"{HERE}\"`. This is the first quote level.
1084+
// To escape a quotation mark we use a second quotation mark, so "" is interpreted as " later.
1085+
// This means that the escaped strings can't ever have a single quotation mark!
1086+
// Now there are 3 major things to look out for to not break the msiexec call:
1087+
// 1) Wrap spaces in quotation marks, otherwise it will be interpreted as the end of the msiexec argument.
1088+
// 2) Escape escaping quotation marks, otherwise they will either end the msiexec argument or be ignored.
1089+
// 3) Escape emtpy args in quotation marks, otherwise the argument will get lost.
1090+
let cases = [
1091+
"something",
1092+
"--flag",
1093+
"--empty=",
1094+
"--arg=value",
1095+
"some space", // This simulates `./my-app "some string"`.
1096+
"--arg value", // -> This simulates `./my-app "--arg value"`. Same as above but it triggers the startsWith(`-`) logic.
1097+
"--arg=unwrapped space", // `./my-app --arg="unwrapped space"`
1098+
"--arg=\"wrapped\"", // `./my-app --args=""wrapped""`
1099+
"--arg=\"wrapped space\"", // `./my-app --args=""wrapped space""`
1100+
"--arg=midword\"wrapped space\"", // `./my-app --args=midword""wrapped""`
1101+
"", // `./my-app '""'`
1102+
];
1103+
let cases_escaped = [
1104+
"something",
1105+
"--flag",
1106+
"--empty=",
1107+
"--arg=value",
1108+
"\"\"some space\"\"",
1109+
"\"\"--arg value\"\"",
1110+
"--arg=\"\"unwrapped space\"\"",
1111+
r#"--arg=""""""wrapped"""""""#,
1112+
r#"--arg=""""""wrapped space"""""""#,
1113+
r#"--arg=""midword""""wrapped space"""""""#,
1114+
"\"\"\"\"",
1115+
];
1116+
1117+
// Just to be sure we didn't mess that up
1118+
assert_eq!(cases.len(), cases_escaped.len());
1119+
1120+
for (orig, escaped) in cases.iter().zip(cases_escaped) {
1121+
assert_eq!(escape_msi_property_arg(orig), escaped);
1122+
}
1123+
}
10431124
}

0 commit comments

Comments
 (0)