From 38e902b241f3252fe15d4b76dc10a4b7b5f597df Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 4 Apr 2022 18:59:57 +0200 Subject: [PATCH 1/9] WIP: Start on auto-update Co-Authored-By: Nathan Sobo Co-Authored-By: Max Brunsfeld Co-Authored-By: Keith Simmons --- crates/client/src/client.rs | 2 +- crates/gpui/src/platform.rs | 36 ++++++- crates/gpui/src/platform/mac/platform.rs | 16 ++++ crates/gpui/src/platform/test.rs | 10 +- crates/zed/Cargo.toml | 17 +--- crates/zed/src/auto_updater.rs | 117 +++++++++++++++++++++++ crates/zed/src/main.rs | 10 +- crates/zed/src/zed.rs | 1 + 8 files changed, 189 insertions(+), 20 deletions(-) create mode 100644 crates/zed/src/auto_updater.rs diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 1bae6cd49e5eb9f5e24b86f6165b7a647c6a30e8..f0bc41008c2e6d068cc4cbaa2420241a2c34b8c0 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -43,7 +43,7 @@ pub use rpc::*; pub use user::*; lazy_static! { - static ref ZED_SERVER_URL: String = + pub static ref ZED_SERVER_URL: String = std::env::var("ZED_SERVER_URL").unwrap_or("https://zed.dev".to_string()); pub static ref IMPERSONATE_LOGIN: Option = std::env::var("ZED_IMPERSONATE") .ok() diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 66bd44b26f239d1c2faff0538d9e400350e02f30..cb62885651539d52fa1b55e6409104a73ae244f3 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -17,7 +17,7 @@ use crate::{ text_layout::{LineLayout, RunStyle}, AnyAction, ClipboardItem, Menu, Scene, }; -use anyhow::Result; +use anyhow::{anyhow, Result}; use async_task::Runnable; pub use event::{Event, NavigationDirection}; use postage::oneshot; @@ -25,6 +25,7 @@ use std::{ any::Any, path::{Path, PathBuf}, rc::Rc, + str::FromStr, sync::Arc, }; use time::UtcOffset; @@ -56,6 +57,7 @@ pub trait Platform: Send + Sync { fn local_timezone(&self) -> UtcOffset; fn path_for_resource(&self, name: Option<&str>, extension: Option<&str>) -> Result; + fn app_version(&self) -> Result; } pub(crate) trait ForegroundPlatform { @@ -129,6 +131,38 @@ pub enum CursorStyle { PointingHand, } +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct AppVersion { + major: usize, + minor: usize, + patch: usize, +} + +impl FromStr for AppVersion { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let mut components = s.trim().split('.'); + let major = components + .next() + .ok_or_else(|| anyhow!("missing major version number"))? + .parse()?; + let minor = components + .next() + .ok_or_else(|| anyhow!("missing minor version number"))? + .parse()?; + let patch = components + .next() + .ok_or_else(|| anyhow!("missing patch version number"))? + .parse()?; + Ok(Self { + major, + minor, + patch, + }) + } +} + pub trait FontSystem: Send + Sync { fn add_fonts(&self, fonts: &[Arc>]) -> anyhow::Result<()>; fn load_family(&self, name: &str) -> anyhow::Result>; diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 0b612c978cde8b84cd8377a5bcc836519fbe3ca7..138fd106fb346a41860642e276bbbbfc9e20c56d 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -623,6 +623,22 @@ impl platform::Platform for MacPlatform { } } } + + fn app_version(&self) -> Result { + unsafe { + let bundle: id = NSBundle::mainBundle(); + if bundle.is_null() { + Err(anyhow!("app is not running inside a bundle")) + } else { + let version: id = + msg_send![bundle, objectForInfoDictionaryKey: "CFBundleShortVersionString"]; + let len = msg_send![version, lengthOfBytesUsingEncoding: NSUTF8StringEncoding]; + let bytes = version.UTF8String() as *const u8; + let version = str::from_utf8(slice::from_raw_parts(bytes, len)).unwrap(); + version.parse() + } + } + } } unsafe fn get_foreground_platform(object: &mut Object) -> &MacForegroundPlatform { diff --git a/crates/gpui/src/platform/test.rs b/crates/gpui/src/platform/test.rs index 706439a955d6e96cd3d01c8bfb44a634786780df..ccb98c6ba6dffcbbf12531f593f9955034df93f9 100644 --- a/crates/gpui/src/platform/test.rs +++ b/crates/gpui/src/platform/test.rs @@ -1,4 +1,4 @@ -use super::{CursorStyle, WindowBounds}; +use super::{AppVersion, CursorStyle, WindowBounds}; use crate::{ geometry::vector::{vec2f, Vector2F}, AnyAction, ClipboardItem, @@ -164,6 +164,14 @@ impl super::Platform for Platform { fn path_for_resource(&self, _name: Option<&str>, _extension: Option<&str>) -> Result { Err(anyhow!("app not running inside a bundle")) } + + fn app_version(&self) -> Result { + Ok(AppVersion { + major: 1, + minor: 0, + patch: 0, + }) + } } impl Window { diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 2e1ef780caf77d6e0ffd0352e60b137ee7a07a71..937b9208cea02a041e6ba4d719d6bdfc93b59a4f 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -14,20 +14,6 @@ doctest = false name = "Zed" path = "src/main.rs" -[features] -test-support = [ - "text/test-support", - "client/test-support", - "editor/test-support", - "gpui/test-support", - "language/test-support", - "lsp/test-support", - "project/test-support", - "rpc/test-support", - "tempdir", - "workspace/test-support", -] - [dependencies] breadcrumbs = { path = "../breadcrumbs" } chat_panel = { path = "../chat_panel" } @@ -90,7 +76,7 @@ simplelog = "0.9" smallvec = { version = "1.6", features = ["union"] } smol = "1.2.5" surf = "2.2" -tempdir = { version = "0.3.7", optional = true } +tempdir = { version = "0.3.7" } thiserror = "1.0.29" tiny_http = "0.8" toml = "0.5" @@ -115,7 +101,6 @@ util = { path = "../util", features = ["test-support"] } workspace = { path = "../workspace", features = ["test-support"] } env_logger = "0.8" serde_json = { version = "1.0.64", features = ["preserve_order"] } -tempdir = { version = "0.3.7" } unindent = "0.1.7" [package.metadata.bundle] diff --git a/crates/zed/src/auto_updater.rs b/crates/zed/src/auto_updater.rs new file mode 100644 index 0000000000000000000000000000000000000000..f04fbcfcfbebe60de57d91d802a87d6583232e7f --- /dev/null +++ b/crates/zed/src/auto_updater.rs @@ -0,0 +1,117 @@ +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 49efc9ade2af2749cca42cc20362a1c174dea833..69f8222d778fc52a9ef22d26ce8badd0075f488e 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -19,7 +19,8 @@ use workspace::{ AppState, OpenNew, OpenParams, OpenPaths, Settings, }; use zed::{ - self, assets::Assets, build_window_options, build_workspace, fs::RealFs, languages, menus, + self, assets::Assets, auto_updater::AutoUpdater, build_window_options, build_workspace, + fs::RealFs, languages, menus, }; fn main() { @@ -64,6 +65,13 @@ 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 + }; project::Project::init(&client); client::Channel::init(&client); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index aec0bc533e4a1842789b813c4fc060eaa334e165..b98c5d0dfd11a9e9eba7a1a04c9eea222d8bc615 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1,4 +1,5 @@ pub mod assets; +pub mod auto_updater; pub mod languages; pub mod menus; #[cfg(any(test, feature = "test-support"))] From 9c469f2fdb71cdc8908d3a40138a12736eb53d05 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 4 Apr 2022 14:57:21 -0700 Subject: [PATCH 2/9] Add remaining logic for downloading updates, add status bar indicator --- Cargo.lock | 19 ++ crates/auto_update/Cargo.toml | 22 ++ crates/auto_update/src/auto_update.rs | 288 ++++++++++++++++++++++++++ crates/theme/src/theme.rs | 2 + crates/workspace/src/workspace.rs | 9 +- crates/zed/Cargo.toml | 1 + crates/zed/assets/themes/_base.toml | 2 + crates/zed/src/auto_updater.rs | 117 ----------- crates/zed/src/main.rs | 15 +- crates/zed/src/test.rs | 4 +- crates/zed/src/zed.rs | 3 +- 11 files changed, 345 insertions(+), 137 deletions(-) create mode 100644 crates/auto_update/Cargo.toml create mode 100644 crates/auto_update/src/auto_update.rs delete mode 100644 crates/zed/src/auto_updater.rs diff --git a/Cargo.lock b/Cargo.lock index c4a7112abd41bc7ee3621f0f4d8fa88b026e852f..3bd89c4d797bbf2a44e50200660fe51db6f2618f 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 0000000000000000000000000000000000000000..39f422ea6f80624912af9ae2ed7716ec6fb232ef --- /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 0000000000000000000000000000000000000000..56f94f526f2711c53f51e73de9d74cdd0c1db2e3 --- /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 8fa15a92359c18ff7986978d66088c4f6a9332c1..ff6eba88ef301cf3bb832256fcbb35ae92e348bd 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 c447f3a5fd567c1f5f89a11f2e93edce632648b9..c79713a3536fad98d40738cbb77e24dada896665 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 937b9208cea02a041e6ba4d719d6bdfc93b59a4f..71f65bd4170126d90542d6cfa78af85d5b78255b 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 7f235cbf48e322fc60f1cebee27e7d15a53d7014..3a84ff6db3b5ea8829227cf8250b42bee481b26e 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 f04fbcfcfbebe60de57d91d802a87d6583232e7f..0000000000000000000000000000000000000000 --- 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 69f8222d778fc52a9ef22d26ce8badd0075f488e..fe8fadd13df0697a90c54fc91eb15bd80d7c1783 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 5b3bb41c1523bf910a523766f6af2c9a631d264d..a48e3d461ec755358d09624a8feda273899fbfa3 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 b98c5d0dfd11a9e9eba7a1a04c9eea222d8bc615..7968f234666c9a7128cbe8a63a8e74cffc791065 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 From fb2caf3c58d58f52e30a784524e6f9f0e37dff3b Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 4 Apr 2022 18:05:26 -0700 Subject: [PATCH 3/9] Add application menu item for checking for updates --- crates/auto_update/src/auto_update.rs | 6 ++++++ crates/zed/src/menus.rs | 5 +++++ crates/zed/src/zed.rs | 2 ++ 3 files changed, 13 insertions(+) diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index 56f94f526f2711c53f51e73de9d74cdd0c1db2e3..c8f70b16887e2f7d48073f739cbe806435a2341b 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -70,6 +70,12 @@ pub fn init(http_client: Arc, server_url: String, cx: &mut Mutab } } +pub fn check(cx: &mut MutableAppContext) { + if let Some(updater) = AutoUpdater::get(cx) { + updater.update(cx, |updater, cx| updater.poll(cx)); + } +} + impl AutoUpdater { fn get(cx: &mut MutableAppContext) -> Option> { cx.default_global::>>().clone() diff --git a/crates/zed/src/menus.rs b/crates/zed/src/menus.rs index 33ac76e63cc3d7ff75fb3df1c9f6f9523854acf9..8d23dc4f58252eeda8a31b6a57b8b63656bffdda 100644 --- a/crates/zed/src/menus.rs +++ b/crates/zed/src/menus.rs @@ -13,6 +13,11 @@ pub fn menus(state: &Arc) -> Vec> { keystroke: None, action: Box::new(super::About), }, + MenuItem::Action { + name: "Check for Updates", + keystroke: None, + action: Box::new(super::CheckForUpdates), + }, MenuItem::Separator, MenuItem::Action { name: "Quit", diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 7968f234666c9a7128cbe8a63a8e74cffc791065..479e65dba0dda2576a866498f024312ca441fc4a 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -31,6 +31,7 @@ action!(About); action!(Quit); action!(OpenSettings); action!(AdjustBufferFontSize, f32); +action!(CheckForUpdates); const MIN_FONT_SIZE: f32 = 6.0; @@ -43,6 +44,7 @@ lazy_static! { pub fn init(app_state: &Arc, cx: &mut gpui::MutableAppContext) { cx.add_global_action(quit); + cx.add_global_action(|_: &CheckForUpdates, cx| auto_update::check(cx)); cx.add_global_action({ move |action: &AdjustBufferFontSize, cx| { cx.update_global::(|settings, cx| { From e566a8335f3c5c712eb526ecce01fb09fd8390c4 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 4 Apr 2022 20:53:40 -0700 Subject: [PATCH 4/9] Find path to running app using [NSBundle bundlePath] --- crates/auto_update/src/auto_update.rs | 2 +- crates/gpui/src/platform.rs | 1 + crates/gpui/src/platform/mac/platform.rs | 23 +++++++++++++++++++---- crates/gpui/src/platform/test.rs | 4 ++++ 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index c8f70b16887e2f7d48073f739cbe806435a2341b..a129caf51f980c25423b5a85f543f861836940dd 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -166,7 +166,7 @@ impl AutoUpdater { mounted_app_path.push("/"); let running_app_path = ZED_APP_PATH .clone() - .map_or_else(|| cx.platform().path_for_resource(None, None), Ok)?; + .map_or_else(|| cx.platform().app_path(), Ok)?; let mut dmg_file = File::create(&dmg_path).await?; let response = client diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index cb62885651539d52fa1b55e6409104a73ae244f3..284be36205eade5347cc4dfa2e928449d5de633f 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -57,6 +57,7 @@ pub trait Platform: Send + Sync { fn local_timezone(&self) -> UtcOffset; fn path_for_resource(&self, name: Option<&str>, extension: Option<&str>) -> Result; + fn app_path(&self) -> Result; fn app_version(&self) -> Result; } diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 138fd106fb346a41860642e276bbbbfc9e20c56d..e159b2535763ff4f7a9524e643a3c7e4ed01052d 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -615,15 +615,23 @@ impl platform::Platform for MacPlatform { if path.is_null() { Err(anyhow!("resource could not be found")) } else { - let len = msg_send![path, lengthOfBytesUsingEncoding: NSUTF8StringEncoding]; - let bytes = path.UTF8String() as *const u8; - let path = str::from_utf8(slice::from_raw_parts(bytes, len)).unwrap(); - Ok(PathBuf::from(path)) + Ok(path_from_objc(path)) } } } } + fn app_path(&self) -> Result { + unsafe { + let bundle: id = NSBundle::mainBundle(); + if bundle.is_null() { + Err(anyhow!("app is not running inside a bundle")) + } else { + Ok(path_from_objc(msg_send![bundle, bundlePath])) + } + } + } + fn app_version(&self) -> Result { unsafe { let bundle: id = NSBundle::mainBundle(); @@ -641,6 +649,13 @@ impl platform::Platform for MacPlatform { } } +unsafe fn path_from_objc(path: id) -> PathBuf { + let len = msg_send![path, lengthOfBytesUsingEncoding: NSUTF8StringEncoding]; + let bytes = path.UTF8String() as *const u8; + let path = str::from_utf8(slice::from_raw_parts(bytes, len)).unwrap(); + PathBuf::from(path) +} + unsafe fn get_foreground_platform(object: &mut Object) -> &MacForegroundPlatform { let platform_ptr: *mut c_void = *object.get_ivar(MAC_PLATFORM_IVAR); assert!(!platform_ptr.is_null()); diff --git a/crates/gpui/src/platform/test.rs b/crates/gpui/src/platform/test.rs index ccb98c6ba6dffcbbf12531f593f9955034df93f9..5bbf7d06c1ae239231a33a84f57f6f1b2a0e25e2 100644 --- a/crates/gpui/src/platform/test.rs +++ b/crates/gpui/src/platform/test.rs @@ -165,6 +165,10 @@ impl super::Platform for Platform { Err(anyhow!("app not running inside a bundle")) } + fn app_path(&self) -> Result { + Err(anyhow!("app not running inside a bundle")) + } + fn app_version(&self) -> Result { Ok(AppVersion { major: 1, From 61c479ebc80ba968bf708906827bb32704de81eb Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 Apr 2022 10:02:37 +0200 Subject: [PATCH 5/9] Pass an `NSString` to `objectForInfoDictionaryKey` --- crates/gpui/src/platform/mac/platform.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index e159b2535763ff4f7a9524e643a3c7e4ed01052d..d620c47c15c0477439f4476e884f3dbec4dcc18e 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -638,8 +638,7 @@ impl platform::Platform for MacPlatform { if bundle.is_null() { Err(anyhow!("app is not running inside a bundle")) } else { - let version: id = - msg_send![bundle, objectForInfoDictionaryKey: "CFBundleShortVersionString"]; + let version: id = msg_send![bundle, objectForInfoDictionaryKey: ns_string("CFBundleShortVersionString")]; let len = msg_send![version, lengthOfBytesUsingEncoding: NSUTF8StringEncoding]; let bytes = version.UTF8String() as *const u8; let version = str::from_utf8(slice::from_raw_parts(bytes, len)).unwrap(); From bd0b063bd1c6df9e6a8b7c21cfc0e1d9dfe5622d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 Apr 2022 10:07:45 +0200 Subject: [PATCH 6/9] =?UTF-8?q?Display=20`Installing=20update=E2=80=A6`=20?= =?UTF-8?q?when=20the=20new=20app=20is=20being=20copied?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/auto_update/src/auto_update.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index a129caf51f980c25423b5a85f543f861836940dd..a39f8027e6cd4d9a50a38ea5657be0ea82da787c 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -30,6 +30,7 @@ pub enum AutoUpdateStatus { Idle, Checking, Downloading, + Installing, Updated, Errored, } @@ -175,6 +176,11 @@ impl AutoUpdater { smol::io::copy(response.bytes(), &mut dmg_file).await?; log::info!("downloaded update. path:{:?}", dmg_path); + this.update(&mut cx, |this, cx| { + this.status = AutoUpdateStatus::Installing; + cx.notify(); + }); + let output = Command::new("hdiutil") .args(&["attach", "-nobrowse"]) .arg(&dmg_path) @@ -215,7 +221,7 @@ impl AutoUpdater { } this.update(&mut cx, |this, cx| { - this.status = AutoUpdateStatus::Idle; + this.status = AutoUpdateStatus::Updated; cx.notify(); }); Ok(()) @@ -245,6 +251,11 @@ impl View for AutoUpdateIndicator { theme.auto_update_progress_message.clone(), ) .boxed(), + AutoUpdateStatus::Installing => Text::new( + "Installing update…".to_string(), + theme.auto_update_done_message.clone(), + ) + .boxed(), AutoUpdateStatus::Updated => Text::new( "Restart to update Zed".to_string(), theme.auto_update_done_message.clone(), From 493450f6a83223c2dfd350a994cfd249318254f6 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 Apr 2022 10:15:26 +0200 Subject: [PATCH 7/9] Massage styling of auto-update messages a bit --- crates/auto_update/src/auto_update.rs | 2 +- crates/zed/assets/themes/_base.toml | 2 +- crates/zed/src/zed.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index a39f8027e6cd4d9a50a38ea5657be0ea82da787c..dcb36b8dd5f419585cea59e042b15c0650d8a89a 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -253,7 +253,7 @@ impl View for AutoUpdateIndicator { .boxed(), AutoUpdateStatus::Installing => Text::new( "Installing update…".to_string(), - theme.auto_update_done_message.clone(), + theme.auto_update_progress_message.clone(), ) .boxed(), AutoUpdateStatus::Updated => Text::new( diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index 3a84ff6db3b5ea8829227cf8250b42bee481b26e..890825992b1ff93711e4a48b13c231823bfafc28 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -84,7 +84,7 @@ cursor_position = "$text.2" diagnostic_message = "$text.2" lsp_message = "$text.2" auto_update_progress_message = "$text.2" -auto_update_done_message = "$text.0" +auto_update_done_message = "$text.2" [workspace.toolbar] background = "$surface.1" diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 479e65dba0dda2576a866498f024312ca441fc4a..6d904842be7ca12ac23fd1dd4766f04ea67adf5d 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -179,8 +179,8 @@ pub fn build_workspace( 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); + status_bar.add_right_item(cursor_position, cx); }); workspace From 4adb245771eda22edfc25abaa2ecdebea8498b69 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 Apr 2022 10:16:08 +0200 Subject: [PATCH 8/9] :lipstick: --- crates/auto_update/src/auto_update.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index dcb36b8dd5f419585cea59e042b15c0650d8a89a..e32c45ccf6937a752755c34ada32740a5a7b296f 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -10,7 +10,7 @@ use gpui::{ 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 std::{env, ffi::OsString, path::PathBuf, sync::Arc, time::Duration}; use surf::Request; use workspace::{ItemHandle, Settings, StatusItemView}; @@ -18,11 +18,10 @@ 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") + pub static ref ZED_APP_VERSION: Option = 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); + pub static ref ZED_APP_PATH: Option = env::var("ZED_APP_PATH").ok().map(PathBuf::from); } #[derive(Clone, PartialEq, Eq)] From 4a5c49eb6e220b0f0c9bf3e3f25e9b3eb13f336a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 Apr 2022 10:18:37 +0200 Subject: [PATCH 9/9] Skip checking for updates when an update has already been installed --- crates/auto_update/src/auto_update.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index e32c45ccf6937a752755c34ada32740a5a7b296f..4e70333cadf20e165cfb8e60187f566d940d3280 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -105,7 +105,7 @@ impl AutoUpdater { } pub fn poll(&mut self, cx: &mut ModelContext) { - if self.pending_poll.is_some() { + if self.pending_poll.is_some() || self.status == AutoUpdateStatus::Updated { return; }