Merge branch 'master' into v3.1
大石头 authored at 2024-07-13 19:03:18
25.68 KiB
Stardust
using NewLife;
using NewLife.Http;
using NewLife.IO;
using NewLife.Log;
using NewLife.Remoting.Clients;
using NewLife.Remoting.Models;
using NewLife.Serialization;
using NewLife.Threading;
using Stardust.Models;

namespace Stardust.Managers;

/// <summary>应用服务管理</summary>
public class ServiceManager : DisposeBase
{
    #region 属性
    /// <summary>应用服务集合</summary>
    public ServiceInfo[]? Services { get; private set; }

    /// <summary>延迟时间。重启进程或服务的延迟时间,默认3000ms</summary>
    public Int32 Delay { get; set; } = 3000;

    ///// <summary>星尘服务地址</summary>
    //public String Server { get; set; }

    /// <summary>服务启动前触发</summary>
    public event EventHandler<ServiceEventArgs>? ServiceStarting;

    /// <summary>服务停止后触发</summary>
    public event EventHandler<ServiceEventArgs>? ServiceStoped;

    /// <summary>服务改变事件</summary>
    public event EventHandler? ServiceChanged;

    ///// <summary>事件客户端</summary>
    //public IEventProvider EventProvider { get; set; }

    /// <summary>正在运行的应用服务信息</summary>
    private readonly List<ServiceController> _controllers = [];
    private CsvDb<ProcessInfo>? _db;
    private StarClient? _client;
    #endregion

    #region 构造
    /// <summary>实例化应用服务管理</summary>
    public ServiceManager() { }

    /// <summary>销毁</summary>
    /// <param name="disposing"></param>
    protected override void Dispose(Boolean disposing)
    {
        base.Dispose(disposing);

        Stop(disposing ? "Dispose" : "GC");
    }
    #endregion

    #region 方法
    /// <summary>添加应用服务,或替换同名服务</summary>
    /// <param name="services">应用服务集合</param>
    public Int32 Add(params ServiceInfo[] services)
    {
        var count = 0;
        var list = Services?.ToList() ?? [];
        foreach (var item in services)
        {
            if (item.Name.IsNullOrEmpty()) continue;

            var flag = false;
            for (var i = 0; i < list.Count; i++)
            {
                // 替换更新已有项
                if (list[i].Name.EqualIgnoreCase(item.Name))
                {
                    list[i] = item;
                    flag = true;
                }
            }
            if (!flag)
            {
                list.Add(item);
                count++;
            }
        }

        Services = list.ToArray();

        //_timer?.SetNext(-1);
        CheckNow();

        RaiseServiceChanged();

        return count;
    }

    /// <summary>删除应用服务</summary>
    /// <param name="serviceName"></param>
    /// <returns></returns>
    public Int32 Remove(String serviceName)
    {
        if (serviceName.IsNullOrEmpty()) return 0;

        var count = 0;
        var list = Services?.ToList() ?? [];
        for (var i = list.Count - 1; i >= 0; i--)
        {
            var item = list[i];
            if (item.Name.EqualIgnoreCase(serviceName))
            {
                list.RemoveAt(i);
                count++;
            }
        }

        if (count > 0)
        {
            Services = list.ToArray();

            RaiseServiceChanged();
        }

        return count;
    }

    /// <summary>开始管理,拉起应用进程</summary>
    public void Start()
    {
        if (_timer != null) return;

        using var span = Tracer?.NewSpan("ServiceManager-Start");

        WriteLog("启动应用服务管理");

        var data = Setting.Current.DataPath;
        var db = new CsvDb<ProcessInfo>((x, y) => x?.Name == y?.Name) { FileName = data.CombinePath("Service.csv") };
        db.Remove(e => e.UpdateTime.AddDays(1) < DateTime.Now);
        _db = db;

        // 从数据库加载应用状态
        foreach (var item in db.FindAll())
        {
            _controllers.Add(new ServiceController
            {
                Name = item.Name,
                ProcessId = item.ProcessId,
                ProcessName = item.ProcessName,
                StartTime = item.CreateTime,
                Delay = Delay,

                EventProvider = _client,
                Tracer = Tracer,
                Log = Log,
            });
        }

        _timer = new TimerX(DoWork, null, 1000, 30_000) { Async = true };
    }

