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.
This commit is contained in:
Conrad Irwin 2024-01-31 15:46:24 -07:00 committed by GitHub
parent 5b7b5bfea5
commit 2187513026
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 166 additions and 96 deletions

1
Cargo.lock generated
View File

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

View File

@ -1,7 +1,7 @@
mod update_notification; mod update_notification;
use anyhow::{anyhow, Context, Result}; 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::kvp::KEY_VALUE_STORE;
use db::RELEASE_CHANNEL; use db::RELEASE_CHANNEL;
use gpui::{ use gpui::{
@ -108,29 +108,28 @@ pub fn init(http_client: Arc<ZedHttpClient>, cx: &mut AppContext) {
}) })
.detach(); .detach();
if let Some(version) = ZED_APP_VERSION.or_else(|| cx.app_metadata().app_version) { let version = release_channel::AppVersion::global(cx);
let auto_updater = cx.new_model(|cx| { let auto_updater = cx.new_model(|cx| {
let updater = AutoUpdater::new(version, http_client); let updater = AutoUpdater::new(version, http_client);
let mut update_subscription = AutoUpdateSetting::get_global(cx) let mut update_subscription = AutoUpdateSetting::get_global(cx)
.0 .0
.then(|| updater.start_polling(cx)); .then(|| updater.start_polling(cx));
cx.observe_global::<SettingsStore>(move |updater, cx| { cx.observe_global::<SettingsStore>(move |updater, cx| {
if AutoUpdateSetting::get_global(cx).0 { if AutoUpdateSetting::get_global(cx).0 {
if update_subscription.is_none() { if update_subscription.is_none() {
update_subscription = Some(updater.start_polling(cx)) update_subscription = Some(updater.start_polling(cx))
}
} else {
update_subscription.take();
} }
}) } else {
.detach(); update_subscription.take();
}
})
.detach();
updater updater
}); });
cx.set_global(GlobalAutoUpdate(Some(auto_updater))); cx.set_global(GlobalAutoUpdate(Some(auto_updater)));
}
} }
pub fn check(_: &Check, cx: &mut WindowContext) { pub fn check(_: &Check, cx: &mut WindowContext) {

View File

@ -329,6 +329,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
fn init_test(cx: &mut AppContext) -> Model<ChannelStore> { fn init_test(cx: &mut AppContext) -> Model<ChannelStore> {
let settings_store = SettingsStore::test(cx); let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store); cx.set_global(settings_store);
release_channel::init("0.0.0", cx);
client::init_settings(cx); client::init_settings(cx);
let http = FakeHttpClient::with_404_response(); let http = FakeHttpClient::with_404_response();

View File

@ -15,14 +15,13 @@ use futures::{
TryFutureExt as _, TryStreamExt, TryFutureExt as _, TryStreamExt,
}; };
use gpui::{ use gpui::{
actions, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, Global, Model, SemanticVersion, actions, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, Global, Model, Task, WeakModel,
Task, WeakModel,
}; };
use lazy_static::lazy_static; use lazy_static::lazy_static;
use parking_lot::RwLock; use parking_lot::RwLock;
use postage::watch; use postage::watch;
use rand::prelude::*; use rand::prelude::*;
use release_channel::ReleaseChannel; use release_channel::{AppVersion, ReleaseChannel};
use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, PeerId, RequestMessage}; use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, PeerId, RequestMessage};
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; 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") pub static ref ADMIN_API_TOKEN: Option<String> = std::env::var("ZED_ADMIN_API_TOKEN")
.ok() .ok()
.and_then(|s| if s.is_empty() { None } else { Some(s) }); .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> = pub static ref ZED_APP_PATH: Option<PathBuf> =
std::env::var("ZED_APP_PATH").ok().map(PathBuf::from); std::env::var("ZED_APP_PATH").ok().map(PathBuf::from);
pub static ref ZED_ALWAYS_ACTIVE: bool = pub static ref ZED_ALWAYS_ACTIVE: bool =
@ -1011,13 +1007,22 @@ impl Client {
.update(|cx| ReleaseChannel::try_global(cx)) .update(|cx| ReleaseChannel::try_global(cx))
.ok() .ok()
.flatten(); .flatten();
let app_version = cx
.update(|cx| AppVersion::global(cx).to_string())
.ok()
.unwrap_or_default();
let request = Request::builder() let request = Request::builder()
.header( .header(
"Authorization", "Authorization",
format!("{} {}", credentials.user_id, credentials.access_token), 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(); let http = self.http.clone();
cx.background_executor().spawn(async move { cx.background_executor().spawn(async move {

View File

@ -61,6 +61,7 @@ util = { path = "../util" }
uuid.workspace = true uuid.workspace = true
[dev-dependencies] [dev-dependencies]
release_channel = { path = "../release_channel" }
async-trait.workspace = true async-trait.workspace = true
audio = { path = "../audio" } audio = { path = "../audio" }
call = { path = "../call", features = ["test-support"] } call = { path = "../call", features = ["test-support"] }

View File

@ -64,6 +64,7 @@ use time::OffsetDateTime;
use tokio::sync::{watch, Semaphore}; use tokio::sync::{watch, Semaphore};
use tower::ServiceBuilder; use tower::ServiceBuilder;
use tracing::{field, info_span, instrument, Instrument}; use tracing::{field, info_span, instrument, Instrument};
use util::SemanticVersion;
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30); pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(10); pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(10);
@ -795,6 +796,7 @@ fn broadcast<F>(
lazy_static! { lazy_static! {
static ref ZED_PROTOCOL_VERSION: HeaderName = HeaderName::from_static("x-zed-protocol-version"); 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); 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> { pub fn routes(server: Arc<Server>) -> Router<Body> {
Router::new() Router::new()
.route("/rpc", get(handle_websocket_request)) .route("/rpc", get(handle_websocket_request))
@ -838,6 +866,7 @@ pub fn routes(server: Arc<Server>) -> Router<Body> {
pub async fn handle_websocket_request( pub async fn handle_websocket_request(
TypedHeader(ProtocolVersion(protocol_version)): TypedHeader<ProtocolVersion>, TypedHeader(ProtocolVersion(protocol_version)): TypedHeader<ProtocolVersion>,
_app_version_header: Option<TypedHeader<AppVersionHeader>>,
ConnectInfo(socket_address): ConnectInfo<SocketAddr>, ConnectInfo(socket_address): ConnectInfo<SocketAddr>,
Extension(server): Extension<Arc<Server>>, Extension(server): Extension<Arc<Server>>,
Extension(user): Extension<User>, Extension(user): Extension<User>,
@ -851,6 +880,7 @@ pub async fn handle_websocket_request(
) )
.into_response(); .into_response();
} }
let socket_address = socket_address.to_string(); let socket_address = socket_address.to_string();
ws.on_upgrade(move |socket| { ws.on_upgrade(move |socket| {
use util::ResultExt; use util::ResultExt;

View File

@ -153,6 +153,7 @@ impl TestServer {
} }
let settings = SettingsStore::test(cx); let settings = SettingsStore::test(cx);
cx.set_global(settings); cx.set_global(settings);
release_channel::init("0.0.0", cx);
client::init_settings(cx); client::init_settings(cx);
}); });

View File

@ -1,14 +1,13 @@
use client::ZED_APP_VERSION;
use gpui::AppContext; use gpui::AppContext;
use human_bytes::human_bytes; use human_bytes::human_bytes;
use release_channel::ReleaseChannel; use release_channel::{AppVersion, ReleaseChannel};
use serde::Serialize; use serde::Serialize;
use std::{env, fmt::Display}; use std::{env, fmt::Display};
use sysinfo::{RefreshKind, System, SystemExt}; use sysinfo::{RefreshKind, System, SystemExt};
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize)]
pub struct SystemSpecs { pub struct SystemSpecs {
app_version: Option<String>, app_version: String,
release_channel: &'static str, release_channel: &'static str,
os_name: &'static str, os_name: &'static str,
os_version: Option<String>, os_version: Option<String>,
@ -18,9 +17,7 @@ pub struct SystemSpecs {
impl SystemSpecs { impl SystemSpecs {
pub fn new(cx: &AppContext) -> Self { pub fn new(cx: &AppContext) -> Self {
let app_version = ZED_APP_VERSION let app_version = AppVersion::global(cx).to_string();
.or_else(|| cx.app_metadata().app_version)
.map(|v| v.to_string());
let release_channel = ReleaseChannel::global(cx).display_name(); let release_channel = ReleaseChannel::global(cx).display_name();
let os_name = cx.app_metadata().os_name; let os_name = cx.app_metadata().os_name;
let system = System::new_with_specifics(RefreshKind::new().with_memory()); 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), Some(os_version) => format!("OS: {} {}", self.os_name, os_version),
None => format!("OS: {}", self.os_name), None => format!("OS: {}", self.os_name),
}; };
let app_version_information = self let app_version_information =
.app_version format!("Zed: v{} ({})", self.app_version, self.release_channel);
.as_ref()
.map(|app_version| format!("Zed: v{} ({})", app_version, self.release_channel));
let system_specs = [ let system_specs = [
app_version_information, app_version_information,
Some(os_information), os_information,
Some(format!("Memory: {}", human_bytes(self.memory as f64))), format!("Memory: {}", human_bytes(self.memory as f64)),
Some(format!("Architecture: {}", self.architecture)), format!("Architecture: {}", self.architecture),
] ]
.into_iter() .into_iter()
.flatten()
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join("\n"); .join("\n");

View File

@ -11,7 +11,7 @@ use crate::{
Pixels, PlatformInput, Point, RenderGlyphParams, RenderImageParams, RenderSvgParams, Scene, Pixels, PlatformInput, Point, RenderGlyphParams, RenderImageParams, RenderSvgParams, Scene,
SharedString, Size, Task, TaskLabel, WindowContext, SharedString, Size, Task, TaskLabel, WindowContext,
}; };
use anyhow::{anyhow, Result}; use anyhow::Result;
use async_task::Runnable; use async_task::Runnable;
use futures::channel::oneshot; use futures::channel::oneshot;
use parking::Unparker; use parking::Unparker;
@ -23,11 +23,10 @@ use std::hash::{Hash, Hasher};
use std::time::Duration; use std::time::Duration;
use std::{ use std::{
any::Any, any::Any,
fmt::{self, Debug, Display}, fmt::{self, Debug},
ops::Range, ops::Range,
path::{Path, PathBuf}, path::{Path, PathBuf},
rc::Rc, rc::Rc,
str::FromStr,
sync::Arc, sync::Arc,
}; };
use uuid::Uuid; use uuid::Uuid;
@ -39,6 +38,7 @@ pub(crate) use mac::*;
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
pub(crate) use test::*; pub(crate) use test::*;
use time::UtcOffset; use time::UtcOffset;
pub use util::SemanticVersion;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub(crate) fn current_platform() -> Rc<dyn Platform> { 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 /// A clipboard item that should be copied to the clipboard
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub struct ClipboardItem { pub struct ClipboardItem {

View File

@ -1,9 +1,9 @@
use gpui::{AppContext, Global}; use gpui::{AppContext, Global, SemanticVersion};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use std::env; use std::env;
#[doc(hidden)] #[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(|| { Lazy::new(|| {
env::var("ZED_RELEASE_CHANNEL") env::var("ZED_RELEASE_CHANNEL")
.unwrap_or_else(|_| include_str!("../../zed/RELEASE_CHANNEL").trim().to_string()) .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 { } else {
Lazy::new(|| include_str!("../../zed/RELEASE_CHANNEL").trim().to_string()) Lazy::new(|| include_str!("../../zed/RELEASE_CHANNEL").trim().to_string())
}; };
#[doc(hidden)] #[doc(hidden)]
pub static RELEASE_CHANNEL: Lazy<ReleaseChannel> = pub static RELEASE_CHANNEL: Lazy<ReleaseChannel> =
Lazy::new(|| match RELEASE_CHANNEL_NAME.as_str() { 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)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
pub enum ReleaseChannel { pub enum ReleaseChannel {
#[default] #[default]
@ -52,11 +76,12 @@ struct GlobalReleaseChannel(ReleaseChannel);
impl Global for GlobalReleaseChannel {} impl Global for GlobalReleaseChannel {}
impl ReleaseChannel { pub fn init(pkg_version: &str, cx: &mut AppContext) {
pub fn init(cx: &mut AppContext) { AppVersion::init(pkg_version, cx);
cx.set_global(GlobalReleaseChannel(*RELEASE_CHANNEL)) cx.set_global(GlobalReleaseChannel(*RELEASE_CHANNEL))
} }
impl ReleaseChannel {
pub fn global(cx: &AppContext) -> Self { pub fn global(cx: &AppContext) -> Self {
cx.global::<GlobalReleaseChannel>().0 cx.global::<GlobalReleaseChannel>().0
} }

View File

@ -210,7 +210,7 @@ impl SettingsStore {
if let Some(release_settings) = &self if let Some(release_settings) = &self
.raw_user_settings .raw_user_settings
.get(&*release_channel::RELEASE_CHANNEL_NAME) .get(&*release_channel::RELEASE_CHANNEL.dev_name())
{ {
if let Some(release_settings) = setting_value if let Some(release_settings) = setting_value
.deserialize_setting(&release_settings) .deserialize_setting(&release_settings)
@ -543,7 +543,7 @@ impl SettingsStore {
if let Some(release_settings) = &self if let Some(release_settings) = &self
.raw_user_settings .raw_user_settings
.get(&*release_channel::RELEASE_CHANNEL_NAME) .get(&*release_channel::RELEASE_CHANNEL.dev_name())
{ {
if let Some(release_settings) = setting_value if let Some(release_settings) = setting_value
.deserialize_setting(&release_settings) .deserialize_setting(&release_settings)

View File

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

View File

@ -3,6 +3,7 @@ pub mod fs;
pub mod github; pub mod github;
pub mod http; pub mod http;
pub mod paths; pub mod paths;
mod semantic_version;
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
pub mod test; pub mod test;
@ -10,6 +11,7 @@ pub use backtrace::Backtrace;
use futures::Future; use futures::Future;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use rand::{seq::SliceRandom, Rng}; use rand::{seq::SliceRandom, Rng};
pub use semantic_version::SemanticVersion;
use std::{ use std::{
borrow::Cow, borrow::Cow,
cmp::{self, Ordering}, cmp::{self, Ordering},

View File

@ -120,7 +120,7 @@ fn main() {
}); });
app.run(move |cx| { 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") { if let Some(build_sha) = option_env!("ZED_COMMIT_SHA") {
AppCommitSha::set_global(AppCommitSha(build_sha.into()), cx); 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); std::process::exit(-1);
} }
let app_version = client::ZED_APP_VERSION let app_version = if let Some(version) = app_metadata.app_version {
.or(app_metadata.app_version) version.to_string()
.map_or("dev".to_string(), |v| v.to_string()); } else {
option_env!("CARGO_PKG_VERSION")
.unwrap_or("dev")
.to_string()
};
let backtrace = Backtrace::new(); let backtrace = Backtrace::new();
let mut backtrace = backtrace 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(), file: location.file().into(),
line: location.line(), line: location.line(),
}), }),
app_version: app_version.clone(), app_version: app_version.to_string(),
release_channel: RELEASE_CHANNEL.display_name().into(), release_channel: RELEASE_CHANNEL.display_name().into(),
os_name: app_metadata.os_name.into(), os_name: app_metadata.os_name.into(),
os_version: app_metadata os_version: app_metadata