use std::pin::Pin;
use futures_util::{Stream, StreamExt};
use reqwest::Method;
use reqwest_eventsource::{Event, EventSource};
use serde::{Deserialize, Serialize};
use crate::client::Client;
use crate::error::BoxxkiteError;
const DEFAULT_MAX_RUNTIME_SECONDS: i64 = 3600;
#[derive(Debug, Clone)]
pub struct StartProcessOptions {
description: Option<String>,
max_runtime_seconds: i64,
expose_port: Option<i32>,
}
impl Default for StartProcessOptions {
fn default() -> Self {
Self {
description: None,
max_runtime_seconds: DEFAULT_MAX_RUNTIME_SECONDS,
expose_port: None,
}
}
}
impl StartProcessOptions {
pub fn new() -> Self {
Self::default()
}
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn max_runtime_seconds(mut self, max_runtime_seconds: i64) -> Self {
self.max_runtime_seconds = max_runtime_seconds;
self
}
pub fn expose_port(mut self, expose_port: i32) -> Self {
self.expose_port = Some(expose_port);
self
}
}
#[derive(Serialize)]
struct StartProcessBody<'a> {
command: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<&'a str>,
max_runtime_seconds: i64,
#[serde(skip_serializing_if = "Option::is_none")]
expose_port: Option<i32>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ProcessStartResult {
pub process_id: String,
pub status: String,
pub started_at: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ProcessInfo {
pub process_id: String,
pub command: String,
pub description: Option<String>,
pub status: String,
pub started_at: String,
pub exit_code: Option<i32>,
pub expose_port: Option<i32>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ProcessListResult {
pub processes: Vec<ProcessInfo>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ProcessOutputResult {
pub status: String,
pub stdout_chunk: String,
pub next_offset: i64,
pub truncated: bool,
pub exit_code: Option<i32>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum ProcessStreamEvent {
Output {
stdout_chunk: String,
next_offset: i64,
truncated: bool,
},
Exit {
status: String,
exit_code: Option<i32>,
},
}
#[derive(Debug, Clone, Deserialize)]
pub struct ProcessInputResult {
pub bytes_written: i64,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ProcessStopResult {
pub status: String,
pub exit_code: Option<i32>,
}
impl Client {
pub async fn start_process(
&self,
session_id: &str,
command: &str,
options: StartProcessOptions,
) -> Result<ProcessStartResult, BoxxkiteError> {
let body = StartProcessBody {
command,
description: options.description.as_deref(),
max_runtime_seconds: options.max_runtime_seconds,
expose_port: options.expose_port,
};
let builder = self
.request(
Method::POST,
&format!("/v1/sandboxes/{session_id}/processes"),
)
.json(&body);
self.send(builder).await
}
pub async fn list_processes(
&self,
session_id: &str,
) -> Result<ProcessListResult, BoxxkiteError> {
let builder = self.request(
Method::GET,
&format!("/v1/sandboxes/{session_id}/processes"),
);
self.send(builder).await
}
pub async fn get_process_output(
&self,
session_id: &str,
process_id: &str,
since_offset: i64,
) -> Result<ProcessOutputResult, BoxxkiteError> {
let builder = self
.request(
Method::GET,
&format!("/v1/sandboxes/{session_id}/processes/{process_id}/output"),
)
.query(&[("since_offset", since_offset.to_string())]);
self.send(builder).await
}
pub fn stream_process_output(
&self,
session_id: &str,
process_id: &str,
since_offset: i64,
) -> Pin<Box<dyn Stream<Item = Result<ProcessStreamEvent, BoxxkiteError>> + Send + 'static>> {
let request_builder = self
.request(
Method::GET,
&format!("/v1/sandboxes/{session_id}/processes/{process_id}/stream"),
)
.query(&[("since_offset", since_offset.to_string())]);
Box::pin(async_stream::stream! {
let mut event_source = match EventSource::new(request_builder) {
Ok(event_source) => event_source,
Err(err) => {
yield Err(BoxxkiteError::Config(format!("failed to build stream request: {err}")));
return;
}
};
while let Some(event) = event_source.next().await {
match event {
Ok(Event::Open) => continue,
Ok(Event::Message(message)) => {
yield serde_json::from_str::<ProcessStreamEvent>(&message.data).map_err(BoxxkiteError::from);
}
Err(reqwest_eventsource::Error::StreamEnded) => break,
Err(reqwest_eventsource::Error::InvalidStatusCode(status, response)) => {
let bytes = response.bytes().await.unwrap_or_default();
yield Err(crate::error::api_error_from_bytes(status.as_u16(), &bytes));
break;
}
Err(err) => {
yield Err(BoxxkiteError::from(err));
break;
}
}
}
event_source.close();
})
}
pub async fn send_process_input(
&self,
session_id: &str,
process_id: &str,
data: &str,
) -> Result<ProcessInputResult, BoxxkiteError> {
#[derive(Serialize)]
struct Body<'a> {
data: &'a str,
}
let builder = self
.request(
Method::POST,
&format!("/v1/sandboxes/{session_id}/processes/{process_id}/input"),
)
.json(&Body { data });
self.send(builder).await
}
pub async fn stop_process(
&self,
session_id: &str,
process_id: &str,
) -> Result<ProcessStopResult, BoxxkiteError> {
let builder = self.request(
Method::POST,
&format!("/v1/sandboxes/{session_id}/processes/{process_id}/stop"),
);
self.send(builder).await
}
}