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(
123 &mut self,
124 timeout: Option<Duration>,
125 ) -> anyhow::Result<Option<FirmwareEvent>> {
126 CancelContext::new()
129 .with_timeout(timeout.unwrap_or(Duration::MAX))
130 .until_cancelled(Self::wait_for_boot_event(self))
131 .await
132 .ok()
133 .transpose()
134 }
135
136 async fn wait_for_enlightened_shutdown_ready(&mut self) -> anyhow::Result<()> {
137 Self::wait_for_enlightened_shutdown_ready(self)
138 .await
139 .map(|_| ())
140 }
141
142 async fn send_enlightened_shutdown(&mut self, kind: ShutdownKind) -> anyhow::Result<()> {
143 Self::send_enlightened_shutdown(self, kind).await
144 }
145
146 async fn restart_openhcl(
147 &mut self,
148 new_openhcl: &ResolvedArtifact,
149 flags: OpenHclServicingFlags,
150 ) -> anyhow::Result<()> {
151 Self::save_openhcl(self, new_openhcl, flags).await?;
152 Self::restore_openhcl(self).await
153 }
154
155 async fn save_openhcl(
156 &mut self,
157 new_openhcl: &ResolvedArtifact,
158 flags: OpenHclServicingFlags,
159 ) -> anyhow::Result<()> {
160 Self::save_openhcl(self, new_openhcl, flags).await
161 }
162
163 async fn restore_openhcl(&mut self) -> anyhow::Result<()> {
164 Self::restore_openhcl(self).await
165 }
166
167 async fn update_command_line(&mut self, command_line: &str) -> anyhow::Result<()> {
168 Self::update_command_line(self, command_line).await
169 }
170
171 fn inspector(&self) -> Option<OpenVmmInspector> {
172 Some(OpenVmmInspector {
173 worker: self.inner.worker.clone(),
174 })
175 }
176
177 fn take_framebuffer_access(&mut self) -> Option<OpenVmmFramebufferAccess> {
178 self.inner
179 .framebuffer_view
180 .take()
181 .map(|view| OpenVmmFramebufferAccess { view })
182 }
183
184 async fn reset(&mut self) -> anyhow::Result<()> {
185 Self::reset(self).await
186 }
187
188 async fn set_vtl2_settings(&mut self, settings: &Vtl2Settings) -> anyhow::Result<()> {
189 Self::set_vtl2_settings(self, settings).await
190 }
191
192 async fn set_vmbus_drive(
193 &mut self,
194 _disk: &crate::Drive,
195 _controller_id: &guid::Guid,
196 _controller_location: u32,
197 ) -> anyhow::Result<()> {
198 todo!("openvmm set vmbus drive")
199 }
200
201 async fn add_pcie_device(
202 &mut self,
203 port_name: String,
204 resource: vm_resource::Resource<vm_resource::kind::PciDeviceHandleKind>,
205 ) -> anyhow::Result<()> {
206 Self::add_pcie_device(self, port_name, resource).await
207 }
208
209 async fn remove_pcie_device(&mut self, port_name: String) -> anyhow::Result<()> {
210 Self::remove_pcie_device(self, port_name).await
211 }
212}
213
214pub(super) struct PetriVmInner {
215 pub(super) resources: PetriVmResourcesOpenVmm,
216 pub(super) mesh: Mesh,
217 pub(super) worker: Arc<Worker>,
218 pub(super) framebuffer_view: Option<View>,
219 pub(super) cidata_mounted: bool,
223 pub(super) tcp_pipette_port: Option<u16>,
226 pub(super) pid: i32,
227}
228
229struct PetriVmHaltReceiver {
230 halt_notif: Receiver<HaltReason>,
231 already_received: Option<Result<HaltReason, RecvError>>,
232}
233
234macro_rules! petri_vm_fn {
237 ($(#[$($attrss:tt)*])* $vis:vis async fn $fn_name:ident (&mut self $(,$arg:ident: $ty:ty)*) $(-> $ret:ty)?) => {
238 $(#[$($attrss)*])*
239 $vis async fn $fn_name(&mut self, $($arg:$ty,)*) $(-> $ret)? {
240 Self::wait_for_halt_or_internal(&mut self.halt, self.inner.$fn_name($($arg,)*)).await
241 }
242 };
243}
244
245impl PetriVmOpenVmm {
248 pub(super) fn new(inner: PetriVmInner, halt_notif: Receiver<HaltReason>) -> Self {
249 Self {
250 inner,
251 halt: PetriVmHaltReceiver {
252 halt_notif,
253 already_received: None,
254 },
255 }
256 }
257
258 pub fn vtl2_vsock_path(&self) -> anyhow::Result<&Path> {
260 self.inner
261 .resources
262 .vtl2_vsock_path
263 .as_deref()
264 .context("VM is not configured with OpenHCL")
265 }
266
267 pub fn pid(&self) -> i32 {
269 self.inner.pid
270 }
271
272 petri_vm_fn!(
273 pub async fn wait_for_boot_event(&mut self) -> anyhow::Result<FirmwareEvent>
276 );
277 petri_vm_fn!(
278 pub async fn wait_for_enlightened_shutdown_ready(&mut self) -> anyhow::Result<Option<mesh::OneshotReceiver<()>>>
282 );
283 petri_vm_fn!(
284 pub async fn send_enlightened_shutdown(&mut self, kind: ShutdownKind) -> anyhow::Result<()>
286 );
287 petri_vm_fn!(
288 pub async fn wait_for_kvp(&mut self) -> anyhow::Result<mesh::Sender<hyperv_ic_resources::kvp::KvpRpc>>
291 );
292 petri_vm_fn!(
293 pub async fn save_openhcl(
295 &mut self,
296 new_openhcl: &ResolvedArtifact,
297 flags: OpenHclServicingFlags
298 ) -> anyhow::Result<()>
299 );
300 petri_vm_fn!(
301 pub async fn restore_openhcl(
303 &mut self
304 ) -> anyhow::Result<()>
305 );
306 petri_vm_fn!(
307 pub async fn update_command_line(
309 &mut self,
310 command_line: &str
311 ) -> anyhow::Result<()>
312 );
313
314 petri_vm_fn!(
315 pub async fn add_pcie_device(
317 &mut self,
318 port_name: String,
319 resource: vm_resource::Resource<vm_resource::kind::PciDeviceHandleKind>
320 ) -> anyhow::Result<()>
321 );
322 petri_vm_fn!(
323 pub async fn remove_pcie_device(
325 &mut self,
326 port_name: String
327 ) -> anyhow::Result<()>
328 );
329 petri_vm_fn!(
330 pub async fn reset(&mut self) -> anyhow::Result<()>
332 );
333 petri_vm_fn!(
334 pub async fn dump_state(&mut self, path: &Path) -> anyhow::Result<()>
336 );
337 petri_vm_fn!(
338 pub async fn wait_for_agent(&mut self, set_high_vtl: bool) -> anyhow::Result<PipetteClient>
340 );
341 petri_vm_fn!(
342 pub async fn set_vtl2_settings(&mut self, settings: &Vtl2Settings) -> anyhow::Result<()>
344 );
345
346 petri_vm_fn!(
347 pub async fn pause(&mut self) -> anyhow::Result<()>
349 );
350 petri_vm_fn!(
351 pub async fn save_state(&mut self) -> anyhow::Result<Vec<u8>>
354 );
355 petri_vm_fn!(
356 pub async fn resume(&mut self) -> anyhow::Result<()>
358 );
359 petri_vm_fn!(
360 pub async fn verify_save_restore(&mut self) -> anyhow::Result<()>
364 );
365 petri_vm_fn!(pub(crate) async fn launch_linux_direct_pipette(&mut self) -> anyhow::Result<()>);
366
367 pub async fn wait_for_halt_or<T, F: Future<Output = anyhow::Result<T>>>(
374 &mut self,
375 future: F,
376 ) -> anyhow::Result<T> {
377 Self::wait_for_halt_or_internal(&mut self.halt, future).await
378 }
379
380 async fn wait_for_halt_or_internal<T, F: Future<Output = anyhow::Result<T>>>(
381 halt: &mut PetriVmHaltReceiver,
382 future: F,
383 ) -> anyhow::Result<T> {
384 let future = &mut std::pin::pin!(future);
385 enum Either<T> {
386 Future(anyhow::Result<T>),
387 Halt(Result<HaltReason, RecvError>),
388 }
389 let res = (
390 future.map(Either::Future),
391 halt.halt_notif.recv().map(Either::Halt),
392 )
393 .race()
394 .await;
395
396 match res {
397 Either::Future(Ok(success)) => Ok(success),
398 Either::Future(Err(e)) => {
399 tracing::warn!(
400 ?e,
401 "Future returned with an error, sleeping for 5 seconds to let outstanding work finish"
402 );
403 let mut c = CancelContext::new().with_timeout(Duration::from_secs(5));
404 c.cancelled().await;
405 Err(e)
406 }
407 Either::Halt(halt_result) => {
408 tracing::warn!(
409 halt_result = format_args!("{:x?}", halt_result),
410 "Halt channel returned while waiting for other future, sleeping for 5 seconds to let outstanding work finish"
411 );
412 let mut c = CancelContext::new().with_timeout(Duration::from_secs(5));
413 let try_again = c.until_cancelled(future).await;
414
415 match try_again {
416 Ok(fut_result) => {
417 halt.already_received = Some(halt_result);
418 if let Err(e) = &fut_result {
419 tracing::warn!(
420 ?e,
421 "Future returned with an error, sleeping for 5 seconds to let outstanding work finish"
422 );
423 let mut c = CancelContext::new().with_timeout(Duration::from_secs(5));
424 c.cancelled().await;
425 }
426 fut_result
427 }
428 Err(_cancel) => match halt_result {
429 Ok(halt_reason) => Err(anyhow::anyhow!("VM halted: {:x?}", halt_reason)),
430 Err(e) => Err(e).context("VM disappeared"),
431 },
432 }
433 }
434 }
435 }
436}
437
438impl PetriVmInner {
439 async fn wait_for_boot_event(&mut self) -> anyhow::Result<FirmwareEvent> {
440 self.resources
441 .firmware_event_recv
442 .recv()
443 .await
444 .context("Failed to get firmware boot event")
445 }
446
447 async fn wait_for_enlightened_shutdown_ready(
448 &mut self,
449 ) -> anyhow::Result<Option<mesh::OneshotReceiver<()>>> {
450 let Some(send) = self.resources.shutdown_ic_send.as_ref() else {
451 return Ok(None);
452 };
453 let recv = send
454 .call(ShutdownRpc::WaitReady, ())
455 .await
456 .context("waiting for shutdown IC to be ready")?;
457 Ok(Some(recv))
458 }
459
460 async fn send_enlightened_shutdown(&mut self, kind: ShutdownKind) -> anyhow::Result<()> {
461 let send = self
462 .resources
463 .shutdown_ic_send
464 .as_ref()
465 .context("shutdown IC not configured")?;
466 let shutdown_result = send
467 .call(
468 ShutdownRpc::Shutdown,
469 hyperv_ic_resources::shutdown::ShutdownParams {
470 shutdown_type: match kind {
471 ShutdownKind::Shutdown => {
472 hyperv_ic_resources::shutdown::ShutdownType::PowerOff
473 }
474 ShutdownKind::Reboot => hyperv_ic_resources::shutdown::ShutdownType::Reboot,
475 },
476 force: false,
477 },
478 )
479 .await?;
480
481 tracing::info!(?shutdown_result, "Shutdown sent");
482 anyhow::ensure!(
483 shutdown_result == hyperv_ic_resources::shutdown::ShutdownResult::Ok,
484 "Got non-Ok shutdown response"
485 );
486
487 Ok(())
488 }
489
490 async fn wait_for_kvp(
491 &mut self,
492 ) -> anyhow::Result<mesh::Sender<hyperv_ic_resources::kvp::KvpRpc>> {
493 tracing::info!("Waiting for KVP IC");
494 let send = self
495 .resources
496 .kvp_ic_send
497 .as_ref()
498 .context("KVP IC not configured")?;
499 let (send, _) = send
500 .call_failable(hyperv_ic_resources::kvp::KvpConnectRpc::WaitForGuest, ())
501 .await
502 .context("failed to connect to KVP IC")?;
503
504 Ok(send)
505 }
506
507 async fn save_openhcl(
508 &self,
509 new_openhcl: &ResolvedArtifact,
510 flags: OpenHclServicingFlags,
511 ) -> anyhow::Result<()> {
512 let ged_send = self
513 .resources
514 .ged_send
515 .as_ref()
516 .context("openhcl not configured")?;
517
518 let igvm_file = fs_err::File::open(new_openhcl).context("failed to open igvm file")?;
519 self.worker
520 .save_openhcl(ged_send, flags, igvm_file.into())
521 .await
522 }
523
524 async fn update_command_line(&mut self, command_line: &str) -> anyhow::Result<()> {
525 self.worker.update_command_line(command_line).await
526 }
527
528 async fn add_pcie_device(
529 &mut self,
530 port_name: String,
531 resource: vm_resource::Resource<vm_resource::kind::PciDeviceHandleKind>,
532 ) -> anyhow::Result<()> {
533 self.worker.add_pcie_device(port_name, resource).await
534 }
535
536 async fn remove_pcie_device(&mut self, port_name: String) -> anyhow::Result<()> {
537 self.worker.remove_pcie_device(port_name).await
538 }
539 async fn dump_state(&mut self, path: &Path) -> anyhow::Result<()> {
540 self.worker.dump_state(path).await
541 }
542 async fn restore_openhcl(&self) -> anyhow::Result<()> {
543 let ged_send = self
544 .resources
545 .ged_send
546 .as_ref()
547 .context("openhcl not configured")?;
548
549 self.worker.restore_openhcl(ged_send).await
550 }
551
552 async fn set_vtl2_settings(&self, settings: &Vtl2Settings) -> anyhow::Result<()> {
553 let ged_send = self
554 .resources
555 .ged_send
556 .as_ref()
557 .context("openhcl not configured")?;
558
559 ged_send
560 .call_failable(
561 get_resources::ged::GuestEmulationRequest::ModifyVtl2Settings,
562 prost::Message::encode_to_vec(settings),
563 )
564 .await?;
565
566 Ok(())
567 }
568
569 async fn reset(&mut self) -> anyhow::Result<()> {
570 tracing::info!("Resetting VM");
571 self.worker.reset().await?;
572 while self.resources.firmware_event_recv.try_recv().is_ok() {}
575 self.cidata_mounted = false;
577 if let Some(agent) = self.resources.linux_direct_serial_agent.as_mut() {
582 agent.reset();
583
584 if self.resources.properties.using_vtl0_pipette {
585 self.launch_linux_direct_pipette().await?;
586 }
587 }
588 Ok(())
589 }
590
591 async fn wait_for_agent(&mut self, set_high_vtl: bool) -> anyhow::Result<PipetteClient> {
592 #[cfg(target_os = "linux")]
593 if let Some(guest_cid) = self.resources.properties.vhost_vsock_guest_cid {
594 assert!(
595 !set_high_vtl,
596 "kernel vhost-vsock pipette transport does not support VTL2"
597 );
598 return self.wait_for_agent_vhost_vsock(guest_cid).await;
599 }
600
601 if let Some(port) = self.tcp_pipette_port {
603 assert!(!set_high_vtl, "TCP pipette transport does not support VTL2");
604 return self.wait_for_agent_tcp(port).await;
605 }
606
607 let listener = if set_high_vtl {
608 self.resources
609 .vtl2_pipette_listener
610 .as_mut()
611 .context("VM is not configured with VTL 2")?
612 } else {
613 &mut self.resources.pipette_listener
614 };
615
616 tracing::info!(set_high_vtl, "listening for pipette connection");
617 let client = loop {
618 let (conn, _) = listener
619 .accept()
620 .await
621 .context("failed to accept pipette connection")?;
622 tracing::info!(set_high_vtl, "handshaking with pipette");
623 let socket = PolledSocket::new(&self.resources.driver, conn)?;
624 match PipetteClient::new(&self.resources.driver, socket, &self.resources.output_dir)
625 .await
626 {
627 Ok(client) => break client,
628 Err(e) => {
629 tracing::warn!(
635 error = e.as_ref() as &dyn std::error::Error,
636 "pipette connection not live, retrying"
637 );
638 }
639 }
640 };
641 tracing::info!(set_high_vtl, "completed pipette handshake");
642
643 if !set_high_vtl
648 && self.resources.properties.uses_pipette_as_init
649 && self.resources.properties.has_agent_disk
650 && !self.cidata_mounted
651 {
652 tracing::info!("mounting CIDATA agent disk via pipette");
653 client
654 .unix_shell()
655 .cmd("mkdir")
656 .arg("-p")
657 .arg("/cidata")
658 .run()
659 .await
660 .context("failed to create /cidata mount point")?;
661 client
662 .unix_shell()
663 .cmd("mount")
664 .arg("LABEL=cidata")
665 .arg("/cidata")
666 .run()
667 .await
668 .context("failed to mount CIDATA disk")?;
669 self.cidata_mounted = true;
670 }
671
672 Ok(client)
673 }
674
675 #[cfg(target_os = "linux")]
677 async fn wait_for_agent_vhost_vsock(
678 &mut self,
679 guest_cid: u32,
680 ) -> anyhow::Result<PipetteClient> {
681 tracing::info!(
682 guest_cid,
683 port = PIPETTE_PORT,
684 "connecting to pipette via kernel vhost-vsock"
685 );
686 let socket = loop {
687 let connect = async {
688 let socket = VmSocket::new().context("failed to create AF_VSOCK socket")?;
689 socket
690 .set_connect_timeout(Duration::from_secs(5))
691 .context("failed to set AF_VSOCK connect timeout")?;
692 let mut socket = PolledSocket::new(&self.resources.driver, socket)
693 .context("failed to create polled AF_VSOCK socket")?
694 .convert();
695 socket
696 .connect(&VmAddress::vsock(guest_cid, PIPETTE_PORT).into())
697 .await
698 .context("failed to connect to guest AF_VSOCK listener")?;
699 Ok::<_, anyhow::Error>(socket)
700 };
701
702 match connect.await {
703 Ok(socket) => break socket,
704 Err(error) => {
705 tracing::trace!(
706 error = error.as_ref() as &dyn std::error::Error,
707 "AF_VSOCK connect failed, guest not ready yet"
708 );
709 }
710 }
711
712 pal_async::timer::PolledTimer::new(&self.resources.driver)
713 .sleep(Duration::from_secs(1))
714 .await;
715 };
716 tracing::info!("AF_VSOCK connected, handshaking with pipette");
717 let client = PipetteClient::new(&self.resources.driver, socket, &self.resources.output_dir)
718 .await
719 .context("pipette AF_VSOCK handshake failed")?;
720 tracing::info!("completed pipette AF_VSOCK handshake");
721 Ok(client)
722 }
723
724 async fn wait_for_agent_tcp(&mut self, port: u16) -> anyhow::Result<PipetteClient> {
730 tracing::info!(port, "connecting to pipette via TCP");
731 let addr = std::net::SocketAddr::from((std::net::Ipv4Addr::LOCALHOST, port));
732 let client = loop {
733 match PolledSocket::connect_tcp(&self.resources.driver, addr).await {
734 Ok(socket) => {
735 socket
736 .get()
737 .set_nodelay(true)
738 .context("failed to set TCP_NODELAY")?;
739 tracing::info!("TCP connected, handshaking with pipette");
740 match PipetteClient::new(
741 &self.resources.driver,
742 socket,
743 &self.resources.output_dir,
744 )
745 .await
746 {
747 Ok(client) => break client,
748 Err(e) => {
749 tracing::warn!(
750 error = e.as_ref() as &dyn std::error::Error,
751 "pipette TCP connection failed, retrying"
752 );
753 }
754 }
755 }
756 Err(e) => {
757 tracing::trace!(
758 error = &e as &dyn std::error::Error,
759 "TCP connect failed, guest not ready yet"
760 );
761 }
762 }
763 pal_async::timer::PolledTimer::new(&self.resources.driver)
765 .sleep(Duration::from_secs(1))
766 .await;
767 };
768 tracing::info!("completed pipette TCP handshake");
769 Ok(client)
770 }
771
772 async fn pause(&self) -> anyhow::Result<()> {
773 self.worker.pause().await?;
774 Ok(())
775 }
776
777 async fn save_state(&self) -> anyhow::Result<Vec<u8>> {
778 let state_msg = self.worker.save().await?;
779 Ok(mesh::payload::encode(state_msg))
780 }
781
782 async fn resume(&self) -> anyhow::Result<()> {
783 self.worker.resume().await?;
784 Ok(())
785 }
786
787 async fn verify_save_restore(&self) -> anyhow::Result<()> {
788 for i in 0..2 {
789 let result = self.worker.pulse_save_restore().await;
790 match result {
791 Ok(()) => {}
792 Err(RpcError::Channel(err)) => return Err(err.into()),
793 Err(RpcError::Call(PulseSaveRestoreError::ResetNotSupported)) => {
794 tracing::warn!("Reset not supported, could not test save + restore.");
795 break;
796 }
797 Err(RpcError::Call(PulseSaveRestoreError::Other(err))) => {
798 return Err(anyhow::Error::from(err))
799 .context(format!("Save + restore {i} failed."));
800 }
801 }
802 }
803
804 Ok(())
805 }
806
807 async fn launch_linux_direct_pipette(&mut self) -> anyhow::Result<()> {
808 self.resources
810 .linux_direct_serial_agent
811 .as_mut()
812 .unwrap()
813 .run_command("mkdir /cidata && mount LABEL=cidata /cidata && sh -c '/cidata/pipette &'")
814 .await?;
815 Ok(())
816 }
817}
818
819pub struct OpenVmmInspector {
821 worker: Arc<Worker>,
822}
823
824#[async_trait]
825impl PetriVmInspector for OpenVmmInspector {
826 async fn inspect(&self, path: &str) -> anyhow::Result<inspect::Node> {
827 Ok(self.worker.inspect(path).await)
828 }
829}
830
831pub struct OpenVmmFramebufferAccess {
833 view: View,
834}
835
836#[async_trait]
837impl PetriVmFramebufferAccess for OpenVmmFramebufferAccess {
838 async fn screenshot(
839 &mut self,
840 image: &mut Vec<u8>,
841 ) -> anyhow::Result<Option<VmScreenshotMeta>> {
842 const BYTES_PER_PIXEL: usize = 4;
847 let (width, height) = self.view.resolution();
848 let (widthsize, heightsize) = (width as usize, height as usize);
849 let len = widthsize * heightsize * BYTES_PER_PIXEL;
850
851 image.resize(len, 0);
852 for (i, line) in (0..height).zip(image.chunks_exact_mut(widthsize * BYTES_PER_PIXEL)) {
853 self.view.read_line(i, line);
854 for pixel in line.chunks_exact_mut(BYTES_PER_PIXEL) {
855 pixel.swap(0, 2);
856 pixel[3] = 0xFF;
857 }
858 }
859
860 Ok(Some(VmScreenshotMeta {
861 color: image::ExtendedColorType::Rgba8,
862 width,
863 height,
864 }))
865 }
866}