修剪不可见字符。仅修剪ASCII,不包含Unicode
智能大石头 authored at 2023-03-09 08:19:44 大石头 committed at 2023-03-09 14:44:19
22.62 KiB
X_NET20
using System.ComponentModel;
using System.Diagnostics;
using System.Management;
using System.Net.NetworkInformation;
using System.Runtime.InteropServices;
using System.Security;
using Microsoft.VisualBasic.Devices;
using Microsoft.Win32;
using NewLife.Log;
using NewLife.Model;
using NewLife.Serialization;

namespace NewLife;

/// <summary>机器信息</summary>
/// <remarks>
/// 文档 https://newlifex.com/core/machine_info
/// 
/// 刷新信息成本较高,建议采用单例模式
/// </remarks>
public class MachineInfo
{
    #region 属性
    /// <summary>系统名称</summary>
    [DisplayName("系统名称")]
    public String OSName { get; set; }

    /// <summary>系统版本</summary>
    [DisplayName("系统版本")]
    public String OSVersion { get; set; }

    /// <summary>产品名称。制造商</summary>
    [DisplayName("产品名称")]
    public String Product { get; set; }

    /// <summary>处理器型号</summary>
    [DisplayName("处理器型号")]
    public String Processor { get; set; }

    ///// <summary>处理器序列号</summary>
    //public String CpuID { get; set; }

    /// <summary>硬件唯一标识。取主板编码,部分品牌存在重复</summary>
    [DisplayName("硬件唯一标识")]
    public String UUID { get; set; }

    /// <summary>软件唯一标识。系统标识,操作系统重装后更新,Linux系统的machine_id,Android的android_id,Ghost系统存在重复</summary>
    [DisplayName("软件唯一标识")]
    public String Guid { get; set; }

    /// <summary>计算机序列号。适用于品牌机,跟笔记本标签显示一致</summary>
    [DisplayName("计算机序列号")]
    public String Serial { get; set; }

    /// <summary>主板。序列号或家族信息</summary>
    [DisplayName("主板")]
    public String Board { get; set; }

    /// <summary>磁盘序列号</summary>
    [DisplayName("磁盘序列号")]
    public String DiskID { get; set; }

    /// <summary>内存总量。单位Byte</summary>
    [DisplayName("内存总量")]
    public UInt64 Memory { get; set; }

    /// <summary>可用内存。单位Byte</summary>
    [DisplayName("可用内存")]
    public UInt64 AvailableMemory { get; set; }

    /// <summary>CPU占用率</summary>
    [DisplayName("CPU占用率")]
    public Single CpuRate { get; set; }

    /// <summary>网络上行速度。字节每秒,初始化后首次读取为0</summary>
    [DisplayName("网络上行速度")]
    public UInt64 UplinkSpeed { get; set; }

    /// <summary>网络下行速度。字节每秒,初始化后首次读取为0</summary>
    [DisplayName("网络下行速度")]
    public UInt64 DownlinkSpeed { get; set; }

    /// <summary>温度。单位度</summary>
    [DisplayName("温度")]
    public Double Temperature { get; set; }

    /// <summary>电池剩余。小于1的小数,常用百分比表示</summary>
    [DisplayName("电池剩余")]
    public Double Battery { get; set; }
    #endregion

    #region 构造
    /// <summary>当前机器信息。默认null,在RegisterAsync后才能使用</summary>
    public static MachineInfo Current { get; set; }

    static MachineInfo() => RegisterAsync();

