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:
commit
f247d15f6e
110 changed files with 32160 additions and 1255 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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>),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
|
|
|
|||
Loading…
Reference in a new issue