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