    private static Task<MachineInfo> _task;
    /// <summary>异步注册一个初始化后的机器信息实例</summary>
    /// <returns></returns>
    public static Task<MachineInfo> RegisterAsync()
    {
        if (_task != null) return _task;

        return _task = Task.Factory.StartNew(() =>
        {
            var set = Setting.Current;
            var dataPath = set.DataPath;
            if (dataPath.IsNullOrEmpty()) dataPath = "Data";

            // 文件缓存,加快机器信息获取
            var file = Path.GetTempPath().CombinePath("machine_info.json");
            var file2 = dataPath.CombinePath("machine_info.json").GetBasePath();
            if (Current == null)
            {
                var f = file;
                if (!File.Exists(f)) f = file2;
                if (File.Exists(f))
                {
                    try
                    {
                        //XTrace.WriteLine("Load MachineInfo {0}", f);
                        Current = File.ReadAllText(f).ToJsonEntity<MachineInfo>();
                    }
                    catch { }
                }
            }

            var mi = Current ?? new MachineInfo();

            mi.Init();
            Current = mi;

            //// 定时刷新
            //if (msRefresh > 0) mi._timer = new TimerX(s => mi.Refresh(), null, msRefresh, msRefresh) { Async = true };

            // 注册到对象容器
            ObjectContainer.Current.AddSingleton(mi);

            try
            {
                var json = mi.ToJson(true);
                File.WriteAllText(file.EnsureDirectory(true), json);
                File.WriteAllText(file2.EnsureDirectory(true), json);
            }
            catch { }

            return mi;
        });
    }

    /// <summary>获取当前信息,如果未设置则等待异步注册结果</summary>
    /// <returns></returns>
    public static MachineInfo GetCurrent() => Current ?? RegisterAsync().Result;

    /// <summary>从对象容器中获取一个已注册机器信息实例</summary>
    /// <returns></returns>
    public static MachineInfo Resolve() => ObjectContainer.Current.Resolve<MachineInfo>();
    #endregion

    #region 方法
    /// <summary>刷新</summary>
    public void Init()
    {
        var osv = Environment.OSVersion;
        if (OSVersion.IsNullOrEmpty()) OSVersion = osv.Version + "";
        if (OSName.IsNullOrEmpty()) OSName = (osv + "").TrimStart("Microsoft").TrimEnd(OSVersion).Trim();
        if (Guid.IsNullOrEmpty()) Guid = "";

        try
        {
            //if (Runtime.Windows)
            LoadWindowsInfoFx();
        }
        catch (Exception ex)
        {
            if (XTrace.Log.Level <= LogLevel.Debug) XTrace.WriteException(ex);
        }

        // 裁剪不可见字符,顺带去掉两头空白
        OSName = OSName.TrimInvisible()?.Trim();
        OSVersion = OSVersion.TrimInvisible()?.Trim();
        Product = Product.TrimInvisible()?.Trim();
        Processor = Processor.TrimInvisible()?.Trim();
        UUID = UUID.TrimInvisible()?.Trim();
        Guid = Guid.TrimInvisible()?.Trim();
        Serial = Serial.TrimInvisible()?.Trim();
        Board = Board.TrimInvisible()?.Trim();
        DiskID = DiskID.TrimInvisible()?.Trim();

        // 无法读取系统标识时,随机生成一个guid,借助文件缓存确保其不变
        if (Guid.IsNullOrEmpty()) Guid = "0-" + System.Guid.NewGuid().ToString();
        if (UUID.IsNullOrEmpty()) UUID = "0-" + System.Guid.NewGuid().ToString();

        try
        {
            Refresh();
        }
        catch { }
    }

    private void LoadWindowsInfoFx()
    {
        var machine_guid = "";

        var reg = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Cryptography");
        if (reg != null) machine_guid = reg.GetValue("MachineGuid") + "";
        if (machine_guid.IsNullOrEmpty())
        {
            //reg = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64);
            //if (reg != null) machine_guid = reg.GetValue("MachineGuid") + "";
        }

        var ci = new ComputerInfo();
        try
        {
            Memory = ci.TotalPhysicalMemory;

            // 系统名取WMI可能出错
            OSName = ci.OSFullName.TrimStart("Microsoft").Trim();
            OSVersion = ci.OSVersion;
        }
        catch
        {
            var reg2 = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion");
            if (reg2 != null)
            {
                OSName = reg2.GetValue("ProductName") + "";
                OSVersion = reg2.GetValue("ReleaseId") + "";
            }
        }

        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", "SerialNumber");

        var sn = GetInfo("Win32_BIOS", "SerialNumber");
        if (!sn.IsNullOrEmpty() && !sn.EqualIgnoreCase("System Serial Number")) Serial = sn;
        Board = GetInfo("Win32_BaseBoard", "SerialNumber");

        // UUID取不到时返回 FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF
        if (!uuid.IsNullOrEmpty() && !uuid.EqualIgnoreCase("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF")) UUID = uuid;

        //// 可能因WMI导致读取UUID失败
        //if (UUID.IsNullOrEmpty())
        //{
        //    var reg3 = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion");
        //    if (reg3 != null) UUID = reg3.GetValue("ProductId") + "";
        //}

        if (!machine_guid.IsNullOrEmpty()) Guid = machine_guid;

        //// 读取主板温度,不太准。标准方案是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;
        //}

        //// 电池剩余
        //str = GetInfo("Win32_Battery", "EstimatedChargeRemaining");
        //if (!str.IsNullOrEmpty()) Battery = str.SplitAsInt().Average() / 100.0;
    }

