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:
<TargetFrameworks>net46;netstandard2.0;net8.0-windows</TargetFrameworks>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 NuGet package includes XML documentation for IntelliSense and generated API documentation.
The raw emergency console path uses the Hyper-V WMI provider:
root\virtualization\v2
Screen capture uses Hyper-V thumbnail capture. Input uses Hyper-V virtual keyboard and mouse WMI devices where available.
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;
var client = new HyperVConsoleClient();
foreach (var vm in client.GetVirtualMachines())
{
Console.WriteLine($"{vm.Name} - {vm.Id} - {vm.State}");
}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,
AllowKeyboardInput = true,
AllowMouseInput = true,
AllowPowerControl = false
};
var client = new HyperVConsoleClient(policy);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;
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;
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}");
foreach (var limitation in caps.Limitations)
{
Console.WriteLine($"Note: {limitation}");
}Typical logic:
var mode = caps.RecommendedMode == HyperVConsoleMode.EnhancedSession
? HyperVConsoleMode.EnhancedSession
: HyperVConsoleMode.RawHostConsole;Enhanced Session is better for normal interactive control when the guest is healthy enough. Raw host console is the fallback for boot, BIOS, recovery, and broken guest scenarios.
using HyperVConsoleKit;
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
{
Width = 1024,
Height = 768
});
File.WriteAllBytes("console.rgb565", frame.RawBytes);
Console.WriteLine($"{frame.Width}x{frame.Height}");
Console.WriteLine(frame.PixelFormat);
Console.WriteLine($"{frame.RawBytes.Length} bytes");That 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;
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
{
Width = 1024,
Height = 768,
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);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);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
};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.
var caps = client.GetConsoleCapabilities(vm.Id);
if (caps.CanSendMouseInputNow)
{
using var session = client.OpenConsole(vm.Id);
// Coordinates are absolute Hyper-V mouse coordinates, 0 to 32767.
var moved = session.TrySendMouseMove(16000, 16000);
var clicked = session.TrySendMouseClick(16000, 16000, MouseButton.Left);
Console.WriteLine($"Moved: {moved}, Clicked: {clicked}");
}In a browser viewer, map canvas coordinates to Hyper-V absolute coordinates:
static int ToHyperVMouseCoordinate(double canvasPosition, double canvasSize)
{
return (int)Math.Round((canvasPosition / canvasSize) * 32767);
}If mouse methods return false, keep the technician UI keyboard-first.
client.StartVirtualMachine(vm.Id);
client.StopVirtualMachine(vm.Id, force: false);
client.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 and launch it:
var caps = client.GetConsoleCapabilities(vm.Id);
if (caps.SupportsEnhancedSession)
{
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 { Width = 1024, Height = 768 },
cancellationToken);
await session.SendTextAsync("Administrator", cancellationToken);
await session.SendKeyAsync(ConsoleKeyCode.Enter, cancellationToken);Internally, WMI calls are serialized per HyperVConsoleClient instance to avoid concurrent access to WMI COM objects from multiple callers.
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 is not production-ready remote access software.
It does include practical gateway hooks in appsettings.json:
{
"HyperVConsole": {
"ApiKey": "",
"AllowedVmIds": [],
"MaxWidth": 1024,
"MaxHeight": 768,
"MaxFramesPerSecond": 5,
"MaxBytesPerSecond": 500000,
"MaxColorDepth": "Rgb332",
"MaxConcurrentViewers": 3,
"AllowKeyboardInput": true,
"AllowMouseInput": true,
"AllowPowerControl": false
}
}If ApiKey is set, pass it as ?apiKey=... in the browser or as X-Console-Api-Key for API calls. AllowedVmIds lets you expose only specific VMs.
The test project covers logic that does not require Hyper-V:
dotnet test HyperVConsoleKit.slnCurrent tests cover pixel conversion, tile diffing, and policy clamping.
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.
- 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 sample web console is not secured and should not be exposed as-is.
MIT.