    /// <summary>停止管理,按需杀掉进程</summary>
    /// <param name="reason"></param>
    public void Stop(String reason)
    {
        using var span = Tracer?.NewSpan("ServiceManager-Stop", reason);

        WriteLog("停止应用服务管理:{0}", reason);

        _timer?.TryDispose();
        _timer = null;

        // 伴随服务停止一起退出
        var svcs = _controllers;
        for (var i = svcs.Count - 1; i >= 0; i--)
        {
            var ctrl = svcs[i];
            if (ctrl.Info != null && ctrl.Info.AutoStop)
            {
                ctrl.Stop(reason);
                svcs.RemoveAt(i);
            }
        }

        SaveDb();
    }

    /// <summary>
    /// 启动所有应用
    /// </summary>
    public void StartAll()
    {
        Start();

        var svcs = Services;
        if (svcs == null) return;

        var changed = false;
        foreach (var item in svcs)
        {
            if (item != null && item.Enable)
            {
                changed |= StartService(item, null);
            }
        }

        // 保存状态
        if (changed) SaveDb();
    }

    /// <summary>
    /// 停止所有应用
    /// </summary>
    /// <param name="reason"></param>
    public void StopAll(String reason)
    {
        var svcs = _controllers;
        for (var i = svcs.Count - 1; i >= 0; i--)
        {
            var ctrl = svcs[i];
            if (ctrl.Running)
            {
                ctrl.Stop(reason);
                svcs.RemoveAt(i);
            }
        }

        SaveDb();
    }

    /// <summary>保存应用状态到数据库</summary>
    private void SaveDb()
    {
        var db = _db;
        if (db == null) return;

        var list = _controllers.Select(e => e.ToModel()).ToList();

        if (list.Count == 0)
            db.Clear();
        else
            db.Write(list, false);
    }

    /// <summary>检查服务。一般用于改变服务后,让其即时生效</summary>
    //public void CheckService() => DoWork(null);

    private void RaiseServiceChanged() => ServiceChanged?.Invoke(this, EventArgs.Empty);
    #endregion

    #region 服务控制
    /// <summary>检查并启动服务</summary>
    /// <param name="service"></param>
    /// <param name="deploy"></param>
    /// <param name="isCheck">仅检查时,不记录埋点</param>
    /// <returns>本次是否成功启动,原来已启动返回false</returns>
    private Boolean StartService(ServiceInfo service, DeployInfo? deploy, Boolean isCheck = false)
    {
        using var span = isCheck ? null : Tracer?.NewSpan("ServiceManager-StartService", service);
        if (deploy != null) span?.AppendTag(deploy);

        lock (this)
        {
            var controller = _controllers.FirstOrDefault(e => e.Name.EqualIgnoreCase(service.Name));
            if (controller != null)
            {
                controller.EventProvider = _client;
                controller.SetInfo(service);

                if (deploy != null) controller.DeployInfo = deploy;

                return controller.Check();
            }

            controller = new ServiceController
            {
                Name = service.Name,
                //Info = service,
                DeployInfo = deploy,

                EventProvider = _client,
                Tracer = Tracer,
                Log = Log,
            };
            controller.SetInfo(service);
            _controllers.Add(controller);

            Fix(service);
            controller.Init();

            ServiceStarting?.Invoke(this, new ServiceEventArgs { Controller = controller });

            if (controller.Start())
            {
                var svc = controller.Info;
                if (svc != null && svc.Mode == ServiceModes.RunOnce && !svc.Enable) RaiseServiceChanged();

                return true;
            }
        }

        return false;
    }

    /// <summary>停止服务</summary>
    /// <param name="serviceName"></param>
    /// <param name="reason"></param>
    /// <returns></returns>
    private Boolean StopService(String serviceName, String reason)
    {
        using var span = Tracer?.NewSpan("ServiceManager-StopService", serviceName);

        var controller = _controllers.FirstOrDefault(e => e.Name.EqualIgnoreCase(serviceName));
        if (controller != null)
        {
            // 先删除再停止,避免并发
            _controllers.Remove(controller);

            controller.Stop(reason);

            ServiceStoped?.Invoke(this, new ServiceEventArgs { Controller = controller });

            controller.TryDispose();

            return true;
        }
        else
        {
            span?.AppendTag($"无法找到服务[{serviceName}]的控制器");
        }

        return false;
    }