    private ICollection<String> _excludes = new List<String>();

    /// <summary>获取实时数据,如CPU、内存、温度</summary>
    public void Refresh()
    {
        if (Runtime.Windows)
        {
            MEMORYSTATUSEX ms = default;
            ms.Init();
            if (GlobalMemoryStatusEx(ref ms))
            {
                Memory = ms.ullTotalPhys;
                AvailableMemory = ms.ullAvailPhys;
            }
        }
        // 特别识别Linux发行版
        else if (Runtime.Linux)
        {
            var dic = ReadInfo("/proc/meminfo");
            if (dic != null)
            {
                if (dic.TryGetValue("MemTotal", out var str))
                    Memory = (UInt64)str.TrimEnd(" kB").ToInt() * 1024;

                if (dic.TryGetValue("MemAvailable", out str) ||
                    dic.TryGetValue("MemFree", out str))
                    AvailableMemory = (UInt64)str.TrimEnd(" kB").ToInt() * 1024;
            }

            // respberrypi + fedora
            if (TryRead("/sys/class/thermal/thermal_zone0/temp", out var 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))
                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;
            }

            //var upt = Execute("uptime");
            //if (!upt.IsNullOrEmpty())
            //{
            //    str = upt.Substring("load average:");
            //    if (!str.IsNullOrEmpty()) CpuRate = (Single)str.Split(",")[0].ToDouble();
            //}

            //file = "/proc/loadavg";
            //if (File.Exists(file)) CpuRate = (Single)File.ReadAllText(file).Substring(null, " ").ToDouble() / Environment.ProcessorCount;

            var file = "/proc/stat";
            if (File.Exists(file))
            {
                // CPU指标:user,nice, system, idle, iowait, irq, softirq
                // cpu  57057 0 14420 1554816 0 443 0 0 0 0

                using var reader = new StreamReader(file);
                var line = reader.ReadLine();
                if (!line.IsNullOrEmpty() && line.StartsWith("cpu"))
                {
                    var vs = line.TrimStart("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 : ((Single)(total - idle) / total);
                }
            }
        }

        if (Runtime.Windows)
        {
            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 : ((Single)(total - idle) / total);

#if __CORE__
            if (!_excludes.Contains(nameof(Temperature)))
            {
                var temp = ReadWmic(@"/namespace:\\root\wmi path MSAcpi_ThermalZoneTemperature", "CurrentTemperature");
                if (temp != null && temp.Count > 0)
                {
                    if (temp.TryGetValue("CurrentTemperature", out var str) && !str.IsNullOrEmpty())
                        Temperature = (str.SplitAsInt().Average() - 2732) / 10.0;
                }
                else
                    _excludes.Add(nameof(Temperature));
            }

            if (!_excludes.Contains(nameof(Battery)))
            {
                var battery = ReadWmic("path win32_battery", "EstimatedChargeRemaining");
                if (battery != null && battery.Count > 0)
                {
                    if (battery.TryGetValue("EstimatedChargeRemaining", out var str) && !str.IsNullOrEmpty())
                        Battery = str.SplitAsInt().Average() / 100.0;
                }
                else
                    _excludes.Add(nameof(Battery));
            }
#else
            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;
                }

                if (str.IsNullOrEmpty()) _excludes.Add(nameof(Temperature));
            }

