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