新增Anolis龙蜥操作系统
大石头 编写于 2024-05-19 18:25:03
Stardust
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using NewLife;
using NewLife.Http;
using NewLife.Log;
using NewLife.Messaging;
using NewLife.Remoting;
using Stardust.Models;
#if NET45_OR_GREATER || NETCOREAPP || NETSTANDARD
using TaskEx = System.Threading.Tasks.Task;
#endif

namespace Stardust;

/// <summary>本地星尘客户端。连接本机星尘代理StarAgent</summary>
public class LocalStarClient
{
    #region 属性
    /// <summary>本机代理信息</summary>
    public AgentInfo? Info { get; private set; }

    /// <summary>本地服务端地址</summary>
    public String? Server { get; set; }

    /// <summary>本地星尘代理服务地址</summary>
    public static Int32 Port { get; set; } = 5500;

    private AgentInfo? _local;
    private ApiClient? _client;
    #endregion

    #region 构造
    /// <summary>实例化</summary>
    public LocalStarClient()
    {
        //_local = AgentInfo.GetLocal();
        //_local.Server = StarSetting.Current.Server;
    }
    #endregion

    #region 方法
    [MemberNotNull(nameof(_client))]
    private void Init()
    {
        if (_client != null) return;

        _client = new ApiClient($"udp://127.0.0.1:{Port}")
        {
            Timeout = 3_000,
            Log = Log,
        };

        var set = StarSetting.Current;
        if (set.Debug) _client.EncoderLog = Log;

        _local = AgentInfo.GetLocal(false);
        _local.Server = !Server.IsNullOrEmpty() ? Server : StarSetting.Current.Server;
    }

    /// <summary>获取信息</summary>
    /// <returns></returns>
    public AgentInfo? GetInfo()
    {
        //!!! 通过进程名判断是否存在,可能会误判,因为获取其它dotnet进程命令行需要管理员权限
        //// 检测进程是否存在,如果进程都不存在,没必要获取信息
        //if (!Process.GetProcessesByName("StarAgent").Any())
        //{
        //    var pis = Process.GetProcessesByName("dotnet");
        //    if (pis.Length == 0) return null;

        //    if (!pis.Any(p => AppInfo.GetProcessName(p).EqualIgnoreCase("StarAgent"))) return null;
        //}

        // 判断目标端口是否已使用,作为是否探测星尘代理的依据,加速应用启动
        //if (!IPAddress.Any.CheckPort(NetType.Udp, 5500) &&
        //    !IPAddress.Loopback.CheckPort(NetType.Udp, 5500) &&
        //    !IPAddress.IPv6Loopback.CheckPort(NetType.Udp, 5500)) return null;
        try
        {
            // 某些情况下检查端口占用会抛出异常,原因未知
            var gp = IPGlobalProperties.GetIPGlobalProperties();
            var eps = gp.GetActiveUdpListeners();
            if (eps.Length > 0 && !eps.Any(ep => ep.Port == Port)) return null;
        }
        catch { }

        var task = TaskEx.Run(GetInfoAsync);
        return task.Wait(1000) ? task.Result : null;
    }

    /// <summary>获取信息</summary>
    /// <returns></returns>
    public async Task<AgentInfo?> GetInfoAsync()
    {
        Init();

        try
        {
            return Info = await _client.InvokeAsync<AgentInfo>("Info", _local);
        }
        catch (TimeoutException)
        {
            return null;
        }
        catch
        {
            throw;
        }
    }

    /// <summary>向StarAgent发送心跳</summary>
    /// <returns></returns>
    public async Task<PingResponse?> PingAsync(AppInfo appInfo, Int32 watchdogTimeout)
    {
        Init();

        var info = new LocalPingInfo
        {
            ProcessId = appInfo.Id,
            ProcessName = appInfo.Name,
            Version = appInfo.Version,
            AppName = appInfo.AppName,

            WatchdogTimeout = watchdogTimeout,
        };

        return await _client.InvokeAsync<PingResponse>("Ping", info);
    }
    #endregion

