From 21875130267189df0b0bf3e4a7d33acab165c84b Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 31 Jan 2024 15:46:24 -0700 Subject: [PATCH] app version to server (#7130) - Send app version and release stage to collab on connect - Read the new header on the server Release Notes: - Added the ability to collaborate with users on different releases of Zed. --- Cargo.lock | 1 + crates/auto_update/src/auto_update.rs | 39 ++++++++++--------- crates/channel/src/channel_store_tests.rs | 1 + crates/client/src/client.rs | 19 ++++++---- crates/collab/Cargo.toml | 1 + crates/collab/src/rpc.rs | 30 +++++++++++++++ crates/collab/src/tests/test_server.rs | 1 + crates/feedback/src/system_specs.rs | 22 ++++------- crates/gpui/src/platform.rs | 45 ++-------------------- crates/release_channel/src/lib.rs | 37 +++++++++++++++--- crates/settings/src/settings_store.rs | 4 +- crates/util/src/semantic_version.rs | 46 +++++++++++++++++++++++ crates/util/src/util.rs | 2 + crates/zed/src/main.rs | 14 ++++--- 14 files changed, 166 insertions(+), 96 deletions(-) create mode 100644 crates/util/src/semantic_version.rs diff --git a/Cargo.lock b/Cargo.lock index ddb47a656a..ee03a44288 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1486,6 +1486,7 @@ dependencies = [ "prometheus", "prost 0.8.0", "rand 0.8.5", + "release_channel", "reqwest", "rpc", "scrypt", diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index d04acb10f1..cb08871c6d 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -1,7 +1,7 @@ mod update_notification; use anyhow::{anyhow, Context, Result}; -use client::{Client, TelemetrySettings, ZED_APP_PATH, ZED_APP_VERSION}; +use client::{Client, TelemetrySettings, ZED_APP_PATH}; use db::kvp::KEY_VALUE_STORE; use db::RELEASE_CHANNEL; use gpui::{ @@ -108,29 +108,28 @@ pub fn init(http_client: Arc, cx: &mut AppContext) { }) .detach(); - if let Some(version) = ZED_APP_VERSION.or_else(|| cx.app_metadata().app_version) { - let auto_updater = cx.new_model(|cx| { - let updater = AutoUpdater::new(version, http_client); + let version = release_channel::AppVersion::global(cx); + let auto_updater = cx.new_model(|cx| { + let updater = AutoUpdater::new(version, http_client); - let mut update_subscription = AutoUpdateSetting::get_global(cx) - .0 - .then(|| updater.start_polling(cx)); + let mut update_subscription = AutoUpdateSetting::get_global(cx) + .0 + .then(|| updater.start_polling(cx)); - cx.observe_global::(move |updater, cx| { - if AutoUpdateSetting::get_global(cx).0 { - if update_subscription.is_none() { - update_subscription = Some(updater.start_polling(cx)) - } - } else { - update_subscription.take(); + cx.observe_global::(move |updater, cx| { + if AutoUpdateSetting::get_global(cx).0 { + if update_subscription.is_none() { + update_subscription = Some(updater.start_polling(cx)) } - }) - .detach(); + } else { + update_subscription.take(); + } + }) + .detach(); - updater - }); - cx.set_global(GlobalAutoUpdate(Some(auto_updater))); - } + updater + }); + cx.set_global(GlobalAutoUpdate(Some(auto_updater))); } pub fn check(_: &Check, cx: &mut WindowContext) { diff --git a/crates/channel/src/channel_store_tests.rs b/crates/channel/src/channel_store_tests.rs index 6450736d34..57b9d20710 100644 --- a/crates/channel/src/channel_store_tests.rs +++ b/crates/channel/src/channel_store_tests.rs @@ -329,6 +329,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) { fn init_test(cx: &mut AppContext) -> Model { let settings_store = SettingsStore::test(cx); cx.set_global(settings_store); + release_channel::init("0.0.0", cx); client::init_settings(cx); let http = FakeHttpClient::with_404_response(); diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 3b182e25ca..dc95d0ca67 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -15,14 +15,13 @@ use futures::{ TryFutureExt as _, TryStreamExt, }; use gpui::{ - actions, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, Global, Model, SemanticVersion, - Task, WeakModel, + actions, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, Global, Model, Task, WeakModel, }; use lazy_static::lazy_static; use parking_lot::RwLock; use postage::watch; use rand::prelude::*; -use release_channel::ReleaseChannel; +use release_channel::{AppVersion, ReleaseChannel}; use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, PeerId, RequestMessage}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -58,9 +57,6 @@ lazy_static! { pub static ref ADMIN_API_TOKEN: Option = std::env::var("ZED_ADMIN_API_TOKEN") .ok() .and_then(|s| if s.is_empty() { None } else { Some(s) }); - 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); pub static ref ZED_ALWAYS_ACTIVE: bool = @@ -1011,13 +1007,22 @@ impl Client { .update(|cx| ReleaseChannel::try_global(cx)) .ok() .flatten(); + let app_version = cx + .update(|cx| AppVersion::global(cx).to_string()) + .ok() + .unwrap_or_default(); let request = Request::builder() .header( "Authorization", format!("{} {}", credentials.user_id, credentials.access_token), ) - .header("x-zed-protocol-version", rpc::PROTOCOL_VERSION); + .header("x-zed-protocol-version", rpc::PROTOCOL_VERSION) + .header("x-zed-app-version", app_version) + .header( + "x-zed-release-channel", + release_channel.map(|r| r.dev_name()).unwrap_or("unknown"), + ); let http = self.http.clone(); cx.background_executor().spawn(async move { diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index a271e1a8f3..c2809d30b7 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -61,6 +61,7 @@ util = { path = "../util" } uuid.workspace = true [dev-dependencies] +release_channel = { path = "../release_channel" } async-trait.workspace = true audio = { path = "../audio" } call = { path = "../call", features = ["test-support"] } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index cb758f0cac..c97c283b2f 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -64,6 +64,7 @@ use time::OffsetDateTime; use tokio::sync::{watch, Semaphore}; use tower::ServiceBuilder; use tracing::{field, info_span, instrument, Instrument}; +use util::SemanticVersion; pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30); pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(10); @@ -795,6 +796,7 @@ fn broadcast( lazy_static! { static ref ZED_PROTOCOL_VERSION: HeaderName = HeaderName::from_static("x-zed-protocol-version"); + static ref ZED_APP_VERSION: HeaderName = HeaderName::from_static("x-zed-app-version"); } pub struct ProtocolVersion(u32); @@ -824,6 +826,32 @@ impl Header for ProtocolVersion { } } +pub struct AppVersionHeader(SemanticVersion); +impl Header for AppVersionHeader { + fn name() -> &'static HeaderName { + &ZED_APP_VERSION + } + + fn decode<'i, I>(values: &mut I) -> Result + where + Self: Sized, + I: Iterator, + { + let version = values + .next() + .ok_or_else(axum::headers::Error::invalid)? + .to_str() + .map_err(|_| axum::headers::Error::invalid())? + .parse() + .map_err(|_| axum::headers::Error::invalid())?; + Ok(Self(version)) + } + + fn encode>(&self, values: &mut E) { + values.extend([self.0.to_string().parse().unwrap()]); + } +} + pub fn routes(server: Arc) -> Router { Router::new() .route("/rpc", get(handle_websocket_request)) @@ -838,6 +866,7 @@ pub fn routes(server: Arc) -> Router { pub async fn handle_websocket_request( TypedHeader(ProtocolVersion(protocol_version)): TypedHeader, + _app_version_header: Option>, ConnectInfo(socket_address): ConnectInfo, Extension(server): Extension>, Extension(user): Extension, @@ -851,6 +880,7 @@ pub async fn handle_websocket_request( ) .into_response(); } + let socket_address = socket_address.to_string(); ws.on_upgrade(move |socket| { use util::ResultExt; diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index e10f03605a..13fd15e3f4 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -153,6 +153,7 @@ impl TestServer { } let settings = SettingsStore::test(cx); cx.set_global(settings); + release_channel::init("0.0.0", cx); client::init_settings(cx); }); diff --git a/crates/feedback/src/system_specs.rs b/crates/feedback/src/system_specs.rs index 1d1fc93841..2e4535d189 100644 --- a/crates/feedback/src/system_specs.rs +++ b/crates/feedback/src/system_specs.rs @@ -1,14 +1,13 @@ -use client::ZED_APP_VERSION; use gpui::AppContext; use human_bytes::human_bytes; -use release_channel::ReleaseChannel; +use release_channel::{AppVersion, ReleaseChannel}; use serde::Serialize; use std::{env, fmt::Display}; use sysinfo::{RefreshKind, System, SystemExt}; #[derive(Clone, Debug, Serialize)] pub struct SystemSpecs { - app_version: Option, + app_version: String, release_channel: &'static str, os_name: &'static str, os_version: Option, @@ -18,9 +17,7 @@ pub struct SystemSpecs { impl SystemSpecs { pub fn new(cx: &AppContext) -> Self { - let app_version = ZED_APP_VERSION - .or_else(|| cx.app_metadata().app_version) - .map(|v| v.to_string()); + let app_version = AppVersion::global(cx).to_string(); let release_channel = ReleaseChannel::global(cx).display_name(); let os_name = cx.app_metadata().os_name; let system = System::new_with_specifics(RefreshKind::new().with_memory()); @@ -48,18 +45,15 @@ impl Display for SystemSpecs { Some(os_version) => format!("OS: {} {}", self.os_name, os_version), None => format!("OS: {}", self.os_name), }; - let app_version_information = self - .app_version - .as_ref() - .map(|app_version| format!("Zed: v{} ({})", app_version, self.release_channel)); + let app_version_information = + format!("Zed: v{} ({})", self.app_version, self.release_channel); let system_specs = [ app_version_information, - Some(os_information), - Some(format!("Memory: {}", human_bytes(self.memory as f64))), - Some(format!("Architecture: {}", self.architecture)), + os_information, + format!("Memory: {}", human_bytes(self.memory as f64)), + format!("Architecture: {}", self.architecture), ] .into_iter() - .flatten() .collect::>() .join("\n"); diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 67c3a70ccb..df886fc4d6 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -11,7 +11,7 @@ use crate::{ Pixels, PlatformInput, Point, RenderGlyphParams, RenderImageParams, RenderSvgParams, Scene, SharedString, Size, Task, TaskLabel, WindowContext, }; -use anyhow::{anyhow, Result}; +use anyhow::Result; use async_task::Runnable; use futures::channel::oneshot; use parking::Unparker; @@ -23,11 +23,10 @@ use std::hash::{Hash, Hasher}; use std::time::Duration; use std::{ any::Any, - fmt::{self, Debug, Display}, + fmt::{self, Debug}, ops::Range, path::{Path, PathBuf}, rc::Rc, - str::FromStr, sync::Arc, }; use uuid::Uuid; @@ -39,6 +38,7 @@ pub(crate) use mac::*; #[cfg(any(test, feature = "test-support"))] pub(crate) use test::*; use time::UtcOffset; +pub use util::SemanticVersion; #[cfg(target_os = "macos")] pub(crate) fn current_platform() -> Rc { @@ -697,45 +697,6 @@ impl Default for CursorStyle { } } -/// A datastructure representing a semantic version number -#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd, Serialize)] -pub struct SemanticVersion { - major: usize, - minor: usize, - patch: usize, -} - -impl FromStr for SemanticVersion { - 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, - }) - } -} - -impl Display for SemanticVersion { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}.{}.{}", self.major, self.minor, self.patch) - } -} - /// A clipboard item that should be copied to the clipboard #[derive(Clone, Debug, Eq, PartialEq)] pub struct ClipboardItem { diff --git a/crates/release_channel/src/lib.rs b/crates/release_channel/src/lib.rs index 410b02868e..e83ef0e069 100644 --- a/crates/release_channel/src/lib.rs +++ b/crates/release_channel/src/lib.rs @@ -1,9 +1,9 @@ -use gpui::{AppContext, Global}; +use gpui::{AppContext, Global, SemanticVersion}; use once_cell::sync::Lazy; use std::env; #[doc(hidden)] -pub static RELEASE_CHANNEL_NAME: Lazy = if cfg!(debug_assertions) { +static RELEASE_CHANNEL_NAME: Lazy = if cfg!(debug_assertions) { Lazy::new(|| { env::var("ZED_RELEASE_CHANNEL") .unwrap_or_else(|_| include_str!("../../zed/RELEASE_CHANNEL").trim().to_string()) @@ -11,6 +11,7 @@ pub static RELEASE_CHANNEL_NAME: Lazy = if cfg!(debug_assertions) { } else { Lazy::new(|| include_str!("../../zed/RELEASE_CHANNEL").trim().to_string()) }; + #[doc(hidden)] pub static RELEASE_CHANNEL: Lazy = Lazy::new(|| match RELEASE_CHANNEL_NAME.as_str() { @@ -39,6 +40,29 @@ impl AppCommitSha { } } +struct GlobalAppVersion(SemanticVersion); + +impl Global for GlobalAppVersion {} + +pub struct AppVersion; + +impl AppVersion { + pub fn init(pkg_version: &str, cx: &mut AppContext) { + let version = if let Some(from_env) = env::var("ZED_APP_VERSION").ok() { + from_env.parse().expect("invalid ZED_APP_VERSION") + } else { + cx.app_metadata() + .app_version + .unwrap_or_else(|| pkg_version.parse().expect("invalid version in Cargo.toml")) + }; + cx.set_global(GlobalAppVersion(version)) + } + + pub fn global(cx: &AppContext) -> SemanticVersion { + cx.global::().0 + } +} + #[derive(Debug, Copy, Clone, PartialEq, Eq, Default)] pub enum ReleaseChannel { #[default] @@ -52,11 +76,12 @@ struct GlobalReleaseChannel(ReleaseChannel); impl Global for GlobalReleaseChannel {} -impl ReleaseChannel { - pub fn init(cx: &mut AppContext) { - cx.set_global(GlobalReleaseChannel(*RELEASE_CHANNEL)) - } +pub fn init(pkg_version: &str, cx: &mut AppContext) { + AppVersion::init(pkg_version, cx); + cx.set_global(GlobalReleaseChannel(*RELEASE_CHANNEL)) +} +impl ReleaseChannel { pub fn global(cx: &AppContext) -> Self { cx.global::().0 } diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 7398876137..116dc1520f 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -210,7 +210,7 @@ impl SettingsStore { if let Some(release_settings) = &self .raw_user_settings - .get(&*release_channel::RELEASE_CHANNEL_NAME) + .get(&*release_channel::RELEASE_CHANNEL.dev_name()) { if let Some(release_settings) = setting_value .deserialize_setting(&release_settings) @@ -543,7 +543,7 @@ impl SettingsStore { if let Some(release_settings) = &self .raw_user_settings - .get(&*release_channel::RELEASE_CHANNEL_NAME) + .get(&*release_channel::RELEASE_CHANNEL.dev_name()) { if let Some(release_settings) = setting_value .deserialize_setting(&release_settings) diff --git a/crates/util/src/semantic_version.rs b/crates/util/src/semantic_version.rs new file mode 100644 index 0000000000..f5e4562adf --- /dev/null +++ b/crates/util/src/semantic_version.rs @@ -0,0 +1,46 @@ +use std::{ + fmt::{self, Display}, + str::FromStr, +}; + +use anyhow::{anyhow, Result}; +use serde::Serialize; + +/// A datastructure representing a semantic version number +#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd, Serialize)] +pub struct SemanticVersion { + pub major: usize, + pub minor: usize, + pub patch: usize, +} + +impl FromStr for SemanticVersion { + 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, + }) + } +} + +impl Display for SemanticVersion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}.{}.{}", self.major, self.minor, self.patch) + } +} diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index b1205aa109..30f2628482 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -3,6 +3,7 @@ pub mod fs; pub mod github; pub mod http; pub mod paths; +mod semantic_version; #[cfg(any(test, feature = "test-support"))] pub mod test; @@ -10,6 +11,7 @@ pub use backtrace::Backtrace; use futures::Future; use lazy_static::lazy_static; use rand::{seq::SliceRandom, Rng}; +pub use semantic_version::SemanticVersion; use std::{ borrow::Cow, cmp::{self, Ordering}, diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 97ee6d1c68..a7d7b42c91 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -120,7 +120,7 @@ fn main() { }); app.run(move |cx| { - ReleaseChannel::init(cx); + release_channel::init(env!("CARGO_PKG_VERSION"), cx); if let Some(build_sha) = option_env!("ZED_COMMIT_SHA") { AppCommitSha::set_global(AppCommitSha(build_sha.into()), cx); } @@ -608,9 +608,13 @@ fn init_panic_hook(app: &App, installation_id: Option, session_id: Strin std::process::exit(-1); } - let app_version = client::ZED_APP_VERSION - .or(app_metadata.app_version) - .map_or("dev".to_string(), |v| v.to_string()); + let app_version = if let Some(version) = app_metadata.app_version { + version.to_string() + } else { + option_env!("CARGO_PKG_VERSION") + .unwrap_or("dev") + .to_string() + }; let backtrace = Backtrace::new(); let mut backtrace = backtrace @@ -639,7 +643,7 @@ fn init_panic_hook(app: &App, installation_id: Option, session_id: Strin file: location.file().into(), line: location.line(), }), - app_version: app_version.clone(), + app_version: app_version.to_string(), release_channel: RELEASE_CHANNEL.display_name().into(), os_name: app_metadata.os_name.into(), os_version: app_metadata