1use std::collections::HashMap;
2use std::io::{BufReader, Read, Write};
3use std::net::TcpStream;
4use std::sync::Arc;
5
6use anyhow::{bail, Result};
7use tokio::sync::{mpsc, oneshot};
8use tracing::info;
9
10pub use vm::{default_data_dir, MountConfig};
12pub use vm_proto::{DirEntry, ReadDirResponse, StatResponse, WatchEvent};
13pub use vm_proxy::config::{ExposeHostMapping, NetworkConfig, ProxyConfig, SecretConfig};
14
15pub struct GuestStream(TcpStream);
27
28impl GuestStream {
29 pub fn into_inner(self) -> TcpStream {
31 self.0
32 }
33
34 pub fn try_clone(&self) -> std::io::Result<Self> {
36 self.0.try_clone().map(GuestStream)
37 }
38}
39
40impl Read for GuestStream {
41 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
42 self.0.read(buf)
43 }
44}
45
46impl Write for GuestStream {
47 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
48 self.0.write(buf)
49 }
50
51 fn flush(&mut self) -> std::io::Result<()> {
52 self.0.flush()
53 }
54}
55
56#[derive(Debug, Clone, Default)]
58pub enum StorageMode {
59 #[default]
61 Direct,
62 #[cfg(feature = "cas")]
64 Cas {
65 cas_dir: Option<String>,
67 },
68}
69
70#[derive(Debug, Clone)]
72pub struct SandboxConfig {
73 pub data_dir: Option<String>,
76 pub cpus: usize,
78 pub memory_mb: u64,
80 pub disk_size_mb: u64,
82 pub mounts: Vec<MountConfig>,
84 pub allow_net: bool,
86 pub secrets: HashMap<String, SecretConfig>,
88 pub allowed_hosts: Vec<String>,
90 pub ports: Vec<vm_proto::PortMapping>,
92 pub expose_host: Vec<ExposeHostMapping>,
94 pub from: Option<String>,
96 pub storage: StorageMode,
98 pub env: HashMap<String, String>,
101}
102
103impl Default for SandboxConfig {
104 fn default() -> Self {
105 Self {
106 data_dir: None,
107 cpus: 2,
108 memory_mb: 2048,
109 disk_size_mb: 4096,
110 mounts: vec![],
111 allow_net: false,
112 secrets: HashMap::new(),
113 allowed_hosts: vec![],
114 ports: vec![],
115 expose_host: vec![],
116 from: None,
117 storage: StorageMode::default(),
118 env: HashMap::new(),
119 }
120 }
121}
122
123#[derive(Debug)]
125pub struct ExecResult {
126 pub stdout: String,
127 pub stderr: String,
128 pub exit_code: i32,
129}
130
131#[derive(Debug)]
133pub enum ShellEvent {
134 Output(Vec<u8>),
136 Exit(i32),
138 Error(String),
140}
141
142#[derive(Clone)]
144pub struct ShellWriter {
145 writer: Arc<std::sync::Mutex<TcpStream>>,
146}
147
148impl ShellWriter {
149 pub fn send_input(&self, data: &[u8]) -> Result<()> {
151 use std::io::Write;
152 let mut w = self.writer.lock().unwrap();
153 vm_proto::frame::write_frame(&mut *w, vm_proto::frame::STDIN, data)?;
154 w.flush()?;
155 Ok(())
156 }
157
158 pub fn resize(&self, rows: u16, cols: u16) -> Result<()> {
160 let mut w = self.writer.lock().unwrap();
161 let payload = vm_proto::frame::resize_payload(rows, cols);
162 vm_proto::frame::write_frame(&mut *w, vm_proto::frame::RESIZE, &payload)?;
163 Ok(())
164 }
165}
166
167pub struct ShellReader {
169 output_rx: mpsc::UnboundedReceiver<ShellEvent>,
170}
171
172impl ShellReader {
173 pub async fn recv(&mut self) -> Option<ShellEvent> {
175 self.output_rx.recv().await
176 }
177}
178
179pub struct ShellHandle {
181 writer: ShellWriter,
182 reader: ShellReader,
183 _reader_thread: std::thread::JoinHandle<()>,
184}
185
186impl ShellHandle {
187 pub fn split(self) -> (ShellWriter, ShellReader) {
189 std::mem::forget(self._reader_thread);
191 (self.writer, self.reader)
192 }
193}
194
195pub struct WatchReceiver {
197 event_rx: mpsc::UnboundedReceiver<WatchEvent>,
198}
199
200impl WatchReceiver {
201 pub async fn recv(&mut self) -> Option<WatchEvent> {
203 self.event_rx.recv().await
204 }
205
206 pub fn try_recv(&mut self) -> Result<WatchEvent, tokio::sync::mpsc::error::TryRecvError> {
208 self.event_rx.try_recv()
209 }
210}
211
212pub struct WatchHandle {
214 pub receiver: WatchReceiver,
215 _reader_thread: std::thread::JoinHandle<()>,
216}
217
218enum SandboxCmd {
223 Exec {
224 argv: Vec<String>,
225 reply: oneshot::Sender<Result<ExecResult>>,
226 },
227 ReadFile {
228 path: String,
229 reply: oneshot::Sender<Result<Vec<u8>>>,
230 },
231 WriteFile {
232 path: String,
233 content: Vec<u8>,
234 reply: oneshot::Sender<Result<()>>,
235 },
236 ReadDir {
237 path: String,
238 reply: oneshot::Sender<Result<ReadDirResponse>>,
239 },
240 Mkdir {
241 path: String,
242 recursive: bool,
243 reply: oneshot::Sender<Result<()>>,
244 },
245 Rename {
246 old_path: String,
247 new_path: String,
248 reply: oneshot::Sender<Result<()>>,
249 },
250 Chmod {
251 path: String,
252 mode: u32,
253 reply: oneshot::Sender<Result<()>>,
254 },
255 Download {
256 url: String,
257 path: String,
258 extract: bool,
259 strip_components: u32,
260 progress_tx: std::sync::mpsc::Sender<vm_proto::DownloadProgress>,
261 reply: oneshot::Sender<Result<()>>,
262 },
263 OpenShell {
264 argv: Option<Vec<String>>,
265 rows: u16,
266 cols: u16,
267 cwd: Option<String>,
268 extra_env: HashMap<String, String>,
269 reply: oneshot::Sender<Result<TcpStream>>,
270 },
271 Watch {
272 path: String,
273 recursive: bool,
274 reply: oneshot::Sender<Result<TcpStream>>,
275 },
276 Remove {
277 path: String,
278 recursive: bool,
279 reply: oneshot::Sender<Result<()>>,
280 },
281 DiscardOverlay {
282 path: String,
283 reply: oneshot::Sender<Result<()>>,
284 },
285 AddPortForward {
286 mapping: vm_proto::PortMapping,
287 reply: oneshot::Sender<Result<()>>,
288 },
289 DialGuest {
290 port: u16,
291 reply: oneshot::Sender<Result<TcpStream>>,
292 },
293 Checkpoint {
294 name: String,
295 reply: oneshot::Sender<Result<()>>,
296 },
297 Stop {
298 reply: oneshot::Sender<Result<()>>,
299 },
300}
301
302pub struct AsyncSandbox {
312 cmd_tx: std::sync::mpsc::Sender<SandboxCmd>,
313 instance_dir: String,
314}
315
316impl AsyncSandbox {
317 pub async fn boot(config: SandboxConfig) -> Result<Self> {
319 let (ready_tx, ready_rx) = oneshot::channel::<Result<String>>();
320 let (cmd_tx, cmd_rx) = std::sync::mpsc::channel();
321
322 std::thread::Builder::new()
323 .name("hanzo-vm".into())
324 .spawn(move || match boot_vm(config) {
325 Ok(booted) => {
326 if ready_tx.send(Ok(booted.instance_dir.clone())).is_err() {
327 return;
328 }
329 run_vm_loop(
330 booted.sandbox,
331 &booted.instance_dir,
332 &booted.data_dir,
333 cmd_rx,
334 booted.proxy_handle,
335 booted.fwd_handle,
336 #[cfg(feature = "cas")]
337 booted.nbd_handle,
338 booted.default_env,
339 );
340 }
341 Err(e) => {
342 let _ = ready_tx.send(Err(e));
343 }
344 })?;
345
346 let instance_dir = ready_rx.await??;
347
348 Ok(Self {
349 cmd_tx,
350 instance_dir,
351 })
352 }
353
354 pub async fn exec(&self, argv: &[&str]) -> Result<ExecResult> {
356 let (reply_tx, reply_rx) = oneshot::channel();
357 self.cmd_tx
358 .send(SandboxCmd::Exec {
359 argv: argv.iter().map(|s| s.to_string()).collect(),
360 reply: reply_tx,
361 })
362 .map_err(|_| anyhow::anyhow!("VM thread exited"))?;
363 reply_rx.await?
364 }
365
366 pub async fn exec_shell(&self, command: &str) -> Result<ExecResult> {
368 self.exec_in("/bin/sh", command).await
369 }
370
371 pub async fn exec_in(&self, shell: &str, command: &str) -> Result<ExecResult> {
376 self.exec(&[shell, "-c", command]).await
377 }
378
379 pub async fn open_shell(
384 &self,
385 rows: u16,
386 cols: u16,
387 cwd: Option<&str>,
388 argv: Option<&[&str]>,
389 extra_env: HashMap<String, String>,
390 ) -> Result<ShellHandle> {
391 let (reply_tx, reply_rx) = oneshot::channel();
392 self.cmd_tx
393 .send(SandboxCmd::OpenShell {
394 argv: argv.map(|a| a.iter().map(|s| s.to_string()).collect()),
395 rows,
396 cols,
397 cwd: cwd.map(|s| s.to_string()),
398 extra_env,
399 reply: reply_tx,
400 })
401 .map_err(|_| anyhow::anyhow!("VM thread exited"))?;
402 let stream = reply_rx.await??;
403
404 let writer_stream = stream.try_clone()?;
406 let reader_stream = stream;
407
408 let (event_tx, event_rx) = mpsc::unbounded_channel();
409
410 let reader_thread = std::thread::Builder::new()
411 .name("vm-shell-reader".into())
412 .spawn(move || {
413 let mut reader = BufReader::new(reader_stream);
414 loop {
415 match vm_proto::frame::read_frame(&mut reader) {
416 Ok(Some((vm_proto::frame::STDOUT, payload))) => {
417 if event_tx.send(ShellEvent::Output(payload)).is_err() {
418 break;
419 }
420 }
421 Ok(Some((vm_proto::frame::EXIT, payload))) => {
422 let code = vm_proto::frame::parse_exit_code(&payload).unwrap_or(0);
423 let _ = event_tx.send(ShellEvent::Exit(code));
424 break;
425 }
426 Ok(Some((vm_proto::frame::ERROR, payload))) => {
427 let msg = String::from_utf8_lossy(&payload).to_string();
428 let _ = event_tx.send(ShellEvent::Error(msg));
429 break;
430 }
431 Ok(Some(_)) => {} Ok(None) | Err(_) => break,
433 }
434 }
435 })?;
436
437 Ok(ShellHandle {
438 writer: ShellWriter {
439 writer: Arc::new(std::sync::Mutex::new(writer_stream)),
440 },
441 reader: ShellReader {
442 output_rx: event_rx,
443 },
444 _reader_thread: reader_thread,
445 })
446 }
447
448 pub async fn read_file(&self, path: &str) -> Result<Vec<u8>> {
450 let (reply_tx, reply_rx) = oneshot::channel();
451 self.cmd_tx
452 .send(SandboxCmd::ReadFile {
453 path: path.to_string(),
454 reply: reply_tx,
455 })
456 .map_err(|_| anyhow::anyhow!("VM thread exited"))?;
457 reply_rx.await?
458 }
459
460 pub async fn write_file(&self, path: &str, content: &[u8]) -> Result<()> {
462 let (reply_tx, reply_rx) = oneshot::channel();
463 self.cmd_tx
464 .send(SandboxCmd::WriteFile {
465 path: path.to_string(),
466 content: content.to_vec(),
467 reply: reply_tx,
468 })
469 .map_err(|_| anyhow::anyhow!("VM thread exited"))?;
470 reply_rx.await?
471 }
472
473 pub async fn discard_overlay(&self, path: &str) -> Result<()> {
476 let (reply_tx, reply_rx) = oneshot::channel();
477 self.cmd_tx
478 .send(SandboxCmd::DiscardOverlay {
479 path: path.to_string(),
480 reply: reply_tx,
481 })
482 .map_err(|_| anyhow::anyhow!("VM thread exited"))?;
483 reply_rx.await?
484 }
485
486 pub async fn add_port_forward(&self, host_port: u16, guest_port: u16) -> Result<()> {
488 let (reply_tx, reply_rx) = oneshot::channel();
489 self.cmd_tx
490 .send(SandboxCmd::AddPortForward {
491 mapping: vm_proto::PortMapping {
492 host_port,
493 guest_port,
494 },
495 reply: reply_tx,
496 })
497 .map_err(|_| anyhow::anyhow!("VM thread exited"))?;
498 reply_rx.await?
499 }
500
501 pub async fn dial_guest(&self, port: u16) -> Result<GuestStream> {
514 let (reply_tx, reply_rx) = oneshot::channel();
515 self.cmd_tx
516 .send(SandboxCmd::DialGuest {
517 port,
518 reply: reply_tx,
519 })
520 .map_err(|_| anyhow::anyhow!("VM thread exited"))?;
521 let stream = reply_rx.await??;
522 Ok(GuestStream(stream))
523 }
524
525 pub async fn remove(&self, path: &str, recursive: bool) -> Result<()> {
527 let (reply_tx, reply_rx) = oneshot::channel();
528 self.cmd_tx
529 .send(SandboxCmd::Remove {
530 path: path.to_string(),
531 recursive,
532 reply: reply_tx,
533 })
534 .map_err(|_| anyhow::anyhow!("VM thread exited"))?;
535 reply_rx.await?
536 }
537
538 pub async fn read_dir(&self, path: &str) -> Result<ReadDirResponse> {
540 let (reply_tx, reply_rx) = oneshot::channel();
541 self.cmd_tx
542 .send(SandboxCmd::ReadDir {
543 path: path.to_string(),
544 reply: reply_tx,
545 })
546 .map_err(|_| anyhow::anyhow!("VM thread exited"))?;
547 reply_rx.await?
548 }
549
550 pub async fn mkdir(&self, path: &str, recursive: bool) -> Result<()> {
553 let (reply_tx, reply_rx) = oneshot::channel();
554 self.cmd_tx
555 .send(SandboxCmd::Mkdir {
556 path: path.to_string(),
557 recursive,
558 reply: reply_tx,
559 })
560 .map_err(|_| anyhow::anyhow!("VM thread exited"))?;
561 reply_rx.await?
562 }
563
564 pub async fn rename(&self, old_path: &str, new_path: &str) -> Result<()> {
566 let (reply_tx, reply_rx) = oneshot::channel();
567 self.cmd_tx
568 .send(SandboxCmd::Rename {
569 old_path: old_path.to_string(),
570 new_path: new_path.to_string(),
571 reply: reply_tx,
572 })
573 .map_err(|_| anyhow::anyhow!("VM thread exited"))?;
574 reply_rx.await?
575 }
576
577 pub async fn chmod(&self, path: &str, mode: u32) -> Result<()> {
579 let (reply_tx, reply_rx) = oneshot::channel();
580 self.cmd_tx
581 .send(SandboxCmd::Chmod {
582 path: path.to_string(),
583 mode,
584 reply: reply_tx,
585 })
586 .map_err(|_| anyhow::anyhow!("VM thread exited"))?;
587 reply_rx.await?
588 }
589
590 pub async fn download(
597 &self,
598 url: &str,
599 path: &str,
600 extract: bool,
601 strip_components: u32,
602 ) -> Result<(
603 tokio::sync::oneshot::Receiver<Result<()>>,
604 std::sync::mpsc::Receiver<vm_proto::DownloadProgress>,
605 )> {
606 let (reply_tx, reply_rx) = oneshot::channel();
607 let (progress_tx, progress_rx) = std::sync::mpsc::channel();
608 self.cmd_tx
609 .send(SandboxCmd::Download {
610 url: url.to_string(),
611 path: path.to_string(),
612 extract,
613 strip_components,
614 progress_tx,
615 reply: reply_tx,
616 })
617 .map_err(|_| anyhow::anyhow!("VM thread exited"))?;
618 Ok((reply_rx, progress_rx))
619 }
620
621 pub async fn checkpoint(&self, name: &str) -> Result<()> {
624 let (reply_tx, reply_rx) = oneshot::channel();
625 self.cmd_tx
626 .send(SandboxCmd::Checkpoint {
627 name: name.to_string(),
628 reply: reply_tx,
629 })
630 .map_err(|_| anyhow::anyhow!("VM thread exited"))?;
631 reply_rx.await?
632 }
633
634 pub async fn open_watch(&self, path: &str, recursive: bool) -> Result<WatchHandle> {
638 let (reply_tx, reply_rx) = oneshot::channel();
639 self.cmd_tx
640 .send(SandboxCmd::Watch {
641 path: path.to_string(),
642 recursive,
643 reply: reply_tx,
644 })
645 .map_err(|_| anyhow::anyhow!("VM thread exited"))?;
646 let stream = reply_rx.await??;
647
648 let (event_tx, event_rx) = mpsc::unbounded_channel();
649
650 let reader_thread = std::thread::Builder::new()
651 .name("vm-watch-reader".into())
652 .spawn(move || {
653 let mut reader = BufReader::new(stream);
654 loop {
655 match vm_proto::frame::read_frame(&mut reader) {
656 Ok(Some((vm_proto::frame::WATCH_EVENT, payload))) => {
657 if let Some(event) = WatchEvent::decode(&payload) {
658 if event_tx.send(event).is_err() {
659 break;
660 }
661 }
662 }
663 Ok(Some(_)) => {} Ok(None) | Err(_) => break,
665 }
666 }
667 })?;
668
669 Ok(WatchHandle {
670 receiver: WatchReceiver { event_rx },
671 _reader_thread: reader_thread,
672 })
673 }
674
675 pub async fn stop(&self) -> Result<()> {
677 let (reply_tx, reply_rx) = oneshot::channel();
678 let _ = self.cmd_tx.send(SandboxCmd::Stop { reply: reply_tx });
679 reply_rx.await.unwrap_or(Ok(()))
680 }
681
682 pub fn instance_dir(&self) -> &str {
684 &self.instance_dir
685 }
686}
687
688impl Drop for AsyncSandbox {
689 fn drop(&mut self) {
690 let (reply_tx, _) = oneshot::channel();
692 let _ = self.cmd_tx.send(SandboxCmd::Stop { reply: reply_tx });
693 let _ = std::fs::remove_dir_all(&self.instance_dir);
695 }
696}
697
698use std::sync::atomic::{AtomicU64, Ordering};
703
704static INSTANCE_COUNTER: AtomicU64 = AtomicU64::new(0);
707
708#[cfg(target_os = "macos")]
709extern "C" {
710 fn clonefile(src: *const libc::c_char, dst: *const libc::c_char, flags: u32) -> libc::c_int;
711}
712
713#[cfg(target_os = "macos")]
714fn clone_file_cow(src: &str, dst: &str) -> Result<()> {
715 use anyhow::Context as _;
716 let c_src = std::ffi::CString::new(src).context("invalid source path")?;
717 let c_dst = std::ffi::CString::new(dst).context("invalid destination path")?;
718 let ret = unsafe { clonefile(c_src.as_ptr(), c_dst.as_ptr(), 0) };
719 if ret != 0 {
720 let err = std::io::Error::last_os_error();
721 bail!("clonefile({} -> {}) failed: {}", src, dst, err);
722 }
723 Ok(())
724}
725
726#[cfg(target_os = "linux")]
727use vm::clone_file as clone_file_cow;
728
729struct BootedVm {
730 sandbox: vm::Sandbox,
731 instance_dir: String,
732 data_dir: String,
733 proxy_handle: Option<vm_proxy::ProxyHandle>,
734 fwd_handle: Option<vm::PortForwardHandle>,
735 #[cfg(feature = "cas")]
736 nbd_handle: Option<vm_store::NbdHandle>,
737 default_env: HashMap<String, String>,
738}
739
740fn boot_vm(config: SandboxConfig) -> Result<BootedVm> {
741 let default_env = config.env.clone();
742 let data_dir = config.data_dir.unwrap_or_else(vm::default_data_dir);
743
744 let kernel_path = format!("{}/Image", data_dir);
746 let rootfs_path = format!("{}/rootfs.ext4", data_dir);
747 let initrd_path_str = format!("{}/initramfs.cpio.gz", data_dir);
748
749 if !std::path::Path::new(&kernel_path).exists() {
750 bail!(
751 "Kernel not found at {}. Run `hanzo-vm init` to download.",
752 kernel_path
753 );
754 }
755
756 let checkpoints_dir = format!("{}/checkpoints", data_dir);
758 #[allow(unused_mut, unused_variables)]
759 let mut cas_index: Option<String> = None;
760
761 let source = match &config.from {
762 Some(name) => {
763 vm::validate_checkpoint_name(name).map_err(|e| anyhow::anyhow!(e))?;
764
765 #[cfg(feature = "cas")]
767 {
768 let idx_path = format!("{}/{}.idx", checkpoints_dir, name);
769 let ext4_path = format!("{}/{}.ext4", checkpoints_dir, name);
770 if std::path::Path::new(&idx_path).exists() {
771 cas_index = Some(idx_path.clone());
772 idx_path
773 } else if std::path::Path::new(&ext4_path).exists() {
774 ext4_path
775 } else {
776 bail!("Checkpoint '{}' not found", name);
777 }
778 }
779
780 #[cfg(not(feature = "cas"))]
781 {
782 let path = format!("{}/{}.ext4", checkpoints_dir, name);
783 if !std::path::Path::new(&path).exists() {
784 bail!("Checkpoint '{}' not found", name);
785 }
786 path
787 }
788 }
789 None => {
790 if !std::path::Path::new(&rootfs_path).exists() {
791 bail!(
792 "Rootfs not found at {}. Run `hanzo-vm init` to download.",
793 rootfs_path
794 );
795 }
796 rootfs_path
797 }
798 };
799
800 #[cfg(feature = "cas")]
802 let use_nbd = cas_index.is_some() || matches!(config.storage, StorageMode::Cas { .. });
803 #[cfg(not(feature = "cas"))]
804 let use_nbd = false;
805
806 let seq = INSTANCE_COUNTER.fetch_add(1, Ordering::Relaxed);
810 let instance_dir = format!("{}/instances/sdk-{}-{}", data_dir, std::process::id(), seq,);
811 std::fs::create_dir_all(&instance_dir)?;
812 let work_rootfs = format!("{}/rootfs.ext4", instance_dir);
813
814 if use_nbd {
815 std::fs::File::create(&work_rootfs)?;
817 } else {
818 clone_file_cow(&source, &work_rootfs)?;
820 }
821
822 if !use_nbd {
824 let f = std::fs::OpenOptions::new().write(true).open(&work_rootfs)?;
825 let target = config.disk_size_mb * 1024 * 1024;
826 let current = f.metadata()?.len();
827 if target > current {
828 f.set_len(target)?;
829 }
830 drop(f);
831 }
832
833 #[cfg(feature = "cas")]
835 let nbd_handle = if use_nbd {
836 let socket_path = format!("{}/nbd.sock", instance_dir);
837 let cas_dir_str = match &config.storage {
838 StorageMode::Cas { cas_dir } => cas_dir
839 .clone()
840 .unwrap_or_else(|| format!("{}/cas", data_dir)),
841 _ => format!("{}/cas", data_dir),
842 };
843 let index_path = if let Some(ref idx) = cas_index {
844 idx.clone()
845 } else {
846 let source_hash = blake3::hash(source.as_bytes()).to_hex();
847 format!("{}/cas/indexes/{}.idx", data_dir, &source_hash[..16])
848 };
849 let target_size = config.disk_size_mb * 1024 * 1024;
850 Some(vm_store::start_cas_nbd_server(
851 &source,
852 &cas_dir_str,
853 &index_path,
854 &socket_path,
855 target_size,
856 )?)
857 } else {
858 None
859 };
860
861 let initrd_path = if std::path::Path::new(&initrd_path_str).exists() {
862 Some(initrd_path_str)
863 } else {
864 None
865 };
866
867 let (vm_fd, proxy_handle) = if config.allow_net {
869 let proxy_config = ProxyConfig {
870 secrets: config.secrets,
871 expose_host: config.expose_host,
872 network: NetworkConfig {
873 allow: config.allowed_hosts,
874 },
875 ..Default::default()
876 };
877
878 let (vm_fd, host_fd) = vm_proxy::create_socketpair()?;
879 let handle = vm_proxy::start(host_fd, proxy_config)?;
880 (Some(vm_fd), Some(handle))
881 } else {
882 (None, None)
883 };
884
885 let mut builder = vm::Sandbox::builder()
887 .kernel(&kernel_path)
888 .rootfs(&work_rootfs)
889 .cpus(config.cpus)
890 .memory_mb(config.memory_mb)
891 .console(false); if let Some(fd) = vm_fd {
894 builder = builder.network_fd(fd);
895 }
896 #[cfg(feature = "cas")]
897 if let Some(ref handle) = nbd_handle {
898 builder = builder.nbd_uri(handle.uri());
899 }
900 if let Some(ref initrd) = initrd_path {
901 builder = builder.initrd(initrd);
902 }
903 for m in &config.mounts {
904 builder = builder.mount(m.clone());
905 }
906
907 let sandbox = builder.build()?;
908
909 info!(
910 "booting VM ({}cpus, {}MB RAM, {}MB disk, storage={:?})",
911 config.cpus, config.memory_mb, config.disk_size_mb, config.storage
912 );
913
914 sandbox.start()?;
915
916 let fwd_handle = if !config.ports.is_empty() {
918 Some(sandbox.start_port_forwarding(&config.ports)?)
919 } else {
920 None
921 };
922
923 if let Some(ref handle) = proxy_handle {
925 if !handle.placeholders.is_empty() {
926 sandbox.write_file(
927 "/usr/local/share/ca-certificates/vm-proxy.crt",
928 &handle.ca_cert_pem,
929 )?;
930 sandbox.exec(
931 &["update-ca-certificates", "--fresh"],
932 &mut std::io::sink(),
933 &mut std::io::sink(),
934 )?;
935 }
936 }
937
938 info!("VM ready");
939
940 Ok(BootedVm {
941 sandbox,
942 instance_dir,
943 data_dir,
944 proxy_handle,
945 fwd_handle,
946 #[cfg(feature = "cas")]
947 nbd_handle,
948 default_env,
949 })
950}
951
952fn run_vm_loop(
953 sandbox: vm::Sandbox,
954 instance_dir: &str,
955 data_dir: &str,
956 cmd_rx: std::sync::mpsc::Receiver<SandboxCmd>,
957 proxy_handle: Option<vm_proxy::ProxyHandle>,
958 _fwd_handle: Option<vm::PortForwardHandle>,
959 #[cfg(feature = "cas")] nbd_handle: Option<vm_store::NbdHandle>,
960 default_env: HashMap<String, String>,
961) {
962 let mut env = default_env;
964 if let Some(ref handle) = proxy_handle {
965 env.extend(handle.placeholders.clone());
966 }
967
968 let _proxy = proxy_handle;
970 let mut extra_fwd_handles: Vec<vm::PortForwardHandle> = Vec::new();
972
973 let sandbox = Arc::new(sandbox);
977
978 while let Ok(cmd) = cmd_rx.recv() {
979 match cmd {
980 SandboxCmd::Exec { argv, reply } => {
981 let result = exec_command(&sandbox, &argv, &env);
982 let _ = reply.send(result);
983 }
984 SandboxCmd::ReadFile { path, reply } => {
988 let sb = sandbox.clone();
989 std::thread::spawn(move || {
990 let _ = reply.send(sb.read_file(&path));
991 });
992 }
993 SandboxCmd::ReadDir { path, reply } => {
994 let sb = sandbox.clone();
995 std::thread::spawn(move || {
996 let _ = reply.send(sb.read_dir(&path));
997 });
998 }
999 SandboxCmd::WriteFile {
1000 path,
1001 content,
1002 reply,
1003 } => {
1004 let _ = reply.send(sandbox.write_file(&path, &content));
1005 }
1006 SandboxCmd::Mkdir {
1007 path,
1008 recursive,
1009 reply,
1010 } => {
1011 let _ = reply.send(sandbox.mkdir(&path, recursive));
1012 }
1013 SandboxCmd::Rename {
1014 old_path,
1015 new_path,
1016 reply,
1017 } => {
1018 let _ = reply.send(sandbox.rename(&old_path, &new_path));
1019 }
1020 SandboxCmd::Chmod { path, mode, reply } => {
1021 let _ = reply.send(sandbox.chmod(&path, mode));
1022 }
1023 SandboxCmd::Download {
1024 url,
1025 path,
1026 extract,
1027 strip_components,
1028 progress_tx,
1029 reply,
1030 } => {
1031 let result = sandbox.download(&url, &path, extract, strip_components, |p| {
1032 let _ = progress_tx.send(p);
1033 });
1034 let _ = reply.send(result);
1035 }
1036 SandboxCmd::OpenShell {
1037 argv,
1038 rows,
1039 cols,
1040 cwd,
1041 extra_env,
1042 reply,
1043 } => {
1044 let default_argv = ["/bin/bash".to_string(), "-l".to_string()];
1045 let shell_argv: Vec<String> = argv.unwrap_or_else(|| default_argv.to_vec());
1046 let shell_argv_refs: Vec<&str> = shell_argv.iter().map(|s| s.as_str()).collect();
1047 let mut merged_env = env.clone();
1048 merged_env.extend(extra_env);
1049 let result = sandbox.open_shell_with_cwd(
1050 &shell_argv_refs,
1051 &merged_env,
1052 rows,
1053 cols,
1054 cwd.as_deref(),
1055 );
1056 let _ = reply.send(result);
1057 }
1058 SandboxCmd::Watch {
1059 path,
1060 recursive,
1061 reply,
1062 } => {
1063 let result = sandbox.open_watch(&path, recursive);
1064 let _ = reply.send(result);
1065 }
1066 SandboxCmd::Remove {
1067 path,
1068 recursive,
1069 reply,
1070 } => {
1071 let _ = reply.send(sandbox.remove(&path, recursive));
1072 }
1073 SandboxCmd::DiscardOverlay { path, reply } => {
1074 let _ = reply.send(sandbox.discard_overlay(&path));
1075 }
1076 SandboxCmd::AddPortForward { mapping, reply } => {
1077 let result = sandbox.start_port_forwarding(&[mapping]);
1078 match result {
1079 Ok(handle) => {
1080 extra_fwd_handles.push(handle);
1081 let _ = reply.send(Ok(()));
1082 }
1083 Err(e) => {
1084 let _ = reply.send(Err(e));
1085 }
1086 }
1087 }
1088 SandboxCmd::DialGuest { port, reply } => {
1089 let sb = sandbox.clone();
1091 std::thread::spawn(move || {
1092 let _ = reply.send(sb.connect_forward(port));
1093 });
1094 }
1095 SandboxCmd::Checkpoint { name, reply } => {
1096 let result = (|| -> Result<()> {
1097 vm::validate_checkpoint_name(&name).map_err(|e| anyhow::anyhow!(e))?;
1098 let checkpoints_dir = format!("{}/checkpoints", data_dir);
1099 std::fs::create_dir_all(&checkpoints_dir)?;
1100
1101 #[cfg(feature = "cas")]
1103 if let Some(ref handle) = nbd_handle {
1104 let index_path = format!("{}/{}.idx", checkpoints_dir, name);
1105 if std::path::Path::new(&index_path).exists() {
1106 std::fs::remove_file(&index_path)?;
1107 }
1108 handle.save_checkpoint(&index_path)?;
1109 info!("checkpoint '{}' saved (CAS)", name);
1110 return Ok(());
1111 }
1112
1113 let checkpoint_path = format!("{}/{}.ext4", checkpoints_dir, name);
1115 if std::path::Path::new(&checkpoint_path).exists() {
1116 std::fs::remove_file(&checkpoint_path)?;
1117 }
1118 let work_rootfs = format!("{}/rootfs.ext4", instance_dir);
1119 clone_file_cow(&work_rootfs, &checkpoint_path)?;
1120 info!("checkpoint '{}' saved", name);
1121 Ok(())
1122 })();
1123 let _ = reply.send(result);
1124 }
1125 SandboxCmd::Stop { reply } => {
1126 let _ = reply.send(sandbox.stop());
1127 break;
1128 }
1129 }
1130 }
1131
1132 let _ = sandbox.stop();
1134}
1135
1136fn exec_command(
1137 sandbox: &vm::Sandbox,
1138 argv: &[String],
1139 env: &HashMap<String, String>,
1140) -> Result<ExecResult> {
1141 let mut stdout_buf = Vec::new();
1142 let mut stderr_buf = Vec::new();
1143 let argv_refs: Vec<&str> = argv.iter().map(|s| s.as_str()).collect();
1144
1145 let exit_code = sandbox.exec_with_env(&argv_refs, env, &mut stdout_buf, &mut stderr_buf)?;
1146
1147 Ok(ExecResult {
1148 stdout: String::from_utf8_lossy(&stdout_buf).to_string(),
1149 stderr: String::from_utf8_lossy(&stderr_buf).to_string(),
1150 exit_code,
1151 })
1152}