MacSystemProbe.cs 3.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
  1. namespace RackPeek.Domain.Discovery;
  2. /// <summary>
  3. /// macOS host probe. Disks are deliberately left out: everything that reports them
  4. /// (diskutil, system_profiler) needs a plist parser for a field that is optional
  5. /// anyway, and omitting is better than guessing — null means "don't touch" on merge.
  6. /// </summary>
  7. public sealed class MacSystemProbe : ISystemProbe {
  8. public bool IsSupported => OperatingSystem.IsMacOS();
  9. public async Task<RawSystemSnapshot> ReadAsync(CancellationToken cancellationToken = default) {
  10. // The four probes spawn independent processes, so they run side by side.
  11. Task<string?> osName = ReadOsNameAsync(cancellationToken);
  12. Task<long?> memoryBytes = ReadMemoryBytesAsync(cancellationToken);
  13. Task<string?> platformUuid = ReadPlatformUuidAsync(cancellationToken);
  14. Task<bool> hypervisorPresent = ReadHypervisorPresentAsync(cancellationToken);
  15. return new RawSystemSnapshot {
  16. Hostname = SystemProbeCommon.Hostname(),
  17. Cores = SystemProbeCommon.Cores(),
  18. Nics = SystemProbeCommon.Nics(),
  19. FallbackMemoryBytes = SystemProbeCommon.FallbackMemoryBytes(),
  20. OsName = await osName,
  21. MemoryBytes = await memoryBytes,
  22. PlatformUuid = await platformUuid,
  23. HypervisorPresent = await hypervisorPresent
  24. };
  25. }
  26. private static async Task<string?> ReadOsNameAsync(CancellationToken cancellationToken) {
  27. var product = await SystemProbeCommon.TryRunAsync("sw_vers", "-productName", cancellationToken);
  28. var version = await SystemProbeCommon.TryRunAsync("sw_vers", "-productVersion", cancellationToken);
  29. if (string.IsNullOrWhiteSpace(product))
  30. return null;
  31. return string.IsNullOrWhiteSpace(version) ? product : $"{product} {version}";
  32. }
  33. private static async Task<long?> ReadMemoryBytesAsync(CancellationToken cancellationToken) {
  34. var value = await SystemProbeCommon.TryRunAsync("sysctl", "-n hw.memsize", cancellationToken);
  35. return long.TryParse(value, out var bytes) ? bytes : null;
  36. }
  37. private static async Task<bool> ReadHypervisorPresentAsync(CancellationToken cancellationToken) {
  38. var value = await SystemProbeCommon.TryRunAsync("sysctl", "-n kern.hv_vmm_present", cancellationToken);
  39. return value?.Trim() == "1";
  40. }
  41. private static async Task<string?> ReadPlatformUuidAsync(CancellationToken cancellationToken) {
  42. var output = await SystemProbeCommon.TryRunAsync(
  43. "ioreg", "-rd1 -c IOPlatformExpertDevice", cancellationToken);
  44. return ParsePlatformUuid(output);
  45. }
  46. /// <summary>Pulls IOPlatformUUID out of an ioreg dump. Public so it can be tested off a macOS host.</summary>
  47. public static string? ParsePlatformUuid(string? ioregOutput) {
  48. if (string.IsNullOrWhiteSpace(ioregOutput))
  49. return null;
  50. foreach (var line in ioregOutput.Split('\n')) {
  51. if (!line.Contains("IOPlatformUUID", StringComparison.Ordinal))
  52. continue;
  53. var parts = line.Split('=', 2);
  54. if (parts.Length == 2)
  55. return parts[1].Trim().Trim('"').Trim();
  56. }
  57. return null;
  58. }
  59. }