a3s_box_runtime/local_execution/
backend.rs1use std::path::PathBuf;
4
5use a3s_box_core::{
6 pty::PtyRequest, ExecOutput, ExecRequest, ExecutionEventBatch, ExecutionEventsRequest,
7 ExecutionId, ExecutionManagerError, ExecutionManagerResult, ExecutionProcess,
8 ExecutionProcessInventory, ExecutionResourceUpdate, ExecutionState, ExecutionStats,
9 FileRequest, FileResponse, FilesystemRequest, FilesystemResponse, KillOutcome, OperationId,
10};
11use async_trait::async_trait;
12use chrono::{DateTime, Utc};
13
14use crate::local_execution::OciRuntimeBinding;
15use crate::{BoxRecord, ManagedRuntimeRoute};
16
17#[derive(Debug, Clone)]
19pub struct LocalExecutionHandle {
20 pub started_at: DateTime<Utc>,
21 pub pid: Option<u32>,
22 pub pid_start_time: Option<u64>,
23 pub exec_socket_path: PathBuf,
24 pub console_log: PathBuf,
25 pub anonymous_volumes: Vec<String>,
26 pub oci_runtime: Option<OciRuntimeBinding>,
29}
30
31impl LocalExecutionHandle {
32 pub(crate) fn validate(&self, execution_id: &ExecutionId) -> ExecutionManagerResult<()> {
33 if self.pid.is_none() && self.pid_start_time.is_some() {
34 return Err(ExecutionManagerError::Internal(format!(
35 "backend returned a PID start time without a PID for {execution_id}"
36 )));
37 }
38 if let Some(binding) = &self.oci_runtime {
39 binding.validate_for(execution_id)?;
40 }
41 if self.exec_socket_path.as_os_str().is_empty() && self.oci_runtime.is_none() {
42 return Err(ExecutionManagerError::Internal(format!(
43 "backend returned an empty exec socket path for {execution_id}"
44 )));
45 }
46 if self.console_log.as_os_str().is_empty() {
47 return Err(ExecutionManagerError::Internal(format!(
48 "backend returned an empty console log path for {execution_id}"
49 )));
50 }
51 Ok(())
52 }
53}
54
55#[derive(Debug, Clone)]
57pub struct LocalExecutionObservation {
58 pub state: ExecutionState,
59 pub handle: Option<LocalExecutionHandle>,
60 pub exit_code: Option<i32>,
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub struct LocalExecutionTermination {
67 pub outcome: KillOutcome,
68 pub exit_code: Option<i32>,
69}
70
71impl LocalExecutionObservation {
72 pub(crate) fn validate(&self, execution_id: &ExecutionId) -> ExecutionManagerResult<()> {
73 match self.state {
74 ExecutionState::Running | ExecutionState::Paused => {
75 self.handle.as_ref().ok_or_else(|| {
76 ExecutionManagerError::Internal(format!(
77 "backend returned {:?} without runtime evidence for {execution_id}",
78 self.state
79 ))
80 })?;
81 }
82 ExecutionState::Created
83 | ExecutionState::Creating
84 | ExecutionState::Stopped
85 | ExecutionState::Failed => {}
86 }
87 if let Some(handle) = &self.handle {
88 handle.validate(execution_id)?;
89 }
90 Ok(())
91 }
92}
93
94#[async_trait]
99pub trait LocalExecutionBackend: Send + Sync {
100 fn route_for_create(&self, _record: &BoxRecord) -> ExecutionManagerResult<ManagedRuntimeRoute> {
106 Ok(ManagedRuntimeRoute::Unspecified)
107 }
108
109 async fn preflight(&self, _record: &BoxRecord) -> ExecutionManagerResult<()> {
112 Ok(())
113 }
114
115 async fn start(&self, record: &BoxRecord) -> ExecutionManagerResult<LocalExecutionHandle>;
116
117 async fn inspect(
118 &self,
119 record: &BoxRecord,
120 ) -> ExecutionManagerResult<LocalExecutionObservation>;
121
122 async fn pause(
123 &self,
124 record: &BoxRecord,
125 keep_memory: bool,
126 ) -> ExecutionManagerResult<LocalExecutionHandle>;
127
128 async fn resume(&self, record: &BoxRecord) -> ExecutionManagerResult<LocalExecutionHandle>;
129
130 async fn preflight_resource_update(
132 &self,
133 _record: &BoxRecord,
134 _update: &ExecutionResourceUpdate,
135 ) -> ExecutionManagerResult<()> {
136 Err(ExecutionManagerError::Unavailable(
137 "this execution backend does not support live resource updates".to_string(),
138 ))
139 }
140
141 async fn update_resources(
143 &self,
144 _record: &BoxRecord,
145 _operation_id: &OperationId,
146 _update: &ExecutionResourceUpdate,
147 ) -> ExecutionManagerResult<()> {
148 Err(ExecutionManagerError::Unavailable(
149 "this execution backend does not support live resource updates".to_string(),
150 ))
151 }
152
153 async fn list_processes(
155 &self,
156 _record: &BoxRecord,
157 ) -> ExecutionManagerResult<ExecutionProcessInventory> {
158 Err(ExecutionManagerError::Unavailable(
159 "this execution backend does not expose process inventory".to_string(),
160 ))
161 }
162
163 async fn stats(&self, _record: &BoxRecord) -> ExecutionManagerResult<ExecutionStats> {
165 Err(ExecutionManagerError::Unavailable(
166 "this execution backend does not expose runtime stats".to_string(),
167 ))
168 }
169
170 async fn events(
172 &self,
173 _record: &BoxRecord,
174 _request: ExecutionEventsRequest,
175 ) -> ExecutionManagerResult<ExecutionEventBatch> {
176 Err(ExecutionManagerError::Unavailable(
177 "this execution backend does not expose runtime events".to_string(),
178 ))
179 }
180
181 async fn execute(
185 &self,
186 _record: &BoxRecord,
187 _request: ExecRequest,
188 ) -> ExecutionManagerResult<ExecOutput> {
189 Err(ExecutionManagerError::Unavailable(
190 "this execution backend does not expose captured process sessions".to_string(),
191 ))
192 }
193
194 async fn start_process(
196 &self,
197 _record: &BoxRecord,
198 _request: ExecRequest,
199 ) -> ExecutionManagerResult<ExecutionProcess> {
200 Err(ExecutionManagerError::Unavailable(
201 "this execution backend does not expose streaming process sessions".to_string(),
202 ))
203 }
204
205 async fn start_pty(
207 &self,
208 _record: &BoxRecord,
209 _request: PtyRequest,
210 ) -> ExecutionManagerResult<ExecutionProcess> {
211 Err(ExecutionManagerError::Unavailable(
212 "this execution backend does not expose PTY sessions".to_string(),
213 ))
214 }
215
216 async fn transfer_file(
218 &self,
219 _record: &BoxRecord,
220 _request: FileRequest,
221 ) -> ExecutionManagerResult<FileResponse> {
222 Err(ExecutionManagerError::Unavailable(
223 "this execution backend does not expose file transfer sessions".to_string(),
224 ))
225 }
226
227 async fn filesystem(
229 &self,
230 _record: &BoxRecord,
231 _request: FilesystemRequest,
232 ) -> ExecutionManagerResult<FilesystemResponse> {
233 Err(ExecutionManagerError::Unavailable(
234 "this execution backend does not expose filesystem sessions".to_string(),
235 ))
236 }
237
238 async fn prepare_quiescent_rootfs(&self, _record: &BoxRecord) -> ExecutionManagerResult<()> {
241 Ok(())
242 }
243
244 async fn cleanup_quiescent_rootfs(&self, _record: &BoxRecord) -> ExecutionManagerResult<()> {
247 Ok(())
248 }
249
250 async fn stop_for_restart(
253 &self,
254 record: &BoxRecord,
255 _timeout_secs: Option<u64>,
256 ) -> ExecutionManagerResult<KillOutcome> {
257 self.kill(record).await
258 }
259
260 async fn kill(&self, record: &BoxRecord) -> ExecutionManagerResult<KillOutcome>;
261
262 async fn kill_with_status(
266 &self,
267 record: &BoxRecord,
268 ) -> ExecutionManagerResult<LocalExecutionTermination> {
269 Ok(LocalExecutionTermination {
270 outcome: self.kill(record).await?,
271 exit_code: None,
272 })
273 }
274}