Skip to content

Commit a2f1ada

Browse files
authored
petri: hyperv watchdogs and screenshots (#1826)
Take screenshots and collect inspect info using a watchdog when using the Hyper-V backend as well as OpenVMM
1 parent 62d952d commit a2f1ada

File tree

12 files changed

+506
-308
lines changed

12 files changed

+506
-308
lines changed

petri/src/openhcl_diag.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,12 +92,16 @@ impl OpenHclDiagHandler {
9292
client.crash(pid).await
9393
}
9494

95-
pub async fn test_inspect(&self) -> anyhow::Result<()> {
95+
pub async fn inspect(
96+
&self,
97+
path: impl Into<String>,
98+
depth: Option<usize>,
99+
timeout: Option<std::time::Duration>,
100+
) -> anyhow::Result<inspect::Node> {
96101
self.diag_client()
97102
.await?
98-
.inspect("", None, None)
103+
.inspect(path, depth, timeout)
99104
.await
100-
.map(|_| ())
101105
}
102106

103107
pub async fn kmsg(&self) -> anyhow::Result<KmsgStream> {

petri/src/vm/hyperv/hyperv.psm1

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,3 +401,35 @@ function Restart-OpenHCL
401401

402402
$result | Trace-CimMethodExecution -CimInstance $guestManagementService -MethodName "ReloadManagementVtl" -TimeoutSeconds $TimeoutHintSeconds
403403
}
404+
405+
function Get-VmScreenshot
406+
{
407+
[CmdletBinding()]
408+
Param(
409+
[Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)]
410+
[System.Object]
411+
$Vm,
412+
413+
[Parameter(Mandatory = $true)]
414+
[string] $Path
415+
)
416+
417+
$vmms = Get-Vmms
418+
$vmcs = Get-MsvmComputerSystem $Vm
419+
420+
# Get the resolution of the screen at the moment
421+
$videoHead = $vmcs | Get-CimAssociatedInstance -ResultClassName "Msvm_VideoHead"
422+
$x = $videoHead.CurrentHorizontalResolution
423+
$y = $videoHead.CurrentVerticalResolution
424+
425+
# Get screenshot
426+
$image = $vmms | Invoke-CimMethod -MethodName "GetVirtualSystemThumbnailImage" -Arguments @{
427+
TargetSystem = $vmcs
428+
WidthPixels = $x
429+
HeightPixels = $y
430+
}
431+
432+
[IO.File]::WriteAllBytes($Path, $image.ImageData)
433+
434+
return "$x,$y"
435+
}

petri/src/vm/hyperv/mod.rs

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,18 @@ use vmsocket::VmSocket;
1010
use super::ProcessorTopology;
1111
use crate::Firmware;
1212
use crate::IsolationType;
13+
use crate::NoPetriVmInspector;
1314
use crate::OpenHclConfig;
1415
use crate::OpenHclServicingFlags;
1516
use crate::PetriVmConfig;
17+
use crate::PetriVmFramebufferAccess;
1618
use crate::PetriVmResources;
1719
use crate::PetriVmRuntime;
1820
use crate::PetriVmmBackend;
1921
use crate::SecureBootTemplate;
2022
use crate::ShutdownKind;
2123
use crate::UefiConfig;
24+
use crate::VmScreenshotMeta;
2225
use crate::disk_image::AgentImage;
2326
use crate::hyperv::powershell::HyperVSecureBootTemplate;
2427
use crate::openhcl_diag::OpenHclDiagHandler;
@@ -42,6 +45,8 @@ use pipette_client::PipetteClient;
4245
use std::fs;
4346
use std::io::Write;
4447
use std::path::Path;
48+
use std::sync::Arc;
49+
use std::sync::Weak;
4550
use std::time::Duration;
4651
use vm::HyperVVM;
4752
use vmm_core_defs::HaltReason;
@@ -51,10 +56,10 @@ pub struct HyperVPetriBackend {}
5156

5257
/// Resources needed at runtime for a Hyper-V Petri VM
5358
pub struct HyperVPetriRuntime {
54-
vm: HyperVVM,
59+
vm: Arc<HyperVVM>,
5560
log_tasks: Vec<Task<anyhow::Result<()>>>,
5661
temp_dir: tempfile::TempDir,
57-
openhcl_diag_handler: Option<OpenHclDiagHandler>,
62+
is_openhcl: bool,
5863
driver: DefaultDriver,
5964
}
6065

@@ -299,7 +304,7 @@ impl PetriVmmBackend for HyperVPetriBackend {
299304
}
300305
}
301306

302-
let openhcl_diag_handler = if let Some((
307+
if let Some((
303308
src_igvm_file,
304309
OpenHclConfig {
305310
vtl2_nvme_boot: _, // TODO, see #1649.
@@ -365,13 +370,7 @@ impl PetriVmmBackend for HyperVPetriBackend {
365370
}
366371
}
367372
}));
368-
369-
Some(OpenHclDiagHandler::new(
370-
diag_client::DiagClient::from_hyperv_id(driver.clone(), *vm.vmid()),
371-
))
372-
} else {
373-
None
374-
};
373+
}
375374

