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