    /// <summary>
    /// 设置服务集合,常用于读取配置文件后设置
    /// </summary>
    /// <param name="services"></param>
    public void SetServices(ServiceInfo[] services)
    {
        // 一般由外部配置文件改变而驱动,所以本方法不会引发ServiceChanged事件

        var svcs = Services;
        var flag = svcs == null || svcs.Length != services.Length;
        if (!flag)
        {
            foreach (var item in services)
            {
                // 如果新服务在原列表里不存在,或者数值不同,则认为有改变
                var s = svcs?.FirstOrDefault(e => e.Name == item.Name);
                if (s == null || s != item && s.ToJson() != item.ToJson())
                {
                    flag = true;
                    break;
                }
            }
        }

        if (flag)
        {
            using var span = Tracer?.NewSpan("ServiceManager-SetServices", services);

            WriteLog("应用服务配置变更");

            // 拷贝,避免应用后无法及时感知变化。具体ServiceInfo也要克隆,否则上面对比service配置信息时永远失败
            Services = services.Select(e => e.Clone()).ToArray();

            _status = 0;
            //_timer?.SetNext(-1);
            CheckNow();
        }
    }

    private async Task UploadService(ServiceInfo[] svcs)
    {
        if (svcs == null) return;
        if (_client == null) return;

        svcs = svcs.Where(e => !e.Name.IsNullOrEmpty()).ToArray();
        if (svcs.Length == 0) return;

        using var span = Tracer?.NewSpan(nameof(UploadService), svcs);
        WriteLog("上报应用服务 {0}", svcs.Join(",", e => e.Name));

        await _client.UploadDeploy(svcs);
    }

    private async Task<DeployInfo[]?> PullService(String? appName)
    {
        if (Services == null) return null;
        if (_client == null) return null;

        using var span = Tracer?.NewSpan(nameof(PullService), appName);
        WriteLog("拉取应用服务:{0}", appName ?? "(所有)");

        var svcs = Services.ToList();

        var rs = await _client.GetDeploy();
        if (rs == null) return null;

        // 过滤应用
        if (!appName.IsNullOrEmpty()) rs = rs.Where(e => e.Name.EqualIgnoreCase(appName)).ToArray();

        WriteLog("取得应用服务:{0}", rs.Join(",", e => e.Name));
        WriteLog("可用:{0}", rs.Where(e => e.Service != null && e.Service.Enable).Join(",", e => e.Name));
        if (rs.Length > 0) WriteLog(rs.ToJson(true));

        // 旧版服务
        span?.AppendTag(svcs);

        // 合并
        foreach (var item in rs)
        {
            var svc = item.Service;
            if (svc == null || svc.Name.IsNullOrEmpty()) continue;

            // 下载文件到工作目录
            Fix(svc);
            if (svc.Enable && !item.Url.IsNullOrEmpty()) await Download(item, svc);

            var old = svcs.FirstOrDefault(e => e.Name.EqualIgnoreCase(item.Name));
            if (old == null)
            {
                WriteLog("新增[{0}]:Enable={1}", item.Name, svc.Enable);

                old = svc;
                //svc.ReloadOnChange = true;

                svcs.Add(old);
            }
            else
            {
                if (!old.Enable && svc.Enable)
                    WriteLog("启用[{0}]", item.Name);
                else if (old.Enable && !svc.Enable)
                    WriteLog("禁用[{0}]", item.Name);

                old.FileName = svc.FileName;
                old.Arguments = svc.Arguments;
                old.WorkingDirectory = svc.WorkingDirectory;
                old.UserName = svc.UserName;
                old.Environments = svc.Environments;
                old.Enable = svc.Enable;
                old.AutoStop = svc.AutoStop;
                old.ReloadOnChange = svc.ReloadOnChange;
                old.MaxMemory = svc.MaxMemory;
                old.Mode = svc.Mode;
                old.ZipFile = svc.ZipFile;
            }
        }

        // 新版服务
        span?.AppendTag(svcs);

        Services = svcs.ToArray();

        RaiseServiceChanged();

        return rs;
    }

