auto_update.rs

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