diff --git a/.changes/wix-autolaunch-args.md b/.changes/wix-autolaunch-args.md new file mode 100644 index 000000000000..3bd9d4fce4ab --- /dev/null +++ b/.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. diff --git a/.changes/wix-maintain-args.md b/.changes/wix-maintain-args.md new file mode 100644 index 000000000000..9b7ec030887d --- /dev/null +++ b/.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`. + diff --git a/core/tauri/src/updater/core.rs b/core/tauri/src/updater/core.rs index d7e6885c0c53..13a40936f983 100644 --- a/core/tauri/src/updater/core.rs +++ b/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) -> 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::>() - .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::>() + .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); + } + } } diff --git a/tooling/bundler/src/bundle/windows/templates/main.wxs b/tooling/bundler/src/bundle/windows/templates/main.wxs index 06b7ad187fc6..5145d21e5490 100644 --- a/tooling/bundler/src/bundle/windows/templates/main.wxs +++ b/tooling/bundler/src/bundle/windows/templates/main.wxs @@ -32,6 +32,11 @@ + + + + + {{#if allow_downgrades}} {{else}} @@ -67,8 +72,7 @@ - - + @@ -309,6 +313,11 @@ {{/if}} + + + AUTOLAUNCHAPP AND NOT Installed + +