auto_update.rs

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