Initial commit
This commit is contained in:
312
QemuVmManager.Core/DiskManager.cs
Normal file
312
QemuVmManager.Core/DiskManager.cs
Normal file
@@ -0,0 +1,312 @@
|
||||
using System.Diagnostics;
|
||||
using QemuVmManager.Models;
|
||||
|
||||
namespace QemuVmManager.Core;
|
||||
|
||||
public class DiskManager
|
||||
{
|
||||
private readonly string _diskDirectory;
|
||||
|
||||
public DiskManager(string diskDirectory = "vm-disks")
|
||||
{
|
||||
_diskDirectory = diskDirectory;
|
||||
Directory.CreateDirectory(_diskDirectory);
|
||||
}
|
||||
|
||||
public async Task<bool> CreateDiskImageAsync(DiskConfiguration diskConfig)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Ensure the directory exists
|
||||
var directory = Path.GetDirectoryName(diskConfig.Path);
|
||||
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
// Check if disk already exists
|
||||
if (File.Exists(diskConfig.Path))
|
||||
{
|
||||
return true; // Disk already exists
|
||||
}
|
||||
|
||||
// Create disk image using qemu-img
|
||||
var sizeInBytes = diskConfig.Size * 1024 * 1024 * 1024; // Convert GB to bytes
|
||||
var sizeInMB = diskConfig.Size * 1024; // Convert GB to MB for qemu-img
|
||||
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "qemu-img",
|
||||
Arguments = $"create -f {diskConfig.Format} \"{diskConfig.Path}\" {sizeInMB}M",
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
|
||||
var started = process.Start();
|
||||
if (!started)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to start qemu-img process");
|
||||
}
|
||||
|
||||
await process.WaitForExitAsync();
|
||||
|
||||
if (process.ExitCode != 0)
|
||||
{
|
||||
var error = await process.StandardError.ReadToEndAsync();
|
||||
throw new InvalidOperationException($"Failed to create disk image: {error}");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to create disk image '{diskConfig.Path}': {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> CreateDiskImagesForVmAsync(VmConfiguration vmConfig)
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var disk in vmConfig.Storage.Disks)
|
||||
{
|
||||
await CreateDiskImageAsync(disk);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to create disk images for VM '{vmConfig.Name}': {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public bool ValidateDiskImage(string diskPath, string format)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(diskPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use qemu-img info to validate the disk
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "qemu-img",
|
||||
Arguments = $"info -f {format} \"{diskPath}\"",
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
|
||||
var started = process.Start();
|
||||
if (!started)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
process.WaitForExit();
|
||||
return process.ExitCode == 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<DiskInfo> GetDiskInfoAsync(string diskPath, string format)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(diskPath))
|
||||
{
|
||||
return new DiskInfo { Exists = false };
|
||||
}
|
||||
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "qemu-img",
|
||||
Arguments = $"info -f {format} \"{diskPath}\"",
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
|
||||
var started = process.Start();
|
||||
if (!started)
|
||||
{
|
||||
return new DiskInfo { Exists = false };
|
||||
}
|
||||
|
||||
await process.WaitForExitAsync();
|
||||
|
||||
if (process.ExitCode != 0)
|
||||
{
|
||||
return new DiskInfo { Exists = false };
|
||||
}
|
||||
|
||||
var output = await process.StandardOutput.ReadToEndAsync();
|
||||
return ParseDiskInfo(output, diskPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new DiskInfo { Exists = false };
|
||||
}
|
||||
}
|
||||
|
||||
private DiskInfo ParseDiskInfo(string qemuImgOutput, string diskPath)
|
||||
{
|
||||
var info = new DiskInfo
|
||||
{
|
||||
Path = diskPath,
|
||||
Exists = true
|
||||
};
|
||||
|
||||
var lines = qemuImgOutput.Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (var line in lines)
|
||||
{
|
||||
var trimmedLine = line.Trim();
|
||||
if (trimmedLine.StartsWith("virtual size:"))
|
||||
{
|
||||
var sizePart = trimmedLine.Substring("virtual size:".Length).Trim();
|
||||
info.VirtualSize = sizePart;
|
||||
}
|
||||
else if (trimmedLine.StartsWith("disk size:"))
|
||||
{
|
||||
var sizePart = trimmedLine.Substring("disk size:".Length).Trim();
|
||||
info.DiskSize = sizePart;
|
||||
}
|
||||
else if (trimmedLine.StartsWith("format:"))
|
||||
{
|
||||
var formatPart = trimmedLine.Substring("format:".Length).Trim();
|
||||
info.Format = formatPart;
|
||||
}
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
public async Task<bool> ResizeDiskAsync(string diskPath, string format, long newSizeGB)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(diskPath))
|
||||
{
|
||||
throw new FileNotFoundException($"Disk image not found: {diskPath}");
|
||||
}
|
||||
|
||||
var newSizeMB = newSizeGB * 1024; // Convert GB to MB
|
||||
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "qemu-img",
|
||||
Arguments = $"resize -f {format} \"{diskPath}\" {newSizeMB}M",
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
|
||||
var started = process.Start();
|
||||
if (!started)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to start qemu-img process");
|
||||
}
|
||||
|
||||
await process.WaitForExitAsync();
|
||||
|
||||
if (process.ExitCode != 0)
|
||||
{
|
||||
var error = await process.StandardError.ReadToEndAsync();
|
||||
throw new InvalidOperationException($"Failed to resize disk image: {error}");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to resize disk image '{diskPath}': {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> ConvertDiskAsync(string sourcePath, string sourceFormat, string targetPath, string targetFormat)
|
||||
{
|
||||
try
|
||||
{
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "qemu-img",
|
||||
Arguments = $"convert -f {sourceFormat} -O {targetFormat} \"{sourcePath}\" \"{targetPath}\"",
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
|
||||
var started = process.Start();
|
||||
if (!started)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to start qemu-img process");
|
||||
}
|
||||
|
||||
await process.WaitForExitAsync();
|
||||
|
||||
if (process.ExitCode != 0)
|
||||
{
|
||||
var error = await process.StandardError.ReadToEndAsync();
|
||||
throw new InvalidOperationException($"Failed to convert disk image: {error}");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to convert disk image: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public bool DeleteDiskImage(string diskPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(diskPath))
|
||||
{
|
||||
File.Delete(diskPath);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class DiskInfo
|
||||
{
|
||||
public string Path { get; set; } = string.Empty;
|
||||
public bool Exists { get; set; }
|
||||
public string? VirtualSize { get; set; }
|
||||
public string? DiskSize { get; set; }
|
||||
public string? Format { get; set; }
|
||||
}
|
356
QemuVmManager.Core/QemuCommandBuilder.cs
Normal file
356
QemuVmManager.Core/QemuCommandBuilder.cs
Normal file
@@ -0,0 +1,356 @@
|
||||
using QemuVmManager.Models;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace QemuVmManager.Core;
|
||||
|
||||
public class QemuCommandBuilder
|
||||
{
|
||||
private readonly VmConfiguration _config;
|
||||
private readonly List<string> _arguments = new();
|
||||
private readonly VirtualizationType _virtualizationType;
|
||||
|
||||
public QemuCommandBuilder(VmConfiguration config, VirtualizationType virtualizationType = VirtualizationType.TCG)
|
||||
{
|
||||
_config = config;
|
||||
_virtualizationType = virtualizationType;
|
||||
}
|
||||
|
||||
public string BuildCommand()
|
||||
{
|
||||
_arguments.Clear();
|
||||
|
||||
// Basic QEMU command
|
||||
_arguments.Add("qemu-system-x86_64");
|
||||
|
||||
// Machine and CPU configuration
|
||||
AddMachineConfiguration();
|
||||
AddCpuConfiguration();
|
||||
AddMemoryConfiguration();
|
||||
|
||||
// Storage configuration
|
||||
AddStorageConfiguration();
|
||||
|
||||
// Network configuration
|
||||
AddNetworkConfiguration();
|
||||
|
||||
// Display configuration
|
||||
AddDisplayConfiguration();
|
||||
|
||||
// Boot configuration
|
||||
AddBootConfiguration();
|
||||
|
||||
// Advanced features
|
||||
AddAdvancedConfiguration();
|
||||
|
||||
// Extra arguments
|
||||
_arguments.AddRange(_config.Advanced.ExtraArgs);
|
||||
|
||||
// Handle CPU model based on virtualization type
|
||||
if (_virtualizationType == VirtualizationType.TCG || _virtualizationType == VirtualizationType.HyperV)
|
||||
{
|
||||
// Replace 'host' CPU model with 'qemu64' for TCG compatibility
|
||||
for (int i = 0; i < _arguments.Count; i++)
|
||||
{
|
||||
if (_arguments[i] == "-cpu" && i + 1 < _arguments.Count && _arguments[i + 1] == "host")
|
||||
{
|
||||
_arguments[i + 1] = "qemu64";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove -enable-kvm if not using KVM
|
||||
if (_virtualizationType != VirtualizationType.KVM)
|
||||
{
|
||||
_arguments.RemoveAll(arg => arg == "-enable-kvm");
|
||||
}
|
||||
|
||||
// Add WHPX-specific configurations
|
||||
if (_virtualizationType == VirtualizationType.HyperV)
|
||||
{
|
||||
AddWHPXSpecificConfiguration();
|
||||
}
|
||||
|
||||
return string.Join(" ", _arguments);
|
||||
}
|
||||
|
||||
private void AddMachineConfiguration()
|
||||
{
|
||||
_arguments.Add("-machine");
|
||||
|
||||
switch (_virtualizationType)
|
||||
{
|
||||
case VirtualizationType.KVM:
|
||||
_arguments.Add("type=q35,accel=kvm:tcg");
|
||||
break;
|
||||
case VirtualizationType.HyperV:
|
||||
// Use WHPX hardware acceleration
|
||||
_arguments.Add("type=pc-i440fx-10.1,accel=whpx:tcg,kernel-irqchip=off");
|
||||
break;
|
||||
case VirtualizationType.HAXM:
|
||||
_arguments.Add("type=q35,accel=hax:tcg");
|
||||
break;
|
||||
case VirtualizationType.HVF:
|
||||
_arguments.Add("type=q35,accel=hvf:tcg");
|
||||
break;
|
||||
case VirtualizationType.TCG:
|
||||
default:
|
||||
_arguments.Add("type=q35,accel=tcg");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void AddCpuConfiguration()
|
||||
{
|
||||
var cpu = _config.Cpu;
|
||||
|
||||
_arguments.Add("-cpu");
|
||||
_arguments.Add(cpu.Model);
|
||||
|
||||
// For WHPX, use simpler CPU configuration to avoid exit code 4
|
||||
if (_virtualizationType == VirtualizationType.HyperV)
|
||||
{
|
||||
_arguments.Add("-smp");
|
||||
_arguments.Add($"{Math.Min(cpu.Cores, 2)},cores={Math.Min(cpu.Cores, 2)},sockets=1,threads=1");
|
||||
}
|
||||
else
|
||||
{
|
||||
_arguments.Add("-smp");
|
||||
_arguments.Add($"{cpu.Cores},cores={cpu.Cores},sockets={cpu.Sockets},threads={cpu.Threads}");
|
||||
}
|
||||
}
|
||||
|
||||
private void AddMemoryConfiguration()
|
||||
{
|
||||
var memory = _config.Memory;
|
||||
|
||||
// For WHPX, use smaller memory allocation to avoid exit code 4
|
||||
if (_virtualizationType == VirtualizationType.HyperV)
|
||||
{
|
||||
var whpxMemorySize = Math.Min(memory.Size, 4096); // Limit to 4GB for WHPX
|
||||
_arguments.Add("-m");
|
||||
_arguments.Add($"{whpxMemorySize}{memory.Unit}");
|
||||
}
|
||||
else
|
||||
{
|
||||
_arguments.Add("-m");
|
||||
_arguments.Add($"{memory.Size}{memory.Unit}");
|
||||
}
|
||||
}
|
||||
|
||||
private void AddStorageConfiguration()
|
||||
{
|
||||
var storage = _config.Storage;
|
||||
|
||||
// Add disks
|
||||
for (int i = 0; i < storage.Disks.Count; i++)
|
||||
{
|
||||
var disk = storage.Disks[i];
|
||||
var driveLetter = (char)('a' + i);
|
||||
|
||||
_arguments.Add("-drive");
|
||||
var driveArgs = $"file={disk.Path.Replace('\\', '/')},format={disk.Format},cache={disk.Cache},id=drive{i},if=none";
|
||||
_arguments.Add(driveArgs);
|
||||
|
||||
// For WHPX, only use the first disk on IDE to avoid conflicts
|
||||
if (_virtualizationType == VirtualizationType.HyperV && i > 0)
|
||||
{
|
||||
Console.WriteLine($"Warning: Additional disk {i} disabled for WHPX compatibility to avoid IDE conflicts. Disk: {disk.Path}");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add device
|
||||
_arguments.Add("-device");
|
||||
if (disk.Interface == "virtio" && _virtualizationType != VirtualizationType.HyperV)
|
||||
{
|
||||
// Use virtio for better performance, but avoid with WHPX due to MSI issues
|
||||
_arguments.Add($"virtio-blk-pci,drive=drive{i}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Use IDE for WHPX compatibility
|
||||
_arguments.Add($"ide-hd,drive=drive{i}");
|
||||
}
|
||||
}
|
||||
|
||||
// Add CD-ROM if specified
|
||||
if (!string.IsNullOrEmpty(storage.Cdrom))
|
||||
{
|
||||
if (_virtualizationType == VirtualizationType.HyperV)
|
||||
{
|
||||
// For WHPX, use the second IDE controller for CD-ROM
|
||||
_arguments.Add("-drive");
|
||||
_arguments.Add($"file={storage.Cdrom.Replace('\\', '/')},media=cdrom,if=ide,index=1");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Use standard CD-ROM for other virtualization types
|
||||
_arguments.Add("-cdrom");
|
||||
_arguments.Add(storage.Cdrom.Replace('\\', '/'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddNetworkConfiguration()
|
||||
{
|
||||
var network = _config.Network;
|
||||
|
||||
for (int i = 0; i < network.Interfaces.Count; i++)
|
||||
{
|
||||
var nic = network.Interfaces[i];
|
||||
|
||||
// Use user network as fallback since bridge might not be available
|
||||
_arguments.Add("-netdev");
|
||||
var netdevArgs = $"user,id=net{i}";
|
||||
_arguments.Add(netdevArgs);
|
||||
|
||||
_arguments.Add("-device");
|
||||
var deviceArgs = "";
|
||||
|
||||
// Use different network models based on virtualization type
|
||||
if (nic.Model == "virtio-net-pci" && _virtualizationType == VirtualizationType.HyperV)
|
||||
{
|
||||
// Use e1000 for WHPX compatibility to avoid MSI issues
|
||||
deviceArgs = $"e1000,netdev=net{i}";
|
||||
}
|
||||
else
|
||||
{
|
||||
deviceArgs = $"{nic.Model},netdev=net{i}";
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(nic.Mac))
|
||||
{
|
||||
deviceArgs += $",mac={nic.Mac}";
|
||||
}
|
||||
_arguments.Add(deviceArgs);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddDisplayConfiguration()
|
||||
{
|
||||
var display = _config.Display;
|
||||
|
||||
_arguments.Add("-display");
|
||||
_arguments.Add(display.Type);
|
||||
|
||||
_arguments.Add("-vga");
|
||||
_arguments.Add(display.Vga);
|
||||
|
||||
if (display.EnableSpice)
|
||||
{
|
||||
_arguments.Add("-spice");
|
||||
_arguments.Add($"port={display.SpicePort},addr=127.0.0.1,disable-ticketing=on");
|
||||
}
|
||||
}
|
||||
|
||||
private void AddBootConfiguration()
|
||||
{
|
||||
var boot = _config.Boot;
|
||||
|
||||
if (boot.Order.Count > 0)
|
||||
{
|
||||
_arguments.Add("-boot");
|
||||
_arguments.Add($"order={string.Join("", boot.Order)}");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(boot.Kernel))
|
||||
{
|
||||
_arguments.Add("-kernel");
|
||||
_arguments.Add(boot.Kernel);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(boot.Initrd))
|
||||
{
|
||||
_arguments.Add("-initrd");
|
||||
_arguments.Add(boot.Initrd);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(boot.Cmdline))
|
||||
{
|
||||
_arguments.Add("-append");
|
||||
_arguments.Add(boot.Cmdline);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddAdvancedConfiguration()
|
||||
{
|
||||
var advanced = _config.Advanced;
|
||||
|
||||
if (advanced.EnableAudio)
|
||||
{
|
||||
_arguments.Add("-device");
|
||||
_arguments.Add("intel-hda");
|
||||
_arguments.Add("-device");
|
||||
_arguments.Add("hda-duplex");
|
||||
}
|
||||
|
||||
if (advanced.EnableUsb)
|
||||
{
|
||||
_arguments.Add("-usb");
|
||||
_arguments.Add("-device");
|
||||
_arguments.Add("usb-tablet");
|
||||
}
|
||||
|
||||
// Disable virtio devices for WHPX to avoid MSI issues
|
||||
if (advanced.EnableBalloon && _virtualizationType != VirtualizationType.HyperV)
|
||||
{
|
||||
_arguments.Add("-device");
|
||||
_arguments.Add("virtio-balloon-pci");
|
||||
}
|
||||
|
||||
if (advanced.EnableVirtioRng && _virtualizationType != VirtualizationType.HyperV)
|
||||
{
|
||||
_arguments.Add("-device");
|
||||
_arguments.Add("virtio-rng-pci");
|
||||
}
|
||||
|
||||
if (advanced.EnableVirtioFs && _virtualizationType != VirtualizationType.HyperV)
|
||||
{
|
||||
_arguments.Add("-device");
|
||||
_arguments.Add("virtio-fs-pci");
|
||||
}
|
||||
|
||||
// Add shared folders (disabled for compatibility)
|
||||
// Note: 9p filesystem support is not available in all QEMU builds
|
||||
// For now, shared folders are disabled to ensure compatibility
|
||||
if (advanced.SharedFolders.Any())
|
||||
{
|
||||
// Log that shared folders are disabled
|
||||
Console.WriteLine($"Warning: Shared folders are disabled for compatibility. {advanced.SharedFolders.Count} folder(s) configured but not used.");
|
||||
}
|
||||
}
|
||||
|
||||
private void AddWHPXSpecificConfiguration()
|
||||
{
|
||||
// Add WHPX-specific configurations to avoid MSI issues
|
||||
|
||||
// Disable MSI for better WHPX compatibility
|
||||
_arguments.Add("-global");
|
||||
_arguments.Add("pcie-root-port.msi=off");
|
||||
|
||||
// Use legacy interrupt mode for better compatibility
|
||||
_arguments.Add("-global");
|
||||
_arguments.Add("pcie-root-port.msix=off");
|
||||
|
||||
// Add additional WHPX optimizations
|
||||
_arguments.Add("-global");
|
||||
_arguments.Add("pcie-root-port.ari=off");
|
||||
|
||||
// Add WHPX-specific optimizations
|
||||
_arguments.Add("-rtc");
|
||||
_arguments.Add("base=localtime");
|
||||
|
||||
// Memory allocation optimizations for WHPX
|
||||
// Note: -mem-path is not needed for WHPX and can cause issues
|
||||
|
||||
// Memory preallocation is not available in this QEMU build
|
||||
|
||||
// Add additional WHPX optimizations to avoid exit code 4
|
||||
_arguments.Add("-no-reboot");
|
||||
_arguments.Add("-no-shutdown");
|
||||
|
||||
// Use simpler interrupt handling (removed KVM-specific option)
|
||||
|
||||
// Disable some features that might cause issues with WHPX
|
||||
// _arguments.Add("-no-acpi"); // Commented out as it might cause issues
|
||||
}
|
||||
}
|
927
QemuVmManager.Core/QemuProcessManager.cs
Normal file
927
QemuVmManager.Core/QemuProcessManager.cs
Normal file
@@ -0,0 +1,927 @@
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using QemuVmManager.Models;
|
||||
|
||||
namespace QemuVmManager.Core;
|
||||
|
||||
public class QemuProcessManager
|
||||
{
|
||||
private readonly Dictionary<string, Process> _runningVms = new();
|
||||
private readonly Dictionary<string, VmStatus> _vmStatuses = new();
|
||||
private readonly Dictionary<string, PerformanceMonitor> _performanceMonitors = new();
|
||||
|
||||
public event EventHandler<VmStatusChangedEventArgs>? VmStatusChanged;
|
||||
|
||||
public bool IsQemuInstalled()
|
||||
{
|
||||
try
|
||||
{
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "qemu-system-x86_64",
|
||||
Arguments = "--version",
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
|
||||
var started = process.Start();
|
||||
if (!started)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
process.WaitForExit(5000);
|
||||
return process.ExitCode == 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public string GetQemuVersion()
|
||||
{
|
||||
try
|
||||
{
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "qemu-system-x86_64",
|
||||
Arguments = "--version",
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
|
||||
var started = process.Start();
|
||||
if (!started)
|
||||
{
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
var output = process.StandardOutput.ReadToEnd();
|
||||
process.WaitForExit(5000);
|
||||
|
||||
if (process.ExitCode == 0)
|
||||
{
|
||||
var lines = output.Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
||||
return lines.FirstOrDefault() ?? "Unknown";
|
||||
}
|
||||
|
||||
return "Unknown";
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
public string GetQemuAccelerators()
|
||||
{
|
||||
try
|
||||
{
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "qemu-system-x86_64",
|
||||
Arguments = "-accel help",
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
|
||||
var started = process.Start();
|
||||
if (!started)
|
||||
{
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
var output = process.StandardOutput.ReadToEnd();
|
||||
process.WaitForExit(5000);
|
||||
|
||||
if (process.ExitCode == 0)
|
||||
{
|
||||
return output.Trim();
|
||||
}
|
||||
|
||||
return "Unknown";
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
public VirtualizationType GetAvailableVirtualization()
|
||||
{
|
||||
try
|
||||
{
|
||||
// First check if virtualization is enabled in BIOS
|
||||
if (!IsVirtualizationEnabled())
|
||||
{
|
||||
return VirtualizationType.TCG;
|
||||
}
|
||||
|
||||
if (OperatingSystem.IsLinux())
|
||||
{
|
||||
// Check for KVM support on Linux
|
||||
if (File.Exists("/dev/kvm"))
|
||||
{
|
||||
// Test if KVM is accessible
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "qemu-system-x86_64",
|
||||
Arguments = "-accel help",
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
|
||||
if (process.Start())
|
||||
{
|
||||
process.WaitForExit(3000);
|
||||
var output = process.StandardOutput.ReadToEnd();
|
||||
if (output.Contains("kvm") && process.ExitCode == 0)
|
||||
{
|
||||
return VirtualizationType.KVM;
|
||||
}
|
||||
}
|
||||
}
|
||||
return VirtualizationType.TCG;
|
||||
}
|
||||
else if (OperatingSystem.IsWindows())
|
||||
{
|
||||
// Check QEMU's available accelerators first
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "qemu-system-x86_64",
|
||||
Arguments = "-accel help",
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
|
||||
if (process.Start())
|
||||
{
|
||||
process.WaitForExit(3000);
|
||||
var output = process.StandardOutput.ReadToEnd();
|
||||
|
||||
// Check for WHPX (Windows Hypervisor Platform) support
|
||||
if (output.Contains("whpx") && process.ExitCode == 0)
|
||||
{
|
||||
return VirtualizationType.HyperV;
|
||||
}
|
||||
|
||||
// Check for Hyper-V support (hvf)
|
||||
if (output.Contains("hvf") && process.ExitCode == 0)
|
||||
{
|
||||
return VirtualizationType.HVF;
|
||||
}
|
||||
|
||||
// Check for HAXM support
|
||||
if (output.Contains("hax") && process.ExitCode == 0)
|
||||
{
|
||||
return VirtualizationType.HAXM;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: Check for Hyper-V support using WMI
|
||||
try
|
||||
{
|
||||
var wmiProcess = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "powershell",
|
||||
Arguments = "-Command \"Get-WmiObject -Class Msvm_VirtualSystemSettingData -Namespace root\\virtualization\\v2 -ErrorAction SilentlyContinue | Select-Object -First 1\"",
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
|
||||
if (wmiProcess.Start())
|
||||
{
|
||||
wmiProcess.WaitForExit(3000);
|
||||
if (wmiProcess.ExitCode == 0 && !string.IsNullOrEmpty(wmiProcess.StandardOutput.ReadToEnd()))
|
||||
{
|
||||
return VirtualizationType.HyperV;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Hyper-V check failed
|
||||
}
|
||||
|
||||
return VirtualizationType.TCG;
|
||||
}
|
||||
else if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
// Check for HVF (Hypervisor.framework) on macOS
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "qemu-system-x86_64",
|
||||
Arguments = "-accel help",
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
|
||||
if (process.Start())
|
||||
{
|
||||
process.WaitForExit(3000);
|
||||
var output = process.StandardOutput.ReadToEnd();
|
||||
if (output.Contains("hvf") && process.ExitCode == 0)
|
||||
{
|
||||
return VirtualizationType.HVF;
|
||||
}
|
||||
}
|
||||
|
||||
return VirtualizationType.TCG;
|
||||
}
|
||||
|
||||
return VirtualizationType.TCG;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return VirtualizationType.TCG;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsVirtualizationEnabled()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (OperatingSystem.IsLinux())
|
||||
{
|
||||
// Check if virtualization is enabled in BIOS
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "lscpu",
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
|
||||
if (process.Start())
|
||||
{
|
||||
process.WaitForExit(3000);
|
||||
var output = process.StandardOutput.ReadToEnd();
|
||||
return output.Contains("Virtualization:") &&
|
||||
(output.Contains("VT-x") || output.Contains("AMD-V") || output.Contains("SVM"));
|
||||
}
|
||||
}
|
||||
else if (OperatingSystem.IsWindows())
|
||||
{
|
||||
// Multiple methods to check virtualization on Windows
|
||||
|
||||
// Method 1: Check using systeminfo
|
||||
try
|
||||
{
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "systeminfo",
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
|
||||
if (process.Start())
|
||||
{
|
||||
process.WaitForExit(3000);
|
||||
var output = process.StandardOutput.ReadToEnd();
|
||||
|
||||
// Check multiple possible virtualization indicators
|
||||
if (output.Contains("Virtualization Enabled In Firmware: Yes") ||
|
||||
output.Contains("Virtualization: Enabled") ||
|
||||
output.Contains("Hyper-V Requirements:") && output.Contains("Yes"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Continue to next method
|
||||
}
|
||||
|
||||
// Method 2: Check using PowerShell Get-ComputerInfo
|
||||
try
|
||||
{
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "powershell",
|
||||
Arguments = "-Command \"Get-ComputerInfo | Select-Object HyperVRequirementVirtualizationFirmwareEnabled\"",
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
|
||||
if (process.Start())
|
||||
{
|
||||
process.WaitForExit(3000);
|
||||
var output = process.StandardOutput.ReadToEnd();
|
||||
return output.Contains("True");
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Continue to next method
|
||||
}
|
||||
|
||||
// Method 3: Check using wmic
|
||||
try
|
||||
{
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "wmic",
|
||||
Arguments = "cpu get VirtualizationFirmwareEnabled",
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
|
||||
if (process.Start())
|
||||
{
|
||||
process.WaitForExit(3000);
|
||||
var output = process.StandardOutput.ReadToEnd();
|
||||
return output.Contains("TRUE");
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Continue to next method
|
||||
}
|
||||
|
||||
// Method 4: Check using PowerShell Get-WmiObject
|
||||
try
|
||||
{
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "powershell",
|
||||
Arguments = "-Command \"Get-WmiObject -Class Msvm_VirtualSystemSettingData -Namespace root\\virtualization\\v2 | Select-Object VirtualSystemIdentifiers\"",
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
|
||||
if (process.Start())
|
||||
{
|
||||
process.WaitForExit(3000);
|
||||
var output = process.StandardOutput.ReadToEnd();
|
||||
// If we can query Hyper-V WMI, virtualization is likely enabled
|
||||
return !output.Contains("Get-WmiObject") && output.Length > 0;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// All methods failed
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> StartVmAsync(VmConfiguration config)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_runningVms.ContainsKey(config.Name))
|
||||
{
|
||||
throw new InvalidOperationException($"VM '{config.Name}' is already running");
|
||||
}
|
||||
|
||||
var virtualizationType = GetAvailableVirtualization();
|
||||
var commandBuilder = new QemuCommandBuilder(config, virtualizationType);
|
||||
var command = commandBuilder.BuildCommand();
|
||||
|
||||
Console.WriteLine($"[{config.Name}] Starting QEMU with command:");
|
||||
Console.WriteLine($"[{config.Name}] {command}");
|
||||
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "qemu-system-x86_64",
|
||||
Arguments = command.Replace("qemu-system-x86_64 ", ""),
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = false
|
||||
},
|
||||
EnableRaisingEvents = true
|
||||
};
|
||||
|
||||
var outputLines = new List<string>();
|
||||
var errorLines = new List<string>();
|
||||
|
||||
// Set up event handlers for output capture
|
||||
process.OutputDataReceived += (sender, e) =>
|
||||
{
|
||||
if (!string.IsNullOrEmpty(e.Data))
|
||||
{
|
||||
outputLines.Add(e.Data);
|
||||
Console.WriteLine($"[{config.Name}] {e.Data}");
|
||||
}
|
||||
};
|
||||
|
||||
process.ErrorDataReceived += (sender, e) =>
|
||||
{
|
||||
if (!string.IsNullOrEmpty(e.Data))
|
||||
{
|
||||
errorLines.Add(e.Data);
|
||||
Console.WriteLine($"[{config.Name}] ERROR: {e.Data}");
|
||||
}
|
||||
};
|
||||
|
||||
process.Exited += (sender, e) => OnVmExitedWithDetails(config.Name, process, outputLines, errorLines);
|
||||
|
||||
var started = process.Start();
|
||||
if (!started)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to start QEMU process");
|
||||
}
|
||||
|
||||
// Start reading output
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
|
||||
// Wait a moment to see if it starts successfully
|
||||
await Task.Delay(3000);
|
||||
|
||||
if (process.HasExited)
|
||||
{
|
||||
var errorMessage = $"QEMU process exited immediately with code {process.ExitCode}";
|
||||
if (errorLines.Any())
|
||||
{
|
||||
errorMessage += $". Errors: {string.Join("; ", errorLines)}";
|
||||
}
|
||||
if (outputLines.Any())
|
||||
{
|
||||
errorMessage += $". Output: {string.Join("; ", outputLines)}";
|
||||
}
|
||||
|
||||
Console.WriteLine($"[{config.Name}] {errorMessage}");
|
||||
UpdateVmStatus(config.Name, VmState.Error, -1, errorMessage);
|
||||
throw new InvalidOperationException(errorMessage);
|
||||
}
|
||||
|
||||
_runningVms[config.Name] = process;
|
||||
UpdateVmStatus(config.Name, VmState.Running, process.Id);
|
||||
|
||||
// Start monitoring in background
|
||||
_ = Task.Run(() => MonitorVmAsync(config.Name, process));
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UpdateVmStatus(config.Name, VmState.Error, -1, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> StopVmAsync(string vmName, bool force = false)
|
||||
{
|
||||
if (!_runningVms.TryGetValue(vmName, out var process))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
UpdateVmStatus(vmName, VmState.Stopping, process.Id);
|
||||
|
||||
if (force)
|
||||
{
|
||||
process.Kill();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Try graceful shutdown first
|
||||
process.CloseMainWindow();
|
||||
|
||||
// Wait for graceful shutdown
|
||||
try
|
||||
{
|
||||
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
|
||||
await process.WaitForExitAsync(cts.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Timeout occurred, kill the process
|
||||
process.Kill();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UpdateVmStatus(vmName, VmState.Error, process.Id, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> PauseVmAsync(string vmName)
|
||||
{
|
||||
if (!_runningVms.TryGetValue(vmName, out var process))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Use QEMU monitor to pause VM
|
||||
await SendQemuCommandAsync(vmName, "stop");
|
||||
UpdateVmStatus(vmName, VmState.Paused, process.Id);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UpdateVmStatus(vmName, VmState.Error, process.Id, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> ResumeVmAsync(string vmName)
|
||||
{
|
||||
if (!_runningVms.TryGetValue(vmName, out var process))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Use QEMU monitor to resume VM
|
||||
await SendQemuCommandAsync(vmName, "cont");
|
||||
UpdateVmStatus(vmName, VmState.Running, process.Id);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UpdateVmStatus(vmName, VmState.Error, process.Id, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public VmStatus? GetVmStatus(string vmName)
|
||||
{
|
||||
return _vmStatuses.TryGetValue(vmName, out var status) ? status : null;
|
||||
}
|
||||
|
||||
public IEnumerable<VmStatus> GetAllVmStatuses()
|
||||
{
|
||||
return _vmStatuses.Values;
|
||||
}
|
||||
|
||||
public bool IsVmRunning(string vmName)
|
||||
{
|
||||
return _runningVms.ContainsKey(vmName) && !_runningVms[vmName].HasExited;
|
||||
}
|
||||
|
||||
private async Task MonitorVmAsync(string vmName, Process process)
|
||||
{
|
||||
try
|
||||
{
|
||||
while (!process.HasExited)
|
||||
{
|
||||
await Task.Delay(5000); // Check every 5 seconds
|
||||
|
||||
// Update resource usage if VM is running
|
||||
if (process.HasExited == false)
|
||||
{
|
||||
var resourceUsage = await GetVmResourceUsageAsync(vmName);
|
||||
UpdateVmResourceUsage(vmName, resourceUsage);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UpdateVmStatus(vmName, VmState.Error, process.Id, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private Task<VmResourceUsage> GetVmResourceUsageAsync(string vmName)
|
||||
{
|
||||
try
|
||||
{
|
||||
// This is a simplified implementation
|
||||
// In a real scenario, you would use QEMU monitor commands or libvirt
|
||||
var usage = new VmResourceUsage();
|
||||
|
||||
// Get CPU usage from process
|
||||
if (_runningVms.TryGetValue(vmName, out var process))
|
||||
{
|
||||
var startTime = process.StartTime;
|
||||
var totalProcessorTime = process.TotalProcessorTime;
|
||||
var realTime = DateTime.Now - startTime;
|
||||
|
||||
usage.CpuUsage = (totalProcessorTime.TotalMilliseconds / (Environment.ProcessorCount * realTime.TotalMilliseconds)) * 100;
|
||||
|
||||
// Get memory usage
|
||||
usage.MemoryUsage = process.WorkingSet64 / (1024 * 1024); // Convert to MB
|
||||
}
|
||||
|
||||
return Task.FromResult(usage);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Task.FromResult(new VmResourceUsage());
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendQemuCommandAsync(string vmName, string command)
|
||||
{
|
||||
// This would require QEMU monitor socket or similar mechanism
|
||||
// For now, this is a placeholder
|
||||
await Task.Delay(100);
|
||||
}
|
||||
|
||||
private void OnVmExited(string vmName)
|
||||
{
|
||||
if (_runningVms.TryGetValue(vmName, out var process))
|
||||
{
|
||||
_runningVms.Remove(vmName);
|
||||
UpdateVmStatus(vmName, VmState.Stopped, -1);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnVmExitedWithDetails(string vmName, Process process, List<string> outputLines, List<string> errorLines)
|
||||
{
|
||||
if (_runningVms.TryGetValue(vmName, out var runningProcess))
|
||||
{
|
||||
_runningVms.Remove(vmName);
|
||||
}
|
||||
|
||||
var errorMessage = $"Process exited with code {process.ExitCode}";
|
||||
if (errorLines.Any())
|
||||
{
|
||||
errorMessage += $". Errors: {string.Join("; ", errorLines)}";
|
||||
}
|
||||
if (outputLines.Any())
|
||||
{
|
||||
errorMessage += $". Output: {string.Join("; ", outputLines)}";
|
||||
}
|
||||
|
||||
Console.WriteLine($"[{vmName}] {errorMessage}");
|
||||
UpdateVmStatus(vmName, VmState.Stopped, -1, errorMessage);
|
||||
}
|
||||
|
||||
private void UpdateVmStatus(string vmName, VmState state, int processId, string? errorMessage = null)
|
||||
{
|
||||
if (!_vmStatuses.TryGetValue(vmName, out var status))
|
||||
{
|
||||
status = new VmStatus { Name = vmName };
|
||||
_vmStatuses[vmName] = status;
|
||||
}
|
||||
|
||||
var oldState = status.State;
|
||||
|
||||
status.State = state;
|
||||
status.ProcessId = processId;
|
||||
status.ErrorMessage = errorMessage;
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case VmState.Running:
|
||||
status.StartedAt = DateTime.UtcNow;
|
||||
status.StoppedAt = null;
|
||||
break;
|
||||
case VmState.Stopped:
|
||||
case VmState.Error:
|
||||
status.StoppedAt = DateTime.UtcNow;
|
||||
break;
|
||||
}
|
||||
|
||||
VmStatusChanged?.Invoke(this, new VmStatusChangedEventArgs(vmName, oldState, state));
|
||||
}
|
||||
|
||||
private void UpdateVmResourceUsage(string vmName, VmResourceUsage usage)
|
||||
{
|
||||
if (_vmStatuses.TryGetValue(vmName, out var status))
|
||||
{
|
||||
status.ResourceUsage = usage;
|
||||
}
|
||||
}
|
||||
|
||||
// Enhanced performance monitoring methods
|
||||
public async Task<VmPerformanceMetrics> GetVmPerformanceMetricsAsync(string vmName)
|
||||
{
|
||||
if (!_performanceMonitors.TryGetValue(vmName, out var monitor))
|
||||
{
|
||||
return new VmPerformanceMetrics();
|
||||
}
|
||||
|
||||
return await monitor.GetCurrentMetricsAsync();
|
||||
}
|
||||
|
||||
public Task StartPerformanceMonitoringAsync(string vmName)
|
||||
{
|
||||
if (_runningVms.TryGetValue(vmName, out var process))
|
||||
{
|
||||
var monitor = new PerformanceMonitor(process);
|
||||
_performanceMonitors[vmName] = monitor;
|
||||
return monitor.StartMonitoringAsync();
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void StopPerformanceMonitoring(string vmName)
|
||||
{
|
||||
if (_performanceMonitors.TryGetValue(vmName, out var monitor))
|
||||
{
|
||||
monitor.StopMonitoring();
|
||||
_performanceMonitors.Remove(vmName);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<VmPerformanceMetrics>> GetPerformanceHistoryAsync(string vmName, int maxSamples = 100)
|
||||
{
|
||||
if (_performanceMonitors.TryGetValue(vmName, out var monitor))
|
||||
{
|
||||
return await monitor.GetPerformanceHistoryAsync(maxSamples);
|
||||
}
|
||||
return new List<VmPerformanceMetrics>();
|
||||
}
|
||||
}
|
||||
|
||||
public class PerformanceMonitor
|
||||
{
|
||||
private readonly Process _process;
|
||||
private readonly List<VmPerformanceMetrics> _history = new();
|
||||
private readonly object _lock = new();
|
||||
private bool _isMonitoring = false;
|
||||
private CancellationTokenSource? _cancellationTokenSource;
|
||||
|
||||
public PerformanceMonitor(Process process)
|
||||
{
|
||||
_process = process;
|
||||
}
|
||||
|
||||
public async Task StartMonitoringAsync()
|
||||
{
|
||||
if (_isMonitoring) return;
|
||||
|
||||
_isMonitoring = true;
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
while (_isMonitoring && !_process.HasExited)
|
||||
{
|
||||
try
|
||||
{
|
||||
var metrics = await GetCurrentMetricsAsync();
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_history.Add(metrics);
|
||||
// Keep only last 1000 samples
|
||||
if (_history.Count > 1000)
|
||||
{
|
||||
_history.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
await Task.Delay(2000, _cancellationTokenSource.Token); // Sample every 2 seconds
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Continue monitoring even if there's an error
|
||||
}
|
||||
}
|
||||
}, _cancellationTokenSource.Token);
|
||||
}
|
||||
|
||||
public void StopMonitoring()
|
||||
{
|
||||
_isMonitoring = false;
|
||||
_cancellationTokenSource?.Cancel();
|
||||
}
|
||||
|
||||
public Task<VmPerformanceMetrics> GetCurrentMetricsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var metrics = new VmPerformanceMetrics
|
||||
{
|
||||
Timestamp = DateTime.UtcNow,
|
||||
ProcessId = _process.Id
|
||||
};
|
||||
|
||||
if (!_process.HasExited)
|
||||
{
|
||||
_process.Refresh();
|
||||
|
||||
// CPU metrics
|
||||
metrics.CpuUsagePercent = _process.TotalProcessorTime.TotalMilliseconds /
|
||||
(Environment.ProcessorCount * (DateTime.Now - _process.StartTime).TotalMilliseconds) * 100;
|
||||
|
||||
// Memory metrics
|
||||
metrics.MemoryUsageMB = _process.WorkingSet64 / (1024 * 1024);
|
||||
metrics.PrivateMemoryMB = _process.PrivateMemorySize64 / (1024 * 1024);
|
||||
metrics.VirtualMemoryMB = _process.VirtualMemorySize64 / (1024 * 1024);
|
||||
|
||||
// Process metrics
|
||||
metrics.ThreadCount = _process.Threads.Count;
|
||||
metrics.HandleCount = _process.HandleCount;
|
||||
|
||||
// Performance counters (if available)
|
||||
try
|
||||
{
|
||||
using var cpuCounter = new PerformanceCounter("Processor", "% Processor Time", "_Total");
|
||||
metrics.SystemCpuUsagePercent = cpuCounter.NextValue();
|
||||
}
|
||||
catch
|
||||
{
|
||||
metrics.SystemCpuUsagePercent = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult(metrics);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Task.FromResult(new VmPerformanceMetrics { Timestamp = DateTime.UtcNow });
|
||||
}
|
||||
}
|
||||
|
||||
public Task<List<VmPerformanceMetrics>> GetPerformanceHistoryAsync(int maxSamples = 100)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return Task.FromResult(_history.TakeLast(maxSamples).ToList());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public class VmStatusChangedEventArgs : EventArgs
|
||||
{
|
||||
public string VmName { get; }
|
||||
public VmState OldState { get; }
|
||||
public VmState NewState { get; }
|
||||
|
||||
public VmStatusChangedEventArgs(string vmName, VmState oldState, VmState newState)
|
||||
{
|
||||
VmName = vmName;
|
||||
OldState = oldState;
|
||||
NewState = newState;
|
||||
}
|
||||
}
|
17
QemuVmManager.Core/QemuVmManager.Core.csproj
Normal file
17
QemuVmManager.Core/QemuVmManager.Core.csproj
Normal file
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\QemuVmManager.Models\QemuVmManager.Models.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Diagnostics.PerformanceCounter" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
Reference in New Issue
Block a user