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