Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 20 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,12 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v7

- name: Setup .NET
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v6
with:
dotnet-version: |
8.0.x
9.0.x
dotnet-version: 8.0.x

- name: Restore
run: dotnet restore HyperVConsoleKit.sln
Expand All @@ -34,3 +32,20 @@ jobs:

- name: Pack
run: dotnet pack src\HyperVConsoleKit\HyperVConsoleKit.csproj --configuration Release --no-build --output artifacts\packages

- name: Publish self-contained compatibility probe
run: dotnet publish samples\CompatibilityProbe\CompatibilityProbe.csproj --configuration Release --runtime win-x64 --self-contained true --output artifacts\compatibility-probe

- name: Verify package and publish artifacts
shell: pwsh
run: .\build\Verify-Artifacts.ps1 -PackageDirectory artifacts\packages -PublishDirectory artifacts\compatibility-probe

- name: Upload compatibility validation bundle
uses: actions/upload-artifact@v7
with:
name: HyperVConsoleKit-0.6.0-preview.1-validation
path: |
artifacts/compatibility-probe/**
artifacts/packages/**
if-no-files-found: error
retention-days: 14
15 changes: 13 additions & 2 deletions .github/workflows/publish-nuget.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v7

- name: Setup .NET
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v6
with:
dotnet-version: 9.0.x

Expand All @@ -42,9 +42,20 @@ jobs:
throw "Release tag '${{ github.event.release.tag_name }}' does not match package version 'v$version'."
}

- name: Verify physical compatibility matrix
shell: pwsh
run: .\build\Verify-CompatibilityMatrix.ps1

- name: Pack
run: dotnet pack src\HyperVConsoleKit\HyperVConsoleKit.csproj --configuration Release --no-build --output artifacts\packages

- name: Publish self-contained compatibility probe
run: dotnet publish samples\CompatibilityProbe\CompatibilityProbe.csproj --configuration Release --runtime win-x64 --self-contained true --output artifacts\compatibility-probe

- name: Verify package and publish artifacts
shell: pwsh
run: .\build\Verify-Artifacts.ps1 -PackageDirectory artifacts\packages -PublishDirectory artifacts\compatibility-probe

- name: NuGet login
uses: NuGet/login@v1
id: login
Expand Down
15 changes: 15 additions & 0 deletions HyperVConsoleKit.sln
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HyperVConsoleKit.Tests", "tests\HyperVConsoleKit.Tests\HyperVConsoleKit.Tests.csproj", "{82276E05-EF92-4499-89FC-1CC227702324}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompatibilityProbe", "samples\CompatibilityProbe\CompatibilityProbe.csproj", "{9B4C74C8-9E41-4681-B8EA-9EF538057F63}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -117,6 +119,18 @@ Global
{82276E05-EF92-4499-89FC-1CC227702324}.Release|x64.Build.0 = Release|Any CPU
{82276E05-EF92-4499-89FC-1CC227702324}.Release|x86.ActiveCfg = Release|Any CPU
{82276E05-EF92-4499-89FC-1CC227702324}.Release|x86.Build.0 = Release|Any CPU
{9B4C74C8-9E41-4681-B8EA-9EF538057F63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9B4C74C8-9E41-4681-B8EA-9EF538057F63}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9B4C74C8-9E41-4681-B8EA-9EF538057F63}.Debug|x64.ActiveCfg = Debug|Any CPU
{9B4C74C8-9E41-4681-B8EA-9EF538057F63}.Debug|x64.Build.0 = Debug|Any CPU
{9B4C74C8-9E41-4681-B8EA-9EF538057F63}.Debug|x86.ActiveCfg = Debug|Any CPU
{9B4C74C8-9E41-4681-B8EA-9EF538057F63}.Debug|x86.Build.0 = Debug|Any CPU
{9B4C74C8-9E41-4681-B8EA-9EF538057F63}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9B4C74C8-9E41-4681-B8EA-9EF538057F63}.Release|Any CPU.Build.0 = Release|Any CPU
{9B4C74C8-9E41-4681-B8EA-9EF538057F63}.Release|x64.ActiveCfg = Release|Any CPU
{9B4C74C8-9E41-4681-B8EA-9EF538057F63}.Release|x64.Build.0 = Release|Any CPU
{9B4C74C8-9E41-4681-B8EA-9EF538057F63}.Release|x86.ActiveCfg = Release|Any CPU
{9B4C74C8-9E41-4681-B8EA-9EF538057F63}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -129,5 +143,6 @@ Global
{40CCF4A0-91E2-4B01-B45C-0DAC2F0DB07C} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{DEB5D1AA-CA37-4BBA-BF7C-43A72B026720} = {5D20AA90-6969-D8BD-9DCD-8634F4692FDA}
{82276E05-EF92-4499-89FC-1CC227702324} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{9B4C74C8-9E41-4681-B8EA-9EF538057F63} = {5D20AA90-6969-D8BD-9DCD-8634F4692FDA}
EndGlobalSection
EndGlobal
39 changes: 27 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,25 @@ It is not trying to replace RDP, VMConnect, ScreenConnect, TeamViewer, or a prop
The core library targets:

```xml
<TargetFrameworks>net46;netstandard2.0;net8.0-windows</TargetFrameworks>
<TargetFramework>net8.0-windows</TargetFramework>
```

That means it is compatible with modern .NET on Windows, including .NET Core/.NET services that can consume `netstandard2.0`, plus .NET 8 Windows apps and services. It is not cross-platform .NET because Hyper-V console capture and input are Windows-only WMI APIs.
The supported deployment shape is a self-contained .NET 8 Windows x64 application or service. It is not
cross-platform because Hyper-V console capture and input are Windows-only management APIs.

The NuGet package includes XML documentation for IntelliSense and generated API documentation.

## How It Works

The raw emergency console path uses the Hyper-V WMI provider:
The raw emergency console path uses `Microsoft.Management.Infrastructure` with a local CIM session connected to
the Hyper-V provider:

```text
root\virtualization\v2
```

Screen capture uses Hyper-V thumbnail capture. Input uses Hyper-V virtual keyboard and mouse WMI devices where available.
The package does not depend on `System.Management` and does not load `wminet_utils.dll`.

Single captures return raw `Rgb565` bytes sized exactly:

Expand Down Expand Up @@ -81,14 +84,17 @@ Your process must run on the Hyper-V host with administrator rights or as LocalS
```csharp
using HyperVConsoleKit;

var client = new HyperVConsoleClient();
using var client = new HyperVConsoleClient();

foreach (var vm in client.GetVirtualMachines())
{
Console.WriteLine($"{vm.Name} - {vm.Id} - {vm.State}");
}
```

`HyperVConsoleClient` owns its local CIM session and is disposable. Dispose any console sessions before disposing
their client.

## Agent Policy

If you are embedding this in an MSP agent, set policy at the client or session boundary. This keeps your service from accidentally opening an unlimited, full-resolution, full-input remote console.
Expand All @@ -110,7 +116,7 @@ var policy = new HyperVConsolePolicy
AllowPowerControl = false
};

var client = new HyperVConsoleClient(policy);
using var client = new HyperVConsoleClient(policy);
```

Power control defaults to disabled. Policy and stream options are copied when retained, so changing the original
Expand Down Expand Up @@ -177,7 +183,7 @@ using var session = client.OpenConsole(
```csharp
using HyperVConsoleKit;

var client = new HyperVConsoleClient();
using var client = new HyperVConsoleClient();
var vms = client.GetVirtualMachines();

foreach (var vm in vms)
Expand All @@ -202,7 +208,7 @@ Use this before deciding what UI to show a technician.
```csharp
using HyperVConsoleKit;

var client = new HyperVConsoleClient();
using var client = new HyperVConsoleClient();
var vm = client.GetVirtualMachines().First(v => v.Name == "Recovery VM");

var caps = client.GetConsoleCapabilities(vm.Id);
Expand Down Expand Up @@ -241,7 +247,7 @@ recommends `RawHostConsole`; Enhanced Session remains an optional local VMConnec
```csharp
using HyperVConsoleKit;

var client = new HyperVConsoleClient();
using var client = new HyperVConsoleClient();
var vm = client.GetVirtualMachines().First(v => v.IsRunning);

using var session = client.OpenConsole(vm.Id, new HyperVConsoleOpenOptions
Expand Down Expand Up @@ -321,7 +327,7 @@ The streaming API gives you frames and lets you choose the transport.
```csharp
using HyperVConsoleKit;

var client = new HyperVConsoleClient();
using var client = new HyperVConsoleClient();
var vm = client.GetVirtualMachines().First(v => v.IsRunning);

using var session = client.OpenConsole(vm.Id, new HyperVConsoleOpenOptions
Expand Down Expand Up @@ -603,7 +609,7 @@ shortcuts may still need the sample's dedicated command buttons.
## Power Controls

```csharp
var powerClient = new HyperVConsoleClient(new HyperVConsolePolicy
using var powerClient = new HyperVConsoleClient(new HyperVConsolePolicy
{
AllowPowerControl = true
});
Expand Down Expand Up @@ -758,8 +764,14 @@ dotnet test HyperVConsoleKit.sln
```

Current tests cover pixel conversion, tile diffing, policy validation/cloning, input contracts, and frame-hub
lifecycle/keyframe behavior. GitHub CI builds, tests, and packs on pushes and pull requests; publishing repeats
the tests and verifies that a release tag matches the package version.
lifecycle/keyframe behavior. They also cover older-provider shapes, optional properties, immediate and asynchronous
Hyper-V method completion, and dependency rejection. GitHub CI builds, tests, creates a self-contained x64 probe,
and verifies package/publish artifacts; publishing repeats the checks and verifies that a release tag matches the
package version.

For physical-host compatibility validation, build and run the unchanged probe described in
[`docs/COMPATIBILITY_VALIDATION.md`](docs/COMPATIBILITY_VALIDATION.md). Physical Hyper-V validation is required
for every supported Windows release; CI alone is insufficient.

## WebSocket Frame Shape Used By The Sample

Expand Down Expand Up @@ -813,6 +825,7 @@ Guest is broken -> use RawHostConsole fallback
## Requirements

- Windows.
- .NET 8, or a self-contained .NET 8 `win-x64` publish.
- Hyper-V installed.
- Administrator privileges or LocalSystem.
- Hyper-V WMI provider at `root\virtualization\v2`.
Expand All @@ -824,6 +837,8 @@ Guest is broken -> use RawHostConsole fallback
- Enhanced Session is detected and launchable, but not headlessly streamed.
- Mouse input depends on Hyper-V mouse device availability and guest behavior.
- International keyboard layouts and IME scenarios may need extra mapping work.
- The compatibility matrix and current physical-host evidence are tracked in
[`docs/COMPATIBILITY_VALIDATION.md`](docs/COMPATIBILITY_VALIDATION.md).
- The sample web console has baseline controls, but still needs product-grade identity, authorization, TLS,
distributed rate limiting, durable audit storage, and operational monitoring before internet exposure.

Expand Down
15 changes: 15 additions & 0 deletions THIRD-PARTY-NOTICES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Third-party notices

HyperVConsoleKit uses the following third-party package:

## Microsoft.Management.Infrastructure 3.0.0

Copyright (c) Microsoft Corporation.

Licensed under the MIT License. The package license and source are available from:

- https://www.nuget.org/packages/Microsoft.Management.Infrastructure/3.0.0
- https://github.com/PowerShell/MMI

Microsoft.Management.Infrastructure is used for local Windows CIM access. HyperVConsoleKit does not use
`System.Management`.
68 changes: 68 additions & 0 deletions build/Verify-Artifacts.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
param(
[Parameter(Mandatory = $true)]
[string]$PackageDirectory,

[Parameter(Mandatory = $true)]
[string]$PublishDirectory
)

$ErrorActionPreference = 'Stop'

$packageDirectoryPath = (Resolve-Path -LiteralPath $PackageDirectory).Path
$publishDirectoryPath = (Resolve-Path -LiteralPath $PublishDirectory).Path

$forbiddenPublishFiles = Get-ChildItem -LiteralPath $publishDirectoryPath -Recurse -File |
Where-Object { $_.Name -ieq 'System.Management.dll' }
if ($forbiddenPublishFiles) {
throw "System.Management.dll was found in publish output: $($forbiddenPublishFiles.FullName -join ', ')"
}

$requiredNativeAsset = Get-ChildItem -LiteralPath $publishDirectoryPath -Recurse -File |
Where-Object { $_.Name -ieq 'Microsoft.Management.Infrastructure.Native.Unmanaged.dll' } |
Select-Object -First 1
if (-not $requiredNativeAsset) {
throw 'The x64 Microsoft.Management.Infrastructure native runtime asset is missing from publish output.'
}

Add-Type -AssemblyName System.IO.Compression
$projectPath = Join-Path $PSScriptRoot '..\src\HyperVConsoleKit\HyperVConsoleKit.csproj'
[xml]$project = Get-Content -LiteralPath $projectPath
$packageVersion = $project.Project.PropertyGroup.Version | Select-Object -First 1
$expectedPackageName = "HyperVConsoleKit.$packageVersion.nupkg"
$packages = Get-ChildItem -LiteralPath $packageDirectoryPath -Filter $expectedPackageName -File
if (-not $packages) {
throw "Expected NuGet package $expectedPackageName was not found in $packageDirectoryPath."
}

foreach ($package in $packages) {
$archive = [System.IO.Compression.ZipFile]::OpenRead($package.FullName)
try {
$forbiddenEntries = $archive.Entries |
Where-Object { $_.FullName -match '(^|/)System\.Management(\.dll)?$' }
if ($forbiddenEntries) {
throw "System.Management was found in package $($package.Name)."
}

$nuspec = $archive.Entries | Where-Object { $_.FullName -like '*.nuspec' } | Select-Object -First 1
if (-not $nuspec) {
throw "Package $($package.Name) has no nuspec."
}

$reader = [System.IO.StreamReader]::new($nuspec.Open())
try {
$nuspecText = $reader.ReadToEnd()
}
finally {
$reader.Dispose()
}

if ($nuspecText -match 'System\.Management') {
throw "System.Management was found in the generated nuspec for $($package.Name)."
}
}
finally {
$archive.Dispose()
}
}

Write-Host 'Artifact verification passed: no System.Management package/assembly and the MMI x64 runtime asset is present.'
14 changes: 14 additions & 0 deletions build/Verify-CompatibilityMatrix.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
param(
[string]$MatrixPath = 'validation\compatibility-matrix.json'
)

$ErrorActionPreference = 'Stop'

$matrix = Get-Content -LiteralPath $MatrixPath -Raw | ConvertFrom-Json
$pending = @($matrix.required | Where-Object { $_.status -ne 'Pass' })
if ($pending.Count -gt 0) {
$names = $pending | ForEach-Object { "$($_.operatingSystem) $($_.architecture): $($_.status)" }
throw "Physical compatibility validation is incomplete: $($names -join '; ')"
}

Write-Host "Physical compatibility matrix passed for package $($matrix.packageVersion)."
51 changes: 51 additions & 0 deletions docs/COMPATIBILITY_VALIDATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# HyperVConsoleKit 0.6 compatibility validation

The same self-contained `win-x64` probe must pass on every supported operating system before the prerelease is
published.

## Required matrix

| Operating system | Status | Evidence |
| --- | --- | --- |
| Windows Server 2012 R2 x64 with ESU | Pending | Required physical Hyper-V host |
| Windows Server 2016 x64 | Pending | Must include the host exhibiting the original `wminet_utils.dll` failure |
| Windows Server 2019 x64 | Pending | Required physical Hyper-V host |
| Windows Server 2022 x64 | Pending | Required physical Hyper-V host |
| Windows Server 2025 x64 | Passed | Build 26100; local Hyper-V probe passed |
| Windows 10 x64 | Pending | Required physical Hyper-V host |
| Windows 11 x64 | Pending | Required physical Hyper-V host |

The Server 2025 run passed VM enumeration and lookup, capability discovery, first and sustained capture,
recommended-size capture, mouse movement and click, Enter, Shift+Tab, Ctrl+Alt+Del, repeated open/close,
client disposal, asynchronous-job VM start, force-stop with restoration to the original offline state, and
absence of both `System.Management.dll` and `wminet_utils.dll`. The unchanged self-contained probe passed once as
an elevated administrator and once as `NT AUTHORITY\SYSTEM`.

## Build the unchanged probe

```powershell
dotnet publish samples\CompatibilityProbe\CompatibilityProbe.csproj `
--configuration Release `
--runtime win-x64 `
--self-contained true `
--output artifacts\compatibility-probe
```

Copy the complete `artifacts\compatibility-probe` directory to each host. From an elevated prompt on the Hyper-V
host, with a designated running test VM:

```powershell
.\CompatibilityProbe.exe `
--vm "Test VM" `
--send-input `
--output .\compatibility-result.json
```

`--send-input` sends a mouse click, Enter, Shift+Tab, and Ctrl+Alt+Del to the selected VM. Use only a designated
test VM. When starting from an offline test VM, add `--power-cycle` to exercise the package's start and force-stop
paths; the probe returns the VM to its original offline state.

Repeat the probe under `LocalSystem`, using the same files and VM. Retain the JSON report with the OS build,
process architecture, package version, per-check results, loaded managed assemblies, and loaded native modules.

CI success is not a substitute for these physical-host results.
Loading