feat(plugin): harden V4 host integration

Preserve legacy wire indices and route concurrent V4 responses and background requests by correlation and stable tab ID.
This commit is contained in:
Hakan Seven 2026-08-15 20:17:43 +03:00
commit f247d15f6e
110 changed files with 32160 additions and 1255 deletions

View file

@ -14,12 +14,7 @@ serde = { version = "1", features = ["derive"] }
# Pulled in only by the `host` feature, which adds the `acadrust`-typed
# `HostApi` runtime surface. The default crate stays dependency-free so engine
# crates and external tooling can depend on the manifest/ribbon contract cheaply.
#
# acadrust types appear in the HostApi trait object, so the plugin and host
# MUST resolve the exact same acadrust source. The root Cargo.toml pins the
# exact revision via [patch.crates-io]; this crate uses the same version
# requirement so the patch applies consistently across the workspace.
acadrust = { version = "0.4", optional = true, features = ["serde"] }
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "c28b2cd", optional = true, features = ["serde"] }
# Runtime IPC and serialization (host feature only).
interprocess = { version = "2", optional = true }
@ -42,9 +37,7 @@ serde_json = "1"
serde = { version = "1", features = ["derive"] }
cargo-lock = "11"
# acadrust is scanned at build time to generate the embedded type registry.
# Use the same version requirement as the root Cargo.toml so the
# [patch.crates-io] override applies here too.
acadrust = { version = "0.4", features = ["serde"] }
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "c28b2cd", features = ["serde"] }
[dev-dependencies]
serde_json = "1"

View file

@ -18,7 +18,10 @@ fn main() {
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
generate_type_registry(&out_dir);
generate_version_info(&out_dir);
println!("cargo:rerun-if-changed=Cargo.lock");
println!(
"cargo:rerun-if-changed={}",
workspace_cargo_lock_path().display()
);
}
// ════════════════════════════════════════════════════════════════════════════

View file

@ -256,10 +256,6 @@ impl<'de> Deserialize<'de> for PluginNotification {
}
}
/// An add-on package's entry point: its manifest, optional ribbon tab, and
/// command dispatch. Built-in (in-tree) and dynamically-loaded (cdylib) plugins
/// implement the same trait from this crate, so an out-of-tree add-on targets
/// the stable contract rather than the host binary.
/// An add-on package's entry point: its manifest, optional ribbon tab, and
/// command dispatch. Built-in (in-tree) and dynamically-loaded (cdylib) plugins
/// implement the same trait from this crate, so an out-of-tree add-on targets

View file

