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