Skip to main content

petri/vm/openvmm/
runtime.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Methods to interact with a running [`PetriVmOpenVmm`].
5
6use 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
49/// A running VM that tests can interact with.
50// DEVNOTE: Really the PetriVmInner is the actual VM and channels that we interact
51// with. This struct exists as a wrapper to provide error handling, such as not
52// hanging indefinitely when waiting on certain channels if the VM crashes.
53pub 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        // The event arrives on a channel, so cancelling the wait can't discard
129        // one that has already been delivered.
130        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    /// Whether CIDATA has already been mounted inside the guest.
222    /// Used to skip re-mounting after save/restore (where guest state is
223    /// preserved) while still mounting after a full reset/reboot.
224    pub(super) cidata_mounted: bool,
225    /// Resolved TCP pipette port for no-vmbus Windows guests. Set once
226    /// during startup and reused across reconnections (e.g. after reset).
227    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
236// Wrap a PetriVmInner function in [`PetriVmOpenVmm::wait_for_halt_or_internal`] to
237// provide better error handling.
238macro_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
247// TODO: Add all runtime functions that are not backend specific
248// to the `PetriVmRuntime` trait
249impl 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    /// Get the path to the VTL 2 vsock socket, if the VM is configured with OpenHCL.
261    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    /// Get the PID of the openvmm child process.
270    pub fn pid(&self) -> i32 {
271        self.inner.pid
272    }
273
274    petri_vm_fn!(
275        /// Waits for an event emitted by the firmware about its boot status, and
276        /// returns that status.
277        pub async fn wait_for_boot_event(&mut self) -> anyhow::Result<FirmwareEvent>
278    );
279    petri_vm_fn!(
280        /// Waits for an IPMI SEL notification received from OpenHCL.
281        pub async fn wait_for_ipmi_sel(&mut self) -> anyhow::Result<IpmiSelEvent>
282    );
283    petri_vm_fn!(
284        /// Wait for OpenHCL's VTL0 start report, preserving a guest-reported
285        /// initialization error separately from transport and VM-halt errors.
286        pub async fn wait_for_vtl0_start(&mut self) -> anyhow::Result<Result<(), get_resources::ged::Vtl0StartError>>
287    );
288    petri_vm_fn!(
289        /// Waits for the Hyper-V shutdown IC to be ready, returning a receiver
290        /// that will be closed when it is no longer ready. Returns `None` if
291        /// the shutdown IC is not configured.
292        pub async fn wait_for_enlightened_shutdown_ready(&mut self) -> anyhow::Result<Option<mesh::OneshotReceiver<()>>>
293    );
294    petri_vm_fn!(
295        /// Instruct the guest to shutdown via the Hyper-V shutdown IC.
296        pub async fn send_enlightened_shutdown(&mut self, kind: ShutdownKind) -> anyhow::Result<()>
297    );
298    petri_vm_fn!(
299        /// Inject an ACPI power-button event without waiting for guest shutdown.
300        ///
301        /// Requires direct ACPI, UEFI, or PCAT boot and a guest power-button handler.
302        pub async fn send_acpi_shutdown(&mut self) -> anyhow::Result<()>
303    );
304    petri_vm_fn!(
305        /// Waits for the KVP IC to be ready, returning a sender that can be used
306        /// to send requests to it.
307        pub async fn wait_for_kvp(&mut self) -> anyhow::Result<mesh::Sender<hyperv_ic_resources::kvp::KvpRpc>>
308    );
309    petri_vm_fn!(
310        /// Stages the new OpenHCL file and saves the existing state.
311        pub async fn save_openhcl(
312            &mut self,
313            new_openhcl: &ResolvedArtifact,
314            flags: OpenHclServicingFlags
315        ) -> anyhow::Result<()>
316    );
317    petri_vm_fn!(
318        /// Restores OpenHCL from a previously saved state.
319        pub async fn restore_openhcl(
320            &mut self
321        ) -> anyhow::Result<()>
322    );
323    petri_vm_fn!(
324        /// Updates the command line parameters of the running VM.
325        pub async fn update_command_line(
326            &mut self,
327            command_line: &str
328        ) -> anyhow::Result<()>
329    );
330
331    petri_vm_fn!(
332        /// Hot-add a PCIe device to a named port at runtime.
333        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        /// Hot-remove a PCIe device from a named port at runtime.
341        pub async fn remove_pcie_device(
342            &mut self,
343            port_name: String
344        ) -> anyhow::Result<()>
345    );
346    petri_vm_fn!(
347        /// Resets the hardware state of the VM, simulating a power cycle.
348        pub async fn reset(&mut self) -> anyhow::Result<()>
349    );
350    petri_vm_fn!(
351        /// Dumps the VM's processor and memory state to a `.vmrs` file at `path`.
352        pub async fn dump_state(&mut self, path: &Path) -> anyhow::Result<()>
353    );
354    petri_vm_fn!(
355        /// Wait for a connection from a pipette agent
356        pub async fn wait_for_agent(&mut self, set_high_vtl: bool) -> anyhow::Result<PipetteClient>
357    );
358    petri_vm_fn!(
359        /// Set the OpenHCL VTL2 settings.
360        pub async fn set_vtl2_settings(&mut self, settings: &Vtl2Settings) -> anyhow::Result<()>
361    );
362
363    petri_vm_fn!(
364        /// Pause the VM. Call [`resume`](Self::resume) to continue execution.
365        pub async fn pause(&mut self) -> anyhow::Result<()>
366    );
367    petri_vm_fn!(
368        /// Save the VM's device and processor state, returning the serialized
369        /// bytes. The VM should be paused before calling this.
370        pub async fn save_state(&mut self) -> anyhow::Result<Vec<u8>>
371    );
372    petri_vm_fn!(
373        /// Resume a paused VM.
374        pub async fn resume(&mut self) -> anyhow::Result<()>
375    );
376    petri_vm_fn!(
377        /// Perform a pulse save/restore cycle: pause the VM, save all state,
378        /// reset, restore, and resume. Useful for verifying that device state
379        /// survives a save/restore round-trip.
380        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    /// Wrap the provided future in a race with the worker process's halt
385    /// notification channel. This is useful for preventing a future from
386    /// waiting indefinitely if the VM dies for any reason. If the worker
387    /// process crashes the halt notification channel will return an error, and
388    /// if the VM halts for any other reason the future will complete with that
389    /// reason.
390    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        // Discard any firmware events from the boot that was abandoned, so
629        // that they aren't mistaken for the results of the new boot.
630        while self.resources.firmware_event_recv.try_recv().is_ok() {}
631        // Guest state is lost on reset, so CIDATA needs to be remounted.
632        self.cidata_mounted = false;
633        // Restart Petri-managed Linux-direct agents over serial. Image-owned
634        // and initrd-injected agents restart through their own boot path.
635        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        // Use TCP transport if configured (Windows no-vmbus guests).
656        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                    // During save/restore cycles, stale connections from
684                    // previous hvsock relay sessions can accumulate in the
685                    // listener backlog. These are already-closed sockets
686                    // that fail during the mesh handshake. Drain them and
687                    // retry until we get a live connection.
688                    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        // When pipette runs as PID 1 init and a CIDATA agent disk is
698        // attached, mount it so test files are available at /cidata.
699        // Skip if already mounted (e.g. reconnecting after save/restore
700        // where guest state is preserved).
701        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    /// Connect to pipette directly through the host's AF_VSOCK namespace.
730    #[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    /// Connect to pipette via TCP through consomme port forwarding.
779    ///
780    /// The guest pipette agent listens on `0.0.0.0:{port}` and consomme
781    /// forwards connections from `localhost:{port}` on the host into the
782    /// guest. We retry until the guest's network stack and pipette are up.
783    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            // Wait before retrying — guest network stack may not be up yet.
818            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        // Start pipette through serial on linux direct.
863        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
873/// Interface for inspecting OpenVMM
874pub 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
885/// Interface to the OpenVMM framebuffer
886pub 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        // Our framebuffer uses 4 bytes per pixel, approximating an
897        // BGRA image, however it only actually contains BGR data.
898        // The fourth byte is effectively noise. We can set the 'alpha'
899        // value to 0xFF to make the image opaque.
900        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}