1use super::PetriVmResourcesOpenVmm;
7use crate::OpenHclServicingFlags;
8use crate::PetriHaltReason;
9use crate::PetriHaltReasonDetail;
10use crate::PetriVmFramebufferAccess;
11use crate::PetriVmInspector;
12use crate::PetriVmRuntime;
13use crate::ShutdownKind;
14use crate::VmScreenshotMeta;
15use crate::openhcl_diag::OpenHclDiagHandler;
16use crate::worker::Worker;
17use anyhow::Context;
18use async_trait::async_trait;
19use framebuffer::View;
20use futures::FutureExt;
21use futures_concurrency::future::Race;
22use get_resources::ged::FirmwareEvent;
23use hyperv_ic_resources::shutdown::ShutdownRpc;
24use mesh::CancelContext;
25use mesh::Receiver;
26use mesh::RecvError;
27use mesh::rpc::RpcError;
28use mesh::rpc::RpcSend;
29use mesh_process::Mesh;
30use openvmm_defs::rpc::PulseSaveRestoreError;
31use pal_async::socket::PolledSocket;
32use petri_artifacts_core::ResolvedArtifact;
33#[cfg(target_os = "linux")]
34use pipette_client::PIPETTE_PORT;
35use pipette_client::PipetteClient;
36use std::future::Future;
37use std::path::Path;
38use std::sync::Arc;
39use std::time::Duration;
40use vmm_core_defs::HaltReason;
41#[cfg(target_os = "linux")]
42use vmsocket::VmAddress;
43#[cfg(target_os = "linux")]
44use vmsocket::VmSocket;
45use vtl2_settings_proto::Vtl2Settings;
46
47pub struct PetriVmOpenVmm {
52 inner: PetriVmInner,
53 halt: PetriVmHaltReceiver,
54}
55
56#[async_trait]
57impl PetriVmRuntime for PetriVmOpenVmm {
58 type VmInspector = OpenVmmInspector;
59 type VmFramebufferAccess = OpenVmmFramebufferAccess;
60
61 async fn teardown(self) -> anyhow::Result<()> {
62 tracing::info!("waiting for worker");
63 let worker = Arc::into_inner(self.inner.worker)
64 .context("all references to the OpenVMM worker have not been closed")?;
65 worker.shutdown().await?;
66
67 tracing::info!("Worker quit, waiting for mesh");
68 self.inner.mesh.shutdown().await;
69
70 tracing::info!("Mesh shutdown, waiting for logging tasks");
71 for t in self.inner.resources.log_stream_tasks {
72 t.await?;
73 }
74
75 Ok(())
76 }
77
78 async fn wait_for_halt(&mut self, allow_reset: bool) -> anyhow::Result<PetriHaltReasonDetail> {
79 let halt_reason = if let Some(already) = self.halt.already_received.take() {
80 already.map_err(anyhow::Error::from)
81 } else {
82 self.halt
83 .halt_notif
84 .recv()
85 .await
86 .context("Failed to get halt reason")
87 }?;
88
89 tracing::info!(?halt_reason, "Got halt reason");
90
91 let reason = match halt_reason {
92 HaltReason::PowerOff => PetriHaltReason::PowerOff,
93 HaltReason::Reset => PetriHaltReason::Reset,
94 HaltReason::Hibernate => PetriHaltReason::Hibernate,
95 HaltReason::TripleFault { .. } => PetriHaltReason::TripleFault,
96 _ => PetriHaltReason::Other,
97 };
98
99 if allow_reset && reason == PetriHaltReason::Reset {
100 self.reset().await?
101 }
102
103 Ok(PetriHaltReasonDetail {
104 reason,
105 detail: format!("{halt_reason:?}"),
106 })
107 }
108
109 async fn wait_for_agent(&mut self, set_high_vtl: bool) -> anyhow::Result<PipetteClient> {
110 Self::wait_for_agent(self, set_high_vtl).await
111 }
112
113 fn openhcl_diag(&self) -> Option<OpenHclDiagHandler> {
114 self.inner.resources.vtl2_vsock_path.as_ref().map(|path| {
115 OpenHclDiagHandler::new(diag_client::DiagClient::from_hybrid_vsock(
116 self.inner.resources.driver.clone(),
117 path,
118 ))
119 })
120 }
121
122 async fn wait_for_boot_event(&mut self) -> anyhow::Result<FirmwareEvent> {
123 Self::wait_for_boot_event(self).await
124 }
125
126 async fn wait_for_enlightened_shutdown_ready(&mut self) -> anyhow::Result<()> {
127 Self::wait_for_enlightened_shutdown_ready(self)
128 .await
129 .map(|_| ())
130 }
131
132 async fn send_enlightened_shutdown(&mut self, kind: ShutdownKind) -> anyhow::Result<()> {
133 Self::send_enlightened_shutdown(self, kind).await
134 }
135
136 async fn restart_openhcl(
137 &mut self,
138 new_openhcl: &ResolvedArtifact,
139 flags: OpenHclServicingFlags,
140 ) -> anyhow::Result<()> {
141 Self::save_openhcl(self, new_openhcl, flags).await?;
142 Self::restore_openhcl(self).await
143 }
144
145 async fn save_openhcl(
146 &mut self,
147 new_openhcl: &ResolvedArtifact,
148 flags: OpenHclServicingFlags,
149 ) -> anyhow::Result<()> {
150 Self::save_openhcl(self, new_openhcl, flags).await
151 }
152
153 async fn restore_openhcl(&mut self) -> anyhow::Result<()> {
154 Self::restore_openhcl(self).await
155 }
156
157 async fn update_command_line(&mut self, command_line: &str) -> anyhow::Result<()> {
158 Self::update_command_line(self, command_line).await
159 }
160
161 fn inspector(&self) -> Option<OpenVmmInspector> {
162 Some(OpenVmmInspector {
163 worker: self.inner.worker.clone(),
164 })
165 }
166
167 fn take_framebuffer_access(&mut self) -> Option<OpenVmmFramebufferAccess> {
168 self.inner
169 .framebuffer_view
170 .take()
171 .map(|view| OpenVmmFramebufferAccess { view })
172 }
173
174 async fn reset(&mut self) -> anyhow::Result<()> {
175 Self::reset(self).await
176 }
177
178 async fn set_vtl2_settings(&mut self, settings: &Vtl2Settings) -> anyhow::Result<()> {
179 Self::set_vtl2_settings(self, settings).await
180 }
181
182 async fn set_vmbus_drive(
183 &mut self,
184 _disk: &crate::Drive,
185 _controller_id: &guid::Guid,
186 _controller_location: u32,
187 ) -> anyhow::Result<()> {
188 todo!("openvmm set vmbus drive")
189 }
190
191 async fn add_pcie_device(
192 &mut self,
193 port_name: String,
194 resource: vm_resource::Resource<vm_resource::kind::PciDeviceHandleKind>,
195 ) -> anyhow::Result<()> {
196 Self::add_pcie_device(self, port_name, resource).await
197 }
198
199 async fn remove_pcie_device(&mut self, port_name: String) -> anyhow::Result<()> {
200 Self::remove_pcie_device(self, port_name).await
201 }
202}
203
204pub(super) struct PetriVmInner {
205 pub(super) resources: PetriVmResourcesOpenVmm,
206 pub(super) mesh: Mesh,
207 pub(super) worker: Arc<Worker>,
208 pub(super) framebuffer_view: Option<View>,
209 pub(super) cidata_mounted: bool,
213 pub(super) tcp_pipette_port: Option<u16>,
216 pub(super) pid: i32,
217}
218
219struct PetriVmHaltReceiver {
220 halt_notif: Receiver<HaltReason>,
221 already_received: Option<Result<HaltReason, RecvError>>,
222}
223
224macro_rules! petri_vm_fn {
227 ($(#[$($attrss:tt)*])* $vis:vis async fn $fn_name:ident (&mut self $(,$arg:ident: $ty:ty)*) $(-> $ret:ty)?) => {
228 $(#[$($attrss)*])*
229 $vis async fn $fn_name(&mut self, $($arg:$ty,)*) $(-> $ret)? {
230 Self::wait_for_halt_or_internal(&mut self.halt, self.inner.$fn_name($($arg,)*)).await
231 }
232 };
233}
234
235impl PetriVmOpenVmm {
238 pub(super) fn new(inner: PetriVmInner, halt_notif: Receiver<HaltReason>) -> Self {
239 Self {
240 inner,
241 halt: PetriVmHaltReceiver {
242 halt_notif,
243 already_received: None,
244 },
245 }
246 }
247
248 pub fn vtl2_vsock_path(&self) -> anyhow::Result<&Path> {
250 self.inner
251 .resources
252 .vtl2_vsock_path
253 .as_deref()
254 .context("VM is not configured with OpenHCL")
255 }
256
257 pub fn pid(&self) -> i32 {
259 self.inner.pid
260 }
261
262 petri_vm_fn!(
263 pub async fn wait_for_boot_event(&mut self) -> anyhow::Result<FirmwareEvent>
266 );
267 petri_vm_fn!(
268 pub async fn wait_for_enlightened_shutdown_ready(&mut self) -> anyhow::Result<Option<mesh::OneshotReceiver<()>>>
272 );
273 petri_vm_fn!(
274 pub async fn send_enlightened_shutdown(&mut self, kind: ShutdownKind) -> anyhow::Result<()>
276 );
277 petri_vm_fn!(
278 pub async fn wait_for_kvp(&mut self) -> anyhow::Result<mesh::Sender<hyperv_ic_resources::kvp::KvpRpc>>
281 );
282 petri_vm_fn!(
283 pub async fn save_openhcl(
285 &mut self,
286 new_openhcl: &ResolvedArtifact,
287 flags: OpenHclServicingFlags
288 ) -> anyhow::Result<()>
289 );
290 petri_vm_fn!(
291 pub async fn restore_openhcl(
293 &mut self
294 ) -> anyhow::Result<()>
295 );
296 petri_vm_fn!(
297 pub async fn update_command_line(
299 &mut self,
300 command_line: &str
301 ) -> anyhow::Result<()>
302 );
303
304 petri_vm_fn!(
305 pub async fn add_pcie_device(
307 &mut self,
308 port_name: String,
309 resource: vm_resource::Resource<vm_resource::kind::PciDeviceHandleKind>
310 ) -> anyhow::Result<()>
311 );
312 petri_vm_fn!(
313 pub async fn remove_pcie_device(
315 &mut self,
316 port_name: String
317 ) -> anyhow::Result<()>
318 );
319 petri_vm_fn!(
320 pub async fn reset(&mut self) -> anyhow::Result<()>
322 );
323 petri_vm_fn!(
324 pub async fn dump_state(&mut self, path: &Path) -> anyhow::Result<()>
326 );
327 petri_vm_fn!(
328 pub async fn wait_for_agent(&mut self, set_high_vtl: bool) -> anyhow::Result<PipetteClient>
330 );
331 petri_vm_fn!(
332 pub async fn set_vtl2_settings(&mut self, settings: &Vtl2Settings) -> anyhow::Result<()>
334 );
335
336 petri_vm_fn!(
337 pub async fn pause(&mut self) -> anyhow::Result<()>
339 );
340 petri_vm_fn!(
341 pub async fn save_state(&mut self) -> anyhow::Result<Vec<u8>>
344 );
345 petri_vm_fn!(
346 pub async fn resume(&mut self) -> anyhow::Result<()>
348 );
349 petri_vm_fn!(
350 pub async fn verify_save_restore(&mut self) -> anyhow::Result<()>
354 );
355 petri_vm_fn!(pub(crate) async fn launch_linux_direct_pipette(&mut self) -> anyhow::Result<()>);
356
357 pub async fn wait_for_halt_or<T, F: Future<Output = anyhow::Result<T>>>(
364 &mut self,
365 future: F,
366 ) -> anyhow::Result<T> {
367 Self::wait_for_halt_or_internal(&mut self.halt, future).await
368 }
369
370 async fn wait_for_halt_or_internal<T, F: Future<Output = anyhow::Result<T>>>(
371 halt: &mut PetriVmHaltReceiver,
372 future: F,
373 ) -> anyhow::Result<T> {
374 let future = &mut std::pin::pin!(future);
375 enum Either<T> {
376 Future(anyhow::Result<T>),
377 Halt(Result<HaltReason, RecvError>),
378 }
379 let res = (
380 future.map(Either::Future),
381 halt.halt_notif.recv().map(Either::Halt),
382 )
383 .race()
384 .await;
385
386 match res {
387 Either::Future(Ok(success)) => Ok(success),
388 Either::Future(Err(e)) => {
389 tracing::warn!(
390 ?e,
391 "Future returned with an error, sleeping for 5 seconds to let outstanding work finish"
392 );
393 let mut c = CancelContext::new().with_timeout(Duration::from_secs(5));
394 c.cancelled().await;
395 Err(e)
396 }
397 Either::Halt(halt_result) => {
398 tracing::warn!(
399 halt_result = format_args!("{:x?}", halt_result),
400 "Halt channel returned while waiting for other future, sleeping for 5 seconds to let outstanding work finish"
401 );
402 let mut c = CancelContext::new().with_timeout(Duration::from_secs(5));
403 let try_again = c.until_cancelled(future).await;
404
405 match try_again {
406 Ok(fut_result) => {
407 halt.already_received = Some(halt_result);
408 if let Err(e) = &fut_result {
409 tracing::warn!(
410 ?e,
411 "Future returned with an error, sleeping for 5 seconds to let outstanding work finish"
412 );
413 let mut c = CancelContext::new().with_timeout(Duration::from_secs(5));
414 c.cancelled().await;
415 }
416 fut_result
417 }
418 Err(_cancel) => match halt_result {
419 Ok(halt_reason) => Err(anyhow::anyhow!("VM halted: {:x?}", halt_reason)),
420 Err(e) => Err(e).context("VM disappeared"),
421 },
422 }
423 }
424 }
425 }
426}
427
428impl PetriVmInner {
429 async fn wait_for_boot_event(&mut self) -> anyhow::Result<FirmwareEvent> {
430 self.resources
431 .firmware_event_recv
432 .recv()
433 .await
434 .context("Failed to get firmware boot event")
435 }
436
437 async fn wait_for_enlightened_shutdown_ready(
438 &mut self,
439 ) -> anyhow::Result<Option<mesh::OneshotReceiver<()>>> {
440 let Some(send) = self.resources.shutdown_ic_send.as_ref() else {
441 return Ok(None);
442 };
443 let recv = send
444 .call(ShutdownRpc::WaitReady, ())
445 .await
446 .context("waiting for shutdown IC to be ready")?;
447 Ok(Some(recv))
448 }
449
450 async fn send_enlightened_shutdown(&mut self, kind: ShutdownKind) -> anyhow::Result<()> {
451 let send = self
452 .resources
453 .shutdown_ic_send
454 .as_ref()
455 .context("shutdown IC not configured")?;
456 let shutdown_result = send
457 .call(
458 ShutdownRpc::Shutdown,
459 hyperv_ic_resources::shutdown::ShutdownParams {
460 shutdown_type: match kind {
461 ShutdownKind::Shutdown => {
462 hyperv_ic_resources::shutdown::ShutdownType::PowerOff
463 }
464 ShutdownKind::Reboot => hyperv_ic_resources::shutdown::ShutdownType::Reboot,
465 },
466 force: false,
467 },
468 )
469 .await?;
470
471 tracing::info!(?shutdown_result, "Shutdown sent");
472 anyhow::ensure!(
473 shutdown_result == hyperv_ic_resources::shutdown::ShutdownResult::Ok,
474 "Got non-Ok shutdown response"
475 );
476
477 Ok(())
478 }
479
480 async fn wait_for_kvp(
481 &mut self,
482 ) -> anyhow::Result<mesh::Sender<hyperv_ic_resources::kvp::KvpRpc>> {
483 tracing::info!("Waiting for KVP IC");
484 let send = self
485 .resources
486 .kvp_ic_send
487 .as_ref()
488 .context("KVP IC not configured")?;
489 let (send, _) = send
490 .call_failable(hyperv_ic_resources::kvp::KvpConnectRpc::WaitForGuest, ())
491 .await
492 .context("failed to connect to KVP IC")?;
493
494 Ok(send)
495 }
496
497 async fn save_openhcl(
498 &self,
499 new_openhcl: &ResolvedArtifact,
500 flags: OpenHclServicingFlags,
501 ) -> anyhow::Result<()> {
502 let ged_send = self
503 .resources
504 .ged_send
505 .as_ref()
506 .context("openhcl not configured")?;
507
508 let igvm_file = fs_err::File::open(new_openhcl).context("failed to open igvm file")?;
509 self.worker
510 .save_openhcl(ged_send, flags, igvm_file.into())
511 .await
512 }
513
514 async fn update_command_line(&mut self, command_line: &str) -> anyhow::Result<()> {
515 self.worker.update_command_line(command_line).await
516 }
517
518 async fn add_pcie_device(
519 &mut self,
520 port_name: String,
521 resource: vm_resource::Resource<vm_resource::kind::PciDeviceHandleKind>,
522 ) -> anyhow::Result<()> {
523 self.worker.add_pcie_device(port_name, resource).await
524 }
525
526 async fn remove_pcie_device(&mut self, port_name: String) -> anyhow::Result<()> {
527 self.worker.remove_pcie_device(port_name).await
528 }
529 async fn dump_state(&mut self, path: &Path) -> anyhow::Result<()> {
530 self.worker.dump_state(path).await
531 }
532 async fn restore_openhcl(&self) -> anyhow::Result<()> {
533 let ged_send = self
534 .resources
535 .ged_send
536 .as_ref()
537 .context("openhcl not configured")?;
538
539 self.worker.restore_openhcl(ged_send).await
540 }
541
542 async fn set_vtl2_settings(&self, settings: &Vtl2Settings) -> anyhow::Result<()> {
543 let ged_send = self
544 .resources
545 .ged_send
546 .as_ref()
547 .context("openhcl not configured")?;
548
549 ged_send
550 .call_failable(
551 get_resources::ged::GuestEmulationRequest::ModifyVtl2Settings,
552 prost::Message::encode_to_vec(settings),
553 )
554 .await?;
555
556 Ok(())
557 }
558
559 async fn reset(&mut self) -> anyhow::Result<()> {
560 tracing::info!("Resetting VM");
561 self.worker.reset().await?;
562 self.cidata_mounted = false;
564 if let Some(agent) = self.resources.linux_direct_serial_agent.as_mut() {
569 agent.reset();
570
571 if self.resources.properties.using_vtl0_pipette {
572 self.launch_linux_direct_pipette().await?;
573 }
574 }
575 Ok(())
576 }
577
578 async fn wait_for_agent(&mut self, set_high_vtl: bool) -> anyhow::Result<PipetteClient> {
579 #[cfg(target_os = "linux")]
580 if let Some(guest_cid) = self.resources.properties.vhost_vsock_guest_cid {
581 assert!(
582 !set_high_vtl,
583 "kernel vhost-vsock pipette transport does not support VTL2"
584 );
585 return self.wait_for_agent_vhost_vsock(guest_cid).await;
586 }
587
588 if let Some(port) = self.tcp_pipette_port {
590 assert!(!set_high_vtl, "TCP pipette transport does not support VTL2");
591 return self.wait_for_agent_tcp(port).await;
592 }
593
594 let listener = if set_high_vtl {
595 self.resources
596 .vtl2_pipette_listener
597 .as_mut()
598 .context("VM is not configured with VTL 2")?
599 } else {
600 &mut self.resources.pipette_listener
601 };
602
603 tracing::info!(set_high_vtl, "listening for pipette connection");
604 let client = loop {
605 let (conn, _) = listener
606 .accept()
607 .await
608 .context("failed to accept pipette connection")?;
609 tracing::info!(set_high_vtl, "handshaking with pipette");
610 let socket = PolledSocket::new(&self.resources.driver, conn)?;
611 match PipetteClient::new(&self.resources.driver, socket, &self.resources.output_dir)
612 .await
613 {
614 Ok(client) => break client,
615 Err(e) => {
616 tracing::warn!(
622 error = e.as_ref() as &dyn std::error::Error,
623 "pipette connection not live, retrying"
624 );
625 }
626 }
627 };
628 tracing::info!(set_high_vtl, "completed pipette handshake");
629
630 if !set_high_vtl
635 && self.resources.properties.uses_pipette_as_init
636 && self.resources.properties.has_agent_disk
637 && !self.cidata_mounted
638 {
639 tracing::info!("mounting CIDATA agent disk via pipette");
640 client
641 .unix_shell()
642 .cmd("mkdir")
643 .arg("-p")
644 .arg("/cidata")
645 .run()
646 .await
647 .context("failed to create /cidata mount point")?;
648 client
649 .unix_shell()
650 .cmd("mount")
651 .arg("LABEL=cidata")
652 .arg("/cidata")
653 .run()
654 .await
655 .context("failed to mount CIDATA disk")?;
656 self.cidata_mounted = true;
657 }
658
659 Ok(client)
660 }
661
662 #[cfg(target_os = "linux")]
664 async fn wait_for_agent_vhost_vsock(
665 &mut self,
666 guest_cid: u32,
667 ) -> anyhow::Result<PipetteClient> {
668 tracing::info!(
669 guest_cid,
670 port = PIPETTE_PORT,
671 "connecting to pipette via kernel vhost-vsock"
672 );
673 let socket = loop {
674 let connect = async {
675 let socket = VmSocket::new().context("failed to create AF_VSOCK socket")?;
676 socket
677 .set_connect_timeout(Duration::from_secs(5))
678 .context("failed to set AF_VSOCK connect timeout")?;
679 let mut socket = PolledSocket::new(&self.resources.driver, socket)
680 .context("failed to create polled AF_VSOCK socket")?
681 .convert();
682 socket
683 .connect(&VmAddress::vsock(guest_cid, PIPETTE_PORT).into())
684 .await
685 .context("failed to connect to guest AF_VSOCK listener")?;
686 Ok::<_, anyhow::Error>(socket)
687 };
688
689 match connect.await {
690 Ok(socket) => break socket,
691 Err(error) => {
692 tracing::trace!(
693 error = error.as_ref() as &dyn std::error::Error,
694 "AF_VSOCK connect failed, guest not ready yet"
695 );
696 }
697 }
698
699 pal_async::timer::PolledTimer::new(&self.resources.driver)
700 .sleep(Duration::from_secs(1))
701 .await;
702 };
703 tracing::info!("AF_VSOCK connected, handshaking with pipette");
704 let client = PipetteClient::new(&self.resources.driver, socket, &self.resources.output_dir)
705 .await
706 .context("pipette AF_VSOCK handshake failed")?;
707 tracing::info!("completed pipette AF_VSOCK handshake");
708 Ok(client)
709 }
710
711 async fn wait_for_agent_tcp(&mut self, port: u16) -> anyhow::Result<PipetteClient> {
717 tracing::info!(port, "connecting to pipette via TCP");
718 let addr = std::net::SocketAddr::from((std::net::Ipv4Addr::LOCALHOST, port));
719 let client = loop {
720 match PolledSocket::connect_tcp(&self.resources.driver, addr).await {
721 Ok(socket) => {
722 socket
723 .get()
724 .set_nodelay(true)
725 .context("failed to set TCP_NODELAY")?;
726 tracing::info!("TCP connected, handshaking with pipette");
727 match PipetteClient::new(
728 &self.resources.driver,
729 socket,
730 &self.resources.output_dir,
731 )
732 .await
733 {
734 Ok(client) => break client,
735 Err(e) => {
736 tracing::warn!(
737 error = e.as_ref() as &dyn std::error::Error,
738 "pipette TCP connection failed, retrying"
739 );
740 }
741 }
742 }
743 Err(e) => {
744 tracing::trace!(
745 error = &e as &dyn std::error::Error,
746 "TCP connect failed, guest not ready yet"
747 );
748 }
749 }
750 pal_async::timer::PolledTimer::new(&self.resources.driver)
752 .sleep(Duration::from_secs(1))
753 .await;
754 };
755 tracing::info!("completed pipette TCP handshake");
756 Ok(client)
757 }
758
759 async fn pause(&self) -> anyhow::Result<()> {
760 self.worker.pause().await?;
761 Ok(())
762 }
763
764 async fn save_state(&self) -> anyhow::Result<Vec<u8>> {
765 let state_msg = self.worker.save().await?;
766 Ok(mesh::payload::encode(state_msg))
767 }
768
769 async fn resume(&self) -> anyhow::Result<()> {
770 self.worker.resume().await?;
771 Ok(())
772 }
773
774 async fn verify_save_restore(&self) -> anyhow::Result<()> {
775 for i in 0..2 {
776 let result = self.worker.pulse_save_restore().await;
777 match result {
778 Ok(()) => {}
779 Err(RpcError::Channel(err)) => return Err(err.into()),
780 Err(RpcError::Call(PulseSaveRestoreError::ResetNotSupported)) => {
781 tracing::warn!("Reset not supported, could not test save + restore.");
782 break;
783 }
784 Err(RpcError::Call(PulseSaveRestoreError::Other(err))) => {
785 return Err(anyhow::Error::from(err))
786 .context(format!("Save + restore {i} failed."));
787 }
788 }
789 }
790
791 Ok(())
792 }
793
794 async fn launch_linux_direct_pipette(&mut self) -> anyhow::Result<()> {
795 self.resources
797 .linux_direct_serial_agent
798 .as_mut()
799 .unwrap()
800 .run_command("mkdir /cidata && mount LABEL=cidata /cidata && sh -c '/cidata/pipette &'")
801 .await?;
802 Ok(())
803 }
804}
805
806pub struct OpenVmmInspector {
808 worker: Arc<Worker>,
809}
810
811#[async_trait]
812impl PetriVmInspector for OpenVmmInspector {
813 async fn inspect(&self, path: &str) -> anyhow::Result<inspect::Node> {
814 Ok(self.worker.inspect(path).await)
815 }
816}
817
818pub struct OpenVmmFramebufferAccess {
820 view: View,
821}
822
823#[async_trait]
824impl PetriVmFramebufferAccess for OpenVmmFramebufferAccess {
825 async fn screenshot(
826 &mut self,
827 image: &mut Vec<u8>,
828 ) -> anyhow::Result<Option<VmScreenshotMeta>> {
829 const BYTES_PER_PIXEL: usize = 4;
834 let (width, height) = self.view.resolution();
835 let (widthsize, heightsize) = (width as usize, height as usize);
836 let len = widthsize * heightsize * BYTES_PER_PIXEL;
837
838 image.resize(len, 0);
839 for (i, line) in (0..height).zip(image.chunks_exact_mut(widthsize * BYTES_PER_PIXEL)) {
840 self.view.read_line(i, line);
841 for pixel in line.chunks_exact_mut(BYTES_PER_PIXEL) {
842 pixel.swap(0, 2);
843 pixel[3] = 0xFF;
844 }
845 }
846
847 Ok(Some(VmScreenshotMeta {
848 color: image::ExtendedColorType::Rgba8,
849 width,
850 height,
851 }))
852 }
853}