mod.rs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587
  1. // Copyright 2019-2021 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. //! The Tauri updater.
  5. //!
  6. //! The updater is focused on making Tauri's application updates **as safe and transparent as updates to a website**.
  7. //!
  8. //! Instead of publishing a feed of versions from which your app must select, Tauri updates to the version your server tells it to. This allows you to intelligently update your clients based on the request you give to Tauri.
  9. //!
  10. //! The server can remotely drive behaviors like rolling back or phased rollouts.
  11. //!
  12. //! The update JSON Tauri requests should be dynamically generated based on criteria in the request, and whether an update is required.
  13. //!
  14. //! Tauri's installer is also designed to be fault-tolerant, and ensure that any updates installed are valid and safe.
  15. //!
  16. //! # Configuration
  17. //!
  18. //! Once you have your Tauri project ready, you need to configure the updater.
  19. //!
  20. //! Add this in tauri.conf.json
  21. //! ```json
  22. //! "updater": {
  23. //! "active": true,
  24. //! "endpoints": [
  25. //! "https://releases.myapp.com/{target}}/{current_version}}"
  26. //! ],
  27. //! "dialog": true,
  28. //! "pubkey": ""
  29. //! }
  30. //! ```
  31. //!
  32. //! The required keys are "active" and "endpoints", others are optional.
  33. //!
  34. //! "active" must be a boolean. By default, it's set to false.
  35. //!
  36. //! "endpoints" must be an array. The string `{{target}}` and `{{current_version}}` are automatically replaced in the URL allowing you determine [server-side](#update-server-json-format) if an update is available. If multiple endpoints are specified, the updater will fallback if a server is not responding within the pre-defined timeout.
  37. //!
  38. //! "dialog" if present must be a boolean. By default, it's set to true. If enabled, [events](#events) are turned-off as the updater will handle everything. If you need the custom events, you MUST turn off the built-in dialog.
  39. //!
  40. //! "pubkey" if present must be a valid public-key generated with Tauri cli. See [Signing updates](#signing-updates).
  41. //!
  42. //! ## Update Requests
  43. //!
  44. //! Tauri is indifferent to the request the client application provides for update checking.
  45. //!
  46. //! `Accept: application/json` is added to the request headers because Tauri is responsible for parsing the response.
  47. //!
  48. //! For the requirements imposed on the responses and the body format of an update, response see [Server Support](#server-support).
  49. //!
  50. //! Your update request must *at least* include a version identifier so that the server can determine whether an update for this specific version is required.
  51. //!
  52. //! It may also include other identifying criteria such as operating system version, to allow the server to deliver as fine-grained an update as you would like.
  53. //!
  54. //! How you include the version identifier or other criteria is specific to the server that you are requesting updates from. A common approach is to use query parameters, [Configuration](#configuration) shows an example of this.
  55. //!
  56. //! ## Built-in dialog
  57. //!
  58. //! By default, updater uses a built-in dialog API from Tauri.
  59. //!
  60. //! ![New Update](https://i.imgur.com/UMilB5A.png)
  61. //!
  62. //! The dialog release notes is represented by the update `note` provided by the [server](#server-support).
  63. //!
  64. //! If the user accepts, the download and install are initialized. The user will be then prompted to restart the application.
  65. //!
  66. //! ## Javascript API
  67. //!
  68. //! **Attention, you need to _disable built-in dialog_ in your [tauri configuration](#configuration), otherwise, events aren't emitted and the javascript API will NOT work.**
  69. //!
  70. //!
  71. //! ```javascript
  72. //! import { checkUpdate, installUpdate } from "@tauri-apps/api/updater";
  73. //!
  74. //! try {
  75. //! const {shouldUpdate, manifest} = await checkUpdate();
  76. //!
  77. //! if (shouldUpdate) {
  78. //! // display dialog
  79. //! await installUpdate();
  80. //! // install complete, ask to restart
  81. //! }
  82. //! } catch(error) {
  83. //! console.log(error);
  84. //! }
  85. //! ```
  86. //!
  87. //! ## Events
  88. //!
  89. //! **Attention, you need to _disable built-in dialog_ in your [tauri configuration](#configuration), otherwise, events aren't emitted.**
  90. //!
  91. //! To know when an update is ready to be installed, you can subscribe to these events:
  92. //!
  93. //! ### Initialize updater and check if a new version is available
  94. //!
  95. //! #### If a new version is available, the event `tauri://update-available` is emitted.
  96. //!
  97. //! Event : `tauri://update`
  98. //!
  99. //! ### Rust
  100. //! ```ignore
  101. //! dispatcher.emit("tauri://update", None);
  102. //! ```
  103. //!
  104. //! ### Javascript
  105. //! ```js
  106. //! import { emit } from "@tauri-apps/api/event";
  107. //! emit("tauri://update");
  108. //! ```
  109. //!
  110. //! ### Listen New Update Available
  111. //!
  112. //! Event : `tauri://update-available`
  113. //!
  114. //! Emitted data:
  115. //! ```text
  116. //! version Version announced by the server
  117. //! date Date announced by the server
  118. //! body Note announced by the server
  119. //! ```
  120. //!
  121. //! ### Rust
  122. //! ```ignore
  123. //! dispatcher.listen("tauri://update-available", move |msg| {
  124. //! println("New version available: {:?}", msg);
  125. //! })
  126. //! ```
  127. //!
  128. //! ### Javascript
  129. //! ```js
  130. //! import { listen } from "@tauri-apps/api/event";
  131. //! listen("tauri://update-available", function (res) {
  132. //! console.log("New version available: ", res);
  133. //! });
  134. //! ```
  135. //!
  136. //! ### Emit Install and Download
  137. //!
  138. //! You need to emit this event to initialize the download and listen to the [install progress](#listen-install-progress).
  139. //!
  140. //! Event : `tauri://update-install`
  141. //!
  142. //! ### Rust
  143. //! ```ignore
  144. //! dispatcher.emit("tauri://update-install", None);
  145. //! ```
  146. //!
  147. //! ### Javascript
  148. //! ```js
  149. //! import { emit } from "@tauri-apps/api/event";
  150. //! emit("tauri://update-install");
  151. //! ```
  152. //!
  153. //! ### Listen Install Progress
  154. //!
  155. //! Event : `tauri://update-status`
  156. //!
  157. //! Emitted data:
  158. //! ```text
  159. //! status [ERROR/PENDING/DONE]
  160. //! error String/null
  161. //! ```
  162. //!
  163. //! PENDING is emitted when the download is started and DONE when the install is complete. You can then ask to restart the application.
  164. //!
  165. //! ERROR is emitted when there is an error with the updater. We suggest to listen to this event even if the dialog is enabled.
  166. //!
  167. //! ### Rust
  168. //! ```ignore
  169. //! dispatcher.listen("tauri://update-status", move |msg| {
  170. //! println("New status: {:?}", msg);
  171. //! })
  172. //! ```
  173. //!
  174. //! ### Javascript
  175. //! ```js
  176. //! import { listen } from "@tauri-apps/api/event";
  177. //! listen("tauri://update-status", function (res) {
  178. //! console.log("New status: ", res);
  179. //! });
  180. //! ```
  181. //!
  182. //! # Server Support
  183. //!
  184. //! Your server should determine whether an update is required based on the [Update Request](#update-requests) your client issues.
  185. //!
  186. //! If an update is required your server should respond with a status code of [200 OK](http://tools.ietf.org/html/rfc2616#section-10.2.1) and include the [update JSON](#update-server-json-format) in the body. To save redundantly downloading the same version multiple times your server must not inform the client to update.
  187. //!
  188. //! If no update is required your server must respond with a status code of [204 No Content](http://tools.ietf.org/html/rfc2616#section-10.2.5).
  189. //!
  190. //! ## Update Server JSON Format
  191. //!
  192. //! When an update is available, Tauri expects the following schema in response to the update request provided:
  193. //!
  194. //! ```json
  195. //! {
  196. //! "url": "https://mycompany.example.com/myapp/releases/myrelease.tar.gz",
  197. //! "version": "0.0.1",
  198. //! "notes": "Theses are some release notes",
  199. //! "pub_date": "2020-09-18T12:29:53+01:00",
  200. //! "signature": ""
  201. //! }
  202. //! ```
  203. //!
  204. //! The only required keys are "url" and "version", the others are optional.
  205. //!
  206. //! "pub_date" if present must be formatted according to ISO 8601.
  207. //!
  208. //! "signature" if present must be a valid signature generated with Tauri cli. See [Signing updates](#signing-updates).
  209. //!
  210. //! ## Update File JSON Format
  211. //!
  212. //! The alternate update technique uses a plain JSON file meaning you can store your update metadata on S3, gist, or another static file store. Tauri will check against the name/version field and if the version is smaller than the current one and the platform is available, the update will be triggered. The format of this file is detailed below:
  213. //!
  214. //! ```json
  215. //! {
  216. //! "name":"v1.0.0",
  217. //! "notes":"Test version",
  218. //! "pub_date":"2020-06-22T19:25:57Z",
  219. //! "platforms": {
  220. //! "darwin": {
  221. //! "signature":"",
  222. //! "url":"https://github.com/lemarier/tauri-test/releases/download/v1.0.0/app.app.tar.gz"
  223. //! },
  224. //! "linux": {
  225. //! "signature":"",
  226. //! "url":"https://github.com/lemarier/tauri-test/releases/download/v1.0.0/app.AppImage.tar.gz"
  227. //! },
  228. //! "win64": {
  229. //! "signature":"",
  230. //! "url":"https://github.com/lemarier/tauri-test/releases/download/v1.0.0/app.x64.msi.zip"
  231. //! }
  232. //! }
  233. //! }
  234. //! ```
  235. //!
  236. //!
  237. //! # Bundler (Artifacts)
  238. //!
  239. //! The Tauri bundler will automatically generate update artifacts if the updater is enabled in `tauri.conf.json`
  240. //!
  241. //! If the bundler can locate your private and pubkey, your update artifacts will be automatically signed.
  242. //!
  243. //! The signature can be found in the `sig` file. The signature can be uploaded to GitHub safely or made public as long as your private key is secure.
  244. //!
  245. //! You can see how it's [bundled with the CI](https://github.com/tauri-apps/tauri/blob/feature/new_updater/.github/workflows/artifacts-updater.yml#L44) and a [sample tauri.conf.json](https://github.com/tauri-apps/tauri/blob/feature/new_updater/examples/updater/src-tauri/tauri.conf.json#L52)
  246. //!
  247. //! ## macOS
  248. //!
  249. //! On MACOS we create a .tar.gz from the whole application. (.app)
  250. //!
  251. //! ```text
  252. //! target/release/bundle
  253. //! └── osx
  254. //! └── app.app
  255. //! └── app.app.tar.gz (update bundle)
  256. //! └── app.app.tar.gz.sig (if signature enabled)
  257. //! ```
  258. //!
  259. //! ## Windows
  260. //!
  261. //! On Windows we create a .zip from the MSI, when downloaded and validated, we run the MSI install.
  262. //!
  263. //! ```text
  264. //! target/release
  265. //! └── app.x64.msi
  266. //! └── app.x64.msi.zip (update bundle)
  267. //! └── app.x64.msi.zip.sig (if signature enabled)
  268. //! ```
  269. //!
  270. //! ## Linux
  271. //!
  272. //! On Linux, we create a .tar.gz from the AppImage.
  273. //!
  274. //! ```text
  275. //! target/release/bundle
  276. //! └── appimage
  277. //! └── app.AppImage
  278. //! └── app.AppImage.tar.gz (update bundle)
  279. //! └── app.AppImage.tar.gz.sig (if signature enabled)
  280. //! ```
  281. //!
  282. //! # Signing updates
  283. //!
  284. //! We offer a built-in signature to ensure your update is safe to be installed.
  285. //!
  286. //! To sign your updates, you need two things.
  287. //!
  288. //! The *Public-key* (pubkey) should be added inside your `tauri.conf.json` to validate the update archive before installing.
  289. //!
  290. //! The *Private key* (privkey) is used to sign your update and should NEVER be shared with anyone. Also, if you lost this key, you'll NOT be able to publish a new update to the current user base (if pubkey is set in tauri.conf.json). It's important to save it at a safe place and you can always access it.
  291. //!
  292. //! To generate your keys you need to use the Tauri cli.
  293. //!
  294. //! ```bash
  295. //! tauri signer sign -g -w ~/.tauri/myapp.key
  296. //! ```
  297. //!
  298. //! You have multiple options available
  299. //! ```bash
  300. //! Tauri updates signer.
  301. //!
  302. //! USAGE:
  303. //! tauri signer sign [FLAGS] [OPTIONS]
  304. //!
  305. //! FLAGS:
  306. //! --force Overwrite private key even if it exists on the specified path
  307. //! -g, --generate Generate keypair to sign files
  308. //! -h, --help Prints help information
  309. //! --no-password Set empty password for your private key
  310. //! -V, --version Prints version information
  311. //!
  312. //! OPTIONS:
  313. //! -p, --password <password> Set private key password when signing
  314. //! -k, --private-key <private-key> Load the private key from a string
  315. //! -f, --private-key-path <private-key-path> Load the private key from a file
  316. //! --sign-file <sign-file> Sign the specified file
  317. //! -w, --write-keys <write-keys> Write private key to a file
  318. //! ```
  319. //!
  320. //! ***
  321. //!
  322. //! Environment variables used to sign with `tauri-bundler`:
  323. //! If they are set, and `tauri.conf.json` expose the public key, the bundler will automatically generate and sign the updater artifacts.
  324. //!
  325. //! `TAURI_PRIVATE_KEY` Path or String of your private key
  326. //!
  327. //! `TAURI_KEY_PASSWORD` Your private key password (optional)
  328. mod core;
  329. mod error;
  330. pub use self::error::Error;
  331. use crate::{
  332. api::dialog::blocking::ask, runtime::EventLoopProxy, utils::config::UpdaterConfig, AppHandle,
  333. Env, EventLoopMessage, Manager, Runtime, UpdaterEvent,
  334. };
  335. /// Check for new updates
  336. pub const EVENT_CHECK_UPDATE: &str = "tauri://update";
  337. /// New update available
  338. pub const EVENT_UPDATE_AVAILABLE: &str = "tauri://update-available";
  339. /// Used to initialize an update *should run check-update first (once you received the update available event)*
  340. pub const EVENT_INSTALL_UPDATE: &str = "tauri://update-install";
  341. /// Send updater status or error even if dialog is enabled, you should
  342. /// always listen for this event. It'll send you the install progress
  343. /// and any error triggered during update check and install
  344. pub const EVENT_STATUS_UPDATE: &str = "tauri://update-status";
  345. /// this is the status emitted when the download start
  346. pub const EVENT_STATUS_PENDING: &str = "PENDING";
  347. /// When you got this status, something went wrong
  348. /// you can find the error message inside the `error` field.
  349. pub const EVENT_STATUS_ERROR: &str = "ERROR";
  350. /// When you receive this status, you should ask the user to restart
  351. pub const EVENT_STATUS_SUCCESS: &str = "DONE";
  352. /// When you receive this status, this is because the application is running last version
  353. pub const EVENT_STATUS_UPTODATE: &str = "UPTODATE";
  354. #[derive(Clone, serde::Serialize)]
  355. struct StatusEvent {
  356. status: String,
  357. error: Option<String>,
  358. }
  359. #[derive(Clone, serde::Serialize)]
  360. struct UpdateManifest {
  361. version: String,
  362. date: String,
  363. body: String,
  364. }
  365. /// Check if there is any new update with builtin dialog.
  366. pub(crate) async fn check_update_with_dialog<R: Runtime>(
  367. updater_config: UpdaterConfig,
  368. package_info: crate::PackageInfo,
  369. handle: AppHandle<R>,
  370. ) {
  371. if let Some(endpoints) = updater_config.endpoints.clone() {
  372. let endpoints = endpoints
  373. .iter()
  374. .map(|e| e.to_string())
  375. .collect::<Vec<String>>();
  376. let env = handle.state::<Env>().inner().clone();
  377. // check updates
  378. match self::core::builder(env)
  379. .urls(&endpoints[..])
  380. .current_version(&package_info.version)
  381. .build()
  382. .await
  383. {
  384. Ok(updater) => {
  385. let pubkey = updater_config.pubkey.clone();
  386. // if dialog enabled only
  387. if updater.should_update && updater_config.dialog {
  388. let body = updater.body.clone().unwrap_or_else(|| String::from(""));
  389. let handle_ = handle.clone();
  390. let dialog = prompt_for_install(
  391. handle_,
  392. &updater.clone(),
  393. &package_info.name,
  394. &body.clone(),
  395. pubkey,
  396. )
  397. .await;
  398. if let Err(e) = dialog {
  399. send_status_update(&handle, UpdaterEvent::Error(e.to_string()));
  400. }
  401. }
  402. }
  403. Err(e) => {
  404. send_status_update(&handle, UpdaterEvent::Error(e.to_string()));
  405. }
  406. }
  407. }
  408. }
  409. /// Experimental listener
  410. /// This function should be run on the main thread once.
  411. pub(crate) fn listener<R: Runtime>(
  412. updater_config: UpdaterConfig,
  413. package_info: crate::PackageInfo,
  414. handle: &AppHandle<R>,
  415. ) {
  416. let handle_ = handle.clone();
  417. // Wait to receive the event `"tauri://update"`
  418. handle.listen_global(EVENT_CHECK_UPDATE, move |_msg| {
  419. let handle = handle_.clone();
  420. let package_info = package_info.clone();
  421. // prepare our endpoints
  422. let endpoints = updater_config
  423. .endpoints
  424. .as_ref()
  425. .expect("Something wrong with endpoints")
  426. .iter()
  427. .map(|e| e.to_string())
  428. .collect::<Vec<String>>();
  429. let pubkey = updater_config.pubkey.clone();
  430. // check updates
  431. crate::async_runtime::spawn(async move {
  432. let handle = handle.clone();
  433. let handle_ = handle.clone();
  434. let pubkey = pubkey.clone();
  435. let env = handle.state::<Env>().inner().clone();
  436. match self::core::builder(env)
  437. .urls(&endpoints[..])
  438. .current_version(&package_info.version)
  439. .build()
  440. .await
  441. {
  442. Ok(updater) => {
  443. // send notification if we need to update
  444. if updater.should_update {
  445. let body = updater.body.clone().unwrap_or_else(|| String::from(""));
  446. // Emit `tauri://update-available`
  447. let _ = handle.emit_all(
  448. EVENT_UPDATE_AVAILABLE,
  449. UpdateManifest {
  450. body: body.clone(),
  451. date: updater.date.clone(),
  452. version: updater.version.clone(),
  453. },
  454. );
  455. let _ = handle.create_proxy().send_event(EventLoopMessage::Updater(
  456. UpdaterEvent::UpdateAvailable {
  457. body,
  458. date: updater.date.clone(),
  459. version: updater.version.clone(),
  460. },
  461. ));
  462. // Listen for `tauri://update-install`
  463. handle.once_global(EVENT_INSTALL_UPDATE, move |_msg| {
  464. let handle = handle_.clone();
  465. let updater = updater.clone();
  466. // Start installation
  467. crate::async_runtime::spawn(async move {
  468. // emit {"status": "PENDING"}
  469. send_status_update(&handle, UpdaterEvent::Pending);
  470. // Launch updater download process
  471. // macOS we display the `Ready to restart dialog` asking to restart
  472. // Windows is closing the current App and launch the downloaded MSI when ready (the process stop here)
  473. // Linux we replace the AppImage by launching a new install, it start a new AppImage instance, so we're closing the previous. (the process stop here)
  474. let update_result = updater.clone().download_and_install(pubkey.clone()).await;
  475. if let Err(err) = update_result {
  476. // emit {"status": "ERROR", "error": "The error message"}
  477. send_status_update(&handle, UpdaterEvent::Error(err.to_string()));
  478. } else {
  479. // emit {"status": "DONE"}
  480. send_status_update(&handle, UpdaterEvent::Updated);
  481. }
  482. });
  483. });
  484. } else {
  485. send_status_update(&handle, UpdaterEvent::AlreadyUpToDate);
  486. }
  487. }
  488. Err(e) => {
  489. send_status_update(&handle, UpdaterEvent::Error(e.to_string()));
  490. }
  491. }
  492. });
  493. });
  494. }
  495. // Send a status update via `tauri://update-status` event.
  496. fn send_status_update<R: Runtime>(handle: &AppHandle<R>, message: UpdaterEvent) {
  497. let _ = handle.emit_all(
  498. EVENT_STATUS_UPDATE,
  499. if let UpdaterEvent::Error(error) = &message {
  500. StatusEvent {
  501. error: Some(error.clone()),
  502. status: message.clone().status_message().into(),
  503. }
  504. } else {
  505. StatusEvent {
  506. error: None,
  507. status: message.clone().status_message().into(),
  508. }
  509. },
  510. );
  511. let _ = handle
  512. .create_proxy()
  513. .send_event(EventLoopMessage::Updater(message));
  514. }
  515. // Prompt a dialog asking if the user want to install the new version
  516. // Maybe we should add an option to customize it in future versions.
  517. async fn prompt_for_install<R: Runtime>(
  518. handle: AppHandle<R>,
  519. updater: &self::core::Update,
  520. app_name: &str,
  521. body: &str,
  522. pubkey: String,
  523. ) -> crate::Result<()> {
  524. // remove single & double quote
  525. let escaped_body = body.replace(&['\"', '\''][..], "");
  526. let windows = handle.windows();
  527. let parent_window = windows.values().next();
  528. // todo(lemarier): We should review this and make sure we have
  529. // something more conventional.
  530. let should_install = ask(
  531. parent_window,
  532. format!(r#"A new version of {} is available! "#, app_name),
  533. format!(
  534. r#"{} {} is now available -- you have {}.
  535. Would you like to install it now?
  536. Release Notes:
  537. {}"#,
  538. app_name, updater.version, updater.current_version, escaped_body,
  539. ),
  540. );
  541. if should_install {
  542. // Launch updater download process
  543. // macOS we display the `Ready to restart dialog` asking to restart
  544. // Windows is closing the current App and launch the downloaded MSI when ready (the process stop here)
  545. // Linux we replace the AppImage by launching a new install, it start a new AppImage instance, so we're closing the previous. (the process stop here)
  546. updater.download_and_install(pubkey.clone()).await?;
  547. // Ask user if we need to restart the application
  548. let should_exit = ask(
  549. parent_window,
  550. "Ready to Restart",
  551. "The installation was successful, do you want to restart the application now?",
  552. );
  553. if should_exit {
  554. handle.restart();
  555. }
  556. }
  557. Ok(())
  558. }