use std::sync::Arc;
use arcbox_connect::sandbox_v1 as pb;
use arcbox_connect::sandbox_v1::{ExecutionEvent, KeepAlive, execution_event};
use buffa_types::google::protobuf::Empty;
use connectrpc::{
ConnectError, InboundStream, RequestContext, Response, ServiceRequest, ServiceResult,
ServiceStream,
};
use tokio_stream::StreamExt as _;
use tokio_stream::wrappers::ReceiverStream;
use super::SharedRuntime;
use crate::ApiError;
use super::sandbox_resume;
use super::{ConnectRuntimeExt as _, ContextExt as _, with_keepalive};
use arcbox_computer::SandboxHost as _;
use arcbox_computer::locks::SandboxOperationLocks;
const DEFAULT_WAIT_FOR_PORT_TIMEOUT_SECS: u32 = 30;
const MAX_WAIT_FOR_PORT_TIMEOUT_SECS: u32 = 600;
const fn resolve_wait_for_port_budget(requested: u32) -> u32 {
match requested {
0 => DEFAULT_WAIT_FOR_PORT_TIMEOUT_SECS,
t if t > MAX_WAIT_FOR_PORT_TIMEOUT_SECS => MAX_WAIT_FOR_PORT_TIMEOUT_SECS,
t => t,
}
}
pub struct SandboxProcessServiceImpl {
runtime: SharedRuntime,
operations: Arc<SandboxOperationLocks>,
}
impl SandboxProcessServiceImpl {
#[must_use]
pub(super) fn new(runtime: SharedRuntime, operations: Arc<SandboxOperationLocks>) -> Self {
Self {
runtime,
operations,
}
}
}
#[allow(
refining_impl_trait,
reason = "the trait returns `impl Encodable<M>`; naming the concrete body \
type is strictly more informative and these impls are registered on a \
Router rather than named by callers"
)]
impl pb::SandboxProcessService for SandboxProcessServiceImpl {
async fn start_execution(
&self,
ctx: RequestContext,
request: ServiceRequest<'_, pb::StartExecutionRequest>,
) -> ServiceResult<pb::Execution> {
let machine = ctx.sandbox_machine_id()?;
let req = request.to_owned_message();
let runtime = self.runtime.ready()?;
let execution = sandbox_resume::with_auto_resume(
runtime,
&self.operations,
&ctx,
&machine,
&req.sandbox_id,
|| {
let req = req.clone();
async {
let mut agent = runtime.agent(&machine)?;
agent.sandbox_exec_start(req).await
}
},
)
.await?;
Response::ok(execution)
}
async fn attach_execution(
&self,
ctx: RequestContext,
request: ServiceRequest<'_, pb::AttachExecutionRequest>,
) -> ServiceResult<ServiceStream<ExecutionEvent>> {
let machine = ctx.sandbox_machine_id()?;
let agent = self
.runtime
.ready()?
.get_agent(&machine)
.map_err(ApiError::from)?;
let rx = agent
.sandbox_exec_attach(request.to_owned_message())
.await
.map_err(ApiError::from)?;
let stream =
ReceiverStream::new(rx).map(|r| r.map_err(|e| ConnectError::from(ApiError::from(e))));
let stream = with_keepalive(stream, || ExecutionEvent {
event: Some(execution_event::Event::from(KeepAlive::default())),
..Default::default()
});
Response::ok(Box::pin(stream))
}
async fn write_stdin(
&self,
ctx: RequestContext,
request: ServiceRequest<'_, pb::WriteStdinRequest>,
) -> ServiceResult<pb::StdinStatus> {
let machine = ctx.sandbox_machine_id()?;
let req = request.to_owned_message();
let runtime = self.runtime.ready()?;
let status = sandbox_resume::with_auto_resume(
runtime,
&self.operations,
&ctx,
&machine,
&req.sandbox_id,
|| {
let req = req.clone();
async {
let mut agent = runtime.agent(&machine)?;
agent.sandbox_stdin_write(req).await
}
},
)
.await?;
Response::ok(status)
}
async fn stream_stdin(
&self,
ctx: RequestContext,
mut requests: InboundStream<pb::WriteStdinRequest>,
) -> ServiceResult<pb::StdinStatus> {
let machine = ctx.sandbox_machine_id()?;
let mut last = None;
while let Some(item) = requests.next().await {
let req = item?.to_owned_message();
let runtime = self.runtime.ready()?;
let status = sandbox_resume::with_auto_resume(
runtime,
&self.operations,
&ctx,
&machine,
&req.sandbox_id,
|| {
let req = req.clone();
async {
let mut agent = runtime.agent(&machine)?;
agent.sandbox_stdin_write(req).await
}
},
)
.await?;
last = Some(status);
}
let last = last
.ok_or_else(|| ConnectError::invalid_argument("stream_stdin: empty request stream"))?;
Response::ok(last)
}
async fn get_stdin_status(
&self,
ctx: RequestContext,
request: ServiceRequest<'_, pb::GetStdinStatusRequest>,
) -> ServiceResult<pb::StdinStatus> {
let machine = ctx.sandbox_machine_id()?;
let mut agent = self
.runtime
.ready()?
.get_agent(&machine)
.map_err(ApiError::from)?;
let status = agent
.sandbox_stdin_status(request.to_owned_message())
.await
.map_err(ApiError::from)?;
Response::ok(status)
}
async fn signal_execution(
&self,
ctx: RequestContext,
request: ServiceRequest<'_, pb::SignalExecutionRequest>,
) -> ServiceResult<Empty> {
let machine = ctx.sandbox_machine_id()?;
let mut agent = self
.runtime
.ready()?
.get_agent(&machine)
.map_err(ApiError::from)?;
agent
.sandbox_exec_signal(request.to_owned_message())
.await
.map_err(ApiError::from)?;
Response::ok(Empty::default())
}
async fn resize_execution_tty(
&self,
ctx: RequestContext,
request: ServiceRequest<'_, pb::ResizeExecutionTtyRequest>,
) -> ServiceResult<Empty> {
let machine = ctx.sandbox_machine_id()?;
let mut agent = self
.runtime
.ready()?
.get_agent(&machine)
.map_err(ApiError::from)?;
agent
.sandbox_exec_resize(request.to_owned_message())
.await
.map_err(ApiError::from)?;
Response::ok(Empty::default())
}
async fn wait_execution(
&self,
ctx: RequestContext,
request: ServiceRequest<'_, pb::WaitExecutionRequest>,
) -> ServiceResult<pb::Execution> {
let machine = ctx.sandbox_machine_id()?;
let mut agent = self
.runtime
.ready()?
.get_agent(&machine)
.map_err(ApiError::from)?;
let execution = agent
.sandbox_exec_wait(request.to_owned_message())
.await
.map_err(ApiError::from)?;
Response::ok(execution)
}
async fn list_executions(
&self,
ctx: RequestContext,
request: ServiceRequest<'_, pb::ListExecutionsRequest>,
) -> ServiceResult<pb::ListExecutionsResponse> {
let machine = ctx.sandbox_machine_id()?;
let mut agent = self
.runtime
.ready()?
.get_agent(&machine)
.map_err(ApiError::from)?;
let listing = agent
.sandbox_exec_list(request.to_owned_message())
.await
.map_err(ApiError::from)?;
Response::ok(listing)
}
async fn wait_for_port(
&self,
ctx: RequestContext,
request: ServiceRequest<'_, pb::WaitForPortRequest>,
) -> ServiceResult<Empty> {
let machine = ctx.sandbox_machine_id()?;
let mut req = request.to_owned_message();
req.timeout_seconds = resolve_wait_for_port_budget(req.timeout_seconds);
let runtime = self.runtime.ready()?;
sandbox_resume::with_auto_resume(
runtime,
&self.operations,
&ctx,
&machine,
&req.sandbox_id,
|| {
let req = req.clone();
async {
let mut agent = runtime.agent(&machine)?;
agent.sandbox_wait_for_port(req).await
}
},
)
.await?;
Response::ok(Empty::default())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn wait_for_port_budget_defaults_and_caps() {
assert_eq!(
resolve_wait_for_port_budget(0),
DEFAULT_WAIT_FOR_PORT_TIMEOUT_SECS
);
assert_eq!(resolve_wait_for_port_budget(5), 5);
assert_eq!(
resolve_wait_for_port_budget(MAX_WAIT_FOR_PORT_TIMEOUT_SECS),
MAX_WAIT_FOR_PORT_TIMEOUT_SECS
);
assert_eq!(
resolve_wait_for_port_budget(u32::MAX),
MAX_WAIT_FOR_PORT_TIMEOUT_SECS
);
}
}