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 cx.platform().open_url(&format!("{server_url}/releases"));
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 db: project::Db,
117 http_client: Arc<dyn HttpClient>,
118 server_url: String,
119 ) -> Self {
120 Self {
121 status: AutoUpdateStatus::Idle,
122 current_version,
123 db,
124 http_client,
125 server_url,
126 pending_poll: None,
127 }
128 }
129
130 pub fn start_polling(&self, cx: &mut ModelContext<Self>) -> Task<()> {
131 cx.spawn(|this, mut cx| async move {
132 loop {
133 this.update(&mut cx, |this, cx| this.poll(cx));
134 cx.background().timer(POLL_INTERVAL).await;
135 }
136 })
137 }
138
139 pub fn poll(&mut self, cx: &mut ModelContext<Self>) {
140 if self.pending_poll.is_some() || self.status == AutoUpdateStatus::Updated {
141 return;
142 }
143
144 self.status = AutoUpdateStatus::Checking;
145 cx.notify();
146
147 self.pending_poll = Some(cx.spawn(|this, mut cx| async move {
148 let result = Self::update(this.clone(), cx.clone()).await;
149 this.update(&mut cx, |this, cx| {
150 this.pending_poll = None;
151 if let Err(error) = result {
152 log::error!("auto-update failed: error:{:?}", error);
153 this.status = AutoUpdateStatus::Errored;
154 cx.notify();
155 }
156 });
157 }));
158 }
159
160 pub fn status(&self) -> AutoUpdateStatus {
161 self.status
162 }
163
164 pub fn dismiss_error(&mut self, cx: &mut ModelContext<Self>) {
165 self.status = AutoUpdateStatus::Idle;
166 cx.notify();
167 }
168
169 async fn update(this: ModelHandle<Self>, mut cx: AsyncAppContext) -> Result<()> {
170 let (client, server_url, current_version) = this.read_with(&cx, |this, _| {
171 (
172 this.http_client.clone(),
173 this.server_url.clone(),
174 this.current_version,
175 )
176 });
177
178 let preview_param = cx.read(|cx| {
179 if cx.has_global::<ReleaseChannel>() {
180 if *cx.global::<ReleaseChannel>() == ReleaseChannel::Preview {
181 return "&preview=1";
182 }
183 }
184 ""
185 });
186
187 let mut response = client
188 .get(
189 &format!("{server_url}/api/releases/latest?token={ZED_SECRET_CLIENT_TOKEN}&asset=Zed.dmg{preview_param}"),
190 Default::default(),
191 true,
192 )
193 .await?;
194
195 let mut body = Vec::new();
196 response
197 .body_mut()
198 .read_to_end(&mut body)
199 .await
200 .context("error reading release")?;
201 let release: JsonRelease =
202 serde_json::from_slice(body.as_slice()).context("error deserializing release")?;
203
204 let latest_version = release.version.parse::<AppVersion>()?;
205 if latest_version <= current_version {
206 this.update(&mut cx, |this, cx| {
207 this.status = AutoUpdateStatus::Idle;
208 cx.notify();
209 });
210 return Ok(());
211 }
212
213 this.update(&mut cx, |this, cx| {
214 this.status = AutoUpdateStatus::Downloading;
215 cx.notify();
216 });
217
218 let temp_dir = tempdir::TempDir::new("zed-auto-update")?;
219 let dmg_path = temp_dir.path().join("Zed.dmg");
220 let mount_path = temp_dir.path().join("Zed");
221 let running_app_path = ZED_APP_PATH
222 .clone()
223 .map_or_else(|| cx.platform().app_path(), Ok)?;
224 let running_app_filename = running_app_path
225 .file_name()
226 .ok_or_else(|| anyhow!("invalid running app path"))?;
227 let mut mounted_app_path: OsString = mount_path.join(running_app_filename).into();
228 mounted_app_path.push("/");
229
230 let mut dmg_file = File::create(&dmg_path).await?;
231 let mut response = client.get(&release.url, Default::default(), true).await?;
232 smol::io::copy(response.body_mut(), &mut dmg_file).await?;
233 log::info!("downloaded update. path:{:?}", dmg_path);
234
235 this.update(&mut cx, |this, cx| {
236 this.status = AutoUpdateStatus::Installing;
237 cx.notify();
238 });
239
240 let output = Command::new("hdiutil")
241 .args(&["attach", "-nobrowse"])
242 .arg(&dmg_path)
243 .arg("-mountroot")
244 .arg(&temp_dir.path())
245 .output()
246 .await?;
247 if !output.status.success() {
248 Err(anyhow!(
249 "failed to mount: {:?}",
250 String::from_utf8_lossy(&output.stderr)
251 ))?;
252 }
253
254 let output = Command::new("rsync")
255 .args(&["-av", "--delete"])
256 .arg(&mounted_app_path)
257 .arg(&running_app_path)
258 .output()
259 .await?;
260 if !output.status.success() {
261 Err(anyhow!(
262 "failed to copy app: {:?}",
263 String::from_utf8_lossy(&output.stderr)
264 ))?;
265 }
266
267 let output = Command::new("hdiutil")
268 .args(&["detach"])
269 .arg(&mount_path)
270 .output()
271 .await?;
272 if !output.status.success() {
273 Err(anyhow!(
274 "failed to unmount: {:?}",
275 String::from_utf8_lossy(&output.stderr)
276 ))?;
277 }
278
279 this.update(&mut cx, |this, cx| {
280 this.set_should_show_update_notification(true, cx)
281 .detach_and_log_err(cx);
282 this.status = AutoUpdateStatus::Updated;
283 cx.notify();
284 });
285 Ok(())
286 }
287
288 fn set_should_show_update_notification(
289 &self,
290 should_show: bool,
291 cx: &AppContext,
292 ) -> Task<Result<()>> {
293 let db = self.db.clone();
294 cx.background().spawn(async move {
295 if should_show {
296 db.write_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY, "")?;
297 } else {
298 db.delete_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)?;
299 }
300 Ok(())
301 })
302 }
303
304 fn should_show_update_notification(&self, cx: &AppContext) -> Task<Result<bool>> {
305 let db = self.db.clone();
306 cx.background()
307 .spawn(async move { Ok(db.read_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)?.is_some()) })
308 }
309}