Merge branch 'master' into v3.1
大石头 authored at 2024-07-13 19:03:18
11.14 KiB
Stardust
#if !NET40
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Net.NetworkInformation;
using System.Reflection;
using NewLife;
using NewLife.Log;
using Stardust.Windows;

namespace Stardust.Managers;

/// <summary>机器信息提供者,增强机器信息获取能力</summary>
public class MachineInfoProvider : IMachineInfo
{
    static readonly Dictionary<String, String> _allwinner_archs = new()
    {
        ["sun8i"] = "Cortex-A7",
        ["sun9i"] = "Cortex-A15",
        ["sun50i"] = "Cortex-A53",
        ["sun55i"] = "Cortex-A55",
        ["sun60i"] = "Cortex-A76",
    };
    static readonly Dictionary<String, String> _allwinner_socs = new()
    {
        ["sun8i"] = "H3",
        ["sun50i-h616"] = "H616",
        ["sun50iw9"] = "H616",
        ["sun50iw10"] = "A133"
    };

    /// <summary>初始化时执行一次</summary>
    /// <param name="info"></param>
    public void Init(MachineInfo info)
    {
        // 本地保存,然后统一赋值
        var mi = new MachineInfo();

        // 从设备树获取信息
        var devTree = "/proc/device-tree/";
        if (!Directory.Exists(devTree)) devTree = "/sys/firmware/devicetree/base/";
        if (Directory.Exists(devTree))
        {
            if (TryRead(devTree + "model", out var str))
            {
                if (mi.Product.IsNullOrEmpty()) mi.Product = str;
            }
            if (TryRead(devTree + "compatible", out str))
            {
                var ss = str.Split(',');
                if (ss.Length >= 3)
                {
                    if (mi.Vendor.IsNullOrEmpty()) mi.Vendor = ss[0];
                    if (mi.Board.IsNullOrEmpty()) mi.Board = ss[1];
                    if (mi.Processor.IsNullOrEmpty()) mi.Processor = ss[2];
                }
                else if (ss.Length >= 2)
                {
                    if (mi.Vendor.IsNullOrEmpty()) mi.Vendor = ss[0];
                    if (mi.Processor.IsNullOrEmpty()) mi.Processor = ss[1];
                }
            }
            if (TryRead(devTree + "serial-number", out str))
            {
                if (mi.UUID.IsNullOrEmpty()) mi.UUID = str;
            }
        }

        // 识别单板机信息
        {
            var dic = ReadRelease();
            if (dic != null && dic.Count > 0)
            {
                // 计算权重,如果足够高则优先release文件
                var weight = 0;
                if (dic.TryGetValue("Vendor", out var str) && !str.IsNullOrEmpty()) weight++;
                if (dic.TryGetValue("Product", out str) && !str.IsNullOrEmpty()) weight++;
                if (dic.TryGetValue("Board", out str) && !str.IsNullOrEmpty()) weight++;
                if (dic.TryGetValue("Serial", out str) && !str.IsNullOrEmpty()) weight++;

                // 权重很高,覆盖设备树的数据
                if (weight >= 3)
                {
                    if (!info.Vendor.IsNullOrEmpty()) mi.Vendor = info.Vendor;
                    if (!info.Product.IsNullOrEmpty()) mi.Product = info.Product;
                    if (!info.Board.IsNullOrEmpty()) mi.Board = info.Board;
                }

                var vendor = "";
                if (dic.TryGetValue("VENDOR", out str) && !str.IsNullOrEmpty())
                {
                    // Armbian太多,设备树优先,这里的权重不高
                    vendor = str;
                    if (vendor.EqualIgnoreCase("Armbian")) vendor = null;

                    if (mi.Vendor.IsNullOrEmpty() || !vendor.IsNullOrEmpty())
                        mi.Vendor = str;
                }

                if (dic.TryGetValue("BOARD_NAME", out str) && !str.IsNullOrEmpty() &&
                    (mi.Product.IsNullOrEmpty() || !vendor.IsNullOrEmpty()))
                    mi.Product = str;
                if (dic.TryGetValue("BOARD", out str) && !str.IsNullOrEmpty() &&
                    (mi.Board.IsNullOrEmpty() || !vendor.IsNullOrEmpty()))
                    mi.Board = str;
            }
        }

        // 识别全志sunxi平台
        // https://linux-sunxi.org/Allwinner_SoC_Family
        if (TryRead("/sys/class/sunxi_info/sys_info", out var value))
        {
            var dic = value.SplitAsDictionary(":", Environment.NewLine, true);
            if (dic.TryGetValue("sunxi_platform", out var txt) && !txt.IsNullOrEmpty())
            {
                MatchAllwinner(mi, txt);
            }
            if (dic.TryGetValue("sunxi_chipid", out txt) && !txt.IsNullOrEmpty())
                mi.UUID = txt;
            if (dic.TryGetValue("sunxi_serial", out txt) && !txt.IsNullOrEmpty())
                mi.Serial = txt;
        }

        // Armbian跑在全志平台上。如:Allwinner sun8i Family
        if (!mi.Processor.IsNullOrEmpty() && mi.Processor.Contains("sun"))
        {
            var txt = mi.Processor.Split(' ', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(e => e.StartsWithIgnoreCase("sun"));
            MatchAllwinner(mi, txt);
        }

        // 统一赋值
        if (!mi.UUID.IsNullOrEmpty()) info.UUID = mi.UUID;
        if (!mi.Serial.IsNullOrEmpty()) info.Serial = mi.Serial;
        if (!mi.Product.IsNullOrEmpty()) info.Product = mi.Product;
        if (!mi.Board.IsNullOrEmpty()) info.Board = mi.Board;
        if (!mi.Vendor.IsNullOrEmpty()) info.Vendor = mi.Vendor;
        if (!mi.Processor.IsNullOrEmpty()) info.Processor = mi.Processor;
    }

    private static void MatchAllwinner(MachineInfo info, String? txt)
    {
        if (txt.IsNullOrEmpty()) return;

        var p = txt.IndexOf('p');
        if (p > 0) txt = txt[..p];

        // SoC处理器
        if (_allwinner_socs.TryGetValue(txt, out var soc))
            info.Processor = soc;
        else
            info.Processor = txt;

        // 内核架构
        if (info.Product.IsNullOrEmpty())
        {
            p = txt.IndexOf('i');
            if (p > 0) txt = txt[..(p + 1)];

            if (_allwinner_archs.TryGetValue(txt, out var arch))
                info.Product = arch;
        }

        // 制造商
        if (info.Vendor.IsNullOrEmpty()) info.Vendor = "Allwinner";
    }

    private static IDictionary<String, String>? ReadRelease()
    {
        var di = "/etc/".AsDirectory();
        if (!di.Exists) return null;

        var fis = di.GetFiles("*-release");
        foreach (var fi in fis)
        {
            var txt = File.ReadAllText(fi.FullName);
            if (!txt.IsNullOrEmpty() && (txt.Contains("BOARD_NAME=") || txt.Contains("BOARD=") || txt.Contains("VENDOR=") || txt.Contains("Serial=")))
            {
                return txt.SplitAsDictionary("=", "\n", true);
            }
        }

        return null;
    }

    /// <summary>刷新时执行</summary>
    /// <param name="info"></param>
    public void Refresh(MachineInfo info)
    {
        if (Runtime.Windows)
        {
            // 是否使用WiFi
            if (NetworkInterface.GetAllNetworkInterfaces().Any(e => e.NetworkInterfaceType == NetworkInterfaceType.Wireless80211 && e.OperationalStatus == OperationalStatus.Up))
            {
                // 从WiFi获取信号强度
                foreach (var item in NativeWifi.GetConnectedNetworkSsids())
                {
                    if (item.wlanSignalQuality > 0)
                    {
                        info["Ssid"] = item.dot11Ssid;

                        // 表示网络的信号质量的百分比值。 WLAN_SIGNAL_QUALITY 的类型为 ULONG。
                        // 此成员包含介于 0 和 100 之间的值。 值为 0 表示实际 RSSI 信号强度为 -100 dbm。 值为 100 表示实际 RSSI 信号强度为 -50 dbm。
                        // 可以使用线性内插计算 1 到 99 之间的 wlanSignalQuality 值的 RSSI 信号强度值。
                        //info["Signal"] = item.wlanSignalQuality / 100d * (-50 - -100) + -100;
                        info["Signal"] = item.wlanSignalQuality;

                        break;
                    }
                }
            }
        }
        else if (Runtime.Linux)
        {
            var signal = 0;
            var file = "/proc/net/wireless";
            if (File.Exists(file))
            {
                var line = File.ReadAllLines(file)?.LastOrDefault();
                if (!line.IsNullOrEmpty())
                {
                    var ss = line.Split(' ', StringSplitOptions.RemoveEmptyEntries);
                    if (ss.Length > 3)
                    {
                        // Inter-| sta-|   Quality        |   Discarded packets               | Missed | WE
                        // face | tus | link level noise |  nwid  crypt   frag  retry   misc | beacon | 22
                        // wlan0: 0000   70.  -38.  -256        0      0      0    739      0        0

                        // Quality=70/70  Signal level=-38 dBm  Noise level=-256 dBm
                        info["Signal"] = signal = (ss[3]?.TrimEnd('.')).ToInt();
                    }
                }
            }

            if (signal == 0)
            {
                var rs = Execute("iw", "dev wlan0 link", 1_000);
                if (!rs.IsNullOrEmpty())
                {
                    /*
                     * Connected to 24:4b:fe:6d:5c:f8 (on wlan0)
                     * SSID: FeiFan
                     * freq: 2462
                     * RX: 36978860 bytes (198669 packets)
                     * TX: 12425460 bytes (48657 packets)
                     * signal: -31 dBm
                     * tx bitrate: 78.0 MBit/s VHT-MCS 8 VHT-NSS 1
                     * 
                     */
                    var dic = rs.SplitAsDictionary(":", "\n");
                    if (dic.TryGetValue("SSID", out var value))
                        info["SSID"] = value;

                    if (dic.TryGetValue("signal", out value))
                        info["Signal"] = signal = value.TrimEnd("dBm").Trim().ToInt();
                }
            }
        }
    }

    private static Boolean TryRead(String fileName, [NotNullWhen(true)] 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 String? Execute(String cmd, String? arguments = null, Int32 msWait = 5_000)
    {
        try
        {
#if DEBUG
            if (XTrace.Log.Level <= LogLevel.Debug) XTrace.WriteLine("Execute({0} {1})", cmd, arguments);
#endif

            var psi = new ProcessStartInfo(cmd, arguments ?? String.Empty)
            {
                // UseShellExecute 必须 false,以便于后续重定向输出流
                UseShellExecute = false,
                CreateNoWindow = true,
                WindowStyle = ProcessWindowStyle.Hidden,
                RedirectStandardOutput = true,
                //RedirectStandardError = true,
            };
            var process = Process.Start(psi);
            if (process == null) return null;

            if (!process.WaitForExit(msWait))
            {
                process.Kill();
                return null;
            }

            return process.StandardOutput.ReadToEnd();
        }
        catch { return null; }
    }
}
#endif