From 5149696e1c0a70db2410004bc573c71f3d3b5f79 Mon Sep 17 00:00:00 2001 From: Aptivi CEO Date: Mon, 13 Nov 2023 14:05:30 +0300 Subject: [PATCH] add - doc - Added full hardware probing support for macOS --- 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 --- SpecProbe.ConsoleTest/Program.cs | 1 + SpecProbe/Hardware/HardwareProber.cs | 8 ++ SpecProbe/Hardware/Probers/HardDiskProber.cs | 132 +++++++++++++++++- SpecProbe/Hardware/Probers/MemoryProber.cs | 33 ++++- SpecProbe/Hardware/Probers/ProcessorProber.cs | 69 ++++++++- SpecProbe/Hardware/Probers/VideoProber.cs | 99 ++++++++++++- SpecProbe/Platform/PlatformMacInterop.cs | 102 ++++++++++++++ 7 files changed, 440 insertions(+), 4 deletions(-) create mode 100644 SpecProbe/Platform/PlatformMacInterop.cs diff --git a/SpecProbe.ConsoleTest/Program.cs b/SpecProbe.ConsoleTest/Program.cs index 142c0e7..12eefd6 100644 --- a/SpecProbe.ConsoleTest/Program.cs +++ b/SpecProbe.ConsoleTest/Program.cs @@ -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); } } } diff --git a/SpecProbe/Hardware/HardwareProber.cs b/SpecProbe/Hardware/HardwareProber.cs index 21043ef..cae83bd 100644 --- a/SpecProbe/Hardware/HardwareProber.cs +++ b/SpecProbe/Hardware/HardwareProber.cs @@ -29,6 +29,7 @@ namespace SpecProbe.Hardware /// public static class HardwareProber { + internal static bool notarized = false; internal static List errors = new(); private static ProcessorPart[] cachedProcessors; private static MemoryPart[] cachedMemory; @@ -69,6 +70,13 @@ public static class HardwareProber public static Exception[] Errors => errors.ToArray(); + /// + /// For Apple's code signing. + /// + /// If your application is using hardened macOS runtime, set this to true. + public static void SetNotarized(bool notarized) => + HardwareProber.notarized = notarized; + private static ProcessorPart[] ProbeProcessors() { // Get the base part class instances from the part prober diff --git a/SpecProbe/Hardware/Probers/HardDiskProber.cs b/SpecProbe/Hardware/Probers/HardDiskProber.cs index dc38747..ec31e52 100644 --- a/SpecProbe/Hardware/Probers/HardDiskProber.cs +++ b/SpecProbe/Hardware/Probers/HardDiskProber.cs @@ -122,7 +122,137 @@ public BaseHardwarePartInfo[] GetBaseHardwarePartsLinux() public BaseHardwarePartInfo[] GetBaseHardwarePartsMacOS() { - throw new NotImplementedException(); + // Some variables to install. + List diskParts = new(); + List partitions = new(); + + // Get the blocks + try + { + List 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() diff --git a/SpecProbe/Hardware/Probers/MemoryProber.cs b/SpecProbe/Hardware/Probers/MemoryProber.cs index a0d7514..909dcea 100644 --- a/SpecProbe/Hardware/Probers/MemoryProber.cs +++ b/SpecProbe/Hardware/Probers/MemoryProber.cs @@ -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() diff --git a/SpecProbe/Hardware/Probers/ProcessorProber.cs b/SpecProbe/Hardware/Probers/ProcessorProber.cs index 38a7d98..6a7f900 100644 --- a/SpecProbe/Hardware/Probers/ProcessorProber.cs +++ b/SpecProbe/Hardware/Probers/ProcessorProber.cs @@ -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() diff --git a/SpecProbe/Hardware/Probers/VideoProber.cs b/SpecProbe/Hardware/Probers/VideoProber.cs index c400475..48122fd 100644 --- a/SpecProbe/Hardware/Probers/VideoProber.cs +++ b/SpecProbe/Hardware/Probers/VideoProber.cs @@ -108,7 +108,104 @@ public BaseHardwarePartInfo[] GetBaseHardwarePartsLinux() public BaseHardwarePartInfo[] GetBaseHardwarePartsMacOS() { - throw new NotImplementedException(); + // Video card list + List 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 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() diff --git a/SpecProbe/Platform/PlatformMacInterop.cs b/SpecProbe/Platform/PlatformMacInterop.cs new file mode 100644 index 0000000..400ef48 --- /dev/null +++ b/SpecProbe/Platform/PlatformMacInterop.cs @@ -0,0 +1,102 @@ + +// SpecProbe Copyright (C) 2020-2021 Aptivi +// +// This file is part of SpecProbe +// +// SpecProbe is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// SpecProbe is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +using System; +using System.Runtime.InteropServices; + +namespace SpecProbe.Platform +{ + internal static unsafe class PlatformMacInterop + { + #region System Framework Paths + const string cgFrameworkPath = "/System/Library/Frameworks/ApplicationServices.framework/Frameworks/CoreGraphics.framework/CoreGraphics"; + #endregion + + #region Video adapter macOS API pinvokes + /// + /// CGError CGGetOnlineDisplayList(uint32_t maxDisplays, CGDirectDisplayID *onlineDisplays, uint32_t *displayCount); + /// + [DllImport(cgFrameworkPath)] + public static extern CGError CGGetOnlineDisplayList(uint maxDisplays, uint[] onlineDisplays, out uint displayCount); + + /// + /// CGError CGGetOnlineDisplayList(uint32_t maxDisplays, CGDirectDisplayID *onlineDisplays, uint32_t *displayCount); + /// + [DllImport(cgFrameworkPath)] + public static extern CGError CGGetOnlineDisplayList(uint maxDisplays, ref uint[] onlineDisplays, out uint displayCount); + + /// + /// uint32_t CGDisplayModelNumber(CGDirectDisplayID display); + /// + [DllImport(cgFrameworkPath)] + public static extern uint CGDisplayModelNumber(uint display); + + /// + /// uint32_t CGDisplayVendorNumber(CGDirectDisplayID display); + /// + [DllImport(cgFrameworkPath)] + public static extern uint CGDisplayVendorNumber(uint display); + #endregion + + #region Common + internal enum CGError + { + /// + /// The requested operation is inappropriate for the parameters passed in, or the current system state. + /// + kCGErrorCannotComplete = 1004, + /// + /// A general failure occurred. + /// + kCGErrorFailure = 1000, + /// + /// One or more of the parameters passed to a function is invalid. Check for pointers. + /// + kCGErrorIllegalArgument = 1001, + /// + /// The parameter representing a connection to the window server is invalid. + /// + kCGErrorInvalidConnection = 1002, + /// + /// The CPSProcessSerNum or context identifier parameter is not valid. + /// + kCGErrorInvalidContext = 1003, + /// + /// The requested operation is not valid for the parameters passed in, or the current system state. + /// + kCGErrorInvalidOperation = 1010, + /// + /// The requested operation could not be completed as the indicated resources were not found. + /// + kCGErrorNoneAvailable = 1011, + /// + /// A parameter passed in has a value that is inappropriate, or which does not map to a useful operation or value. + /// + kCGErrorRangeCheck = 1007, + /// + /// The requested operation was completed successfully. + /// + kCGErrorSuccess = 0, + /// + /// A data type or token was encountered that did not match the expected type or token. + /// + kCGTypeCheck = 1008, + } + #endregion + } +}