daytona_client/
process.rs

1//! Process execution within sandboxes
2
3use base64::{Engine, engine::general_purpose::STANDARD};
4use tracing::{debug, info};
5use uuid::Uuid;
6
7use crate::{
8    client::DaytonaClient,
9    error::Result,
10    models::{
11        CreateSessionRequest, ExecuteRequest, ExecuteResponse, Language, Session, SessionCommand,
12        SessionExecuteRequest, SessionExecuteResponse,
13    },
14};
15
16/// Process executor for running commands and code in sandboxes
17pub struct ProcessExecutor<'a> {
18    client: &'a DaytonaClient,
19}
20
21impl<'a> ProcessExecutor<'a> {
22    /// Create a new process executor
23    pub fn new(client: &'a DaytonaClient) -> Self {
24        Self { client }
25    }
26
27    /// Execute a raw command in a sandbox
28    pub async fn execute_command(
29        &self,
30        sandbox_id: &Uuid,
31        command: &str,
32    ) -> Result<ExecuteResponse> {
33        info!("Executing command in sandbox {}: {}", sandbox_id, command);
34
35        let request = ExecuteRequest {
36            command: command.to_string(),
37            cwd: None,
38            timeout: Some(self.client.config.timeout_seconds),
39        };
40
41        let response = self
42            .client
43            .build_request(
44                reqwest::Method::POST,
45                &format!("/toolbox/{}/toolbox/process/execute", sandbox_id),
46            )
47            .json(&request)
48            .send()
49            .await?;
50
51        self.client.handle_response(response).await
52    }
53
54    /// Execute with custom options
55    pub async fn execute_with_options(
56        &self,
57        sandbox_id: &Uuid,
58        request: ExecuteRequest,
59    ) -> Result<ExecuteResponse> {
60        info!("Executing command with options in sandbox {}", sandbox_id);
61
62        let response = self
63            .client
64            .build_request(
65                reqwest::Method::POST,
66                &format!("/toolbox/{}/toolbox/process/execute", sandbox_id),
67            )
68            .json(&request)
69            .send()
70            .await?;
71
72        self.client.handle_response(response).await
73    }
74
75    /// Execute code in a specific language
76    pub async fn execute_code(
77        &self,
78        sandbox_id: &Uuid,
79        language: Language,
80        code: &str,
81    ) -> Result<ExecuteResponse> {
82        info!("Executing {:?} code in sandbox {}", language, sandbox_id);
83        debug!("Code:\n{}", code);
84
85        // First, write the code to a temporary file
86        let file_ext = match language {
87            Language::Python => "py",
88            Language::Javascript => "js",
89            Language::Typescript => "ts",
90            Language::Rust => "rs",
91            Language::Go => "go",
92            Language::Java => "java",
93            Language::Ruby => "rb",
94            Language::Php => "php",
95            Language::Shell => "sh",
96        };
97
98        let filename = format!("/tmp/code_{}.{}", Uuid::new_v4(), file_ext);
99
100        // Upload the code file
101        let files = crate::files::FileManager::new(self.client);
102        files.upload(sandbox_id, &filename, code.as_bytes()).await?;
103
104        // Execute the file
105        let command = match language {
106            Language::Python => format!("python {}", filename),
107            Language::Javascript => format!("node {}", filename),
108            Language::Typescript => format!("ts-node {}", filename),
109            Language::Rust => format!("rustc {} -o /tmp/rust_out && /tmp/rust_out", filename),
110            Language::Go => format!("go run {}", filename),
111            Language::Java => {
112                let class_name = filename.strip_suffix(".java").unwrap_or(&filename);
113                format!("javac {} && java {}", filename, class_name)
114            }
115            Language::Ruby => format!("ruby {}", filename),
116            Language::Php => format!("php {}", filename),
117            Language::Shell => format!("bash {}", filename),
118        };
119
120        self.execute_command(sandbox_id, &command).await
121    }
122
123    /// Execute code using stdin (for simpler cases)
124    pub async fn execute_code_stdin(
125        &self,
126        sandbox_id: &Uuid,
127        language: Language,
128        code: &str,
129    ) -> Result<ExecuteResponse> {
130        // Base64 encode the code to avoid shell escaping issues
131        let encoded_code = STANDARD.encode(code.as_bytes());
132
133        let command = match language {
134            Language::Python => format!("echo {} | base64 -d | python", encoded_code),
135            Language::Javascript => format!("echo {} | base64 -d | node", encoded_code),
136            Language::Shell => format!("echo {} | base64 -d | bash", encoded_code),
137            _ => {
138                // For languages that don't easily support stdin, fall back to file method
139                return self.execute_code(sandbox_id, language, code).await;
140            }
141        };
142
143        self.execute_command(sandbox_id, &command).await
144    }
145
146    /// Run a shell script
147    pub async fn run_script(&self, sandbox_id: &Uuid, script: &str) -> Result<ExecuteResponse> {
148        self.execute_code(sandbox_id, Language::Shell, script).await
149    }
150
151    /// Install packages in the sandbox
152    pub async fn install_packages(
153        &self,
154        sandbox_id: &Uuid,
155        language: Language,
156        packages: Vec<String>,
157    ) -> Result<ExecuteResponse> {
158        let command = match language {
159            Language::Python => format!("pip install {}", packages.join(" ")),
160            Language::Javascript => format!("npm install {}", packages.join(" ")),
161            Language::Ruby => format!("gem install {}", packages.join(" ")),
162            Language::Php => format!("composer require {}", packages.join(" ")),
163            Language::Go => format!("go get {}", packages.join(" ")),
164            Language::Rust => format!("cargo add {}", packages.join(" ")),
165            _ => {
166                return Err(crate::error::DaytonaError::InvalidParameters(format!(
167                    "Package installation not supported for {:?}",
168                    language
169                )));
170            }
171        };
172
173        self.execute_command(sandbox_id, &command).await
174    }
175
176    /// Create a new session for persistent command execution
177    pub async fn create_session(&self, sandbox_id: &Uuid, session_id: &str) -> Result<()> {
178        info!(
179            "Creating new session {} in sandbox {}",
180            session_id, sandbox_id
181        );
182
183        let request = CreateSessionRequest {
184            session_id: session_id.to_string(),
185        };
186
187        let response = self
188            .client
189            .build_request(
190                reqwest::Method::POST,
191                &format!("/toolbox/{}/toolbox/process/session", sandbox_id),
192            )
193            .json(&request)
194            .send()
195            .await?;
196
197        if response.status().is_success() {
198            Ok(())
199        } else {
200            let error_text = response.text().await?;
201            Err(crate::error::DaytonaError::RequestFailed(format!(
202                "Failed to create session: {}",
203                error_text
204            )))
205        }
206    }
207
208    /// Delete a session
209    pub async fn delete_session(&self, sandbox_id: &Uuid, session_id: &str) -> Result<()> {
210        info!("Deleting session {} in sandbox {}", session_id, sandbox_id);
211
212        let response = self
213            .client
214            .build_request(
215                reqwest::Method::DELETE,
216                &format!(
217                    "/toolbox/{}/toolbox/process/session/{}",
218                    sandbox_id, session_id
219                ),
220            )
221            .send()
222            .await?;
223
224        if response.status().is_success() {
225            Ok(())
226        } else {
227            let error_text = response.text().await?;
228            Err(crate::error::DaytonaError::RequestFailed(format!(
229                "Failed to delete session: {}",
230                error_text
231            )))
232        }
233    }
234
235    /// Get session information
236    pub async fn get_session(&self, sandbox_id: &Uuid, session_id: &str) -> Result<Session> {
237        debug!("Getting session {} in sandbox {}", session_id, sandbox_id);
238
239        let response = self
240            .client
241            .build_request(
242                reqwest::Method::GET,
243                &format!(
244                    "/toolbox/{}/toolbox/process/session/{}",
245                    sandbox_id, session_id
246                ),
247            )
248            .send()
249            .await?;
250
251        self.client.handle_response(response).await
252    }
253
254    /// List all sessions
255    pub async fn list_sessions(&self, sandbox_id: &Uuid) -> Result<Vec<Session>> {
256        debug!("Listing sessions in sandbox {}", sandbox_id);
257
258        let response = self
259            .client
260            .build_request(
261                reqwest::Method::GET,
262                &format!("/toolbox/{}/toolbox/process/session", sandbox_id),
263            )
264            .send()
265            .await?;
266
267        self.client.handle_response(response).await
268    }
269
270    /// Execute a command in a session
271    pub async fn execute_session_command(
272        &self,
273        sandbox_id: &Uuid,
274        session_id: &str,
275        request: SessionExecuteRequest,
276    ) -> Result<SessionExecuteResponse> {
277        info!(
278            "Executing command '{}' in session {} in sandbox {}",
279            request.command, session_id, sandbox_id
280        );
281
282        let response = self
283            .client
284            .build_request(
285                reqwest::Method::POST,
286                &format!(
287                    "/toolbox/{}/toolbox/process/session/{}/exec",
288                    sandbox_id, session_id
289                ),
290            )
291            .json(&request)
292            .send()
293            .await?;
294
295        self.client.handle_response(response).await
296    }
297
298    /// Get session command result
299    pub async fn get_session_command(
300        &self,
301        sandbox_id: &Uuid,
302        session_id: &str,
303        command_id: &str,
304    ) -> Result<SessionCommand> {
305        debug!(
306            "Getting command {} from session {} in sandbox {}",
307            command_id, session_id, sandbox_id
308        );
309
310        let response = self
311            .client
312            .build_request(
313                reqwest::Method::GET,
314                &format!(
315                    "/toolbox/{}/toolbox/process/session/{}/command/{}",
316                    sandbox_id, session_id, command_id
317                ),
318            )
319            .send()
320            .await?;
321
322        self.client.handle_response(response).await
323    }
324
325    /// Get session command logs
326    pub async fn get_session_command_logs(
327        &self,
328        sandbox_id: &Uuid,
329        session_id: &str,
330        command_id: &str,
331    ) -> Result<String> {
332        debug!(
333            "Getting logs for command {} from session {} in sandbox {}",
334            command_id, session_id, sandbox_id
335        );
336
337        let response = self
338            .client
339            .build_request(
340                reqwest::Method::GET,
341                &format!(
342                    "/toolbox/{}/toolbox/process/session/{}/command/{}/logs",
343                    sandbox_id, session_id, command_id
344                ),
345            )
346            .send()
347            .await?;
348
349        if response.status().is_success() {
350            Ok(response.text().await?)
351        } else {
352            let error_text = response.text().await?;
353            Err(crate::error::DaytonaError::RequestFailed(format!(
354                "Failed to get command logs: {}",
355                error_text
356            )))
357        }
358    }
359
360    /// Get project directory
361    pub async fn get_project_dir(&self, sandbox_id: &Uuid) -> Result<String> {
362        debug!("Getting project directory for sandbox {}", sandbox_id);
363
364        let response = self
365            .client
366            .build_request(
367                reqwest::Method::GET,
368                &format!("/toolbox/{}/toolbox/project-dir", sandbox_id),
369            )
370            .send()
371            .await?;
372
373        if response.status().is_success() {
374            Ok(response.text().await?.trim().to_string())
375        } else {
376            let error_text = response.text().await?;
377            Err(crate::error::DaytonaError::RequestFailed(format!(
378                "Failed to get project directory: {}",
379                error_text
380            )))
381        }
382    }
383}
384
385impl DaytonaClient {
386    /// Get process executor
387    pub fn process(&self) -> ProcessExecutor<'_> {
388        ProcessExecutor::new(self)
389    }
390}