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