app version to server (#7130)

Conrad Irwin created

- 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.

Change summary

Cargo.lock                                |  1 
crates/auto_update/src/auto_update.rs     | 41 ++++++++++-----------
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, 167 insertions(+), 97 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1486,6 +1486,7 @@ dependencies = [
  "prometheus",
  "prost 0.8.0",
  "rand 0.8.5",
+ "release_channel",
  "reqwest",
  "rpc",
  "scrypt",

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<ZedHttpClient>, 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));
-
-            cx.observe_global::<SettingsStore>(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();
+        let mut update_subscription = AutoUpdateSetting::get_global(cx)
+            .0
+            .then(|| updater.start_polling(cx));
+
+        cx.observe_global::<SettingsStore>(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) {

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<ChannelStore> {
     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();

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<String> = 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<SemanticVersion> = std::env::var("ZED_APP_VERSION")
-        .ok()
-        .and_then(|v| v.parse().ok());
     pub static ref ZED_APP_PATH: Option<PathBuf> =
         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 {

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"] }

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<F>(
 
 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<Self, axum::headers::Error>
+    where
+        Self: Sized,
+        I: Iterator<Item = &'i axum::http::HeaderValue>,
+    {
+        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<E: Extend<axum::http::HeaderValue>>(&self, values: &mut E) {
+        values.extend([self.0.to_string().parse().unwrap()]);
+    }
+}
+
 pub fn routes(server: Arc<Server>) -> Router<Body> {
     Router::new()
         .route("/rpc", get(handle_websocket_request))
@@ -838,6 +866,7 @@ pub fn routes(server: Arc<Server>) -> Router<Body> {
 
 pub async fn handle_websocket_request(
     TypedHeader(ProtocolVersion(protocol_version)): TypedHeader<ProtocolVersion>,
+    _app_version_header: Option<TypedHeader<AppVersionHeader>>,
     ConnectInfo(socket_address): ConnectInfo<SocketAddr>,
     Extension(server): Extension<Arc<Server>>,
     Extension(user): Extension<User>,
@@ -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;

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);
         });
 

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<String>,
+    app_version: String,
     release_channel: &'static str,
     os_name: &'static str,
     os_version: Option<String>,
@@ -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::<Vec<String>>()
         .join("\n");
 

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<dyn Platform> {
@@ -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<Self> {
-        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 {

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<String> = if cfg!(debug_assertions) {
+static RELEASE_CHANNEL_NAME: Lazy<String> = 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<String> = if cfg!(debug_assertions) {
 } else {
     Lazy::new(|| include_str!("../../zed/RELEASE_CHANNEL").trim().to_string())
 };
+
 #[doc(hidden)]
 pub static RELEASE_CHANNEL: Lazy<ReleaseChannel> =
     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::<GlobalAppVersion>().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::<GlobalReleaseChannel>().0
     }

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)

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<Self> {
+        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)
+    }
+}

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},

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<String>, 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<String>, 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