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