Merge remote-tracking branch 'origin/main' into HEAD

This commit is contained in:
Hakan Seven committed 2026-08-29 15:58:18 +03:00
commit 1c52a2afcf
121 files changed
+946 -538

No files matched your search

+8
View File
@@ -299,6 +299,14 @@ pub trait BuiltinPlugin: Send + Sync {
/// the V4 runner guarantees it is only called for plugins that report
/// API major 4 or newer.
fn on_notification(&mut self, _command_id: Option<u64>, _notification: HostNotification) {}
/// Lifecycle callback invoked once after the runner connects. The plugin
/// receives the active document tab's `HostApi` so it can set up worker
/// threads or cache `plugin_request_sender()` before any user command runs.
///
/// Added in API v5. The runner only calls this for plugins that report API
/// major 5 or newer.
fn on_load(&mut self, _host: &mut dyn HostApi) {}
}
/// A point-driven interactive command a plugin starts via
+5 -5
View File
@@ -17,8 +17,9 @@
/// `document()` / `document_mut()` to local cached copies for out-of-process
/// plugins and appends `document_reader` / `document_view` at the end of the
/// vtable so API v2 plugins keep working. v4 adds full-duplex notifications on
/// a multiplexed socket while leaving the V2/V3 ABI and protocol untouched.
pub const API_VERSION: u32 = 4;
/// a multiplexed socket while leaving the V2/V3 ABI and protocol untouched. v5
/// adds the `BuiltinPlugin::on_load` lifecycle callback.
pub const API_VERSION: u32 = 5;
/// Oldest plugin API major the current host still loads. This keeps previously
/// compiled cdylibs usable as long as their vtable layout is a prefix of the
@@ -26,9 +27,8 @@ pub const API_VERSION: u32 = 4;
pub const API_VERSION_MIN_SUPPORTED: u32 = 2;
/// Environment variable that caps the API major accepted by the host at
/// runtime. Set to `3` to disable V4 plugins while keeping V2/V3 plugins
/// working; set to `2` for V2-only mode. The same mechanism will apply to
/// future API majors.
/// runtime. Set to `4` to disable V5 plugins, `3` to disable V4, or `2` for
/// V2-only mode.
pub const MAX_API_VERSION_ENV: &str = "OCS_PLUGIN_MAX_API_VERSION";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+18
View File
@@ -56,15 +56,33 @@ pub fn run(socket_name: &str, cdylib_path: &Path) -> Result<(), Box<dyn std::err
if version >= 4 {
let client = V4Client::connect_handshake(socket_name, &token)?;
if version >= 5 {
invoke_on_load(&mut *plugin, &client, &interactive)?;
}
run_v4(&mut *plugin, &interactive, client)
} else {
let client = IpcClient::connect(socket_name)?;
eprintln!("[runner] connected to host");
client.send_handshake(&token)?;
// V2/V3 plugins receive HostApi only during dispatch, so on_load is not
// meaningful there; skip it to keep the old protocol unchanged.
run_v3(&*plugin, &interactive, client)
}
}
fn invoke_on_load(
plugin: &mut dyn BuiltinPlugin,
client: &V4Client,
interactive: &InteractiveRegistry,
) -> Result<(), Box<dyn std::error::Error>> {
let mut proxy = client.plugin_host_api(0, interactive.clone());
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
plugin.on_load(&mut proxy)
}))
.map_err(|_| std::io::Error::other("plugin on_load() panicked"))?;
Ok(())
}
fn run_v3(
plugin: &dyn BuiltinPlugin,
interactive: &InteractiveRegistry,
+75 -1
View File
@@ -11,6 +11,12 @@
//! `acadrust` source with its own via [`acadrust_sources_compatible`]. Two
//! sources are considered compatible only when they resolve to the same 40
//! character git commit hash.
//!
//! The rustc version string is also recorded because Rust has no stable ABI:
//! a plugin built with a different compiler than the host can pass simple calls
//! and then crash the runner on compound types. For API v4 and later the host
//! can compare the plugin's declared rustc with its own via
//! [`rustc_versions_compatible`].
use std::sync::OnceLock;
@@ -26,6 +32,7 @@ pub fn get_embedded_version_info_json() -> &'static str {
#[derive(Debug, Clone)]
struct VersionInfo {
acadrust_source: String,
rustc_version: String,
}
pub const ACADRUST_GATE_API_VERSION: u32 = 4;
@@ -48,6 +55,8 @@ fn parsed_version_info() -> &'static VersionInfo {
INFO.get_or_init(|| VersionInfo {
acadrust_source: json_string_value(EMBEDDED_VERSION_INFO_JSON, "acadrust_source")
.unwrap_or_default(),
rustc_version: json_string_value(EMBEDDED_VERSION_INFO_JSON, "rustc_version")
.unwrap_or_default(),
})
}
@@ -56,6 +65,30 @@ pub fn host_acadrust_source() -> &'static str {
&parsed_version_info().acadrust_source
}
/// Returns the host's `rustc --version` output.
pub fn host_rustc_version() -> &'static str {
&parsed_version_info().rustc_version
}
/// Returns whether two `rustc --version` outputs are considered compatible.
///
/// The full version line (e.g. "rustc 1.98.0 (hash date)") is compared
/// token-by-token after normalising whitespace. This is intentionally strict:
/// Rust has no stable ABI, so even the same nominal compiler built in different
/// environments can produce incompatible layouts.
pub fn rustc_versions_compatible(a: &str, b: &str) -> bool {
if a.is_empty() || b.is_empty() {
return false;
}
let a_tokens: Vec<&str> = a.split_whitespace().collect();
let b_tokens: Vec<&str> = b.split_whitespace().collect();
a_tokens.len() == b_tokens.len()
&& a_tokens
.iter()
.zip(b_tokens)
.all(|(a, b)| a.eq_ignore_ascii_case(b))
}
/// Extracts the full git commit hash from a Cargo source.
pub fn acadrust_source_hash(source: &str) -> Option<&str> {
let hash = source.rsplit('#').next()?;
@@ -85,6 +118,7 @@ mod tests {
let value: serde_json::Value = serde_json::from_str(json).expect("valid JSON");
assert!(value.get("ocs_version").is_some());
assert!(value.get("acadrust_source").is_some());
assert!(value.get("rustc_version").is_some());
}
#[test]
@@ -96,6 +130,7 @@ mod tests {
"ocs_plugin_api_version",
"acadrust_version",
"acadrust_source",
"rustc_version",
"api_version",
"api_version_min_supported",
"build_timestamp",
@@ -112,7 +147,7 @@ mod tests {
value["ocs_plugin_api_version"].as_str(),
Some(env!("CARGO_PKG_VERSION"))
);
assert_eq!(value["api_version"].as_i64(), Some(4));
assert_eq!(value["api_version"].as_i64(), Some(5));
assert_eq!(value["api_version_min_supported"].as_i64(), Some(2));
// acadrust_version should have a patch component (e.g. "0.4.0").
@@ -125,6 +160,45 @@ mod tests {
assert!(!host_acadrust_source().is_empty());
}
#[test]
fn host_rustc_version_is_non_empty() {
assert!(!host_rustc_version().is_empty());
}
#[test]
fn rustc_version_comparison_is_strict() {
assert!(rustc_versions_compatible(
"rustc 1.98.0 (abc123 2026-01-01)",
"rustc 1.98.0 (abc123 2026-01-01)"
));
assert!(!rustc_versions_compatible(
"rustc 1.96.0 (def456 2025-11-01)",
"rustc 1.98.0 (abc123 2026-01-01)"
));
}
#[test]
fn rustc_version_comparison_normalises_whitespace() {
assert!(rustc_versions_compatible(
"rustc 1.98.0 (abc123 2026-01-01)",
"rustc 1.98.0 (abc123 2026-01-01)"
));
}
#[test]
fn rustc_version_comparison_rejects_empty() {
assert!(!rustc_versions_compatible("", "rustc 1.98.0"));
assert!(!rustc_versions_compatible("rustc 1.98.0", ""));
assert!(!rustc_versions_compatible("", ""));
}
#[test]
fn rustc_version_comparison_rejects_collapsed_tokens() {
// A concatenated version must not accidentally match a spaced one.
assert!(!rustc_versions_compatible("rustc 1.98.0", "rustc1.98.0"));
assert!(!rustc_versions_compatible("rustc1.98.0", "rustc 1.98.0"));
}
#[test]
fn dependency_gate_starts_at_api_v4() {
assert!(!uses_acadrust_gate(3));