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