mirror of
https://github.com/tauri-apps/tauri.git
synced 2026-04-03 10:11:15 +02:00
refactor(updater/wix): launch app after update, maintain args (#10966)
* refactor(updater/wix): launch app after update, maintain args * change files
This commit is contained in:
5
.changes/wix-autolaunch-args.md
Normal file
5
.changes/wix-autolaunch-args.md
Normal file
@@ -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
.changes/wix-maintain-args.md
Normal file
6
.changes/wix-maintain-args.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"tauri": "minor:enhance"
|
||||
---
|
||||
|
||||
On Windows, maintain current CLI arguments when relaunching the app after updates using `.msi`.
|
||||
|
||||
@@ -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(¤t_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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user