Skip to content

Commit 9f2c341

Browse files
feat(core): configure msiexec display options, closes #3951 (#4061)
Co-authored-by: Fabian-Lars <fabianlars@fabianlars.de>
1 parent 1948ae5 commit 9f2c341

File tree

11 files changed

+229
-16
lines changed

11 files changed

+229
-16
lines changed

.changes/silent-windows-update.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"tauri-bundler": patch
3+
"tauri": patch
4+
"cli.rs": patch
5+
"cli.js": patch
6+
"tauri-utils": patch
7+
---
8+
9+
Allow configuring the display options for the MSI execution allowing quieter updates.

core/tauri-utils/src/config.rs

Lines changed: 121 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use heck::ToKebabCase;
1616
use schemars::JsonSchema;
1717
use serde::{
1818
de::{Deserializer, Error as DeError, Visitor},
19-
Deserialize, Serialize,
19+
Deserialize, Serialize, Serializer,
2020
};
2121
use serde_json::Value as JsonValue;
2222
use serde_with::skip_serializing_none;
@@ -1872,6 +1872,89 @@ impl<'de> Deserialize<'de> for UpdaterEndpoint {
18721872
}
18731873
}
18741874

1875+
/// Install modes for the Windows update.
1876+
#[derive(Debug, PartialEq, Clone)]
1877+
#[cfg_attr(feature = "schema", derive(JsonSchema))]
1878+
#[cfg_attr(feature = "schema", schemars(rename_all = "camelCase"))]
1879+
pub enum WindowsUpdateInstallMode {
1880+
/// Specifies there's a basic UI during the installation process, including a final dialog box at the end.
1881+
BasicUi,
1882+
/// The quiet mode means there's no user interaction required.
1883+
/// Requires admin privileges if the installer does.
1884+
Quiet,
1885+
/// Specifies unattended mode, which means the installation only shows a progress bar.
1886+
Passive,
1887+
}
1888+
1889+
impl WindowsUpdateInstallMode {
1890+
/// Returns the associated `msiexec.exe` arguments.
1891+
pub fn msiexec_args(&self) -> &'static [&'static str] {
1892+
match self {
1893+
Self::BasicUi => &["/qb+"],
1894+
Self::Quiet => &["/quiet"],
1895+
Self::Passive => &["/passive"],
1896+
}
1897+
}
1898+
}
1899+
1900+
impl Display for WindowsUpdateInstallMode {
1901+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1902+
write!(
1903+
f,
1904+
"{}",
1905+
match self {
1906+
Self::BasicUi => "basicUI",
1907+
Self::Quiet => "quiet",
1908+
Self::Passive => "passive",
1909+
}
1910+
)
1911+
}
1912+
}
1913+
1914+
impl Default for WindowsUpdateInstallMode {
1915+
fn default() -> Self {
1916+
Self::Passive
1917+
}
1918+
}
1919+
1920+
impl Serialize for WindowsUpdateInstallMode {
1921+
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
1922+
where
1923+
S: Serializer,
1924+
{
1925+
serializer.serialize_str(self.to_string().as_ref())
1926+
}
1927+
}
1928+
1929+
impl<'de> Deserialize<'de> for WindowsUpdateInstallMode {
1930+
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
1931+
where
1932+
D: Deserializer<'de>,
1933+
{
1934+
let s = String::deserialize(deserializer)?;
1935+
match s.to_lowercase().as_str() {
1936+
"basicui" => Ok(Self::BasicUi),
1937+
"quiet" => Ok(Self::Quiet),
1938+
"passive" => Ok(Self::Passive),
1939+
_ => Err(DeError::custom(format!(
1940+
"unknown update install mode '{}'",
1941+
s
1942+
))),
1943+
}
1944+
}
1945+
}
1946+
1947+
/// The updater configuration for Windows.
1948+
#[skip_serializing_none]
1949+
#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
1950+
#[cfg_attr(feature = "schema", derive(JsonSchema))]
1951+
#[serde(rename_all = "camelCase", deny_unknown_fields)]
1952+
pub struct UpdaterWindowsConfig {
1953+
/// The installation mode for the update on Windows. Defaults to `passive`.
1954+
#[serde(default)]
1955+
pub install_mode: WindowsUpdateInstallMode,
1956+
}
1957+
18751958
/// The Updater configuration object.
18761959
#[skip_serializing_none]
18771960
#[derive(Debug, PartialEq, Clone, Serialize)]
@@ -1900,6 +1983,9 @@ pub struct UpdaterConfig {
19001983
/// Signature public key.
19011984
#[serde(default)] // use default just so the schema doesn't flag it as required
19021985
pub pubkey: String,
1986+
/// The Windows configuration for the updater.
1987+
#[serde(default)]
1988+
pub windows: UpdaterWindowsConfig,
19031989
}
19041990

