1use std::io::{Read, Write};
8use std::net::TcpStream;
9use std::os::fd::{FromRawFd, IntoRawFd, OwnedFd};
10use std::os::unix::net::UnixStream;
11use std::path::{Path, PathBuf};
12use std::process::{Child, Command, Stdio};
13use std::sync::atomic::{AtomicBool, Ordering};
14use std::sync::{Arc, Mutex};
15use std::time::{Duration, Instant};
16
17use crossbeam_channel::{bounded, Receiver, Sender};
18
19use crate::api;
20use crate::configuration::{ConfigData, VirtualMachineConfiguration};
21use crate::error::{Result, VzError};
22use crate::net_backend;
23
24const GUEST_CID: u32 = 3;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum VmState {
28 Stopped = 0,
29 Running = 1,
30 Error = 3,
31 Unknown = -1,
33}
34
35pub struct VirtualMachine {
36 config: ConfigData,
37 serial_in: Option<OwnedFd>,
40 serial_out: Option<OwnedFd>,
41 run_dir: PathBuf,
42 api_socket: String,
43 vsock_socket: String,
44 ch_pid: Mutex<Option<i32>>,
45 virtiofsd: Mutex<Vec<Child>>,
46 state_tx: Sender<VmState>,
47 state_rx: Receiver<VmState>,
48 running: Arc<AtomicBool>,
49}
50
51fn find_binary(name: &str) -> Option<PathBuf> {
53 if let Ok(path) = std::env::var("PATH") {
54 for dir in path.split(':') {
55 let candidate = Path::new(dir).join(name);
56 if candidate.is_file() {
57 return Some(candidate);
58 }
59 }
60 }
61 if let Ok(home) = std::env::var("HOME") {
62 let candidate = Path::new(&home).join(".local/bin").join(name);
63 if candidate.is_file() {
64 return Some(candidate);
65 }
66 }
67 None
68}
69
70fn dup_owned(fd: i32) -> Option<OwnedFd> {
72 let duped = unsafe { libc::dup(fd) };
73 if duped < 0 {
74 return None;
75 }
76 Some(unsafe { OwnedFd::from_raw_fd(duped) })
77}
78
79fn wait_for_socket(path: &str, timeout: Duration) -> bool {
80 let deadline = Instant::now() + timeout;
81 while Instant::now() < deadline {
82 if Path::new(path).exists() {
83 return true;
84 }
85 std::thread::sleep(Duration::from_millis(5));
86 }
87 false
88}
89
90impl VirtualMachine {
91 pub fn new(config: &VirtualMachineConfiguration) -> Self {
92 let inner = config.inner.borrow().clone();
93 let serial_in = inner.serial_read_fd.and_then(dup_owned);
94 let serial_out = inner.serial_write_fd.and_then(dup_owned);
95 let (state_tx, state_rx) = bounded(1);
96
97 let run_dir = std::env::temp_dir().join(format!("hanzo-vm-{}", std::process::id()));
102 let _ = std::fs::create_dir_all(&run_dir);
103
104 let api_socket = run_dir.join("ch-api.sock").to_string_lossy().into_owned();
105 let vsock_socket = run_dir.join("vsock.sock").to_string_lossy().into_owned();
106
107 VirtualMachine {
108 config: inner,
109 serial_in,
110 serial_out,
111 run_dir,
112 api_socket,
113 vsock_socket,
114 ch_pid: Mutex::new(None),
115 virtiofsd: Mutex::new(Vec::new()),
116 state_tx,
117 state_rx,
118 running: Arc::new(AtomicBool::new(false)),
119 }
120 }
121
122 pub fn supported() -> bool {
123 Path::new("/dev/kvm").exists() && find_binary("cloud-hypervisor").is_some()
124 }
125
126 pub fn start(&self) -> Result<()> {
127 let ch_bin = find_binary("cloud-hypervisor")
128 .ok_or_else(|| VzError::new("cloud-hypervisor not found on PATH or in ~/.local/bin"))?;
129
130 let mut fs_sockets = Vec::new();
133 if !self.config.mounts.is_empty() {
134 let fsd_bin = find_binary("virtiofsd")
135 .ok_or_else(|| VzError::new("virtiofsd not found on PATH or in ~/.local/bin"))?;
136 for (tag, host_path, _read_only) in &self.config.mounts {
137 let socket = self.run_dir.join(format!("fs-{}.sock", tag));
138 let log = std::fs::File::create(self.run_dir.join(format!("fs-{}.log", tag)))
139 .map_err(|e| VzError::new(format!("virtiofsd log: {}", e)))?;
140 let child = Command::new(&fsd_bin)
141 .arg(format!("--socket-path={}", socket.display()))
142 .arg("--shared-dir")
143 .arg(host_path)
144 .arg("--cache")
145 .arg("auto")
146 .arg("--sandbox")
147 .arg("none")
148 .stdin(Stdio::null())
149 .stdout(Stdio::null())
150 .stderr(Stdio::from(log))
151 .spawn()
152 .map_err(|e| VzError::new(format!("spawn virtiofsd: {}", e)))?;
153 self.virtiofsd.lock().unwrap().push(child);
154 fs_sockets.push((tag.clone(), socket));
155 }
156 for (tag, socket) in &fs_sockets {
157 if !wait_for_socket(&socket.to_string_lossy(), Duration::from_secs(5)) {
158 return Err(VzError::new(format!(
159 "virtiofsd socket for {} did not appear",
160 tag
161 )));
162 }
163 }
164 }
165
166 let net_socket = self.run_dir.join("net.sock");
168 if let Some(fd) = self.config.network_fd {
169 net_backend::spawn(&net_socket.to_string_lossy(), fd)?;
170 }
171
172 let stdio_of = |fd: &Option<OwnedFd>| -> Result<Stdio> {
174 match fd {
175 Some(fd) => {
176 Ok(Stdio::from(fd.try_clone().map_err(|e| {
177 VzError::new(format!("dup console fd: {}", e))
178 })?))
179 }
180 None => Ok(Stdio::null()),
181 }
182 };
183 let stdin = stdio_of(&self.serial_in)?;
184 let stdout = stdio_of(&self.serial_out)?;
185
186 let _ = std::fs::remove_file(&self.api_socket);
187 let _ = std::fs::remove_file(&self.vsock_socket);
188
189 let mut child = Command::new(&ch_bin)
190 .arg("--api-socket")
191 .arg(format!("path={}", self.api_socket))
192 .stdin(stdin)
193 .stdout(stdout)
194 .stderr(Stdio::inherit())
195 .spawn()
196 .map_err(|e| VzError::new(format!("spawn cloud-hypervisor: {}", e)))?;
197
198 if !wait_for_socket(&self.api_socket, Duration::from_secs(5)) {
199 let _ = child.kill();
200 let _ = child.wait();
201 return Err(VzError::new("cloud-hypervisor API socket did not appear"));
202 }
203
204 let vm_config = self.vm_config_json(&fs_sockets, &net_socket);
205 if let Err(e) = api::put(&self.api_socket, "vm.create", Some(&vm_config.to_string()))
206 .and_then(|_| api::put(&self.api_socket, "vm.boot", None))
207 {
208 let _ = child.kill();
209 let _ = child.wait();
210 return Err(e);
211 }
212
213 *self.ch_pid.lock().unwrap() = Some(child.id() as i32);
214 self.running.store(true, Ordering::Release);
215 let _ = self.state_tx.try_send(VmState::Running);
216
217 let running = self.running.clone();
220 let state_tx = self.state_tx.clone();
221 std::thread::Builder::new()
222 .name("hanzo-vm-monitor".into())
223 .spawn(move || {
224 let _ = child.wait();
225 running.store(false, Ordering::Release);
226 let _ = state_tx.try_send(VmState::Stopped);
227 })
228 .map_err(|e| VzError::new(format!("spawn monitor thread: {}", e)))?;
229
230 Ok(())
231 }
232
233 fn vm_config_json(
234 &self,
235 fs_sockets: &[(String, PathBuf)],
236 net_socket: &Path,
237 ) -> serde_json::Value {
238 let c = &self.config;
239 let mut cfg = serde_json::json!({
240 "cpus": { "boot_vcpus": c.cpu_count, "max_vcpus": c.cpu_count },
241 "memory": { "size": c.memory_size, "shared": true },
243 "payload": { "kernel": c.kernel_path, "cmdline": c.command_line },
244 "serial": { "mode": "Off" },
245 "rng": { "src": "/dev/urandom" },
246 });
247 cfg["console"] = if c.serial_read_fd.is_some() {
252 serde_json::json!({ "mode": "Tty" })
253 } else if c.serial_write_fd.is_some() {
254 serde_json::json!({ "mode": "File", "file": "/proc/self/fd/1" })
255 } else {
256 serde_json::json!({ "mode": "Off" })
257 };
258 if let Some(ref initrd) = c.initrd_path {
259 cfg["payload"]["initramfs"] = serde_json::json!(initrd);
260 }
261 if let Some(ref disk) = c.disk_path {
262 cfg["disks"] = serde_json::json!([{
263 "path": disk,
264 "readonly": c.disk_read_only,
265 "image_type": "Raw",
266 }]);
267 }
268 if c.has_socket {
269 cfg["vsock"] = serde_json::json!({ "cid": GUEST_CID, "socket": self.vsock_socket });
270 }
271 if !fs_sockets.is_empty() {
272 let fs: Vec<_> = fs_sockets
273 .iter()
274 .map(|(tag, socket)| {
275 serde_json::json!({
276 "tag": tag,
277 "socket": socket.to_string_lossy(),
278 "num_queues": 1,
279 "queue_size": 1024,
280 })
281 })
282 .collect();
283 cfg["fs"] = serde_json::json!(fs);
284 }
285 if c.network_fd.is_some() {
286 let mut net = serde_json::json!({
287 "vhost_user": true,
288 "vhost_socket": net_socket.to_string_lossy(),
289 "num_queues": 2,
290 "queue_size": 256,
291 });
292 if let Some(mac) = c.network_mac {
293 net["mac"] = serde_json::json!(format!(
294 "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
295 mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]
296 ));
297 }
298 cfg["net"] = serde_json::json!([net]);
299 }
300 cfg
301 }
302
303 pub fn stop(&self) -> Result<()> {
304 let _ = api::put(&self.api_socket, "vm.shutdown", None);
306 let _ = api::put(&self.api_socket, "vmm.shutdown", None);
307
308 if let Some(pid) = *self.ch_pid.lock().unwrap() {
309 let deadline = Instant::now() + Duration::from_secs(2);
310 while self.running.load(Ordering::Acquire) && Instant::now() < deadline {
311 std::thread::sleep(Duration::from_millis(10));
312 }
313 if self.running.load(Ordering::Acquire) {
314 unsafe { libc::kill(pid, libc::SIGKILL) };
315 }
316 }
317
318 for mut child in self.virtiofsd.lock().unwrap().drain(..) {
319 let _ = child.kill();
320 let _ = child.wait();
321 }
322
323 self.running.store(false, Ordering::Release);
324 let _ = self.state_tx.try_send(VmState::Stopped);
325 Ok(())
326 }
327
328 pub fn state_channel(&self) -> Receiver<VmState> {
329 self.state_rx.clone()
330 }
331
332 pub fn can_start(&self) -> bool {
333 !self.running.load(Ordering::Acquire)
334 }
335
336 pub fn can_stop(&self) -> bool {
337 self.running.load(Ordering::Acquire)
338 }
339
340 pub fn can_pause(&self) -> bool {
341 false
342 }
343 pub fn can_resume(&self) -> bool {
344 false
345 }
346
347 pub fn can_request_stop(&self) -> bool {
348 self.can_stop()
349 }
350
351 pub fn connect_to_vsock_port(&self, port: u32) -> Result<TcpStream> {
360 let mut stream = UnixStream::connect(&self.vsock_socket)
361 .map_err(|e| VzError::new(format!("vsock connect failed: {}", e)))?;
362 let _ = stream.set_read_timeout(Some(Duration::from_secs(2)));
363
364 stream
365 .write_all(format!("CONNECT {}\n", port).as_bytes())
366 .map_err(|e| VzError::new(format!("vsock handshake send: {}", e)))?;
367
368 let mut line = Vec::with_capacity(16);
370 let mut byte = [0u8; 1];
371 loop {
372 match stream.read(&mut byte) {
373 Ok(1) => {
374 if byte[0] == b'\n' {
375 break;
376 }
377 line.push(byte[0]);
378 if line.len() > 32 {
379 return Err(VzError::new("vsock handshake: oversized response"));
380 }
381 }
382 Ok(_) => return Err(VzError::new("vsock connect refused (EOF)")),
383 Err(e) => return Err(VzError::new(format!("vsock handshake read: {}", e))),
384 }
385 }
386 if !line.starts_with(b"OK ") {
387 return Err(VzError::new(format!(
388 "vsock connect refused: {}",
389 String::from_utf8_lossy(&line)
390 )));
391 }
392
393 let _ = stream.set_read_timeout(None);
394 Ok(unsafe { TcpStream::from_raw_fd(stream.into_raw_fd()) })
395 }
396
397 pub fn state(&self) -> VmState {
398 if self.running.load(Ordering::Acquire) {
399 VmState::Running
400 } else {
401 VmState::Stopped
402 }
403 }
404}
405
406impl Drop for VirtualMachine {
407 fn drop(&mut self) {
408 if let Some(pid) = *self.ch_pid.lock().unwrap() {
410 if self.running.load(Ordering::Acquire) {
411 unsafe { libc::kill(pid, libc::SIGKILL) };
412 }
413 }
414 for mut child in self.virtiofsd.lock().unwrap().drain(..) {
415 let _ = child.kill();
416 let _ = child.wait();
417 }
418 let _ = std::fs::remove_dir_all(&self.run_dir);
419 }
420}