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