    void Fix(ServiceInfo svc)
    {
        var name = svc.Name;
        if (!name.IsNullOrEmpty())
        {
            if (svc.FileName.IsNullOrEmpty()) svc.FileName = $"{name}.zip";

            // 如果没有设置工作目录,默认工作目录是上一级的apps子目录,按应用放置
            if (svc.WorkingDirectory.IsNullOrEmpty()) svc.WorkingDirectory = $"../apps/{name}";
        }
    }

    async Task Download(DeployInfo info, ServiceInfo svc)
    {
        var url = info.Url;
        if (url.IsNullOrEmpty()) return;

        using var span = Tracer?.NewSpan("ServiceManager-Download", info.Url);

        // 下载的文件名必须是压缩包
        var zipName = svc.FileName;
        if (!zipName.EndsWithIgnoreCase(".zip")) zipName = $"{svc.Name}.zip";

        var dst = svc.WorkingDirectory.CombinePath(zipName).AsFile();
        span?.AppendTag(dst.FullName);

        var flag = false;
        if (!dst.Exists)
        {
            WriteLog("文件不存在:{0}", dst);
            flag = true;
        }
        else if (!info.Hash.IsNullOrEmpty())
        {
            var hash = dst.MD5().ToHex();
            if (!hash.EqualIgnoreCase(info.Hash))
            {
                WriteLog("文件哈希不匹配:{0}(本地)!={1}(远程)", hash, info.Hash);
                flag = true;
            }
        }
        if (!flag)
        {
            var hash = dst.MD5().ToHex();
            WriteLog("文件已存在:{0} MD5: {1}", dst, hash);

            svc.ZipFile = dst.FullName;
        }
        if (flag)
        {
            if (_client != null) url = _client.BuildUrl(url);

            WriteLog("下载[{0}]:{1} {2}", svc.Name, info.Version, url);

            // 先下载到临时目录,再整体拷贝,避免进程退出
            var tmp = Path.GetTempFileName();

            var http = new HttpClient();
            await http.DownloadFileAsync(url, tmp);

            WriteLog("下载完成,准备覆盖:{0}", dst.FullName);

            // 校验哈希
            var ti = tmp.AsFile();
            var hash = ti.MD5().ToHex();
            if (!info.Hash.IsNullOrEmpty() && !hash.EqualIgnoreCase(info.Hash))
                WriteLog("下载失败,校验错误!{0}(本地)!={1}(远程)", hash, info.Hash);
            else
            {
                // 删除原文件
                if (dst.Exists)
                {
                    try
                    {
                        dst.Delete();
                    }
                    catch
                    {
                        dst.MoveTo(dst.FullName + ".del");
                    }
                }

                // 创建目录,下载到临时目录的文件拷贝到这里
                dst.FullName.EnsureDirectory(true);
                ti.MoveTo(dst.FullName);

                svc.ZipFile = dst.FullName;
            }
        }
    }

