瀏覽代碼

feat(core): configure msiexec display options, closes #3951 (#4061)

Co-authored-by: Fabian-Lars <fabianlars@fabianlars.de>
Lucas Fernandes Nogueira 3 年之前
父節點
當前提交
9f2c341319

+ 9 - 0
.changes/silent-windows-update.md

@@ -0,0 +1,9 @@
+---
+"tauri-bundler": patch
+"tauri": patch
+"cli.rs": patch
+"cli.js": patch
+"tauri-utils": patch
+---
+
+Allow configuring the display options for the MSI execution allowing quieter updates.

+ 121 - 2
core/tauri-utils/src/config.rs

@@ -16,7 +16,7 @@ use heck::ToKebabCase;
 use schemars::JsonSchema;
 use serde::{
   de::{Deserializer, Error as DeError, Visitor},
-  Deserialize, Serialize,
+  Deserialize, Serialize, Serializer,
 };
 use serde_json::Value as JsonValue;
 use serde_with::skip_serializing_none;
@@ -1872,6 +1872,89 @@ impl<'de> Deserialize<'de> for UpdaterEndpoint {
   }
 }
 
+/// Install modes for the Windows update.
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "schema", derive(JsonSchema))]
+#[cfg_attr(feature = "schema", schemars(rename_all = "camelCase"))]
+pub enum WindowsUpdateInstallMode {
+  /// Specifies there's a basic UI during the installation process, including a final dialog box at the end.
+  BasicUi,
+  /// The quiet mode means there's no user interaction required.
+  /// Requires admin privileges if the installer does.
+  Quiet,
+  /// Specifies unattended mode, which means the installation only shows a progress bar.
+  Passive,
+}
+
+impl WindowsUpdateInstallMode {
+  /// Returns the associated `msiexec.exe` arguments.
+  pub fn msiexec_args(&self) -> &'static [&'static str] {
+    match self {
+      Self::BasicUi => &["/qb+"],
+      Self::Quiet => &["/quiet"],
+      Self::Passive => &["/passive"],
+    }
+  }
+}
+
+impl Display for WindowsUpdateInstallMode {
+  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+    write!(
+      f,
+      "{}",
+      match self {
+        Self::BasicUi => "basicUI",
+        Self::Quiet => "quiet",
+        Self::Passive => "passive",
+      }
+    )
+  }
+}
+
+impl Default for WindowsUpdateInstallMode {
+  fn default() -> Self {
+    Self::Passive
+  }
+}
+
+impl Serialize for WindowsUpdateInstallMode {
+  fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
+  where
+    S: Serializer,
+  {
+    serializer.serialize_str(self.to_string().as_ref())
+  }
+}
+
+impl<'de> Deserialize<'de> for WindowsUpdateInstallMode {
+  fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
+  where
+    D: Deserializer<'de>,
+  {
+    let s = String::deserialize(deserializer)?;
+    match s.to_lowercase().as_str() {
+      "basicui" => Ok(Self::BasicUi),
+      "quiet" => Ok(Self::Quiet),
+      "passive" => Ok(Self::Passive),
+      _ => Err(DeError::custom(format!(
+        "unknown update install mode '{}'",
+        s
+      ))),
+    }
+  }
+}
+
+/// The updater configuration for Windows.
+#[skip_serializing_none]
+#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
+#[cfg_attr(feature = "schema", derive(JsonSchema))]
+#[serde(rename_all = "camelCase", deny_unknown_fields)]
+pub struct UpdaterWindowsConfig {
+  /// The installation mode for the update on Windows. Defaults to `passive`.
+  #[serde(default)]
+  pub install_mode: WindowsUpdateInstallMode,
+}
+
 /// The Updater configuration object.
 #[skip_serializing_none]
 #[derive(Debug, PartialEq, Clone, Serialize)]
