auto_update.rs

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