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