Skip to content

Commit

Permalink
feat(core): configure msiexec display options, closes #3951 (#4061)
Browse files Browse the repository at this point in the history
Co-authored-by: Fabian-Lars <fabianlars@fabianlars.de>
  • Loading branch information
lucasfernog and FabianLars committed May 15, 2022
1 parent 1948ae5 commit 9f2c341
Show file tree
Hide file tree
Showing 11 changed files with 229 additions and 16 deletions.
9 changes: 9 additions & 0 deletions .changes/silent-windows-update.md
Original file line number Diff line number Diff line change
@@ -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.
123 changes: 121 additions & 2 deletions core/tauri-utils/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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 {
Expand All @@ -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)?;
Expand All @@ -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,
})
}
}
Expand All @@ -1941,6 +2030,7 @@ impl Default for UpdaterConfig {
dialog: default_dialog(),
endpoints: None,
pubkey: "".into(),
windows: Default::default(),
}
}
}
Expand Down Expand Up @@ -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;
Expand All @@ -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
);
}
}

Expand Down Expand Up @@ -2948,6 +3066,7 @@ mod test {
dialog: true,
pubkey: "".into(),
endpoints: None,
windows: Default::default(),
},
security: SecurityConfig {
csp: None,
Expand Down
26 changes: 20 additions & 6 deletions core/tauri/src/updater/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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");

Expand Down
1 change: 1 addition & 0 deletions core/tests/app-updater/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
WixTools/
12 changes: 10 additions & 2 deletions core/tests/app-updater/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@
"../../../examples/.icons/icon.icns",
"../../../examples/.icons/icon.ico"
],
"category": "DeveloperTool"
"category": "DeveloperTool",
"windows": {
"wix": {
"skipWebviewInstall": true
}
}
},
"allowlist": {
"all": false
Expand All @@ -27,7 +32,10 @@
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDE5QzMxNjYwNTM5OEUwNTgKUldSWTRKaFRZQmJER1h4d1ZMYVA3dnluSjdpN2RmMldJR09hUFFlZDY0SlFqckkvRUJhZDJVZXAK",
"endpoints": [
"http://localhost:3007"
]
],
"windows": {
"installMode": "quiet"
}
}
}
}
5 changes: 3 additions & 2 deletions core/tests/app-updater/tests/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ struct Config {
struct PlatformUpdate {
signature: String,
url: &'static str,
with_elevated_task: bool,
}

#[derive(Serialize)]
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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 {
Expand Down
9 changes: 8 additions & 1 deletion tooling/bundler/src/bundle/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down
11 changes: 11 additions & 0 deletions tooling/bundler/src/bundle/windows/msi/wix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Loading

0 comments on commit 9f2c341

Please sign in to comment.