    #region 进程控制
    /// <summary>自杀并重启</summary>
    /// <returns></returns>
    public Boolean KillAndRestartMySelf()
    {
        Init();

        var p = Process.GetCurrentProcess();
        var fileName = p.MainModule?.FileName;
        var args = "";
        if (!fileName.IsNullOrEmpty())
        {
            var ext = Path.ChangeExtension(fileName, ".dll");
            args = Environment.CommandLine.TrimStart(ext).Trim();
        }

        // 发起命令
        var rs = _client.Invoke<String>("KillAndStart", new
        {
            processId = p.Id,
            delay = 3,
            fileName,
            arguments = args,
            workingDirectory = Environment.CurrentDirectory,
        });

        // 本进程退出
        //p.Kill();

        return !rs.IsNullOrEmpty();
    }
    #endregion

    #region 安装星尘代理
    /// <summary>探测并安装星尘代理</summary>
    /// <param name="url">zip包下载源</param>
    /// <param name="version">版本号</param>
    /// <param name="target">目标目录</param>
    public Boolean ProbeAndInstall(String? url = null, String? version = null, String? target = null)
    {
        //if (url.IsNullOrEmpty()) throw new ArgumentNullException(nameof(url));
        if (url.IsNullOrEmpty())
        {
            var set = NewLife.Setting.Current;
            url = set.PluginServer.EnsureEnd("/");
            url += "star/";
            if (Environment.Version.Major >= 8)
                url += "staragent80.zip";
            else if (Environment.Version.Major >= 7)
                url += "staragent70.zip";
            else if (Environment.Version.Major >= 6)
                url += "staragent60.zip";
            else if (Environment.Version.Major >= 5)
                url += "staragent50.zip";
            else if (Environment.Version.Major >= 4)
                url += "staragent45.zip";
            else
                url += "staragent31.zip";
        }

        // 尝试连接,获取版本
        try
        {
            var info = GetInfo();
            if (info != null)
            {
                // 比目标版本高,不需要安装
                if (String.Compare(info.Version, version) >= 0) return true;

                if (!info.FileName.IsNullOrEmpty()) info.FileName = info.FileName.TrimEnd(" (deleted)");
                if (target.IsNullOrEmpty()) target = Path.GetDirectoryName(info.FileName);

                WriteLog("StarAgent在用版本 v{0},低于目标版本 v{1}", info.Version, version);
            }
        }
        catch (Exception ex)
        {
            WriteLog("没有探测到StarAgent,{0}", ex.GetTrue()?.Message);
        }

        if (target.IsNullOrEmpty())
        {
            // 在进程中查找
            var p = Process.GetProcesses().FirstOrDefault(e => e.ProcessName == "StarAgent");
            if (p != null)
            {
                try
                {
                    if (p.MainModule != null)
                        target = Path.GetDirectoryName(p.MainModule.FileName);
                }
                catch { }

                if (target.IsNullOrEmpty()) target = Path.GetDirectoryName(p.MainWindowTitle);

                WriteLog("发现进程StarAgent,ProcessId={0},target={1}", p.Id, target);
            }
        }

        // 准备安装,甭管是否能够成功重启,先覆盖了文件再说
        {
            if (target.IsNullOrEmpty()) target = "..\\staragent";
            target = target.GetFullPath();
            target.EnsureDirectory(false);

            WriteLog("目标:{0}", target);

            var ug = new Stardust.Web.Upgrade
            {
                SourceFile = Path.GetFileName(url).GetFullPath(),
                DestinationPath = target,

                Log = XTrace.Log,
            };

            WriteLog("下载:{0}", url);

            var client = new HttpClient();
            client.DownloadFileAsync(url, ug.SourceFile).Wait();

            ug.Extract();
            ug.Update();

            File.Delete(ug.SourceFile);
        }

        {
            // 在进程中查找
            var info = Info;
            var inService = info?.Arguments == "-s";
            var p = info != null && info.ProcessId > 0 ?
                Process.GetProcessById(info.ProcessId) :
                Process.GetProcesses().FirstOrDefault(e => e.ProcessName == "StarAgent");

            // 重启目标
            if (p != null && !inService)
            {
                try
                {
                    p.Kill();
                }
                catch (Win32Exception) { }
                catch (Exception ex)
                {
                    XTrace.WriteException(ex);
                }
            }

            var fileName = info?.FileName;
            if (!fileName.IsNullOrEmpty() && Path.GetFullPath(fileName).EqualIgnoreCase("dotnet.exe")) fileName = info?.Arguments;

            var rs = false;
            if (Runtime.Windows)
                rs = RunAgentOnWindows(fileName, target, inService);
            else if (Runtime.Linux)
                rs = RunAgentOnLinux(fileName, target, inService);
            if (!rs)
                rs = RunAgentOnDotnet(fileName, target, inService);
        }

        return true;
    }

