diff --git a/.changes/updater-install-no-restart.md b/.changes/updater-install-no-restart.md new file mode 100644 index 0000000000..9a9c1165c5 --- /dev/null +++ b/.changes/updater-install-no-restart.md @@ -0,0 +1,6 @@ +--- +"updater": minor +"updater-js": minor +--- + +On Windows, add a new option `restartAfterInstall`/`restart_after_install` to install an update without the installer re-launching the app diff --git a/examples/api/src-tauri/tauri.conf.json b/examples/api/src-tauri/tauri.conf.json index 00b095be87..be4918de3f 100644 --- a/examples/api/src-tauri/tauri.conf.json +++ b/examples/api/src-tauri/tauri.conf.json @@ -77,7 +77,9 @@ }, "updater": { "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDE5QzMxNjYwNTM5OEUwNTgKUldSWTRKaFRZQmJER1h4d1ZMYVA3dnluSjdpN2RmMldJR09hUFFlZDY0SlFqckkvRUJhZDJVZXAK", + "dangerousInsecureTransportProtocol": true, "endpoints": [ + "http://localhost:5173/updater-test/updater.json", "https://tauri-update-server.vercel.app/update/{{target}}/{{current_version}}" ] } @@ -99,6 +101,9 @@ "localePath": "locales/pt-BR.wxl" } } + }, + "nsis": { + "compression": "none" } }, "iOS": { diff --git a/examples/api/src/views/Updater.svelte b/examples/api/src/views/Updater.svelte index 26d074a6bb..7205e48d6f 100644 --- a/examples/api/src/views/Updater.svelte +++ b/examples/api/src/views/Updater.svelte @@ -1,12 +1,17 @@ -
{#if !isChecking && !newUpdate} - + {:else if !isInstalling && newUpdate} - + {:else}
{progress}% diff --git a/examples/api/updater-test/.gitignore b/examples/api/updater-test/.gitignore new file mode 100644 index 0000000000..78bd45a3a6 --- /dev/null +++ b/examples/api/updater-test/.gitignore @@ -0,0 +1,5 @@ +* + +!.gitignore +!README.md +!updater.json diff --git a/examples/api/updater-test/README.md b/examples/api/updater-test/README.md new file mode 100644 index 0000000000..21551301a3 --- /dev/null +++ b/examples/api/updater-test/README.md @@ -0,0 +1,10 @@ +To test the updater: + +1. Comment out `verify_signature` inside `plugins/updater/src/updater.rs` +2. Run `pnpm tauri build --debug` to build the bundles +3. Copy the bundle you want to test to this folder (`examples/api/updater-test/`) +4. Run `pnpm tauri dev` and open the Updater tab to check + +If the bundle you're testing doesn't exist in the `updater.json` yet, add it manually and welcome to open an PR + +> The `updater.json` and debug bundles will be served by `vite` diff --git a/examples/api/updater-test/updater.json b/examples/api/updater-test/updater.json new file mode 100644 index 0000000000..84edf614aa --- /dev/null +++ b/examples/api/updater-test/updater.json @@ -0,0 +1,11 @@ +{ + "version": "2.1.0", + "notes": "Test update!", + "pub_date": "2026-03-01T14:04:20+00:00", + "platforms": { + "windows-x86_64": { + "signature": "", + "url": "http://localhost:5173/updater-test/Tauri API_2.0.0_x64_en-US.msi" + } + } +} diff --git a/examples/api/vite.config.js b/examples/api/vite.config.js index f7d87db87d..aab72321ea 100644 --- a/examples/api/vite.config.js +++ b/examples/api/vite.config.js @@ -27,7 +27,7 @@ export default defineConfig(async () => { port: 5173, strictPort: true, fs: { - allow: ['.', '../../tooling/api/dist'] + allow: ['.'] } } } diff --git a/plugins/updater/api-iife.js b/plugins/updater/api-iife.js index e13f5191a2..fcdb28f808 100644 --- a/plugins/updater/api-iife.js +++ b/plugins/updater/api-iife.js @@ -1 +1 @@ -if("__TAURI__"in window){var __TAURI_PLUGIN_UPDATER__=function(t){"use strict";function e(t,e,s,n){if("function"==typeof e?t!==e||!n:!e.has(t))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===s?n:"a"===s?n.call(t):n?n.value:e.get(t)}function s(t,e,s,n,i){if("function"==typeof e||!e.has(t))throw new TypeError("Cannot write private member to an object whose class did not declare it");return e.set(t,s),s}var n,i,a,r,o;"function"==typeof SuppressedError&&SuppressedError;const d="__TAURI_TO_IPC_KEY__";class c{constructor(t){n.set(this,void 0),i.set(this,0),a.set(this,[]),r.set(this,void 0),s(this,n,t||(()=>{})),this.id=function(t,e=!1){return window.__TAURI_INTERNALS__.transformCallback(t,e)}(t=>{const o=t.index;if("end"in t)return void(o==e(this,i,"f")?this.cleanupCallback():s(this,r,o));const d=t.message;if(o==e(this,i,"f")){for(e(this,n,"f").call(this,d),s(this,i,e(this,i,"f")+1);e(this,i,"f")in e(this,a,"f");){const t=e(this,a,"f")[e(this,i,"f")];e(this,n,"f").call(this,t),delete e(this,a,"f")[e(this,i,"f")],s(this,i,e(this,i,"f")+1)}e(this,i,"f")===e(this,r,"f")&&this.cleanupCallback()}else e(this,a,"f")[o]=d})}cleanupCallback(){window.__TAURI_INTERNALS__.unregisterCallback(this.id)}set onmessage(t){s(this,n,t)}get onmessage(){return e(this,n,"f")}[(n=new WeakMap,i=new WeakMap,a=new WeakMap,r=new WeakMap,d)](){return`__CHANNEL__:${this.id}`}toJSON(){return this[d]()}}async function l(t,e={},s){return window.__TAURI_INTERNALS__.invoke(t,e,s)}class h{get rid(){return e(this,o,"f")}constructor(t){o.set(this,void 0),s(this,o,t)}async close(){return l("plugin:resources|close",{rid:this.rid})}}o=new WeakMap;class u extends h{constructor(t){super(t.rid),this.available=!0,this.currentVersion=t.currentVersion,this.version=t.version,this.date=t.date,this.body=t.body,this.rawJson=t.rawJson}async download(t,e){_(e);const s=new c;t&&(s.onmessage=t);const n=await l("plugin:updater|download",{onEvent:s,rid:this.rid,...e});this.downloadedBytes=new h(n)}async install(){if(!this.downloadedBytes)throw new Error("Update.install called before Update.download");await l("plugin:updater|install",{updateRid:this.rid,bytesRid:this.downloadedBytes.rid}),this.downloadedBytes=void 0}async downloadAndInstall(t,e){_(e);const s=new c;t&&(s.onmessage=t),await l("plugin:updater|download_and_install",{onEvent:s,rid:this.rid,...e})}async close(){await(this.downloadedBytes?.close()),await super.close()}}function _(t){t?.headers&&(t.headers=Array.from(new Headers(t.headers).entries()))}return t.Update=u,t.check=async function(t){_(t);const e=await l("plugin:updater|check",{...t});return e?new u(e):null},t}({});Object.defineProperty(window.__TAURI__,"updater",{value:__TAURI_PLUGIN_UPDATER__})} +if("__TAURI__"in window){var __TAURI_PLUGIN_UPDATER__=function(t){"use strict";function e(t,e,s,n){if("function"==typeof e?t!==e||!n:!e.has(t))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===s?n:"a"===s?n.call(t):n?n.value:e.get(t)}function s(t,e,s,n,i){if("function"==typeof e||!e.has(t))throw new TypeError("Cannot write private member to an object whose class did not declare it");return e.set(t,s),s}var n,i,a,r,o;"function"==typeof SuppressedError&&SuppressedError;const d="__TAURI_TO_IPC_KEY__";class c{constructor(t){n.set(this,void 0),i.set(this,0),a.set(this,[]),r.set(this,void 0),s(this,n,t||(()=>{})),this.id=function(t,e=!1){return window.__TAURI_INTERNALS__.transformCallback(t,e)}(t=>{const o=t.index;if("end"in t)return void(o==e(this,i,"f")?this.cleanupCallback():s(this,r,o));const d=t.message;if(o==e(this,i,"f")){for(e(this,n,"f").call(this,d),s(this,i,e(this,i,"f")+1);e(this,i,"f")in e(this,a,"f");){const t=e(this,a,"f")[e(this,i,"f")];e(this,n,"f").call(this,t),delete e(this,a,"f")[e(this,i,"f")],s(this,i,e(this,i,"f")+1)}e(this,i,"f")===e(this,r,"f")&&this.cleanupCallback()}else e(this,a,"f")[o]=d})}cleanupCallback(){window.__TAURI_INTERNALS__.unregisterCallback(this.id)}set onmessage(t){s(this,n,t)}get onmessage(){return e(this,n,"f")}[(n=new WeakMap,i=new WeakMap,a=new WeakMap,r=new WeakMap,d)](){return`__CHANNEL__:${this.id}`}toJSON(){return this[d]()}}async function l(t,e={},s){return window.__TAURI_INTERNALS__.invoke(t,e,s)}class h{get rid(){return e(this,o,"f")}constructor(t){o.set(this,void 0),s(this,o,t)}async close(){return l("plugin:resources|close",{rid:this.rid})}}o=new WeakMap;class u extends h{constructor(t){super(t.rid),this.available=!0,this.currentVersion=t.currentVersion,this.version=t.version,this.date=t.date,this.body=t.body,this.rawJson=t.rawJson}async download(t,e){_(e);const s=new c;t&&(s.onmessage=t);const n=await l("plugin:updater|download",{onEvent:s,rid:this.rid,...e});this.downloadedBytes=new h(n)}async install(t){if(!this.downloadedBytes)throw new Error("Update.install called before Update.download");await l("plugin:updater|install",{updateRid:this.rid,bytesRid:this.downloadedBytes.rid,...t}),this.downloadedBytes=void 0}async downloadAndInstall(t,e){_(e);const s=new c;t&&(s.onmessage=t),await l("plugin:updater|download_and_install",{onEvent:s,rid:this.rid,...e})}async close(){await(this.downloadedBytes?.close()),await super.close()}}function _(t){t?.headers&&(t.headers=Array.from(new Headers(t.headers).entries()))}return t.Update=u,t.check=async function(t){_(t);const e=await l("plugin:updater|check",{...t});return e?new u(e):null},t}({});Object.defineProperty(window.__TAURI__,"updater",{value:__TAURI_PLUGIN_UPDATER__})} diff --git a/plugins/updater/guest-js/index.ts b/plugins/updater/guest-js/index.ts index b651843671..1a120921ac 100644 --- a/plugins/updater/guest-js/index.ts +++ b/plugins/updater/guest-js/index.ts @@ -40,6 +40,14 @@ interface DownloadOptions { timeout?: number } +/** Options used when installing an update */ +interface InstallOptions { + /** + * If the Windows installer should restart the app after installed, default is `true` + */ + restartAfterInstall?: boolean +} + interface UpdateMetadata { rid: number currentVersion: string @@ -95,14 +103,15 @@ class Update extends Resource { } /** Install downloaded updater package */ - async install(): Promise { + async install(options?: InstallOptions): Promise { if (!this.downloadedBytes) { throw new Error('Update.install called before Update.download') } await invoke('plugin:updater|install', { updateRid: this.rid, - bytesRid: this.downloadedBytes.rid + bytesRid: this.downloadedBytes.rid, + ...options }) // Don't need to call close, we did it in rust side already @@ -112,7 +121,7 @@ class Update extends Resource { /** Downloads the updater package and installs it */ async downloadAndInstall( onEvent?: (progress: DownloadEvent) => void, - options?: DownloadOptions + options?: DownloadOptions & InstallOptions ): Promise { convertToRustHeaders(options) const channel = new Channel() diff --git a/plugins/updater/src/commands.rs b/plugins/updater/src/commands.rs index 129c413c2b..edc1b88fbd 100644 --- a/plugins/updater/src/commands.rs +++ b/plugins/updater/src/commands.rs @@ -142,12 +142,22 @@ pub(crate) async fn install( webview: Webview, update_rid: ResourceId, bytes_rid: ResourceId, + restart_after_install: Option, ) -> Result<()> { let update = webview.resources_table().get::(update_rid)?; let bytes = webview .resources_table() .get::(bytes_rid)?; - update.install(&bytes.0)?; + + if let Some(restart_after_install) = restart_after_install { + let update = (*update).clone(); + update + .restart_after_install(restart_after_install) + .install(&bytes.0)?; + } else { + update.install(&bytes.0)?; + } + let _ = webview.resources_table().close(bytes_rid); Ok(()) } @@ -159,6 +169,7 @@ pub(crate) async fn download_and_install( on_event: Channel, headers: Option>, timeout: Option, + restart_after_install: Option, ) -> Result<()> { let update = webview.resources_table().get::(rid)?; @@ -176,6 +187,10 @@ pub(crate) async fn download_and_install( update.timeout = Some(Duration::from_millis(timeout)); } + if let Some(restart_after_install) = restart_after_install { + update = update.restart_after_install(restart_after_install); + } + let mut first_chunk = true; update diff --git a/plugins/updater/src/config.rs b/plugins/updater/src/config.rs index 8c6e8bc290..411956b9be 100644 --- a/plugins/updater/src/config.rs +++ b/plugins/updater/src/config.rs @@ -32,17 +32,30 @@ impl WindowsUpdateInstallMode { } } + #[cfg(windows)] + pub(crate) fn msi_restart_after_install_args(&self) -> &'static [&'static str] { + &["AUTOLAUNCHAPP=True"] + } + /// Returns the associated nsis arguments. pub fn nsis_args(&self) -> &'static [&'static str] { // `/P`: Passive // `/S`: Silent // `/R`: Restart match self { - Self::Passive => &["/P", "/R"], - Self::Quiet => &["/S", "/R"], + Self::Passive => &["/P"], + Self::Quiet => &["/S"], _ => &[], } } + + #[cfg(windows)] + pub(crate) fn nsis_restart_after_install_args(&self) -> &'static [&'static str] { + match self { + Self::BasicUi => &[], + _ => &["/R"], + } + } } impl Display for WindowsUpdateInstallMode { @@ -63,6 +76,8 @@ impl Display for WindowsUpdateInstallMode { #[serde(rename_all = "camelCase")] pub struct WindowsConfig { /// Additional arguments given to the NSIS or WiX installer. + /// + /// Note: this applies to both WiX and NSIS installers #[serde( default, alias = "installer-args", diff --git a/plugins/updater/src/lib.rs b/plugins/updater/src/lib.rs index 8eba4269d5..e4e21e16cf 100644 --- a/plugins/updater/src/lib.rs +++ b/plugins/updater/src/lib.rs @@ -83,9 +83,9 @@ impl> UpdaterExt for T { builder = builder.target(target); } - let args = self.env().args_os; - if !args.is_empty() { - builder = builder.current_exe_args(args); + #[cfg(windows)] + { + builder = builder.current_exe_args(self.env().args_os); } builder.version_comparator = version_comparator.clone(); diff --git a/plugins/updater/src/updater.rs b/plugins/updater/src/updater.rs index 1425228744..891890060e 100644 --- a/plugins/updater/src/updater.rs +++ b/plugins/updater/src/updater.rs @@ -129,16 +129,34 @@ impl RemoteRelease { pub type OnBeforeExit = Arc; pub type OnBeforeRequest = Arc ClientBuilder + Send + Sync + 'static>; pub type VersionComparator = Arc bool + Send + Sync>; +#[cfg(target_os = "macos")] type MainThreadClosure = Box; -type RunOnMainThread = - Box std::result::Result<(), tauri::Error> + Send + Sync + 'static>; +#[cfg(target_os = "macos")] +type RunOnMainThread = Arc tauri::Result<()> + Send + Sync + 'static>; -pub struct UpdaterBuilder { - #[allow(dead_code)] +// TODO: Move more fields to this in v3 if we can mark those fields non `pub` +/// Updater context shared between [`UpdaterBuilder`], [`Updater`] and [`Update`] +#[derive(Clone)] +struct UpdaterContext { + config: Config, + configure_client: Option, + #[cfg(target_os = "macos")] run_on_main_thread: RunOnMainThread, + /// App name, used for creating named tempfiles + #[cfg(windows)] app_name: String, + #[cfg(windows)] + installer_args: Vec, + #[cfg(windows)] + current_exe_args: Vec, + #[cfg(windows)] + on_before_exit: Option, + #[cfg(windows)] + restart_after_install: bool, +} + +pub struct UpdaterBuilder { current_version: Version, - config: Config, pub(crate) version_comparator: Option, executable_path: Option, target: Option, @@ -147,27 +165,38 @@ pub struct UpdaterBuilder { timeout: Option, proxy: Option, no_proxy: bool, - installer_args: Vec, - current_exe_args: Vec, - on_before_exit: Option, - configure_client: Option, + context: UpdaterContext, } impl UpdaterBuilder { pub(crate) fn new(app: &AppHandle, config: crate::Config) -> Self { - let app_ = app.clone(); - let run_on_main_thread = move |f| app_.run_on_main_thread(f); + #[cfg(target_os = "macos")] + let run_on_main_thread = { + let app_ = app.clone(); + Arc::new(move |f| app_.run_on_main_thread(f)) + }; Self { - run_on_main_thread: Box::new(run_on_main_thread), - installer_args: config - .windows - .as_ref() - .map(|w| w.installer_args.clone()) - .unwrap_or_default(), - current_exe_args: Vec::new(), - app_name: app.package_info().name.clone(), + context: UpdaterContext { + #[cfg(windows)] + installer_args: config + .windows + .as_ref() + .map(|w| w.installer_args.clone()) + .unwrap_or_default(), + config, + configure_client: None, + #[cfg(target_os = "macos")] + run_on_main_thread, + #[cfg(windows)] + app_name: app.package_info().name.clone(), + #[cfg(windows)] + current_exe_args: Vec::new(), + #[cfg(windows)] + on_before_exit: None, + #[cfg(windows)] + restart_after_install: true, + }, current_version: app.package_info().version.clone(), - config, version_comparator: None, executable_path: None, target: None, @@ -176,8 +205,6 @@ impl UpdaterBuilder { timeout: None, proxy: None, no_proxy: false, - on_before_exit: None, - configure_client: None, } } @@ -197,7 +224,7 @@ impl UpdaterBuilder { pub fn endpoints(mut self, endpoints: Vec) -> Result { crate::config::validate_endpoints( &endpoints, - self.config.dangerous_insecure_transport_protocol, + self.context.config.dangerous_insecure_transport_protocol, )?; self.endpoints.replace(endpoints); @@ -251,26 +278,40 @@ impl UpdaterBuilder { } pub fn pubkey>(mut self, pubkey: S) -> Self { - self.config.pubkey = pubkey.into(); + self.context.config.pubkey = pubkey.into(); self } /// Adds an argument to pass to the Windows installer. + /// + /// Note: this applies to both WiX and NSIS installers + #[cfg_attr(not(windows), allow(unused))] pub fn installer_arg(mut self, arg: S) -> Self where S: Into, { - self.installer_args.push(arg.into()); + #[cfg(windows)] + { + self.context.installer_args.push(arg.into()); + } self } /// Adds multiple arguments to pass to the Windows installer. + /// + /// Note: this applies to both WiX and NSIS installers + #[cfg_attr(not(windows), allow(unused))] pub fn installer_args(mut self, args: I) -> Self where I: IntoIterator, S: Into, { - self.installer_args.extend(args.into_iter().map(Into::into)); + #[cfg(windows)] + { + self.context + .installer_args + .extend(args.into_iter().map(Into::into)); + } self } @@ -280,14 +321,32 @@ impl UpdaterBuilder { /// [`Self::installer_arg`], [`crate::Builder::installer_arg`] /// and the `plugins > updater > windows > installerArgs` config, /// not the ones managed by us (e.g. `/UPDATER` flag passed to the NSIS installer) + #[cfg_attr(not(windows), allow(unused))] pub fn clear_installer_args(mut self) -> Self { - self.installer_args.clear(); + #[cfg(windows)] + { + self.context.installer_args.clear(); + } self } /// Function to run before we run the installer and exit the app through `std::process::exit(0)` on Windows + #[cfg_attr(not(windows), allow(unused))] pub fn on_before_exit(mut self, f: F) -> Self { - self.on_before_exit.replace(Arc::new(f)); + #[cfg(windows)] + { + self.context.on_before_exit.replace(Arc::new(f)); + } + self + } + + /// If the Windows installer should restart the app after installed, default is `true` + #[cfg_attr(not(windows), allow(unused))] + pub fn restart_after_install(mut self, restart_after_install: bool) -> Self { + #[cfg(windows)] + { + self.context.restart_after_install = restart_after_install; + } self } @@ -299,14 +358,14 @@ impl UpdaterBuilder { mut self, f: F, ) -> Self { - self.configure_client.replace(Arc::new(f)); + self.context.configure_client.replace(Arc::new(f)); self } pub fn build(self) -> Result { let endpoints = self .endpoints - .unwrap_or_else(|| self.config.endpoints.clone()); + .unwrap_or_else(|| self.context.config.endpoints.clone()); if endpoints.is_empty() { return Err(Error::EmptyEndpoints); @@ -324,44 +383,36 @@ impl UpdaterBuilder { }; Ok(Updater { - run_on_main_thread: Arc::new(self.run_on_main_thread), - config: self.config, - app_name: self.app_name, current_version: self.current_version, version_comparator: self.version_comparator, timeout: self.timeout, proxy: self.proxy, no_proxy: self.no_proxy, endpoints, - installer_args: self.installer_args, - current_exe_args: self.current_exe_args, arch, target: self.target, headers: self.headers, extract_path, - on_before_exit: self.on_before_exit, - configure_client: self.configure_client, + context: self.context.clone(), }) } } +#[cfg(windows)] impl UpdaterBuilder { pub(crate) fn current_exe_args(mut self, args: I) -> Self where I: IntoIterator, S: Into, { - self.current_exe_args + self.context + .current_exe_args .extend(args.into_iter().map(Into::into)); self } } pub struct Updater { - #[allow(dead_code)] - run_on_main_thread: Arc, - config: Config, - app_name: String, current_version: Version, version_comparator: Option, timeout: Option, @@ -374,12 +425,7 @@ pub struct Updater { target: Option, headers: HeaderMap, extract_path: PathBuf, - on_before_exit: Option, - configure_client: Option, - #[allow(unused)] - installer_args: Vec, - #[allow(unused)] - current_exe_args: Vec, + context: UpdaterContext, } impl Updater { @@ -449,10 +495,10 @@ impl Updater { } let mut request = ClientBuilder::new().user_agent(UPDATER_USER_AGENT); - if self.config.dangerous_accept_invalid_certs { + if self.context.config.dangerous_accept_invalid_certs { request = request.danger_accept_invalid_certs(true); } - if self.config.dangerous_accept_invalid_hostnames { + if self.context.config.dangerous_accept_invalid_hostnames { request = request.danger_accept_invalid_hostnames(true); } if let Some(timeout) = self.timeout { @@ -467,7 +513,7 @@ impl Updater { request = request.proxy(proxy); } - if let Some(ref configure_client) = self.configure_client { + if let Some(ref configure_client) = self.context.configure_client { request = configure_client(request); } @@ -537,10 +583,6 @@ impl Updater { let update = if should_update { Some(Update { - run_on_main_thread: self.run_on_main_thread.clone(), - config: self.config.clone(), - on_before_exit: self.on_before_exit.clone(), - app_name: self.app_name.clone(), current_version: self.current_version.to_string(), target: target.to_owned(), extract_path: self.extract_path.clone(), @@ -554,9 +596,7 @@ impl Updater { proxy: self.proxy.clone(), no_proxy: self.no_proxy, headers: self.headers.clone(), - installer_args: self.installer_args.clone(), - current_exe_args: self.current_exe_args.clone(), - configure_client: self.configure_client.clone(), + context: self.context.clone(), }) } else { None @@ -600,11 +640,6 @@ impl Updater { #[derive(Clone)] pub struct Update { - #[allow(dead_code)] - run_on_main_thread: Arc, - config: Config, - #[allow(unused)] - on_before_exit: Option, /// Update description pub body: Option, /// Version used to check for update @@ -633,14 +668,7 @@ pub struct Update { /// Extract path #[allow(unused)] extract_path: PathBuf, - /// App name, used for creating named tempfiles on Windows - #[allow(unused)] - app_name: String, - #[allow(unused)] - installer_args: Vec, - #[allow(unused)] - current_exe_args: Vec, - configure_client: Option, + context: UpdaterContext, } impl Resource for Update {} @@ -661,10 +689,10 @@ impl Update { } let mut request = ClientBuilder::new().user_agent(UPDATER_USER_AGENT); - if self.config.dangerous_accept_invalid_certs { + if self.context.config.dangerous_accept_invalid_certs { request = request.danger_accept_invalid_certs(true); } - if self.config.dangerous_accept_invalid_hostnames { + if self.context.config.dangerous_accept_invalid_hostnames { request = request.danger_accept_invalid_hostnames(true); } if let Some(timeout) = self.timeout { @@ -676,7 +704,7 @@ impl Update { let proxy = reqwest::Proxy::all(proxy.as_str())?; request = request.proxy(proxy); } - if let Some(ref configure_client) = self.configure_client { + if let Some(ref configure_client) = self.context.configure_client { request = configure_client(request); } let response = request @@ -709,7 +737,7 @@ impl Update { } on_download_finish(); - verify_signature(&buffer, &self.signature, &self.config.pubkey)?; + verify_signature(&buffer, &self.signature, &self.context.config.pubkey)?; Ok(buffer) } @@ -733,6 +761,16 @@ impl Update { fn install_inner(&self, _bytes: &[u8]) -> Result<()> { Ok(()) } + + /// If the Windows installer should restart the app after installed, default is `true` + #[cfg_attr(not(windows), allow(unused))] + pub fn restart_after_install(mut self, restart_after_install: bool) -> Self { + #[cfg(windows)] + { + self.context.restart_after_install = restart_after_install; + } + self + } } #[cfg(windows)] @@ -785,7 +823,6 @@ impl Update { /// │ └──[AppName]_[version]_x64-setup.exe # NSIS installer /// └── ... fn install_inner(&self, bytes: &[u8]) -> Result<()> { - use std::iter::once; use windows_sys::{ w, Win32::UI::{Shell::ShellExecuteW, WindowsAndMessaging::SW_SHOW}, @@ -793,48 +830,7 @@ impl Update { let updater_type = self.extract(bytes)?; - let install_mode = self.config.install_mode(); - let current_args = &self.current_exe_args()[1..]; - let msi_args; - let nsis_args; - - let installer_args: Vec<&OsStr> = match &updater_type { - WindowsUpdaterType::Nsis { .. } => { - nsis_args = current_args - .iter() - .map(escape_nsis_current_exe_arg) - .collect::>(); - - install_mode - .nsis_args() - .iter() - .map(OsStr::new) - .chain(once(OsStr::new("/UPDATE"))) - .chain(once(OsStr::new("/ARGS"))) - .chain(nsis_args.iter().map(OsStr::new)) - .chain(self.installer_args()) - .collect() - } - WindowsUpdaterType::Msi { path, .. } => { - let escaped_args = current_args - .iter() - .map(escape_msi_property_arg) - .collect::>() - .join(" "); - msi_args = OsString::from(format!("LAUNCHAPPARGS=\"{escaped_args}\"")); - - [OsStr::new("/i"), path.as_os_str()] - .into_iter() - .chain(install_mode.msiexec_args().iter().map(OsStr::new)) - .chain(once(OsStr::new("/promptrestart"))) - .chain(self.installer_args()) - .chain(once(OsStr::new("AUTOLAUNCHAPP=True"))) - .chain(once(msi_args.as_os_str())) - .collect() - } - }; - - if let Some(on_before_exit) = self.on_before_exit.as_ref() { + if let Some(on_before_exit) = self.context.on_before_exit.as_ref() { log::debug!("running on_before_exit hook"); on_before_exit(); } @@ -846,9 +842,11 @@ impl Update { |p| OsString::from(format!("{p}\\System32\\msiexec.exe")), ), }; - let file = encode_wide(file); + let parameters = self.updater_parameters(&updater_type); - let parameters = installer_args.join(OsStr::new(" ")); + log::debug!("Executing updater {file:?} with parameters: {parameters:?}"); + + let file = encode_wide(file); let parameters = encode_wide(parameters); unsafe { @@ -865,18 +863,72 @@ impl Update { std::process::exit(0); } - fn installer_args(&self) -> Vec<&OsStr> { - self.installer_args - .iter() - .map(OsStr::new) - .collect::>() + fn updater_parameters(&self, updater_type: &WindowsUpdaterType) -> OsString { + let install_mode = self.context.config.install_mode(); + let current_args = &self.context.current_exe_args[1..]; + + match updater_type { + WindowsUpdaterType::Nsis { .. } => { + let mut installer_args: Vec<&OsStr> = Vec::new(); + installer_args.extend(install_mode.nsis_args().iter().map(OsStr::new)); + installer_args.push(OsStr::new("/UPDATE")); + + let nsis_current_exe_arg; + if self.context.restart_after_install { + nsis_current_exe_arg = current_args + .iter() + .map(escape_nsis_current_exe_arg) + .collect::>(); + + installer_args.extend( + install_mode + .nsis_restart_after_install_args() + .iter() + .map(OsStr::new), + ); + installer_args.push(OsStr::new("/ARGS")); + installer_args.extend(nsis_current_exe_arg.iter().map(OsStr::new)); + } + + installer_args.extend(self.installer_args()); + + installer_args.join(OsStr::new(" ")) + } + WindowsUpdaterType::Msi { path, .. } => { + let mut installer_args: Vec<&OsStr> = vec![OsStr::new("/i"), path.as_os_str()]; + installer_args.extend(install_mode.msiexec_args().iter().map(OsStr::new)); + installer_args.push(OsStr::new("/promptrestart")); + installer_args.extend(self.installer_args()); + + let msi_current_exe_arg; + if self.context.restart_after_install { + msi_current_exe_arg = format!( + "LAUNCHAPPARGS=\"{}\"", + current_args + .iter() + .map(escape_msi_property_arg) + .collect::>() + .join(" ") + ); + + installer_args.extend( + install_mode + .msi_restart_after_install_args() + .iter() + .map(OsStr::new), + ); + installer_args.push(OsStr::new(&msi_current_exe_arg)); + } + + installer_args.join(OsStr::new(" ")) + } + } } - fn current_exe_args(&self) -> Vec<&OsStr> { - self.current_exe_args - .iter() - .map(OsStr::new) - .collect::>() + fn installer_args( + &self, + ) -> std::iter::Map, fn(&OsString) -> &OsStr> { + self.context.installer_args.iter().map(OsStr::new) } fn extract(&self, bytes: &[u8]) -> Result { @@ -890,7 +942,10 @@ impl Update { fn make_temp_dir(&self) -> Result { Ok(tempfile::Builder::new() - .prefix(&format!("{}-{}-updater-", self.app_name, self.version)) + .prefix(&format!( + "{}-{}-updater-", + self.context.app_name, self.version + )) .tempdir()? .keep()) } @@ -938,7 +993,10 @@ impl Update { let temp_dir = self.make_temp_dir()?; let mut temp_file = tempfile::Builder::new() - .prefix(&format!("{}-{}-installer", self.app_name, self.version)) + .prefix(&format!( + "{}-{}-installer", + self.context.app_name, self.version + )) .suffix(ext) .rand_bytes(0) .tempfile_in(temp_dir)?; @@ -1277,7 +1335,7 @@ impl Update { ); let (tx, rx) = std::sync::mpsc::channel(); - let res = (self.run_on_main_thread)(Box::new(move || { + let res = (self.context.run_on_main_thread)(Box::new(move || { let mut script = osakit::Script::new_from_source(osakit::Language::AppleScript, &apple_script); script.compile().expect("invalid AppleScript"); @@ -1450,7 +1508,7 @@ where } // Validate signature -fn verify_signature(data: &[u8], release_signature: &str, pub_key: &str) -> Result { +fn verify_signature(data: &[u8], release_signature: &str, pub_key: &str) -> Result<()> { // we need to convert the pub key let pub_key_decoded = base64_to_string(pub_key)?; let public_key = PublicKey::decode(&pub_key_decoded)?; @@ -1459,7 +1517,7 @@ fn verify_signature(data: &[u8], release_signature: &str, pub_key: &str) -> Resu // Validate signature or bail out public_key.verify(data, &signature, true)?; - Ok(true) + Ok(()) } fn base64_to_string(base64_string: &str) -> Result { @@ -1498,25 +1556,31 @@ impl PathExt for PathBuf { // adapted from https://github.com/rust-lang/rust/blob/1c047506f94cd2d05228eb992b0a6bbed1942349/library/std/src/sys/args/windows.rs#L174 #[cfg(windows)] -fn escape_nsis_current_exe_arg(arg: &&OsStr) -> String { - let arg = arg.to_string_lossy(); - let mut cmd: Vec = Vec::new(); +fn escape_nsis_current_exe_arg(arg: impl AsRef) -> OsString { + use std::os::windows::ffi::{OsStrExt, OsStringExt}; + + let arg = arg.as_ref(); + let mut cmd: Vec = Vec::new(); // compared to std we additionally escape `/` so that nsis won't interpret them as a beginning of an nsis argument. - let quote = arg.chars().any(|c| c == ' ' || c == '\t' || c == '/') || arg.is_empty(); + let quote = arg + .as_encoded_bytes() + .iter() + .any(|c| *c == b' ' || *c == b'\t' || *c == b'/') + || arg.is_empty(); let escape = true; if quote { - cmd.push('"'); + cmd.push('"' as u16); } let mut backslashes: usize = 0; - for x in arg.chars() { + for x in arg.encode_wide() { if escape { - if x == '\\' { + if x == '\\' as u16 { backslashes += 1; } else { - if x == '"' { + if x == '"' as u16 { // Add n+1 backslashes to total 2n+1 before internal '"'. - cmd.extend((0..=backslashes).map(|_| '\\')); + cmd.extend((0..=backslashes).map(|_| '\\' as u16)); } backslashes = 0; } @@ -1525,10 +1589,10 @@ fn escape_nsis_current_exe_arg(arg: &&OsStr) -> String { } if quote { // Add n backslashes to total 2n before ending '"'. - cmd.extend((0..backslashes).map(|_| '\\')); - cmd.push('"'); + cmd.extend((0..backslashes).map(|_| '\\' as u16)); + cmd.push('"' as u16); } - cmd.into_iter().collect() + OsString::from_wide(&cmd) } #[cfg(windows)] diff --git a/plugins/updater/tests/app-updater/tauri.conf.json b/plugins/updater/tests/app-updater/tauri.conf.json index fc70993ebb..705a73f825 100644 --- a/plugins/updater/tests/app-updater/tauri.conf.json +++ b/plugins/updater/tests/app-updater/tauri.conf.json @@ -6,8 +6,7 @@ "endpoints": ["http://localhost:3007"], "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUwNDRGMjkwRjg2MDhCRDAKUldUUWkyRDRrUEpFNEQ4SmdwcU5PaXl6R2ZRUUNvUnhIaVkwVUltV0NMaEx6VTkrWVhpT0ZqeEEK", "windows": { - "installMode": "quiet", - "installerArgs": ["/NS"] + "installMode": "quiet" } } }, diff --git a/plugins/updater/tests/app-updater/tests/update.rs b/plugins/updater/tests/app-updater/tests/update.rs index f962eff9ec..acaee63c8b 100644 --- a/plugins/updater/tests/app-updater/tests/update.rs +++ b/plugins/updater/tests/app-updater/tests/update.rs @@ -46,7 +46,7 @@ struct Update { platforms: HashMap, } -fn build_app(cwd: &Path, config: &Config, bundle_updater: bool, target: BundleTarget) { +fn build_app(cwd: &Path, config: &Config, target: Option) { let mut command = Command::new("cargo"); command .args(["tauri", "build", "--verbose"]) @@ -56,17 +56,10 @@ fn build_app(cwd: &Path, config: &Config, bundle_updater: bool, target: BundleTa .env("TAURI_SIGNING_PRIVATE_KEY_PASSWORD", "") .current_dir(cwd); - #[cfg(target_os = "linux")] - command.args(["--bundles", target.name()]); - #[cfg(target_os = "macos")] - command.args(["--bundles", target.name()]); - - if bundle_updater { - #[cfg(windows)] - command.args(["--bundles", "msi", "nsis"]); + if let Some(target) = target { + command.arg("--bundles").arg(target.name()); } else { - #[cfg(windows)] - command.args(["--bundles", target.name()]); + command.arg("--no-bundle"); } let status = command @@ -318,7 +311,7 @@ fn update_app() { { // bundle app update config.version = "1.0.0"; - build_app(&manifest_dir, &config, true, BundleTarget::default()); + build_app(&manifest_dir, &config, Some(bundle_target)); let bundle_updater_ext = if v1_compatible { out_bundle_path @@ -401,7 +394,7 @@ fn update_app() { config.version = "0.1.0"; // bundle initial app version - build_app(&manifest_dir, &config, false, bundle_target); + build_app(&manifest_dir, &config, None); for expected_exit_code in status_checks { let mut binary_cmd = if cfg!(windows) { diff --git a/plugins/updater/tests/updater-migration/v1-app/Cargo.lock b/plugins/updater/tests/updater-migration/v1-app/Cargo.lock index e5ba9948e6..6c1c30fef7 100644 --- a/plugins/updater/tests/updater-migration/v1-app/Cargo.lock +++ b/plugins/updater/tests/updater-migration/v1-app/Cargo.lock @@ -3795,7 +3795,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] @@ -4138,9 +4138,9 @@ dependencies = [ [[package]] name = "wry" -version = "0.24.10" +version = "0.24.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00711278ed357350d44c749c286786ecac644e044e4da410d466212152383b45" +checksum = "4a2a144c3ab5e83e04724bc8e67cea552ffae413185fda459fafdae173fd985d" dependencies = [ "base64 0.13.1", "block",