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