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