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