    private Boolean RunAgentOnWindows(String? fileName, String target, Boolean inService)
    {
        if (!fileName.IsNullOrEmpty() && Path.GetExtension(fileName) == ".dll") return false;
        if (fileName.IsNullOrEmpty()) fileName = target.CombinePath("StarAgent.exe").GetFullPath();
        if (!File.Exists(fileName)) return false;

        WriteLog("RunAgentOnWindows fileName={0}, inService={1}", fileName, inService);

        if (inService)
        {
            Process.Start(fileName, "-stop").WaitForExit(5_000);
            Process.Start(fileName, "-start").WaitForExit(5_000);

            WriteLog("启动服务成功");
        }
        else
        {
            var si = new ProcessStartInfo(fileName, "-run")
            {
                WorkingDirectory = Path.GetDirectoryName(fileName) ?? "",
                //UseShellExecute = true
            };
            var p = Process.Start(si);

            WriteLog("启动进程成功 pid={0}", p?.Id);
        }

        return true;
    }

    private Boolean RunAgentOnLinux(String? fileName, String target, Boolean inService)
    {
        if (!fileName.IsNullOrEmpty() && Path.GetExtension(fileName) == ".dll") return false;
        if (fileName.IsNullOrEmpty()) fileName = target.CombinePath("StarAgent").GetFullPath();
        if (!File.Exists(fileName)) return false;

        WriteLog("RunAgentOnLinux fileName={0}, inService={1}", fileName, inService);

        // 在Linux中设置执行权限
        Process.Start("chmod", $"+x {fileName}").WaitForExit(5_000);

        if (inService)
        {
            Process.Start(fileName, "-stop").WaitForExit(5_000);
            Process.Start(fileName, "-start").WaitForExit(5_000);

            WriteLog("启动服务成功");
        }
        else
        {
            var si = new ProcessStartInfo(fileName, "-run")
            {
                WorkingDirectory = Path.GetDirectoryName(fileName) ?? "",
                //UseShellExecute = true
            };
            var p = Process.Start(si);

            WriteLog("启动进程成功 pid={0}", p?.Id);
        }

        return true;
    }

    private Boolean RunAgentOnDotnet(String? fileName, String target, Boolean inService)
    {
        if (fileName.IsNullOrEmpty()) fileName = target.CombinePath("StarAgent.dll").GetFullPath();
        if (!File.Exists(fileName)) return false;

        WriteLog("RunAgentOnDotnet fileName={0}, inService={1}", fileName, inService);

        if (inService)
        {
            Process.Start("dotnet", $"{fileName} -stop").WaitForExit(5_000);
            Process.Start("dotnet", $"{fileName} -start").WaitForExit(5_000);

            WriteLog("启动服务成功");
        }
        else
        {
            var si = new ProcessStartInfo("dotnet", $"{fileName} -run")
            {
                WorkingDirectory = Path.GetDirectoryName(fileName) ?? "",
                //UseShellExecute = true
            };
            var p = Process.Start(si);

            WriteLog("启动进程成功 pid={0}", p?.Id);
        }

        return true;
    }