    Int32 _status;
    Boolean _busy;
    private TimerX? _timer;
    private async Task DoWork(Object state)
    {
        // 如果其它任务正在使用管理器,则跳过这一次检查
        if (_busy) return;

        var svcs = Services;
        if (svcs == null) return;

        using var span = Tracer?.NewSpan("ServiceManager-DoWork", svcs.Length);

        // 应用服务的上报和拉取
        DeployInfo[]? deploys = null;
        if (_client != null && _client.Logined)
        {
            // 上传失败不应该影响本地拉起服务
            try
            {
                //if (_status == 0 && svcs.Length > 0)
                if (_status == 0)
                {
                    var svcs2 = svcs.Where(e => e.Enable || !e.Name.EqualIgnoreCase("test", "test2")).ToArray();
                    if (svcs2.Length > 0) await UploadService(svcs2);

                    _status = 1;
                }

                if (_status == 1)
                {
                    deploys = await PullService(null);

                    _status = 2;
                }
            }
            catch (Exception ex)
            {
                XTrace.WriteException(ex);
            }
        }

        // 检查应用服务变化,先停止再启动
        var changed = false;
        svcs = Services ?? [];

        // 停止不再使用的服务
        var controllers = _controllers;
        for (var i = controllers.Count - 1; i >= 0; i--)
        {
            var controller = controllers[i];
            var service = svcs.FirstOrDefault(e => e.Name.EqualIgnoreCase(controller.Name));
            if (service == null || !service.Enable)
            {
                controller.Stop("配置停止");
                controllers.RemoveAt(i);
                changed = true;
            }
            else if (controller.Running && controller.Info != null && service.ToJson() != controller.Info.ToJson())
            {
                // 启动成功的短时间内,不认可配置改变,因为可能就是这一次发布的配置变化
                if (controller.StartTime.Year > 2000 && controller.StartTime.AddSeconds(5) < DateTime.Now)
                {
                    controller.Stop("配置改变");
                    controllers.RemoveAt(i);
                    changed = true;
                }
            }
        }

        // 检查并启动服务
        foreach (var item in svcs)
        {
            if (item != null && item.Enable)
            {
                changed |= StartService(item, deploys?.FirstOrDefault(e => e.Name == item.Name), true);
            }
        }

        // 保存状态
        if (changed) SaveDb();
    }

    /// <summary>马上检查应用服务</summary>
    public void CheckNow() => _timer?.SetNext(-1);

    private IDisposable CreateBusy()
    {
        _busy = true;

        return new MyBusy(this);
    }

    class MyBusy : IDisposable
    {
        private ServiceManager _mgr;
        public MyBusy(ServiceManager mgr) => _mgr = mgr;

        public void Dispose() => _mgr._busy = false;
    }
    #endregion

    #region 安装卸载
    /// <summary>安装服务,添加后启动</summary>
    /// <param name="service"></param>
    /// <returns></returns>
    public ProcessInfo? Install(ServiceInfo service)
    {
        using var span = Tracer?.NewSpan("ServiceManager-Install", service);

        // 设置繁忙,避免同步进行健康检查
        using var busy = CreateBusy();

        Add(service);

        if (!StartService(service, null)) return null;

        SaveDb();

        return _controllers.FirstOrDefault(e => e.Name.EqualIgnoreCase(service.Name))?.ToModel();
    }

    /// <summary>卸载服务</summary>
    /// <param name="name"></param>
    /// <param name="reason"></param>
    /// <returns></returns>
    public Boolean Uninstall(String name, String reason)
    {
        using var span = Tracer?.NewSpan("ServiceManager-Uninstall", new { name, reason });

        var svc = Services?.FirstOrDefault(e => e.Name.EqualIgnoreCase(name));
        if (svc == null) return false;

        // 设置繁忙,避免同步进行健康检查
        using var busy = CreateBusy();

        StopService(svc.Name, reason);

        SaveDb();

        Remove(svc.Name);

        return true;
    }
    #endregion

    #region 发布事件
    /// <summary>关联订阅事件</summary>
    /// <param name="client"></param>
    public void Attach(StarClient client)
    {
        _client = client;

        // 设置繁忙,避免同步进行健康检查
        using var busy = CreateBusy();

        client.RegisterCommand("deploy/publish", DoControl);
        client.RegisterCommand("deploy/install", DoControl);
        client.RegisterCommand("deploy/start", DoControl);
        client.RegisterCommand("deploy/stop", DoControl);
        client.RegisterCommand("deploy/restart", DoControl);
        client.RegisterCommand("deploy/uninstall", DoControl);

        //_timer?.SetNext(-1);
        CheckNow();
    }

    private CommandReplyModel DoControl(CommandModel cmd)
    {
        using var span = Tracer?.NewSpan("ServiceManager-DoControl", cmd);

        var my = cmd.Argument?.ToJsonEntity<MyApp>();
        var serviceName = my?.DeployName ?? my?.AppName;
        if (my == null || serviceName.IsNullOrEmpty())
            return new CommandReplyModel { Status = CommandStatus.错误, Data = "参数错误" };

        WriteLog("{0} Id={1} Name={2}", cmd.Command, my.Id, serviceName);

        var changed = cmd.Command switch
        {
            "deploy/publish" => OnInstall(serviceName, cmd),
            "deploy/install" => OnInstall(serviceName, cmd),
            "deploy/start" => OnStart(serviceName, cmd),
            "deploy/stop" => OnStop(serviceName, cmd),
            "deploy/restart" => OnRestart(serviceName, cmd),
            "deploy/uninstall" => OnUninstall(serviceName, cmd),
            _ => throw new Exception($"不支持命令[{cmd.Command}]"),
        };

        // 保存状态
        if (changed) SaveDb();

        return new CommandReplyModel { Data = "成功" };
    }

