Merge remote-tracking branch 'origin/main' into plugin/host-model-api

# Conflicts:
#	crates/ocs_plugin_api/src/process/v4.rs
#	src/app/update/mod.rs
This commit is contained in:
Felix Riestra committed 2026-09-19 23:58:01 +02:00
commit 8648225ac5
309 files changed
+81166 -4157

No files matched your search

+66 -74
View File
@@ -193,7 +193,7 @@ fn call_timeout() -> Duration {
/// Per-request-kind timeout floors. The user-configured default is raised to
/// these minima so that no request kind can be configured into an unsafe value.
fn request_timeout(kind: &'static str) -> Duration {
pub(crate) fn request_timeout(kind: &'static str) -> Duration {
base_max_floor(call_timeout(), kind)
}
@@ -228,7 +228,7 @@ fn base_max_floor(base: Duration, kind: &'static str) -> Duration {
base.max(floor)
}
fn request_kind(req: &HostRequest) -> &'static str {
pub(crate) fn request_kind(req: &HostRequest) -> &'static str {
match req {
HostRequest::GetManifest => "GetManifest",
HostRequest::GetRibbon => "GetRibbon",
@@ -554,6 +554,43 @@ impl PluginProcess {
}
}
/// Legacy (non-V4) round trip for simple interactive calls: send `req`,
/// then loop until the matching response arrives, answering any nested
/// plugin request with an error. `kind` drives the timeout floor;
/// `nested_what` names the operation in that error; `extract` pulls the
/// expected payload out of the response.
fn call_simple<T>(
&self,
req: HostRequest,
kind: &'static str,
nested_what: &'static str,
extract: impl Fn(HostResponse) -> Result<T, Box<HostResponse>>,
) -> Result<T, PluginError> {
self.send_request(req)?;
let timeout = request_timeout(kind);
let deadline = Instant::now() + timeout;
loop {
match recv_with_deadline::<PluginToHost>(
&self.stream,
&self.child,
deadline,
timeout,
kind,
)? {
PluginToHost::Response(resp) => match extract(resp) {
Ok(v) => return Ok(v),
Err(other) => return Err(PluginError::UnexpectedResponse(other)),
},
PluginToHost::Request(req) => {
let resp = crate::ipc::protocol::PluginResponse::Error(format!(
"unexpected nested request during {nested_what}: {req:?}"
));
self.send_response(resp)?;
}
}
}
}
/// Send an interactive event for `command_id` and return the step the
/// plugin command produces. Interactive events are not expected to trigger
/// nested host API calls, so this path does not supply a `HostApi`.
@@ -573,30 +610,15 @@ impl PluginProcess {
other => Err(PluginError::UnexpectedResponse(Box::new(other))),
}
} else {
self.send_request(HostRequest::InteractiveEvent { command_id, event })?;
let kind = "InteractiveEvent";
let timeout = request_timeout(kind);
let deadline = Instant::now() + timeout;
loop {
match recv_with_deadline::<PluginToHost>(
&self.stream,
&self.child,
deadline,
timeout,
kind,
)? {
PluginToHost::Response(HostResponse::CommandStep(s)) => return Ok(*s),
PluginToHost::Response(other) => {
return Err(PluginError::UnexpectedResponse(Box::new(other)))
}
PluginToHost::Request(req) => {
let resp = crate::ipc::protocol::PluginResponse::Error(format!(
"unexpected nested request during interactive event: {req:?}"
));
self.send_response(resp)?;
}
}
}
self.call_simple(
HostRequest::InteractiveEvent { command_id, event },
"InteractiveEvent",
"interactive event",
|resp| match resp {
HostResponse::CommandStep(s) => Ok(*s),
other => Err(Box::new(other)),
},
)
}
}
@@ -622,30 +644,15 @@ impl PluginProcess {
other => Err(PluginError::UnexpectedResponse(Box::new(other))),
}
} else {
self.send_request(HostRequest::GetPrompt { command_id })?;
let kind = "GetPrompt";
let timeout = request_timeout(kind);
let deadline = Instant::now() + timeout;
loop {
match recv_with_deadline::<PluginToHost>(
&self.stream,
&self.child,
deadline,
timeout,
kind,
)? {
PluginToHost::Response(HostResponse::Text(s)) => return Ok(s),
PluginToHost::Response(other) => {
return Err(PluginError::UnexpectedResponse(Box::new(other)))
}
PluginToHost::Request(req) => {
let resp = crate::ipc::protocol::PluginResponse::Error(format!(
"unexpected nested request during get_prompt: {req:?}"
));
self.send_response(resp)?;
}
}
}
self.call_simple(
HostRequest::GetPrompt { command_id },
"GetPrompt",
"get_prompt",
|resp| match resp {
HostResponse::Text(s) => Ok(s),
other => Err(Box::new(other)),
},
)
}
}
@@ -662,30 +669,15 @@ impl PluginProcess {
other => Err(PluginError::UnexpectedResponse(Box::new(other))),
}
} else {
self.send_request(HostRequest::NeedsEntityPick { command_id })?;
let kind = "NeedsEntityPick";
let timeout = request_timeout(kind);
let deadline = Instant::now() + timeout;
loop {
match recv_with_deadline::<PluginToHost>(
&self.stream,
&self.child,
deadline,
timeout,
kind,
)? {
PluginToHost::Response(HostResponse::Bool(b)) => return Ok(b),
PluginToHost::Response(other) => {
return Err(PluginError::UnexpectedResponse(Box::new(other)))
}
PluginToHost::Request(req) => {
let resp = crate::ipc::protocol::PluginResponse::Error(format!(
"unexpected nested request during needs_entity_pick: {req:?}"
));
self.send_response(resp)?;
}
}
}
self.call_simple(
HostRequest::NeedsEntityPick { command_id },
"NeedsEntityPick",
"needs_entity_pick",
|resp| match resp {
HostResponse::Bool(b) => Ok(b),
other => Err(Box::new(other)),
},
)
}
}
+1 -59
View File
@@ -16,65 +16,7 @@ use crate::ipc::v4::protocol::{HostToPluginV4, NotificationEnvelope};
use crate::ipc::v4::server::{
default_notify_rate_limit, run_host_reader_thread, HostIncoming, RateLimiter, V4HostShared,
};
use crate::process::PluginError;
/// Default maximum time to wait for a plugin call to respond.
const CALL_TIMEOUT_DEFAULT: Duration = Duration::from_secs(30);
fn call_timeout() -> Duration {
std::env::var("OCS_PLUGIN_CALL_TIMEOUT_SECS")
.ok()
.and_then(|s| s.parse().ok())
.map(Duration::from_secs)
.unwrap_or(CALL_TIMEOUT_DEFAULT)
}
/// Per-request-kind timeout floors.
fn request_timeout(kind: &'static str) -> Duration {
base_max_floor(call_timeout(), kind)
}
fn execute_code_timeout() -> Duration {
const DEFAULT: Duration = Duration::from_secs(60);
std::env::var("OCS_PLUGIN_EXECUTE_TIMEOUT_SECS")
.ok()
.and_then(|s| s.parse().ok())
.map(Duration::from_secs)
.unwrap_or(DEFAULT)
.max(DEFAULT)
}
fn base_max_floor(base: Duration, kind: &'static str) -> Duration {
#[cfg(test)]
if let Some(secs) = std::env::var("OCS_PLUGIN_TEST_FLOOR_SECS")
.ok()
.and_then(|s| s.parse().ok())
{
return base.max(Duration::from_secs(secs));
}
let floor = match kind {
"GetManifest" | "GetRibbon" => Duration::from_secs(5),
"Dispatch" => Duration::from_secs(10),
"InteractiveEvent" | "GetPrompt" | "NeedsEntityPick" => Duration::from_secs(2),
"ExecuteCode" => execute_code_timeout(),
_ => Duration::from_secs(1),
};
base.max(floor)
}
fn request_kind(req: &HostRequest) -> &'static str {
match req {
HostRequest::GetManifest => "GetManifest",
HostRequest::GetRibbon => "GetRibbon",
HostRequest::Dispatch { .. } => "Dispatch",
HostRequest::InteractiveEvent { .. } => "InteractiveEvent",
HostRequest::GetPrompt { .. } => "GetPrompt",
HostRequest::NeedsEntityPick { .. } => "NeedsEntityPick",
HostRequest::ExecuteCode { .. } => "ExecuteCode",
HostRequest::DropInteractive { .. } => "DropInteractive",
HostRequest::Shutdown => "Shutdown",
}
}
use crate::process::{request_kind, request_timeout, PluginError};
type DeferredRequest = (u64, Option<u64>, Box<PluginRequest>);