forked from 1b-agent/opencadstudio-upstream
Snapshot: V4 plugin API and host integration
This commit is contained in:
1 parent
448869ac35
commit
29c41a4041
36 files changed
+5463
-253
No files matched your search
@@ -0,0 +1,164 @@
|
||||
//! Host-side V4 IPC reader and dispatcher helpers.
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{mpsc, Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use interprocess::local_socket::Stream;
|
||||
|
||||
use crate::host::PluginNotification;
|
||||
use crate::ipc::transport::{recv, TransportError};
|
||||
use crate::ipc::v4::protocol::PluginToHostV4;
|
||||
|
||||
/// Default plugin→host notification rate limit (notifications per second).
|
||||
const DEFAULT_NOTIFY_RATE_LIMIT: f64 = 1000.0;
|
||||
|
||||
/// Minimum interval between verbose drop logs to avoid spam.
|
||||
const DROP_LOG_INTERVAL: Duration = Duration::from_secs(1);
|
||||
|
||||
macro_rules! vlog {
|
||||
($($arg:tt)*) => {{
|
||||
if crate::process::verbose() {
|
||||
eprintln!($($arg)*);
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
/// 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> },
|
||||
}
|
||||
|
||||
/// Token-bucket rate limiter.
|
||||
pub struct RateLimiter {
|
||||
rate: f64,
|
||||
tokens: f64,
|
||||
last: Instant,
|
||||
}
|
||||
|
||||
impl RateLimiter {
|
||||
pub fn new(rate_per_second: f64) -> Self {
|
||||
Self {
|
||||
rate: rate_per_second.max(0.0),
|
||||
tokens: rate_per_second,
|
||||
last: Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn allow(&mut self) -> bool {
|
||||
if self.rate <= 0.0 {
|
||||
return false;
|
||||
}
|
||||
let now = Instant::now();
|
||||
let elapsed = now.duration_since(self.last).as_secs_f64();
|
||||
self.tokens = (self.tokens + elapsed * self.rate).min(self.rate);
|
||||
self.last = now;
|
||||
if self.tokens >= 1.0 {
|
||||
self.tokens -= 1.0;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_notify_rate_limit() -> f64 {
|
||||
std::env::var("OCS_PLUGIN_NOTIFY_RATE_LIMIT")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.map(|n: f64| n.max(0.0))
|
||||
.unwrap_or(DEFAULT_NOTIFY_RATE_LIMIT)
|
||||
}
|
||||
|
||||
/// State shared between the host main thread and the V4 reader thread.
|
||||
pub struct V4HostShared {
|
||||
pub writer: Mutex<Option<Stream>>,
|
||||
pub alive: AtomicBool,
|
||||
}
|
||||
|
||||
/// Run the host-side V4 reader thread until the socket closes or a fatal error
|
||||
/// occurs. Responses and requests are forwarded to `incoming`; notifications
|
||||
/// are dispatched immediately to `handler` subject to `rate_limiter`.
|
||||
pub fn run_host_reader_thread(
|
||||
mut reader: Stream,
|
||||
shared: Arc<V4HostShared>,
|
||||
incoming: mpsc::Sender<HostIncoming>,
|
||||
handler: Arc<dyn Fn(Option<u64>, PluginNotification) + Send + Sync>,
|
||||
mut rate_limiter: RateLimiter,
|
||||
) {
|
||||
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
let mut last_drop_log: Option<Instant> = None;
|
||||
loop {
|
||||
match recv::<PluginToHostV4>(&mut reader) {
|
||||
Ok(PluginToHostV4::Response { id, payload }) => {
|
||||
if incoming
|
||||
.send(HostIncoming::Response { id, payload })
|
||||
.is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(PluginToHostV4::Notification(envelope)) => {
|
||||
if rate_limiter.allow() {
|
||||
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
(handler)(envelope.command_id, envelope.payload)
|
||||
}));
|
||||
} else {
|
||||
let now = Instant::now();
|
||||
let should_log = last_drop_log
|
||||
.map(|t| now.duration_since(t) >= DROP_LOG_INTERVAL)
|
||||
.unwrap_or(true);
|
||||
if should_log && crate::process::verbose() {
|
||||
eprintln!(
|
||||
"[plugin] plugin→host notification rate limit exceeded; dropping"
|
||||
);
|
||||
last_drop_log = Some(now);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(PluginToHostV4::Request { id, payload }) => {
|
||||
if incoming
|
||||
.send(HostIncoming::Request { id, payload: Box::new(payload) })
|
||||
.is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(TransportError::Disconnected) => {
|
||||
// Peer closed the connection; this is the normal shutdown
|
||||
// path, so don't log it as an error.
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
vlog!("[plugin] V4 host reader error: {e}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
shared.alive.store(false, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "host"))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn rate_limiter_allows_burst_then_throttles() {
|
||||
let mut limiter = RateLimiter::new(10.0);
|
||||
for _ in 0..10 {
|
||||
assert!(limiter.allow());
|
||||
}
|
||||
// Immediately after the burst we should be out of tokens.
|
||||
assert!(!limiter.allow());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rate_limiter_refills_over_time() {
|
||||
let mut limiter = RateLimiter::new(100.0);
|
||||
assert!(limiter.allow());
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
assert!(limiter.allow());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user