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