Skip to main content

boxxkite_client/
session.rs

1//! [`SandboxSession`]: a thin, session-scoped view over [`Client`] whose
2//! every method forwards to the identically-named `Client` method with this
3//! session's id bound in, plus [`Client::with_sandbox`], the
4//! create-on-enter/destroy-on-exit convenience. Mirrors `sdk-python`'s
5//! `SandboxSession`/`client.sandbox()` context manager, `sdk-js`'s
6//! `SandboxSession`/`withSandbox`, and `sdk-go`'s `Session`/`WithSandbox`.
7
8use std::future::Future;
9use std::pin::Pin;
10
11use futures_util::Stream;
12
13use crate::audit::{AuditLogEntry, AuditLogResponse, GetLogOptions};
14use crate::client::Client;
15use crate::desktop::DesktopStream;
16use crate::error::BoxxkiteError;
17use crate::files::{
18    ExecOptions, ExecResult, FileCreateResult, FileOptions, GlobOptions, GlobResult, GrepOptions,
19    GrepResult, HttpRequestOptions, HttpRequestResult, LsOptions, LsResult, StrReplaceOptions,
20    StrReplaceResult, ViewOptions, ViewResult,
21};
22use crate::lsp::{LspCompletionResult, LspOpenResult, LspStartResult, LspStopResult};
23use crate::preview::{PreviewRevokeResult, PreviewUrl};
24use crate::processes::{
25    ProcessInputResult, ProcessListResult, ProcessOutputResult, ProcessStartResult,
26    ProcessStopResult, StartProcessOptions,
27};
28use crate::sandboxes::CreateSandboxOptions;
29use crate::takeover::TakeoverStream;
30
31/// A [`Client`] bound to one sandbox session id -- obtained from
32/// [`Client::with_sandbox`]. Holds a cheap clone of the underlying `Client`
33/// (see [`Client`]'s own note on cloning), so it is `Clone` and can outlive
34/// the borrow that produced it.
35#[derive(Clone, Debug)]
36pub struct SandboxSession {
37    client: Client,
38    id: String,
39}
40
41impl SandboxSession {
42    /// The bound sandbox session id.
43    pub fn id(&self) -> &str {
44        &self.id
45    }
46
47    /// The underlying [`Client`], for the handful of calls not scoped to a
48    /// single session (e.g. [`Client::list_sandboxes`]).
49    pub fn client(&self) -> &Client {
50        &self.client
51    }
52
53    pub async fn exec(
54        &self,
55        command: &str,
56        options: ExecOptions,
57    ) -> Result<ExecResult, BoxxkiteError> {
58        self.client.exec(&self.id, command, options).await
59    }
60
61    pub async fn http_request(
62        &self,
63        method: &str,
64        url: &str,
65        options: HttpRequestOptions,
66    ) -> Result<HttpRequestResult, BoxxkiteError> {
67        self.client
68            .http_request(&self.id, method, url, options)
69            .await
70    }
71
72    pub async fn file_create(
73        &self,
74        path: &str,
75        content: &str,
76        options: FileOptions,
77    ) -> Result<FileCreateResult, BoxxkiteError> {
78        self.client
79            .file_create(&self.id, path, content, options)
80            .await
81    }
82
83    pub async fn view(&self, path: &str, options: ViewOptions) -> Result<ViewResult, BoxxkiteError> {
84        self.client.view(&self.id, path, options).await
85    }
86
87    pub async fn str_replace(
88        &self,
89        path: &str,
90        old_str: &str,
91        new_str: &str,
92        options: StrReplaceOptions,
93    ) -> Result<StrReplaceResult, BoxxkiteError> {
94        self.client
95            .str_replace(&self.id, path, old_str, new_str, options)
96            .await
97    }
98
99    pub async fn ls(&self, options: LsOptions) -> Result<LsResult, BoxxkiteError> {
100        self.client.ls(&self.id, options).await
101    }
102
103    pub async fn glob(
104        &self,
105        pattern: &str,
106        options: GlobOptions,
107    ) -> Result<GlobResult, BoxxkiteError> {
108        self.client.glob(&self.id, pattern, options).await
109    }
110
111    pub async fn grep(
112        &self,
113        pattern: &str,
114        options: GrepOptions,
115    ) -> Result<GrepResult, BoxxkiteError> {
116        self.client.grep(&self.id, pattern, options).await
117    }
118
119    pub async fn get_log(&self, options: GetLogOptions) -> Result<AuditLogResponse, BoxxkiteError> {
120        self.client.get_log(&self.id, options).await
121    }
122
123    pub fn watch(
124        &self,
125    ) -> Pin<Box<dyn Stream<Item = Result<AuditLogEntry, BoxxkiteError>> + Send + 'static>> {
126        self.client.watch(&self.id)
127    }
128
129    pub async fn start_process(
130        &self,
131        command: &str,
132        options: StartProcessOptions,
133    ) -> Result<ProcessStartResult, BoxxkiteError> {
134        self.client.start_process(&self.id, command, options).await
135    }
136
137    pub async fn list_processes(&self) -> Result<ProcessListResult, BoxxkiteError> {
138        self.client.list_processes(&self.id).await
139    }
140
141    pub async fn get_process_output(
142        &self,
143        process_id: &str,
144        since_offset: i64,
145    ) -> Result<ProcessOutputResult, BoxxkiteError> {
146        self.client
147            .get_process_output(&self.id, process_id, since_offset)
148            .await
149    }
150
151    pub async fn send_process_input(
152        &self,
153        process_id: &str,
154        data: &str,
155    ) -> Result<ProcessInputResult, BoxxkiteError> {
156        self.client
157            .send_process_input(&self.id, process_id, data)
158            .await
159    }
160
161    pub async fn stop_process(&self, process_id: &str) -> Result<ProcessStopResult, BoxxkiteError> {
162        self.client.stop_process(&self.id, process_id).await
163    }
164
165    pub async fn takeover(&self) -> Result<TakeoverStream, BoxxkiteError> {
166        self.client.takeover(&self.id).await
167    }
168
169    pub async fn desktop_takeover(&self) -> Result<DesktopStream, BoxxkiteError> {
170        self.client.desktop_takeover(&self.id).await
171    }
172
173    pub async fn create_preview_url(
174        &self,
175        port: u16,
176        ttl_seconds: Option<u32>,
177    ) -> Result<PreviewUrl, BoxxkiteError> {
178        self.client
179            .create_preview_url(&self.id, port, ttl_seconds)
180            .await
181    }
182
183    pub async fn revoke_preview_url(
184        &self,
185        port: u16,
186        token_id: &str,
187    ) -> Result<PreviewRevokeResult, BoxxkiteError> {
188        self.client
189            .revoke_preview_url(&self.id, port, token_id)
190            .await
191    }
192
193    pub async fn lsp_start(&self, language: &str) -> Result<LspStartResult, BoxxkiteError> {
194        self.client.lsp_start(&self.id, language).await
195    }
196
197    pub async fn lsp_open(
198        &self,
199        lsp_id: &str,
200        path: &str,
201        content: &str,
202    ) -> Result<LspOpenResult, BoxxkiteError> {
203        self.client.lsp_open(&self.id, lsp_id, path, content).await
204    }
205
206    pub async fn lsp_completion(
207        &self,
208        lsp_id: &str,
209        path: &str,
210        line: u32,
211        character: u32,
212    ) -> Result<LspCompletionResult, BoxxkiteError> {
213        self.client
214            .lsp_completion(&self.id, lsp_id, path, line, character)
215            .await
216    }
217
218    pub async fn lsp_stop(&self, lsp_id: &str) -> Result<LspStopResult, BoxxkiteError> {
219        self.client.lsp_stop(&self.id, lsp_id).await
220    }
221}
222
223impl Client {
224    /// Create a sandbox, run `f` against a [`SandboxSession`] bound to it, and
225    /// tear the sandbox down when `f` returns -- even on error. Teardown is
226    /// best-effort (an already-gone session doesn't turn a successful `f` into
227    /// an error), mirroring every sibling SDK's cleanup semantics.
228    ///
229    /// ```no_run
230    /// use boxxkite_client::{Client, CreateSandboxOptions, ExecOptions};
231    ///
232    /// # async fn example() -> Result<(), boxxkite_client::BoxxkiteError> {
233    /// let client = Client::new("https://cp.example.com", "bxk_live_...")?;
234    /// let stdout = client
235    ///     .with_sandbox(CreateSandboxOptions::new().label("demo"), |sb| async move {
236    ///         let result = sb.exec("echo hi", ExecOptions::new()).await?;
237    ///         Ok(result.stdout)
238    ///     })
239    ///     .await?;
240    /// assert_eq!(stdout, "hi\n");
241    /// # Ok(())
242    /// # }
243    /// ```
244    pub async fn with_sandbox<F, Fut, T>(
245        &self,
246        options: CreateSandboxOptions,
247        f: F,
248    ) -> Result<T, BoxxkiteError>
249    where
250        F: FnOnce(SandboxSession) -> Fut,
251        Fut: Future<Output = Result<T, BoxxkiteError>>,
252    {
253        let sandbox = self.create_sandbox(options).await?;
254        let session = SandboxSession {
255            client: self.clone(),
256            id: sandbox.id.clone(),
257        };
258        let result = f(session).await;
259        // Best-effort teardown: don't let a cleanup failure mask f's result.
260        let _ = self.destroy_sandbox(&sandbox.id).await;
261        result
262    }
263}