1use super::PetriVmConfigOpenVmm;
7use super::PetriVmOpenVmm;
8use super::PetriVmResourcesOpenVmm;
9use crate::Firmware;
10use crate::PetriLogFile;
11use crate::PetriLogSource;
12use crate::worker::Worker;
13use anyhow::Context;
14use diag_client::DiagClient;
15use disk_backend_resources::FileDiskHandle;
16use framebuffer::FramebufferAccess;
17use guid::Guid;
18use hvlite_defs::config::DeviceVtl;
19use image::ColorType;
20use mesh_process::Mesh;
21use mesh_process::ProcessConfig;
22use mesh_worker::WorkerHost;
23use pal_async::DefaultDriver;
24use pal_async::pipe::PolledPipe;
25use pal_async::task::Spawn;
26use pal_async::task::Task;
27use pal_async::timer::PolledTimer;
28use petri_artifacts_common::tags::MachineArch;
29use petri_artifacts_common::tags::OsFlavor;
30use pipette_client::PipetteClient;
31use scsidisk_resources::SimpleScsiDiskHandle;
32use std::io::Write;
33use std::path::PathBuf;
34use std::sync::Arc;
35use std::time::Duration;
36use storvsp_resources::ScsiControllerHandle;
37use storvsp_resources::ScsiDeviceAndPath;
38use storvsp_resources::ScsiPath;
39use vm_resource::IntoResource;
40
41impl PetriVmConfigOpenVmm {
42 async fn run_core(self) -> anyhow::Result<PetriVmOpenVmm> {
43 let Self {
44 firmware,
45 arch,
46 mut config,
47
48 mut resources,
49
50 openvmm_log_file,
51
52 ged,
53 vtl2_settings,
54 framebuffer_access,
55 } = self;
56
57 if firmware.is_openhcl() {
58 const UH_CIDATA_SCSI_INSTANCE: Guid =
60 guid::guid!("766e96f8-2ceb-437e-afe3-a93169e48a7c");
61
62 let uh_agent_disk = resources
63 .openhcl_agent_image
64 .as_ref()
65 .unwrap()
66 .build()
67 .context("failed to build agent image")?;
68
69 config.vmbus_devices.push((
70 DeviceVtl::Vtl2,
71 ScsiControllerHandle {
72 instance_id: UH_CIDATA_SCSI_INSTANCE,
73 max_sub_channel_count: 1,
74 io_queue_depth: None,
75 devices: vec![ScsiDeviceAndPath {
76 path: ScsiPath {
77 path: 0,
78 target: 0,
79 lun: 0,
80 },
81 device: SimpleScsiDiskHandle {
82 read_only: true,
83 parameters: Default::default(),
84 disk: FileDiskHandle(uh_agent_disk.into_file()).into_resource(),
85 }
86 .into_resource(),
87 }],
88 requests: None,
89 }
90 .into_resource(),
91 ));
92 }
93
94 if let Some(mut ged) = ged {
96 ged.vtl2_settings = Some(prost::Message::encode_to_vec(&vtl2_settings.unwrap()));
97 config
98 .vmbus_devices
99 .push((DeviceVtl::Vtl2, ged.into_resource()));
100 }
101
102 let vtl2_vsock_path = config
103 .vtl2_vmbus
104 .as_ref()
105 .and_then(|s| s.vsock_path.as_ref().map(|v| v.into()));
106
107 tracing::debug!(?config, ?firmware, ?arch, "VM config");
108
109 let mesh = Mesh::new("petri_mesh".to_string())?;
110
111 let host = Self::openvmm_host(&mut resources, &mesh, openvmm_log_file)
112 .await
113 .context("failed to create host process")?;
114 let (worker, halt_notif) = Worker::launch(&host, config)
115 .await
116 .context("failed to launch vm worker")?;
117
118 let worker = Arc::new(worker);
119 let watchdog_tasks = Self::start_watchdog_tasks(
120 framebuffer_access,
121 worker.clone(),
122 vtl2_vsock_path,
123 &resources.log_source,
124 &resources.driver,
125 )?;
126
127 let mut vm = PetriVmOpenVmm::new(
128 super::runtime::PetriVmInner {
129 arch,
130 resources,
131 mesh,
132 worker,
133 watchdog_tasks,
134 quirks: firmware.quirks(),
135 },
136 halt_notif,
137 );
138
139 tracing::info!("Resuming VM");
140 vm.resume().await?;
141
142 if !firmware.is_openhcl()
147 && !matches!(firmware, Firmware::Pcat { .. })
148 && !matches!(arch, MachineArch::Aarch64)
149 {
150 tracing::info!("Testing save/restore");
151 vm.verify_save_restore().await?;
152 }
153
154 tracing::info!("VM ready");
155 Ok(vm)
156 }
157
158 pub async fn run_without_agent(self) -> anyhow::Result<PetriVmOpenVmm> {
161 self.run_core().await
162 }
163
164 pub async fn run(self) -> anyhow::Result<(PetriVmOpenVmm, PipetteClient)> {
166 let mut vm = self.run_with_lazy_pipette().await?;
167 let client = vm.wait_for_agent().await?;
168 Ok((vm, client))
169 }
170
171 pub async fn run_with_lazy_pipette(mut self) -> anyhow::Result<PetriVmOpenVmm> {
175 const CIDATA_SCSI_INSTANCE: Guid = guid::guid!("766e96f8-2ceb-437e-afe3-a93169e48a7b");
176
177 let agent_disk = self
179 .resources
180 .agent_image
181 .build()
182 .context("failed to build agent image")?;
183
184 self.config.vmbus_devices.push((
188 DeviceVtl::Vtl0,
189 ScsiControllerHandle {
190 instance_id: CIDATA_SCSI_INSTANCE,
191 max_sub_channel_count: 1,
192 io_queue_depth: None,
193 devices: vec![ScsiDeviceAndPath {
194 path: ScsiPath {
195 path: 0,
196 target: 0,
197 lun: 0,
198 },
199 device: SimpleScsiDiskHandle {
200 read_only: true,
201 parameters: Default::default(),
202 disk: FileDiskHandle(agent_disk.into_file()).into_resource(),
203 }
204 .into_resource(),
205 }],
206 requests: None,
207 }
208 .into_resource(),
209 ));
210
211 if matches!(self.firmware.os_flavor(), OsFlavor::Windows) {
212 let mut imc_hive_file = tempfile::tempfile().context("failed to create temp file")?;
215 imc_hive_file
216 .write_all(include_bytes!("../../../guest-bootstrap/imc.hiv"))
217 .context("failed to write imc hive")?;
218
219 self.config.vmbus_devices.push((
221 DeviceVtl::Vtl0,
222 vmbfs_resources::VmbfsImcDeviceHandle {
223 file: imc_hive_file,
224 }
225 .into_resource(),
226 ));
227 }
228
229 let is_linux_direct = self.firmware.is_linux_direct();
230
231 let mut vm = self.run_core().await?;
233
234 if is_linux_direct {
235 vm.launch_linux_direct_pipette().await?;
236 }
237
238 Ok(vm)
239 }
240
241 fn start_watchdog_tasks(
242 framebuffer_access: Option<FramebufferAccess>,
243 worker: Arc<Worker>,
244 vtl2_vsock_path: Option<PathBuf>,
245 log_source: &PetriLogSource,
246 driver: &DefaultDriver,
247 ) -> anyhow::Result<Vec<Task<()>>> {
248 const TIMEOUT_DURATION_MINUTES: u64 = 6;
251 const TIMER_DURATION: Duration = Duration::from_secs(TIMEOUT_DURATION_MINUTES * 60 - 10);
252
253 let mut tasks = Vec::new();
254
255 let mut timer = PolledTimer::new(driver);
256 tasks.push(driver.spawn("petri-watchdog-inspect", {
257 let log_source = log_source.clone();
258 async move {
259 timer.sleep(TIMER_DURATION).await;
260 tracing::warn!(
261 "Test has been running for almost {TIMEOUT_DURATION_MINUTES} minutes,
262 saving inspect details."
263 );
264
265 if let Err(e) =
266 log_source.write_attachment("timeout_inspect.log", worker.inspect_all().await)
267 {
268 tracing::error!(?e, "Failed to save inspect log");
269 return;
270 }
271 tracing::info!("Watchdog inspect task finished.");
272 }
273 }));
274
275 if let Some(fba) = framebuffer_access {
276 let mut view = fba.view()?;
277 let mut timer = PolledTimer::new(driver);
278 let log_source = log_source.clone();
279 tasks.push(driver.spawn("petri-watchdog-screenshot", async move {
280 let mut image = Vec::new();
281 let mut last_image = Vec::new();
282 loop {
283 timer.sleep(Duration::from_secs(2)).await;
284 tracing::trace!("Taking screenshot.");
285
286 const BYTES_PER_PIXEL: usize = 4;
292 let (width, height) = view.resolution();
293 let (widthsize, heightsize) = (width as usize, height as usize);
294 let len = widthsize * heightsize * BYTES_PER_PIXEL;
295
296 image.resize(len, 0);
297 for (i, line) in
298 (0..height).zip(image.chunks_exact_mut(widthsize * BYTES_PER_PIXEL))
299 {
300 view.read_line(i, line);
301 for pixel in line.chunks_exact_mut(BYTES_PER_PIXEL) {
302 pixel.swap(0, 2);
303 pixel[3] = 0xFF;
304 }
305 }
306
307 if image == last_image {
308 tracing::trace!("No change in framebuffer, skipping screenshot.");
309 continue;
310 }
311
312 let r = log_source
313 .create_attachment("screenshot.png")
314 .and_then(|mut f| {
315 image::write_buffer_with_format(
316 &mut f,
317 &image,
318 width.into(),
319 height.into(),
320 ColorType::Rgba8,
321 image::ImageFormat::Png,
322 )
323 .map_err(Into::into)
324 });
325
326 if let Err(e) = r {
327 tracing::error!(?e, "Failed to save screenshot");
328 } else {
329 tracing::info!("Screenshot saved.");
330 }
331 std::mem::swap(&mut image, &mut last_image);
332 }
333 }));
334 }
335
336 if let Some(vtl2_vsock_path) = vtl2_vsock_path {
337 let mut timer = PolledTimer::new(driver);
338 let driver2 = driver.clone();
339 let log_source = log_source.clone();
340 tasks.push(driver.spawn("petri-watchdog-inspect-vtl2", async move {
341 timer.sleep(TIMER_DURATION).await;
342 tracing::warn!(
343 "Test has been running for almost {TIMEOUT_DURATION_MINUTES} minutes, saving openhcl inspect details."
344 );
345
346 let diag_client =
347 DiagClient::from_hybrid_vsock(driver2, &vtl2_vsock_path);
348
349 let output = match diag_client.inspect("", None, None).await {
350 Err(e) => {
351 tracing::error!(?e, "Failed to inspect vtl2");
352 return;
353 }
354 Ok(output) => output,
355 };
356
357 let formatted_output = format!("{output:#}");
358 if let Err(e) = log_source.write_attachment("timeout_openhcl_inspect.log", formatted_output) {
359 tracing::error!(?e, "Failed to save ohcldiag-dev inspect log");
360 return;
361 }
362
363 tracing::info!("Watchdog OpenHCL inspect task finished.");
364 }));
365 }
366
367 Ok(tasks)
368 }
369
370 async fn openvmm_host(
371 resources: &mut PetriVmResourcesOpenVmm,
372 mesh: &Mesh,
373 log_file: PetriLogFile,
374 ) -> anyhow::Result<WorkerHost> {
375 let (stderr_read, stderr_write) = pal::pipe_pair()?;
378 let task = resources.driver.spawn(
379 "serial log",
380 crate::log_stream(
381 log_file,
382 PolledPipe::new(&resources.driver, stderr_read)
383 .context("failed to create polled pipe")?,
384 ),
385 );
386 resources.log_stream_tasks.push(task);
387
388 let (host, runner) = mesh_worker::worker_host();
389 mesh.launch_host(
390 ProcessConfig::new("vmm")
391 .process_name(&resources.openvmm_path)
392 .stderr(Some(stderr_write)),
393 hvlite_defs::entrypoint::MeshHostParams { runner },
394 )
395 .await?;
396 Ok(host)
397 }
398}