@@ -1900,6 +1983,9 @@ pub struct UpdaterConfig {
   /// Signature public key.
   #[serde(default)] // use default just so the schema doesn't flag it as required
   pub pubkey: String,
+  /// The Windows configuration for the updater.
+  #[serde(default)]
+  pub windows: UpdaterWindowsConfig,
 }
 
 impl<'de> Deserialize<'de> for UpdaterConfig {
@@ -1915,6 +2001,8 @@ impl<'de> Deserialize<'de> for UpdaterConfig {
       dialog: bool,
       endpoints: Option<Vec<UpdaterEndpoint>>,
       pubkey: Option<String>,
+      #[serde(default)]
+      windows: UpdaterWindowsConfig,
     }
 
     let config = InnerUpdaterConfig::deserialize(deserializer)?;
@@ -1930,6 +2018,7 @@ impl<'de> Deserialize<'de> for UpdaterConfig {
       dialog: config.dialog,
       endpoints: config.endpoints,
       pubkey: config.pubkey.unwrap_or_default(),
+      windows: config.windows,
     })
   }
 }
@@ -1941,6 +2030,7 @@ impl Default for UpdaterConfig {
       dialog: default_dialog(),
       endpoints: None,
       pubkey: "".into(),
+      windows: Default::default(),
     }
   }
 }
@@ -2611,6 +2701,25 @@ mod build {
     }
   }
 
+  impl ToTokens for WindowsUpdateInstallMode {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+      let prefix = quote! { ::tauri::utils::config::WindowsUpdateInstallMode };
+
+      tokens.append_all(match self {
+        Self::BasicUi => quote! { #prefix::BasicUi },
+        Self::Quiet => quote! { #prefix::Quiet },
+        Self::Passive => quote! { #prefix::Passive },
+      })
+    }
+  }
+
+  impl ToTokens for UpdaterWindowsConfig {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+      let install_mode = &self.install_mode;
+      literal_struct!(tokens, UpdaterWindowsConfig, install_mode);
+    }
+  }
+
   impl ToTokens for UpdaterConfig {
     fn to_tokens(&self, tokens: &mut TokenStream) {
       let active = self.active;
@@ -2628,8 +2737,17 @@ mod build {
           })
           .as_ref(),
       );
+      let windows = &self.windows;
 
-      literal_struct!(tokens, UpdaterConfig, active, dialog, pubkey, endpoints);
+      literal_struct!(
+        tokens,
+        UpdaterConfig,
+        active,
+        dialog,
+        pubkey,
+        endpoints,
+        windows
+      );
     }
   }
 