            if (!_excludes.Contains(nameof(Battery)))
            {
                // 电池剩余
                var str = GetInfo("Win32_Battery", "EstimatedChargeRemaining");
                if (!str.IsNullOrEmpty())
                    Battery = str.SplitAsInt().Average() / 100.0;
                else
                    _excludes.Add(nameof(Battery));
            }
#endif
        }

        RefreshSpeed();
    }

    private Int64 _lastTime;
    private Int64 _lastSent;
    private Int64 _lastReceived;
    /// <summary>刷新网络速度</summary>
    public void RefreshSpeed()
    {
        var sent = 0L;
        var received = 0L;
        foreach (var ni in NetworkInterface.GetAllNetworkInterfaces())
        {
            var st = ni.GetIPv4Statistics();
            sent += st.BytesSent;
            received += st.BytesReceived;
        }

        var now = Runtime.TickCount64;
        if (_lastTime > 0)
        {
            var interval = now - _lastTime;
            if (interval > 0)
            {
                var s1 = (sent - _lastSent) * 1000 / interval;
                var s2 = (received - _lastReceived) * 1000 / interval;
                if (s1 >= 0) UplinkSpeed = (UInt64)s1;
                if (s2 >= 0) DownlinkSpeed = (UInt64)s2;
            }
        }

        _lastSent = sent;
        _lastReceived = received;
        _lastTime = now;
    }
    #endregion

    #region 辅助
    private static Boolean TryRead(String fileName, out String value)
    {
        value = null;

        if (!File.Exists(fileName)) return false;

        try
        {
            value = File.ReadAllText(fileName)?.Trim();
            if (value.IsNullOrEmpty()) return false;
        }
        catch { return false; }

        return true;
    }

    private static IDictionary<String, String> ReadInfo(String file, Char separate = ':')
    {
        if (file.IsNullOrEmpty() || !File.Exists(file)) return null;

        var dic = new Dictionary<String, String>(StringComparer.OrdinalIgnoreCase);

        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] = value.TrimInvisible();
                }
            }
        }

        return dic;
    }

    private static String Execute(String cmd, String arguments = null)
    {
        try
        {
            var psi = new ProcessStartInfo(cmd, arguments)
            {
                // UseShellExecute 必须 false,以便于后续重定向输出流
                UseShellExecute = false,
                CreateNoWindow = true,
                RedirectStandardOutput = true
            };
            var process = Process.Start(psi);
            if (!process.WaitForExit(3_000))
            {
                process.Kill();
                return null;
            }

            return process.StandardOutput.ReadToEnd();
        }
        catch { return null; }
    }
    #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>
    /// <returns>返回可用空间,字节,获取失败返回-1</returns>
    public static Int64 GetFreeSpace(String path = null)
    {
        if (path.IsNullOrEmpty()) path = ".";

        var driveInfo = new DriveInfo(Path.GetPathRoot(path.GetFullPath()));
        if (driveInfo == null || !driveInfo.IsReady) return -1;

        try
        {
            return driveInfo.AvailableFreeSpace;
        }
        catch { return -1; }
    }

    /// <summary>获取指定目录下文件名,支持去掉后缀的去重,主要用于Linux</summary>
    /// <param name="path"></param>
    /// <param name="trimSuffix"></param>
    /// <returns></returns>
    public static ICollection<String> GetFiles(String path, Boolean trimSuffix = false)
    {
        var list = new List<String>();
        if (path.IsNullOrEmpty()) return list;

        var di = path.AsDirectory();
        if (!di.Exists) return list;

        var list2 = di.GetFiles().Select(e => e.Name).ToList();
        foreach (var item in list2)
        {
            var line = item?.Trim();
            if (!line.IsNullOrEmpty())
            {
                if (trimSuffix)
                {
                    if (!list2.Any(e => e != line && line.StartsWith(e))) list.Add(line);
                }
                else
                {
                    list.Add(line);
                }
            }
        }

        return list;
    }
    #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;
        public Int64 TotalTime;
    }

    private SystemTime _systemTime;

    /// <summary>获取WMI信息</summary>
    /// <param name="path"></param>
    /// <param name="property"></param>
    /// <param name="nameSpace"></param>
    /// <returns></returns>
    public static String GetInfo(String path, String property, String nameSpace = null)
    {
        // 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) bbs.Add(val.ToString().TrimInvisible().Trim());
            }
        }
        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();
    }
    #endregion
}