diff --git a/Cargo.lock b/Cargo.lock index c4a7112abd..3bd89c4d79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -544,6 +544,24 @@ dependencies = [ "winapi", ] +[[package]] +name = "auto_update" +version = "0.1.0" +dependencies = [ + "anyhow", + "client", + "gpui", + "lazy_static", + "log", + "serde", + "serde_json", + "smol", + "surf", + "tempdir", + "theme", + "workspace", +] + [[package]] name = "autocfg" version = "0.1.7" @@ -5987,6 +6005,7 @@ dependencies = [ "async-compression", "async-recursion", "async-trait", + "auto_update", "breadcrumbs", "chat_panel", "client", diff --git a/crates/auto_update/Cargo.toml b/crates/auto_update/Cargo.toml new file mode 100644 index 0000000000..39f422ea6f --- /dev/null +++ b/crates/auto_update/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "auto_update" +version = "0.1.0" +edition = "2021" + +[lib] +path = "src/auto_update.rs" +doctest = false + +[dependencies] +gpui = { path = "../gpui" } +theme = { path = "../theme" } +client = { path = "../client" } +workspace = { path = "../workspace" } +anyhow = "1.0.38" +lazy_static = "1.4" +log = "0.4" +serde = { version = "1", features = ["derive"] } +serde_json = { version = "1.0.64", features = ["preserve_order"] } +smol = "1.2.5" +surf = "2.2" +tempdir = "0.3.7" diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs new file mode 100644 index 0000000000..56f94f526f --- /dev/null +++ b/crates/auto_update/src/auto_update.rs @@ -0,0 +1,288 @@ +use anyhow::{anyhow, Result}; +use client::http::{self, HttpClient}; +use gpui::{ + action, + elements::{Empty, MouseEventHandler, Text}, + platform::AppVersion, + AsyncAppContext, Element, Entity, ModelContext, ModelHandle, MutableAppContext, Task, View, + ViewContext, +}; +use lazy_static::lazy_static; +use serde::Deserialize; +use smol::{fs::File, io::AsyncReadExt, process::Command}; +use std::{ffi::OsString, path::PathBuf, sync::Arc, time::Duration}; +use surf::Request; +use workspace::{ItemHandle, Settings, StatusItemView}; + +const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60); +const ACCESS_TOKEN: &'static str = "618033988749894"; + +lazy_static! { + pub static ref ZED_APP_VERSION: Option = std::env::var("ZED_APP_VERSION") + .ok() + .and_then(|v| v.parse().ok()); + pub static ref ZED_APP_PATH: Option = + std::env::var("ZED_APP_PATH").ok().map(PathBuf::from); +} + +#[derive(Clone, PartialEq, Eq)] +pub enum AutoUpdateStatus { + Idle, + Checking, + Downloading, + Updated, + Errored, +} + +pub struct AutoUpdater { + status: AutoUpdateStatus, + current_version: AppVersion, + http_client: Arc, + pending_poll: Option>, + server_url: String, +} + +pub struct AutoUpdateIndicator { + updater: Option>, +} + +action!(DismissErrorMessage); + +#[derive(Deserialize)] +struct JsonRelease { + version: String, + url: http::Url, +} + +impl Entity for AutoUpdater { + type Event = (); +} + +pub fn init(http_client: Arc, server_url: String, cx: &mut MutableAppContext) { + if let Some(version) = ZED_APP_VERSION.clone().or(cx.platform().app_version().ok()) { + let auto_updater = cx.add_model(|cx| { + let updater = AutoUpdater::new(version, http_client, server_url); + updater.start_polling(cx).detach(); + updater + }); + cx.set_global(Some(auto_updater)); + cx.add_action(AutoUpdateIndicator::dismiss_error_message); + } +} + +impl AutoUpdater { + fn get(cx: &mut MutableAppContext) -> Option> { + cx.default_global::>>().clone() + } + + fn new( + current_version: AppVersion, + http_client: Arc, + server_url: String, + ) -> Self { + Self { + status: AutoUpdateStatus::Idle, + current_version, + http_client, + server_url, + pending_poll: None, + } + } + + pub fn start_polling(&self, cx: &mut ModelContext) -> Task<()> { + cx.spawn(|this, mut cx| async move { + loop { + this.update(&mut cx, |this, cx| this.poll(cx)); + cx.background().timer(POLL_INTERVAL).await; + } + }) + } + + pub fn poll(&mut self, cx: &mut ModelContext) { + if self.pending_poll.is_some() { + return; + } + + self.status = AutoUpdateStatus::Checking; + cx.notify(); + + self.pending_poll = Some(cx.spawn(|this, mut cx| async move { + let result = Self::update(this.clone(), cx.clone()).await; + this.update(&mut cx, |this, cx| { + this.pending_poll = None; + if let Err(error) = result { + log::error!("auto-update failed: error:{:?}", error); + this.status = AutoUpdateStatus::Errored; + cx.notify(); + } + }); + })); + } + + async fn update(this: ModelHandle, mut cx: AsyncAppContext) -> Result<()> { + let (client, server_url, current_version) = this.read_with(&cx, |this, _| { + ( + this.http_client.clone(), + this.server_url.clone(), + this.current_version, + ) + }); + let mut response = client + .send(Request::new( + http::Method::Get, + http::Url::parse(&format!( + "{server_url}/api/releases/latest?token={ACCESS_TOKEN}&asset=Zed.dmg" + ))?, + )) + .await?; + let release = response + .body_json::() + .await + .map_err(|err| anyhow!("error deserializing release {:?}", err))?; + let latest_version = release.version.parse::()?; + if latest_version <= current_version { + this.update(&mut cx, |this, cx| { + this.status = AutoUpdateStatus::Idle; + cx.notify(); + }); + return Ok(()); + } + + this.update(&mut cx, |this, cx| { + this.status = AutoUpdateStatus::Downloading; + cx.notify(); + }); + + let temp_dir = tempdir::TempDir::new("zed-auto-update")?; + let dmg_path = temp_dir.path().join("Zed.dmg"); + let mount_path = temp_dir.path().join("Zed"); + let mut mounted_app_path: OsString = mount_path.join("Zed.app").into(); + mounted_app_path.push("/"); + let running_app_path = ZED_APP_PATH + .clone() + .map_or_else(|| cx.platform().path_for_resource(None, None), Ok)?; + + let mut dmg_file = File::create(&dmg_path).await?; + let response = client + .send(Request::new(http::Method::Get, release.url)) + .await?; + smol::io::copy(response.bytes(), &mut dmg_file).await?; + log::info!("downloaded update. path:{:?}", dmg_path); + + let output = Command::new("hdiutil") + .args(&["attach", "-nobrowse"]) + .arg(&dmg_path) + .arg("-mountroot") + .arg(&temp_dir.path()) + .output() + .await?; + if !output.status.success() { + Err(anyhow!( + "failed to mount: {:?}", + String::from_utf8_lossy(&output.stderr) + ))?; + } + + let output = Command::new("rsync") + .args(&["-av", "--delete"]) + .arg(&mounted_app_path) + .arg(&running_app_path) + .output() + .await?; + if !output.status.success() { + Err(anyhow!( + "failed to copy app: {:?}", + String::from_utf8_lossy(&output.stderr) + ))?; + } + + let output = Command::new("hdiutil") + .args(&["detach"]) + .arg(&mount_path) + .output() + .await?; + if !output.status.success() { + Err(anyhow!( + "failed to unmount: {:?}", + String::from_utf8_lossy(&output.stderr) + ))?; + } + + this.update(&mut cx, |this, cx| { + this.status = AutoUpdateStatus::Idle; + cx.notify(); + }); + Ok(()) + } +} + +impl Entity for AutoUpdateIndicator { + type Event = (); +} + +impl View for AutoUpdateIndicator { + fn ui_name() -> &'static str { + "AutoUpdateIndicator" + } + + fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox { + if let Some(updater) = &self.updater { + let theme = &cx.global::().theme.workspace.status_bar; + match &updater.read(cx).status { + AutoUpdateStatus::Checking => Text::new( + "Checking for updates…".to_string(), + theme.auto_update_progress_message.clone(), + ) + .boxed(), + AutoUpdateStatus::Downloading => Text::new( + "Downloading update…".to_string(), + theme.auto_update_progress_message.clone(), + ) + .boxed(), + AutoUpdateStatus::Updated => Text::new( + "Restart to update Zed".to_string(), + theme.auto_update_done_message.clone(), + ) + .boxed(), + AutoUpdateStatus::Errored => { + MouseEventHandler::new::(0, cx, |_, cx| { + let theme = &cx.global::().theme.workspace.status_bar; + Text::new( + "Auto update failed".to_string(), + theme.auto_update_done_message.clone(), + ) + .boxed() + }) + .on_click(|cx| cx.dispatch_action(DismissErrorMessage)) + .boxed() + } + AutoUpdateStatus::Idle => Empty::new().boxed(), + } + } else { + Empty::new().boxed() + } + } +} + +impl StatusItemView for AutoUpdateIndicator { + fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext) {} +} + +impl AutoUpdateIndicator { + pub fn new(cx: &mut ViewContext) -> Self { + let updater = AutoUpdater::get(cx); + if let Some(updater) = &updater { + cx.observe(updater, |_, _, cx| cx.notify()).detach(); + } + Self { updater } + } + + fn dismiss_error_message(&mut self, _: &DismissErrorMessage, cx: &mut ViewContext) { + if let Some(updater) = &self.updater { + updater.update(cx, |updater, cx| { + updater.status = AutoUpdateStatus::Idle; + cx.notify(); + }); + } + } +} diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 8fa15a9235..ff6eba88ef 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -153,6 +153,8 @@ pub struct StatusBar { pub cursor_position: TextStyle, pub diagnostic_message: TextStyle, pub lsp_message: TextStyle, + pub auto_update_progress_message: TextStyle, + pub auto_update_done_message: TextStyle, } #[derive(Deserialize, Default)] diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index c447f3a5fd..c79713a353 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -183,12 +183,9 @@ pub struct AppState { pub user_store: ModelHandle, pub fs: Arc, pub channel_list: ModelHandle, - pub build_window_options: &'static dyn Fn() -> WindowOptions<'static>, - pub build_workspace: &'static dyn Fn( - ModelHandle, - &Arc, - &mut ViewContext, - ) -> Workspace, + pub build_window_options: fn() -> WindowOptions<'static>, + pub build_workspace: + fn(ModelHandle, &Arc, &mut ViewContext) -> Workspace, } #[derive(Clone)] diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 937b9208ce..71f65bd417 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -15,6 +15,7 @@ name = "Zed" path = "src/main.rs" [dependencies] +auto_update = { path = "../auto_update" } breadcrumbs = { path = "../breadcrumbs" } chat_panel = { path = "../chat_panel" } collections = { path = "../collections" } diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index 7f235cbf48..3a84ff6db3 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -83,6 +83,8 @@ item_spacing = 8 cursor_position = "$text.2" diagnostic_message = "$text.2" lsp_message = "$text.2" +auto_update_progress_message = "$text.2" +auto_update_done_message = "$text.0" [workspace.toolbar] background = "$surface.1" diff --git a/crates/zed/src/auto_updater.rs b/crates/zed/src/auto_updater.rs deleted file mode 100644 index f04fbcfcfb..0000000000 --- a/crates/zed/src/auto_updater.rs +++ /dev/null @@ -1,117 +0,0 @@ -use anyhow::{anyhow, Result}; -use client::http::{self, HttpClient}; -use gpui::{platform::AppVersion, AsyncAppContext, Entity, ModelContext, ModelHandle, Task}; -use serde::Deserialize; -use smol::io::AsyncReadExt; -use std::{sync::Arc, time::Duration}; -use surf::Request; - -const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60); - -#[derive(Clone, PartialEq, Eq)] -pub enum AutoUpdateStatus { - Idle, - Checking, - Downloading, - Updated, - Errored { error: String }, -} - -pub struct AutoUpdater { - status: AutoUpdateStatus, - current_version: AppVersion, - http_client: Arc, - pending_poll: Option>, - server_url: String, -} - -#[derive(Deserialize)] -struct JsonRelease { - version: String, - url: http::Url, -} - -impl Entity for AutoUpdater { - type Event = (); -} - -impl AutoUpdater { - pub fn new( - current_version: AppVersion, - http_client: Arc, - server_url: String, - ) -> Self { - Self { - status: AutoUpdateStatus::Idle, - current_version, - http_client, - server_url, - pending_poll: None, - } - } - - pub fn start_polling(&mut self, cx: &mut ModelContext) -> Task<()> { - cx.spawn(|this, mut cx| async move { - loop { - this.update(&mut cx, |this, cx| this.poll(cx)); - cx.background().timer(POLL_INTERVAL).await; - } - }) - } - - pub fn poll(&mut self, cx: &mut ModelContext) { - if self.pending_poll.is_some() { - return; - } - - self.status = AutoUpdateStatus::Checking; - self.pending_poll = Some(cx.spawn(|this, mut cx| async move { - if let Err(error) = Self::update(this.clone(), cx.clone()).await { - this.update(&mut cx, |this, cx| { - this.status = AutoUpdateStatus::Errored { - error: error.to_string(), - }; - cx.notify(); - }); - } - - this.update(&mut cx, |this, _| this.pending_poll = None); - })); - cx.notify(); - } - - async fn update(this: ModelHandle, mut cx: AsyncAppContext) -> Result<()> { - let (client, server_url) = this.read_with(&cx, |this, _| { - (this.http_client.clone(), this.server_url.clone()) - }); - let mut response = client - .send(Request::new( - http::Method::Get, - http::Url::parse(&format!("{server_url}/api/releases/latest"))?, - )) - .await?; - let release = response - .body_json::() - .await - .map_err(|err| anyhow!("error deserializing release {:?}", err))?; - let latest_version = release.version.parse::()?; - let current_version = cx.platform().app_version()?; - if latest_version <= current_version { - this.update(&mut cx, |this, cx| { - this.status = AutoUpdateStatus::Idle; - cx.notify(); - }); - return Ok(()); - } - - let temp_dir = tempdir::TempDir::new("zed")?; - let dmg_path = temp_dir.path().join("Zed.dmg"); - let mut dmg_file = smol::fs::File::create(dmg_path).await?; - let response = client - .send(Request::new(http::Method::Get, release.url)) - .await?; - smol::io::copy(response.bytes(), &mut dmg_file).await?; - - Ok(()) - } -} diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 69f8222d77..fe8fadd13d 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -19,8 +19,7 @@ use workspace::{ AppState, OpenNew, OpenParams, OpenPaths, Settings, }; use zed::{ - self, assets::Assets, auto_updater::AutoUpdater, build_window_options, build_workspace, - fs::RealFs, languages, menus, + self, assets::Assets, build_window_options, build_workspace, fs::RealFs, languages, menus, }; fn main() { @@ -65,14 +64,8 @@ fn main() { let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx)); let channel_list = cx.add_model(|cx| ChannelList::new(user_store.clone(), client.clone(), cx)); - let auto_updater = if let Ok(current_version) = cx.platform().app_version() { - Some(cx.add_model(|cx| { - AutoUpdater::new(current_version, http, client::ZED_SERVER_URL.clone()) - })) - } else { - None - }; + auto_update::init(http, client::ZED_SERVER_URL.clone(), cx); project::Project::init(&client); client::Channel::init(&client); client::init(client.clone(), cx); @@ -133,8 +126,8 @@ fn main() { client, user_store, fs, - build_window_options: &build_window_options, - build_workspace: &build_workspace, + build_window_options, + build_workspace, }); journal::init(app_state.clone(), cx); zed::init(&app_state, cx); diff --git a/crates/zed/src/test.rs b/crates/zed/src/test.rs index 5b3bb41c15..a48e3d461e 100644 --- a/crates/zed/src/test.rs +++ b/crates/zed/src/test.rs @@ -39,7 +39,7 @@ pub fn test_app_state(cx: &mut MutableAppContext) -> Arc { client, user_store, fs: FakeFs::new(cx.background().clone()), - build_window_options: &build_window_options, - build_workspace: &build_workspace, + build_window_options, + build_workspace, }) } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index b98c5d0dfd..7968f23466 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1,5 +1,4 @@ pub mod assets; -pub mod auto_updater; pub mod languages; pub mod menus; #[cfg(any(test, feature = "test-support"))] @@ -173,11 +172,13 @@ pub fn build_workspace( workspace::lsp_status::LspStatus::new(workspace.project(), app_state.languages.clone(), cx) }); let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new()); + let auto_update = cx.add_view(|cx| auto_update::AutoUpdateIndicator::new(cx)); workspace.status_bar().update(cx, |status_bar, cx| { status_bar.add_left_item(diagnostic_summary, cx); status_bar.add_left_item(diagnostic_message, cx); status_bar.add_left_item(lsp_status, cx); status_bar.add_right_item(cursor_position, cx); + status_bar.add_right_item(auto_update, cx); }); workspace