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(crate) fn boot_phase_timer(&self, phase: &'static str) -> crate::prom::BootPhaseTimer {
180 crate::prom::BootPhaseTimer::new(self.prom.clone(), phase)
181 }
182
183 pub fn set_log_config(&mut self, log_config: a3s_box_core::log::LogConfig) {
186 self.log_config = log_config;
187 }
188
189 pub fn set_healthcheck_disabled(&mut self, disabled: bool) {
191 self.healthcheck_disabled = disabled;
192 }
193
194 pub fn metrics_prom(&self) -> Option<&crate::prom::RuntimeMetrics> {
196 self.prom.as_ref()
197 }
198
199 pub fn anonymous_volumes(&self) -> &[String] {
204 &self.anonymous_volumes
205 }
206
207 pub fn image_config(&self) -> Option<&crate::oci::OciImageConfig> {
209 self.image_config.as_ref()
210 }
211
212 pub fn resolved_execution_plan(&self) -> Option<&ResolvedExecutionPlan> {
214 self.resolved_execution_plan.as_ref()
215 }
216
217 pub fn exit_code(&self) -> Option<i32> {
223 self.shim_exit_code
224 }
225
226 #[cfg(not(target_os = "windows"))]
227 fn persisted_exit_code(&self) -> Option<i32> {
228 crate::rootfs::read_persisted_exit_code(&self.home_dir.join("boxes").join(&self.box_id))
229 }
230
231 pub async fn try_wait_exit(&mut self) -> Result<Option<i32>> {
237 if let Some(code) = self.shim_exit_code {
238 return Ok(Some(code));
239 }
240
241 #[cfg(not(target_os = "windows"))]
242 let box_dir = self.home_dir.join("boxes").join(&self.box_id);
243
244 let mut handler = self.handler.write().await;
245 let Some(handler) = handler.as_mut() else {
246 #[cfg(not(target_os = "windows"))]
250 if let Some(code) = crate::rootfs::read_persisted_exit_code(&box_dir) {
251 self.shim_exit_code = Some(code);
252 }
253 return Ok(self.shim_exit_code);
254 };
255
256 if let Some(code) = handler.try_wait_exit()? {
257 #[cfg(target_os = "windows")]
258 let code = collect_windows_guest_result(
259 &self.home_dir.join("boxes").join(&self.box_id),
260 &self.log_config,
261 code,
262 )?;
263 #[cfg(not(target_os = "windows"))]
264 let Some(code) = crate::rootfs::resolve_workload_exit_code(&box_dir, Some(code)) else {
265 return Ok(None);
266 };
267 self.shim_exit_code = Some(code);
268 return Ok(Some(code));
269 }
270
271 #[cfg(not(target_os = "windows"))]
272 if handler.has_exited() {
273 if let Some(code) =
278 crate::rootfs::resolve_workload_exit_code(&box_dir, handler.exit_code())
279 {
280 self.shim_exit_code = Some(code);
281 return Ok(Some(code));
282 }
283 }
284
285 Ok(None)
286 }
287
288 pub async fn has_exited(&self) -> bool {
294 if self.shim_exit_code.is_some() {
295 return true;
296 }
297
298 let handler = self.handler.read().await;
299 if let Some(handler) = handler.as_ref() {
300 return handler.has_exited();
301 }
302 drop(handler);
303
304 #[cfg(not(target_os = "windows"))]
305 {
306 self.persisted_exit_code().is_some()
307 }
308
309 #[cfg(target_os = "windows")]
310 {
311 false
312 }
313 }
314
315 #[cfg(unix)]
323 pub async fn run_deferred_main(
324 &mut self,
325 spec_json: &[u8],
326 timeout: std::time::Duration,
327 ) -> Result<a3s_box_core::exec::ExecOutput> {
328 let log_dir = self.home_dir.join("boxes").join(&self.box_id).join("logs");
329 let console_out_path = log_dir.join("console.log");
330 let console_err_path = a3s_box_core::log::stderr_console_path(&console_out_path);
331 let console_out_start = std::fs::metadata(&console_out_path)
332 .map(|metadata| metadata.len())
333 .unwrap_or(0);
334 let console_err_start = std::fs::metadata(&console_err_path)
335 .map(|metadata| metadata.len())
336 .unwrap_or(0);
337
338 let acked = {
339 let owned_client;
340 let client = if let Some(client) = self.exec_client.as_ref() {
341 client
342 } else {
343 let socket_path = self
344 .exec_socket_path
345 .as_deref()
346 .ok_or_else(|| BoxError::ExecError("Exec client not connected".to_string()))?;
347 owned_client = Self::connect_exec_client_for_request(socket_path).await?;
348 &owned_client
349 };
350 client.spawn_main(Some(spec_json)).await?
351 };
352 let exit_wait_timeout = if acked {
353 timeout
354 } else {
355 tracing::debug!(
361 box_id = %self.box_id,
362 "spawn-main was not acknowledged; waiting briefly for main exit"
363 );
364 timeout.min(std::time::Duration::from_secs(2))
365 };
366
367 let start = std::time::Instant::now();
369 let exit_code = loop {
370 if let Some(code) = self.try_wait_exit().await? {
371 break code;
372 }
373 if start.elapsed() >= exit_wait_timeout {
374 let message = if acked {
375 "deferred main did not exit within the timeout"
376 } else {
377 "spawn-main was not acknowledged by the guest"
378 };
379 return Err(BoxError::ExecError(message.to_string()));
380 }
381 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
382 };
383
384 let json_path = log_dir.join("container.json");
391 let drain_start = std::time::Instant::now();
392 let max_wait = std::time::Duration::from_secs(2);
393 let min_wait = std::time::Duration::from_millis(500);
394 let quiet_window = std::time::Duration::from_millis(200);
395 let mut last_len: Option<u64> = None;
396 let mut last_change = drain_start;
397 loop {
398 let len = std::fs::metadata(&json_path).map(|m| m.len()).unwrap_or(0);
399 if last_len != Some(len) {
400 last_len = Some(len);
401 last_change = std::time::Instant::now();
402 }
403 let elapsed = drain_start.elapsed();
404 if elapsed >= max_wait || (elapsed >= min_wait && last_change.elapsed() >= quiet_window)
405 {
406 break;
407 }
408 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
409 }
410 let (mut stdout, mut stderr) = self.read_container_logs();
411 if stdout.is_empty() {
412 stdout = Self::read_file_from_offset(&console_out_path, console_out_start);
413 }
414 if stderr.is_empty() {
415 stderr = Self::read_file_from_offset(&console_err_path, console_err_start);
416 }
417 let truncated = stdout.len() > a3s_box_core::exec::MAX_OUTPUT_BYTES
418 || stderr.len() > a3s_box_core::exec::MAX_OUTPUT_BYTES;
419 stdout.truncate(a3s_box_core::exec::MAX_OUTPUT_BYTES);
420 stderr.truncate(a3s_box_core::exec::MAX_OUTPUT_BYTES);
421 Ok(a3s_box_core::exec::ExecOutput {
422 stdout,
423 stderr,
424 exit_code,
425 truncated,
426 })
427 }
428
429 #[cfg(unix)]
430 fn read_file_from_offset(path: &Path, offset: u64) -> Vec<u8> {
431 use std::io::{Read, Seek, SeekFrom};
432
433 let mut file = match std::fs::File::open(path) {
434 Ok(file) => file,
435 Err(_) => return vec![],
436 };
437 if file.seek(SeekFrom::Start(offset)).is_err() {
438 return vec![];
439 }
440
441 let mut bytes = Vec::new();
442 if file.read_to_end(&mut bytes).is_err() {
443 return vec![];
444 }
445 bytes
446 }
447
448 #[cfg(unix)]
450 fn read_container_logs(&self) -> (Vec<u8>, Vec<u8>) {
451 let path = self
452 .home_dir
453 .join("boxes")
454 .join(&self.box_id)
455 .join("logs")
456 .join("container.json");
457 let (mut out, mut err) = (Vec::new(), Vec::new());
458 if let Ok(content) = std::fs::read_to_string(&path) {
459 for line in content.lines() {
460 if let Ok(entry) = serde_json::from_str::<a3s_box_core::log::LogEntry>(line) {
461 if entry.stream == "stderr" {
462 err.extend_from_slice(entry.log.as_bytes());
463 } else {
464 out.extend_from_slice(entry.log.as_bytes());
465 }
466 }
467 }
468 }
469 (out, err)
470 }
471
472 #[cfg(unix)]
476 #[tracing::instrument(skip(self, request), fields(box_id = %self.box_id))]
477 pub async fn exec_request(
478 &self,
479 request: &a3s_box_core::exec::ExecRequest,
480 ) -> Result<a3s_box_core::exec::ExecOutput> {
481 if request.cmd.is_empty() {
482 return Err(BoxError::ExecError(
483 "Exec request requires a non-empty command".to_string(),
484 ));
485 }
486
487 let state = self.state.read().await;
488 match *state {
489 BoxState::Ready | BoxState::Busy | BoxState::Compacting => {}
490 BoxState::Created => {
491 return Err(BoxError::ExecError("VM not yet booted".to_string()));
492 }
493 BoxState::Stopped => {
494 return Err(BoxError::ExecError("VM is stopped".to_string()));
495 }
496 }
497 drop(state);
498
499 let owned_client;
500 let client = if let Some(client) = self.exec_client.as_ref() {
501 client
502 } else {
503 let socket_path = self
504 .exec_socket_path
505 .as_deref()
506 .ok_or_else(|| BoxError::ExecError("Exec client not connected".to_string()))?;
507 owned_client = Self::connect_exec_client_for_request(socket_path).await?;
508 &owned_client
509 };
510
511 let exec_start = std::time::Instant::now();
512 let result = client.exec_command(request).await;
513
514 if let Some(ref prom) = self.prom {
516 prom.exec_total.inc();
517 prom.exec_duration
518 .observe(exec_start.elapsed().as_secs_f64());
519 if result.is_err() || result.as_ref().is_ok_and(|o| o.exit_code != 0) {
520 prom.exec_errors_total.inc();
521 }
522 }
523
524 result
525 }
526
527 #[cfg(unix)]
531 #[tracing::instrument(skip(self, cmd), fields(box_id = %self.box_id))]
532 pub async fn exec_command(
533 &self,
534 cmd: Vec<String>,
535 timeout_ns: u64,
536 ) -> Result<a3s_box_core::exec::ExecOutput> {
537 let request = a3s_box_core::exec::ExecRequest {
538 request_id: None,
539 cmd,
540 timeout_ns,
541 env: vec![],
542 working_dir: None,
543 rootfs: None,
544 stdin: None,
545 stdin_streaming: false,
546 user: None,
547 streaming: false,
548 };
549
550 self.exec_request(&request).await
551 }
552}