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