重构 MachineInfo 平台实现为局部部分类,改进GetInfo对WMI的支持。 将 MachineInfo 各平台实现拆分为独立 partial class 文件(Windows、Linux、macOS、容器检测等),主文件仅保留通用逻辑和接口定义。平台相关实现分别迁移至对应文件,提升代码结构清晰度和可维护性,便于后续扩展和维护。石头 authored at 2026-07-26 09:18:36
diff --git a/NewLife.Core/Common/MachineInfo.Container.cs b/NewLife.Core/Common/MachineInfo.Container.cs
new file mode 100644
index 0000000..4b7ebc3
--- /dev/null
+++ b/NewLife.Core/Common/MachineInfo.Container.cs
@@ -0,0 +1,139 @@
+namespace NewLife;
+
+/// <summary>机器信息容器检测部分</summary>
+public partial class MachineInfo
+{
+ #region 容器检测
+ /// <summary>是否运行在容器中</summary>
+ /// <remarks>
+ /// 检测依据:
+ /// 1. 环境变量 DOTNET_RUNNING_IN_CONTAINER
+ /// 2. 存在 /.dockerenv 文件
+ /// 3. /proc/1/cgroup 包含 docker/kubepods/containerd 等关键字
+ /// </remarks>
+ public static Boolean IsInContainer
+ {
+ get
+ {
+ // 环境变量检测
+ var env = Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER");
+ if (env.EqualIgnoreCase("true", "1")) return true;
+
+ if (!Runtime.Linux) return false;
+
+ // Docker 环境文件检测
+ if (File.Exists("/.dockerenv")) return true;
+
+ // cgroup 检测
+ var cgroupFile = "/proc/1/cgroup";
+ if (File.Exists(cgroupFile))
+ {
+ try
+ {
+ var content = File.ReadAllText(cgroupFile);
+ if (content.Contains("docker") ||
+ content.Contains("kubepods") ||
+ content.Contains("containerd") ||
+ content.Contains("lxc"))
+ return true;
+ }
+ catch { }
+ }
+
+ return false;
+ }
+ }
+
+ /// <summary>获取容器资源限制</summary>
+ /// <returns>元组:(内存限制字节数, CPU核数限制)。返回null表示无限制或获取失败</returns>
+ /// <remarks>
+ /// 读取 cgroup v1/v2 的资源限制配置:
+ /// - 内存限制:/sys/fs/cgroup/memory/memory.limit_in_bytes (v1) 或 /sys/fs/cgroup/memory.max (v2)
+ /// - CPU限制:/sys/fs/cgroup/cpu/cpu.cfs_quota_us 和 cpu.cfs_period_us (v1) 或 /sys/fs/cgroup/cpu.max (v2)
+ /// </remarks>
+ public static (Int64? MemoryLimit, Double? CpuLimit) GetContainerLimits()
+ {
+ Int64? memoryLimit = null;
+ Double? cpuLimit = null;
+
+ if (!Runtime.Linux) return (memoryLimit, cpuLimit);
+
+ // 尝试读取内存限制
+ // cgroup v2
+ var memFile = "/sys/fs/cgroup/memory.max";
+ if (File.Exists(memFile))
+ {
+ if (TryRead(memFile, out var value) && value != "max")
+ memoryLimit = value.ToLong();
+ }
+ else
+ {
+ // cgroup v1
+ memFile = "/sys/fs/cgroup/memory/memory.limit_in_bytes";
+ if (File.Exists(memFile) && TryRead(memFile, out var value))
+ {
+ var limit = value.ToLong();
+ // 如果接近系统最大值(通常是一个很大的数),则认为无限制
+ if (limit > 0 && limit < 9_000_000_000_000_000_000L)
+ memoryLimit = limit;
+ }
+ }
+
+ // 尝试读取 CPU 限制
+ // cgroup v2: cpu.max 格式为 "quota period" 或 "max period"
+ var cpuFile = "/sys/fs/cgroup/cpu.max";
+ if (File.Exists(cpuFile))
+ {
+ if (TryRead(cpuFile, out var value))
+ {
+ var parts = value.Split(' ');
+ if (parts.Length >= 2 && parts[0] != "max")
+ {
+ var quota = parts[0].ToLong();
+ var period = parts[1].ToLong();
+ if (quota > 0 && period > 0)
+ cpuLimit = (Double)quota / period;
+ }
+ }
+ }
+ else
+ {
+ // cgroup v1
+ var quotaFile = "/sys/fs/cgroup/cpu/cpu.cfs_quota_us";
+ var periodFile = "/sys/fs/cgroup/cpu/cpu.cfs_period_us";
+ if (File.Exists(quotaFile) && File.Exists(periodFile))
+ {
+ if (TryRead(quotaFile, out var quotaStr) && TryRead(periodFile, out var periodStr))
+ {
+ var quota = quotaStr.ToLong();
+ var period = periodStr.ToLong();
+ // quota=-1 表示无限制
+ if (quota > 0 && period > 0)
+ cpuLimit = (Double)quota / period;
+ }
+ }
+ }
+
+ return (memoryLimit, cpuLimit);
+ }
+
+ /// <summary>获取容器内存使用量</summary>
+ /// <returns>当前内存使用量(字节)。获取失败返回null</returns>
+ public static Int64? GetContainerMemoryUsage()
+ {
+ if (!Runtime.Linux) return null;
+
+ // cgroup v2
+ var usageFile = "/sys/fs/cgroup/memory.current";
+ if (File.Exists(usageFile) && TryRead(usageFile, out var value))
+ return value.ToLong();
+
+ // cgroup v1
+ usageFile = "/sys/fs/cgroup/memory/memory.usage_in_bytes";
+ if (File.Exists(usageFile) && TryRead(usageFile, out value))
+ return value.ToLong();
+
+ return null;
+ }
+ #endregion
+}
diff --git a/NewLife.Core/Common/MachineInfo.cs b/NewLife.Core/Common/MachineInfo.cs
index 3966013..f8e8ea6 100644
--- a/NewLife.Core/Common/MachineInfo.cs
+++ b/NewLife.Core/Common/MachineInfo.cs
@@ -1,26 +1,13 @@
using System.ComponentModel;
-using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
using System.Net.NetworkInformation;
using System.Reflection;
-using System.Runtime.InteropServices;
-using System.Security;
using NewLife.Collections;
+using NewLife.Data;
using NewLife.Log;
using NewLife.Model;
using NewLife.Reflection;
using NewLife.Serialization;
-using System.Runtime.Versioning;
-using System.Diagnostics.CodeAnalysis;
-using NewLife.Windows;
-using NewLife.Data;
-
-#if NETFRAMEWORK
-using System.Management;
-using Microsoft.VisualBasic.Devices;
-#endif
-#if NETFRAMEWORK || NET5_0_OR_GREATER
-using Microsoft.Win32;
-#endif
namespace NewLife;
@@ -43,7 +30,7 @@ public interface IMachineInfo
///
/// 刷新信息成本较高,建议采用单例模式
/// </remarks>
-public class MachineInfo : IExtend
+public partial class MachineInfo : IExtend
{
#region 属性
/// <summary>系统名称</summary>
@@ -280,322 +267,6 @@ public class MachineInfo : IExtend
/// <summary>裁剪不可见字符并去除两端空白</summary>
private static String? Clean(String? value) => value.TrimInvisible()?.Trim();
-#if NET5_0_OR_GREATER
- [SupportedOSPlatform("windows")]
-#endif
- private void LoadWindowsInfo()
- {
- var str = "";
-
- // 从注册表读取 MachineGuid
-#if NETFRAMEWORK || NET6_0_OR_GREATER
- var reg = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Cryptography");
- if (reg != null) str = reg.GetValue("MachineGuid") + "";
- if (str.IsNullOrEmpty())
- {
- reg = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64);
- reg = reg?.OpenSubKey(@"SOFTWARE\Microsoft\Cryptography");
- if (reg != null) str = reg.GetValue("MachineGuid") + "";
- }
-
- if (!str.IsNullOrEmpty()) Guid = str;
-
- reg = Registry.LocalMachine.OpenSubKey(@"SYSTEM\HardwareConfig");
- if (reg != null)
- {
- str = (reg.GetValue("LastConfig") + "")?.Trim('{', '}').ToUpper();
-
- // UUID取不到时返回 FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF
- if (!str.IsNullOrEmpty() && !str.EqualIgnoreCase("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF")) UUID = str;
- }
-
- reg = Registry.LocalMachine.OpenSubKey(@"HARDWARE\DESCRIPTION\System\BIOS");
- reg ??= Registry.LocalMachine.OpenSubKey(@"SYSTEM\HardwareConfig\Current");
- if (reg != null)
- {
- Product = (reg.GetValue("SystemProductName") + "").Replace("System Product Name", null);
- if (Product.IsNullOrEmpty()) Product = reg.GetValue("BaseBoardProduct") + "";
-
- Vendor = reg.GetValue("SystemManufacturer") + "";
- if (Vendor.IsNullOrEmpty()) Vendor = reg.GetValue("ASUSTeK COMPUTER INC.") + "";
- }
-
- reg = Registry.LocalMachine.OpenSubKey(@"HARDWARE\DESCRIPTION\System\CentralProcessor\0");
- if (reg != null) Processor = reg.GetValue("ProcessorNameString") + "";
-
- // 旧版系统(如win2008)没有UUID的注册表项,需要用wmic查询。也可能因为过去的某个BUG,导致GUID跟UUID相等
- if (UUID.IsNullOrEmpty() || UUID == Guid || Vendor.IsNullOrEmpty())
- {
- var csproduct = ReadWmic("csproduct", "Name", "UUID", "Vendor");
- if (csproduct != null)
- {
- if (csproduct.TryGetValue("Name", out str) && !str.IsNullOrEmpty() && Product.IsNullOrEmpty()) Product = str;
- if (csproduct.TryGetValue("UUID", out str) && !str.IsNullOrEmpty()) UUID = str;
- if (csproduct.TryGetValue("Vendor", out str) && !str.IsNullOrEmpty()) Vendor = str;
- }
- }
-#else
- str = "reg".Execute(@"query HKLM\SOFTWARE\Microsoft\Cryptography /v MachineGuid", 0, false);
- if (!str.IsNullOrEmpty() && str.Contains("REG_SZ")) Guid = str.Substring("REG_SZ", null).Trim();
-
- var csproduct = ReadWmiComMulti("csproduct", "Name", "UUID", "Vendor");
- if (csproduct != null)
- {
- if (csproduct.TryGetValue("Name", out str)) Product = str;
- if (csproduct.TryGetValue("UUID", out str)) UUID = str;
- if (csproduct.TryGetValue("Vendor", out str)) Vendor = str;
- }
-#endif
- // 获取内存大小
-#if NETFRAMEWORK || WINDOWS
- {
- var ci = new Microsoft.VisualBasic.Devices.ComputerInfo();
- Memory = ci.TotalPhysicalMemory;
- }
-#endif
-
- // 获取操作系统名称和版本
-#if NETFRAMEWORK
- try
- {
- var ci = new Microsoft.VisualBasic.Devices.ComputerInfo();
-
- // 系统名取WMI可能出错
- OSName = ci.OSFullName?.Replace("®", null).TrimPrefix("Microsoft").Trim();
- OSVersion = ci.OSVersion;
- }
- catch
- {
- try
- {
- var reg2 = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion");
- if (reg2 != null)
- {
- OSName = reg2.GetValue("ProductName") + "";
- OSVersion = reg2.GetValue("ReleaseId") + "";
- }
- }
- catch (Exception ex)
- {
- if (XTrace.Log.Level <= LogLevel.Debug) XTrace.WriteException(ex);
- }
- }
- //#elif NET5_0_OR_GREATER
- // OSName = GetInfo("Win32_OperatingSystem", "Caption")?.TrimStart("Microsoft").Trim();
- // OSVersion = GetInfo("Win32_OperatingSystem", "Version");
-#else
- var os = ReadWmiComMulti("os", "Caption", "Version");
- if (os == null || os.Count == 0)
- {
- os = ReadPowerShell("Get-WmiObject Win32_OperatingSystem | Select-Object Caption, Version | ConvertTo-Json");
- }
- if (os is { Count: > 0 })
- {
- if (os.TryGetValue("Caption", out str)) OSName = str.TrimPrefix("Microsoft").Trim();
- if (os.TryGetValue("Version", out str)) OSVersion = str;
- }
-#endif
-
-#if NETFRAMEWORK
- //Processor = GetInfo("Win32_Processor", "Name");
- //CpuID = GetInfo("Win32_Processor", "ProcessorId");
- //var uuid = GetInfo("Win32_ComputerSystemProduct", "UUID");
- //Product = GetInfo("Win32_ComputerSystemProduct", "Name");
- DiskID = GetInfo("Win32_DiskDrive where mediatype=\"Fixed hard disk media\"", "SerialNumber");
-
- var sn = GetInfo("Win32_BIOS", "SerialNumber");
- if (!sn.IsNullOrEmpty() && !sn.EqualIgnoreCase("System Serial Number")) Serial = sn;
- Board = GetInfo("Win32_BaseBoard", "SerialNumber");
-#else
- var diskStr = ReadWmiComSingle("diskdrive where mediatype=\"Fixed hard disk media\"", "serialnumber");
- if (!diskStr.IsNullOrEmpty()) DiskID = diskStr?.Trim();
-
- var sn = ReadWmiComSingle("bios", "serialnumber");
- if (!sn.IsNullOrEmpty() && !sn.EqualIgnoreCase("System Serial Number")) Serial = sn?.Trim();
-
- var boardStr = ReadWmiComSingle("baseboard", "serialnumber");
- if (!boardStr.IsNullOrEmpty()) Board = boardStr?.Trim();
-
-
- //// 不要在刷新里面取CPU负载,因为运行wmic会导致CPU负载很不准确,影响测量
- //var cpu = ReadWmic("cpu", "Name", "ProcessorId", "LoadPercentage");
- //if (cpu != null)
- //{
- // if (cpu.TryGetValue("Name", out str)) Processor = str;
- // //if (cpu.TryGetValue("ProcessorId", out str)) CpuID = str;
- // if (cpu.TryGetValue("LoadPercentage", out str)) CpuRate = (Single)(str.ToDouble() / 100);
- //}
-#endif
-
-#if !NETFRAMEWORK
- if (OSName.IsNullOrEmpty())
- OSName = RuntimeInformation.OSDescription.TrimPrefix("Microsoft").Trim();
- if (OSVersion.IsNullOrEmpty())
- OSVersion = Environment.OSVersion.Version.ToString();
-#endif
- }
-
-#if NET5_0_OR_GREATER
- [SupportedOSPlatform("linux")]
-#endif
- private void LoadLinuxInfo()
- {
- var str = GetLinuxName();
- if (!str.IsNullOrEmpty()) OSName = str;
-
- var device = ReadDeviceInfo();
-
- if (device.TryGetValue("Platform", out str))
- OSName = str;
- if (device.TryGetValue("Version", out str))
- OSVersion = str;
-
- // 树莓派的Hardware无法区分P0/P4
- var dic = ReadInfo("/proc/cpuinfo");
- if (dic != null)
- {
- if (dic.TryGetValue("Hardware", out str) ||
- dic.TryGetValue("cpu model", out str) ||
- dic.TryGetValue("model name", out str))
- Processor = str?.TrimPrefix("vendor ");
-
- if (device.TryGetValue("Product", out str))
- Product = str;
- else if (dic.TryGetValue("Model", out str))
- Product = str;
-
- if (dic.TryGetValue("vendor_id", out str))
- Vendor = str;
-
- //if (device.TryGetValue("Fingerprint", out str) && !str.IsNullOrEmpty())
- // CpuID = str;
- if (dic.TryGetValue("Serial", out str) && !str.IsNullOrEmpty() && !str.Trim('0').IsNullOrEmpty())
- UUID = str;
- }
-
- var mid = "/etc/machine-id";
- if (!File.Exists(mid)) mid = "/var/lib/dbus/machine-id";
- if (TryRead(mid, out var value))
- Guid = value;
- else if (device.TryGetValue("android_id", out str) && !str.IsNullOrEmpty() && str != "unknown")
- Guid = str;
- //else if (android.TryGetValue("Id", out str))
- // Guid = str;
-
- // DMI信息位于 /sys/class/dmi/id/ 目录,可以直接读取,不需要执行dmidecode命令
- var uuid = "";
- var file = "/sys/class/dmi/id/product_uuid";
- if (!File.Exists(file)) file = "/etc/uuid";
- if (!File.Exists(file)) file = "/proc/serial_num"; // miui12支持/proc/serial_num
- if (TryRead(file, out value))
- uuid = value;
- else if (device.TryGetValue("Serial", out str) && str != "unknown")
- uuid = str;
- if (!uuid.IsNullOrEmpty()) UUID = uuid;
-
- // 从release文件读取产品
- var prd = GetProductByRelease();
- if (!prd.IsNullOrEmpty()) Product = prd;
-
- if (prd.IsNullOrEmpty() && TryRead("/sys/class/dmi/id/product_name", out var product_name))
- {
- Product = product_name;
-
- // 增加制造商。如 Tencent Cloud,它的产品名只有 CVM。阿里云产品名 Alibaba Cloud ECS
- if (TryRead("/sys/class/dmi/id/sys_vendor", out var vendor) && !vendor.IsNullOrEmpty())
- {
- Vendor = vendor;
-
- if (!product_name.IsNullOrEmpty() && !product_name.Contains(vendor))
- {
- // 红帽KVM太流行,细化处理
- if (product_name == "KVM" && vendor == "Red Hat" &&
- TryRead("/sys/class/dmi/id/product_version", out var ver) && !ver.IsNullOrEmpty())
- {
- var p = ver.IndexOf('(');
- if (p > 0) ver = ver[..p].Trim();
- Product = ver;
- }
- }
- }
- }
-
- file = "/sys/class/dmi/id/product_serial";
- if (TryRead(file, out value)) Serial = value;
-
- // 在DMI信息内,没有太好的BoardID取值
- file = "/sys/class/dmi/id/product_sku";
- if (TryRead(file, out value) && !value.IsNullOrEmpty())
- Board = value;
- else
- {
- file = "/sys/class/dmi/id/product_family";
- if (TryRead(file, out value)) Board = value;
- }
-
- // 在虚拟机中,uuid可能出现一个时间id和一个guid。
- var disks = GetFiles("/dev/disk/by-uuid", false);
- if (disks.Count > 0)
- {
- // 去掉时间id例如 2025-08-14-18-36-42-00,因为它随着时间在改变
- disks = disks.Where(e => !e.IsNullOrEmpty() && (e.Length < 10 || e[4] != '-' || e[..10].ToDateTime().Year < 2000)).ToList();
- }
-
- if (disks.Count == 0)
- {
- // id中需要剔除QEMU,去掉virtio-前缀,例如 virtio-uf6ag3b49w6v4e9ldgcj
- disks = GetFiles("/dev/disk/by-id", true);
- disks = disks.Where(e => !e.IsNullOrEmpty() && !e.Contains("QEMU_")).Select(e => e.TrimPrefix("virtio-")).ToList();
- }
-
- if (disks.Count == 0) disks = GetFiles("/dev/disk/by-partuuid", true);
- if (disks.Count > 0) DiskID = disks.Where(e => !e.IsNullOrEmpty()).Join(",");
-
- // 从*-release文件读取产品信息,具有更高优先级
- file = "/etc/os-release";
- if (TryRead(file, out value))
- {
- var dic2 = value.SplitAsDictionary("=", Environment.NewLine, true);
-
- if (dic2.TryGetValue("Vendor", out str)) Vendor = str;
- if (dic2.TryGetValue("Product", out str)) Product = str;
- if (dic2.TryGetValue("Serial", out str)) Serial = str;
- if (dic2.TryGetValue("Board", out str)) Board = str;
- }
- }
-
-#if NET5_0_OR_GREATER
- [SupportedOSPlatform("macos")]
-#endif
- private void LoadMacInfo()
- {
- var dic = ReadCommand("sw_vers");
- if (dic != null)
- {
- if (dic.TryGetValue("ProductName", out var str)) OSName = str;
- if (dic.TryGetValue("ProductVersion", out str)) OSVersion = str;
- }
-
- dic = ReadCommand("system_profiler", "SPHardwareDataType");
- if (dic != null)
- {
- //if (dic2.TryGetValue("Model Name", out str)) Product = str;
- if (dic.TryGetValue("Model Identifier", out var str)) Product = str;
- if (dic.TryGetValue("Processor Name", out str)) Processor = str;
- if (dic.TryGetValue("Memory", out str)) Memory = (UInt64)str.TrimSuffix("GB").Trim().ToLong() * 1024 * 1024 * 1024;
- if (dic.TryGetValue("Serial Number (system)", out str)) Serial = str;
- if (dic.TryGetValue("Hardware UUID", out str)) UUID = str;
- }
-
- if (Vendor.IsNullOrEmpty()) Vendor = "Apple";
-
- dic = ReadCommand("diskutil", "info disk1");
- if (dic != null)
- {
- if (dic.TryGetValue("Disk / Partition UUID", out var str)) DiskID = str;
- }
- }
-
private readonly ICollection<String> _excludes = [];
/// <summary>获取实时数据,如CPU、内存、温度</summary>
@@ -612,208 +283,6 @@ public class MachineInfo : IExtend
Provider?.Refresh(this);
}
- private void RefreshWindows()
- {
- MEMORYSTATUSEX ms = default;
- ms.Init();
- if (GlobalMemoryStatusEx(ref ms))
- {
- Memory = ms.ullTotalPhys;
- AvailableMemory = ms.ullAvailPhys;
- FreeMemory = ms.ullAvailPhys;
- }
-
- GetSystemTimes(out var idleTime, out var kernelTime, out var userTime);
-
- var current = new SystemTime
- {
- IdleTime = idleTime.ToLong(),
- TotalTime = kernelTime.ToLong() + userTime.ToLong(),
- };
-
- var idle = current.IdleTime - (_systemTime?.IdleTime ?? 0);
- var total = current.TotalTime - (_systemTime?.TotalTime ?? 0);
- _systemTime = current;
-
- CpuRate = total == 0 ? 0 : Math.Round((Double)(total - idle) / total, 4);
-
- var power = new PowerStatus();
-
-#if NETFRAMEWORK
- if (!_excludes.Contains(nameof(Temperature)))
- {
- // 读取主板温度,不太准。标准方案是ring0通过IOPort读取CPU温度,太难在基础类库实现
- var str = GetInfo("Win32_TemperatureProbe", "CurrentReading");
- if (!str.IsNullOrEmpty())
- {
- Temperature = str.SplitAsInt().Average();
- }
- else
- {
- str = GetInfo("MSAcpi_ThermalZoneTemperature", "CurrentTemperature", "root/wmi");
- if (!str.IsNullOrEmpty())
- Temperature = (str.SplitAsInt().Average() - 2732) / 10.0;
- else
- {
- if (XTrace.Log.Level <= LogLevel.Debug) XTrace.WriteLine("Temperature信息无法读取");
- _excludes.Add(nameof(Temperature));
- Temperature = 0;
- }
- }
- }
-
- if (power.BatteryLifePercent > 0)
- Battery = power.BatteryLifePercent;
- else if (!_excludes.Contains(nameof(Battery)))
- {
- // 电池剩余
- var str = GetInfo("Win32_Battery", "EstimatedChargeRemaining");
- if (!str.IsNullOrEmpty())
- Battery = str.SplitAsInt().Average() / 100.0;
- else
- {
- if (XTrace.Log.Level <= LogLevel.Debug) XTrace.WriteLine("Battery信息无法读取");
- _excludes.Add(nameof(Battery));
- Battery = 0;
- }
- }
-#else
- if (!_excludes.Contains(nameof(Temperature)))
- {
- var str = ReadWmiComSingle(@"/namespace:\\root\wmi path MSAcpi_ThermalZoneTemperature", "CurrentTemperature");
- if (!str.IsNullOrEmpty())
- {
- Temperature = (str.SplitAsInt().Average() - 2732) / 10.0;
- }
- else
- {
- if (XTrace.Log.Level <= LogLevel.Debug) XTrace.WriteLine("Temperature信息无法读取");
- _excludes.Add(nameof(Temperature));
- Temperature = 0;
- }
- }
-
- if (power.BatteryLifePercent > 0)
- Battery = power.BatteryLifePercent;
- else if (!_excludes.Contains(nameof(Battery)))
- {
- var str = ReadWmiComSingle("path win32_battery", "EstimatedChargeRemaining");
- if (!str.IsNullOrEmpty())
- {
- Battery = str.SplitAsInt().Average() / 100.0;
- }
- else
- {
- if (XTrace.Log.Level <= LogLevel.Debug) XTrace.WriteLine("Battery信息无法读取");
- _excludes.Add(nameof(Battery));
- Battery = 0;
- }
- }
-#endif
- }
-
- private void RefreshLinux()
- {
- var dic = ReadInfo("/proc/meminfo");
- if (dic != null)
- {
- if (dic.TryGetValue("MemTotal", out var str) && !str.IsNullOrEmpty())
- Memory = (UInt64)str.TrimSuffix(" kB").ToInt() * 1024;
-
- // MemAvailable是系统内核预测的可用内存,过低则认为不能安全分配给新进程,可能过于悲观;
- // MemFree是完全空闲的内存,未被使用的物理内存页,但内核不敢用;
- static UInt64 GetMem(IDictionary<String, String?> mem, String key)
- {
- return mem.TryGetValue(key, out var v) && !v.IsNullOrEmpty() ? (UInt64)v.TrimSuffix(" kB").ToInt() * 1024 : 0;
- }
-
- var ma = GetMem(dic, "MemAvailable");
- var mf = GetMem(dic, "MemFree");
- var buffers = GetMem(dic, "Buffers");
- var cached = GetMem(dic, "Cached");
- var srecl = GetMem(dic, "SReclaimable");
- var shmem = GetMem(dic, "Shmem");
-
- AvailableMemory = ma;
-
- // FreeMemory采用 free 命令的宽松口径:free + buffers + cache + SReclaimable - Shmem
- var cache = cached + srecl;
- if (cache > shmem) cache -= shmem;
-
- FreeMemory = mf + buffers + cache;
- }
-
- // A2/A4温度获取,Buildroot,CPU温度和主板温度
- if (TryRead("/sys/class/thermal/thermal_zone0/temp", out var value) ||
- TryRead("/sys/class/thermal/thermal_zone1/temp", out value))
- {
- Temperature = value.ToDouble();
- // 有时候温度会超过1000,可能是毫度。机器温度不会低于0度
- if (Temperature > 1000) Temperature /= 1000;
- }
- // respberrypi + fedora
- else if (TryRead("/sys/class/thermal/thermal_zone0/temp", out value) ||
- TryRead("/sys/class/hwmon/hwmon0/temp1_input", out value) ||
- TryRead("/sys/class/hwmon/hwmon0/temp2_input", out value) ||
- TryRead("/sys/class/hwmon/hwmon0/device/hwmon/hwmon0/temp2_input", out value) ||
- TryRead("/sys/devices/virtual/thermal/thermal_zone0/temp", out value))
- {
- Temperature = value.ToDouble() / 1000;
- }
- // A2温度获取,Ubuntu 16.04 LTS, Linux 3.4.39
- else if (TryRead("/sys/class/hwmon/hwmon0/device/temp_value", out value))
- {
- if (!value.IsNullOrEmpty()) Temperature = value.Substring(null, ":").ToDouble();
- }
-
- // 电池剩余
- if (TryRead("/sys/class/power_supply/BAT0/energy_now", out var energy_now) &&
- TryRead("/sys/class/power_supply/BAT0/energy_full", out var energy_full))
- {
- Battery = energy_now.ToDouble() / energy_full.ToDouble();
- }
- else if (TryRead("/sys/class/power_supply/battery/capacity", out var capacity))
- {
- Battery = capacity.ToDouble() / 100.0;
- }
- else if (Runtime.Mono)
- {
- var battery = ReadDeviceBattery();
- if (battery.TryGetValue("ChargeLevel", out var obj)) Battery = obj.ToDouble();
- }
-
- var file = "/proc/stat";
- if (!_excludes.Contains(nameof(CpuRate)) && File.Exists(file))
- {
- // CPU指标:user,nice, system, idle, iowait, irq, softirq
- // cpu 57057 0 14420 1554816 0 443 0 0 0 0
- try
- {
- using var reader = new StreamReader(file);
- var line = reader.ReadLine();
- if (!line.IsNullOrEmpty() && line.StartsWith("cpu"))
- {
- var vs = line.TrimPrefix("cpu").Trim().Split(' ');
- var current = new SystemTime
- {
- IdleTime = vs[3].ToLong(),
- TotalTime = vs.Take(7).Select(e => e.ToLong()).Sum().ToLong(),
- };
-
- var idle = current.IdleTime - (_systemTime?.IdleTime ?? 0);
- var total = current.TotalTime - (_systemTime?.TotalTime ?? 0);
- _systemTime = current;
-
- CpuRate = total == 0 ? 0 : Math.Round((Double)(total - idle) / total, 4);
- }
- }
- catch
- {
- _excludes.Add(nameof(CpuRate));
- }
- }
- }
-
private Int64 _lastTime;
private Int64 _lastSent;
private Int64 _lastReceived;
@@ -859,57 +328,6 @@ public class MachineInfo : IExtend
#endregion
#region 辅助
- /// <summary>获取Linux发行版名称</summary>
- /// <returns>Linux发行版名称</returns>
- public static String? GetLinuxName()
- {
- var fr = "/etc/redhat-release";
- if (TryRead(fr, out var value)) return value;
-
- var dr = "/etc/debian-release";
- if (TryRead(dr, out value)) return value;
-
- var sr = "/etc/os-release";
- if (TryRead(sr, out value))
- {
- var dic = value.SplitAsDictionary("=", "\n", true);
- if (dic.TryGetValue("PRETTY_NAME", out var pretty) && !pretty.IsNullOrEmpty()) return pretty.Trim();
- if (dic.TryGetValue("NAME", out var name) && !name.IsNullOrEmpty()) return name.Trim();
- }
-
- var uname = "uname".Execute("-sr", 0, false)?.Trim();
- if (!uname.IsNullOrEmpty())
- {
- // 支持Android系统名
- var ss = uname.Split('-');
- foreach (var item in ss)
- {
- if (!item.IsNullOrEmpty() && item.StartsWithIgnoreCase("Android")) return item;
- }
-
- return uname;
- }
-
- return null;
- }
-
- private static String? GetProductByRelease()
- {
- var di = "/etc/".AsDirectory();
- if (!di.Exists) return null;
-
- foreach (var fi in di.GetFiles("*-release"))
- {
- if (!fi.Name.EqualIgnoreCase("redhat-release", "debian-release", "os-release", "system-release"))
- {
- var dic = File.ReadAllText(fi.FullName).SplitAsDictionary("=", "\n", true);
- if (dic.TryGetValue("BOARD", out var str)) return str;
- if (dic.TryGetValue("BOARD_NAME", out str)) return str;
- }
- }
-
- return null;
- }
private static Boolean TryRead(String fileName, [NotNullWhen(true)] out String? value)
{
@@ -958,138 +376,6 @@ public class MachineInfo : IExtend
return dic;
}
- private static IDictionary<String, String>? ReadCommand(String cmd, String? arguments = null)
- {
- var str = cmd.Execute(arguments, 0, false);
- if (str.IsNullOrEmpty()) return null;
-
- return str.SplitAsDictionary(":", "\n", true);
- }
-
- /// <summary>通过 PowerShell 命令读取信息</summary>
- /// <param name="command">PowerShell命令</param>
- /// <returns>解析后的字典</returns>
- public static IDictionary<String, String> ReadPowerShell(String command)
- {
- var dic = new Dictionary<String, String>(StringComparer.OrdinalIgnoreCase);
-
- var args = $"-Command \"{command}\"";
- var str = "powershell.exe".Execute(args, 3_000) ?? String.Empty;
- if (!String.IsNullOrWhiteSpace(str))
- {
- foreach (var item in str.DecodeJson()!)
- {
- dic[item.Key] = item.Value?.ToString() ?? String.Empty;
- }
- }
- return dic;
- }
-
- /// <summary>通过WMIC命令读取信息</summary>
- /// <param name="type">WMI类型</param>
- /// <param name="keys">查询字段</param>
- /// <returns>解析后的字典</returns>
- public static IDictionary<String, String> ReadWmic(String type, params String[] keys)
- {
- var dic = new Dictionary<String, IList<String>>(StringComparer.OrdinalIgnoreCase);
- var dic2 = new Dictionary<String, String>(StringComparer.OrdinalIgnoreCase);
-
- var args = $"{type} get {keys.Join(",")} /format:list";
- var str = "wmic".Execute(args, 0, false)?.Trim();
- if (str.IsNullOrEmpty()) return dic2;
-
- var ss = str.Split("\r\n");
- foreach (var item in ss)
- {
- var ks = item?.Split('=');
- if (ks != null && ks.Length >= 2)
- {
- var k = ks[0].Trim();
- var v = ks[1].Trim().TrimInvisible();
- if (!k.IsNullOrEmpty() && !v.IsNullOrEmpty())
- {
- if (!dic.TryGetValue(k, out var list))
- dic[k] = list = [];
-
- list.Add(v);
- }
- }
- }
-
- // 排序,避免多个磁盘序列号时,顺序变动
- foreach (var item in dic)
- {
- dic2[item.Key] = item.Value.OrderBy(e => e).Join();
- }
-
- return dic2;
- }
-
- private static readonly Dictionary<String, String> _wmiAliases = new(StringComparer.OrdinalIgnoreCase)
- {
- { "csproduct", "Win32_ComputerSystemProduct" },
- { "os", "Win32_OperatingSystem" },
- { "diskdrive", "Win32_DiskDrive" },
- { "bios", "Win32_BIOS" },
- { "baseboard", "Win32_BaseBoard" },
- { "win32_battery", "Win32_Battery" },
- };
-
- /// <summary>通过 WMI 查询多属性(自动选择 COM/wmic)。优先使用 COM 避免开进程</summary>
- /// <param name="type">WMI 类型,支持 wmic 格式如 "csproduct"、"/namespace:\\root\wmi path MSAcpi_ThermalZoneTemperature"</param>
- /// <param name="keys">查询字段</param>
- /// <returns>属性字典</returns>
- public static IDictionary<String, String> ReadWmiComMulti(String type, params String[] keys)
- {
- // 解析命名空间和类名
- var nameSpace = "root\\cimv2";
- var wmiClass = type;
-
- if (type.StartsWith("/namespace:"))
- {
- var p = type.IndexOf(" path ", StringComparison.Ordinal);
- if (p > 0)
- {
- nameSpace = type["/namespace:".Length..p].Trim('\\');
- wmiClass = type[(p + 6)..].Trim();
- }
- }
- else if (type.StartsWith("path "))
- {
- wmiClass = type[5..].Trim();
- }
-
- // 映射 wmic 别名到 WMI 类名
- var alias = wmiClass;
- var whereIdx = wmiClass.IndexOf(" where ", StringComparison.OrdinalIgnoreCase);
- if (whereIdx > 0) alias = wmiClass[..whereIdx];
-
- if (_wmiAliases.TryGetValue(alias, out var fullClass))
- {
- wmiClass = whereIdx > 0 ? $"{fullClass}{wmiClass[whereIdx..]}" : fullClass;
- }
-
- // 逐属性查询
- var dic = new Dictionary<String, String>(StringComparer.OrdinalIgnoreCase);
- foreach (var key in keys)
- {
- var value = GetInfo(wmiClass, key, nameSpace);
- if (!value.IsNullOrEmpty()) dic[key] = value;
- }
-
- return dic;
- }
-
- /// <summary>通过 WMI 查询单个属性(自动回退 wmic)。优先使用 COM 避免开进程</summary>
- /// <param name="type">WMI 类型</param>
- /// <param name="property">属性名</param>
- /// <returns>属性值,失败返回 null</returns>
- private static String? ReadWmiComSingle(String type, String property)
- {
- var dic = ReadWmiComMulti(type, property);
- return dic.TryGetValue(property, out var v) && !v.IsNullOrEmpty() ? v : null;
- }
-
/// <summary>获取设备信息。用于Xamarin</summary>
/// <returns>设备信息字典</returns>
public static IDictionary<String, String?> ReadDeviceInfo()
@@ -1164,36 +450,6 @@ public class MachineInfo : IExtend
}
#endregion
- #region 内存
- [DllImport("Kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
- [SecurityCritical]
- [return: MarshalAs(UnmanagedType.Bool)]
- internal static extern Boolean GlobalMemoryStatusEx(ref MEMORYSTATUSEX lpBuffer);
-
- internal struct MEMORYSTATUSEX
- {
- internal UInt32 dwLength;
-
- internal UInt32 dwMemoryLoad;
-
- internal UInt64 ullTotalPhys;
-
- internal UInt64 ullAvailPhys;
-
- internal UInt64 ullTotalPageFile;
-
- internal UInt64 ullAvailPageFile;
-
- internal UInt64 ullTotalVirtual;
-
- internal UInt64 ullAvailVirtual;
-
- internal UInt64 ullAvailExtendedVirtual;
-
- internal void Init() => dwLength = checked((UInt32)Marshal.SizeOf(typeof(MEMORYSTATUSEX)));
- }
- #endregion
-
#region 磁盘
/// <summary>获取指定目录所在盘可用空间,默认当前目录</summary>
/// <param name="path">目录路径</param>
@@ -1251,159 +507,6 @@ public class MachineInfo : IExtend
}
#endregion
- #region 容器检测
- /// <summary>是否运行在容器中</summary>
- /// <remarks>
- /// 检测依据:
- /// 1. 环境变量 DOTNET_RUNNING_IN_CONTAINER
- /// 2. 存在 /.dockerenv 文件
- /// 3. /proc/1/cgroup 包含 docker/kubepods/containerd 等关键字
- /// </remarks>
- public static Boolean IsInContainer
- {
- get
- {
- // 环境变量检测
- var env = Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER");
- if (env.EqualIgnoreCase("true", "1")) return true;
-
- if (!Runtime.Linux) return false;
-
- // Docker 环境文件检测
- if (File.Exists("/.dockerenv")) return true;
-
- // cgroup 检测
- var cgroupFile = "/proc/1/cgroup";
- if (File.Exists(cgroupFile))
- {
- try
- {
- var content = File.ReadAllText(cgroupFile);
- if (content.Contains("docker") ||
- content.Contains("kubepods") ||
- content.Contains("containerd") ||
- content.Contains("lxc"))
- return true;
- }
- catch { }
- }
-
- return false;
- }
- }
-
- /// <summary>获取容器资源限制</summary>
- /// <returns>元组:(内存限制字节数, CPU核数限制)。返回null表示无限制或获取失败</returns>
- /// <remarks>
- /// 读取 cgroup v1/v2 的资源限制配置:
- /// - 内存限制:/sys/fs/cgroup/memory/memory.limit_in_bytes (v1) 或 /sys/fs/cgroup/memory.max (v2)
- /// - CPU限制:/sys/fs/cgroup/cpu/cpu.cfs_quota_us 和 cpu.cfs_period_us (v1) 或 /sys/fs/cgroup/cpu.max (v2)
- /// </remarks>
- public static (Int64? MemoryLimit, Double? CpuLimit) GetContainerLimits()
- {
- Int64? memoryLimit = null;
- Double? cpuLimit = null;
-
- if (!Runtime.Linux) return (memoryLimit, cpuLimit);
-
- // 尝试读取内存限制
- // cgroup v2
- var memFile = "/sys/fs/cgroup/memory.max";
- if (File.Exists(memFile))
- {
- if (TryRead(memFile, out var value) && value != "max")
- memoryLimit = value.ToLong();
- }
- else
- {
- // cgroup v1
- memFile = "/sys/fs/cgroup/memory/memory.limit_in_bytes";
- if (File.Exists(memFile) && TryRead(memFile, out var value))
- {
- var limit = value.ToLong();
- // 如果接近系统最大值(通常是一个很大的数),则认为无限制
- if (limit > 0 && limit < 9_000_000_000_000_000_000L)
- memoryLimit = limit;
- }
- }
-
- // 尝试读取 CPU 限制
- // cgroup v2: cpu.max 格式为 "quota period" 或 "max period"
- var cpuFile = "/sys/fs/cgroup/cpu.max";
- if (File.Exists(cpuFile))
- {
- if (TryRead(cpuFile, out var value))
- {
- var parts = value.Split(' ');
- if (parts.Length >= 2 && parts[0] != "max")
- {
- var quota = parts[0].ToLong();
- var period = parts[1].ToLong();
- if (quota > 0 && period > 0)
- cpuLimit = (Double)quota / period;
- }
- }
- }
- else
- {
- // cgroup v1
- var quotaFile = "/sys/fs/cgroup/cpu/cpu.cfs_quota_us";
- var periodFile = "/sys/fs/cgroup/cpu/cpu.cfs_period_us";
- if (File.Exists(quotaFile) && File.Exists(periodFile))
- {
- if (TryRead(quotaFile, out var quotaStr) && TryRead(periodFile, out var periodStr))
- {
- var quota = quotaStr.ToLong();
- var period = periodStr.ToLong();
- // quota=-1 表示无限制
- if (quota > 0 && period > 0)
- cpuLimit = (Double)quota / period;
- }
- }
- }
-
- return (memoryLimit, cpuLimit);
- }
-
- /// <summary>获取容器内存使用量</summary>
- /// <returns>当前内存使用量(字节)。获取失败返回null</returns>
- public static Int64? GetContainerMemoryUsage()
- {
- if (!Runtime.Linux) return null;
-
- // cgroup v2
- var usageFile = "/sys/fs/cgroup/memory.current";
- if (File.Exists(usageFile) && TryRead(usageFile, out var value))
- return value.ToLong();
-
- // cgroup v1
- usageFile = "/sys/fs/cgroup/memory/memory.usage_in_bytes";
- if (File.Exists(usageFile) && TryRead(usageFile, out value))
- return value.ToLong();
-
- return null;
- }
- #endregion
-
- #region Windows辅助
- [DllImport("kernel32.dll", SetLastError = true)]
- private static extern Boolean GetSystemTimes(out FILETIME idleTime, out FILETIME kernelTime, out FILETIME userTime);
-
- private struct FILETIME
- {
- public UInt32 Low;
-
- public UInt32 High;
-
- public FILETIME(Int64 time)
- {
- Low = (UInt32)time;
- High = (UInt32)(time >> 32);
- }
-
- public Int64 ToLong() => (Int64)(((UInt64)High << 32) | Low);
- }
-
private class SystemTime
{
public Int64 IdleTime;
@@ -1411,232 +514,4 @@ public class MachineInfo : IExtend
}
private SystemTime? _systemTime;
-
- /// <summary>获取WMI信息。NETFX直接用ManagementObjectSearcher;net*-windows反射System.Management并回退COM;其它运行时用COM并回退wmic</summary>
- /// <param name="path">WMI路径</param>
- /// <param name="property">属性名</param>
- /// <param name="nameSpace">命名空间,默认 root\cimv2</param>
- /// <returns>查询结果</returns>
- public static String GetInfo(String path, String property, String? nameSpace = null)
- {
- // 规范化命名空间:WMI 要求反斜杠分隔,调用者可能传入 root/wmi 等正斜杠格式
- if (nameSpace != null) nameSpace = nameSpace.Replace("/", "\\");
-
-#if NETFRAMEWORK
- // Linux Mono不支持WMI
- if (Runtime.Mono) return "";
-
- var bbs = new List<String>();
- try
- {
- var wql = $"Select {property} From {path}";
- var cimobject = new ManagementObjectSearcher(nameSpace, wql);
- var moc = cimobject.Get();
- foreach (var mo in moc)
- {
- var val = mo?.Properties?[property]?.Value;
- if (val != null)
- {
- var v = val.ToString().TrimInvisible()?.Trim();
- if (v != null) bbs.Add(v);
- }
- }
- }
- catch (Exception ex)
- {
- if (XTrace.Log.Level <= LogLevel.Debug) XTrace.WriteLine("WMI.GetInfo({0})失败!{1}", path, ex.Message);
- return "";
- }
-
- bbs.Sort();
-
- return bbs.Distinct().Join();
-#else
- var ns = nameSpace ?? "root\\cimv2";
-
-#if __WIN__
- // net*-windows:优先反射 System.Management 走标准 WMI,失败回退 COM;两者均失败降级 wmic 兜底
- {
- var value = QueryWmiSystemManagement(path, property, ns);
- if (value != null) return value;
-
- value = QueryWmiCom(path, property, ns);
- if (value != null) return value;
-
- // 两种 WMI 路径均失败(如 WMI 服务异常),降级到 wmic 进程兜底
- if (XTrace.Log.Level <= LogLevel.Debug)
- XTrace.WriteLine("WMI 均失败,回退 wmic: {0}.{1}", path, property);
- }
-#else
- // 其它运行时(netstandard/net*非windows):优先 COM,失败回退 wmic
-#if NET5_0_OR_GREATER
- if (OperatingSystem.IsWindows())
-#else
- if (Runtime.Windows)
-#endif
- {
- var value = QueryWmiCom(path, property, ns);
- if (value != null) return value;
- }
-#endif
-
- // wmic 兜底(__WIN__ 降级 + 其它运行时)。不带命名空间时也要用 path 前缀以支持完整类名
- var type = !ns.EqualIgnoreCase("root\\cimv2") ? $"/namespace:\\\\{ns} path {path}" : $"path {path}";
- var dic = ReadWmic(type, property);
- return dic.TryGetValue(property, out var v) ? v : "";
-#endif
- }
-
-#if !NETFRAMEWORK
- /// <summary>通过反射 System.Management 查询 WMI 属性。用于 net*-windows,不新增编译期依赖,不开进程</summary>
- /// <remarks>
- /// System.Management.dll 随 Windows 桌面运行时提供(net*-windows),通过反射调用避免核心库直接引用该程序集。
- /// 内部使用 ManagementObjectSearcher(nameSpace, wql).Get() 枚举结果并读取属性。
- /// </remarks>
- /// <param name="wmiClass">WMI 类名,如 Win32_OperatingSystem</param>
- /// <param name="property">属性名</param>
- /// <param name="nameSpace">命名空间,如 root\cimv2</param>
- /// <param name="throwOnError">失败时是否抛出异常,默认false吞掉异常返回null</param>
- /// <returns>查询结果,失败返回null</returns>
-#if NET5_0_OR_GREATER
- [SupportedOSPlatform("windows")]
-#endif
- internal static String? QueryWmiSystemManagement(String wmiClass, String property, String nameSpace, Boolean throwOnError = false)
- {
- try
- {
- // 反射加载 System.Management(Windows桌面运行时内置,无编译期引用)
- var asm = Assembly.Load("System.Management");
- var searcherType = asm.GetType("System.Management.ManagementObjectSearcher")!;
- var moType = asm.GetType("System.Management.ManagementObject")!;
- var enumType = asm.GetType("System.Management.ManagementObjectCollection")!;
-
- // new ManagementObjectSearcher(nameSpace, wql)
- var wql = $"SELECT {property} FROM {wmiClass}";
- var searcher = Activator.CreateInstance(searcherType, nameSpace, wql);
- if (searcher == null) return null;
-
- // .Get() → ManagementObjectCollection
- var getMethod = searcherType.GetMethod("Get", Type.EmptyTypes)!;
- var results = getMethod.Invoke(searcher, null);
- if (results == null) return null;
-
- // foreach (ManagementObject obj in results)
- var bbs = new List<String>();
- var getEnumeratorMethod = enumType.GetMethod("GetEnumerator")!;
- var enumerator = getEnumeratorMethod.Invoke(results, null);
- var moveNextMethod = enumerator!.GetType().GetMethod("MoveNext")!;
- var currentProperty = enumerator.GetType().GetProperty("Current")!;
-
- var itemProp = moType.GetProperty("Item", [typeof(String)])!;
- var getValueMethod = itemProp.GetGetMethod()!;
-
- while ((Boolean)moveNextMethod.Invoke(enumerator, null)!)
- {
- var obj = currentProperty.GetValue(enumerator);
- if (obj == null) continue;
-
- var val = getValueMethod.Invoke(obj, [property]);
- if (val != null)
- {
- var v = val.ToString()?.TrimInvisible()?.Trim();
- if (!v.IsNullOrEmpty()) bbs.Add(v);
- }
- }
-
- // 清理 COM 引用
- if (results is IDisposable d1) d1.Dispose();
- if (searcher is IDisposable d2) d2.Dispose();
-
- if (bbs.Count > 0)
- {
- bbs.Sort();
- return bbs.Distinct().Join();
- }
- }
- catch (Exception ex)
- {
- if (throwOnError) throw;
- if (XTrace.Log.Level <= LogLevel.Debug)
- {
- XTrace.WriteLine("QueryWmiSystemManagement 失败: {0}.{1} @ {2}", wmiClass, property, nameSpace);
- XTrace.WriteException(ex is TargetInvocationException tie ? tie.InnerException! : ex);
- }
- }
-
- return null;
- }
-
- /// <summary>通过 COM 查询 WMI 属性。使用 Windows 内置 winmgmts 组件,不新增依赖,不开进程</summary>
- /// <remarks>
- /// 通过 winmgmts moniker 绑定 SWbemServices,执行 WQL 查询并枚举结果,SWbemObject 按名暴露 WMI 属性。
- /// 相比 System.Management 无需该程序集,适用于 netstandard/net*非windows 运行时下的 Windows 主机。
- /// COM 查询在部分环境可能失败,此时调用方应回退 wmic。
- /// </remarks>
- /// <param name="wmiClass">WMI 类名,如 Win32_OperatingSystem</param>
- /// <param name="property">属性名</param>
- /// <param name="nameSpace">命名空间,如 root\cimv2</param>
- /// <param name="throwOnError">失败时是否抛出异常,默认false吞掉异常返回null</param>
- /// <returns>查询结果,失败返回null</returns>
-#if NET5_0_OR_GREATER
- [SupportedOSPlatform("windows")]
-#endif
- internal static String? QueryWmiCom(String wmiClass, String property, String nameSpace, Boolean throwOnError = false)
- {
- Object? services = null;
- Object? results = null;
- try
- {
- // 通过 winmgmts moniker 绑定 WMI 服务,规避 SWbemLocator.ConnectServer 的 IDispatch 参数编组问题
- var moniker = $"winmgmts:\\\\.\\{nameSpace}";
- services = Marshal.BindToMoniker(moniker);
- if (services == null) return null;
-
- // ExecQuery 执行 WQL 查询,返回 SWbemObjectSet
- var wql = $"SELECT {property} FROM {wmiClass}";
- results = services.GetType().InvokeMember("ExecQuery", BindingFlags.InvokeMethod, null, services, new Object[] { wql });
- if (results == null) return null;
-
- // SWbemObjectSet 实现 IEnumVARIANT,可直接 foreach;SWbemObject 通过 IDispatch 按名暴露 WMI 属性
- var bbs = new List<String>();
- foreach (var obj in (System.Collections.IEnumerable)results)
- {
- if (obj == null) continue;
- try
- {
- var val = obj.GetType().InvokeMember(property, BindingFlags.GetProperty, null, obj, null);
- if (val != null)
- {
- var v = val.ToString()?.TrimInvisible()?.Trim();
- if (!v.IsNullOrEmpty()) bbs.Add(v);
- }
- }
- finally { try { Marshal.FinalReleaseComObject(obj); } catch { } }
- }
-
- if (bbs.Count > 0)
- {
- bbs.Sort();
- return bbs.Distinct().Join();
- }
- }
- catch (Exception ex)
- {
- if (throwOnError) throw;
- if (XTrace.Log.Level <= LogLevel.Debug)
- {
- XTrace.WriteLine("QueryWmiCom 失败: {0}.{1} @ {2}", wmiClass, property, nameSpace);
- XTrace.WriteException(ex is TargetInvocationException tie ? tie.InnerException! : ex);
- }
- }
- finally
- {
- if (results != null) try { Marshal.FinalReleaseComObject(results); } catch { }
- if (services != null) try { Marshal.FinalReleaseComObject(services); } catch { }
- }
-
- return null;
- }
-#endif
- #endregion
}
\ No newline at end of file
diff --git a/NewLife.Core/Common/MachineInfo.Linux.cs b/NewLife.Core/Common/MachineInfo.Linux.cs
new file mode 100644
index 0000000..fa11342
--- /dev/null
+++ b/NewLife.Core/Common/MachineInfo.Linux.cs
@@ -0,0 +1,292 @@
+using System.Runtime.Versioning;
+
+namespace NewLife;
+
+/// <summary>机器信息 Linux 部分</summary>
+public partial class MachineInfo
+{
+#if NET5_0_OR_GREATER
+ [SupportedOSPlatform("linux")]
+#endif
+ private void LoadLinuxInfo()
+ {
+ var str = GetLinuxName();
+ if (!str.IsNullOrEmpty()) OSName = str;
+
+ var device = ReadDeviceInfo();
+
+ if (device.TryGetValue("Platform", out str))
+ OSName = str;
+ if (device.TryGetValue("Version", out str))
+ OSVersion = str;
+
+ // 树莓派的Hardware无法区分P0/P4
+ var dic = ReadInfo("/proc/cpuinfo");
+ if (dic != null)
+ {
+ if (dic.TryGetValue("Hardware", out str) ||
+ dic.TryGetValue("cpu model", out str) ||
+ dic.TryGetValue("model name", out str))
+ Processor = str?.TrimPrefix("vendor ");
+
+ if (device.TryGetValue("Product", out str))
+ Product = str;
+ else if (dic.TryGetValue("Model", out str))
+ Product = str;
+
+ if (dic.TryGetValue("vendor_id", out str))
+ Vendor = str;
+
+ //if (device.TryGetValue("Fingerprint", out str) && !str.IsNullOrEmpty())
+ // CpuID = str;
+ if (dic.TryGetValue("Serial", out str) && !str.IsNullOrEmpty() && !str.Trim('0').IsNullOrEmpty())
+ UUID = str;
+ }
+
+ var mid = "/etc/machine-id";
+ if (!File.Exists(mid)) mid = "/var/lib/dbus/machine-id";
+ if (TryRead(mid, out var value))
+ Guid = value;
+ else if (device.TryGetValue("android_id", out str) && !str.IsNullOrEmpty() && str != "unknown")
+ Guid = str;
+ //else if (android.TryGetValue("Id", out str))
+ // Guid = str;
+
+ // DMI信息位于 /sys/class/dmi/id/ 目录,可以直接读取,不需要执行dmidecode命令
+ var uuid = "";
+ var file = "/sys/class/dmi/id/product_uuid";
+ if (!File.Exists(file)) file = "/etc/uuid";
+ if (!File.Exists(file)) file = "/proc/serial_num"; // miui12支持/proc/serial_num
+ if (TryRead(file, out value))
+ uuid = value;
+ else if (device.TryGetValue("Serial", out str) && str != "unknown")
+ uuid = str;
+ if (!uuid.IsNullOrEmpty()) UUID = uuid;
+
+ // 从release文件读取产品
+ var prd = GetProductByRelease();
+ if (!prd.IsNullOrEmpty()) Product = prd;
+
+ if (prd.IsNullOrEmpty() && TryRead("/sys/class/dmi/id/product_name", out var product_name))
+ {
+ Product = product_name;
+
+ // 增加制造商。如 Tencent Cloud,它的产品名只有 CVM。阿里云产品名 Alibaba Cloud ECS
+ if (TryRead("/sys/class/dmi/id/sys_vendor", out var vendor) && !vendor.IsNullOrEmpty())
+ {
+ Vendor = vendor;
+
+ if (!product_name.IsNullOrEmpty() && !product_name.Contains(vendor))
+ {
+ // 红帽KVM太流行,细化处理
+ if (product_name == "KVM" && vendor == "Red Hat" &&
+ TryRead("/sys/class/dmi/id/product_version", out var ver) && !ver.IsNullOrEmpty())
+ {
+ var p = ver.IndexOf('(');
+ if (p > 0) ver = ver[..p].Trim();
+ Product = ver;
+ }
+ }
+ }
+ }
+
+ file = "/sys/class/dmi/id/product_serial";
+ if (TryRead(file, out value)) Serial = value;
+
+ // 在DMI信息内,没有太好的BoardID取值
+ file = "/sys/class/dmi/id/product_sku";
+ if (TryRead(file, out value) && !value.IsNullOrEmpty())
+ Board = value;
+ else
+ {
+ file = "/sys/class/dmi/id/product_family";
+ if (TryRead(file, out value)) Board = value;
+ }
+
+ // 在虚拟机中,uuid可能出现一个时间id和一个guid。
+ var disks = GetFiles("/dev/disk/by-uuid", false);
+ if (disks.Count > 0)
+ {
+ // 去掉时间id例如 2025-08-14-18-36-42-00,因为它随着时间在改变
+ disks = disks.Where(e => !e.IsNullOrEmpty() && (e.Length < 10 || e[4] != '-' || e[..10].ToDateTime().Year < 2000)).ToList();
+ }
+
+ if (disks.Count == 0)
+ {
+ // id中需要剔除QEMU,去掉virtio-前缀,例如 virtio-uf6ag3b49w6v4e9ldgcj
+ disks = GetFiles("/dev/disk/by-id", true);
+ disks = disks.Where(e => !e.IsNullOrEmpty() && !e.Contains("QEMU_")).Select(e => e.TrimPrefix("virtio-")).ToList();
+ }
+
+ if (disks.Count == 0) disks = GetFiles("/dev/disk/by-partuuid", true);
+ if (disks.Count > 0) DiskID = disks.Where(e => !e.IsNullOrEmpty()).Join(",");
+
+ // 从*-release文件读取产品信息,具有更高优先级
+ file = "/etc/os-release";
+ if (TryRead(file, out value))
+ {
+ var dic2 = value.SplitAsDictionary("=", Environment.NewLine, true);
+
+ if (dic2.TryGetValue("Vendor", out str)) Vendor = str;
+ if (dic2.TryGetValue("Product", out str)) Product = str;
+ if (dic2.TryGetValue("Serial", out str)) Serial = str;
+ if (dic2.TryGetValue("Board", out str)) Board = str;
+ }
+ }
+
+ private void RefreshLinux()
+ {
+ var dic = ReadInfo("/proc/meminfo");
+ if (dic != null)
+ {
+ if (dic.TryGetValue("MemTotal", out var str) && !str.IsNullOrEmpty())
+ Memory = (UInt64)str.TrimSuffix(" kB").ToInt() * 1024;
+
+ // MemAvailable是系统内核预测的可用内存,过低则认为不能安全分配给新进程,可能过于悲观;
+ // MemFree是完全空闲的内存,未被使用的物理内存页,但内核不敢用;
+ static UInt64 GetMem(IDictionary<String, String?> mem, String key)
+ {
+ return mem.TryGetValue(key, out var v) && !v.IsNullOrEmpty() ? (UInt64)v.TrimSuffix(" kB").ToInt() * 1024 : 0;
+ }
+
+ var ma = GetMem(dic, "MemAvailable");
+ var mf = GetMem(dic, "MemFree");
+ var buffers = GetMem(dic, "Buffers");
+ var cached = GetMem(dic, "Cached");
+ var srecl = GetMem(dic, "SReclaimable");
+ var shmem = GetMem(dic, "Shmem");
+
+ AvailableMemory = ma;
+
+ // FreeMemory采用 free 命令的宽松口径:free + buffers + cache + SReclaimable - Shmem
+ var cache = cached + srecl;
+ if (cache > shmem) cache -= shmem;
+
+ FreeMemory = mf + buffers + cache;
+ }
+
+ // A2/A4温度获取,Buildroot,CPU温度和主板温度
+ if (TryRead("/sys/class/thermal/thermal_zone0/temp", out var value) ||
+ TryRead("/sys/class/thermal/thermal_zone1/temp", out value))
+ {
+ Temperature = value.ToDouble();
+ // 有时候温度会超过1000,可能是毫度。机器温度不会低于0度
+ if (Temperature > 1000) Temperature /= 1000;
+ }
+ // respberrypi + fedora
+ else if (TryRead("/sys/class/thermal/thermal_zone0/temp", out value) ||
+ TryRead("/sys/class/hwmon/hwmon0/temp1_input", out value) ||
+ TryRead("/sys/class/hwmon/hwmon0/temp2_input", out value) ||
+ TryRead("/sys/class/hwmon/hwmon0/device/hwmon/hwmon0/temp2_input", out value) ||
+ TryRead("/sys/devices/virtual/thermal/thermal_zone0/temp", out value))
+ {
+ Temperature = value.ToDouble() / 1000;
+ }
+ // A2温度获取,Ubuntu 16.04 LTS, Linux 3.4.39
+ else if (TryRead("/sys/class/hwmon/hwmon0/device/temp_value", out value))
+ {
+ if (!value.IsNullOrEmpty()) Temperature = value.Substring(null, ":").ToDouble();
+ }
+
+ // 电池剩余
+ if (TryRead("/sys/class/power_supply/BAT0/energy_now", out var energy_now) &&
+ TryRead("/sys/class/power_supply/BAT0/energy_full", out var energy_full))
+ {
+ Battery = energy_now.ToDouble() / energy_full.ToDouble();
+ }
+ else if (TryRead("/sys/class/power_supply/battery/capacity", out var capacity))
+ {
+ Battery = capacity.ToDouble() / 100.0;
+ }
+ else if (Runtime.Mono)
+ {
+ var battery = ReadDeviceBattery();
+ if (battery.TryGetValue("ChargeLevel", out var obj)) Battery = obj.ToDouble();
+ }
+
+ var file = "/proc/stat";
+ if (!_excludes.Contains(nameof(CpuRate)) && File.Exists(file))
+ {
+ // CPU指标:user,nice, system, idle, iowait, irq, softirq
+ // cpu 57057 0 14420 1554816 0 443 0 0 0 0
+ try
+ {
+ using var reader = new StreamReader(file);
+ var line = reader.ReadLine();
+ if (!line.IsNullOrEmpty() && line.StartsWith("cpu"))
+ {
+ var vs = line.TrimPrefix("cpu").Trim().Split(' ');
+ var current = new SystemTime
+ {
+ IdleTime = vs[3].ToLong(),
+ TotalTime = vs.Take(7).Select(e => e.ToLong()).Sum().ToLong(),
+ };
+
+ var idle = current.IdleTime - (_systemTime?.IdleTime ?? 0);
+ var total = current.TotalTime - (_systemTime?.TotalTime ?? 0);
+ _systemTime = current;
+
+ CpuRate = total == 0 ? 0 : Math.Round((Double)(total - idle) / total, 4);
+ }
+ }
+ catch
+ {
+ _excludes.Add(nameof(CpuRate));
+ }
+ }
+ }
+
+ #region Linux辅助
+ /// <summary>获取Linux发行版名称</summary>
+ /// <returns>Linux发行版名称</returns>
+ public static String? GetLinuxName()
+ {
+ var fr = "/etc/redhat-release";
+ if (TryRead(fr, out var value)) return value;
+
+ var dr = "/etc/debian-release";
+ if (TryRead(dr, out value)) return value;
+
+ var sr = "/etc/os-release";
+ if (TryRead(sr, out value))
+ {
+ var dic = value.SplitAsDictionary("=", "\n", true);
+ if (dic.TryGetValue("PRETTY_NAME", out var pretty) && !pretty.IsNullOrEmpty()) return pretty.Trim();
+ if (dic.TryGetValue("NAME", out var name) && !name.IsNullOrEmpty()) return name.Trim();
+ }
+
+ var uname = "uname".Execute("-sr", 0, false)?.Trim();
+ if (!uname.IsNullOrEmpty())
+ {
+ // 支持Android系统名
+ var ss = uname.Split('-');
+ foreach (var item in ss)
+ {
+ if (!item.IsNullOrEmpty() && item.StartsWithIgnoreCase("Android")) return item;
+ }
+
+ return uname;
+ }
+
+ return null;
+ }
+
+ private static String? GetProductByRelease()
+ {
+ var di = "/etc/".AsDirectory();
+ if (!di.Exists) return null;
+
+ foreach (var fi in di.GetFiles("*-release"))
+ {
+ if (!fi.Name.EqualIgnoreCase("redhat-release", "debian-release", "os-release", "system-release"))
+ {
+ var dic = File.ReadAllText(fi.FullName).SplitAsDictionary("=", "\n", true);
+ if (dic.TryGetValue("BOARD", out var str)) return str;
+ if (dic.TryGetValue("BOARD_NAME", out str)) return str;
+ }
+ }
+
+ return null;
+ }
+ #endregion
+}
diff --git a/NewLife.Core/Common/MachineInfo.MacOS.cs b/NewLife.Core/Common/MachineInfo.MacOS.cs
new file mode 100644
index 0000000..c9b5b36
--- /dev/null
+++ b/NewLife.Core/Common/MachineInfo.MacOS.cs
@@ -0,0 +1,49 @@
+using System.Runtime.Versioning;
+
+namespace NewLife;
+
+/// <summary>机器信息 macOS 部分</summary>
+public partial class MachineInfo
+{
+#if NET5_0_OR_GREATER
+ [SupportedOSPlatform("macos")]
+#endif
+ private void LoadMacInfo()
+ {
+ var dic = ReadCommand("sw_vers");
+ if (dic != null)
+ {
+ if (dic.TryGetValue("ProductName", out var str)) OSName = str;
+ if (dic.TryGetValue("ProductVersion", out str)) OSVersion = str;
+ }
+
+ dic = ReadCommand("system_profiler", "SPHardwareDataType");
+ if (dic != null)
+ {
+ //if (dic2.TryGetValue("Model Name", out str)) Product = str;
+ if (dic.TryGetValue("Model Identifier", out var str)) Product = str;
+ if (dic.TryGetValue("Processor Name", out str)) Processor = str;
+ if (dic.TryGetValue("Memory", out str)) Memory = (UInt64)str.TrimSuffix("GB").Trim().ToLong() * 1024 * 1024 * 1024;
+ if (dic.TryGetValue("Serial Number (system)", out str)) Serial = str;
+ if (dic.TryGetValue("Hardware UUID", out str)) UUID = str;
+ }
+
+ if (Vendor.IsNullOrEmpty()) Vendor = "Apple";
+
+ dic = ReadCommand("diskutil", "info disk1");
+ if (dic != null)
+ {
+ if (dic.TryGetValue("Disk / Partition UUID", out var str)) DiskID = str;
+ }
+ }
+
+ #region macOS辅助
+ private static IDictionary<String, String>? ReadCommand(String cmd, String? arguments = null)
+ {
+ var str = cmd.Execute(arguments, 0, false);
+ if (str.IsNullOrEmpty()) return null;
+
+ return str.SplitAsDictionary(":", "\n", true);
+ }
+ #endregion
+}
diff --git a/NewLife.Core/Common/MachineInfo.Windows.cs b/NewLife.Core/Common/MachineInfo.Windows.cs
new file mode 100644
index 0000000..887c99a
--- /dev/null
+++ b/NewLife.Core/Common/MachineInfo.Windows.cs
@@ -0,0 +1,565 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Security;
+using NewLife.Log;
+using NewLife.Windows;
+using NewLife.Serialization;
+using System.Runtime.Versioning;
+
+#if NETFRAMEWORK || NET5_0_OR_GREATER
+using Microsoft.Win32;
+#endif
+
+namespace NewLife;
+
+/// <summary>机器信息 Windows 部分</summary>
+public partial class MachineInfo
+{
+ #region 公共基础设施
+ [DllImport("Kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
+ [SecurityCritical]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static extern Boolean GlobalMemoryStatusEx(ref MEMORYSTATUSEX lpBuffer);
+
+ internal struct MEMORYSTATUSEX
+ {
+ internal UInt32 dwLength;
+
+ internal UInt32 dwMemoryLoad;
+
+ internal UInt64 ullTotalPhys;
+
+ internal UInt64 ullAvailPhys;
+
+ internal UInt64 ullTotalPageFile;
+
+ internal UInt64 ullAvailPageFile;
+
+ internal UInt64 ullTotalVirtual;
+
+ internal UInt64 ullAvailVirtual;
+
+ internal UInt64 ullAvailExtendedVirtual;
+
+ internal void Init() => dwLength = checked((UInt32)Marshal.SizeOf(typeof(MEMORYSTATUSEX)));
+ }
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ private static extern Boolean GetSystemTimes(out FILETIME idleTime, out FILETIME kernelTime, out FILETIME userTime);
+
+ private struct FILETIME(Int64 time)
+ {
+ public UInt32 Low = (UInt32)time;
+
+ public UInt32 High = (UInt32)(time >> 32);
+
+ public readonly Int64 ToLong() => (Int64)(((UInt64)High << 32) | Low);
+ }
+
+ /// <summary>刷新内存和CPU数据(Windows/Linux共用 SystemTime)</summary>
+ private void RefreshMemoryAndCpu()
+ {
+ MEMORYSTATUSEX ms = default;
+ ms.Init();
+ if (GlobalMemoryStatusEx(ref ms))
+ {
+ Memory = ms.ullTotalPhys;
+ AvailableMemory = ms.ullAvailPhys;
+ FreeMemory = ms.ullAvailPhys;
+ }
+
+ GetSystemTimes(out var idleTime, out var kernelTime, out var userTime);
+
+ var current = new SystemTime
+ {
+ IdleTime = idleTime.ToLong(),
+ TotalTime = kernelTime.ToLong() + userTime.ToLong(),
+ };
+
+ var idle = current.IdleTime - (_systemTime?.IdleTime ?? 0);
+ var total = current.TotalTime - (_systemTime?.TotalTime ?? 0);
+ _systemTime = current;
+
+ CpuRate = total == 0 ? 0 : Math.Round((Double)(total - idle) / total, 4);
+ }
+
+#if NETFRAMEWORK || NET6_0_OR_GREATER
+ /// <summary>从注册表读取硬件信息(GUID/UUID/Vendor/Product/Processor)并做 csproduct 兜底</summary>
+ private void LoadHardwareFromRegistry()
+ {
+ var str = "";
+
+ var reg = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Cryptography");
+ if (reg != null) str = reg.GetValue("MachineGuid") + "";
+ if (str.IsNullOrEmpty())
+ {
+ reg = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64);
+ reg = reg?.OpenSubKey(@"SOFTWARE\Microsoft\Cryptography");
+ if (reg != null) str = reg.GetValue("MachineGuid") + "";
+ }
+
+ if (!str.IsNullOrEmpty()) Guid = str;
+
+ reg = Registry.LocalMachine.OpenSubKey(@"SYSTEM\HardwareConfig");
+ if (reg != null)
+ {
+ str = (reg.GetValue("LastConfig") + "")?.Trim('{', '}').ToUpper();
+
+ // UUID取不到时返回 FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF
+ if (!str.IsNullOrEmpty() && !str.EqualIgnoreCase("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF")) UUID = str;
+ }
+
+ reg = Registry.LocalMachine.OpenSubKey(@"HARDWARE\DESCRIPTION\System\BIOS");
+ reg ??= Registry.LocalMachine.OpenSubKey(@"SYSTEM\HardwareConfig\Current");
+ if (reg != null)
+ {
+ Product = (reg.GetValue("SystemProductName") + "").Replace("System Product Name", null);
+ if (Product.IsNullOrEmpty()) Product = reg.GetValue("BaseBoardProduct") + "";
+
+ Vendor = reg.GetValue("SystemManufacturer") + "";
+ if (Vendor.IsNullOrEmpty()) Vendor = reg.GetValue("ASUSTeK COMPUTER INC.") + "";
+ }
+
+ reg = Registry.LocalMachine.OpenSubKey(@"HARDWARE\DESCRIPTION\System\CentralProcessor\0");
+ if (reg != null) Processor = reg.GetValue("ProcessorNameString") + "";
+
+ // 旧版系统(如win2008)没有UUID的注册表项,需要用wmic查询。也可能因为过去的某个BUG,导致GUID跟UUID相等
+ if (UUID.IsNullOrEmpty() || UUID == Guid || Vendor.IsNullOrEmpty())
+ {
+ var csproduct = ReadWmic("csproduct", "Name", "UUID", "Vendor");
+ if (csproduct != null)
+ {
+ if (csproduct.TryGetValue("Name", out str) && !str.IsNullOrEmpty() && Product.IsNullOrEmpty()) Product = str;
+ if (csproduct.TryGetValue("UUID", out str) && !str.IsNullOrEmpty()) UUID = str;
+ if (csproduct.TryGetValue("Vendor", out str) && !str.IsNullOrEmpty()) Vendor = str;
+ }
+ }
+ }
+#endif
+ #endregion
+
+ #region Windows WMI辅助
+ /// <summary>通过 PowerShell 命令读取信息</summary>
+ /// <param name="command">PowerShell命令</param>
+ /// <returns>解析后的字典</returns>
+ public static IDictionary<String, String> ReadPowerShell(String command)
+ {
+ var dic = new Dictionary<String, String>(StringComparer.OrdinalIgnoreCase);
+
+ var args = $"-Command \"{command}\"";
+ var str = "powershell.exe".Execute(args, 3_000) ?? String.Empty;
+ if (!String.IsNullOrWhiteSpace(str))
+ {
+ foreach (var item in str.DecodeJson()!)
+ {
+ dic[item.Key] = item.Value?.ToString() ?? String.Empty;
+ }
+ }
+ return dic;
+ }
+
+ /// <summary>通过WMIC命令读取信息</summary>
+ /// <param name="type">WMI类型</param>
+ /// <param name="keys">查询字段</param>
+ /// <returns>解析后的字典</returns>
+ public static IDictionary<String, String> ReadWmic(String type, params String[] keys)
+ {
+ var dic = new Dictionary<String, IList<String>>(StringComparer.OrdinalIgnoreCase);
+ var dic2 = new Dictionary<String, String>(StringComparer.OrdinalIgnoreCase);
+
+ var args = $"{type} get {keys.Join(",")} /format:list";
+ var str = "wmic".Execute(args, 0, false)?.Trim();
+ if (str.IsNullOrEmpty()) return dic2;
+
+ var ss = str.Split("\r\n");
+ foreach (var item in ss)
+ {
+ var ks = item?.Split('=');
+ if (ks != null && ks.Length >= 2)
+ {
+ var k = ks[0].Trim();
+ var v = ks[1].Trim().TrimInvisible();
+ if (!k.IsNullOrEmpty() && !v.IsNullOrEmpty())
+ {
+ if (!dic.TryGetValue(k, out var list))
+ dic[k] = list = [];
+
+ list.Add(v);
+ }
+ }
+ }
+
+ // 排序,避免多个磁盘序列号时,顺序变动
+ foreach (var item in dic)
+ {
+ dic2[item.Key] = item.Value.OrderBy(e => e).Join();
+ }
+
+ return dic2;
+ }
+
+ private static readonly Dictionary<String, String> _wmiAliases = new(StringComparer.OrdinalIgnoreCase)
+ {
+ { "csproduct", "Win32_ComputerSystemProduct" },
+ { "os", "Win32_OperatingSystem" },
+ { "diskdrive", "Win32_DiskDrive" },
+ { "bios", "Win32_BIOS" },
+ { "baseboard", "Win32_BaseBoard" },
+ { "win32_battery", "Win32_Battery" },
+ };
+
+ /// <summary>通过 WMI 查询多属性(自动选择 COM/wmic)。优先使用 COM 避免开进程</summary>
+ /// <param name="type">WMI 类型,支持 wmic 格式如 "csproduct"、"/namespace:\\root\wmi path MSAcpi_ThermalZoneTemperature"</param>
+ /// <param name="keys">查询字段</param>
+ /// <returns>属性字典</returns>
+ public static IDictionary<String, String> ReadWmiComMulti(String type, params String[] keys)
+ {
+ // 解析命名空间和类名
+ var nameSpace = "root\\cimv2";
+ var wmiClass = type;
+
+ if (type.StartsWith("/namespace:"))
+ {
+ var p = type.IndexOf(" path ", StringComparison.Ordinal);
+ if (p > 0)
+ {
+ nameSpace = type["/namespace:".Length..p].Trim('\\');
+ wmiClass = type[(p + 6)..].Trim();
+ }
+ }
+ else if (type.StartsWith("path "))
+ {
+ wmiClass = type[5..].Trim();
+ }
+
+ // 映射 wmic 别名到 WMI 类名
+ var alias = wmiClass;
+ var whereIdx = wmiClass.IndexOf(" where ", StringComparison.OrdinalIgnoreCase);
+ if (whereIdx > 0) alias = wmiClass[..whereIdx];
+
+ if (_wmiAliases.TryGetValue(alias, out var fullClass))
+ {
+ wmiClass = whereIdx > 0 ? $"{fullClass}{wmiClass[whereIdx..]}" : fullClass;
+ }
+
+ // 逐属性查询
+ var dic = new Dictionary<String, String>(StringComparer.OrdinalIgnoreCase);
+ foreach (var key in keys)
+ {
+ var value = GetInfo(wmiClass, key, nameSpace);
+ if (!value.IsNullOrEmpty()) dic[key] = value;
+ }
+
+ return dic;
+ }
+
+ /// <summary>通过 WMI 查询单个属性(自动回退 wmic)。优先使用 COM 避免开进程</summary>
+ /// <param name="type">WMI 类型</param>
+ /// <param name="property">属性名</param>
+ /// <returns>属性值,失败返回 null</returns>
+ private static String? ReadWmiComSingle(String type, String property)
+ {
+ var dic = ReadWmiComMulti(type, property);
+ return dic.TryGetValue(property, out var v) && !v.IsNullOrEmpty() ? v : null;
+ }
+ #endregion
+
+#if !NETFRAMEWORK
+ #region 现代方法(非 NETFRAMEWORK)
+#if NET5_0_OR_GREATER
+ [SupportedOSPlatform("windows")]
+#endif
+ private void LoadWindowsInfo()
+ {
+ var str = "";
+
+ // 从注册表读取 MachineGuid(NET6+ 用 Registry,低版本用 reg.exe)
+#if NET6_0_OR_GREATER
+ LoadHardwareFromRegistry();
+#else
+ str = "reg".Execute(@"query HKLM\SOFTWARE\Microsoft\Cryptography /v MachineGuid", 0, false);
+ if (!str.IsNullOrEmpty() && str.Contains("REG_SZ")) Guid = str.Substring("REG_SZ", null).Trim();
+
+ var csproduct = ReadWmiComMulti("csproduct", "Name", "UUID", "Vendor");
+ if (csproduct != null)
+ {
+ if (csproduct.TryGetValue("Name", out str)) Product = str;
+ if (csproduct.TryGetValue("UUID", out str)) UUID = str;
+ if (csproduct.TryGetValue("Vendor", out str)) Vendor = str;
+ }
+#endif
+
+ // 获取操作系统名称和版本
+ var os = ReadWmiComMulti("os", "Caption", "Version");
+ if (os == null || os.Count == 0)
+ {
+ os = ReadPowerShell("Get-WmiObject Win32_OperatingSystem | Select-Object Caption, Version | ConvertTo-Json");
+ }
+ if (os is { Count: > 0 })
+ {
+ if (os.TryGetValue("Caption", out str)) OSName = str.TrimPrefix("Microsoft").Trim();
+ if (os.TryGetValue("Version", out str)) OSVersion = str;
+ }
+
+ // 磁盘和BIOS序列号
+ var diskStr = ReadWmiComSingle("diskdrive where mediatype=\"Fixed hard disk media\"", "serialnumber");
+ if (!diskStr.IsNullOrEmpty()) DiskID = diskStr?.Trim();
+
+ var sn = ReadWmiComSingle("bios", "serialnumber");
+ if (!sn.IsNullOrEmpty() && !sn.EqualIgnoreCase("System Serial Number")) Serial = sn?.Trim();
+
+ var boardStr = ReadWmiComSingle("baseboard", "serialnumber");
+ if (!boardStr.IsNullOrEmpty()) Board = boardStr?.Trim();
+
+ //// 不要在刷新里面取CPU负载,因为运行wmic会导致CPU负载很不准确,影响测量
+ //var cpu = ReadWmic("cpu", "Name", "ProcessorId", "LoadPercentage");
+ //if (cpu != null)
+ //{
+ // if (cpu.TryGetValue("Name", out str)) Processor = str;
+ // //if (cpu.TryGetValue("ProcessorId", out str)) CpuID = str;
+ // if (cpu.TryGetValue("LoadPercentage", out str)) CpuRate = (Single)(str.ToDouble() / 100);
+ //}
+
+ // OS 名称和版本的兜底
+ if (OSName.IsNullOrEmpty())
+ OSName = RuntimeInformation.OSDescription.TrimPrefix("Microsoft").Trim();
+ if (OSVersion.IsNullOrEmpty())
+ OSVersion = Environment.OSVersion.Version.ToString();
+ }
+
+ /// <summary>刷新 Windows 动态数据(现代路径)</summary>
+ private void RefreshWindows()
+ {
+ RefreshMemoryAndCpu();
+
+ var power = new PowerStatus();
+
+ if (!_excludes.Contains(nameof(Temperature)))
+ {
+ var str = ReadWmiComSingle(@"/namespace:\\root\wmi path MSAcpi_ThermalZoneTemperature", "CurrentTemperature");
+ if (!str.IsNullOrEmpty())
+ {
+ Temperature = (str.SplitAsInt().Average() - 2732) / 10.0;
+ }
+ else
+ {
+ if (XTrace.Log.Level <= LogLevel.Debug) XTrace.WriteLine("Temperature信息无法读取");
+ _excludes.Add(nameof(Temperature));
+ Temperature = 0;
+ }
+ }
+
+ if (power.BatteryLifePercent > 0)
+ Battery = power.BatteryLifePercent;
+ else if (!_excludes.Contains(nameof(Battery)))
+ {
+ var str = ReadWmiComSingle("path win32_battery", "EstimatedChargeRemaining");
+ if (!str.IsNullOrEmpty())
+ {
+ Battery = str.SplitAsInt().Average() / 100.0;
+ }
+ else
+ {
+ if (XTrace.Log.Level <= LogLevel.Debug) XTrace.WriteLine("Battery信息无法读取");
+ _excludes.Add(nameof(Battery));
+ Battery = 0;
+ }
+ }
+ }
+
+ /// <summary>获取WMI信息。net*-windows反射System.Management并回退COM;其它运行时用COM并回退wmic</summary>
+ /// <param name="path">WMI路径</param>
+ /// <param name="property">属性名</param>
+ /// <param name="nameSpace">命名空间,默认 root\cimv2</param>
+ /// <returns>查询结果</returns>
+ public static String GetInfo(String path, String property, String? nameSpace = null)
+ {
+ // 规范化命名空间:WMI 要求反斜杠分隔,调用者可能传入 root/wmi 等正斜杠格式
+ if (nameSpace != null) nameSpace = nameSpace.Replace("/", "\\");
+
+ var ns = nameSpace ?? "root\\cimv2";
+
+#if __WIN__
+ // net*-windows:优先反射 System.Management 走标准 WMI,失败回退 COM;两者均失败降级 wmic 兜底
+ {
+ var value = QueryWmiSystemManagement(path, property, ns);
+ if (value != null) return value;
+
+ value = QueryWmiCom(path, property, ns);
+ if (value != null) return value;
+
+ // 两种 WMI 路径均失败(如 WMI 服务异常),降级到 wmic 进程兜底
+ if (XTrace.Log.Level <= LogLevel.Debug)
+ XTrace.WriteLine("WMI 均失败,回退 wmic: {0}.{1}", path, property);
+ }
+#else
+ // 其它运行时(netstandard/net*非windows):优先 COM,失败回退 wmic
+#if NET5_0_OR_GREATER
+ if (OperatingSystem.IsWindows())
+#else
+ if (Runtime.Windows)
+#endif
+ {
+ var value = QueryWmiCom(path, property, ns);
+ if (value != null) return value;
+ }
+#endif
+
+ // wmic 兜底(__WIN__ 降级 + 其它运行时)。不带命名空间时也要用 path 前缀以支持完整类名
+ var type = !ns.EqualIgnoreCase("root\\cimv2") ? $"/namespace:\\\\{ns} path {path}" : $"path {path}";
+ var dic = ReadWmic(type, property);
+ return dic.TryGetValue(property, out var v) ? v : "";
+ }
+
+ /// <summary>通过反射 System.Management 查询 WMI 属性。用于 net*-windows,不新增编译期依赖,不开进程</summary>
+ /// <remarks>
+ /// System.Management.dll 随 Windows 桌面运行时提供(net*-windows),通过反射调用避免核心库直接引用该程序集。
+ /// 内部使用 ManagementObjectSearcher(nameSpace, wql).Get() 枚举结果并读取属性。
+ /// </remarks>
+ /// <param name="wmiClass">WMI 类名,如 Win32_OperatingSystem</param>
+ /// <param name="property">属性名</param>
+ /// <param name="nameSpace">命名空间,如 root\cimv2</param>
+ /// <param name="throwOnError">失败时是否抛出异常,默认false吞掉异常返回null</param>
+ /// <returns>查询结果,失败返回null</returns>
+#if NET5_0_OR_GREATER
+ [SupportedOSPlatform("windows")]
+#endif
+ internal static String? QueryWmiSystemManagement(String wmiClass, String property, String nameSpace, Boolean throwOnError = false)
+ {
+ try
+ {
+ // 反射加载 System.Management(Windows桌面运行时内置,无编译期引用)
+ var asm = Assembly.Load("System.Management");
+ var searcherType = asm.GetType("System.Management.ManagementObjectSearcher")!;
+ var moType = asm.GetType("System.Management.ManagementObject")!;
+ var enumType = asm.GetType("System.Management.ManagementObjectCollection")!;
+
+ // new ManagementObjectSearcher(nameSpace, wql)
+ var wql = $"SELECT {property} FROM {wmiClass}";
+ var searcher = Activator.CreateInstance(searcherType, nameSpace, wql);
+ if (searcher == null) return null;
+
+ // .Get() → ManagementObjectCollection
+ var getMethod = searcherType.GetMethod("Get", Type.EmptyTypes)!;
+ var results = getMethod.Invoke(searcher, null);
+ if (results == null) return null;
+
+ // foreach (ManagementObject obj in results)
+ var bbs = new List<String>();
+ var getEnumeratorMethod = enumType.GetMethod("GetEnumerator")!;
+ var enumerator = getEnumeratorMethod.Invoke(results, null);
+ var moveNextMethod = enumerator!.GetType().GetMethod("MoveNext")!;
+ var currentProperty = enumerator.GetType().GetProperty("Current")!;
+
+ var itemProp = moType.GetProperty("Item", [typeof(String)])!;
+ var getValueMethod = itemProp.GetGetMethod()!;
+
+ while ((Boolean)moveNextMethod.Invoke(enumerator, null)!)
+ {
+ var obj = currentProperty.GetValue(enumerator);
+ if (obj == null) continue;
+
+ var val = getValueMethod.Invoke(obj, [property]);
+ if (val != null)
+ {
+ var v = val.ToString()?.TrimInvisible()?.Trim();
+ if (!v.IsNullOrEmpty()) bbs.Add(v);
+ }
+ }
+
+ // 清理 COM 引用
+ if (results is IDisposable d1) d1.Dispose();
+ if (searcher is IDisposable d2) d2.Dispose();
+
+ if (bbs.Count > 0)
+ {
+ bbs.Sort();
+ return bbs.Distinct().Join();
+ }
+ }
+ catch (Exception ex)
+ {
+ if (throwOnError) throw;
+ if (XTrace.Log.Level <= LogLevel.Debug)
+ {
+ XTrace.WriteLine("QueryWmiSystemManagement 失败: {0}.{1} @ {2}", wmiClass, property, nameSpace);
+ XTrace.WriteException(ex is TargetInvocationException tie ? tie.InnerException! : ex);
+ }
+ }
+
+ return null;
+ }
+
+ /// <summary>通过 COM 查询 WMI 属性。使用 Windows 内置 winmgmts 组件,不新增依赖,不开进程</summary>
+ /// <remarks>
+ /// 通过 winmgmts moniker 绑定 SWbemServices,执行 WQL 查询并枚举结果,SWbemObject 按名暴露 WMI 属性。
+ /// 相比 System.Management 无需该程序集,适用于 netstandard/net*非windows 运行时下的 Windows 主机。
+ /// COM 查询在部分环境可能失败,此时调用方应回退 wmic。
+ /// </remarks>
+ /// <param name="wmiClass">WMI 类名,如 Win32_OperatingSystem</param>
+ /// <param name="property">属性名</param>
+ /// <param name="nameSpace">命名空间,如 root\cimv2</param>
+ /// <param name="throwOnError">失败时是否抛出异常,默认false吞掉异常返回null</param>
+ /// <returns>查询结果,失败返回null</returns>
+#if NET5_0_OR_GREATER
+ [SupportedOSPlatform("windows")]
+#endif
+ internal static String? QueryWmiCom(String wmiClass, String property, String nameSpace, Boolean throwOnError = false)
+ {
+ Object? services = null;
+ Object? results = null;
+ try
+ {
+ // 通过 winmgmts moniker 绑定 WMI 服务,规避 SWbemLocator.ConnectServer 的 IDispatch 参数编组问题
+ var moniker = $"winmgmts:\\\\.\\{nameSpace}";
+ services = Marshal.BindToMoniker(moniker);
+ if (services == null) return null;
+
+ // ExecQuery 执行 WQL 查询,返回 SWbemObjectSet
+ var wql = $"SELECT {property} FROM {wmiClass}";
+ results = services.GetType().InvokeMember("ExecQuery", BindingFlags.InvokeMethod, null, services, new Object[] { wql });
+ if (results == null) return null;
+
+ // SWbemObjectSet 实现 IEnumVARIANT,可直接 foreach;SWbemObject 通过 IDispatch 按名暴露 WMI 属性
+ var bbs = new List<String>();
+ foreach (var obj in (System.Collections.IEnumerable)results)
+ {
+ if (obj == null) continue;
+ try
+ {
+ var val = obj.GetType().InvokeMember(property, BindingFlags.GetProperty, null, obj, null);
+ if (val != null)
+ {
+ var v = val.ToString()?.TrimInvisible()?.Trim();
+ if (!v.IsNullOrEmpty()) bbs.Add(v);
+ }
+ }
+ finally { try { Marshal.FinalReleaseComObject(obj); } catch { } }
+ }
+
+ if (bbs.Count > 0)
+ {
+ bbs.Sort();
+ return bbs.Distinct().Join();
+ }
+ }
+ catch (Exception ex)
+ {
+ if (throwOnError) throw;
+ if (XTrace.Log.Level <= LogLevel.Debug)
+ {
+ XTrace.WriteLine("QueryWmiCom 失败: {0}.{1} @ {2}", wmiClass, property, nameSpace);
+ XTrace.WriteException(ex is TargetInvocationException tie ? tie.InnerException! : ex);
+ }
+ }
+ finally
+ {
+ if (results != null) try { Marshal.FinalReleaseComObject(results); } catch { }
+ if (services != null) try { Marshal.FinalReleaseComObject(services); } catch { }
+ }
+
+ return null;
+ }
+ #endregion
+#endif
+}
diff --git a/NewLife.Core/Common/MachineInfo.Windows.NetFX.cs b/NewLife.Core/Common/MachineInfo.Windows.NetFX.cs
new file mode 100644
index 0000000..2eac212
--- /dev/null
+++ b/NewLife.Core/Common/MachineInfo.Windows.NetFX.cs
@@ -0,0 +1,150 @@
+#if NETFRAMEWORK
+using System.Management;
+using Microsoft.VisualBasic.Devices;
+using Microsoft.Win32;
+using NewLife.Log;
+using NewLife.Reflection;
+using NewLife.Windows;
+
+namespace NewLife;
+
+/// <summary>机器信息 Windows NETFRAMEWORK 兼容部分。覆盖 Windows.cs 中的同名方法</summary>
+public partial class MachineInfo
+{
+ /// <summary>加载 Windows 静态信息(NETFRAMEWORK 路径)</summary>
+ private void LoadWindowsInfo()
+ {
+ // 从注册表读取硬件信息
+ LoadHardwareFromRegistry();
+
+ // 获取内存大小
+ {
+ var ci = new ComputerInfo();
+ Memory = ci.TotalPhysicalMemory;
+ }
+
+ // 获取操作系统名称和版本
+ try
+ {
+ var ci = new ComputerInfo();
+
+ // 系统名取WMI可能出错
+ OSName = ci.OSFullName?.Replace("®", null).TrimPrefix("Microsoft").Trim();
+ OSVersion = ci.OSVersion;
+ }
+ catch
+ {
+ try
+ {
+ var reg2 = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion");
+ if (reg2 != null)
+ {
+ OSName = reg2.GetValue("ProductName") + "";
+ OSVersion = reg2.GetValue("ReleaseId") + "";
+ }
+ }
+ catch (Exception ex)
+ {
+ if (XTrace.Log.Level <= LogLevel.Debug) XTrace.WriteException(ex);
+ }
+ }
+
+ // 磁盘和BIOS序列号
+ //Processor = GetInfo("Win32_Processor", "Name");
+ //CpuID = GetInfo("Win32_Processor", "ProcessorId");
+ //var uuid = GetInfo("Win32_ComputerSystemProduct", "UUID");
+ //Product = GetInfo("Win32_ComputerSystemProduct", "Name");
+ DiskID = GetInfo("Win32_DiskDrive where mediatype=\"Fixed hard disk media\"", "SerialNumber");
+
+ var sn = GetInfo("Win32_BIOS", "SerialNumber");
+ if (!sn.IsNullOrEmpty() && !sn.EqualIgnoreCase("System Serial Number")) Serial = sn;
+ Board = GetInfo("Win32_BaseBoard", "SerialNumber");
+ }
+
+ /// <summary>刷新 Windows 动态数据(NETFRAMEWORK 路径)</summary>
+ private void RefreshWindows()
+ {
+ RefreshMemoryAndCpu();
+
+ var power = new PowerStatus();
+
+ if (!_excludes.Contains(nameof(Temperature)))
+ {
+ // 读取主板温度,不太准。标准方案是ring0通过IOPort读取CPU温度,太难在基础类库实现
+ var str = GetInfo("Win32_TemperatureProbe", "CurrentReading");
+ if (!str.IsNullOrEmpty())
+ {
+ Temperature = str.SplitAsInt().Average();
+ }
+ else
+ {
+ str = GetInfo("MSAcpi_ThermalZoneTemperature", "CurrentTemperature", "root/wmi");
+ if (!str.IsNullOrEmpty())
+ Temperature = (str.SplitAsInt().Average() - 2732) / 10.0;
+ else
+ {
+ if (XTrace.Log.Level <= LogLevel.Debug) XTrace.WriteLine("Temperature信息无法读取");
+ _excludes.Add(nameof(Temperature));
+ Temperature = 0;
+ }
+ }
+ }
+
+ if (power.BatteryLifePercent > 0)
+ Battery = power.BatteryLifePercent;
+ else if (!_excludes.Contains(nameof(Battery)))
+ {
+ // 电池剩余
+ var str = GetInfo("Win32_Battery", "EstimatedChargeRemaining");
+ if (!str.IsNullOrEmpty())
+ Battery = str.SplitAsInt().Average() / 100.0;
+ else
+ {
+ if (XTrace.Log.Level <= LogLevel.Debug) XTrace.WriteLine("Battery信息无法读取");
+ _excludes.Add(nameof(Battery));
+ Battery = 0;
+ }
+ }
+ }
+
+ /// <summary>获取WMI信息(NETFRAMEWORK 路径,直接使用 ManagementObjectSearcher)</summary>
+ /// <param name="path">WMI路径</param>
+ /// <param name="property">属性名</param>
+ /// <param name="nameSpace">命名空间,默认 root\cimv2</param>
+ /// <returns>查询结果</returns>
+ public static String GetInfo(String path, String property, String? nameSpace = null)
+ {
+ // 规范化命名空间:WMI 要求反斜杠分隔,调用者可能传入 root/wmi 等正斜杠格式
+ if (nameSpace != null) nameSpace = nameSpace.Replace("/", "\\");
+
+ // Linux Mono不支持WMI
+ if (Runtime.Mono) return "";
+
+ var bbs = new List<String>();
+ try
+ {
+ var wql = $"Select {property} From {path}";
+ var cimobject = new ManagementObjectSearcher(nameSpace, wql);
+ var moc = cimobject.Get();
+ foreach (var mo in moc)
+ {
+ var val = mo?.Properties?[property]?.Value;
+ if (val != null)
+ {
+ var v = val.ToString().TrimInvisible()?.Trim();
+ if (v != null) bbs.Add(v);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ if (XTrace.Log.Level <= LogLevel.Debug) XTrace.WriteLine("WMI.GetInfo({0})失败!{1}", path, ex.Message);
+ return "";
+ }
+
+ bbs.Sort();
+
+ return bbs.Distinct().Join();
+ }
+}
+#endif