1mod update_notification;
2
3use anyhow::{anyhow, Context, Result};
4use client::{http::HttpClient, ZED_SECRET_CLIENT_TOKEN};
5use db::Db;
6use gpui::{
7 actions, platform::AppVersion, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
8 MutableAppContext, Task, WeakViewHandle,
9};
10use lazy_static::lazy_static;
11use serde::Deserialize;
12use settings::ReleaseChannel;
13use smol::{fs::File, io::AsyncReadExt, process::Command};
14use std::{env, ffi::OsString, path::PathBuf, sync::Arc, time::Duration};
15use update_notification::UpdateNotification;
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
21lazy_static! {
22 pub static ref ZED_APP_VERSION: Option<AppVersion> = env::var("ZED_APP_VERSION")
23 .ok()
24 .and_then(|v| v.parse().ok());
25 pub static ref ZED_APP_PATH: Option<PathBuf> = env::var("ZED_APP_PATH").ok().map(PathBuf::from);
26}
27
28actions!(auto_update, [Check, DismissErrorMessage, ViewReleaseNotes]);
29
30#[derive(Clone, Copy, PartialEq, Eq)]
31pub enum AutoUpdateStatus {
32 Idle,
33 Checking,
34 Downloading,
35 Installing,
36 Updated,
37 Errored,
38}
39
40pub struct AutoUpdater {
41 status: AutoUpdateStatus,
42 current_version: AppVersion,
43 http_client: Arc<dyn HttpClient>,
44 pending_poll: Option<Task<()>>,
45 db: project::Db,
46 server_url: String,
47}
48
49#[derive(Deserialize)]
50struct JsonRelease {
51 version: String,
52 url: String,
53}
54
55impl Entity for AutoUpdater {
56 type Event = ();
57}
58
59pub fn init(
60 db: Db,
61 http_client: Arc<dyn HttpClient>,
62 server_url: String,
63 cx: &mut MutableAppContext,
64) {
65 if let Some(version) = (*ZED_APP_VERSION).or_else(|| cx.platform().app_version().ok()) {
66 let server_url = server_url;
67 let auto_updater = cx.add_model(|cx| {
68 let updater = AutoUpdater::new(version, db, http_client, server_url.clone());
69 updater.start_polling(cx).detach();
70 updater
71 });
72 cx.set_global(Some(auto_updater));
73 cx.add_global_action(|_: &Check, cx| {
74 if let Some(updater) = AutoUpdater::get(cx) {
75 updater.update(cx, |updater, cx| updater.poll(cx));
76 }
77 });
78 cx.add_global_action(move |_: &ViewReleaseNotes, cx| {
79 let latest_release_url = if cx.has_global::<ReleaseChannel>()
80 && *cx.global::<ReleaseChannel>() == ReleaseChannel::Preview
81 {
82 format!("{server_url}/releases/preview/latest")
83 } else {
84 format!("{server_url}/releases/latest")
85 };
86 cx.platform().open_url(&latest_release_url);
87 });
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 pub 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: 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 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 let db = self.db.clone();
307 cx.background().spawn(async move {
308 if should_show {
309 db.write_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY, "")?;
310 } else {
311 db.delete_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)?;
312 }
313 Ok(())
314 })
315 }
316
317 fn should_show_update_notification(&self, cx: &AppContext) -> Task<Result<bool>> {
318 let db = self.db.clone();
319 cx.background()
320 .spawn(async move { Ok(db.read_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)?.is_some()) })
321 }
322}