diff --git a/docs/MachineInfo-EN.md b/docs/MachineInfo-EN.md new file mode 100644 index 0000000..a7a9495 --- /dev/null +++ b/docs/MachineInfo-EN.md @@ -0,0 +1,181 @@ +# MachineInfo Class Documentation + +## Overview + +The `MachineInfo` class is used to retrieve and store basic machine hardware and system information. It provides cross-platform functionality for gathering machine information, supporting Windows, Linux, and macOS operating systems. + +## Key Features + +### System Information +- **Operating System Info**: OS name and version +- **Hardware Info**: Processor model, manufacturer, product name +- **Identification Info**: Computer serial number, motherboard info, disk serial number + +## Usage Examples + +### Basic Usage + +```csharp +using LuYao.Devices; + +// Get machine information instance +var machineInfo = MachineInfo.Get(); + +// Access basic information +Console.WriteLine($"OS: {machineInfo.OSName}"); +Console.WriteLine($"Version: {machineInfo.OSVersion}"); +Console.WriteLine($"Processor: {machineInfo.Processor}"); +Console.WriteLine($"Vendor: {machineInfo.Vendor}"); +Console.WriteLine($"Product: {machineInfo.Product}"); +Console.WriteLine($"Serial: {machineInfo.Serial}"); +Console.WriteLine($"Board: {machineInfo.Board}"); +Console.WriteLine($"Disk ID: {machineInfo.DiskID}"); +``` + +### Reload Information + +```csharp +var machineInfo = MachineInfo.Get(); + +// Reload machine information +machineInfo.Reload(); + +// Access updated information +Console.WriteLine($"OS: {machineInfo.OSName}"); +``` + +### Complete Example: Display System Information + +```csharp +using System; +using LuYao.Devices; + +class Program +{ + static void Main() + { + var machineInfo = MachineInfo.Get(); + + Console.WriteLine("=== System Information ==="); + Console.WriteLine($"OS: {machineInfo.OSName}"); + Console.WriteLine($"Version: {machineInfo.OSVersion}"); + Console.WriteLine($"Processor: {machineInfo.Processor}"); + Console.WriteLine($"Vendor: {machineInfo.Vendor}"); + Console.WriteLine($"Product: {machineInfo.Product}"); + Console.WriteLine($"Serial: {machineInfo.Serial}"); + Console.WriteLine($"Board: {machineInfo.Board}"); + Console.WriteLine($"Disk ID: {machineInfo.DiskID}"); + } +} +``` + +## Property Reference + +| Property | Type | Description | +|----------|------|-------------| +| `OSName` | `string?` | Operating system name (e.g., "Windows 11", "Ubuntu 22.04") | +| `OSVersion` | `string?` | Operating system version number | +| `Product` | `string?` | Product name (e.g., "ThinkPad X1 Carbon") | +| `Vendor` | `string?` | Manufacturer (e.g., "Lenovo", "Dell", "Apple") | +| `Processor` | `string?` | Processor model (e.g., "Intel Core i7-10750H") | +| `Serial` | `string?` | Computer serial number, suitable for branded machines | +| `Board` | `string?` | Motherboard serial number or family information | +| `DiskID` | `string?` | Disk serial number | + +## Method Reference + +### Get() + +Static method to get and initialize a new `MachineInfo` instance. + +```csharp +var machineInfo = MachineInfo.Get(); +``` + +### Reload() + +Reload machine information. + +```csharp +machineInfo.Reload(); +``` + +## Platform Support + +### Windows +- Supports .NET Framework 4.5+ and .NET Core 3.0+ +- Gets hardware info via registry and WMIC +- Retrieves: OSName, OSVersion, Product, Vendor, Processor, Serial, Board, DiskID + +### Linux +- Supports .NET Core 3.0+ +- Reads `/proc/cpuinfo` for processor information +- Reads DMI information from `/sys/class/dmi/id/` +- Reads disk information from `/sys/block/` +- Retrieves: OSName, OSVersion, Product, Vendor, Processor, Serial, Board, DiskID + +### macOS +- Supports .NET Core 3.0+ +- Uses `system_profiler` for hardware information +- Retrieves: OSName, OSVersion, Product, Processor, Serial +- Vendor defaults to "Apple" + +## Important Notes + +1. **Performance Impact**: Some operations (like WMIC queries on Windows) may be time-consuming. Consider caching the `MachineInfo` instance. + +2. **Permission Requirements**: + - Windows: Some registry keys may require administrator privileges + - Linux: Reading some system files may require root permissions + - macOS: Some system commands may require appropriate permissions + +3. **Cross-platform Compatibility**: Not all properties are available on all platforms. Check for `null` or empty strings before use. For example: + - `Serial`, `Board`, `DiskID` may be empty on some VMs or specific hardware + - `Board` and `DiskID` may not be available on macOS + +## Best Practices + +1. **Singleton Pattern**: Use singleton pattern to cache `MachineInfo` instance and avoid repeated initialization. + +```csharp +public class SystemInfo +{ + private static readonly Lazy _instance = + new Lazy(() => MachineInfo.Get()); + + public static MachineInfo Instance => _instance.Value; +} +``` + +2. **Exception Handling**: Some operations may fail, handle exceptions appropriately. + +```csharp +try +{ + var machineInfo = MachineInfo.Get(); + Console.WriteLine($"OS: {machineInfo.OSName}"); +} +catch (Exception ex) +{ + Console.WriteLine($"Failed to get machine info: {ex.Message}"); +} +``` + +3. **Null Checks**: Some properties may be null, check before use. + +```csharp +var machineInfo = MachineInfo.Get(); + +if (!string.IsNullOrEmpty(machineInfo.Serial)) +{ + Console.WriteLine($"Serial: {machineInfo.Serial}"); +} +else +{ + Console.WriteLine("Serial number not available"); +} +``` + +## Reference + +This implementation is based on the MachineInfo implementation from the [NewLifeX](https://github.com/NewLifeX/X) project. diff --git a/docs/MachineInfo.md b/docs/MachineInfo.md new file mode 100644 index 0000000..b1537bc --- /dev/null +++ b/docs/MachineInfo.md @@ -0,0 +1,186 @@ +# MachineInfo 类文档 + +## 概述 + +`MachineInfo` 类用于获取和存储机器的基本硬件和系统信息。该类提供了跨平台的机器信息获取功能,支持 Windows、Linux 和 macOS 操作系统。 + +## 主要功能 + +### 系统信息 +- **操作系统信息**:操作系统名称和版本 +- **硬件信息**:处理器型号、制造商、产品名称 +- **标识信息**:计算机序列号、主板信息、磁盘序列号 + +## 使用示例 + +### 基本使用 + +```csharp +using LuYao.Devices; + +// 获取机器信息实例 +var machineInfo = MachineInfo.Get(); + +// 访问基本信息 +Console.WriteLine($"操作系统: {machineInfo.OSName}"); +Console.WriteLine($"系统版本: {machineInfo.OSVersion}"); +Console.WriteLine($"处理器: {machineInfo.Processor}"); +Console.WriteLine($"制造商: {machineInfo.Vendor}"); +Console.WriteLine($"产品名称: {machineInfo.Product}"); +Console.WriteLine($"序列号: {machineInfo.Serial}"); +Console.WriteLine($"主板: {machineInfo.Board}"); +Console.WriteLine($"磁盘ID: {machineInfo.DiskID}"); +``` + +### 重新加载信息 + +```csharp +var machineInfo = MachineInfo.Get(); + +// 重新加载机器信息 +machineInfo.Reload(); + +// 访问更新后的信息 +Console.WriteLine($"操作系统: {machineInfo.OSName}"); +``` + +### 完整示例:显示系统信息 + +```csharp +using System; +using LuYao.Devices; + +class Program +{ + static void Main() + { + var machineInfo = MachineInfo.Get(); + + Console.WriteLine("=== 系统信息 ==="); + Console.WriteLine($"操作系统: {machineInfo.OSName}"); + Console.WriteLine($"系统版本: {machineInfo.OSVersion}"); + Console.WriteLine($"处理器: {machineInfo.Processor}"); + Console.WriteLine($"制造商: {machineInfo.Vendor}"); + Console.WriteLine($"产品: {machineInfo.Product}"); + Console.WriteLine($"序列号: {machineInfo.Serial}"); + Console.WriteLine($"主板: {machineInfo.Board}"); + Console.WriteLine($"磁盘ID: {machineInfo.DiskID}"); + } +} +``` + +## 属性说明 + +| 属性 | 类型 | 说明 | +|------|------|------| +| `OSName` | `string?` | 操作系统名称(如 "Windows 11", "Ubuntu 22.04") | +| `OSVersion` | `string?` | 操作系统版本号 | +| `Product` | `string?` | 产品名称(如 "ThinkPad X1 Carbon") | +| `Vendor` | `string?` | 制造商(如 "Lenovo", "Dell", "Apple") | +| `Processor` | `string?` | 处理器型号(如 "Intel Core i7-10750H") | +| `Serial` | `string?` | 计算机序列号,适用于品牌机,跟笔记本标签显示一致 | +| `Board` | `string?` | 主板序列号或家族信息 | +| `DiskID` | `string?` | 磁盘序列号 | + +## 方法说明 + +### Get() + +静态方法,获取并初始化一个新的 `MachineInfo` 实例。 + +```csharp +var machineInfo = MachineInfo.Get(); +``` + +### Reload() + +重新加载机器信息。 + +```csharp +machineInfo.Reload(); +``` + +## 平台支持 + +### Windows +- 支持 .NET Framework 4.5+ 和 .NET Core 3.0+ +- 通过注册表和 WMIC 获取硬件信息 +- 获取:OSName, OSVersion, Product, Vendor, Processor, Serial, Board, DiskID + +### Linux +- 支持 .NET Core 3.0+ +- 读取 `/proc/cpuinfo` 获取处理器信息 +- 读取 `/sys/class/dmi/id/` 下的 DMI 信息 +- 读取 `/sys/block/` 获取磁盘信息 +- 获取:OSName, OSVersion, Product, Vendor, Processor, Serial, Board, DiskID + +### macOS +- 支持 .NET Core 3.0+ +- 使用 `system_profiler` 获取硬件信息 +- 获取:OSName, OSVersion, Product, Processor, Serial +- Vendor 默认为 "Apple" + +## 注意事项 + +1. **性能影响**:某些操作(如 Windows 上的 WMIC 查询)可能耗时较长,建议缓存 `MachineInfo` 实例。 + +2. **权限要求**: + - Windows:某些注册表项可能需要管理员权限 + - Linux:读取某些系统文件可能需要 root 权限 + - macOS:某些系统命令可能需要适当权限 + +3. **跨平台兼容性**:并非所有属性在所有平台上都可用。使用前应检查属性是否为 `null` 或空字符串。例如: + - `Serial`、`Board`、`DiskID` 在某些虚拟机或特定硬件上可能为空 + - macOS 上的 `Board` 和 `DiskID` 可能无法获取 + +## 最佳实践 + +1. **单例模式**:建议使用单例模式缓存 `MachineInfo` 实例,避免重复初始化。 + +```csharp +public class SystemInfo +{ + private static readonly Lazy _instance = + new Lazy(() => MachineInfo.Get()); + + public static MachineInfo Instance => _instance.Value; +} +``` + +2. **异常处理**:某些操作可能失败,应适当处理异常。 + +```csharp +try +{ + var machineInfo = MachineInfo.Get(); + Console.WriteLine($"系统: {machineInfo.OSName}"); +} +catch (Exception ex) +{ + Console.WriteLine($"获取机器信息失败: {ex.Message}"); +} +``` + +3. **空值检查**:某些属性可能为空,使用前应检查。 + +```csharp +var machineInfo = MachineInfo.Get(); + +if (!string.IsNullOrEmpty(machineInfo.Serial)) +{ + Console.WriteLine($"序列号: {machineInfo.Serial}"); +} +else +{ + Console.WriteLine("无法获取序列号"); +} +``` + +## 参考 + +本实现参考了 [NewLifeX](https://github.com/NewLifeX/X) 项目的 MachineInfo 实现。 + +## 相关文档 + +- [SizeHelper 文档](SizeHelper.md) - 用于格式化字节大小 +- [UnitConverter 文档](UnitConverter.md) - 单位转换工具 diff --git a/src/LuYao.Common/Devices/MachineInfo.Linux.cs b/src/LuYao.Common/Devices/MachineInfo.Linux.cs new file mode 100644 index 0000000..a6a86f3 --- /dev/null +++ b/src/LuYao.Common/Devices/MachineInfo.Linux.cs @@ -0,0 +1,360 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; + +#if NET5_0_OR_GREATER +using System.Runtime.Versioning; +#endif + +namespace LuYao.Devices; + +/// +/// MachineInfo 的 Linux 平台实现部分 +/// +public partial class MachineInfo +{ +#if NET5_0_OR_GREATER + [SupportedOSPlatform("linux")] +#endif + private void LoadLinuxInfo() + { + // 优先从 DMI 读取信息(更快且更可靠) + var hasDMI = TryReadDMIInfo(); + + // 只有在 DMI 信息不完整时才读取其他来源 + if (String.IsNullOrEmpty(Processor)) + { + var cpuinfo = ReadInfo("/proc/cpuinfo"); + if (cpuinfo != null) + { + if (cpuinfo.TryGetValue("Hardware", out var str) || + cpuinfo.TryGetValue("cpu model", out str) || + cpuinfo.TryGetValue("model name", out str)) + { + Processor = str; + if (Processor != null && Processor.StartsWith("vendor ")) + Processor = Processor.Substring(7); + } + + if (String.IsNullOrEmpty(Product) && cpuinfo.TryGetValue("Model", out str)) + Product = str; + + if (String.IsNullOrEmpty(Vendor) && cpuinfo.TryGetValue("vendor_id", out str)) + Vendor = str; + } + } + + // 获取 OS 名称 + if (String.IsNullOrEmpty(OSName)) + { + var str = GetLinuxName(); + if (!String.IsNullOrEmpty(str)) OSName = str; + } + + // 从 release 文件读取产品(仅在需要时) + if (String.IsNullOrEmpty(Product) && !hasDMI) + { + var prd = GetProductByRelease(); + if (!String.IsNullOrEmpty(prd)) Product = prd; + } + + // 获取磁盘序列号(仅在需要时) + if (String.IsNullOrEmpty(DiskID)) + { + TryReadDiskSerial(); + } + } + + /// 尝试读取 DMI 信息(一次性读取多个字段以提高性能) + /// 如果成功读取任何DMI信息则返回true + private Boolean TryReadDMIInfo() + { + const String DmiBasePath = "/sys/class/dmi/id/"; + + if (!Directory.Exists(DmiBasePath)) + return false; + + var hasData = false; + + // 读取产品名称 + if (TryReadDmiField(DmiBasePath, "product_name", out var productName)) + { + Product = productName; + hasData = true; + } + + // 读取系统供应商 + if (TryReadDmiField(DmiBasePath, "sys_vendor", out var sysVendor)) + { + Vendor = sysVendor; + hasData = true; + } + + // 读取主板序列号 + if (TryReadDmiField(DmiBasePath, "board_serial", out var boardSerial)) + { + Board = boardSerial; + hasData = true; + } + + // 读取产品序列号 + if (TryReadDmiField(DmiBasePath, "product_serial", out var productSerial)) + { + Serial = productSerial; + hasData = true; + } + + return hasData; + } + + /// 尝试读取DMI字段 + private static Boolean TryReadDmiField(String basePath, String fileName, out String? value) + { + return TryRead(Path.Combine(basePath, fileName), out value); + } + + /// 尝试读取磁盘序列号 + private void TryReadDiskSerial() + { + const String BlockDevicePath = "/sys/block/"; + + try + { + if (!Directory.Exists(BlockDevicePath)) + return; + + // 只检查物理磁盘(跳过循环设备等) + foreach (var diskPath in Directory.GetDirectories(BlockDevicePath)) + { + var diskName = Path.GetFileName(diskPath); + + if (!IsPhysicalDisk(diskName)) + continue; + + var serialFile = Path.Combine(diskPath, "device", "serial"); + if (TryRead(serialFile, out var diskSerial)) + { + DiskID = diskSerial; + return; // 找到第一个物理磁盘序列号即返回 + } + } + } + catch + { + // 忽略磁盘序列号读取错误 + } + } + + /// 判断是否为物理磁盘 + private static Boolean IsPhysicalDisk(String diskName) + { + return diskName.StartsWith("sd") || // SCSI/SATA磁盘 + diskName.StartsWith("nvme") || // NVMe磁盘 + diskName.StartsWith("hd"); // IDE磁盘 + } + + #region Linux辅助方法 + /// 获取Linux发行版名称 + /// Linux发行版名称 + private static String? GetLinuxName() + { + // 按优先级尝试各种方式获取OS名称 + return TryGetFromReleaseFiles() ?? TryGetFromUname(); + } + + /// 从发行版文件获取OS名称 + private static String? TryGetFromReleaseFiles() + { + // 尝试 RedHat 系列 + if (TryRead("/etc/redhat-release", out var value)) + return value; + + // 尝试 Debian 系列 + if (TryRead("/etc/debian-release", out value)) + return value; + + // 尝试通用 os-release 文件 + if (TryRead("/etc/os-release", out value)) + return ParseOsRelease(value); + + return null; + } + + /// 解析 os-release 文件内容 + private static String? ParseOsRelease(String content) + { + var dic = SplitAsDictionary(content, "=", "\n"); + + // 优先使用 PRETTY_NAME + if (dic.TryGetValue("PRETTY_NAME", out var pretty) && !String.IsNullOrEmpty(pretty)) + return pretty.Trim('"'); + + // 退而求其次使用 NAME + if (dic.TryGetValue("NAME", out var name) && !String.IsNullOrEmpty(name)) + return name.Trim('"'); + + return null; + } + + /// 从 uname 命令获取OS名称 + private static String? TryGetFromUname() + { + const Int32 UnameTimeoutMs = 2000; + + try + { + var psi = new ProcessStartInfo + { + FileName = "uname", + Arguments = "-sr", + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using var process = Process.Start(psi); + if (process == null) + return null; + + var uname = process.StandardOutput.ReadToEnd()?.Trim(); + + if (!process.WaitForExit(UnameTimeoutMs)) + { + try { process.Kill(); } catch { } + return null; + } + + if (String.IsNullOrEmpty(uname)) + return null; + + // 特殊处理:提取 Android 系统名 + return ExtractAndroidName(uname) ?? uname; + } + catch + { + return null; + } + } + + /// 从 uname 输出中提取 Android 系统名 + private static String? ExtractAndroidName(String uname) + { + var parts = uname.Split('-'); + foreach (var part in parts) + { + if (!String.IsNullOrEmpty(part) && + part.StartsWith("Android", StringComparison.OrdinalIgnoreCase)) + return part; + } + return null; + } + + private static String? GetProductByRelease() + { + try + { + if (!Directory.Exists("/etc/")) return null; + + var files = Directory.GetFiles("/etc/", "*-release"); + foreach (var file in files) + { + var fileName = Path.GetFileName(file); + if (!fileName.Equals("redhat-release", StringComparison.OrdinalIgnoreCase) && + !fileName.Equals("debian-release", StringComparison.OrdinalIgnoreCase) && + !fileName.Equals("os-release", StringComparison.OrdinalIgnoreCase) && + !fileName.Equals("system-release", StringComparison.OrdinalIgnoreCase)) + { + var content = File.ReadAllText(file); + var dic = SplitAsDictionary(content, "=", "\n"); + if (dic.TryGetValue("BOARD", out var str)) return str; + if (dic.TryGetValue("BOARD_NAME", out str)) return str; + } + } + } + catch + { + // 忽略错误 + } + + return null; + } + + private static Boolean TryRead(String fileName, out String? value) + { + value = null; + + if (!File.Exists(fileName)) return false; + + try + { + value = File.ReadAllText(fileName)?.Trim(); + if (String.IsNullOrEmpty(value)) return false; + } + catch { return false; } + + return true; + } + + /// 读取文件信息,分割为字典 + /// 文件路径 + /// 分隔符 + /// 解析后的字典 + private static Dictionary? ReadInfo(String file, Char separate = ':') + { + if (String.IsNullOrEmpty(file) || !File.Exists(file)) return null; + + var dic = new Dictionary(StringComparer.OrdinalIgnoreCase); + + try + { + using var reader = new StreamReader(file); + while (!reader.EndOfStream) + { + var line = reader.ReadLine(); + if (line != null) + { + var p = line.IndexOf(separate); + if (p > 0) + { + var key = line.Substring(0, p).Trim(); + var value = line.Substring(p + 1).Trim(); + dic[key] = Clean(value); + } + } + } + } + catch + { + return null; + } + + return dic; + } + + private static Dictionary SplitAsDictionary(String content, String keyValueSeparator, String lineSeparator) + { + var dic = new Dictionary(StringComparer.OrdinalIgnoreCase); + + if (String.IsNullOrEmpty(content)) return dic; + + var lines = content.Split(new[] { lineSeparator }, StringSplitOptions.RemoveEmptyEntries); + foreach (var line in lines) + { + var parts = line.Split(new[] { keyValueSeparator }, 2, StringSplitOptions.None); + if (parts.Length == 2) + { + var key = parts[0].Trim(); + var value = parts[1].Trim().Trim('"'); + if (!String.IsNullOrEmpty(key)) + dic[key] = value; + } + } + + return dic; + } + #endregion +} diff --git a/src/LuYao.Common/Devices/MachineInfo.MacOS.cs b/src/LuYao.Common/Devices/MachineInfo.MacOS.cs new file mode 100644 index 0000000..9b7977c --- /dev/null +++ b/src/LuYao.Common/Devices/MachineInfo.MacOS.cs @@ -0,0 +1,190 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; + +#if NET5_0_OR_GREATER +using System.Runtime.Versioning; +#endif + +namespace LuYao.Devices; + +/// +/// MachineInfo 的 macOS 平台实现部分 +/// +public partial class MachineInfo +{ +#if NET5_0_OR_GREATER + [SupportedOSPlatform("macos")] +#endif + private void LoadMacInfo() + { + try + { + // 一次性获取硬件和软件信息以提高性能 + var output = ExecuteCommand("system_profiler", "SPHardwareDataType SPSoftwareDataType"); + if (!String.IsNullOrEmpty(output)) + { + ParseSystemProfilerOutput(output); + } + + // macOS 设备供应商总是 Apple + Vendor = "Apple"; + } + catch + { + // 忽略错误 + } + + // 使用后备方案获取OS名称 + if (String.IsNullOrEmpty(OSName)) + { + OSName = GetFallbackOSName(); + } + } + + /// 解析 system_profiler 命令输出 + private void ParseSystemProfilerOutput(String output) + { + var lines = output.Split('\n'); + var currentSection = ProfilerSection.None; + + foreach (var line in lines) + { + var trimmed = line.Trim(); + + // 检测区域切换 + currentSection = DetectSection(line, currentSection); + + // 根据当前区域解析信息 + switch (currentSection) + { + case ProfilerSection.Hardware: + ParseHardwareInfo(trimmed); + break; + case ProfilerSection.Software: + ParseSoftwareInfo(trimmed); + break; + } + } + } + + /// 检测 system_profiler 输出的区域 + private static ProfilerSection DetectSection(String line, ProfilerSection current) + { + if (line.Contains("Hardware:")) + return ProfilerSection.Hardware; + if (line.Contains("Software:")) + return ProfilerSection.Software; + return current; + } + + /// 解析硬件信息行 + private void ParseHardwareInfo(String trimmed) + { + if (TryExtractValue(trimmed, "Model Name:", out var modelName)) + Product = modelName; + else if (String.IsNullOrEmpty(Product) && TryExtractValue(trimmed, "Model Identifier:", out var modelId)) + Product = modelId; + else if (TryExtractValue(trimmed, "Processor Name:", out var processorName)) + Processor = processorName; + else if (String.IsNullOrEmpty(Processor) && TryExtractValue(trimmed, "Chip:", out var chip)) + Processor = chip; + else if (TryExtractValue(trimmed, "Serial Number (system):", out var serial)) + Serial = serial; + } + + /// 解析软件信息行 + private void ParseSoftwareInfo(String trimmed) + { + if (TryExtractValue(trimmed, "System Version:", out var systemVersion)) + { + OSName = systemVersion; + OSVersion = ExtractVersionNumber(systemVersion); + } + } + + /// 尝试从行中提取键值对 + private static Boolean TryExtractValue(String line, String prefix, out String value) + { + value = ""; + if (!line.StartsWith(prefix)) + return false; + + value = line.Substring(prefix.Length).Trim(); + return true; + } + + /// 从系统版本字符串中提取版本号 + private static String ExtractVersionNumber(String systemVersion) + { + var versionStart = systemVersion.IndexOf('('); + if (versionStart > 0) + return systemVersion.Substring(0, versionStart).Trim(); + return systemVersion; + } + + /// 获取后备OS名称 + private static String GetFallbackOSName() + { +#if NETFRAMEWORK + return Environment.OSVersion.Platform.ToString(); +#else + return RuntimeInformation.OSDescription; +#endif + } + + /// system_profiler 输出区域 + private enum ProfilerSection + { + None, + Hardware, + Software + } + + #region macOS辅助方法 + /// 执行命令并返回标准输出 + /// 命令名称 + /// 命令参数 + /// 命令输出,失败则返回null + private static String? ExecuteCommand(String command, String arguments) + { + const Int32 SystemProfilerTimeoutMs = 10000; // system_profiler 较慢,需要更长超时 + + try + { + var psi = new ProcessStartInfo + { + FileName = command, + Arguments = arguments, + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using var process = Process.Start(psi); + if (process == null) + return null; + + var output = process.StandardOutput.ReadToEnd(); + + // 等待进程结束或超时 + if (!process.WaitForExit(SystemProfilerTimeoutMs)) + { + try { process.Kill(); } catch { } + return null; + } + + return output; + } + catch + { + // 忽略错误 + } + + return null; + } + #endregion +} diff --git a/src/LuYao.Common/Devices/MachineInfo.Windows.cs b/src/LuYao.Common/Devices/MachineInfo.Windows.cs new file mode 100644 index 0000000..ef9c57b --- /dev/null +++ b/src/LuYao.Common/Devices/MachineInfo.Windows.cs @@ -0,0 +1,248 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; + +#if NET5_0_OR_GREATER +using System.Runtime.Versioning; +#endif + +#if NETFRAMEWORK || NET5_0_OR_GREATER +using Microsoft.Win32; +#endif + +namespace LuYao.Devices; + +/// +/// MachineInfo 的 Windows 平台实现部分 +/// +public partial class MachineInfo +{ +#if NET5_0_OR_GREATER + [SupportedOSPlatform("windows")] +#endif + private void LoadWindowsInfo() + { + var str = ""; + + // 从注册表读取硬件信息 +#if NETFRAMEWORK || NET6_0_OR_GREATER + try + { + var reg = Registry.LocalMachine.OpenSubKey(@"HARDWARE\DESCRIPTION\System\BIOS"); + reg ??= Registry.LocalMachine.OpenSubKey(@"SYSTEM\HardwareConfig\Current"); + if (reg != null) + { + Product = (reg.GetValue("SystemProductName")?.ToString() ?? "").Replace("System Product Name", ""); + if (String.IsNullOrEmpty(Product)) Product = reg.GetValue("BaseBoardProduct")?.ToString() ?? ""; + + Vendor = reg.GetValue("SystemManufacturer")?.ToString() ?? ""; + if (String.IsNullOrEmpty(Vendor)) Vendor = reg.GetValue("BaseBoardManufacturer")?.ToString() ?? ""; + } + + reg = Registry.LocalMachine.OpenSubKey(@"HARDWARE\DESCRIPTION\System\CentralProcessor\0"); + if (reg != null) Processor = reg.GetValue("ProcessorNameString")?.ToString() ?? ""; + } + catch + { + // 忽略注册表访问错误 + } +#endif + + // 获取操作系统名称和版本 + try + { +#if NETFRAMEWORK || NET6_0_OR_GREATER + var reg = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion"); + if (reg != null) + { + OSName = reg.GetValue("ProductName")?.ToString() ?? ""; + var releaseId = reg.GetValue("ReleaseId")?.ToString() ?? ""; + if (!String.IsNullOrEmpty(releaseId)) + OSVersion = releaseId; + } +#endif + } + catch + { + // 使用默认值 + } + + // 一次性通过 WMIC 批量获取所有信息以提高性能 + var wmicNeeded = String.IsNullOrEmpty(Vendor) || String.IsNullOrEmpty(Product) || + String.IsNullOrEmpty(OSName) || String.IsNullOrEmpty(Serial) || + String.IsNullOrEmpty(Board) || String.IsNullOrEmpty(DiskID); + + if (wmicNeeded) + { + var wmicData = ReadWmicBatch(); + + // 处理 csproduct 数据 + if (wmicData.TryGetValue("csproduct", out var csproduct)) + { + if (csproduct.TryGetValue("Name", out str) && !String.IsNullOrEmpty(str) && String.IsNullOrEmpty(Product)) + Product = str; + if (csproduct.TryGetValue("Vendor", out str) && !String.IsNullOrEmpty(str) && String.IsNullOrEmpty(Vendor)) + Vendor = str; + } + + // 处理 OS 数据 + if (wmicData.TryGetValue("os", out var os)) + { + if (String.IsNullOrEmpty(OSName) && os.TryGetValue("Caption", out str)) + OSName = str.Replace("Microsoft", "").Trim(); + if (os.TryGetValue("Version", out str)) + OSVersion = str; + } + + // 处理磁盘数据 + if (wmicData.TryGetValue("diskdrive", out var disk) && disk.TryGetValue("SerialNumber", out str)) + DiskID = str?.Trim(); + + // 处理 BIOS 序列号 + if (wmicData.TryGetValue("bios", out var bios) && bios.TryGetValue("SerialNumber", out str) && + !str.Equals("System Serial Number", StringComparison.OrdinalIgnoreCase)) + Serial = str?.Trim(); + + // 处理主板序列号 + if (wmicData.TryGetValue("baseboard", out var board) && board.TryGetValue("SerialNumber", out str)) + Board = str?.Trim(); + } + + if (String.IsNullOrEmpty(OSName)) + { +#if NETFRAMEWORK + OSName = Environment.OSVersion.Platform.ToString().Replace("Microsoft", "").Trim(); +#else + OSName = RuntimeInformation.OSDescription.Replace("Microsoft", "").Trim(); +#endif + } + if (String.IsNullOrEmpty(OSVersion)) + OSVersion = Environment.OSVersion.Version.ToString(); + } + + #region Windows辅助方法 + /// 批量读取 WMIC 信息以提高性能 + /// 包含所有 WMI 数据的嵌套字典,键为WMI类名 + private static Dictionary> ReadWmicBatch() + { + var result = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + // 读取计算机系统产品信息 + AddWmicData(result, "csproduct", ReadWmic("csproduct", "Name", "Vendor")); + + // 读取操作系统信息 + AddWmicData(result, "os", ReadWmic("os", "Caption", "Version")); + + // 读取磁盘驱动器信息 + AddWmicData(result, "diskdrive", ReadWmic("diskdrive where mediatype=\"Fixed hard disk media\"", "SerialNumber")); + + // 读取BIOS信息 + AddWmicData(result, "bios", ReadWmic("bios", "SerialNumber")); + + // 读取主板信息 + AddWmicData(result, "baseboard", ReadWmic("baseboard", "SerialNumber")); + + return result; + } + + /// 将WMIC数据添加到结果字典 + private static void AddWmicData(Dictionary> result, String key, Dictionary data) + { + if (data.Count > 0) + result[key] = data; + } + + /// 通过WMIC命令读取信息 + /// WMI类型 + /// 查询字段 + /// 解析后的字典 + private static Dictionary ReadWmic(String type, params String[] keys) + { + const Int32 WmicTimeoutMilliseconds = 5000; + + var rawData = new Dictionary>(StringComparer.OrdinalIgnoreCase); + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + + try + { + var output = ExecuteWmicCommand(type, keys, WmicTimeoutMilliseconds); + if (String.IsNullOrEmpty(output)) + return result; + + ParseWmicOutput(output, rawData); + ConsolidateWmicData(rawData, result); + } + catch + { + // 忽略错误,返回空结果 + } + + return result; + } + + /// 执行WMIC命令并返回输出 + private static String? ExecuteWmicCommand(String type, String[] keys, Int32 timeoutMs) + { + var args = $"{type} get {String.Join(",", keys)} /format:list"; + var psi = new ProcessStartInfo + { + FileName = "wmic", + Arguments = args, + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using var process = Process.Start(psi); + if (process == null) + return null; + + var output = process.StandardOutput.ReadToEnd(); + + if (!process.WaitForExit(timeoutMs)) + { + try { process.Kill(); } catch { } + return null; + } + + return output; + } + + /// 解析WMIC输出为键值对 + private static void ParseWmicOutput(String output, Dictionary> rawData) + { + var lines = output.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None); + + foreach (var line in lines) + { + var parts = line.Split('='); + if (parts.Length < 2) + continue; + + var key = parts[0].Trim(); + var value = Clean(parts[1].Trim()) ?? ""; + + if (String.IsNullOrEmpty(key) || String.IsNullOrEmpty(value)) + continue; + + if (!rawData.TryGetValue(key, out var list)) + rawData[key] = list = new List(); + + list.Add(value); + } + } + + /// 合并多个值的WMIC数据(如多个磁盘) + private static void ConsolidateWmicData(Dictionary> rawData, Dictionary result) + { + foreach (var item in rawData) + { + // 排序以保证一致性,用逗号连接多个值 + result[item.Key] = String.Join(",", item.Value.OrderBy(e => e)); + } + } + #endregion +} diff --git a/src/LuYao.Common/Devices/MachineInfo.cs b/src/LuYao.Common/Devices/MachineInfo.cs index caee936..2ac25dd 100644 --- a/src/LuYao.Common/Devices/MachineInfo.cs +++ b/src/LuYao.Common/Devices/MachineInfo.cs @@ -1,83 +1,173 @@ using System; using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; using System.Linq; +using System.Net.NetworkInformation; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; namespace LuYao.Devices; +/// +/// 机器信息类,用于获取和存储机器的硬件和系统信息 +/// +/// +/// 该类提供了跨平台的机器信息获取功能,包括操作系统信息、硬件标识、性能指标等。 +/// 支持 Windows、Linux 和 macOS 操作系统。 +/// 刷新信息成本较高,建议采用单例模式或缓存机制。 +/// public partial class MachineInfo { - public static MachineInfo Get() - { - var info = new MachineInfo(); - info.Reload(); - return info; - } + #region 属性 /// 系统名称 + [DisplayName("系统名称")] public String? OSName { get; set; } /// 系统版本 + [DisplayName("系统版本")] public String? OSVersion { get; set; } /// 产品名称 + [DisplayName("产品名称")] public String? Product { get; set; } /// 制造商 + [DisplayName("制造商")] public String? Vendor { get; set; } /// 处理器型号 + [DisplayName("处理器型号")] public String? Processor { get; set; } + /// 计算机序列号。适用于品牌机,跟笔记本标签显示一致 + [DisplayName("计算机序列号")] public String? Serial { get; set; } /// 主板。序列号或家族信息 + [DisplayName("主板")] public String? Board { get; set; } /// 磁盘序列号 + [DisplayName("磁盘序列号")] public String? DiskID { get; set; } - private void Reset() + #endregion + + #region 静态方法 + /// + /// 获取机器信息的静态实例 + /// + /// 初始化后的机器信息实例 + public static MachineInfo Get() { - this.OSName = Environment.OSVersion.Platform.ToString(); - this.OSVersion = Environment.OSVersion.VersionString; + var info = new MachineInfo(); + info.Init(); + return info; } - public void Reload() + #endregion + + #region 初始化和刷新 + /// + /// 初始化机器信息,加载静态硬件信息 + /// + public void Init() { - this.Reset(); + Reset(); + +#if NETFRAMEWORK + // .NET Framework only runs on Windows + LoadWindowsInfo(); +#else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - MachineInfoWindows.Reload(this); + LoadWindowsInfo(); } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - MachineInfoLinux.Reload(this); + LoadLinuxInfo(); } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - MachineInfoMacOS.Reload(this); + LoadMacInfo(); } +#endif + + // 清理数据 + OSName = Clean(OSName); + OSVersion = Clean(OSVersion); + Product = Clean(Product); + Vendor = Clean(Vendor); + Processor = Clean(Processor); + Serial = Clean(Serial); + Board = Clean(Board); + DiskID = Clean(DiskID); } - private static class MachineInfoWindows + /// + /// 重新加载机器信息 + /// + public void Reload() { - public static void Reload(MachineInfo info) - { - throw new NotImplementedException(); - } + Init(); } - private static class MachineInfoLinux + + private void Reset() { - public static void Reload(MachineInfo info) +#if NETFRAMEWORK + OSName = Environment.OSVersion.Platform.ToString(); + OSVersion = Environment.OSVersion.VersionString; +#else + OSName = RuntimeInformation.OSDescription; + OSVersion = Environment.OSVersion.VersionString; +#endif + } + + /// 裁剪不可见字符并去除两端空白 + /// 待清理的字符串 + /// 清理后的字符串 + private static String? Clean(String? value) + { + if (String.IsNullOrEmpty(value)) + return value; + + // 快速路径:检查是否包含控制字符 + if (!ContainsControlCharacters(value)) + return value.Trim(); + + // 慢速路径:清理控制字符后返回 + return RemoveControlCharacters(value).Trim(); + } + + /// 检查字符串是否包含控制字符 + private static Boolean ContainsControlCharacters(String value) + { + const Int32 MinPrintableChar = 32; + const Int32 DeleteChar = 127; + + foreach (var c in value) { - throw new NotImplementedException(); + if (c < MinPrintableChar || c == DeleteChar) + return true; } + return false; } - private static class MachineInfoMacOS + + /// 移除字符串中的控制字符 + private static String RemoveControlCharacters(String value) { - public static void Reload(MachineInfo info) + const Int32 MinPrintableChar = 32; + const Int32 DeleteChar = 127; + + var sb = new StringBuilder(value.Length); + foreach (var c in value) { - throw new NotImplementedException(); + if (c >= MinPrintableChar && c != DeleteChar) + sb.Append(c); } + return sb.ToString(); } + #endregion + } diff --git a/tests/LuYao.Common.UnitTests/Devices/MachineInfoTests.cs b/tests/LuYao.Common.UnitTests/Devices/MachineInfoTests.cs new file mode 100644 index 0000000..8d098ad --- /dev/null +++ b/tests/LuYao.Common.UnitTests/Devices/MachineInfoTests.cs @@ -0,0 +1,115 @@ +using System; +using LuYao.Devices; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace LuYao.Common.UnitTests.Devices; + +/// +/// MachineInfo 类的单元测试 +/// +[TestClass] +public class MachineInfoTests +{ + [TestMethod] + public void Get_ShouldReturnMachineInfo() + { + // Act + var machineInfo = MachineInfo.Get(); + + // Assert + Assert.IsNotNull(machineInfo); + } + + [TestMethod] + public void OSName_ShouldNotBeNullOrEmpty() + { + // Arrange + var machineInfo = MachineInfo.Get(); + + // Assert + Assert.IsFalse(String.IsNullOrEmpty(machineInfo.OSName), "OSName should not be null or empty"); + } + + [TestMethod] + public void OSVersion_ShouldNotBeNullOrEmpty() + { + // Arrange + var machineInfo = MachineInfo.Get(); + + // Assert + Assert.IsFalse(String.IsNullOrEmpty(machineInfo.OSVersion), "OSVersion should not be null or empty"); + } + + [TestMethod] + public void Processor_CanBeRead() + { + // Arrange + var machineInfo = MachineInfo.Get(); + + // Assert - Processor might be null on some platforms, just verify it doesn't throw + var processor = machineInfo.Processor; + Assert.IsTrue(true); + } + + [TestMethod] + public void Reload_ShouldNotThrow() + { + // Arrange + var machineInfo = MachineInfo.Get(); + var originalOSName = machineInfo.OSName; + + // Act + machineInfo.Reload(); + + // Assert - OSName should still be set after reload + Assert.IsFalse(String.IsNullOrEmpty(machineInfo.OSName), "OSName should not be null or empty after reload"); + } + + [TestMethod] + public void AllBasicProperties_CanBeAccessed() + { + // Arrange + var machineInfo = MachineInfo.Get(); + + // Act & Assert - All properties should be accessible + var osName = machineInfo.OSName; + var osVersion = machineInfo.OSVersion; + var product = machineInfo.Product; + var vendor = machineInfo.Vendor; + var processor = machineInfo.Processor; + var serial = machineInfo.Serial; + var board = machineInfo.Board; + var diskId = machineInfo.DiskID; + + // Test passes if we get here without exceptions + Assert.IsTrue(true); + } + + [TestMethod] + public void Product_MayBeNullOnSomePlatforms() + { + // Arrange + var machineInfo = MachineInfo.Get(); + + // Act + var product = machineInfo.Product; + + // Assert - Product can be null on some platforms (e.g., virtual machines) + // Just verify it doesn't throw + Assert.IsTrue(true); + } + + [TestMethod] + public void Serial_MayBeNullOnSomePlatforms() + { + // Arrange + var machineInfo = MachineInfo.Get(); + + // Act + var serial = machineInfo.Serial; + + // Assert - Serial can be null on some platforms + // Just verify it doesn't throw + Assert.IsTrue(true); + } +}