    private class MyApp
    {
        public Int32 Id { get; set; }

        public String? DeployName { get; set; }

        public String? AppName { get; set; }
    }

    Boolean OnInstall(String serviceName, CommandModel cmd)
    {
        using var span = Tracer?.NewSpan("ServiceManager-Install", cmd);

        var dis = PullService(serviceName).Result;
        if (dis == null || dis.Length == 0) throw new Exception($"无法从服务器取得应用信息,安装{serviceName}失败!");

        // 马上停止并拉起应用服务,定时器只用于双保险
        var svc = dis[0]?.Service;
        if (svc != null)
        {
            var rs = StopService(svc.Name, cmd.Command);
            //if (rs) Thread.Sleep(Delay);
            StartService(svc, dis[0]);
        }
        else
        {
            span?.AppendTag($"无法找到{dis[0]}的服务");
        }

        // 尽快调度一次,拉起服务
        //_timer?.SetNext(-1);
        CheckNow();

        return true;
    }

    Boolean OnStart(String serviceName, CommandModel cmd)
    {
        using var span = Tracer?.NewSpan("ServiceManager-Start", cmd);

        var svc = (Services?.FirstOrDefault(e => e.Name.EqualIgnoreCase(serviceName)))
            ?? throw new Exception($"无法找到服务[{serviceName}]");

        var changed = false;
        svc.Enable = true;
        changed |= StartService(svc, null);

        RaiseServiceChanged();

        return changed;
    }

    Boolean OnStop(String serviceName, CommandModel cmd)
    {
        using var span = Tracer?.NewSpan("ServiceManager-Stop", cmd);

        var svc = (Services?.FirstOrDefault(e => e.Name.EqualIgnoreCase(serviceName)))
            ?? throw new Exception($"无法找到服务[{serviceName}]");

        var changed = false;
        svc.Enable = false;
        changed |= StopService(svc.Name, cmd.Command);

        RaiseServiceChanged();

        return changed;
    }

    Boolean OnRestart(String serviceName, CommandModel cmd)
    {
        using var span = Tracer?.NewSpan("ServiceManager-Restart", cmd);

        var svc = (Services?.FirstOrDefault(e => e.Name.EqualIgnoreCase(serviceName)))
            ?? throw new Exception($"无法找到服务[{serviceName}]");

        var changed = false;
        svc.Enable = false;
        changed |= StopService(svc.Name, cmd.Command);
        //if (changed) Thread.Sleep(Delay);
        svc.Enable = true;
        changed |= StartService(svc, null);

        RaiseServiceChanged();

        return changed;
    }

    Boolean OnUninstall(String serviceName, CommandModel cmd)
    {
        using var span = Tracer?.NewSpan("ServiceManager-Uninstall", cmd);

        var svc = (Services?.FirstOrDefault(e => e.Name.EqualIgnoreCase(serviceName)))
            ?? throw new Exception($"无法找到服务[{serviceName}]");

        var changed = false;
        svc.Enable = false;
        changed |= StopService(svc.Name, cmd.Command);

        Remove(serviceName);

        //RaiseServiceChanged();

        return changed;
    }
    #endregion

    #region 日志
    /// <summary>性能追踪</summary>
    public ITracer? Tracer { get; set; }

    /// <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);

        var msg = (args == null || args.Length == 0) ? format : String.Format(format, args);
        DefaultSpan.Current?.AppendTag(msg);

        if (format.Contains("错误") || format.Contains("失败"))
            _client?.WriteErrorEvent(nameof(ServiceManager), msg);
        else
            _client?.WriteInfoEvent(nameof(ServiceManager), msg);
    }
    #endregion
}