@ -143,6 +143,25 @@ mod tests {
}
}
#[test]
fn v4_additions_preserve_legacy_discriminants() {
fn discriminant<T: serde::Serialize>(value: &T) -> u32 {
let bytes = bincode::serialize(value).unwrap();
u32::from_le_bytes(bytes[..4].try_into().unwrap())
}
assert_eq!(discriminant(&HostRequest::Shutdown), 6);
assert_eq!(discriminant(&HostResponse::Error(String::new())), 5);
assert_eq!(
discriminant(&crate::ipc::protocol::PluginRequest::BumpGeometry),
6
);
assert_eq!(
discriminant(&crate::ipc::protocol::PluginResponse::Record(None)),
3
);
}
#[test]
fn transport_rejects_oversized_message() {
let (mut a, _b) = connect_pair();

View file

@ -59,13 +59,13 @@ pub enum HostRequest {
NeedsEntityPick {
command_id: u64,
},
Shutdown,
ExecuteCode {
command_id: u64,
source: CommandSource,
code: String,
tab_index: usize,
},
Shutdown,
}
/// Responses the plugin runner sends back for `HostRequest`.
@ -76,8 +76,8 @@ pub enum HostResponse {
Text(String),
Ribbon(Vec<OwnedRibbonGroup>),
Manifest(OwnedPluginManifest),
CodeExecutionResult(crate::host::ExecutionResult),
Error(String),
CodeExecutionResult(crate::host::ExecutionResult),
}
/// Requests the plugin runner sends to the host.
@ -87,8 +87,6 @@ pub enum PluginRequest {
PushOutput(String),
PushError(String),
AddEntity(EntityType),
/// Add multiple entities in a single request.
AddEntities(Vec<EntityType>),
/// Replace the existing entity carrying this entity's handle in place.
UpdateEntity(EntityType),
/// Delete the entity with `handle`.
@ -119,6 +117,8 @@ pub enum PluginRequest {
/// Ask the host to create/refresh a shared-memory document view and return
/// the file path + current version.
OpenDocumentView,
/// Add multiple entities in a single request.
AddEntities(Vec<EntityType>),
/// V4: ask the host to create/refresh a tab-keyed shared-memory document
/// view and return the file path + current version.
OpenDocumentViewV4 { tab_id: u64 },
@ -134,7 +134,6 @@ pub enum PluginResponse {
Ok,
Bool(bool),
Handle(Handle),
Handles(Vec<Handle>),
Record(Option<ExtendedDataRecord>),
Document(Box<CadDocument>),
Error(String),
@ -143,6 +142,7 @@ pub enum PluginResponse {
path: String,
version: u64,
},
Handles(Vec<Handle>),
/// V4: path to the tab-keyed memory-mapped file and current version.
DocumentViewV4 {
path: String,

View file

@ -11,7 +11,7 @@
//! formats or enum variants.
use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
use std::net::{Shutdown, TcpListener, TcpStream};
use std::sync::Arc;
use std::sync::Mutex;
use std::time::Duration;
@ -82,7 +82,7 @@ pub fn recv_framed<R: Read, T: serde::de::DeserializeOwned>(reader: &mut R) -> R
/// Thread-safe [`PluginRequestSender`] implementation that forwards requests
/// over a single TCP stream to a request proxy server.
pub struct ProxyPluginRequestSender {
stream: Mutex<TcpStream>,
stream: Arc<Mutex<TcpStream>>,
}
impl ProxyPluginRequestSender {
@ -98,21 +98,14 @@ impl ProxyPluginRequestSender {
stream.flush()?;
stream.set_read_timeout(Some(REQUEST_TIMEOUT))?;
Ok(Self {
stream: Mutex::new(stream),
stream: Arc::new(Mutex::new(stream)),
})
}
/// Create a second handle to the same underlying socket so multiple
/// callers can share the proxy connection (requests are still serialized
/// by the mutex on each handle).
/// Create a second handle that shares the connection and its request lock.
pub fn try_clone(&self) -> Result<Self, ProxyError> {
let stream = self
.stream
.lock()
.map_err(|e| ProxyError::Io(std::io::Error::other(e.to_string())))?
.try_clone()?;
Ok(Self {
stream: Mutex::new(stream),
stream: Arc::clone(&self.stream),
})
}
@ -145,6 +138,7 @@ impl ProxyPluginRequestSender {
|| e.kind() == ErrorKind::TimedOut =>
{
if let Err(e) = poll() {
let _ = stream.shutdown(Shutdown::Both);
break Err(e);
}
}

View file

@ -43,7 +43,7 @@ type NotificationQueue = mpsc::Receiver<(Option<u64>, HostNotification)>;
struct Shared {
writer: Mutex<Stream>,
next_id: AtomicU64,
in_flight: Mutex<Option<mpsc::Sender<(u64, PluginResponse)>>>,
in_flight: Mutex<HashMap<u64, mpsc::Sender<PluginResponse>>>,
}
/// A frame the V4 reader thread delivers to the plugin runner loop.
@ -96,7 +96,7 @@ impl V4Client {
let shared = Arc::new(Shared {
writer: Mutex::new(stream),
next_id: AtomicU64::new(1),
in_flight: Mutex::new(None),
in_flight: Mutex::new(HashMap::new()),
});
let shared_for_reader = Arc::clone(&shared);
@ -122,6 +122,15 @@ impl V4Client {
self.runner_queue.recv()
}
/// Wait up to `timeout` for a host request so the runner can also drain
/// notifications while otherwise idle.
pub fn recv_runner_frame_timeout(
&self,
timeout: Duration,
) -> Result<RunnerFrame, mpsc::RecvTimeoutError> {
self.runner_queue.recv_timeout(timeout)
}
/// Non-blocking poll for host-to-plugin notifications.
pub fn try_recv_notification(&self) -> Option<(Option<u64>, HostNotification)> {
self.notifications.lock().unwrap_or_else(|e| e.into_inner()).try_recv().ok()
@ -214,8 +223,13 @@ fn reader_thread(
loop {
match recv::<HostToPluginV4>(&mut reader) {
Ok(HostToPluginV4::Response { id, payload }) => {
if let Some(tx) = shared.in_flight.lock().unwrap_or_else(|e| e.into_inner()).take() {
let _ = tx.send((id, payload));
let tx = shared
.in_flight
.lock()
.unwrap_or_else(|e| e.into_inner())
.remove(&id);
if let Some(tx) = tx {
let _ = tx.send(payload);
} else {
eprintln!("[plugin] V4 reader: unexpected response id={id}");
}
@ -249,8 +263,11 @@ fn reader_thread(
}
}
}));
// Ensure any synchronous waiter is unblocked.
let _ = shared.in_flight.lock().unwrap_or_else(|e| e.into_inner()).take();
shared
.in_flight
.lock()
.unwrap_or_else(|e| e.into_inner())
.clear();
}
/// `HostApi` implementation used inside the plugin process over the V4
@ -264,7 +281,7 @@ pub struct V4PluginHostApi {
next_command_id: Cell<u64>,
record_cache: RefCell<HashMap<(Handle, String), &'static ExtendedDataRecord>>,
doc_view: RefCell<Option<DocumentViewInfo>>,
doc_view_v4: RefCell<Option<DocumentViewInfo>>,
doc_view_v4: RefCell<Option<(u64, DocumentViewInfo)>>,
tab_id_cache: Cell<Option<u64>>,
}
@ -293,7 +310,7 @@ impl V4PluginHostApi {
&self,
req: PluginRequest,
) -> Result<PluginResponse, crate::ipc::transport::TransportError> {
send_plugin_request(&self.shared, req)
send_plugin_request(&self.shared, self.tab_id_cache.get(), req)
}
fn fetch_document(&self) -> CadDocument {
@ -316,23 +333,37 @@ impl V4PluginHostApi {
/// thread-safe [`V4PluginRequestSender`].
fn send_plugin_request(
shared: &Arc<Shared>,
tab_id: Option<u64>,
req: PluginRequest,
) -> Result<PluginResponse, crate::ipc::transport::TransportError> {
let id = shared.next_id.fetch_add(1, Ordering::Relaxed);
let (tx, rx) = mpsc::channel();
*shared.in_flight.lock().unwrap_or_else(|e| e.into_inner()) = Some(tx);
{
shared
.in_flight
.lock()
.unwrap_or_else(|e| e.into_inner())
.insert(id, tx);
let send_result = {
let mut writer = shared.writer.lock().unwrap_or_else(|e| e.into_inner());
send(&mut writer, &PluginToHostV4::Request { id, payload: req })?;
send(
&mut writer,
&PluginToHostV4::Request {
id,
tab_id,
payload: req,
},
)
};
if let Err(error) = send_result {
shared
.in_flight
.lock()
.unwrap_or_else(|e| e.into_inner())
.remove(&id);
return Err(error);
}
match rx.recv() {
Ok((resp_id, resp)) if resp_id == id => Ok(resp),
Ok((resp_id, _)) => Err(crate::ipc::transport::TransportError::Io(
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("V4 response id mismatch: expected {id}, got {resp_id}"),
),
)),
Ok(resp) => Ok(resp),
Err(_) => Err(crate::ipc::transport::TransportError::Io(
std::io::Error::new(
std::io::ErrorKind::NotConnected,
@ -343,17 +374,16 @@ fn send_plugin_request(
}
/// Thread-safe wrapper around the V4 connection so worker threads can issue
/// host requests. A per-sender mutex serializes requests, matching the
/// single-in-flight behavior of [`V4PluginHostApi::request`].
/// host requests for the originating document tab.
struct V4PluginRequestSender {
shared: Arc<Shared>,
lock: Mutex<()>,
tab_id: u64,
}
impl PluginRequestSender for V4PluginRequestSender {
fn request(&self, req: PluginRequest) -> Result<PluginResponse, PluginRequestError> {
let _guard = self.lock.lock().map_err(|e| PluginRequestError(e.to_string()))?;
send_plugin_request(&self.shared, req).map_err(|e| PluginRequestError(e.to_string()))
send_plugin_request(&self.shared, Some(self.tab_id), req)
.map_err(|e| PluginRequestError(e.to_string()))
}
}
@ -631,7 +661,7 @@ impl HostApi for V4PluginHostApi {
fn plugin_request_sender(&self) -> Option<Box<dyn PluginRequestSender>> {
Some(Box::new(V4PluginRequestSender {
shared: Arc::clone(&self.shared),
lock: Mutex::new(()),
tab_id: self.tab_id(),
}))
}
@ -655,14 +685,13 @@ impl HostApi for V4PluginHostApi {
}
}
fn document_view_v4(&mut self, _tab_id: u64) -> Option<DocumentViewInfo> {
fn document_view_v4(&mut self, tab_id: u64) -> Option<DocumentViewInfo> {
{
let mut view = self.doc_view_v4.borrow_mut();
if view.is_none() {
let tab_id = self.tab_id();
if view.as_ref().map(|(cached_id, _)| *cached_id) != Some(tab_id) {
match self.request(PluginRequest::OpenDocumentViewV4 { tab_id }) {
Ok(PluginResponse::DocumentViewV4 { path, version }) => {
*view = Some(DocumentViewInfo { path, version });
*view = Some((tab_id, DocumentViewInfo { path, version }));
}
Ok(other) => {
eprintln!("[plugin] unexpected OpenDocumentViewV4 response: {other:?}");
@ -673,13 +702,18 @@ impl HostApi for V4PluginHostApi {
}
}
}
self.doc_view_v4.borrow().clone()
self.doc_view_v4
.borrow()
.as_ref()
.map(|(_, info)| info.clone())
}
fn close_document_view_v4(&mut self, _tab_id: u64) {
let tab_id = self.tab_id();
fn close_document_view_v4(&mut self, tab_id: u64) {
let _ = self.request(PluginRequest::CloseDocumentViewV4 { tab_id });
self.doc_view_v4.borrow_mut().take();
let mut view = self.doc_view_v4.borrow_mut();
if view.as_ref().map(|(cached_id, _)| *cached_id) == Some(tab_id) {
view.take();
}
}
}
@ -694,7 +728,7 @@ impl V4Client {
let shared = Arc::new(Shared {
writer: Mutex::new(stream),
next_id: AtomicU64::new(1),
in_flight: Mutex::new(None),
in_flight: Mutex::new(HashMap::new()),
});
let shared_for_reader = Arc::clone(&shared);
let notifications = Arc::new(Mutex::new(notify_rx));
@ -776,7 +810,11 @@ mod tests {
let runner = thread::spawn(move || {
let req = recv::<PluginToHostV4>(&mut runner_stream).unwrap();
match req {
PluginToHostV4::Request { id, payload: PluginRequest::PushInfo(s) } => {
PluginToHostV4::Request {
id,
tab_id: _,
payload: PluginRequest::PushInfo(s),
} => {
assert_eq!(s, "hello host");
send(
&mut runner_stream,
@ -804,7 +842,11 @@ mod tests {
let runner = thread::spawn(move || {
let req = recv::<PluginToHostV4>(&mut runner_stream).unwrap();
match req {
PluginToHostV4::Request { id, payload: PluginRequest::AddEntities(v) } => {
PluginToHostV4::Request {
id,
tab_id: _,
payload: PluginRequest::AddEntities(v),
} => {
assert_eq!(v.len(), 2);
send(
&mut runner_stream,
@ -828,6 +870,56 @@ mod tests {
runner.join().unwrap();
}
#[test]
fn concurrent_requests_match_out_of_order_responses() {
let (host_stream, mut runner_stream) = connect_pair();
let client = V4Client::from_stream_for_test(host_stream);
let sender = Arc::new(V4PluginRequestSender {
shared: Arc::clone(&client.shared),
tab_id: 42,
});
let runner = thread::spawn(move || {
let mut requests = Vec::new();
for _ in 0..2 {
match recv::<PluginToHostV4>(&mut runner_stream).unwrap() {
PluginToHostV4::Request {
id,
tab_id: Some(42),
payload: PluginRequest::PushInfo(message),
} => requests.push((id, message)),
other => panic!("unexpected: {other:?}"),
}
}
for (id, message) in requests.into_iter().rev() {
send(
&mut runner_stream,
&HostToPluginV4::Response {
id,
payload: PluginResponse::Bool(message == "first"),
},
)
.unwrap();
}
});
let first_sender = Arc::clone(&sender);
let first = thread::spawn(move || {
first_sender
.request(PluginRequest::PushInfo("first".to_string()))
.unwrap()
});
let second = thread::spawn(move || {
sender
.request(PluginRequest::PushInfo("second".to_string()))
.unwrap()
});
assert!(matches!(first.join().unwrap(), PluginResponse::Bool(true)));
assert!(matches!(second.join().unwrap(), PluginResponse::Bool(false)));
runner.join().unwrap();
}
#[test]
fn v4_client_notification_queue_size_env_is_honored() {
let _guard = ENV_LOCK.lock().unwrap();

View file

@ -29,7 +29,11 @@ pub enum HostToPluginV4 {
#[derive(Debug, Serialize, Deserialize)]
#[allow(clippy::large_enum_variant)]
pub enum PluginToHostV4 {
Request { id: u64, payload: PluginRequest },
Request {
id: u64,
tab_id: Option<u64>,
payload: PluginRequest,
},
Response { id: u64, payload: HostResponse },
Notification(NotificationEnvelope<PluginNotification>),
}

View file

@ -27,7 +27,11 @@ macro_rules! vlog {
/// A frame delivered from the V4 reader thread to the host main thread.
pub enum HostIncoming {
Response { id: u64, payload: crate::ipc::protocol::HostResponse },
Request { id: u64, payload: Box<crate::ipc::protocol::PluginRequest> },
Request {
id: u64,
tab_id: Option<u64>,
payload: Box<crate::ipc::protocol::PluginRequest>,
},
}
/// Token-bucket rate limiter.
@ -117,9 +121,17 @@ pub fn run_host_reader_thread(
}
}
}
Ok(PluginToHostV4::Request { id, payload }) => {
Ok(PluginToHostV4::Request {
id,
tab_id,
payload,
}) => {
if incoming
.send(HostIncoming::Request { id, payload: Box::new(payload) })
.send(HostIncoming::Request {
id,
tab_id,
payload: Box::new(payload),
})
.is_err()
{
break;

View file

@ -346,20 +346,20 @@ impl PluginProcess {
}
Ok(Err(e)) => return Err(e.into()),
Err(mpsc::RecvTimeoutError::Timeout) => {
let status = spawn_failure_status(&child, &last_stderr);
if let Some(child) = child.lock().unwrap_or_else(|e| e.into_inner()).take() {
reap(child);
}
let status = spawn_failure_status(&child, &last_stderr);
return Err(PluginError::RunnerCrashed {
request: "spawn/accept",
status,
});
}
Err(mpsc::RecvTimeoutError::Disconnected) => {
let status = spawn_failure_status(&child, &last_stderr);
if let Some(child) = child.lock().unwrap_or_else(|e| e.into_inner()).take() {
reap(child);
}
let status = spawn_failure_status(&child, &last_stderr);
return Err(PluginError::RunnerCrashed {
request: "spawn/accept",
status,
@ -740,6 +740,7 @@ impl PluginProcess {
}
std::thread::sleep(Duration::from_millis(50));
}
reap(child);
}
None
}

View file

@ -218,8 +218,10 @@ impl PluginManager {
return false;
};
let plugin = self.plugins.remove(index);
plugin.process.shutdown_and_wait(Duration::from_secs(5));
true
plugin
.process
.shutdown_and_wait(Duration::from_secs(5))
.is_some()
}
/// Begin asynchronous shutdown of every plugin process.

View file

@ -1,14 +1,15 @@
//! Private V4 connection owned by [`PluginProcess`](crate::process::PluginProcess).
use std::collections::VecDeque;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{mpsc, Arc, Mutex};
use std::sync::{mpsc, Arc, Mutex, TryLockError};
use std::time::{Duration, Instant};
use interprocess::local_socket::Stream;
use interprocess::TryClone;
use crate::host::{CommandSource, ExecutionResult, HostApi, HostNotification, PluginNotification};
use crate::ipc::protocol::{HostRequest, HostResponse};
use crate::ipc::protocol::{HostRequest, HostResponse, PluginRequest};
use crate::ipc::server::handle_plugin_request;
use crate::ipc::transport::send;
use crate::ipc::v4::protocol::{HostToPluginV4, NotificationEnvelope};
@ -77,6 +78,8 @@ fn request_kind(req: &HostRequest) -> &'static str {
pub(crate) struct V4Connection {
shared: Arc<V4HostShared>,
incoming: Mutex<mpsc::Receiver<HostIncoming>>,
deferred: Mutex<VecDeque<(u64, Option<u64>, Box<PluginRequest>)>>,
call_lock: Mutex<()>,
next_id: AtomicU64,
reader_handle: Mutex<Option<std::thread::JoinHandle<()>>>,
}
@ -117,6 +120,8 @@ impl V4Connection {
Ok(Self {
shared,
incoming: Mutex::new(incoming_rx),
deferred: Mutex::new(VecDeque::new()),
call_lock: Mutex::new(()),
next_id: AtomicU64::new(1),
reader_handle: Mutex::new(Some(handle)),
})
@ -157,6 +162,7 @@ impl V4Connection {
req: HostRequest,
on_start_interactive: &mut dyn FnMut(u64),
) -> Result<HostResponse, PluginError> {
let _call_guard = self.call_lock.lock().unwrap_or_else(|e| e.into_inner());
let id = self.next_id.fetch_add(1, Ordering::Relaxed);
let kind = request_kind(&req);
let timeout = request_timeout(kind);
@ -188,16 +194,24 @@ impl V4Connection {
Ok(HostIncoming::Response { payload, .. }) => {
return Err(PluginError::UnexpectedResponse(Box::new(payload)))
}
Ok(HostIncoming::Request { id: rid, payload }) => {
let resp = handle_plugin_request(host, *payload, on_start_interactive);
let mut writer = self.shared.writer.lock().unwrap_or_else(|e| e.into_inner());
let stream = writer.as_mut().ok_or_else(|| {
PluginError::Io(std::io::Error::new(
std::io::ErrorKind::NotConnected,
"V4 connection is shut down",
))
})?;
send(stream, &HostToPluginV4::Response { id: rid, payload: resp })?;
Ok(HostIncoming::Request {
id: rid,
tab_id,
payload,
}) => {
if tab_id.map_or(true, |request_tab| request_tab == host.tab_id()) {
self.respond_to_plugin_request(
host,
rid,
payload,
on_start_interactive,
)?;
} else {
self.deferred
.lock()
.unwrap_or_else(|e| e.into_inner())
.push_back((rid, tab_id, payload));
}
}
Err(mpsc::RecvTimeoutError::Timeout) => {
self.shared.alive.store(false, Ordering::SeqCst);
@ -226,19 +240,50 @@ impl V4Connection {
host: &mut dyn HostApi,
on_start_interactive: &mut dyn FnMut(u64),
) -> Result<(), PluginError> {
let _call_guard = match self.call_lock.try_lock() {
Ok(guard) => guard,
Err(TryLockError::Poisoned(error)) => error.into_inner(),
Err(TryLockError::WouldBlock) => return Ok(()),
};
let current_tab_id = host.tab_id();
let mut ready = VecDeque::new();
{
let mut deferred = self.deferred.lock().unwrap_or_else(|e| e.into_inner());
let mut waiting = VecDeque::new();
while let Some((id, tab_id, payload)) = deferred.pop_front() {
if tab_id.map_or(true, |request_tab| request_tab == current_tab_id) {
ready.push_back((id, payload));
} else {
waiting.push_back((id, tab_id, payload));
}
}
*deferred = waiting;
}
while let Some((id, payload)) = ready.pop_front() {
self.respond_to_plugin_request(host, id, payload, on_start_interactive)?;
}
let incoming = self.incoming.lock().unwrap_or_else(|e| e.into_inner());
loop {
match incoming.try_recv() {
Ok(HostIncoming::Request { id, payload }) => {
let resp = handle_plugin_request(host, *payload, on_start_interactive);
let mut writer = self.shared.writer.lock().unwrap_or_else(|e| e.into_inner());
let stream = writer.as_mut().ok_or_else(|| {
PluginError::Io(std::io::Error::new(
std::io::ErrorKind::NotConnected,
"V4 connection is shut down",
))
})?;
send(stream, &HostToPluginV4::Response { id, payload: resp })?;
Ok(HostIncoming::Request {
id,
tab_id,
payload,
}) => {
if tab_id.map_or(true, |request_tab| request_tab == current_tab_id) {
self.respond_to_plugin_request(
host,
id,
payload,
on_start_interactive,
)?;
} else {
self.deferred
.lock()
.unwrap_or_else(|e| e.into_inner())
.push_back((id, tab_id, payload));
}
}
Ok(HostIncoming::Response { .. }) => {
// Responses without a matching active call should not
@ -256,6 +301,31 @@ impl V4Connection {
}
}
fn respond_to_plugin_request(
&self,
host: &mut dyn HostApi,
id: u64,
payload: Box<PluginRequest>,
on_start_interactive: &mut dyn FnMut(u64),
) -> Result<(), PluginError> {
let response = handle_plugin_request(host, *payload, on_start_interactive);
let mut writer = self.shared.writer.lock().unwrap_or_else(|e| e.into_inner());
let stream = writer.as_mut().ok_or_else(|| {
PluginError::Io(std::io::Error::new(
std::io::ErrorKind::NotConnected,
"V4 connection is shut down",
))
})?;
send(
stream,
&HostToPluginV4::Response {
id,
payload: response,
},
)?;
Ok(())
}
/// Send an `ExecuteCode` request and block until the plugin returns the
/// result. The active document tab index is included so the REPL session is
/// tied to the tab that issued it.
@ -532,6 +602,7 @@ mod tests {
&mut runner_stream,
&PluginToHostV4::Request {
id: 99,
tab_id: None,
payload: PluginRequest::PushInfo("nested".to_string()),
},
)

View file

@ -89,16 +89,14 @@ fn run_v4(
}));
}
match client.recv_runner_frame() {
match client.recv_runner_frame_timeout(std::time::Duration::from_millis(50)) {
Ok(RunnerFrame::Request { id, payload }) => {
if let Some(resp) = handle_host_request_v4(&mut *plugin, interactive, &client, id, payload) {
client.send_response(id, resp)?;
}
}
Err(_) => {
// Host disconnected.
break;
}
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => continue,
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break,
}
}
Ok(())