From 585e8671e34d5516083939b9c839ea6a0335a4ce Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 20 Mar 2024 14:33:26 -0700 Subject: [PATCH] Add a schema to extensions, to prevent installing extensions on too old of a Zed version (#9599) Release Notes: - N/A --------- Co-authored-by: Marshall --- Cargo.lock | 3 +- crates/client/src/telemetry.rs | 6 +- .../20221109000000_test_schema.sql | 1 + ...320124800_add_extension_schema_version.sql | 2 + crates/collab/src/api/extensions.rs | 20 +++---- crates/collab/src/db.rs | 1 + crates/collab/src/db/queries/extensions.rs | 18 +++--- .../collab/src/db/tables/extension_version.rs | 1 + crates/collab/src/db/tests/extension_tests.rs | 29 ++++++++-- crates/extension/Cargo.toml | 1 + crates/extension/src/extension_manifest.rs | 1 + crates/extension/src/extension_store.rs | 44 ++++++++++----- crates/extension/src/extension_store_test.rs | 3 + crates/extension_api/Cargo.toml | 6 +- crates/extension_api/README.md | 56 +++++++++++++++++++ crates/extension_cli/src/main.rs | 1 + crates/live_kit_client/src/prod.rs | 2 +- crates/rpc/src/extension.rs | 1 + crates/util/src/http.rs | 7 ++- crates/zed/src/main.rs | 4 +- extensions/gleam/extension.toml | 1 + extensions/uiua/extension.toml | 1 + 22 files changed, 165 insertions(+), 44 deletions(-) create mode 100644 crates/collab/migrations/20240320124800_add_extension_schema_version.sql create mode 100644 crates/extension_api/README.md diff --git a/Cargo.lock b/Cargo.lock index 2368e4467c..98e8d966a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3482,6 +3482,7 @@ dependencies = [ "settings", "theme", "toml 0.8.10", + "url", "util", "wasm-encoder", "wasmparser", @@ -12630,7 +12631,7 @@ dependencies = [ [[package]] name = "zed_extension_api" -version = "0.1.0" +version = "0.0.1" dependencies = [ "wit-bindgen", ] diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index ad370253fd..7811fa4e14 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -470,7 +470,11 @@ impl Telemetry { let request = http::Request::builder() .method(Method::POST) - .uri(this.http_client.build_zed_api_url("/telemetry/events")) + .uri( + this.http_client + .build_zed_api_url("/telemetry/events", &[])? + .as_ref(), + ) .header("Content-Type", "text/plain") .header("x-zed-checksum", checksum) .body(json_bytes.into()); diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index b66767a640..29758d7eb1 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -373,6 +373,7 @@ CREATE TABLE extension_versions ( authors TEXT NOT NULL, repository TEXT NOT NULL, description TEXT NOT NULL, + schema_version INTEGER NOT NULL DEFAULT 0, download_count INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (extension_id, version) ); diff --git a/crates/collab/migrations/20240320124800_add_extension_schema_version.sql b/crates/collab/migrations/20240320124800_add_extension_schema_version.sql new file mode 100644 index 0000000000..75fd0f40e4 --- /dev/null +++ b/crates/collab/migrations/20240320124800_add_extension_schema_version.sql @@ -0,0 +1,2 @@ +-- Add migration script here +ALTER TABLE extension_versions ADD COLUMN schema_version INTEGER NOT NULL DEFAULT 0; diff --git a/crates/collab/src/api/extensions.rs b/crates/collab/src/api/extensions.rs index 9e9038d886..c586a23ccb 100644 --- a/crates/collab/src/api/extensions.rs +++ b/crates/collab/src/api/extensions.rs @@ -12,6 +12,7 @@ use axum::{ Extension, Json, Router, }; use collections::HashMap; +use rpc::ExtensionApiManifest; use serde::{Deserialize, Serialize}; use std::{sync::Arc, time::Duration}; use time::PrimitiveDateTime; @@ -33,6 +34,8 @@ pub fn router() -> Router { #[derive(Debug, Deserialize)] struct GetExtensionsParams { filter: Option, + #[serde(default)] + max_schema_version: i32, } #[derive(Debug, Deserialize)] @@ -51,20 +54,14 @@ struct GetExtensionsResponse { pub data: Vec, } -#[derive(Deserialize)] -struct ExtensionManifest { - name: String, - version: String, - description: Option, - authors: Vec, - repository: String, -} - async fn get_extensions( Extension(app): Extension>, Query(params): Query, ) -> Result> { - let extensions = app.db.get_extensions(params.filter.as_deref(), 500).await?; + let extensions = app + .db + .get_extensions(params.filter.as_deref(), params.max_schema_version, 500) + .await?; Ok(Json(GetExtensionsResponse { data: extensions })) } @@ -267,7 +264,7 @@ async fn fetch_extension_manifest( })? .to_vec(); let manifest = - serde_json::from_slice::(&manifest_bytes).with_context(|| { + serde_json::from_slice::(&manifest_bytes).with_context(|| { format!( "invalid manifest for extension {extension_id} version {version}: {}", String::from_utf8_lossy(&manifest_bytes) @@ -287,6 +284,7 @@ async fn fetch_extension_manifest( description: manifest.description.unwrap_or_default(), authors: manifest.authors, repository: manifest.repository, + schema_version: manifest.schema_version.unwrap_or(0), published_at, }) } diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 4cab73c79e..dea287eb6a 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -731,6 +731,7 @@ pub struct NewExtensionVersion { pub description: String, pub authors: Vec, pub repository: String, + pub schema_version: i32, pub published_at: PrimitiveDateTime, } diff --git a/crates/collab/src/db/queries/extensions.rs b/crates/collab/src/db/queries/extensions.rs index 8022e8c833..065b5800ab 100644 --- a/crates/collab/src/db/queries/extensions.rs +++ b/crates/collab/src/db/queries/extensions.rs @@ -4,27 +4,28 @@ impl Database { pub async fn get_extensions( &self, filter: Option<&str>, + max_schema_version: i32, limit: usize, ) -> Result> { self.transaction(|tx| async move { - let mut condition = Condition::all(); + let mut condition = Condition::all().add( + extension::Column::LatestVersion + .into_expr() + .eq(extension_version::Column::Version.into_expr()), + ); if let Some(filter) = filter { let fuzzy_name_filter = Self::fuzzy_like_string(filter); condition = condition.add(Expr::cust_with_expr("name ILIKE $1", fuzzy_name_filter)); } let extensions = extension::Entity::find() + .inner_join(extension_version::Entity) + .select_also(extension_version::Entity) .filter(condition) + .filter(extension_version::Column::SchemaVersion.lte(max_schema_version)) .order_by_desc(extension::Column::TotalDownloadCount) .order_by_asc(extension::Column::Name) .limit(Some(limit as u64)) - .filter( - extension::Column::LatestVersion - .into_expr() - .eq(extension_version::Column::Version.into_expr()), - ) - .inner_join(extension_version::Entity) - .select_also(extension_version::Entity) .all(&*tx) .await?; @@ -170,6 +171,7 @@ impl Database { authors: ActiveValue::Set(version.authors.join(", ")), repository: ActiveValue::Set(version.repository.clone()), description: ActiveValue::Set(version.description.clone()), + schema_version: ActiveValue::Set(version.schema_version), download_count: ActiveValue::NotSet, } })) diff --git a/crates/collab/src/db/tables/extension_version.rs b/crates/collab/src/db/tables/extension_version.rs index 459f2296b1..b3f6565ac5 100644 --- a/crates/collab/src/db/tables/extension_version.rs +++ b/crates/collab/src/db/tables/extension_version.rs @@ -13,6 +13,7 @@ pub struct Model { pub authors: String, pub repository: String, pub description: String, + pub schema_version: i32, pub download_count: i64, } diff --git a/crates/collab/src/db/tests/extension_tests.rs b/crates/collab/src/db/tests/extension_tests.rs index 8844a68480..4a8af2d652 100644 --- a/crates/collab/src/db/tests/extension_tests.rs +++ b/crates/collab/src/db/tests/extension_tests.rs @@ -16,7 +16,7 @@ async fn test_extensions(db: &Arc) { let versions = db.get_known_extension_versions().await.unwrap(); assert!(versions.is_empty()); - let extensions = db.get_extensions(None, 5).await.unwrap(); + let extensions = db.get_extensions(None, 1, 5).await.unwrap(); assert!(extensions.is_empty()); let t0 = OffsetDateTime::from_unix_timestamp_nanos(0).unwrap(); @@ -33,6 +33,7 @@ async fn test_extensions(db: &Arc) { description: "an extension".into(), authors: vec!["max".into()], repository: "ext1/repo".into(), + schema_version: 1, published_at: t0, }, NewExtensionVersion { @@ -41,6 +42,7 @@ async fn test_extensions(db: &Arc) { description: "a good extension".into(), authors: vec!["max".into(), "marshall".into()], repository: "ext1/repo".into(), + schema_version: 1, published_at: t0, }, ], @@ -53,6 +55,7 @@ async fn test_extensions(db: &Arc) { description: "a great extension".into(), authors: vec!["marshall".into()], repository: "ext2/repo".into(), + schema_version: 0, published_at: t0, }], ), @@ -75,7 +78,7 @@ async fn test_extensions(db: &Arc) { ); // The latest version of each extension is returned. - let extensions = db.get_extensions(None, 5).await.unwrap(); + let extensions = db.get_extensions(None, 1, 5).await.unwrap(); assert_eq!( extensions, &[ @@ -102,6 +105,22 @@ async fn test_extensions(db: &Arc) { ] ); + // Extensions with too new of a schema version are excluded. + let extensions = db.get_extensions(None, 0, 5).await.unwrap(); + assert_eq!( + extensions, + &[ExtensionMetadata { + id: "ext2".into(), + name: "Extension Two".into(), + version: "0.2.0".into(), + authors: vec!["marshall".into()], + description: "a great extension".into(), + repository: "ext2/repo".into(), + published_at: t0, + download_count: 0 + },] + ); + // Record extensions being downloaded. for _ in 0..7 { assert!(db.record_extension_download("ext2", "0.0.2").await.unwrap()); @@ -122,7 +141,7 @@ async fn test_extensions(db: &Arc) { .unwrap()); // Extensions are returned in descending order of total downloads. - let extensions = db.get_extensions(None, 5).await.unwrap(); + let extensions = db.get_extensions(None, 1, 5).await.unwrap(); assert_eq!( extensions, &[ @@ -161,6 +180,7 @@ async fn test_extensions(db: &Arc) { description: "a real good extension".into(), authors: vec!["max".into(), "marshall".into()], repository: "ext1/repo".into(), + schema_version: 1, published_at: t0, }], ), @@ -172,6 +192,7 @@ async fn test_extensions(db: &Arc) { description: "an old extension".into(), authors: vec!["marshall".into()], repository: "ext2/repo".into(), + schema_version: 0, published_at: t0, }], ), @@ -196,7 +217,7 @@ async fn test_extensions(db: &Arc) { .collect() ); - let extensions = db.get_extensions(None, 5).await.unwrap(); + let extensions = db.get_extensions(None, 1, 5).await.unwrap(); assert_eq!( extensions, &[ diff --git a/crates/extension/Cargo.toml b/crates/extension/Cargo.toml index 51df62068e..9282836cf5 100644 --- a/crates/extension/Cargo.toml +++ b/crates/extension/Cargo.toml @@ -37,6 +37,7 @@ serde_json.workspace = true settings.workspace = true theme.workspace = true toml.workspace = true +url.workspace = true util.workspace = true wasm-encoder.workspace = true wasmtime.workspace = true diff --git a/crates/extension/src/extension_manifest.rs b/crates/extension/src/extension_manifest.rs index f237e2ad7b..bf2bd45fb5 100644 --- a/crates/extension/src/extension_manifest.rs +++ b/crates/extension/src/extension_manifest.rs @@ -29,6 +29,7 @@ pub struct ExtensionManifest { pub id: Arc, pub name: String, pub version: Arc, + pub schema_version: i32, #[serde(default)] pub description: Option, diff --git a/crates/extension/src/extension_store.rs b/crates/extension/src/extension_store.rs index 7d0d278461..7fe17402bc 100644 --- a/crates/extension/src/extension_store.rs +++ b/crates/extension/src/extension_store.rs @@ -35,6 +35,7 @@ use std::{ time::{Duration, Instant}, }; use theme::{ThemeRegistry, ThemeSettings}; +use url::Url; use util::{ http::{AsyncBody, HttpClient, HttpClientWithUrl}, paths::EXTENSIONS_DIR, @@ -49,6 +50,8 @@ pub use extension_manifest::{ const RELOAD_DEBOUNCE_DURATION: Duration = Duration::from_millis(200); const FS_WATCH_LATENCY: Duration = Duration::from_millis(100); +const CURRENT_SCHEMA_VERSION: i64 = 1; + #[derive(Deserialize)] pub struct ExtensionsApiResponse { pub data: Vec, @@ -377,15 +380,18 @@ impl ExtensionStore { search: Option<&str>, cx: &mut ModelContext, ) -> Task>> { - let url = self.http_client.build_zed_api_url(&format!( - "/extensions{query}", - query = search - .map(|search| format!("?filter={search}")) - .unwrap_or_default() - )); + let version = CURRENT_SCHEMA_VERSION.to_string(); + let mut query = vec![("max_schema_version", version.as_str())]; + if let Some(search) = search { + query.push(("filter", search)); + } + + let url = self.http_client.build_zed_api_url("/extensions", &query); let http_client = self.http_client.clone(); cx.spawn(move |_, _| async move { - let mut response = http_client.get(&url, AsyncBody::empty(), true).await?; + let mut response = http_client + .get(&url?.as_ref(), AsyncBody::empty(), true) + .await?; let mut body = Vec::new(); response @@ -420,7 +426,7 @@ impl ExtensionStore { fn install_or_upgrade_extension_at_endpoint( &mut self, extension_id: Arc, - url: String, + url: Url, operation: ExtensionOperation, cx: &mut ModelContext, ) { @@ -447,7 +453,7 @@ impl ExtensionStore { }); let mut response = http_client - .get(&url, Default::default(), true) + .get(&url.as_ref(), Default::default(), true) .await .map_err(|err| anyhow!("error downloading extension: {}", err))?; let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut())); @@ -482,9 +488,13 @@ impl ExtensionStore { ) { log::info!("installing extension {extension_id} latest version"); - let url = self + let Some(url) = self .http_client - .build_zed_api_url(&format!("/extensions/{extension_id}/download")); + .build_zed_api_url(&format!("/extensions/{extension_id}/download"), &[]) + .log_err() + else { + return; + }; self.install_or_upgrade_extension_at_endpoint( extension_id, @@ -511,9 +521,16 @@ impl ExtensionStore { cx: &mut ModelContext, ) { log::info!("installing extension {extension_id} {version}"); - let url = self + let Some(url) = self .http_client - .build_zed_api_url(&format!("/extensions/{extension_id}/{version}/download")); + .build_zed_api_url( + &format!("/extensions/{extension_id}/{version}/download"), + &[], + ) + .log_err() + else { + return; + }; self.install_or_upgrade_extension_at_endpoint(extension_id, url, operation, cx); } @@ -1104,6 +1121,7 @@ fn manifest_from_old_manifest( description: manifest_json.description, repository: manifest_json.repository, authors: manifest_json.authors, + schema_version: 0, lib: Default::default(), themes: { let mut themes = manifest_json.themes.into_values().collect::>(); diff --git a/crates/extension/src/extension_store_test.rs b/crates/extension/src/extension_store_test.rs index 6755332524..c0bd037c5b 100644 --- a/crates/extension/src/extension_store_test.rs +++ b/crates/extension/src/extension_store_test.rs @@ -145,6 +145,7 @@ async fn test_extension_store(cx: &mut TestAppContext) { id: "zed-ruby".into(), name: "Zed Ruby".into(), version: "1.0.0".into(), + schema_version: 0, description: None, authors: Vec::new(), repository: None, @@ -169,6 +170,7 @@ async fn test_extension_store(cx: &mut TestAppContext) { id: "zed-monokai".into(), name: "Zed Monokai".into(), version: "2.0.0".into(), + schema_version: 0, description: None, authors: vec![], repository: None, @@ -324,6 +326,7 @@ async fn test_extension_store(cx: &mut TestAppContext) { id: "zed-gruvbox".into(), name: "Zed Gruvbox".into(), version: "1.0.0".into(), + schema_version: 0, description: None, authors: vec![], repository: None, diff --git a/crates/extension_api/Cargo.toml b/crates/extension_api/Cargo.toml index 84d2064eb5..cd9a790cad 100644 --- a/crates/extension_api/Cargo.toml +++ b/crates/extension_api/Cargo.toml @@ -1,6 +1,10 @@ [package] name = "zed_extension_api" -version = "0.1.0" +version = "0.0.1" +description = "APIs for creating Zed extensions in Rust" +repository = "https://github.com/zed-industries/zed" +documentation = "https://docs.rs/zed_extension_api" +keywords = ["zed", "extension"] edition = "2021" license = "Apache-2.0" diff --git a/crates/extension_api/README.md b/crates/extension_api/README.md new file mode 100644 index 0000000000..d5a61cde2c --- /dev/null +++ b/crates/extension_api/README.md @@ -0,0 +1,56 @@ +# The Zed Rust Extension API + +This crate lets you write extensions for Zed in Rust. + +## Extension Manifest + +You'll need an `extension.toml` file at the root of your extension directory, with the following structure: + +```toml +id = "my-extension" +name = "My Extension" +description = "..." +version = "0.0.1" +schema_version = 1 +authors = ["Your Name "] +repository = "https://github.com/your/extension-repository" +``` + +## Cargo metadata + +Zed extensions are packaged as WebAssembly files. In your Cargo.toml, you'll +need to set your `crate-type` accordingly: + +```toml +[dependencies] +zed_extension_api = "0.0.1" + +[lib] +crate-type = ["cdylib"] +``` + +## Implementing an Extension + +To define your extension, create a type that implements the `Extension` trait, and register it. + +```rust +use zed_extension_api as zed; + +struct MyExtension { + // ... state +} + +impl zed::Extension for MyExtension { + // ... +} + +zed::register_extension!(MyExtension); +``` + +## Testing your extension + +To run your extension in Zed as you're developing it: + +- Open the extensions view using the `zed: extensions` action in the command palette. +- Click the `Add Dev Extension` button in the top right +- Choose the path to your extension directory. diff --git a/crates/extension_cli/src/main.rs b/crates/extension_cli/src/main.rs index b3884dce9c..8191a5d63b 100644 --- a/crates/extension_cli/src/main.rs +++ b/crates/extension_cli/src/main.rs @@ -94,6 +94,7 @@ async fn main() -> Result<()> { version: manifest.version.to_string(), description: manifest.description, authors: manifest.authors, + schema_version: Some(manifest.schema_version), repository: manifest .repository .ok_or_else(|| anyhow!("missing repository in extension manifest"))?, diff --git a/crates/live_kit_client/src/prod.rs b/crates/live_kit_client/src/prod.rs index a4bd9d4f07..f7452da710 100644 --- a/crates/live_kit_client/src/prod.rs +++ b/crates/live_kit_client/src/prod.rs @@ -684,7 +684,7 @@ impl Drop for RoomDelegate { fn drop(&mut self) { unsafe { CFRelease(self.native_delegate.0); - let _ = Weak::from_raw(self.weak_room); + let _ = Weak::from_raw(self.weak_room as *mut Room); } } } diff --git a/crates/rpc/src/extension.rs b/crates/rpc/src/extension.rs index 59176b976f..01915225b2 100644 --- a/crates/rpc/src/extension.rs +++ b/crates/rpc/src/extension.rs @@ -7,4 +7,5 @@ pub struct ExtensionApiManifest { pub description: Option, pub authors: Vec, pub repository: String, + pub schema_version: Option, } diff --git a/crates/util/src/http.rs b/crates/util/src/http.rs index 6dfde7833b..3ee7b96eac 100644 --- a/crates/util/src/http.rs +++ b/crates/util/src/http.rs @@ -54,7 +54,7 @@ impl HttpClientWithUrl { } /// Builds a Zed API URL using the given path. - pub fn build_zed_api_url(&self, path: &str) -> String { + pub fn build_zed_api_url(&self, path: &str, query: &[(&str, &str)]) -> Result { let base_url = self.base_url(); let base_api_url = match base_url.as_ref() { "https://zed.dev" => "https://api.zed.dev", @@ -63,7 +63,10 @@ impl HttpClientWithUrl { other => other, }; - format!("{}{}", base_api_url, path) + Ok(Url::parse_with_params( + &format!("{}{}", base_api_url, path), + query, + )?) } } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index ce0cdaf77f..bb6a2ca6e8 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -783,7 +783,7 @@ async fn upload_previous_crashes( .unwrap_or("zed-2024-01-17-221900.ips".to_string()); // don't upload old crash reports from before we had this. let mut uploaded = last_uploaded.clone(); - let crash_report_url = http.build_zed_api_url("/telemetry/crashes"); + let crash_report_url = http.build_zed_api_url("/telemetry/crashes", &[])?; for dir in [&*CRASHES_DIR, &*CRASHES_RETIRED_DIR] { let mut children = smol::fs::read_dir(&dir).await?; @@ -809,7 +809,7 @@ async fn upload_previous_crashes( .await .context("error reading crash file")?; - let mut request = Request::post(&crash_report_url) + let mut request = Request::post(&crash_report_url.to_string()) .redirect_policy(isahc::config::RedirectPolicy::Follow) .header("Content-Type", "text/plain"); diff --git a/extensions/gleam/extension.toml b/extensions/gleam/extension.toml index 76684f9504..c4812cd3d9 100644 --- a/extensions/gleam/extension.toml +++ b/extensions/gleam/extension.toml @@ -2,6 +2,7 @@ id = "gleam" name = "Gleam" description = "Gleam support for Zed" version = "0.0.1" +schema_version = 1 authors = ["Marshall Bowers "] repository = "https://github.com/zed-industries/zed" diff --git a/extensions/uiua/extension.toml b/extensions/uiua/extension.toml index 9241483ac2..eddd10f402 100644 --- a/extensions/uiua/extension.toml +++ b/extensions/uiua/extension.toml @@ -2,6 +2,7 @@ id = "uiua" name = "Uiua" description = "Uiua support for Zed" version = "0.0.1" +schema_version = 1 authors = ["Max Brunsfeld "] repository = "https://github.com/zed-industries/zed"