Skip to content

Commit

Permalink
add - doc - Added full hardware probing support for macOS
Browse files Browse the repository at this point in the history
---

We've finally added full hardware probing support for macOS systems!

Please be aware that you may need to notarize your application and set the SetNotarized() to true so that the video prober can use the CoreGraphics framework instead. Otherwise, the fallback system_profiler method will be used instead.

How to code sign: https://learn.microsoft.com/en-us/xamarin/mac/deploy-test/publishing-to-the-app-store/signing

---

Type: add
Breaking: False
Doc Required: True
Part: 1/1
  • Loading branch information
AptiviCEO committed Nov 13, 2023
1 parent e172544 commit 5149696
Show file tree
Hide file tree
Showing 7 changed files with 440 additions and 4 deletions.
1 change: 1 addition & 0 deletions SpecProbe.ConsoleTest/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ public static void Main()
{
TextWriterColor.WriteColor("Error: ", false, 3);
TextWriterColor.WriteColor($"{exc.Message}", true, 8);
TextWriterColor.WriteColor($"{exc.StackTrace}", true, 8);
}
}
}
Expand Down
8 changes: 8 additions & 0 deletions SpecProbe/Hardware/HardwareProber.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ namespace SpecProbe.Hardware
/// </summary>
public static class HardwareProber
{
internal static bool notarized = false;
internal static List<Exception> errors = new();
private static ProcessorPart[] cachedProcessors;
private static MemoryPart[] cachedMemory;
Expand Down Expand Up @@ -69,6 +70,13 @@ public static class HardwareProber
public static Exception[] Errors =>
errors.ToArray();

/// <summary>
/// For Apple's code signing.
/// </summary>
/// <param name="notarized">If your application is using hardened macOS runtime, set this to true.</param>
public static void SetNotarized(bool notarized) =>
HardwareProber.notarized = notarized;

private static ProcessorPart[] ProbeProcessors()
{
// Get the base part class instances from the part prober
Expand Down
132 changes: 131 additions & 1 deletion SpecProbe/Hardware/Probers/HardDiskProber.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,137 @@ public BaseHardwarePartInfo[] GetBaseHardwarePartsLinux()

public BaseHardwarePartInfo[] GetBaseHardwarePartsMacOS()
{
throw new NotImplementedException();
// Some variables to install.
List<HardDiskPart> diskParts = new();
List<HardDiskPart.PartitionPart> partitions = new();

// Get the blocks
try
{
List<int> virtuals = new();
string blockListFolder = "/dev";
string[] blockFolders = Directory.GetFiles(blockListFolder).Where((dir) => dir.Contains("/dev/disk")).ToArray();
for (int i = 0; i < blockFolders.Length; i++)
{
string blockFolder = blockFolders[i];

// Necessary for diskutil parsing
string diskUtilTrue = "Yes";
string diskUtilFixed = "Fixed";
string blockVirtualTag = "Virtual:";
string blockDiskSizeTag = "Disk Size:";
string blockVirtualDiskSizeTag = "Volume Used Space:";
string blockRemovableMediaTag = "Removable Media:";
string blockIsWholeTag = "Whole:";
string blockDiskTag = "Part of Whole:";

// Some variables for the block
bool blockVirtual = false;
bool blockFixed = true;
bool blockIsDisk = true;
string reallyDiskId = "";
ulong actualSize = 0;
int diskNum = 1;

// Execute "diskutil info" on that block
string diskutilOutput = PlatformHelper.ExecuteProcessToString("/usr/sbin/diskutil", $"info {blockFolder}");
string[] diskutilOutputLines = diskutilOutput.Replace("\r", "").Split('\n');
foreach (string diskutilOutputLine in diskutilOutputLines)
{
if (!blockFixed)
break;
string trimmedLine = diskutilOutputLine.Trim();
if (trimmedLine.StartsWith(blockVirtualTag))
{
// Trim the tag to get the value.
blockVirtual = trimmedLine[blockVirtualTag.Length..].Trim() == diskUtilTrue;
}
if (trimmedLine.StartsWith(blockRemovableMediaTag))
{
// Trim the tag to get the value.
blockFixed = trimmedLine[blockRemovableMediaTag.Length..].Trim() == diskUtilFixed;
}
if (trimmedLine.StartsWith(blockIsWholeTag))
{
// Trim the tag to get the value.
blockIsDisk = trimmedLine[blockIsWholeTag.Length..].Trim() == diskUtilTrue;
}
if (trimmedLine.StartsWith(blockDiskTag))
{
// Trim the tag to get the value.
reallyDiskId = trimmedLine[blockDiskTag.Length..].Trim();
diskNum = int.Parse(reallyDiskId["disk".Length..]) + 1;
if (virtuals.Contains(diskNum))
blockVirtual = true;
}
if (trimmedLine.StartsWith(blockDiskSizeTag) && !blockVirtual)
{
// Trim the tag to get the value like:
// Disk Size: 107.4 GB (107374182400 Bytes) (exactly 209715200 512-Byte-Units)
string sizes = trimmedLine[blockDiskSizeTag.Length..].Trim();

// We don't want to make the same mistake as we've done in the past for Inxi.NET, so we need to
// get the number of bytes from that.
sizes = sizes[(sizes.IndexOf('(') + 1)..sizes.IndexOf(" Bytes)")];
actualSize = ulong.Parse(sizes);
}
if (trimmedLine.StartsWith(blockVirtualDiskSizeTag) && blockVirtual)
{
// Trim the tag to get the value like:
// Volume Used Space: 2.0 GB (2013110272 Bytes) (exactly 3931856 512-Byte-Units)
string sizes = trimmedLine[blockVirtualDiskSizeTag.Length..].Trim();

// We don't want to make the same mistake as we've done in the past for Inxi.NET, so we need to
// get the number of bytes from that.
sizes = sizes[(sizes.IndexOf('(') + 1)..sizes.IndexOf(" Bytes)")];
actualSize = ulong.Parse(sizes);
}
}

// Don't continue if the drive is not fixed
if (!blockFixed)
continue;

// Get the disk and the partition number
int partNum = 0;
if (!blockIsDisk)
{
string part = Path.GetFileName(blockFolder)[(reallyDiskId.Length + 1)..];
part = part.Contains("s") ? part[..part.IndexOf("s")] : part;
partNum = int.Parse(part);
}
if (blockVirtual && !virtuals.Contains(diskNum))
virtuals.Add(diskNum);

// Now, either put it to a partition or a disk
if (blockIsDisk)
{
partitions.Clear();
diskParts.Add(new HardDiskPart
{
HardDiskSize = actualSize,
HardDiskNumber = diskNum,
Partitions = partitions.ToArray(),
});
}
else
{
partitions.Add(new HardDiskPart.PartitionPart
{
PartitionNumber = partNum,
PartitionSize = (long)actualSize,
});
diskParts[diskNum - 1].Partitions = partitions.ToArray();
}
}
}
catch (Exception ex)
{
HardwareProber.errors.Add(ex);
}

// Finally, return an array containing information
return diskParts.ToArray();
}

public BaseHardwarePartInfo[] GetBaseHardwarePartsWindows()
Expand Down
33 changes: 32 additions & 1 deletion SpecProbe/Hardware/Probers/MemoryProber.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,38 @@ public BaseHardwarePartInfo[] GetBaseHardwarePartsLinux()

public BaseHardwarePartInfo[] GetBaseHardwarePartsMacOS()
{
throw new NotImplementedException();
// Some variables to install.
long totalMemory = 0;
long totalPhysicalMemory = 0;

// Some constants
const string total = "hw.memsize: ";
const string totalUsable = "hw.memsize_usable: ";

try
{
string sysctlOutput = PlatformHelper.ExecuteProcessToString("/usr/sbin/sysctl", "hw.memsize_usable hw.memsize");
string[] sysctlOutputLines = sysctlOutput.Replace("\r", "").Split('\n');
foreach (string sysctlOutputLine in sysctlOutputLines)
{
if (sysctlOutputLine.StartsWith(total))
totalMemory = long.Parse(sysctlOutputLine[total.Length..]);
if (sysctlOutputLine.StartsWith(totalUsable))
totalPhysicalMemory = long.Parse(sysctlOutputLine[totalUsable.Length..]);
}
}
catch (Exception ex)
{
HardwareProber.errors.Add(ex);
}

// Finally, return a single item array containing information
MemoryPart part = new()
{
TotalMemory = totalMemory,
TotalPhysicalMemory = totalPhysicalMemory,
};
return new[] { part };
}

public BaseHardwarePartInfo[] GetBaseHardwarePartsWindows()
Expand Down
69 changes: 68 additions & 1 deletion SpecProbe/Hardware/Probers/ProcessorProber.cs
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,74 @@ public BaseHardwarePartInfo[] GetBaseHardwarePartsLinux()

public BaseHardwarePartInfo[] GetBaseHardwarePartsMacOS()
{
throw new NotImplementedException();
// Some variables to install.
int numberOfCores = 0;
int numberOfCoresForEachCore = 1;
uint cacheL1 = 0;
uint cacheL2 = 0;
uint cacheL3 = 0;
string name = "";
string cpuidVendor = "";
double clockSpeed = 0.0;

// Some constants
const string physicalId = "machdep.cpu.core_count: ";
const string cpuCores = "machdep.cpu.cores_per_package: ";
const string cpuClockSpeed = "hw.cpufrequency: ";
const string vendorId = "machdep.cpu.vendor: ";
const string modelId = "machdep.cpu.brand_string: ";
const string l1Name = "hw.l1icachesize: ";
const string l2Name = "hw.l2cachesize: ";

try
{
// First, get the vendor information from the SpecProber if not running on ARM
if (!PlatformHelper.IsOnArmOrArm64())
{
Initializer.InitializeNative();
cpuidVendor = Marshal.PtrToStringAnsi(ProcessorHelper.specprobe_get_vendor());
name = Marshal.PtrToStringAnsi(ProcessorHelper.specprobe_get_cpu_name());
}

// Then, fill the rest
string sysctlOutput = PlatformHelper.ExecuteProcessToString("/usr/sbin/sysctl", "machdep.cpu.core_count machdep.cpu.cores_per_package hw.cpufrequency machdep.cpu.vendor machdep.cpu.brand_string hw.l1icachesize hw.l2cachesize");
string[] sysctlOutputLines = sysctlOutput.Replace("\r", "").Split('\n');
foreach (string sysctlOutputLine in sysctlOutputLines)
{
if (sysctlOutputLine.StartsWith(physicalId))
numberOfCores = int.Parse(sysctlOutputLine[physicalId.Length..]);
if (sysctlOutputLine.StartsWith(cpuCores))
numberOfCoresForEachCore = int.Parse(sysctlOutputLine[cpuCores.Length..]);
if (sysctlOutputLine.StartsWith(cpuClockSpeed))
clockSpeed = double.Parse(sysctlOutputLine[cpuClockSpeed.Length..]) / 1000 / 1000;
if (sysctlOutputLine.StartsWith(vendorId) && string.IsNullOrEmpty(cpuidVendor))
cpuidVendor = sysctlOutputLine[vendorId.Length..];
if (sysctlOutputLine.StartsWith(modelId) && string.IsNullOrEmpty(name))
name = sysctlOutputLine[modelId.Length..];
if (sysctlOutputLine.StartsWith(l1Name))
cacheL1 = uint.Parse(sysctlOutputLine[l1Name.Length..]);
if (sysctlOutputLine.StartsWith(l2Name))
cacheL2 = uint.Parse(sysctlOutputLine[l2Name.Length..]);
}
}
catch (Exception ex)
{
HardwareProber.errors.Add(ex);
}

// Finally, return a single item array containing processor information
ProcessorPart processorPart = new()
{
ProcessorCores = numberOfCores,
CoresForEachCore = numberOfCoresForEachCore,
L1CacheSize = cacheL1,
L2CacheSize = cacheL2,
L3CacheSize = cacheL3,
Name = name,
CpuidVendor = cpuidVendor,
Speed = clockSpeed,
};
return new[] { processorPart };
}

public BaseHardwarePartInfo[] GetBaseHardwarePartsWindows()
Expand Down
99 changes: 98 additions & 1 deletion SpecProbe/Hardware/Probers/VideoProber.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,104 @@ public BaseHardwarePartInfo[] GetBaseHardwarePartsLinux()

public BaseHardwarePartInfo[] GetBaseHardwarePartsMacOS()
{
throw new NotImplementedException();
// Video card list
List<VideoPart> videos = new();

// Some tags
string videoCardNameTag = "Device ID:";
string videoCardVendorTag = "Vendor ID:";

// Some variables to install.
string videoCardName = "";
string videoCardDevName = "";
string videoCardVendor = "";

try
{
// Check notarization status
if (HardwareProber.notarized)
return GetBaseHardwarePartsMacOSNotarized();

// Probe the video cards
string sysctlOutput = PlatformHelper.ExecuteProcessToString("/usr/sbin/system_profiler", "SPDisplaysDataType");
string[] sysctlOutputLines = sysctlOutput.Replace("\r", "").Split('\n');
foreach (string sysctlOutputLine in sysctlOutputLines)
{
string line = sysctlOutputLine.Trim();
if (line.StartsWith(videoCardNameTag))
videoCardDevName = line[videoCardNameTag.Length..].Trim();
if (line.StartsWith(videoCardVendorTag))
videoCardVendor = line[videoCardVendorTag.Length..].Trim();
}
videoCardName =
$"V: {videoCardVendor} " +
$"M: {videoCardDevName}";
}
catch (Exception ex)
{
HardwareProber.errors.Add(ex);
}

// Finally, return a single item array containing information
videos.Add(new VideoPart
{
VideoCardName = videoCardName
});
return videos.ToArray();
}

public BaseHardwarePartInfo[] GetBaseHardwarePartsMacOSNotarized()
{
// Video card list
List<VideoPart> videos = new();

// Some variables to install.
string videoCardName;

try
{
// Check notarization status
if (!HardwareProber.notarized)
return GetBaseHardwarePartsMacOS();

// Probe the online displays
var status = PlatformMacInterop.CGGetOnlineDisplayList(uint.MaxValue, null, out uint displays);
if (status != PlatformMacInterop.CGError.kCGErrorSuccess)
throw new Exception(
$"CGGetOnlineDisplayList() probing part from Quartz failed: {status}\n" +
$"Check out https://developer.apple.com/documentation/coregraphics/cgerror/{status.ToString().ToLower()} for more info."
);

// Probe the screens
uint[] screens = new uint[displays];
status = PlatformMacInterop.CGGetOnlineDisplayList(uint.MaxValue, ref screens, out displays);
if (status != PlatformMacInterop.CGError.kCGErrorSuccess)
throw new Exception(
$"CGGetOnlineDisplayList() screen listing part from Quartz failed: {status}\n" +
$"Check out https://developer.apple.com/documentation/coregraphics/cgerror/{status.ToString().ToLower()} for more info."
);

// Probe the model and the vendor number as the video card name
foreach (var screen in screens)
{
videoCardName =
$"V: {PlatformMacInterop.CGDisplayVendorNumber(screen)} " +
$"M: {PlatformMacInterop.CGDisplayModelNumber(screen)}";

VideoPart part = new()
{
VideoCardName = videoCardName,
};
videos.Add(part);
}
}
catch (Exception ex)
{
HardwareProber.errors.Add(ex);
}

// Finally, return an array containing information
return videos.ToArray();
}

public BaseHardwarePartInfo[] GetBaseHardwarePartsWindows()
Expand Down
Loading

0 comments on commit 5149696

Please sign in to comment.