1mod update_notification;
2
3use anyhow::{anyhow, Context, Result};
4use client::{Client, TelemetrySettings, ZED_APP_PATH};
5use db::kvp::KEY_VALUE_STORE;
6use db::RELEASE_CHANNEL;
7use editor::{Editor, MultiBuffer};
8use gpui::{
9 actions, AppContext, AsyncAppContext, Context as _, Global, Model, ModelContext,
10 SemanticVersion, SharedString, Task, View, ViewContext, VisualContext, WindowContext,
11};
12use isahc::AsyncBody;
13
14use markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPreviewView};
15use schemars::JsonSchema;
16use serde::Deserialize;
17use serde_derive::Serialize;
18use smol::io::AsyncReadExt;
19
20use settings::{Settings, SettingsSources, SettingsStore};
21use smol::{fs::File, process::Command};
22
23use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
24use std::{
25 env::consts::{ARCH, OS},
26 ffi::OsString,
27 sync::Arc,
28 time::Duration,
29};
30use update_notification::UpdateNotification;
31use util::{
32 http::{HttpClient, HttpClientWithUrl},
33 ResultExt,
34};
35use workspace::notifications::NotificationId;
36use workspace::Workspace;
37
38const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification";
39const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60);
40
41actions!(
42 auto_update,
43 [
44 Check,
45 DismissErrorMessage,
46 ViewReleaseNotes,
47 ViewReleaseNotesLocally
48 ]
49);
50
51#[derive(Serialize)]
52struct UpdateRequestBody {
53 installation_id: Option<Arc<str>>,
54 release_channel: Option<&'static str>,
55 telemetry: bool,
56}
57
58#[derive(Clone, Copy, PartialEq, Eq)]
59pub enum AutoUpdateStatus {
60 Idle,
61 Checking,
62 Downloading,
63 Installing,
64 Updated,
65 Errored,
66}
67
68pub struct AutoUpdater {
69 status: AutoUpdateStatus,
70 current_version: SemanticVersion,
71 http_client: Arc<HttpClientWithUrl>,
72 pending_poll: Option<Task<Option<()>>>,
73}
74
75#[derive(Deserialize)]
76struct JsonRelease {
77 version: String,
78 url: String,
79}
80
81struct AutoUpdateSetting(bool);
82
83/// Whether or not to automatically check for updates.
84///
85/// Default: true
86#[derive(Clone, Copy, Default, JsonSchema, Deserialize, Serialize)]
87#[serde(transparent)]
88struct AutoUpdateSettingContent(bool);
89
90impl Settings for AutoUpdateSetting {
91 const KEY: Option<&'static str> = Some("auto_update");
92
93 type FileContent = Option<AutoUpdateSettingContent>;
94
95 fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
96 let auto_update = [sources.release_channel, sources.user]
97 .into_iter()
98 .find_map(|value| value.copied().flatten())
99 .unwrap_or(sources.default.ok_or_else(Self::missing_default)?);
100
101 Ok(Self(auto_update.0))
102 }
103}
104
105#[derive(Default)]
106struct GlobalAutoUpdate(Option<Model<AutoUpdater>>);
107
108impl Global for GlobalAutoUpdate {}
109
110#[derive(Deserialize)]
111struct ReleaseNotesBody {
112 title: String,
113 release_notes: String,
114}
115
116pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut AppContext) {
117 AutoUpdateSetting::register(cx);
118
119 cx.observe_new_views(|workspace: &mut Workspace, _cx| {
120 workspace.register_action(|_, action: &Check, cx| check(action, cx));
121
122 workspace.register_action(|_, action, cx| {
123 view_release_notes(action, cx);
124 });
125
126 workspace.register_action(|workspace, _: &ViewReleaseNotesLocally, cx| {
127 view_release_notes_locally(workspace, cx);
128 });
129 })
130 .detach();
131
132 let version = release_channel::AppVersion::global(cx);
133 let auto_updater = cx.new_model(|cx| {
134 let updater = AutoUpdater::new(version, http_client);
135
136 let mut update_subscription = AutoUpdateSetting::get_global(cx)
137 .0
138 .then(|| updater.start_polling(cx));
139
140 cx.observe_global::<SettingsStore>(move |updater, cx| {
141 if AutoUpdateSetting::get_global(cx).0 {
142 if update_subscription.is_none() {
143 update_subscription = Some(updater.start_polling(cx))
144 }
145 } else {
146 update_subscription.take();
147 }
148 })
149 .detach();
150
151 updater
152 });
153 cx.set_global(GlobalAutoUpdate(Some(auto_updater)));
154}
155
156pub fn check(_: &Check, cx: &mut WindowContext) {
157 if let Some(updater) = AutoUpdater::get(cx) {
158 updater.update(cx, |updater, cx| updater.poll(cx));
159 } else {
160 drop(cx.prompt(
161 gpui::PromptLevel::Info,
162 "Could not check for updates",
163 Some("Auto-updates disabled for non-bundled app."),
164 &["Ok"],
165 ));
166 }
167}
168
169pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) -> Option<()> {
170 let auto_updater = AutoUpdater::get(cx)?;
171 let release_channel = ReleaseChannel::try_global(cx)?;
172
173 if matches!(
174 release_channel,
175 ReleaseChannel::Stable | ReleaseChannel::Preview
176 ) {
177 let auto_updater = auto_updater.read(cx);
178 let release_channel = release_channel.dev_name();
179 let current_version = auto_updater.current_version;
180 let url = &auto_updater
181 .http_client
182 .build_url(&format!("/releases/{release_channel}/{current_version}"));
183 cx.open_url(&url);
184 }
185
186 None
187}
188
189fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
190 let release_channel = ReleaseChannel::global(cx);
191 let version = AppVersion::global(cx).to_string();
192
193 let client = client::Client::global(cx).http_client();
194 let url = client.build_url(&format!(
195 "/api/release_notes/{}/{}",
196 release_channel.dev_name(),
197 version
198 ));
199
200 let markdown = workspace
201 .app_state()
202 .languages
203 .language_for_name("Markdown");
204
205 workspace
206 .with_local_workspace(cx, move |_, cx| {
207 cx.spawn(|workspace, mut cx| async move {
208 let markdown = markdown.await.log_err();
209 let response = client.get(&url, Default::default(), true).await;
210 let Some(mut response) = response.log_err() else {
211 return;
212 };
213
214 let mut body = Vec::new();
215 response.body_mut().read_to_end(&mut body).await.ok();
216
217 let body: serde_json::Result<ReleaseNotesBody> =
218 serde_json::from_slice(body.as_slice());
219
220 if let Ok(body) = body {
221 workspace
222 .update(&mut cx, |workspace, cx| {
223 let project = workspace.project().clone();
224 let buffer = project
225 .update(cx, |project, cx| project.create_buffer("", markdown, cx))
226 .expect("creating buffers on a local workspace always succeeds");
227 buffer.update(cx, |buffer, cx| {
228 buffer.edit([(0..0, body.release_notes)], None, cx)
229 });
230 let language_registry = project.read(cx).languages().clone();
231
232 let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
233
234 let tab_description = SharedString::from(body.title.to_string());
235 let editor = cx
236 .new_view(|cx| Editor::for_multibuffer(buffer, Some(project), cx));
237 let workspace_handle = workspace.weak_handle();
238 let view: View<MarkdownPreviewView> = MarkdownPreviewView::new(
239 MarkdownPreviewMode::Default,
240 editor,
241 workspace_handle,
242 language_registry,
243 Some(tab_description),
244 cx,
245 );
246 workspace.add_item_to_active_pane(Box::new(view.clone()), cx);
247 cx.notify();
248 })
249 .log_err();
250 }
251 })
252 .detach();
253 })
254 .detach();
255}
256
257pub fn notify_of_any_new_update(cx: &mut ViewContext<Workspace>) -> Option<()> {
258 let updater = AutoUpdater::get(cx)?;
259 let version = updater.read(cx).current_version;
260 let should_show_notification = updater.read(cx).should_show_update_notification(cx);
261
262 cx.spawn(|workspace, mut cx| async move {
263 let should_show_notification = should_show_notification.await?;
264 if should_show_notification {
265 workspace.update(&mut cx, |workspace, cx| {
266 workspace.show_notification(
267 NotificationId::unique::<UpdateNotification>(),
268 cx,
269 |cx| cx.new_view(|_| UpdateNotification::new(version)),
270 );
271 updater
272 .read(cx)
273 .set_should_show_update_notification(false, cx)
274 .detach_and_log_err(cx);
275 })?;
276 }
277 anyhow::Ok(())
278 })
279 .detach();
280
281 None
282}
283
284impl AutoUpdater {
285 pub fn get(cx: &mut AppContext) -> Option<Model<Self>> {
286 cx.default_global::<GlobalAutoUpdate>().0.clone()
287 }
288
289 fn new(current_version: SemanticVersion, http_client: Arc<HttpClientWithUrl>) -> Self {
290 Self {
291 status: AutoUpdateStatus::Idle,
292 current_version,
293 http_client,
294 pending_poll: None,
295 }
296 }
297
298 pub fn start_polling(&self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
299 cx.spawn(|this, mut cx| async move {
300 loop {
301 this.update(&mut cx, |this, cx| this.poll(cx))?;
302 cx.background_executor().timer(POLL_INTERVAL).await;
303 }
304 })
305 }
306
307 pub fn poll(&mut self, cx: &mut ModelContext<Self>) {
308 if self.pending_poll.is_some() || self.status == AutoUpdateStatus::Updated {
309 return;
310 }
311
312 self.status = AutoUpdateStatus::Checking;
313 cx.notify();
314
315 self.pending_poll = Some(cx.spawn(|this, mut cx| async move {
316 let result = Self::update(this.upgrade()?, cx.clone()).await;
317 this.update(&mut cx, |this, cx| {
318 this.pending_poll = None;
319 if let Err(error) = result {
320 log::error!("auto-update failed: error:{:?}", error);
321 this.status = AutoUpdateStatus::Errored;
322 cx.notify();
323 }
324 })
325 .ok()
326 }));
327 }
328
329 pub fn status(&self) -> AutoUpdateStatus {
330 self.status
331 }
332
333 pub fn dismiss_error(&mut self, cx: &mut ModelContext<Self>) {
334 self.status = AutoUpdateStatus::Idle;
335 cx.notify();
336 }
337
338 async fn update(this: Model<Self>, mut cx: AsyncAppContext) -> Result<()> {
339 let (client, current_version) = this.read_with(&cx, |this, _| {
340 (this.http_client.clone(), this.current_version)
341 })?;
342
343 let mut url_string = client.build_url(&format!(
344 "/api/releases/latest?asset=Zed.dmg&os={}&arch={}",
345 OS, ARCH
346 ));
347 cx.update(|cx| {
348 if let Some(param) = ReleaseChannel::try_global(cx)
349 .and_then(|release_channel| release_channel.release_query_param())
350 {
351 url_string += "&";
352 url_string += param;
353 }
354 })?;
355
356 let mut response = client.get(&url_string, Default::default(), true).await?;
357
358 let mut body = Vec::new();
359 response
360 .body_mut()
361 .read_to_end(&mut body)
362 .await
363 .context("error reading release")?;
364 let release: JsonRelease =
365 serde_json::from_slice(body.as_slice()).context("error deserializing release")?;
366
367 let should_download = match *RELEASE_CHANNEL {
368 ReleaseChannel::Nightly => cx
369 .update(|cx| AppCommitSha::try_global(cx).map(|sha| release.version != sha.0))
370 .ok()
371 .flatten()
372 .unwrap_or(true),
373 _ => release.version.parse::<SemanticVersion>()? > current_version,
374 };
375
376 if !should_download {
377 this.update(&mut cx, |this, cx| {
378 this.status = AutoUpdateStatus::Idle;
379 cx.notify();
380 })?;
381 return Ok(());
382 }
383
384 this.update(&mut cx, |this, cx| {
385 this.status = AutoUpdateStatus::Downloading;
386 cx.notify();
387 })?;
388
389 let temp_dir = tempfile::Builder::new()
390 .prefix("zed-auto-update")
391 .tempdir()?;
392 let dmg_path = temp_dir.path().join("Zed.dmg");
393 let mount_path = temp_dir.path().join("Zed");
394 let running_app_path = ZED_APP_PATH
395 .clone()
396 .map_or_else(|| cx.update(|cx| cx.app_path())?, Ok)?;
397 let running_app_filename = running_app_path
398 .file_name()
399 .ok_or_else(|| anyhow!("invalid running app path"))?;
400 let mut mounted_app_path: OsString = mount_path.join(running_app_filename).into();
401 mounted_app_path.push("/");
402
403 let mut dmg_file = File::create(&dmg_path).await?;
404
405 let (installation_id, release_channel, telemetry) = cx.update(|cx| {
406 let installation_id = Client::global(cx).telemetry().installation_id();
407 let release_channel = ReleaseChannel::try_global(cx)
408 .map(|release_channel| release_channel.display_name());
409 let telemetry = TelemetrySettings::get_global(cx).metrics;
410
411 (installation_id, release_channel, telemetry)
412 })?;
413
414 let request_body = AsyncBody::from(serde_json::to_string(&UpdateRequestBody {
415 installation_id,
416 release_channel,
417 telemetry,
418 })?);
419
420 let mut response = client.get(&release.url, request_body, true).await?;
421 smol::io::copy(response.body_mut(), &mut dmg_file).await?;
422 log::info!("downloaded update. path:{:?}", dmg_path);
423
424 this.update(&mut cx, |this, cx| {
425 this.status = AutoUpdateStatus::Installing;
426 cx.notify();
427 })?;
428
429 let output = Command::new("hdiutil")
430 .args(&["attach", "-nobrowse"])
431 .arg(&dmg_path)
432 .arg("-mountroot")
433 .arg(&temp_dir.path())
434 .output()
435 .await?;
436 if !output.status.success() {
437 Err(anyhow!(
438 "failed to mount: {:?}",
439 String::from_utf8_lossy(&output.stderr)
440 ))?;
441 }
442
443 let output = Command::new("rsync")
444 .args(&["-av", "--delete"])
445 .arg(&mounted_app_path)
446 .arg(&running_app_path)
447 .output()
448 .await?;
449 if !output.status.success() {
450 Err(anyhow!(
451 "failed to copy app: {:?}",
452 String::from_utf8_lossy(&output.stderr)
453 ))?;
454 }
455
456 let output = Command::new("hdiutil")
457 .args(&["detach"])
458 .arg(&mount_path)
459 .output()
460 .await?;
461 if !output.status.success() {
462 Err(anyhow!(
463 "failed to unmount: {:?}",
464 String::from_utf8_lossy(&output.stderr)
465 ))?;
466 }
467
468 this.update(&mut cx, |this, cx| {
469 this.set_should_show_update_notification(true, cx)
470 .detach_and_log_err(cx);
471 this.status = AutoUpdateStatus::Updated;
472 cx.notify();
473 })?;
474 Ok(())
475 }
476
477 fn set_should_show_update_notification(
478 &self,
479 should_show: bool,
480 cx: &AppContext,
481 ) -> Task<Result<()>> {
482 cx.background_executor().spawn(async move {
483 if should_show {
484 KEY_VALUE_STORE
485 .write_kvp(
486 SHOULD_SHOW_UPDATE_NOTIFICATION_KEY.to_string(),
487 "".to_string(),
488 )
489 .await?;
490 } else {
491 KEY_VALUE_STORE
492 .delete_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY.to_string())
493 .await?;
494 }
495 Ok(())
496 })
497 }
498
499 fn should_show_update_notification(&self, cx: &AppContext) -> Task<Result<bool>> {
500 cx.background_executor().spawn(async move {
501 Ok(KEY_VALUE_STORE
502 .read_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)?
503 .is_some())
504 })
505 }
506}