    /// <summary>探测并安装星尘代理</summary>
    /// <param name="url">zip包下载源</param>
    /// <param name="version">版本号</param>
    /// <param name="target">目标目录</param>
    public static Task ProbeAsync(String? url = null, String? version = null, String? target = null)
    {
        return TaskEx.Run(() =>
        {
            var client = new LocalStarClient();
            client.ProbeAndInstall(url, version, target);
        });
    }
    #endregion

    #region 安装卸载应用服务
    ///// <summary>安装应用服务(星尘代理守护)</summary>
    ///// <param name="service"></param>
    ///// <returns></returns>
    //public async Task<ProcessInfo> Install(ServiceInfo service)
    //{
    //    Init();

    //    return await _client.InvokeAsync<ProcessInfo>("Install", service);
    //}

    ///// <summary>安装应用服务(星尘代理守护)</summary>
    ///// <param name="name">服务名,唯一标识</param>
    ///// <param name="fileName">文件</param>
    ///// <param name="arguments">参数</param>
    ///// <param name="workingDirectory">工作目录</param>
    ///// <returns></returns>
    //public async Task<ProcessInfo> Install(String name, String fileName, String arguments = null, String workingDirectory = null)
    //{
    //    Init();

    //    return await _client.InvokeAsync<ProcessInfo>("Install", new ServiceInfo
    //    {
    //        Name = name,
    //        FileName = fileName,
    //        Arguments = arguments,
    //        WorkingDirectory = workingDirectory,
    //        Enable = true,
    //        //ReloadOnChange = true,
    //    });
    //}

    ///// <summary>卸载应用服务</summary>
    ///// <param name="serviceName"></param>
    ///// <returns></returns>
    //public async Task<Boolean> Uninstall(String serviceName)
    //{
    //    Init();

    //    return await _client.InvokeAsync<Boolean>("Uninstall", serviceName);
    //}
    #endregion

    #region 搜索
    /// <summary>在局域网中广播扫描所有StarAgent</summary>
    /// <param name="local">本地信息,用于告知对方我是谁</param>
    /// <param name="timeout"></param>
    /// <returns></returns>
    public static IEnumerable<AgentInfo?> Scan(AgentInfo? local = null, Int32 timeout = 15_000)
    {
        var encoder = new JsonEncoder { Log = XTrace.Log };
        // 构造请求消息
        //var ms = new MemoryStream();
        //var writer = new BinaryWriter(ms);
        //writer.Write("Info");
        //writer.Write(0);

        //var msg = new DefaultMessage
        //{
        //    Payload = ms.ToArray()
        //};
        //var buf = msg.ToPacket().ToArray();

        var buf = encoder.CreateRequest("Info", null).ToPacket()?.ToArray();
        if (buf == null) yield break;

        // 在局域网中广播消息
        var udp = new UdpClient();
        udp.Send(buf, buf.Length, new IPEndPoint(IPAddress.Broadcast, Port));

        var end = DateTime.Now.AddSeconds(timeout);
        while (DateTime.Now < end)
        {
            var rs = new DefaultMessage();
            IPEndPoint? ep = null;
            buf = udp.Receive(ref ep);
            if (buf != null && rs.Read(buf))
            {
                var msg = encoder.Decode(rs);
                if (msg != null && msg.Data != null)
                {
                    var info = (AgentInfo?)encoder.DecodeResult(msg.Action, msg.Data, rs, typeof(AgentInfo));

                    yield return info;
                }
            }
        }
    }
    #endregion

    #region 日志
    /// <summary>日志</summary>
    public ILog Log { get; set; } = Logger.Null;

    /// <summary>写日志</summary>
    /// <param name="format"></param>
    /// <param name="args"></param>
    public void WriteLog(String format, params Object?[] args) => Log?.Info(format, args);
    #endregion
}