Skip to main content

a3s_box_runtime/local_execution/
mod.rs

1//! Durable implementation of the backend-neutral local execution lifecycle.
2
3mod api;
4mod backend;
5mod create;
6#[cfg(test)]
7mod inspection_cancellation_tests;
8mod lifecycle_lock;
9pub use lifecycle_lock::{
10    acquire_blocking as acquire_execution_lifecycle_lock, ExecutionLifecycleLock,
11};
12mod logs;
13mod oci_backend;
14#[cfg(feature = "vm")]
15mod oci_log_projection;
16#[cfg(feature = "vm")]
17mod oci_migration;
18#[cfg(all(feature = "vm", target_os = "linux"))]
19mod oci_owner;
20#[cfg(feature = "vm")]
21pub(crate) mod oci_portable_rootfs;
22#[cfg(feature = "vm")]
23mod oci_production;
24mod oci_session;
25mod operations;
26mod port;
27mod prepared_rootfs;
28mod record;
29mod recovery;
30mod remove;
31mod resources;
32mod restart;
33mod router;
34#[cfg(test)]
35mod router_tests;
36#[cfg(unix)]
37mod session;
38mod session_support;
39#[cfg(not(unix))]
40mod session_unsupported;
41mod snapshot;
42mod store;
43mod support;
44#[cfg(all(feature = "vm", target_os = "linux"))]
45mod transient_registry_auth;
46#[cfg(feature = "vm")]
47mod vm_backend;
48#[cfg(feature = "vm")]
49mod vm_process;
50
51use std::path::PathBuf;
52use std::sync::Arc;
53
54use a3s_box_core::{
55    ExecutionGeneration, ExecutionId, ExecutionIsolation, ExecutionManagerError,
56    ExecutionManagerResult,
57};
58
59pub use backend::{
60    LocalExecutionBackend, LocalExecutionHandle, LocalExecutionObservation,
61    LocalExecutionResourcePlan, LocalExecutionTermination,
62};
63pub use oci_backend::{
64    oci_isolation_request, OciBundlePreparationContext, OciBundleProvider, OciLifecycleAdapter,
65    OciLocalExecutionBackend, OciPreparedExecution, OciRuntimeBinding, OciRuntimeEndpoint,
66    OciRuntimeLaunch, OCI_RUNTIME_BINDING_SCHEMA_VERSION,
67};
68#[cfg(feature = "vm")]
69pub use oci_migration::{NativeLinuxOciMigrationConfig, WindowsWhpxOciMigrationConfig};
70#[cfg(feature = "vm")]
71pub use oci_production::{NativeLinuxOciBundleProvider, WindowsWhpxOciBundleProvider};
72use record::{build_managed_record, status_from_record};
73pub use router::{LocalExecutionBackendRouter, OciMigrationPolicy};
74use store::RuntimeUpdate;
75#[cfg(all(feature = "vm", target_os = "linux"))]
76pub(crate) use transient_registry_auth::{TransientRegistryAuthBroker, TransientRegistryAuthLease};
77#[cfg(feature = "vm")]
78pub use vm_backend::VmLocalExecutionBackend;
79
80use crate::{BoxRecord, ManagedExecutionOperation, ManagedExecutionState, ManagedExecutionStore};
81
82/// Local lifecycle facade shared by service, CLI, and SDK adapters.
83#[derive(Clone)]
84pub struct LocalExecutionManager {
85    store: ManagedExecutionStore,
86    home_dir: PathBuf,
87    backend: Arc<dyn LocalExecutionBackend>,
88}
89
90impl LocalExecutionManager {
91    pub fn new(
92        state_path: impl Into<PathBuf>,
93        home_dir: impl Into<PathBuf>,
94        backend: Arc<dyn LocalExecutionBackend>,
95    ) -> Self {
96        Self {
97            store: ManagedExecutionStore::new(state_path),
98            home_dir: home_dir.into(),
99            backend,
100        }
101    }
102
103    pub fn state_path(&self) -> &std::path::Path {
104        self.store.path()
105    }
106
107    /// Probe the backend selected by the current creation policy before an
108    /// image pull or other product preparation. Record creation repeats all
109    /// mutable capability checks before publishing durable state.
110    pub async fn preflight_isolation(
111        &self,
112        isolation: ExecutionIsolation,
113    ) -> ExecutionManagerResult<()> {
114        self.backend.preflight_isolation(isolation).await
115    }
116
117    pub(super) async fn require_running_record(
118        &self,
119        execution_id: &ExecutionId,
120        generation: ExecutionGeneration,
121    ) -> ExecutionManagerResult<BoxRecord> {
122        self.require_live_record(execution_id, generation, false)
123            .await
124    }
125
126    pub(super) async fn require_observable_record(
127        &self,
128        execution_id: &ExecutionId,
129        generation: ExecutionGeneration,
130    ) -> ExecutionManagerResult<BoxRecord> {
131        self.require_live_record(execution_id, generation, true)
132            .await
133    }
134
135    async fn require_live_record(
136        &self,
137        execution_id: &ExecutionId,
138        generation: ExecutionGeneration,
139        allow_paused: bool,
140    ) -> ExecutionManagerResult<BoxRecord> {
141        let record = self
142            .get(execution_id)
143            .await?
144            .ok_or_else(|| ExecutionManagerError::NotFound(execution_id.clone()))?;
145        support::require_generation(&record, execution_id, generation)?;
146        let state = support::managed_state(&record)?;
147        if state != ManagedExecutionState::Running
148            && !(allow_paused && state == ManagedExecutionState::Paused)
149        {
150            return Err(ExecutionManagerError::Conflict {
151                execution_id: execution_id.clone(),
152                message: if allow_paused {
153                    "execution is neither running nor paused".to_string()
154                } else {
155                    "execution is not running".to_string()
156                },
157            });
158        }
159        if let Some(binding) = record
160            .managed_execution
161            .as_ref()
162            .and_then(|metadata| metadata.oci_runtime.as_ref())
163        {
164            binding.validate_for(execution_id)?;
165            return Ok(record);
166        }
167        if record.exec_socket_path.as_os_str().is_empty() {
168            return Err(ExecutionManagerError::Internal(format!(
169                "execution {execution_id} has no exec endpoint"
170            )));
171        }
172        #[cfg(target_os = "linux")]
173        {
174            let pid = record
175                .pid
176                .ok_or_else(|| ExecutionManagerError::NotFound(execution_id.clone()))?;
177            if !crate::process::is_process_alive_with_identity(pid, record.pid_start_time) {
178                return Err(ExecutionManagerError::NotFound(execution_id.clone()));
179            }
180        }
181        Ok(record)
182    }
183
184    pub(super) async fn require_same_runtime(
185        &self,
186        bound: &BoxRecord,
187        execution_id: &ExecutionId,
188        generation: ExecutionGeneration,
189    ) -> ExecutionManagerResult<()> {
190        let current = self
191            .require_running_record(execution_id, generation)
192            .await?;
193        Self::validate_same_runtime(bound, &current, execution_id)
194    }
195
196    pub(super) async fn require_same_observable_runtime(
197        &self,
198        bound: &BoxRecord,
199        execution_id: &ExecutionId,
200        generation: ExecutionGeneration,
201    ) -> ExecutionManagerResult<()> {
202        let current = self
203            .require_observable_record(execution_id, generation)
204            .await?;
205        Self::validate_same_runtime(bound, &current, execution_id)
206    }
207
208    fn validate_same_runtime(
209        bound: &BoxRecord,
210        current: &BoxRecord,
211        execution_id: &ExecutionId,
212    ) -> ExecutionManagerResult<()> {
213        if current.pid != bound.pid
214            || current.pid_start_time != bound.pid_start_time
215            || current.exec_socket_path != bound.exec_socket_path
216            || current
217                .managed_execution
218                .as_ref()
219                .and_then(|metadata| metadata.oci_runtime.as_ref())
220                != bound
221                    .managed_execution
222                    .as_ref()
223                    .and_then(|metadata| metadata.oci_runtime.as_ref())
224        {
225            return Err(ExecutionManagerError::Conflict {
226                execution_id: execution_id.clone(),
227                message: "runtime generation changed while binding its execution session"
228                    .to_string(),
229            });
230        }
231        Ok(())
232    }
233
234    #[cfg(feature = "vm")]
235    pub fn with_vm_backend(state_path: impl Into<PathBuf>, home_dir: impl Into<PathBuf>) -> Self {
236        let home_dir = home_dir.into();
237        Self::new(
238            state_path,
239            home_dir.clone(),
240            Arc::new(VmLocalExecutionBackend::new(home_dir)),
241        )
242    }
243
244    /// Compose the retained Box backend with an explicitly supplied OCI SDK
245    /// backend. The policy affects new reservations only; every selected route
246    /// is persisted before preflight and remains authoritative after restart.
247    #[cfg(feature = "vm")]
248    pub fn with_oci_migration_backend(
249        state_path: impl Into<PathBuf>,
250        home_dir: impl Into<PathBuf>,
251        oci_backend: Arc<dyn LocalExecutionBackend>,
252        policy: OciMigrationPolicy,
253    ) -> Self {
254        Self::with_oci_migration_backend_and_pull_progress(
255            state_path,
256            home_dir,
257            oci_backend,
258            policy,
259            None,
260        )
261    }
262
263    #[cfg(feature = "vm")]
264    fn with_oci_migration_backend_and_pull_progress(
265        state_path: impl Into<PathBuf>,
266        home_dir: impl Into<PathBuf>,
267        oci_backend: Arc<dyn LocalExecutionBackend>,
268        policy: OciMigrationPolicy,
269        pull_progress_fn: Option<crate::PullProgressFn>,
270    ) -> Self {
271        let home_dir = home_dir.into();
272        let mut legacy_backend = VmLocalExecutionBackend::new(home_dir.clone());
273        if let Some(pull_progress_fn) = pull_progress_fn {
274            legacy_backend = legacy_backend.with_pull_progress_fn(pull_progress_fn);
275        }
276        let legacy: Arc<dyn LocalExecutionBackend> = Arc::new(legacy_backend);
277        let router = LocalExecutionBackendRouter::new(legacy, oci_backend, policy);
278        Self::new(state_path, home_dir, Arc::new(router))
279    }
280}
281
282#[cfg(test)]
283mod tests;