376375
let serial_pipe_path = vm.set_vm_com_port(1)?;
377376
let serial_log_file = log_source.log_file("guest")?;
@@ -390,22 +389,25 @@ impl PetriVmmBackend for HyperVPetriBackend {
390389
vm.start()?;
391390

392391
Ok(HyperVPetriRuntime {
393-
vm,
392+
vm: Arc::new(vm),
394393
log_tasks,
395394
temp_dir,
396-
openhcl_diag_handler,
395+
is_openhcl: openhcl_config.is_some(),
397396
driver: driver.clone(),
398397
})
399398
}
400399
}
401400

402401
#[async_trait]
403402
impl PetriVmRuntime for HyperVPetriRuntime {
404-
async fn teardown(self) -> anyhow::Result<()> {
405-
for t in self.log_tasks {
406-
_ = t.cancel();
407-
}
408-
self.vm.remove()
403+
type VmInspector = NoPetriVmInspector;
404+
type VmFramebufferAccess = HyperVFramebufferAccess;
405+
406+
async fn teardown(mut self) -> anyhow::Result<()> {
407+
futures::future::join_all(self.log_tasks.into_iter().map(|t| t.cancel())).await;
408+
Arc::into_inner(self.vm)
409+
.context("all references to the Hyper-V VM object have not been closed")?
410+
.remove()
409411
}
410412

411413
async fn wait_for_halt(&mut self) -> anyhow::Result<HaltReason> {
@@ -450,8 +452,13 @@ impl PetriVmRuntime for HyperVPetriRuntime {
450452
.context("failed to connect to pipette")
451453
}
452454

453-
fn openhcl_diag(&self) -> Option<&OpenHclDiagHandler> {
454-
self.openhcl_diag_handler.as_ref()
455+
fn openhcl_diag(&self) -> Option<OpenHclDiagHandler> {
456+
self.is_openhcl.then(|| {
457+
OpenHclDiagHandler::new(diag_client::DiagClient::from_hyperv_id(
458+
self.driver.clone(),
459+
*self.vm.vmid(),
460+
))
461+
})
455462
}
456463

457464
async fn wait_for_successful_boot_event(&mut self) -> anyhow::Result<()> {
@@ -483,6 +490,26 @@ impl PetriVmRuntime for HyperVPetriRuntime {
483490
// TODO: Updating the file causes failure ... self.vm.set_openhcl_firmware(new_openhcl.get(), false)?;
484491
self.vm.restart_openhcl(flags).await
485492
}
493+
494+
fn take_framebuffer_access(&mut self) -> Option<HyperVFramebufferAccess> {
495+
Some(HyperVFramebufferAccess {
496+
vm: Arc::downgrade(&self.vm),
497+
})
498+
}
499+
}
500+
501+
/// Interface to the Hyper-V framebuffer
502+
pub struct HyperVFramebufferAccess {
503+
vm: Weak<HyperVVM>,
504+
}
505+
506+
impl PetriVmFramebufferAccess for HyperVFramebufferAccess {
507+
fn screenshot(&mut self, image: &mut Vec<u8>) -> anyhow::Result<VmScreenshotMeta> {
508+
self.vm
509+
.upgrade()
510+
.context("VM no longer exists")?
511+
.screenshot(image)
512+
}
486513
}
487514

488515
fn acl_read_for_vm(path: &Path, id: Option<guid::Guid>) -> anyhow::Result<()> {

petri/src/vm/hyperv/powershell.rs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -538,7 +538,7 @@ pub fn run_set_vm_com_port(vmid: &Guid, port: u8, path: &Path) -> anyhow::Result
538538
}
539539

540540
/// Run Set-VMBusRelay commandlet
541-
pub fn set_vmbus_redirect(vmid: &Guid, ps_mod: &Path, enable: bool) -> anyhow::Result<()> {
541+
pub fn run_set_vmbus_redirect(vmid: &Guid, ps_mod: &Path, enable: bool) -> anyhow::Result<()> {
542542
run_cmd(
543543
PowerShellBuilder::new()
544544
.cmdlet("Import-Module")
@@ -831,3 +831,30 @@ pub fn run_remove_vm_scsi_controller(vmid: &Guid, controller_number: u32) -> any
831831
.map(|_| ())
832832
.context("remove_vm_scsi_controller")
833833
}
834+
835+
/// Run Get-VmScreenshot commandlet
836+
pub fn run_get_vm_screenshot(
837+
vmid: &Guid,
838+
ps_mod: &Path,
839+
path: &Path,
840+
) -> anyhow::Result<(u16, u16)> {
841+
let output = run_cmd(
842+
PowerShellBuilder::new()
843+
.cmdlet("Import-Module")
844+
.positional(ps_mod)
845+
.next()
846+
.cmdlet("Get-VM")
847+
.arg("Id", vmid)
848+
.pipeline()
849+
.cmdlet("Get-VmScreenshot")
850+
.arg("Path", path)
851+
.finish()
852+
.build(),
853+
)
854+
.context("get_vm_screenshot")?;
855+
let (x, y) = output.split_once(',').context("invalid dimensions")?;
856+
Ok((
857+
x.parse().context("invalid x dimension")?,
858+
y.parse().context("invalid y dimension")?,
859+
))
860+
}

petri/src/vm/hyperv/vm.rs

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use super::hvc::VmState;
88
use super::powershell;
99
use crate::OpenHclServicingFlags;
1010
use crate::PetriLogFile;
11+
use crate::VmScreenshotMeta;
1112
use anyhow::Context;
1213
use get_resources::ged::FirmwareEvent;
1314
use guid::Guid;
@@ -30,7 +31,7 @@ pub struct HyperVVM {
3031
name: String,
3132
vmid: Guid,
3233
destroyed: bool,
33-
_temp_dir: TempDir,
34+
temp_dir: TempDir,
3435
ps_mod: PathBuf,
3536
create_time: Timestamp,
3637
log_file: PetriLogFile,
@@ -98,7 +99,7 @@ impl HyperVVM {
9899
name,
99100
vmid,
100101
destroyed: false,
101-
_temp_dir: temp_dir,
102+
temp_dir,
102103
ps_mod,
103104
create_time,
104105
log_file,
@@ -167,7 +168,7 @@ impl HyperVVM {
167168

168169
/// Waits for an event emitted by the firmware about its boot status, and
169170
/// verifies that it is the expected success value.
170-
pub async fn wait_for_successful_boot_event(&mut self) -> anyhow::Result<()> {
171+
pub async fn wait_for_successful_boot_event(&self) -> anyhow::Result<()> {
171172
if let Some(expected_boot_event) = self.expected_boot_event {
172173
self.wait_for(Self::boot_event, Some(expected_boot_event), 240.seconds())
173174
.await
@@ -181,7 +182,7 @@ impl HyperVVM {
181182

182183
/// Waits for an event emitted by the firmware about its boot status, and
183184
/// returns that status.
184-
pub async fn wait_for_boot_event(&mut self) -> anyhow::Result<FirmwareEvent> {
185+
pub async fn wait_for_boot_event(&self) -> anyhow::Result<FirmwareEvent> {
185186
self.wait_for_some(Self::boot_event, 240.seconds()).await
186187
}
187188

@@ -436,13 +437,52 @@ impl HyperVVM {
436437

437438
/// Enable VMBusRelay
438439
pub fn set_vmbus_redirect(&self, enable: bool) -> anyhow::Result<()> {
439-
powershell::set_vmbus_redirect(&self.vmid, &self.ps_mod, enable)
440+
powershell::run_set_vmbus_redirect(&self.vmid, &self.ps_mod, enable)
440441
}
441442

442443
/// Perform an OpenHCL servicing operation.
443444
pub async fn restart_openhcl(&self, flags: OpenHclServicingFlags) -> anyhow::Result<()> {
444445
powershell::run_restart_openhcl(&self.vmid, &self.ps_mod, flags)
445446
}
447+
448+
/// Take a screenshot of the VM
449+
pub fn screenshot(&self, image: &mut Vec<u8>) -> anyhow::Result<VmScreenshotMeta> {
450+
const IN_BYTES_PER_PIXEL: usize = 2;
451+
const OUT_BYTES_PER_PIXEL: usize = 3;
452+
let temp_bin_path = self.temp_dir.path().join("screenshot.bin");
453+
let (width, height) =
454+
powershell::run_get_vm_screenshot(&self.vmid, &self.ps_mod, &temp_bin_path)?;
455+
let (widthsize, heightsize) = (width as usize, height as usize);
456+
let in_len = widthsize * heightsize * IN_BYTES_PER_PIXEL;
457+
let out_len = widthsize * heightsize * OUT_BYTES_PER_PIXEL;
458+
let mut image_rgb565 = fs_err::read(temp_bin_path)?;
459+
image_rgb565.truncate(in_len);
460+
if image_rgb565.len() != in_len {
461+
anyhow::bail!("did not get enough bytes for screenshot");
462+
}
463+
464+
image.resize(out_len, 0);
465+
for (out_pixel, in_pixel) in image
466+
.chunks_exact_mut(OUT_BYTES_PER_PIXEL)
467+
.zip(image_rgb565.chunks_exact(IN_BYTES_PER_PIXEL))
468+
{
469+
// convert from rgb565 ( gggbbbbb rrrrrggg )
470+
// to rgb888 ( rrrrrrrr gggggggg bbbbbbbb )
471+
472+
// red
473+
out_pixel[0] = in_pixel[1] & 0b11111000;
474+
// green
475+
out_pixel[1] = ((in_pixel[1] & 0b00000111) << 5) + ((in_pixel[0] & 0b11100000) >> 3);
476+
// blue
477+
out_pixel[2] = in_pixel[0] << 3;
478+
}
479+
480+
Ok(VmScreenshotMeta {
481+
color: image::ExtendedColorType::Rgb8,
482+
width,
483+
height,
484+
})
485+
}
446486
}
447487

448488
impl Drop for HyperVVM {

0 commit comments

Comments
 (0)