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(|wokrspace: &mut Workspace, _cx| {
88 wokrspace
89 .register_action(|_, action: &Check, cx| check(action, cx))
90 .register_action(|_, _action: &CheckThatAutoUpdaterWorks, cx| {
91 let prompt = cx.prompt(gpui::PromptLevel::Info, "It does!", &["Ok"]);
92 cx.spawn(|_, _cx| async move {
93 prompt.await.ok();
94 })
95 .detach();
96 });
97 })
98 .detach();
99
100 if let Some(version) = *ZED_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 AppContext) {
129 if let Some(updater) = AutoUpdater::get(cx) {
130 updater.update(cx, |updater, cx| updater.poll(cx));
131 }
132}
133
134fn _view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) {
135 if let Some(auto_updater) = AutoUpdater::get(cx) {
136 let auto_updater = auto_updater.read(cx);
137 let server_url = &auto_updater.server_url;
138 let current_version = auto_updater.current_version;
139 if cx.has_global::<ReleaseChannel>() {
140 match cx.global::<ReleaseChannel>() {
141 ReleaseChannel::Dev => {}
142 ReleaseChannel::Nightly => {}
143 ReleaseChannel::Preview => {
144 cx.open_url(&format!("{server_url}/releases/preview/{current_version}"))
145 }
146 ReleaseChannel::Stable => {
147 cx.open_url(&format!("{server_url}/releases/stable/{current_version}"))
148 }
149 }
150 }
151 }
152}
153
154pub fn notify_of_any_new_update(cx: &mut ViewContext<Workspace>) -> Option<()> {
155 let updater = AutoUpdater::get(cx)?;
156 let version = updater.read(cx).current_version;
157 let should_show_notification = updater.read(cx).should_show_update_notification(cx);
158
159 cx.spawn(|workspace, mut cx| async move {
160 let should_show_notification = should_show_notification.await?;
161 if should_show_notification {
162 workspace.update(&mut cx, |workspace, cx| {
163 workspace.show_notification(0, cx, |cx| {
164 cx.build_view(|_| UpdateNotification::new(version))
165 });
166 updater
167 .read(cx)
168 .set_should_show_update_notification(false, cx)
169 .detach_and_log_err(cx);
170 })?;
171 }
172 anyhow::Ok(())
173 })
174 .detach();
175
176 None
177}
178
179impl AutoUpdater {
180 pub fn get(cx: &mut AppContext) -> Option<Model<Self>> {
181 cx.default_global::<Option<Model<Self>>>().clone()
182 }
183
184 fn new(
185 current_version: SemanticVersion,
186 http_client: Arc<dyn HttpClient>,
187 server_url: String,
188 ) -> Self {
189 Self {
190 status: AutoUpdateStatus::Idle,
191 current_version,
192 http_client,
193 server_url,
194 pending_poll: None,
195 }
196 }
197
198 pub fn start_polling(&self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
199 cx.spawn(|this, mut cx| async move {
200 loop {
201 this.update(&mut cx, |this, cx| this.poll(cx))?;
202 cx.background_executor().timer(POLL_INTERVAL).await;
203 }
204 })
205 }
206
207 pub fn poll(&mut self, cx: &mut ModelContext<Self>) {
208 if self.pending_poll.is_some() || self.status == AutoUpdateStatus::Updated {
209 return;
210 }
211
212 self.status = AutoUpdateStatus::Checking;
213 cx.notify();
214
215 self.pending_poll = Some(cx.spawn(|this, mut cx| async move {
216 let result = Self::update(this.upgrade()?, cx.clone()).await;
217 this.update(&mut cx, |this, cx| {
218 this.pending_poll = None;
219 if let Err(error) = result {
220 log::error!("auto-update failed: error:{:?}", error);
221 this.status = AutoUpdateStatus::Errored;
222 cx.notify();
223 }
224 })
225 .ok()
226 }));
227 }
228
229 pub fn status(&self) -> AutoUpdateStatus {
230 self.status
231 }
232
233 pub fn dismiss_error(&mut self, cx: &mut ModelContext<Self>) {
234 self.status = AutoUpdateStatus::Idle;
235 cx.notify();
236 }
237
238 async fn update(this: Model<Self>, mut cx: AsyncAppContext) -> Result<()> {
239 let (client, server_url, current_version) = this.read_with(&cx, |this, _| {
240 (
241 this.http_client.clone(),
242 this.server_url.clone(),
243 this.current_version,
244 )
245 })?;
246
247 let mut url_string = format!(
248 "{server_url}/api/releases/latest?token={ZED_SECRET_CLIENT_TOKEN}&asset=Zed.dmg"
249 );
250 cx.update(|cx| {
251 if cx.has_global::<ReleaseChannel>() {
252 if let Some(param) = cx.global::<ReleaseChannel>().release_query_param() {
253 url_string += "&";
254 url_string += param;
255 }
256 }
257 })?;
258
259 let mut response = client.get(&url_string, Default::default(), true).await?;
260
261 let mut body = Vec::new();
262 response
263 .body_mut()
264 .read_to_end(&mut body)
265 .await
266 .context("error reading release")?;
267 let release: JsonRelease =
268 serde_json::from_slice(body.as_slice()).context("error deserializing release")?;
269
270 let should_download = match *RELEASE_CHANNEL {
271 ReleaseChannel::Nightly => cx
272 .try_read_global::<AppCommitSha, _>(|sha, _| release.version != sha.0)
273 .unwrap_or(true),
274 _ => release.version.parse::<SemanticVersion>()? <= current_version,
275 };
276
277 if !should_download {
278 this.update(&mut cx, |this, cx| {
279 this.status = AutoUpdateStatus::Idle;
280 cx.notify();
281 })?;
282 return Ok(());
283 }
284
285 this.update(&mut cx, |this, cx| {
286 this.status = AutoUpdateStatus::Downloading;
287 cx.notify();
288 })?;
289
290 let temp_dir = tempdir::TempDir::new("zed-auto-update")?;
291 let dmg_path = temp_dir.path().join("Zed.dmg");
292 let mount_path = temp_dir.path().join("Zed");
293 let running_app_path = ZED_APP_PATH
294 .clone()
295 .map_or_else(|| cx.update(|cx| cx.app_path())?, Ok)?;
296 let running_app_filename = running_app_path
297 .file_name()
298 .ok_or_else(|| anyhow!("invalid running app path"))?;
299 let mut mounted_app_path: OsString = mount_path.join(running_app_filename).into();
300 mounted_app_path.push("/");
301
302 let mut dmg_file = File::create(&dmg_path).await?;
303
304 let (installation_id, release_channel, telemetry) = cx.update(|cx| {
305 let installation_id = cx.global::<Arc<Client>>().telemetry().installation_id();
306 let release_channel = cx
307 .has_global::<ReleaseChannel>()
308 .then(|| cx.global::<ReleaseChannel>().display_name());
309 let telemetry = TelemetrySettings::get_global(cx).metrics;
310
311 (installation_id, release_channel, telemetry)
312 })?;
313
314 let request_body = AsyncBody::from(serde_json::to_string(&UpdateRequestBody {
315 installation_id,
316 release_channel,
317 telemetry,
318 })?);
319
320 let mut response = client.get(&release.url, request_body, true).await?;
321 smol::io::copy(response.body_mut(), &mut dmg_file).await?;
322 log::info!("downloaded update. path:{:?}", dmg_path);
323
324 this.update(&mut cx, |this, cx| {
325 this.status = AutoUpdateStatus::Installing;
326 cx.notify();
327 })?;
328
329 let output = Command::new("hdiutil")
330 .args(&["attach", "-nobrowse"])
331 .arg(&dmg_path)
332 .arg("-mountroot")
333 .arg(&temp_dir.path())
334 .output()
335 .await?;
336 if !output.status.success() {
337 Err(anyhow!(
338 "failed to mount: {:?}",
339 String::from_utf8_lossy(&output.stderr)
340 ))?;
341 }
342
343 let output = Command::new("rsync")
344 .args(&["-av", "--delete"])
345 .arg(&mounted_app_path)
346 .arg(&running_app_path)
347 .output()
348 .await?;
349 if !output.status.success() {
350 Err(anyhow!(
351 "failed to copy app: {:?}",
352 String::from_utf8_lossy(&output.stderr)
353 ))?;
354 }
355
356 let output = Command::new("hdiutil")
357 .args(&["detach"])
358 .arg(&mount_path)
359 .output()
360 .await?;
361 if !output.status.success() {
362 Err(anyhow!(
363 "failed to unmount: {:?}",
364 String::from_utf8_lossy(&output.stderr)
365 ))?;
366 }
367
368 this.update(&mut cx, |this, cx| {
369 this.set_should_show_update_notification(true, cx)
370 .detach_and_log_err(cx);
371 this.status = AutoUpdateStatus::Updated;
372 cx.notify();
373 })?;
374 Ok(())
375 }
376
377 fn set_should_show_update_notification(
378 &self,
379 should_show: bool,
380 cx: &AppContext,
381 ) -> Task<Result<()>> {
382 cx.background_executor().spawn(async move {
383 if should_show {
384 KEY_VALUE_STORE
385 .write_kvp(
386 SHOULD_SHOW_UPDATE_NOTIFICATION_KEY.to_string(),
387 "".to_string(),
388 )
389 .await?;
390 } else {
391 KEY_VALUE_STORE
392 .delete_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY.to_string())
393 .await?;
394 }
395 Ok(())
396 })
397 }
398
399 fn should_show_update_notification(&self, cx: &AppContext) -> Task<Result<bool>> {
400 cx.background_executor().spawn(async move {
401 Ok(KEY_VALUE_STORE
402 .read_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)?
403 .is_some())
404 })
405 }
406}