HyperVConsoleKit is a C# library for getting emergency console access to local Hyper-V virtual machines from the host.
It is for the awkward moments: RDP is broken, the guest agent is dead, the VM is sitting at a boot menu, Windows Recovery is waiting for input, a Linux appliance has no remote access, or a technician needs one last way in without relying on the guest OS being healthy.
It is not trying to replace RDP, VMConnect, ScreenConnect, TeamViewer, or a proper in-guest support agent. Think of it as a break-glass console toolkit that an MSP, backup vendor, DR product, or remote control agent can embed as a fallback.
- List local Hyper-V VMs.
- Read VM state and basic console capabilities.
- Capture the current VM console image from the host.
- Stream console frames to your own transport.
- Reduce stream bandwidth with lower resolution, lower color depth, bitrate caps, and changed tiles.
- Send keyboard input, special keys, chords, Ctrl+Alt+Del, and paste-as-keystrokes.
- Try mouse movement and clicks where Hyper-V exposes a synthetic mouse.
- Start, stop, and reset VMs.
- Detect when Enhanced Session is likely available and launch VMConnect.
- Apply policy limits for FPS, resolution, bandwidth, color depth, power control, and input.
- Emit audit events for capture, input, session, and VM power actions.
- Run structured diagnostics that can be uploaded by an agent.
- Fan out one capture loop to multiple viewers while slow viewers drop stale frames.
- Manage one shared console stream per VM with
HyperVConsoleSessionManager.
The core library targets:
<TargetFramework>net8.0-windows</TargetFramework>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.
The raw emergency console path uses Microsoft.Management.Infrastructure with a local CIM session connected to
the Hyper-V provider:
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:
Width * Height * 2
Streaming can keep Rgb565 or reduce to:
Rgb332Gray8Gray4Mono1
The library does not force JPEG, PNG, WebSockets, SignalR, gRPC, or any particular remote-control protocol. You decide what to do with the frames.
From source:
git clone https://github.com/ml6719/HyperVConsole.git
cd HyperVConsole
dotnet build HyperVConsoleKit.slnFrom NuGet once published:
dotnet add package HyperVConsoleKitYour process must run on the Hyper-V host with administrator rights or as LocalSystem.
using HyperVConsoleKit;
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.
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.
using HyperVConsoleKit;
var policy = new HyperVConsolePolicy
{
MaxWidth = 1024,
MaxHeight = 768,
MaxFramesPerSecond = 5,
MaxBytesPerSecond = 500_000,
MaxColorDepth = ConsoleFramePixelFormat.Rgb332,
MaxConcurrentViewers = 3,
IdleTimeout = TimeSpan.FromSeconds(30),
AllowKeyboardInput = true,
AllowMouseInput = true,
AllowPowerControl = false
};
using var client = new HyperVConsoleClient(policy);Power control defaults to disabled. Policy and stream options are copied when retained, so changing the original objects later cannot silently alter a running session.
You can also override policy for a single session:
using var session = client.OpenConsole(vm.Id, new HyperVConsoleOpenOptions
{
Mode = HyperVConsoleMode.RawHostConsole,
Policy = policy
});Hook audit events if you need an activity trail for technician sessions.
client.Activity += (_, e) =>
{
Console.WriteLine($"{e.TimestampUtc:o} {e.UserName} {e.VirtualMachineId} {e.Action} {e.Success} {e.Message}");
};
using var session = client.OpenConsole(vm.Id);
session.Activity += (_, e) =>
{
Console.WriteLine($"{e.Action}: {e.Message}");
};
session.SendCtrlAltDel();Use diagnostics when an agent needs to explain why the console is not usable.
var report = client.RunDiagnostics(vm.Id);
Console.WriteLine(report.OverallStatus);
foreach (var item in report.Items)
{
Console.WriteLine($"{item.Status}: {item.Name} - {item.Message}");
}This is deliberately JSON-friendly, so you can return it from an API endpoint or upload it with your agent telemetry.
Open a console session:
var vm = client.GetVirtualMachines().First(v => v.IsRunning);
using var session = client.OpenConsole(
vm.Id,
new HyperVConsoleOpenOptions { Mode = HyperVConsoleMode.RawHostConsole });using HyperVConsoleKit;
using var client = new HyperVConsoleClient();
var vms = client.GetVirtualMachines();
foreach (var vm in vms)
{
Console.WriteLine($"Name: {vm.Name}");
Console.WriteLine($"Id: {vm.Id}");
Console.WriteLine($"State: {vm.State}");
Console.WriteLine($"Running: {vm.IsRunning}");
Console.WriteLine($"Capture: {vm.SupportsConsoleCapture} / now: {vm.CanCaptureNow}");
Console.WriteLine($"Keyboard: {vm.SupportsKeyboardInput} / now: {vm.CanSendKeyboardInputNow}");
Console.WriteLine($"Mouse: {vm.SupportsMouseInput} / now: {vm.CanSendMouseInputNow}");
Console.WriteLine($"Enhanced: {vm.SupportsEnhancedSession}");
Console.WriteLine($"Suggested: {vm.RecommendedConsoleMode}");
Console.WriteLine();
}Use this before deciding what UI to show a technician.
using HyperVConsoleKit;
using var client = new HyperVConsoleClient();
var vm = client.GetVirtualMachines().First(v => v.Name == "Recovery VM");
var caps = client.GetConsoleCapabilities(vm.Id);
Console.WriteLine($"Raw capture: {caps.SupportsRawCapture}");
Console.WriteLine($"Can capture now: {caps.CanCaptureNow}");
Console.WriteLine($"Keyboard input: {caps.SupportsKeyboardInput}");
Console.WriteLine($"Can type now: {caps.CanSendKeyboardInputNow}");
Console.WriteLine($"Mouse input: {caps.SupportsMouseInput}");
Console.WriteLine($"Can mouse now: {caps.CanSendMouseInputNow}");
Console.WriteLine($"Enhanced session: {caps.SupportsEnhancedSession}");
Console.WriteLine($"Recommended mode: {caps.RecommendedMode}");
Console.WriteLine($"Recommended size: {caps.RecommendedFrameWidth}x{caps.RecommendedFrameHeight}");
foreach (var limitation in caps.Limitations)
{
Console.WriteLine($"Note: {limitation}");
}For headless code, keep using the raw mode:
using var session = client.OpenConsole(vm.Id, new HyperVConsoleOpenOptions
{
Mode = HyperVConsoleMode.RawHostConsole
});SupportsEnhancedSession means the host policy and VM transport configuration permit an Enhanced Session
candidate. Hyper-V does not tell this API whether guest RDP services are actually healthy. The library therefore
recommends RawHostConsole; Enhanced Session remains an optional local VMConnect launch.
using HyperVConsoleKit;
using var client = new HyperVConsoleClient();
var vm = client.GetVirtualMachines().First(v => v.IsRunning);
using var session = client.OpenConsole(vm.Id, new HyperVConsoleOpenOptions
{
Mode = HyperVConsoleMode.RawHostConsole
});
var frame = session.CaptureFrame(new ConsoleFrameOptions());
File.WriteAllBytes("console.rgb565", frame.RawBytes);
Console.WriteLine($"{frame.Width}x{frame.Height}");
Console.WriteLine(frame.PixelFormat);
Console.WriteLine($"{frame.RawBytes.Length} bytes");When Width and Height are left as 0, HyperVConsoleKit asks Hyper-V for the active Msvm_VideoHead resolution and falls back to 1024x768 if Hyper-V does not report one. You can still request an explicit size:
var frame = session.CaptureFrame(new ConsoleFrameOptions
{
Width = 1024,
Height = 768
});The file is raw RGB565. If you ask for 1024x768, it will be:
1024 * 768 * 2 = 1,572,864 bytes
The library deliberately does not depend on an image encoder. If your app wants PNG/JPEG, convert at the edge.
Example using System.Drawing on Windows:
using System.Drawing;
using System.Drawing.Imaging;
using HyperVConsoleKit;
static Bitmap Rgb565ToBitmap(ConsoleFrame frame)
{
var bitmap = new Bitmap(frame.Width, frame.Height, PixelFormat.Format24bppRgb);
var index = 0;
for (var y = 0; y < frame.Height; y++)
{
for (var x = 0; x < frame.Width; x++)
{
var value = frame.RawBytes[index] | (frame.RawBytes[index + 1] << 8);
index += 2;
var red = ((value >> 11) & 0x1F) * 255 / 31;
var green = ((value >> 5) & 0x3F) * 255 / 63;
var blue = (value & 0x1F) * 255 / 31;
bitmap.SetPixel(x, y, Color.FromArgb(red, green, blue));
}
}
return bitmap;
}
using var bmp = Rgb565ToBitmap(frame);
bmp.Save("console.png", ImageFormat.Png);For production, use a faster pixel buffer approach rather than SetPixel.
The streaming API gives you frames and lets you choose the transport.
using HyperVConsoleKit;
using var client = new HyperVConsoleClient();
var vm = client.GetVirtualMachines().First(v => v.IsRunning);
using var session = client.OpenConsole(vm.Id, new HyperVConsoleOpenOptions
{
Mode = HyperVConsoleMode.RawHostConsole
});
using var cts = new CancellationTokenSource();
await session.StreamFramesAsync(
new ConsoleFrameStreamOptions
{
PixelFormat = ConsoleFramePixelFormat.Rgb332,
ActiveFramesPerSecond = 5,
IdleFramesPerSecond = 1,
UseAdaptiveFrameRate = true,
SendChangedTilesOnly = true,
MaxBytesPerSecond = 500_000
},
async (frame, cancellationToken) =>
{
if (frame.UpdateKind == ConsoleFrameUpdateKind.FullFrame)
{
Console.WriteLine($"Full frame: {frame.PayloadBytes} bytes");
await SendFullFrameToYourClient(frame, cancellationToken);
}
else
{
Console.WriteLine($"Changed tiles: {frame.Tiles.Count}, {frame.PayloadBytes} bytes");
await SendChangedTilesToYourClient(frame, cancellationToken);
}
},
cts.Token);Leaving stream Width and Height as 0 uses the VM's recommended video-head resolution. Set them explicitly when you want a fixed transport size.
Your callback is awaited. If DropFramesWhenBehind is true, HyperVConsoleKit uses latest-frame streaming internally: capture can keep moving while your sender is busy, and the sender receives the newest available frame instead of working through stale frames.
Placeholder methods from the example:
static Task SendFullFrameToYourClient(ConsoleFrame frame, CancellationToken cancellationToken)
{
// Send frame.RawBytes plus metadata: width, height, pixel format, sequence number.
return Task.CompletedTask;
}
static Task SendChangedTilesToYourClient(ConsoleFrame frame, CancellationToken cancellationToken)
{
// Send frame.Tiles. Each tile has X, Y, Width, Height, and RawBytes.
return Task.CompletedTask;
}For several viewers, use a frame hub so the VM is captured once and each viewer has its own latest-frame queue:
using var session = client.OpenConsole(vm.Id);
var options = ConsoleFrameStreamOptions.CreatePreset(ConsoleStreamPreset.Balanced);
options.DropFramesWhenBehind = true;
var hub = new HyperVConsoleFrameHub(session, options, maxViewers: 3);
using var hubCts = new CancellationTokenSource();
var hubTask = hub.RunAsync(hubCts.Token);
var viewerTask = hub.AddViewerAsync(async (frame, cancellationToken) =>
{
await SendFullFrameToYourClient(frame, cancellationToken);
}, requestAborted);For an agent or web gateway, prefer the session manager. It keeps one stream hub per VM, reference-counts viewers, and shuts the stream down when the last viewer disconnects:
var manager = new HyperVConsoleSessionManager(client, policy);
await manager.AddViewerAsync(
vm.Id,
ConsoleFrameStreamOptions.CreatePreset(ConsoleStreamPreset.Balanced),
async (frame, cancellationToken) =>
{
await SendFullFrameToYourClient(frame, cancellationToken);
},
requestAborted);Set policy.IdleTimeout to keep an empty shared stream warm briefly for reconnects. Viewers sharing one VM must
request equivalent stream options; a mismatched request is rejected instead of silently inheriting the first
viewer's settings. Late viewers receive a cached full keyframe before tile deltas.
If you do not want to tune every knob yourself:
var options = ConsoleFrameStreamOptions.CreatePreset(ConsoleStreamPreset.LowBandwidth);Available presets:
LatencyBalancedLowBandwidthQuality
You can still override anything:
var options = ConsoleFrameStreamOptions.CreatePreset(ConsoleStreamPreset.LowBandwidth);
options.Width = 800;
options.Height = 600;
options.MaxBytesPerSecond = 250_000;For a technician on a poor connection, use something like:
var options = new ConsoleFrameStreamOptions
{
Width = 640,
Height = 480,
PixelFormat = ConsoleFramePixelFormat.Gray8,
ActiveFramesPerSecond = 3,
IdleFramesPerSecond = 0.5,
UseAdaptiveFrameRate = true,
SendChangedTilesOnly = true,
TileWidth = 64,
TileHeight = 64,
MaxBytesPerSecond = 180_000,
AllowKeyFrameBandwidthBurst = true
};MaxBytesPerSecond is strict for ordinary updates. By default, the first keyframe in a one-second window may burst
above it so a viewer can establish a usable baseline. Set AllowKeyFrameBandwidthBurst = false for a hard cap;
validation then requires the cap to be large enough for one full frame at the selected size and color depth.
For better quality on a LAN:
var options = new ConsoleFrameStreamOptions
{
Width = 1024,
Height = 768,
PixelFormat = ConsoleFramePixelFormat.Rgb565,
ActiveFramesPerSecond = 5,
IdleFramesPerSecond = 1,
UseAdaptiveFrameRate = true,
SendChangedTilesOnly = false
};using var session = client.OpenConsole(vm.Id);
session.SendText("Administrator");
session.SendKey(ConsoleKeyCode.Enter);Common recovery keys:
session.SendKey(ConsoleKeyCode.Tab);
session.SendKey(ConsoleKeyCode.Escape);
session.SendKey(ConsoleKeyCode.F8);
session.SendKey(ConsoleKeyCode.F12);
session.SendKey(ConsoleKeyCode.Up);
session.SendKey(ConsoleKeyCode.Down);
session.SendKey(ConsoleKeyCode.Left);
session.SendKey(ConsoleKeyCode.Right);Ctrl+Alt+Del:
session.SendCtrlAltDel();Key down/up:
session.SendKeyDown(ConsoleKeyCode.Shift);
session.SendKey(ConsoleKeyCode.F8);
session.SendKeyUp(ConsoleKeyCode.Shift);session.SendChord(ConsoleKeyCode.Alt, ConsoleKeyCode.Tab);
session.SendChord(ConsoleKeyCode.Control, ConsoleKeyCode.Shift, ConsoleKeyCode.Escape);There are helpers for the common support actions:
session.SendAltTab();
session.SendWinR();
session.SendCtrlShiftEsc();This is useful for support tools that expose a toolbar of recovery actions.
For long strings, passwords, commands, or recovery scripts, use throttled paste-as-input:
session.PasteTextAsKeystrokes(
"ipconfig /all\r\n",
new ConsolePasteOptions
{
DelayBetweenCharactersMs = 10,
DelayAfterNewLineMs = 25,
ConvertLineEndingsToEnter = true
});Async version:
await session.PasteTextAsKeystrokesAsync(
"sudo systemctl status ssh\n",
new ConsolePasteOptions(),
cancellationToken);This sends keystrokes through Hyper-V. It is not a guest clipboard.
Mouse support is best-effort. Some VMs expose a synthetic mouse, some do not, and guest behavior varies. The coordinates accepted by the mouse APIs are pixels in the VM's current guest display, not a normalized range.
var caps = client.GetConsoleCapabilities(vm.Id);
if (caps.CanSendMouseInputNow)
{
using var session = client.OpenConsole(vm.Id);
var frame = session.CaptureFrame(new ConsoleFrameOptions());
// Prefer the detected guest dimensions because a captured frame may be scaled.
var guestWidth = caps.RecommendedFrameWidth ?? frame.Width;
var guestHeight = caps.RecommendedFrameHeight ?? frame.Height;
var x = guestWidth / 2;
var y = guestHeight / 2;
var moved = session.TrySendMouseMove(x, y);
var clicked = session.TrySendMouseClick(x, y, MouseButton.Left);
Console.WriteLine($"Moved: {moved}, Clicked: {clicked}");
}In a browser viewer, map CSS canvas coordinates to native guest-display pixels:
static int ToGuestPixel(double pointerPosition, double displayedSize, int framebufferSize)
{
var pixel = (int)Math.Floor((pointerPosition / displayedSize) * framebufferSize);
return Math.Clamp(pixel, 0, framebufferSize - 1);
}If mouse methods return false, keep the technician UI keyboard-first.
The ASP.NET Core sample applies this mapping automatically. Click the console once to focus it, then normal key-down and key-up events are forwarded to the Hyper-V virtual keyboard. Browser- or operating-system-reserved shortcuts may still need the sample's dedicated command buttons.
using var powerClient = new HyperVConsoleClient(new HyperVConsolePolicy
{
AllowPowerControl = true
});
powerClient.StartVirtualMachine(vm.Id);
powerClient.StopVirtualMachine(vm.Id, force: false);
powerClient.ResetVirtualMachine(vm.Id);Async:
await client.StartVirtualMachineAsync(vm.Id, cancellationToken);
await client.StopVirtualMachineAsync(vm.Id, force: false, cancellationToken);
await client.ResetVirtualMachineAsync(vm.Id, cancellationToken);Treat these like serious remote-control actions. Your product should require explicit confirmation and log who did what.
Hyper-V Enhanced Session is VMConnect using RDP over VMBus. When it is available, it is usually a better interactive experience than raw framebuffer polling.
HyperVConsoleKit can detect its host/configuration prerequisites and launch it:
var caps = client.GetConsoleCapabilities(vm.Id);
if (caps.SupportsEnhancedSession) // Candidate only; guest readiness is not proven.
{
var launch = client.GetEnhancedSessionLaunchInfo(vm.Id);
Console.WriteLine(launch.VmConnectPath);
Console.WriteLine(launch.Arguments);
client.TryLaunchEnhancedSession(vm.Id);
}Important caveat: Enhanced Session is not exposed by this library as a headless frame/input stream. For automated/web gateway scenarios, use RawHostConsole. For local interactive technician use, launch VMConnect when Enhanced Session is available.
Most operations have async equivalents:
var vms = await client.GetVirtualMachinesAsync(cancellationToken);
var vm = await client.GetVirtualMachineAsync(id, cancellationToken);
using var session = client.OpenConsole(vm.Id);
var frame = await session.CaptureFrameAsync(
new ConsoleFrameOptions(),
cancellationToken);
await session.SendTextAsync("Administrator", cancellationToken);
await session.SendKeyAsync(ConsoleKeyCode.Enter, cancellationToken);Internally, WMI calls are serialized per VM to avoid concurrent access to the same WMI devices. Independent VMs
can capture and accept input concurrently from one HyperVConsoleClient.
This repository includes several small samples.
List VMs:
dotnet run --project samples\ConsoleListVms\ConsoleListVms.csprojCapture a raw frame:
dotnet run --project samples\ConsoleScreenshot\ConsoleScreenshot.csproj -- "My VM" console.rgb565Send keyboard input:
dotnet run --project samples\ConsoleKeyboardInput\ConsoleKeyboardInput.csproj -- "My VM" tab enter
dotnet run --project samples\ConsoleKeyboardInput\ConsoleKeyboardInput.csproj -- "My VM" ctrlaltdel
dotnet run --project samples\ConsoleKeyboardInput\ConsoleKeyboardInput.csproj -- "My VM" paste:"ipconfig /all"Run diagnostics:
dotnet run --project samples\ConsoleDiagnostics\ConsoleDiagnostics.csproj -- "My VM"Diagnostics does not send keyboard input by default. Add --send-input to include a simple Enter key smoke test:
dotnet run --project samples\ConsoleDiagnostics\ConsoleDiagnostics.csproj -- "My VM" --send-inputRun the browser sample:
dotnet run --project samples\AspNetCoreWebConsole\AspNetCoreWebConsole.csproj --urls http://localhost:5088Then open:
http://localhost:5088
The web sample is intentionally just a sample. It includes a defensive baseline, but it is not a substitute for your product's identity, authorization, relay, consent, and audit systems.
It does include practical gateway hooks in appsettings.json:
{
"HyperVConsole": {
"ApiKey": "",
"AllowedVmIds": [],
"AllowedOrigins": [],
"MaxInputCharacters": 4096,
"RequestsPerMinute": 600,
"InputLeaseMinutes": 5,
"MaxWidth": 1024,
"MaxHeight": 768,
"MaxFramesPerSecond": 5,
"MaxBytesPerSecond": 500000,
"MaxColorDepth": "Rgb332",
"MaxConcurrentViewers": 3,
"AllowKeyboardInput": true,
"AllowMouseInput": true,
"AllowPowerControl": false
}
}Outside the Development environment, ApiKey is required. Open /?apiKey=... once to exchange it for an
HTTP-only, same-site cookie; the browser redirects to a clean URL and does not keep the key in local storage or
WebSocket query strings. API clients can use X-Console-Api-Key. AllowedVmIds restricts VM access and
AllowedOrigins can explicitly allow WebSocket origins; when it is empty, only the page's own origin is accepted.
The sample also bounds text bodies and request rate. Browser key-down events use short-lived server leases so held modifiers are released on blur, page unload, stream disconnect, or lease expiry.
The test project covers logic that does not require Hyper-V:
dotnet test HyperVConsoleKit.slnCurrent tests cover pixel conversion, tile diffing, policy validation/cloning, input contracts, and frame-hub 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. Physical Hyper-V validation is required
for every supported Windows release; CI alone is insufficient.
The ASP.NET Core sample sends a small binary envelope:
4 bytes: little-endian JSON header length
N bytes: UTF-8 JSON header
N bytes: raw full-frame or tile payload
The JSON header includes:
- sequence number
- width and height
- pixel format
- update kind
- keyframe flag
- payload byte count
- tile metadata
You can copy this idea, replace it with your own protocol, or send ConsoleFrame through SignalR, gRPC, WebRTC data channels, a relay, or your existing agent transport.
The library does not expose remote access by itself. That is deliberate.
If you build a service on top of it, add:
- strong authentication
- role-based access
- per-VM authorization
- technician identity
- audit logging
- customer approval where required
- encrypted transport
- session timeout
- rate limiting
- explicit confirmation for power actions
- read-only mode
- break-glass mode for reset, stop, and Ctrl+Alt+Del
For many products, the sensible flow is:
Normal agent/RDP works -> use that
Enhanced Session works -> launch VMConnect for local technician
Guest is broken -> use RawHostConsole fallback
- Windows.
- .NET 8, or a self-contained .NET 8
win-x64publish. - Hyper-V installed.
- Administrator privileges or LocalSystem.
- Hyper-V WMI provider at
root\virtualization\v2. - A running VM for meaningful capture/input tests.
- Raw console capture is polling-based. It is good for emergency work, not high-FPS remote desktop.
- 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. - 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.
MIT.