@@ -2948,6 +3066,7 @@ mod test {
         dialog: true,
         pubkey: "".into(),
         endpoints: None,
+        windows: Default::default(),
       },
       security: SecurityConfig {
         csp: None,

+ 20 - 6
core/tauri/src/updater/core.rs

@@ -607,7 +607,20 @@ impl<R: Runtime> Update<R> {
       // we run the setup, appimage re-install or overwrite the
       // macos .app
       #[cfg(target_os = "windows")]
-      copy_files_and_run(archive_buffer, &self.extract_path, self.with_elevated_task)?;
+      copy_files_and_run(
+        archive_buffer,
+        &self.extract_path,
+        self.with_elevated_task,
+        self
+          .app
+          .config()
+          .tauri
+          .updater
+          .windows
+          .install_mode
+          .clone()
+          .msiexec_args(),
+      )?;
       #[cfg(not(target_os = "windows"))]
       copy_files_and_run(archive_buffer, &self.extract_path)?;
     }
@@ -681,6 +694,7 @@ fn copy_files_and_run<R: Read + Seek>(
   archive_buffer: R,
   _extract_path: &Path,
   with_elevated_task: bool,
+  msiexec_args: &[&str],
 ) -> Result {
   // FIXME: We need to create a memory buffer with the MSI and then run it.
   //        (instead of extracting the MSI to a temp path)
@@ -724,13 +738,13 @@ fn copy_files_and_run<R: Read + Seek>(
 
           // Check if there is a task that enables the updater to skip the UAC prompt
           let update_task_name = format!("Update {} - Skip UAC", product_name);
-          if let Ok(status) = Command::new("schtasks")
+          if let Ok(output) = Command::new("schtasks")
             .arg("/QUERY")
             .arg("/TN")
             .arg(update_task_name.clone())
-            .status()
+            .output()
           {
-            if status.success() {
+            if output.status.success() {
               // Rename the MSI to the match file name the Skip UAC task is expecting it to be
               let temp_msi = tmp_dir.with_file_name(bin_name).with_extension("msi");
               Move::from_source(&found_path)
@@ -757,8 +771,8 @@ fn copy_files_and_run<R: Read + Seek>(
       Command::new("msiexec.exe")
         .arg("/i")
         .arg(found_path)
-        // quiet basic UI with prompt at the end
-        .arg("/qb+")
+        .args(msiexec_args)
+        .arg("/promptrestart")
         .spawn()
         .expect("installer failed to start");
 

+ 1 - 0
core/tests/app-updater/.gitignore

@@ -0,0 +1 @@
+WixTools/

+ 10 - 2
core/tests/app-updater/tauri.conf.json

@@ -16,7 +16,12 @@
         "../../../examples/.icons/icon.icns",
         "../../../examples/.icons/icon.ico"
       ],
-      "category": "DeveloperTool"
+      "category": "DeveloperTool",
+      "windows": {
+        "wix": {
+          "skipWebviewInstall": true
+        }
+      }
     },
     "allowlist": {
       "all": false
@@ -27,7 +32,10 @@
       "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDE5QzMxNjYwNTM5OEUwNTgKUldSWTRKaFRZQmJER1h4d1ZMYVA3dnluSjdpN2RmMldJR09hUFFlZDY0SlFqckkvRUJhZDJVZXAK",
       "endpoints": [
         "http://localhost:3007"
-      ]
+      ],
+      "windows": {
+        "installMode": "quiet"
+      }
     }
   }
 }

+ 3 - 2
core/tests/app-updater/tests/update.rs

@@ -29,6 +29,7 @@ struct Config {
 struct PlatformUpdate {
   signature: String,
   url: &'static str,
+  with_elevated_task: bool,
 }
 
 #[derive(Serialize)]
@@ -100,12 +101,11 @@ fn bundle_path(root_dir: &Path, _version: &str) -> PathBuf {
 #[cfg(windows)]
 fn bundle_path(root_dir: &Path, version: &str) -> PathBuf {
   root_dir.join(format!(
-    "target/debug/bundle/msi/app-updater_{}_x64_en-US.AppImage",
+    "target/debug/bundle/msi/app-updater_{}_x64_en-US.msi",
     version
   ))
 }
 
-#[cfg(not(windows))]
 #[test]
 #[ignore]
 fn update_app() {
@@ -173,6 +173,7 @@ fn update_app() {
               PlatformUpdate {
                 signature: signature.clone(),
                 url: "http://localhost:3007/download",
+                with_elevated_task: false,
               },
             );
             let body = serde_json::to_vec(&Update {

+ 8 - 1
tooling/bundler/src/bundle/settings.rs

@@ -108,7 +108,7 @@ pub struct PackageSettings {
 }
 
 /// The updater settings.
-#[derive(Debug, Clone)]
+#[derive(Debug, Default, Clone)]
 pub struct UpdaterSettings {
   /// Whether the updater is active or not.
   pub active: bool,
@@ -118,6 +118,8 @@ pub struct UpdaterSettings {
   pub pubkey: String,
   /// Display built-in dialog or use event system if disabled.
   pub dialog: bool,
+  /// Args to pass to `msiexec.exe` to run the updater on Windows.
+  pub msiexec_args: Option<&'static [&'static str]>,
 }
 
 /// The Linux debian bundle settings.
@@ -700,6 +702,11 @@ impl Settings {
     &self.bundle_settings.windows
   }
 
+  /// Returns the Updater settings.
+  pub fn updater(&self) -> Option<&UpdaterSettings> {
+    self.bundle_settings.updater.as_ref()
+  }
+
   /// Is update enabled
   pub fn is_update_enabled(&self) -> bool {
     match &self.bundle_settings.updater {

+ 11 - 0
tooling/bundler/src/bundle/windows/msi/wix.rs

@@ -567,6 +567,17 @@ pub fn build_wix_app_installer(
   create_dir_all(&output_path)?;
 
   if enable_elevated_update_task {
+    data.insert(
+      "msiexec_args",
+      to_json(
+        settings
+          .updater()
+          .and_then(|updater| updater.msiexec_args.clone())
+          .map(|args| args.join(" "))
+          .unwrap_or_else(|| "/passive".to_string()),
+      ),
+    );
+
     // Create the update task XML
     let mut skip_uac_task = Handlebars::new();
     let xml = include_str!("../templates/update-task.xml");

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

@@ -37,7 +37,7 @@
   <Actions Context="Author">
     <Exec>
       <Command>cmd.exe</Command>
-      <Arguments>/c "msiexec.exe /i %TEMP%\\{{{product_name}}}.msi /qb+"</Arguments>
+      <Arguments>/c "msiexec.exe /i %TEMP%\\{{{product_name}}}.msi {{{msiexec_args}}} /promptrestart"</Arguments>
     </Exec>
   </Actions>
 </Task>

+ 44 - 2
tooling/cli/schema.json

@@ -169,7 +169,10 @@
         "updater": {
           "active": false,
           "dialog": true,
-          "pubkey": ""
+          "pubkey": "",
+          "windows": {
+            "installMode": "passive"
+          }
         },
         "windows": []
       },
@@ -1728,7 +1731,10 @@
           "default": {
             "active": false,
             "dialog": true,
-            "pubkey": ""
+            "pubkey": "",
+            "windows": {
+              "installMode": "passive"
+            }
           },
           "allOf": [
             {
@@ -1783,6 +1789,17 @@
           "description": "Signature public key.",
           "default": "",
           "type": "string"
+        },
+        "windows": {
+          "description": "The Windows configuration for the updater.",
+          "default": {
+            "installMode": "passive"
+          },
+          "allOf": [
+            {
+              "$ref": "#/definitions/UpdaterWindowsConfig"
+            }
+          ]
         }
       },
       "additionalProperties": false
@@ -1792,6 +1809,22 @@
       "type": "string",
       "format": "uri"
     },
+    "UpdaterWindowsConfig": {
+      "description": "The updater configuration for Windows.",
+      "type": "object",
+      "properties": {
+        "installMode": {
+          "description": "The installation mode for the update on Windows. Defaults to `passive`.",
+          "default": "passive",
+          "allOf": [
+            {
+              "$ref": "#/definitions/WindowsUpdateInstallMode"
+            }
+          ]
+        }
+      },
+      "additionalProperties": false
+    },
     "WindowAllowlistConfig": {
       "description": "Allowlist for the window APIs.",
       "type": "object",
@@ -2148,6 +2181,15 @@
       },
       "additionalProperties": false
     },
+    "WindowsUpdateInstallMode": {
+      "description": "Install modes for the Windows update.",
+      "type": "string",
+      "enum": [
+        "basicUi",
+        "quiet",
+        "passive"
+      ]
+    },
     "WixConfig": {
       "description": "Configuration for the MSI bundle using WiX.",
       "type": "object",

+ 1 - 0
tooling/cli/src/interface/rust.rs

@@ -522,6 +522,7 @@ fn tauri_config_to_bundle_settings(
       endpoints: updater_config
         .endpoints
         .map(|endpoints| endpoints.iter().map(|e| e.to_string()).collect()),
+      msiexec_args: Some(updater_config.windows.install_mode.msiexec_args()),
     }),
     ..Default::default()
   })