a3s_box_runtime/vm/
execution.rs1use super::*;
4
5impl VmManager {
6 #[cfg(unix)]
8 pub fn exec_client(&self) -> Option<&ExecClient> {
9 self.exec_client.as_ref()
10 }
11
12 #[cfg(unix)]
13 async fn connect_exec_client_for_request(socket_path: &Path) -> Result<ExecClient> {
14 const ATTEMPT_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(500);
15
16 let client = ExecClient::connect(socket_path).await?;
17 match tokio::time::timeout(ATTEMPT_TIMEOUT, client.heartbeat()).await {
18 Ok(Ok(true)) => Ok(client),
19 Ok(Ok(false)) => Err(BoxError::ExecError(format!(
20 "Exec client not connected: heartbeat failed at {}",
21 socket_path.display()
22 ))),
23 Ok(Err(error)) => Err(error),
24 Err(_) => Err(BoxError::ExecError(format!(
25 "Exec client not connected: heartbeat timed out at {}",
26 socket_path.display()
27 ))),
28 }
29 }
30
31 #[cfg(unix)]
37 pub async fn wait_for_exec_available(&mut self, timeout: std::time::Duration) -> Result<()> {
38 let socket_path = self
39 .exec_socket_path
40 .clone()
41 .ok_or_else(|| BoxError::ExecError("Exec socket path is unavailable".to_string()))?;
42 let deadline = tokio::time::Instant::now() + timeout;
43 loop {
44 match Self::connect_exec_client_for_request(&socket_path).await {
45 Ok(client) => {
46 self.exec_client = Some(client);
47 return Ok(());
48 }
49 Err(error) if tokio::time::Instant::now() < deadline => {
50 tracing::debug!(%error, "Waiting for pooled VM exec readiness");
51 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
52 }
53 Err(error) => return Err(error),
54 }
55 }
56 }
57
58 #[cfg(not(unix))]
59 pub async fn wait_for_exec_available(&mut self, _timeout: std::time::Duration) -> Result<()> {
60 Ok(())
61 }
62
63 #[cfg(unix)]
69 pub async fn attach_running_process(
70 &mut self,
71 pid: u32,
72 exec_socket_path: PathBuf,
73 pty_socket_path: Option<PathBuf>,
74 ) -> Result<()> {
75 let port_forward_socket_path = exec_socket_path.with_file_name("portfwd.sock");
76 let handler = crate::vmm::ShimHandler::from_pid(pid, self.box_id.clone());
77 if !handler.is_running() {
78 return Err(BoxError::StateError(format!(
79 "Cannot attach to non-running VM process {pid}"
80 )));
81 }
82
83 self.exec_client = match ExecClient::connect(&exec_socket_path).await {
84 Ok(client) => Some(client),
85 Err(error) => {
86 tracing::debug!(
87 box_id = %self.box_id,
88 socket_path = %exec_socket_path.display(),
89 error = %error,
90 "Failed to reconnect exec client while attaching to running VM"
91 );
92 None
93 }
94 };
95 self.exec_socket_path = Some(exec_socket_path);
96 self.pty_socket_path = pty_socket_path;
97 self.port_forward_socket_path = Some(port_forward_socket_path);
98 *self.handler.write().await = Some(Box::new(handler));
99 *self.state.write().await = BoxState::Ready;
100 Ok(())
101 }
102
103 #[cfg(windows)]
105 pub async fn attach_running_process(
106 &mut self,
107 pid: u32,
108 exec_socket_path: PathBuf,
109 pty_socket_path: Option<PathBuf>,
110 ) -> Result<()> {
111 let handler = crate::vmm::ShimHandler::from_pid(pid, self.box_id.clone());
112 if !handler.is_running() {
113 return Err(BoxError::StateError(format!(
114 "Cannot attach to non-running VM process {pid}"
115 )));
116 }
117
118 self.exec_socket_path = Some(exec_socket_path);
119 self.pty_socket_path = pty_socket_path;
120 self.port_forward_socket_path = None;
121 *self.handler.write().await = Some(Box::new(handler));
122 *self.state.write().await = BoxState::Ready;
123 Ok(())
124 }
125
126 pub fn exec_socket_path(&self) -> Option<&Path> {
128 self.exec_socket_path.as_deref()
129 }
130
131 pub fn pty_socket_path(&self) -> Option<&Path> {
133 self.pty_socket_path.as_deref()
134 }
135
136 pub fn port_forward_socket_path(&self) -> Option<&Path> {
138 self.port_forward_socket_path.as_deref()
139 }
140
141 pub fn set_provider(&mut self, provider: Box<dyn VmmProvider>) {
146 self.provider = Some(provider);
147 }
148
149 pub fn set_rootfs_provider(&mut self, provider: Box<dyn crate::rootfs::RootfsProvider>) {
154 self.rootfs_provider = provider;
155 }
156
157 pub fn rootfs_provider_name(&self) -> &str {
159 self.rootfs_provider.name()
160 }
161
162 pub fn set_pull_progress_fn(&mut self, f: PullProgressFn) {
165 self.pull_progress_fn = Some(f);
166 }
167
168 pub fn set_metrics(&mut self, metrics: crate::prom::RuntimeMetrics) {
170 self.prom = Some(metrics);
171 }
172
173 pub fn set_log_config(&mut self, log_config: a3s_box_core::log::LogConfig) {
176 self.log_config = log_config;
177 }
178
179 pub fn set_healthcheck_disabled(&mut self, disabled: bool) {
181 self.healthcheck_disabled = disabled;
182 }
183
184 pub fn metrics_prom(&self) -> Option<&crate::prom::RuntimeMetrics> {
186 self.prom.as_ref()
187 }
188
189 pub fn anonymous_volumes(&self) -> &[String] {
194 &self.anonymous_volumes
195 }
196
197 pub fn image_config(&self) -> Option<&crate::oci::OciImageConfig> {
199 self.image_config.as_ref()
200 }
201
202 pub fn resolved_execution_plan(&self) -> Option<&ResolvedExecutionPlan> {
204 self.resolved_execution_plan.as_ref()
205 }
206
207 pub fn exit_code(&self) -> Option<i32> {
213 self.shim_exit_code
214 }
215
216 #[cfg(not(target_os = "windows"))]
217 fn persisted_exit_code(&self) -> Option<i32> {
218 crate::rootfs::read_persisted_exit_code(&self.home_dir.join("boxes").join(&self.box_id))
219 }
220
221 pub async fn try_wait_exit(&mut self) -> Result<Option<i32>> {
227 if let Some(code) = self.shim_exit_code {
228 return Ok(Some(code));
229 }
230
231 #[cfg(not(target_os = "windows"))]
232 let box_dir = self.home_dir.join("boxes").join(&self.box_id);
233
234 let mut handler = self.handler.write().await;
235 let Some(handler) = handler.as_mut() else {
236 #[cfg(not(target_os = "windows"))]
240 if let Some(code) = crate::rootfs::read_persisted_exit_code(&box_dir) {
241 self.shim_exit_code = Some(code);
242 }
243 return Ok(self.shim_exit_code);
244 };
245
246 if let Some(code) = handler.try_wait_exit()? {
247 #[cfg(target_os = "windows")]
248 let code = collect_windows_guest_result(
249 &self.home_dir.join("boxes").join(&self.box_id),
250 &self.log_config,
251 code,
252 )?;
253 #[cfg(not(target_os = "windows"))]
254 let Some(code) = crate::rootfs::resolve_workload_exit_code(&box_dir, Some(code)) else {
255 return Ok(None);
256 };
257 self.shim_exit_code = Some(code);
258 return Ok(Some(code));
259 }
260
261 #[cfg(not(target_os = "windows"))]
262 if handler.has_exited() {
263 if let Some(code) =
268 crate::rootfs::resolve_workload_exit_code(&box_dir, handler.exit_code())
269 {
270 self.shim_exit_code = Some(code);
271 return Ok(Some(code));
272 }
273 }
274
275 Ok(None)
276 }
277
278 pub async fn has_exited(&self) -> bool {
284 if self.shim_exit_code.is_some() {
285 return true;
286 }
287
288 let handler = self.handler.read().await;
289 if let Some(handler) = handler.as_ref() {
290 return handler.has_exited();
291 }
292 drop(handler);
293
294 #[cfg(not(target_os = "windows"))]
295 {
296 self.persisted_exit_code().is_some()
297 }
298
299 #[cfg(target_os = "windows")]
300 {
301 false
302 }
303 }
304
305 #[cfg(unix)]
313 pub async fn run_deferred_main(
314 &mut self,
315 spec_json: &[u8],
316 timeout: std::time::Duration,
317 ) -> Result<a3s_box_core::exec::ExecOutput> {
318 let log_dir = self.home_dir.join("boxes").join(&self.box_id).join("logs");
319 let console_out_path = log_dir.join("console.log");
320 let console_err_path = a3s_box_core::log::stderr_console_path(&console_out_path);
321 let console_out_start = std::fs::metadata(&console_out_path)
322 .map(|metadata| metadata.len())
323 .unwrap_or(0);
324 let console_err_start = std::fs::metadata(&console_err_path)
325 .map(|metadata| metadata.len())
326 .unwrap_or(0);
327
328 let acked = {
329 let owned_client;
330 let client = if let Some(client) = self.exec_client.as_ref() {
331 client
332 } else {
333 let socket_path = self
334 .exec_socket_path
335 .as_deref()
336 .ok_or_else(|| BoxError::ExecError("Exec client not connected".to_string()))?;
337 owned_client = Self::connect_exec_client_for_request(socket_path).await?;
338 &owned_client
339 };
340 client.spawn_main(Some(spec_json)).await?
341 };
342 let exit_wait_timeout = if acked {
343 timeout
344 } else {
345 tracing::debug!(
351 box_id = %self.box_id,
352 "spawn-main was not acknowledged; waiting briefly for main exit"
353 );
354 timeout.min(std::time::Duration::from_secs(2))
355 };
356
357 let start = std::time::Instant::now();
359 let exit_code = loop {
360 if let Some(code) = self.try_wait_exit().await? {
361 break code;
362 }
363 if start.elapsed() >= exit_wait_timeout {
364 let message = if acked {
365 "deferred main did not exit within the timeout"
366 } else {
367 "spawn-main was not acknowledged by the guest"
368 };
369 return Err(BoxError::ExecError(message.to_string()));
370 }
371 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
372 };
373
374 let json_path = log_dir.join("container.json");
381 let drain_start = std::time::Instant::now();
382 let max_wait = std::time::Duration::from_secs(2);
383 let min_wait = std::time::Duration::from_millis(500);
384 let quiet_window = std::time::Duration::from_millis(200);
385 let mut last_len: Option<u64> = None;
386 let mut last_change = drain_start;
387 loop {
388 let len = std::fs::metadata(&json_path).map(|m| m.len()).unwrap_or(0);
389 if last_len != Some(len) {
390 last_len = Some(len);
391 last_change = std::time::Instant::now();
392 }
393 let elapsed = drain_start.elapsed();
394 if elapsed >= max_wait || (elapsed >= min_wait && last_change.elapsed() >= quiet_window)
395 {
396 break;
397 }
398 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
399 }
400 let (mut stdout, mut stderr) = self.read_container_logs();
401 if stdout.is_empty() {
402 stdout = Self::read_file_from_offset(&console_out_path, console_out_start);
403 }
404 if stderr.is_empty() {
405 stderr = Self::read_file_from_offset(&console_err_path, console_err_start);
406 }
407 let truncated = stdout.len() > a3s_box_core::exec::MAX_OUTPUT_BYTES
408 || stderr.len() > a3s_box_core::exec::MAX_OUTPUT_BYTES;
409 stdout.truncate(a3s_box_core::exec::MAX_OUTPUT_BYTES);
410 stderr.truncate(a3s_box_core::exec::MAX_OUTPUT_BYTES);
411 Ok(a3s_box_core::exec::ExecOutput {
412 stdout,
413 stderr,
414 exit_code,
415 truncated,
416 })
417 }
418
419 #[cfg(unix)]
420 fn read_file_from_offset(path: &Path, offset: u64) -> Vec<u8> {
421 use std::io::{Read, Seek, SeekFrom};
422
423 let mut file = match std::fs::File::open(path) {
424 Ok(file) => file,
425 Err(_) => return vec![],
426 };
427 if file.seek(SeekFrom::Start(offset)).is_err() {
428 return vec![];
429 }
430
431 let mut bytes = Vec::new();
432 if file.read_to_end(&mut bytes).is_err() {
433 return vec![];
434 }
435 bytes
436 }
437
438 #[cfg(unix)]
440 fn read_container_logs(&self) -> (Vec<u8>, Vec<u8>) {
441 let path = self
442 .home_dir
443 .join("boxes")
444 .join(&self.box_id)
445 .join("logs")
446 .join("container.json");
447 let (mut out, mut err) = (Vec::new(), Vec::new());
448 if let Ok(content) = std::fs::read_to_string(&path) {
449 for line in content.lines() {
450 if let Ok(entry) = serde_json::from_str::<a3s_box_core::log::LogEntry>(line) {
451 if entry.stream == "stderr" {
452 err.extend_from_slice(entry.log.as_bytes());
453 } else {
454 out.extend_from_slice(entry.log.as_bytes());
455 }
456 }
457 }
458 }
459 (out, err)
460 }
461
462 #[cfg(unix)]
466 #[tracing::instrument(skip(self, request), fields(box_id = %self.box_id))]
467 pub async fn exec_request(
468 &self,
469 request: &a3s_box_core::exec::ExecRequest,
470 ) -> Result<a3s_box_core::exec::ExecOutput> {
471 if request.cmd.is_empty() {
472 return Err(BoxError::ExecError(
473 "Exec request requires a non-empty command".to_string(),
474 ));
475 }
476
477 let state = self.state.read().await;
478 match *state {
479 BoxState::Ready | BoxState::Busy | BoxState::Compacting => {}
480 BoxState::Created => {
481 return Err(BoxError::ExecError("VM not yet booted".to_string()));
482 }
483 BoxState::Stopped => {
484 return Err(BoxError::ExecError("VM is stopped".to_string()));
485 }
486 }
487 drop(state);
488
489 let owned_client;
490 let client = if let Some(client) = self.exec_client.as_ref() {
491 client
492 } else {
493 let socket_path = self
494 .exec_socket_path
495 .as_deref()
496 .ok_or_else(|| BoxError::ExecError("Exec client not connected".to_string()))?;
497 owned_client = Self::connect_exec_client_for_request(socket_path).await?;
498 &owned_client
499 };
500
501 let exec_start = std::time::Instant::now();
502 let result = client.exec_command(request).await;
503
504 if let Some(ref prom) = self.prom {
506 prom.exec_total.inc();
507 prom.exec_duration
508 .observe(exec_start.elapsed().as_secs_f64());
509 if result.is_err() || result.as_ref().is_ok_and(|o| o.exit_code != 0) {
510 prom.exec_errors_total.inc();
511 }
512 }
513
514 result
515 }
516
517 #[cfg(unix)]
521 #[tracing::instrument(skip(self, cmd), fields(box_id = %self.box_id))]
522 pub async fn exec_command(
523 &self,
524 cmd: Vec<String>,
525 timeout_ns: u64,
526 ) -> Result<a3s_box_core::exec::ExecOutput> {
527 let request = a3s_box_core::exec::ExecRequest {
528 request_id: None,
529 cmd,
530 timeout_ns,
531 env: vec![],
532 working_dir: None,
533 rootfs: None,
534 stdin: None,
535 stdin_streaming: false,
536 user: None,
537 streaming: false,
538 };
539
540 self.exec_request(&request).await
541 }
542}