auto_update.rs

  1mod update_notification;
  2
  3use anyhow::{anyhow, Context, Result};
  4use client::{Client, ZED_APP_PATH, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN};
  5use db::kvp::KEY_VALUE_STORE;
  6use gpui::{
  7    actions, platform::AppVersion, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
  8    Task, WeakViewHandle,
  9};
 10use isahc::AsyncBody;
 11use serde::Deserialize;
 12use serde_derive::Serialize;
 13use settings::{Setting, Settings, SettingsStore};
 14use smol::{fs::File, io::AsyncReadExt, process::Command};
 15use std::{ffi::OsString, sync::Arc, time::Duration};
 16use update_notification::UpdateNotification;
 17use util::channel::ReleaseChannel;
 18use util::http::HttpClient;
 19use workspace::Workspace;
 20
 21const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification";
 22const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60);
 23
 24actions!(auto_update, [Check, DismissErrorMessage, ViewReleaseNotes]);
 25
 26#[derive(Serialize)]
 27struct UpdateRequestBody {
 28    installation_id: Option<Arc<str>>,
 29    release_channel: Option<&'static str>,
 30    telemetry: bool,
 31}
 32
 33#[derive(Clone, Copy, PartialEq, Eq)]
 34pub enum AutoUpdateStatus {
 35    Idle,
 36    Checking,
 37    Downloading,
 38    Installing,
 39    Updated,
 40    Errored,
 41}
 42
 43pub struct AutoUpdater {
 44    status: AutoUpdateStatus,
 45    current_version: AppVersion,
 46    http_client: Arc<dyn HttpClient>,
 47    pending_poll: Option<Task<()>>,
 48    server_url: String,
 49}
 50
 51#[derive(Deserialize)]
 52struct JsonRelease {
 53    version: String,
 54    url: String,
 55}
 56
 57impl Entity for AutoUpdater {
 58    type Event = ();
 59}
 60
 61struct AutoUpdateSetting(bool);
 62
 63impl Setting for AutoUpdateSetting {
 64    const KEY: Option<&'static str> = Some("auto_update");
 65
 66    type FileContent = Option<bool>;
 67
 68    fn load(default_value: &Option<bool>, user_values: &[&Option<bool>], _: &AppContext) -> Self {
 69        Self(
 70            Self::json_merge(default_value, user_values)
 71                .unwrap()
 72                .unwrap(),
 73        )
 74    }
 75}
 76
 77pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppContext) {
 78    settings::register_setting::<AutoUpdateSetting>(cx);
 79
 80    if let Some(version) = (*ZED_APP_VERSION).or_else(|| cx.platform().app_version().ok()) {
 81        let auto_updater = cx.add_model(|cx| {
 82            let updater = AutoUpdater::new(version, http_client, server_url);
 83
 84            let mut update_subscription = settings::get_setting::<AutoUpdateSetting>(None, cx)
 85                .0
 86                .then(|| updater.start_polling(cx));
 87
 88            cx.observe_global::<SettingsStore, _>(move |updater, cx| {
 89                if settings::get_setting::<AutoUpdateSetting>(None, cx).0 {
 90                    if update_subscription.is_none() {
 91                        update_subscription = Some(updater.start_polling(cx))
 92                    }
 93                } else {
 94                    update_subscription.take();
 95                }
 96            })
 97            .detach();
 98
 99            updater
100        });
101        cx.set_global(Some(auto_updater));
102        cx.add_global_action(check);
103        cx.add_global_action(view_release_notes);
104        cx.add_action(UpdateNotification::dismiss);
105    }
106}
107
108pub fn check(_: &Check, cx: &mut AppContext) {
109    if let Some(updater) = AutoUpdater::get(cx) {
110        updater.update(cx, |updater, cx| updater.poll(cx));
111    }
112}
113
114fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) {
115    if let Some(auto_updater) = AutoUpdater::get(cx) {
116        let server_url = &auto_updater.read(cx).server_url;
117        let latest_release_url = if cx.has_global::<ReleaseChannel>()
118            && *cx.global::<ReleaseChannel>() == ReleaseChannel::Preview
119        {
120            format!("{server_url}/releases/preview/latest")
121        } else {
122            format!("{server_url}/releases/latest")
123        };
124        cx.platform().open_url(&latest_release_url);
125    }
126}
127
128pub fn notify_of_any_new_update(
129    workspace: WeakViewHandle<Workspace>,
130    cx: &mut AppContext,
131) -> Option<()> {
132    let updater = AutoUpdater::get(cx)?;
133    let version = updater.read(cx).current_version;
134    let should_show_notification = updater.read(cx).should_show_update_notification(cx);
135
136    cx.spawn(|mut cx| async move {
137        let should_show_notification = should_show_notification.await?;
138        if should_show_notification {
139            workspace.update(&mut cx, |workspace, cx| {
140                workspace.show_notification(0, cx, |cx| {
141                    cx.add_view(|_| UpdateNotification::new(version))
142                });
143                updater
144                    .read(cx)
145                    .set_should_show_update_notification(false, cx)
146                    .detach_and_log_err(cx);
147            })?;
148        }
149        anyhow::Ok(())
150    })
151    .detach();
152
153    None
154}
155
156impl AutoUpdater {
157    pub fn get(cx: &mut AppContext) -> Option<ModelHandle<Self>> {
158        cx.default_global::<Option<ModelHandle<Self>>>().clone()
159    }
160
161    fn new(
162        current_version: AppVersion,
163        http_client: Arc<dyn HttpClient>,
164        server_url: String,
165    ) -> Self {
166        Self {
167            status: AutoUpdateStatus::Idle,
168            current_version,
169            http_client,
170            server_url,
171            pending_poll: None,
172        }
173    }
174
175    pub fn start_polling(&self, cx: &mut ModelContext<Self>) -> Task<()> {
176        cx.spawn(|this, mut cx| async move {
177            loop {
178                this.update(&mut cx, |this, cx| this.poll(cx));
179                cx.background().timer(POLL_INTERVAL).await;
180            }
181        })
182    }
183
184    pub fn poll(&mut self, cx: &mut ModelContext<Self>) {
185        if self.pending_poll.is_some() || self.status == AutoUpdateStatus::Updated {
186            return;
187        }
188
189        self.status = AutoUpdateStatus::Checking;
190        cx.notify();
191
192        self.pending_poll = Some(cx.spawn(|this, mut cx| async move {
193            let result = Self::update(this.clone(), cx.clone()).await;
194            this.update(&mut cx, |this, cx| {
195                this.pending_poll = None;
196                if let Err(error) = result {
197                    log::error!("auto-update failed: error:{:?}", error);
198                    this.status = AutoUpdateStatus::Errored;
199                    cx.notify();
200                }
201            });
202        }));
203    }
204
205    pub fn status(&self) -> AutoUpdateStatus {
206        self.status
207    }
208
209    pub fn dismiss_error(&mut self, cx: &mut ModelContext<Self>) {
210        self.status = AutoUpdateStatus::Idle;
211        cx.notify();
212    }
213
214    async fn update(this: ModelHandle<Self>, mut cx: AsyncAppContext) -> Result<()> {
215        let (client, server_url, current_version) = this.read_with(&cx, |this, _| {
216            (
217                this.http_client.clone(),
218                this.server_url.clone(),
219                this.current_version,
220            )
221        });
222
223        let preview_param = cx.read(|cx| {
224            if cx.has_global::<ReleaseChannel>() {
225                if *cx.global::<ReleaseChannel>() == ReleaseChannel::Preview {
226                    return "&preview=1";
227                }
228            }
229            ""
230        });
231
232        let mut response = client
233            .get(
234                &format!("{server_url}/api/releases/latest?token={ZED_SECRET_CLIENT_TOKEN}&asset=Zed.dmg{preview_param}"),
235                Default::default(),
236                true,
237            )
238            .await?;
239
240        let mut body = Vec::new();
241        response
242            .body_mut()
243            .read_to_end(&mut body)
244            .await
245            .context("error reading release")?;
246        let release: JsonRelease =
247            serde_json::from_slice(body.as_slice()).context("error deserializing release")?;
248
249        let latest_version = release.version.parse::<AppVersion>()?;
250        if latest_version <= current_version {
251            this.update(&mut cx, |this, cx| {
252                this.status = AutoUpdateStatus::Idle;
253                cx.notify();
254            });
255            return Ok(());
256        }
257
258        this.update(&mut cx, |this, cx| {
259            this.status = AutoUpdateStatus::Downloading;
260            cx.notify();
261        });
262
263        let temp_dir = tempdir::TempDir::new("zed-auto-update")?;
264        let dmg_path = temp_dir.path().join("Zed.dmg");
265        let mount_path = temp_dir.path().join("Zed");
266        let running_app_path = ZED_APP_PATH
267            .clone()
268            .map_or_else(|| cx.platform().app_path(), Ok)?;
269        let running_app_filename = running_app_path
270            .file_name()
271            .ok_or_else(|| anyhow!("invalid running app path"))?;
272        let mut mounted_app_path: OsString = mount_path.join(running_app_filename).into();
273        mounted_app_path.push("/");
274
275        let mut dmg_file = File::create(&dmg_path).await?;
276
277        let (installation_id, release_channel, telemetry) = cx.read(|cx| {
278            let installation_id = cx.global::<Arc<Client>>().telemetry().installation_id();
279            let release_channel = cx
280                .has_global::<ReleaseChannel>()
281                .then(|| cx.global::<ReleaseChannel>().display_name());
282            let telemetry = cx.global::<Settings>().telemetry().metrics();
283
284            (installation_id, release_channel, telemetry)
285        });
286
287        let request_body = AsyncBody::from(serde_json::to_string(&UpdateRequestBody {
288            installation_id,
289            release_channel,
290            telemetry,
291        })?);
292
293        let mut response = client.get(&release.url, request_body, true).await?;
294        smol::io::copy(response.body_mut(), &mut dmg_file).await?;
295        log::info!("downloaded update. path:{:?}", dmg_path);
296
297        this.update(&mut cx, |this, cx| {
298            this.status = AutoUpdateStatus::Installing;
299            cx.notify();
300        });
301
302        let output = Command::new("hdiutil")
303            .args(&["attach", "-nobrowse"])
304            .arg(&dmg_path)
305            .arg("-mountroot")
306            .arg(&temp_dir.path())
307            .output()
308            .await?;
309        if !output.status.success() {
310            Err(anyhow!(
311                "failed to mount: {:?}",
312                String::from_utf8_lossy(&output.stderr)
313            ))?;
314        }
315
316        let output = Command::new("rsync")
317            .args(&["-av", "--delete"])
318            .arg(&mounted_app_path)
319            .arg(&running_app_path)
320            .output()
321            .await?;
322        if !output.status.success() {
323            Err(anyhow!(
324                "failed to copy app: {:?}",
325                String::from_utf8_lossy(&output.stderr)
326            ))?;
327        }
328
329        let output = Command::new("hdiutil")
330            .args(&["detach"])
331            .arg(&mount_path)
332            .output()
333            .await?;
334        if !output.status.success() {
335            Err(anyhow!(
336                "failed to unmount: {:?}",
337                String::from_utf8_lossy(&output.stderr)
338            ))?;
339        }
340
341        this.update(&mut cx, |this, cx| {
342            this.set_should_show_update_notification(true, cx)
343                .detach_and_log_err(cx);
344            this.status = AutoUpdateStatus::Updated;
345            cx.notify();
346        });
347        Ok(())
348    }
349
350    fn set_should_show_update_notification(
351        &self,
352        should_show: bool,
353        cx: &AppContext,
354    ) -> Task<Result<()>> {
355        cx.background().spawn(async move {
356            if should_show {
357                KEY_VALUE_STORE
358                    .write_kvp(
359                        SHOULD_SHOW_UPDATE_NOTIFICATION_KEY.to_string(),
360                        "".to_string(),
361                    )
362                    .await?;
363            } else {
364                KEY_VALUE_STORE
365                    .delete_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY.to_string())
366                    .await?;
367            }
368            Ok(())
369        })
370    }
371
372    fn should_show_update_notification(&self, cx: &AppContext) -> Task<Result<bool>> {
373        cx.background().spawn(async move {
374            Ok(KEY_VALUE_STORE
375                .read_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)?
376                .is_some())
377        })
378    }
379}