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