19051991
impl<'de> Deserialize<'de> for UpdaterConfig {
@@ -1915,6 +2001,8 @@ impl<'de> Deserialize<'de> for UpdaterConfig {
19152001
dialog: bool,
19162002
endpoints: Option<Vec<UpdaterEndpoint>>,
19172003
pubkey: Option<String>,
2004+
#[serde(default)]
2005+
windows: UpdaterWindowsConfig,
19182006
}
19192007

19202008
let config = InnerUpdaterConfig::deserialize(deserializer)?;
@@ -1930,6 +2018,7 @@ impl<'de> Deserialize<'de> for UpdaterConfig {
19302018
dialog: config.dialog,
19312019
endpoints: config.endpoints,
19322020
pubkey: config.pubkey.unwrap_or_default(),
2021+
windows: config.windows,
19332022
})
19342023
}
19352024
}
@@ -1941,6 +2030,7 @@ impl Default for UpdaterConfig {
19412030
dialog: default_dialog(),
19422031
endpoints: None,
19432032
pubkey: "".into(),
2033+
windows: Default::default(),
19442034
}
19452035
}
19462036
}
@@ -2611,6 +2701,25 @@ mod build {
26112701
}
26122702
}
26132703

2704+
impl ToTokens for WindowsUpdateInstallMode {
2705+
fn to_tokens(&self, tokens: &mut TokenStream) {
2706+
let prefix = quote! { ::tauri::utils::config::WindowsUpdateInstallMode };
2707+
2708+
tokens.append_all(match self {
2709+
Self::BasicUi => quote! { #prefix::BasicUi },
2710+
Self::Quiet => quote! { #prefix::Quiet },
2711+
Self::Passive => quote! { #prefix::Passive },
2712+
})
2713+
}
2714+
}
2715+
2716+
impl ToTokens for UpdaterWindowsConfig {
2717+
fn to_tokens(&self, tokens: &mut TokenStream) {
2718+
let install_mode = &self.install_mode;
2719+
literal_struct!(tokens, UpdaterWindowsConfig, install_mode);
2720+
}
2721+
}
2722+
26142723
impl ToTokens for UpdaterConfig {
26152724
fn to_tokens(&self, tokens: &mut TokenStream) {
26162725
let active = self.active;
@@ -2628,8 +2737,17 @@ mod build {
26282737
})
26292738
.as_ref(),
26302739
);
2740+
let windows = &self.windows;
26312741

2632-
literal_struct!(tokens, UpdaterConfig, active, dialog, pubkey, endpoints);
2742+
literal_struct!(
2743+
tokens,
2744+
UpdaterConfig,
2745+
active,
2746+
dialog,
2747+
pubkey,
2748+
endpoints,
2749+
windows
2750+
);
26332751
}
26342752
}
26352753

@@ -2948,6 +3066,7 @@ mod test {
29483066
dialog: true,
29493067
pubkey: "".into(),
29503068
endpoints: None,
3069+
windows: Default::default(),
29513070
},
29523071
security: SecurityConfig {
29533072
csp: None,

core/tauri/src/updater/core.rs

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -607,7 +607,20 @@ impl<R: Runtime> Update<R> {
607607
// we run the setup, appimage re-install or overwrite the
608608
// macos .app
609609
#[cfg(target_os = "windows")]
610-
copy_files_and_run(archive_buffer, &self.extract_path, self.with_elevated_task)?;
610+
copy_files_and_run(
611+
archive_buffer,
612+
&self.extract_path,
613+
self.with_elevated_task,
614+
self
615+
.app
616+
.config()
617+
.tauri
618+
.updater
619+
.windows
620+
.install_mode
621+
.clone()
622+
.msiexec_args(),
623+
)?;
611624
#[cfg(not(target_os = "windows"))]
612625
copy_files_and_run(archive_buffer, &self.extract_path)?;
613626
}
@@ -681,6 +694,7 @@ fn copy_files_and_run<R: Read + Seek>(
681694
archive_buffer: R,
682695
_extract_path: &Path,
683696
with_elevated_task: bool,
697+
msiexec_args: &[&str],
684698
) -> Result {
685699
// FIXME: We need to create a memory buffer with the MSI and then run it.
686700
// (instead of extracting the MSI to a temp path)
@@ -724,13 +738,13 @@ fn copy_files_and_run<R: Read + Seek>(
724738

725739
// Check if there is a task that enables the updater to skip the UAC prompt
726740
let update_task_name = format!("Update {} - Skip UAC", product_name);
727-
if let Ok(status) = Command::new("schtasks")
741+
if let Ok(output) = Command::new("schtasks")
728742
.arg("/QUERY")
729743
.arg("/TN")
730744
.arg(update_task_name.clone())
731-
.status()
745+
.output()
732746
{
733-
if status.success() {
747+
if output.status.success() {
734748
// Rename the MSI to the match file name the Skip UAC task is expecting it to be
735749
let temp_msi = tmp_dir.with_file_name(bin_name).with_extension("msi");
736750
Move::from_source(&found_path)
@@ -757,8 +771,8 @@ fn copy_files_and_run<R: Read + Seek>(
757771
Command::new("msiexec.exe")
758772
.arg("/i")
759773
.arg(found_path)
760-
// quiet basic UI with prompt at the end
761-
.arg("/qb+")
774+
.args(msiexec_args)
775+
.arg("/promptrestart")
762776
.spawn()
763777
.expect("installer failed to start");
764778

core/tests/app-updater/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
WixTools/

core/tests/app-updater/tauri.conf.json

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@
1616
"../../../examples/.icons/icon.icns",
1717
"../../../examples/.icons/icon.ico"
1818
],
19-
"category": "DeveloperTool"
19+
"category": "DeveloperTool",
20+
"windows": {
21+
"wix": {
22+
"skipWebviewInstall": true
23+
}
24+
}
2025
},
2126
"allowlist": {
2227
"all": false
@@ -27,7 +32,10 @@
2732
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDE5QzMxNjYwNTM5OEUwNTgKUldSWTRKaFRZQmJER1h4d1ZMYVA3dnluSjdpN2RmMldJR09hUFFlZDY0SlFqckkvRUJhZDJVZXAK",
2833
"endpoints": [
2934
"http://localhost:3007"
30-
]
35+
],
36+
"windows": {
37+
"installMode": "quiet"
38+
}
3139
}
3240
}
3341
}

core/tests/app-updater/tests/update.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ struct Config {
2929
struct PlatformUpdate {
3030
signature: String,
3131
url: &'static str,
32+
with_elevated_task: bool,
3233
}
3334

3435
#[derive(Serialize)]
@@ -100,12 +101,11 @@ fn bundle_path(root_dir: &Path, _version: &str) -> PathBuf {
100101
#[cfg(windows)]
101102
fn bundle_path(root_dir: &Path, version: &str) -> PathBuf {
102103
root_dir.join(format!(
103-
"target/debug/bundle/msi/app-updater_{}_x64_en-US.AppImage",
104+
"target/debug/bundle/msi/app-updater_{}_x64_en-US.msi",
104105
version
105106
))
106107
}
107108

108-
#[cfg(not(windows))]
109109
#[test]
110110
#[ignore]
111111
fn update_app() {
@@ -173,6 +173,7 @@ fn update_app() {
173173
PlatformUpdate {
174174
signature: signature.clone(),
175175
url: "http://localhost:3007/download",
176+
with_elevated_task: false,
176177
},
177178
);
178179
let body = serde_json::to_vec(&Update {

tooling/bundler/src/bundle/settings.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ pub struct PackageSettings {
108108
}
109109

110110
/// The updater settings.
111-
#[derive(Debug, Clone)]
111+
#[derive(Debug, Default, Clone)]
112112
pub struct UpdaterSettings {
113113
/// Whether the updater is active or not.
114114
pub active: bool,
@@ -118,6 +118,8 @@ pub struct UpdaterSettings {
118118
pub pubkey: String,
119119
/// Display built-in dialog or use event system if disabled.
120120
pub dialog: bool,
121+
/// Args to pass to `msiexec.exe` to run the updater on Windows.
122+
pub msiexec_args: Option<&'static [&'static str]>,
121123
}
122124

123125
/// The Linux debian bundle settings.
@@ -700,6 +702,11 @@ impl Settings {
700702
&self.bundle_settings.windows
701703
}
702704

705+
/// Returns the Updater settings.
706+
pub fn updater(&self) -> Option<&UpdaterSettings> {
707+
self.bundle_settings.updater.as_ref()
708+
}
709+
703710
/// Is update enabled
704711
pub fn is_update_enabled(&self) -> bool {
705712
match &self.bundle_settings.updater {

tooling/bundler/src/bundle/windows/msi/wix.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,17 @@ pub fn build_wix_app_installer(
567567
create_dir_all(&output_path)?;
568568

569569
if enable_elevated_update_task {
570+
data.insert(
571+
"msiexec_args",
572+
to_json(
573+
settings
574+
.updater()
575+
.and_then(|updater| updater.msiexec_args.clone())
576+
.map(|args| args.join(" "))
577+
.unwrap_or_else(|| "/passive".to_string()),
578+
),
579+
);
580+
570581
// Create the update task XML
571582
let mut skip_uac_task = Handlebars::new();
572583
let xml = include_str!("../templates/update-task.xml");

tooling/bundler/src/bundle/windows/templates/update-task.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
<Actions Context="Author">
3838
<Exec>
3939
<Command>cmd.exe</Command>
40-
<Arguments>/c "msiexec.exe /i %TEMP%\\{{{product_name}}}.msi /qb+"</Arguments>
40+
<Arguments>/c "msiexec.exe /i %TEMP%\\{{{product_name}}}.msi {{{msiexec_args}}} /promptrestart"</Arguments>
4141
</Exec>
4242
</Actions>
4343
</Task>

0 commit comments

Comments
 (0)