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