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 + } +}