1use anyhow::Result;
2use mvm_core::platform;
3
4use super::{firecracker, lima, network};
5use crate::config::*;
6use crate::shell::{run_in_vm, run_in_vm_stdout, run_in_vm_visible};
7use crate::ui;
8use crate::vm::image::RuntimeVolume;
9
10fn require_linux_env() -> Result<()> {
14 if platform::current().needs_lima() {
15 lima::require_running()?;
16 }
17 Ok(())
18}
19
20fn resolve_microvm_dir() -> Result<String> {
22 run_in_vm_stdout(&format!("echo {}", MICROVM_DIR))
23}
24
25fn resolve_vm_dir(slot: &VmSlot) -> Result<String> {
27 run_in_vm_stdout(&format!("echo {}", slot.vm_dir))
28}
29
30fn start_firecracker_daemon(abs_dir: &str) -> Result<()> {
32 ui::info("Starting Firecracker...");
33 run_in_vm_visible(&format!(
34 r#"
35 mkdir -p {dir}
36 sudo rm -f {socket}
37 touch {dir}/console.log {dir}/firecracker.log
38 sudo bash -c 'nohup setsid firecracker --api-sock {socket} --enable-pci \
39 </dev/null >{dir}/console.log 2>{dir}/firecracker.log &
40 echo $! > {dir}/.fc-pid'
41
42 echo "[mvm] Waiting for API socket..."
43 for i in $(seq 1 30); do
44 [ -S {socket} ] && break
45 sleep 0.1
46 done
47
48 if [ ! -S {socket} ]; then
49 echo "[mvm] ERROR: API socket did not appear." >&2
50 exit 1
51 fi
52 echo "[mvm] Firecracker started."
53 "#,
54 socket = API_SOCKET,
55 dir = abs_dir,
56 ))
57}
58
59fn start_vm_firecracker(abs_dir: &str, abs_socket: &str) -> Result<()> {
61 ui::info("Starting Firecracker...");
62 run_in_vm_visible(&format!(
63 r#"
64 mkdir -p {dir}
65 sudo rm -f {socket}
66 touch {dir}/console.log {dir}/firecracker.log
67 sudo bash -c 'nohup setsid firecracker --api-sock {socket} --enable-pci \
68 </dev/null >{dir}/console.log 2>{dir}/firecracker.log &
69 echo $! > {dir}/fc.pid'
70
71 echo "[mvm] Waiting for API socket..."
72 for i in $(seq 1 30); do
73 [ -S {socket} ] && break
74 sleep 0.1
75 done
76
77 if [ ! -S {socket} ]; then
78 echo "[mvm] ERROR: API socket did not appear." >&2
79 exit 1
80 fi
81 echo "[mvm] Firecracker started."
82 "#,
83 socket = abs_socket,
84 dir = abs_dir,
85 ))
86}
87
88fn api_put(path: &str, data: &str) -> Result<()> {
90 api_put_socket(API_SOCKET, path, data)
91}
92
93fn api_put_socket(socket: &str, path: &str, data: &str) -> Result<()> {
95 let script = format!(
96 r#"
97 response=$(sudo curl -s -w "\n%{{http_code}}" -X PUT --unix-socket {socket} \
98 --data '{data}' "http://localhost{path}")
99 code=$(echo "$response" | tail -1)
100 body=$(echo "$response" | sed '$d')
101 if [ "$code" -ge 400 ]; then
102 echo "[mvm] ERROR: PUT {path} returned $code: $body" >&2
103 exit 1
104 fi
105 "#,
106 socket = socket,
107 path = path,
108 data = data,
109 );
110 run_in_vm_visible(&script)
111}
112
113fn configure_microvm(state: &MvmState, abs_dir: &str) -> Result<()> {
115 ui::info("Configuring logger...");
116 api_put(
117 "/logger",
118 &format!(
119 r#"{{"log_path": "{dir}/firecracker.log", "level": "Debug", "show_level": true, "show_log_origin": true}}"#,
120 dir = abs_dir,
121 ),
122 )?;
123
124 let kernel_path = format!("{}/{}", abs_dir, state.kernel);
125 let rootfs_path = format!("{}/{}", abs_dir, state.rootfs);
126
127 let kernel_boot_args = format!(
130 "console=ttyS0 reboot=k panic=1 net.ifnames=0 ip={guest}::{gateway}:255.255.255.252::eth0:off",
131 guest = GUEST_IP,
132 gateway = TAP_IP,
133 );
134
135 ui::info(&format!("Setting boot source: {}", state.kernel));
136 api_put(
137 "/boot-source",
138 &format!(
139 r#"{{"kernel_image_path": "{kernel}", "boot_args": "{args}"}}"#,
140 kernel = kernel_path,
141 args = kernel_boot_args,
142 ),
143 )?;
144
145 ui::info(&format!("Setting rootfs: {}", state.rootfs));
146 api_put(
147 "/drives/rootfs",
148 &format!(
149 r#"{{"drive_id": "rootfs", "path_on_host": "{rootfs}", "is_root_device": true, "is_read_only": false}}"#,
150 rootfs = rootfs_path,
151 ),
152 )?;
153
154 ui::info("Setting network interface...");
155 api_put(
156 "/network-interfaces/net1",
157 &format!(
158 r#"{{"iface_id": "net1", "guest_mac": "{mac}", "host_dev_name": "{tap}"}}"#,
159 mac = FC_MAC,
160 tap = TAP_DEV,
161 ),
162 )?;
163
164 ui::info("Setting vsock device...");
165 api_put(
166 "/vsock",
167 &format!(
168 r#"{{"vsock_id": "vsock0", "guest_cid": {cid}, "uds_path": "{dir}/v.sock"}}"#,
169 cid = mvm_guest::vsock::GUEST_CID,
170 dir = abs_dir,
171 ),
172 )?;
173
174 Ok(())
175}
176
177pub fn start() -> Result<()> {
182 require_linux_env()?;
183
184 if firecracker::is_running()? {
186 ui::info("Firecracker is already running.");
187 ui::info("Use 'mvm stop' to shut down, then 'mvm start' to restart.");
188 return Ok(());
189 }
190
191 let state = read_state_or_discover()?;
193
194 let abs_dir = resolve_microvm_dir()?;
196
197 network::setup()?;
199
200 start_firecracker_daemon(&abs_dir)?;
202
203 configure_microvm(&state, &abs_dir)?;
205
206 ui::info("Starting microVM...");
208 std::thread::sleep(std::time::Duration::from_millis(15));
209 api_put("/actions", r#"{"action_type": "InstanceStart"}"#)?;
210
211 let _ = run_in_vm(&format!("sudo chmod 0666 {}/v.sock 2>/dev/null", abs_dir));
213
214 ui::banner(&[
215 "MicroVM is running!",
216 "",
217 &format!(" Guest IP: {}", GUEST_IP),
218 "",
219 "Use 'mvm status' to check the microVM.",
220 "Use 'mvm stop' to shut down the microVM.",
221 "Use 'mvm shell' to access the Lima VM environment.",
222 ]);
223
224 Ok(())
225}
226
227pub fn stop() -> Result<()> {
229 require_linux_env()?;
230
231 if !firecracker::is_running()? {
232 ui::info("MicroVM is not running.");
233 return Ok(());
234 }
235
236 ui::info("Stopping microVM...");
237
238 let _ = run_in_vm(&format!(
240 r#"sudo curl -s -X PUT --unix-socket {socket} \
241 --data '{{"action_type": "SendCtrlAltDel"}}' \
242 "http://localhost/actions" 2>/dev/null || true"#,
243 socket = API_SOCKET,
244 ));
245
246 std::thread::sleep(std::time::Duration::from_secs(2));
248
249 run_in_vm(&format!(
250 r#"
251 if [ -f {dir}/.fc-pid ]; then
252 sudo kill $(cat {dir}/.fc-pid) 2>/dev/null || true
253 rm -f {dir}/.fc-pid
254 fi
255 sudo pkill -x firecracker 2>/dev/null || true
256 sudo rm -f {socket}
257 rm -f {dir}/.mvm-run-info
258 rm -f {dir}/v.sock
259 "#,
260 dir = MICROVM_DIR,
261 socket = API_SOCKET,
262 ))?;
263
264 network::teardown()?;
266
267 ui::success("MicroVM stopped.");
268 Ok(())
269}
270
271fn read_state_or_discover() -> Result<MvmState> {
273 let json = run_in_vm_stdout(&format!(
274 "cat {dir}/.mvm-state 2>/dev/null || echo 'null'",
275 dir = MICROVM_DIR,
276 ))?;
277
278 if let Ok(state) = serde_json::from_str::<MvmState>(&json)
279 && !state.kernel.is_empty()
280 && !state.rootfs.is_empty()
281 && !state.ssh_key.is_empty()
282 {
283 return Ok(state);
284 }
285
286 let kernel = run_in_vm_stdout(&format!(
288 "cd {} && ls vmlinux-* 2>/dev/null | tail -1",
289 MICROVM_DIR
290 ))?;
291 let rootfs = run_in_vm_stdout(&format!(
292 "cd {} && ls *.ext4 2>/dev/null | tail -1",
293 MICROVM_DIR
294 ))?;
295 let ssh_key = run_in_vm_stdout(&format!(
296 "cd {} && ls *.id_rsa 2>/dev/null | tail -1",
297 MICROVM_DIR
298 ))?;
299
300 if kernel.is_empty() || rootfs.is_empty() || ssh_key.is_empty() {
301 anyhow::bail!(
302 "Missing microVM assets in {}. Run 'mvm setup' first.\n kernel={:?} rootfs={:?} ssh_key={:?}",
303 MICROVM_DIR,
304 kernel,
305 rootfs,
306 ssh_key,
307 );
308 }
309
310 Ok(MvmState {
311 kernel,
312 rootfs,
313 ssh_key,
314 fc_pid: None,
315 })
316}
317
318pub struct FlakeRunConfig {
324 pub name: String,
326 pub slot: VmSlot,
328 pub vmlinux_path: String,
330 pub initrd_path: Option<String>,
332 pub rootfs_path: String,
334 pub revision_hash: String,
336 pub flake_ref: String,
338 pub profile: Option<String>,
340 pub cpus: u32,
342 pub memory: u32,
344 pub volumes: Vec<RuntimeVolume>,
346}
347
348pub fn run_from_build(config: &FlakeRunConfig) -> Result<()> {
354 require_linux_env()?;
355
356 let slot = &config.slot;
357
358 let abs_dir = resolve_vm_dir(slot)?;
360 let abs_socket = format!("{}/fc.socket", abs_dir);
361 let pid_file = format!("{}/fc.pid", abs_dir);
362
363 if firecracker::is_vm_running(&pid_file)? {
364 ui::info(&format!("VM '{}' is already running.", slot.name));
365 ui::info("Use 'mvm stop <name>' to shut it down first.");
366 return Ok(());
367 }
368
369 network::bridge_ensure()?;
371
372 network::tap_create(slot)?;
374
375 start_vm_firecracker(&abs_dir, &abs_socket)?;
377
378 configure_flake_microvm(config, &abs_dir, &abs_socket)?;
380
381 ui::info("Starting microVM...");
383 std::thread::sleep(std::time::Duration::from_millis(15));
384 api_put_socket(
385 &abs_socket,
386 "/actions",
387 r#"{"action_type": "InstanceStart"}"#,
388 )?;
389
390 let _ = run_in_vm(&format!("sudo chmod 0666 {}/v.sock 2>/dev/null", abs_dir));
392
393 write_vm_run_info(config, &abs_dir)?;
395
396 ui::banner(&[
397 &format!("MicroVM '{}' is running!", config.name),
398 "",
399 &format!(" Guest IP: {}", slot.guest_ip),
400 &format!(" Revision: {}", config.revision_hash),
401 "",
402 &format!("Use 'mvm stop {}' to shut down this VM.", config.name),
403 "Use 'mvm status' to list all running VMs.",
404 ]);
405
406 Ok(())
407}
408
409pub fn stop_vm(name: &str) -> Result<()> {
411 require_linux_env()?;
412
413 let abs_vms = run_in_vm_stdout(&format!("echo {}", VMS_DIR))?;
414 let abs_dir = format!("{}/{}", abs_vms, name);
415 let pid_file = format!("{}/fc.pid", abs_dir);
416 let socket = format!("{}/fc.socket", abs_dir);
417
418 if !firecracker::is_vm_running(&pid_file)? {
419 ui::info(&format!("VM '{}' is not running.", name));
420 return Ok(());
421 }
422
423 ui::info(&format!("Stopping VM '{}'...", name));
424
425 let _ = run_in_vm(&format!(
427 r#"sudo curl -s -X PUT --unix-socket {socket} \
428 --data '{{"action_type": "SendCtrlAltDel"}}' \
429 "http://localhost/actions" 2>/dev/null || true"#,
430 socket = socket,
431 ));
432
433 std::thread::sleep(std::time::Duration::from_secs(2));
434
435 run_in_vm(&format!(
437 r#"
438 if [ -f {pid} ]; then
439 sudo kill $(cat {pid}) 2>/dev/null || true
440 fi
441 sudo rm -f {socket}
442 "#,
443 pid = pid_file,
444 socket = socket,
445 ))?;
446
447 if let Some(info) = read_vm_run_info_from(&abs_dir)
449 && let Some(ref vm_name) = info.name
450 {
451 if let Some(idx) = read_slot_index(&abs_dir) {
453 let slot = VmSlot::new(vm_name, idx);
454 let _ = network::tap_destroy(&slot);
455 }
456 }
457
458 let _ = run_in_vm(&format!("rm -rf {}", abs_dir));
460
461 ui::success(&format!("VM '{}' stopped.", name));
462 Ok(())
463}
464
465pub fn stop_all_vms() -> Result<()> {
467 require_linux_env()?;
468
469 let vms = list_vms()?;
470 if vms.is_empty() {
471 ui::info("No VMs are running.");
472 return Ok(());
473 }
474
475 for info in &vms {
476 if let Some(ref name) = info.name {
477 stop_vm(name)?;
478 }
479 }
480
481 let remaining = list_vms()?;
483 if remaining.is_empty() {
484 network::bridge_teardown()?;
485 }
486
487 Ok(())
488}
489
490pub fn logs(name: &str, follow: bool, lines: u32, hypervisor: bool) -> Result<()> {
495 require_linux_env()?;
496
497 let abs_vms = run_in_vm_stdout(&format!("echo {}", VMS_DIR))?;
498 let filename = if hypervisor {
499 "firecracker.log"
500 } else {
501 "console.log"
502 };
503 let log_file = format!("{}/{}/{}", abs_vms, name, filename);
504
505 let exists = run_in_vm_stdout(&format!("[ -f {} ] && echo yes || echo no", log_file))?;
508 if exists.trim() != "yes" {
509 if !hypervisor {
510 let fallback = format!("{}/{}/firecracker.log", abs_vms, name);
512 let fb_exists =
513 run_in_vm_stdout(&format!("[ -f {} ] && echo yes || echo no", fallback))?;
514 if fb_exists.trim() == "yes" {
515 ui::warn(
516 "console.log not found; showing firecracker.log (VM started before log split)",
517 );
518 return show_log_file(&fallback, follow, lines);
519 }
520 }
521 anyhow::bail!("No logs found for VM '{}' (is the name correct?)", name);
522 }
523
524 show_log_file(&log_file, follow, lines)
525}
526
527fn show_log_file(log_file: &str, follow: bool, lines: u32) -> Result<()> {
528 if follow {
529 run_in_vm_visible(&format!("tail -f {}", log_file))?;
530 } else {
531 let output = run_in_vm_stdout(&format!("tail -n {} {}", lines, log_file))?;
532 print!("{}", output);
533 }
534 Ok(())
535}
536
537pub fn list_vms() -> Result<Vec<RunInfo>> {
539 let output = run_in_vm_stdout(&format!(
540 "for f in {dir}/*/run-info.json; do [ -f \"$f\" ] && cat \"$f\"; done 2>/dev/null || true",
541 dir = VMS_DIR,
542 ))?;
543
544 let mut vms = Vec::new();
545 for line in output.lines() {
546 let line = line.trim();
547 if line.is_empty() {
548 continue;
549 }
550 if let Ok(info) = serde_json::from_str::<RunInfo>(line) {
551 if let Some(ref name) = info.name {
553 let abs_vms = run_in_vm_stdout(&format!("echo {}", VMS_DIR))?;
554 let pid_file = format!("{}/{}/fc.pid", abs_vms, name);
555 if firecracker::is_vm_running(&pid_file).unwrap_or(false) {
556 vms.push(info);
557 }
558 }
559 }
560 }
561
562 Ok(vms)
563}
564
565pub fn allocate_slot(name: &str) -> Result<VmSlot> {
567 let output = run_in_vm_stdout(&format!(
568 r#"for f in {dir}/*/run-info.json; do [ -f "$f" ] && cat "$f"; done 2>/dev/null || true"#,
569 dir = VMS_DIR,
570 ))?;
571
572 let mut used_indices: Vec<u8> = Vec::new();
573 for line in output.lines() {
574 let line = line.trim();
575 if line.is_empty() {
576 continue;
577 }
578 if let Ok(info) = serde_json::from_str::<serde_json::Value>(line)
579 && let Some(idx) = info.get("slot_index").and_then(|v| v.as_u64())
580 {
581 used_indices.push(idx as u8);
582 }
583 }
584
585 for i in 0..253u8 {
587 if !used_indices.contains(&i) {
588 return Ok(VmSlot::new(name, i));
589 }
590 }
591
592 anyhow::bail!("No free VM slots available (max 253 VMs)")
593}
594
595fn create_dev_config_drive(abs_dir: &str, config: &FlakeRunConfig) -> Result<String> {
597 let path = format!("{}/config.ext4", abs_dir);
598 let slot = &config.slot;
599
600 let config_json = serde_json::json!({
601 "instance_id": config.name,
602 "guest_ip": slot.guest_ip,
603 "role": config.profile.as_deref().unwrap_or("worker"),
604 });
605 let escaped_json = config_json.to_string().replace('\'', "'\\''");
606
607 let role = config.profile.as_deref().unwrap_or("worker");
609 let toml_name = format!("{}.toml", role);
610 let toml_content = format!("# Dev-mode {} config stub\n", role);
611 let escaped_toml = toml_content.replace('\'', "'\\''");
612
613 run_in_vm(&format!(
614 r#"
615 rm -f {path}
616 truncate -s 4M {path}
617 mkfs.ext4 -q -L mvm-config {path}
618
619 MOUNT_DIR=$(mktemp -d)
620 sudo mount {path} "$MOUNT_DIR"
621 echo '{json}' | sudo tee "$MOUNT_DIR/config.json" >/dev/null
622 echo '{toml}' | sudo tee "$MOUNT_DIR/{toml_name}" >/dev/null
623 sudo chmod 0444 "$MOUNT_DIR/config.json" "$MOUNT_DIR/{toml_name}"
624 sudo umount "$MOUNT_DIR"
625 rmdir "$MOUNT_DIR"
626 chmod 0644 {path}
627 "#,
628 path = path,
629 json = escaped_json,
630 toml = escaped_toml,
631 toml_name = toml_name,
632 ))?;
633 Ok(path)
634}
635
636fn create_dev_secrets_drive(abs_dir: &str) -> Result<String> {
638 let path = format!("{}/secrets.ext4", abs_dir);
639 run_in_vm(&format!(
640 r#"
641 rm -f {path}
642 truncate -s 4M {path}
643 mkfs.ext4 -q -L mvm-secrets {path}
644
645 MOUNT_DIR=$(mktemp -d)
646 sudo mount {path} "$MOUNT_DIR"
647 echo '{{}}' | sudo tee "$MOUNT_DIR/secrets.json" >/dev/null
648 sudo chmod 0400 "$MOUNT_DIR/secrets.json"
649 sudo umount "$MOUNT_DIR"
650 rmdir "$MOUNT_DIR"
651 chmod 0600 {path}
652 "#,
653 path = path,
654 ))?;
655 Ok(path)
656}
657
658fn configure_flake_microvm(config: &FlakeRunConfig, abs_dir: &str, socket: &str) -> Result<()> {
660 let slot = &config.slot;
661
662 ui::info("Configuring logger...");
663 api_put_socket(
664 socket,
665 "/logger",
666 &format!(
667 r#"{{"log_path": "{dir}/firecracker.log", "level": "Debug", "show_level": true, "show_log_origin": true}}"#,
668 dir = abs_dir,
669 ),
670 )?;
671
672 let boot_args = format!(
676 "console=ttyS0 reboot=k panic=1 net.ifnames=0 mvm.ip={ip}/24 mvm.gw={gw}",
677 ip = slot.guest_ip,
678 gw = BRIDGE_IP,
679 );
680
681 ui::info(&format!("Setting boot source: {}", config.vmlinux_path));
682 let boot_source = match &config.initrd_path {
683 Some(initrd) => {
684 ui::info(&format!("Using initrd: {}", initrd));
685 format!(
686 r#"{{"kernel_image_path": "{kernel}", "boot_args": "{args}", "initrd_path": "{initrd}"}}"#,
687 kernel = config.vmlinux_path,
688 args = boot_args,
689 initrd = initrd,
690 )
691 }
692 None => {
693 format!(
694 r#"{{"kernel_image_path": "{kernel}", "boot_args": "{args}"}}"#,
695 kernel = config.vmlinux_path,
696 args = boot_args,
697 )
698 }
699 };
700 api_put_socket(socket, "/boot-source", &boot_source)?;
701
702 ui::info(&format!(
703 "Setting machine config: {} vCPUs, {} MiB",
704 config.cpus, config.memory
705 ));
706 api_put_socket(
707 socket,
708 "/machine-config",
709 &format!(
710 r#"{{"vcpu_count": {cpus}, "mem_size_mib": {mem}}}"#,
711 cpus = config.cpus,
712 mem = config.memory,
713 ),
714 )?;
715
716 ui::info(&format!("Setting rootfs: {}", config.rootfs_path));
717 api_put_socket(
718 socket,
719 "/drives/rootfs",
720 &format!(
721 r#"{{"drive_id": "rootfs", "path_on_host": "{rootfs}", "is_root_device": true, "is_read_only": false}}"#,
722 rootfs = config.rootfs_path,
723 ),
724 )?;
725
726 ui::info("Creating config drive...");
728 let config_drive = create_dev_config_drive(abs_dir, config)?;
729 api_put_socket(
730 socket,
731 "/drives/config",
732 &format!(
733 r#"{{"drive_id": "config", "path_on_host": "{path}", "is_root_device": false, "is_read_only": true}}"#,
734 path = config_drive,
735 ),
736 )?;
737
738 ui::info("Creating secrets drive...");
740 let secrets_drive = create_dev_secrets_drive(abs_dir)?;
741 api_put_socket(
742 socket,
743 "/drives/secrets",
744 &format!(
745 r#"{{"drive_id": "secrets", "path_on_host": "{path}", "is_root_device": false, "is_read_only": true}}"#,
746 path = secrets_drive,
747 ),
748 )?;
749
750 for (idx, vol) in config.volumes.iter().enumerate() {
751 let drive_id = format!("vol{}", idx);
752 ui::info(&format!(
753 "Attaching volume {} -> {} (size {})",
754 vol.host, vol.guest, vol.size
755 ));
756 api_put_socket(
757 socket,
758 &format!("/drives/{}", drive_id),
759 &format!(
760 r#"{{"drive_id": "{id}", "path_on_host": "{host}", "is_root_device": false, "is_read_only": false}}"#,
761 id = drive_id,
762 host = vol.host,
763 ),
764 )?;
765 }
766
767 ui::info(&format!(
768 "Setting network interface: {} (MAC {})",
769 slot.tap_dev, slot.mac
770 ));
771 api_put_socket(
772 socket,
773 "/network-interfaces/net1",
774 &format!(
775 r#"{{"iface_id": "net1", "guest_mac": "{mac}", "host_dev_name": "{tap}"}}"#,
776 mac = slot.mac,
777 tap = slot.tap_dev,
778 ),
779 )?;
780
781 ui::info("Setting vsock device...");
782 api_put_socket(
783 socket,
784 "/vsock",
785 &format!(
786 r#"{{"vsock_id": "vsock0", "guest_cid": {cid}, "uds_path": "{dir}/v.sock"}}"#,
787 cid = mvm_guest::vsock::GUEST_CID,
788 dir = abs_dir,
789 ),
790 )?;
791
792 Ok(())
793}
794
795fn write_vm_run_info(config: &FlakeRunConfig, abs_dir: &str) -> Result<()> {
797 let info = RunInfo {
798 mode: "flake".to_string(),
799 name: Some(config.name.clone()),
800 revision: Some(config.revision_hash.clone()),
801 flake_ref: Some(config.flake_ref.clone()),
802 guest_ip: Some(config.slot.guest_ip.clone()),
803 profile: config.profile.clone(),
804 guest_user: String::new(),
805 cpus: config.cpus,
806 memory: config.memory,
807 };
808
809 let mut json_value = serde_json::to_value(&info)?;
811 if let Some(obj) = json_value.as_object_mut() {
812 obj.insert(
813 "slot_index".to_string(),
814 serde_json::Value::Number(config.slot.index.into()),
815 );
816 }
817
818 let json = serde_json::to_string(&json_value)?;
819 run_in_vm(&format!(
820 "echo '{}' > {dir}/run-info.json",
821 json,
822 dir = abs_dir,
823 ))?;
824 Ok(())
825}
826
827fn read_vm_run_info_from(abs_dir: &str) -> Option<RunInfo> {
829 let json = run_in_vm_stdout(&format!(
830 "cat {dir}/run-info.json 2>/dev/null || echo 'null'",
831 dir = abs_dir,
832 ))
833 .ok()?;
834 serde_json::from_str(&json).ok()
835}
836
837fn read_slot_index(abs_dir: &str) -> Option<u8> {
839 let json = run_in_vm_stdout(&format!(
840 "cat {dir}/run-info.json 2>/dev/null || echo 'null'",
841 dir = abs_dir,
842 ))
843 .ok()?;
844 let value: serde_json::Value = serde_json::from_str(&json).ok()?;
845 value.get("slot_index")?.as_u64().map(|v| v as u8)
846}
847
848pub fn read_run_info() -> Option<RunInfo> {
850 let json = run_in_vm_stdout(&format!(
851 "cat {dir}/.mvm-run-info 2>/dev/null || echo 'null'",
852 dir = MICROVM_DIR,
853 ))
854 .ok()?;
855 serde_json::from_str(&json).ok()
856}