[fix]修正UdpServer在接收广播时连续启动接收的错误,在StarAgent中,此时可能收到广播包,SocketFlags是Broadcast,需要清空,否则报错“参考的对象类型不支持尝试的操作”; 无需设置SocketOptionName.PacketInformation,在ReceiveMessageFromAsync时会自动设置,并且支持ipv6;
石头 authored at 2024-10-10 00:36:00 石头 committed at 2024-10-10 00:45:43
16.97 KiB
X
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using NewLife.Log;
using NewLife.Reflection;
using NewLife.Threading;

namespace NewLife.Agent
{
    /// <summary>服务程序基类</summary>
    public abstract class ServiceBase : IHostedService
    {
        #region 属性
        /// <summary>主机</summary>
        public IHost Host { get; set; }

        /// <summary>服务名</summary>
        public String ServiceName { get; set; }

        /// <summary>显示名</summary>
        public String DisplayName { get; set; }

        /// <summary>描述</summary>
        public String Description { get; set; }
        #endregion

        #region 构造
        /// <summary>初始化</summary>
        public ServiceBase()
        {
        }
        #endregion

        #region 主函数
        /// <summary>服务主函数</summary>
        public void Main()
        {
            MachineInfo.RegisterAsync();

            // 以服务方式启动时,不写控制台日志
            var args = Environment.GetCommandLineArgs();
            if (args == null || args.Length <= 1 || args[1].ToLower() != "-s")
                XTrace.UseConsole();

            if (Host == null)
            {
                if (Runtime.Windows)
                    Host = new WindowsService();
                else
                    Host = new Systemd();
            }

            var service = this;
            service.Log = XTrace.Log;

            // 初始化配置
            var set = Setting.Current;
            if (set.ServiceName.IsNullOrEmpty()) set.ServiceName = service.ServiceName;
            if (set.DisplayName.IsNullOrEmpty()) set.DisplayName = service.DisplayName;
            if (set.Description.IsNullOrEmpty()) set.Description = service.Description;

            // 从程序集构造配置
            var asm = AssemblyX.Entry;
            if (set.ServiceName.IsNullOrEmpty()) set.ServiceName = asm.Name;
            if (set.DisplayName.IsNullOrEmpty()) set.DisplayName = asm.Title;
            if (set.Description.IsNullOrEmpty()) set.Description = asm.Description;

            set.Save();

            // 用配置覆盖
            service.ServiceName = set.ServiceName;
            service.DisplayName = set.DisplayName;
            service.Description = set.Description;

            if (args.Length > 1)
            {
                #region 命令
                var cmd = args[1].ToLower();
                switch (cmd)
                {
                    case "-s":
                        Host.Run(this);
                        break;
                    case "-i":
                        Install();
                        break;
                    case "-u":
                        Host.Remove(ServiceName);
                        break;
                    case "-start":
                        Host.Start(ServiceName);
                        break;
                    case "-stop":
                        Host.Stop(ServiceName);
                        break;
                }
                #endregion
            }
            else
            {
                Console.Title = service.DisplayName;

                // 输出状态,菜单循环
                service.ShowStatus();
                service.ProcessMenu();
            }
        }

        /// <summary>显示状态</summary>
        protected virtual void ShowStatus()
        {
            var color = Console.ForegroundColor;
            Console.ForegroundColor = ConsoleColor.Red;

            var name = ServiceName;
            if (name != DisplayName)
                Console.WriteLine("服务:{0}({1})", DisplayName, name);
            else
                Console.WriteLine("服务:{0}", name);
            Console.WriteLine("描述:{0}", Description);
            Console.Write("状态:{0} ", Host.GetType().Name);

            if (!Host.IsInstalled(name))
                Console.WriteLine("未安装");
            else if (Host.IsRunning(name))
                Console.WriteLine("运行中");
            else
                Console.WriteLine("未启动");

            var asm = AssemblyX.Create(Assembly.GetExecutingAssembly());
            Console.WriteLine();
            Console.WriteLine("{0}\t版本:{1}\t发布:{2:yyyy-MM-dd HH:mm:ss}", asm.Name, asm.FileVersion, asm.Compile);

            var asm2 = AssemblyX.Create(Assembly.GetEntryAssembly());
            if (asm2 != asm)
                Console.WriteLine("{0}\t版本:{1}\t发布:{2:yyyy-MM-dd HH:mm:ss}", asm2.Name, asm2.FileVersion, asm2.Compile);

            Console.ForegroundColor = color;
        }

        /// <summary>处理菜单</summary>
        protected virtual void ProcessMenu()
        {
            var service = this;
            var name = ServiceName;
            while (true)
            {
                //输出菜单
                ShowMenu();
                Console.Write("请选择操作(-x是命令行参数):");

                //读取命令
                var key = Console.ReadKey();
                if (key.KeyChar == '0') break;
                Console.WriteLine();
                Console.WriteLine();

                switch (key.KeyChar)
                {
                    case '1':
                        //输出状态
                        ShowStatus();

                        break;
                    case '2':
                        if (Host.IsInstalled(name))
                            Host.Remove(name);
                        else
                            Install();
                        break;
                    case '3':
                        if (Host.IsRunning(name))
                            Host.Stop(name);
                        else
                            Host.Start(name);
                        break;
                    case '5':
                        #region 循环调试
                        try
                        {
                            Console.WriteLine("正在循环调试……");
                            StartWork("循环开始");

                            Console.WriteLine("任意键结束循环调试!");
                            Console.ReadKey(true);

                            StopWork("循环停止");
                        }
                        catch (Exception ex)
                        {
                            Console.WriteLine(ex.ToString());
                        }
                        #endregion
                        break;
                    case '7':
                        if (WatchDogs.Length > 0) CheckWatchDog();
                        break;
                    default:
                        // 自定义菜单
                        if (_Menus.TryGetValue(key.KeyChar, out var menu)) menu.Callback();
                        break;
                }
            }
        }

        /// <summary>显示菜单</summary>
        protected virtual void ShowMenu()
        {
            var name = ServiceName;

            var color = Console.ForegroundColor;
            Console.ForegroundColor = ConsoleColor.Yellow;

            Console.WriteLine();
            Console.WriteLine("1 显示状态");

            var run = false;
            if (Host.IsInstalled(name))
            {
                if (Host.IsRunning(name))
                {
                    run = true;
                    Console.WriteLine("3 停止服务 -stop");
                }
                else
                {
                    Console.WriteLine("2 卸载服务 -u");

                    Console.WriteLine("3 启动服务 -start");
                }
            }
            else
            {
                Console.WriteLine("2 安装服务 -i");
            }

            if (!run)
            {
                Console.WriteLine("5 循环调试 -run");
            }

            var dogs = WatchDogs;
            if (dogs.Length > 0)
            {
                Console.WriteLine("7 看门狗保护服务 {0}", dogs.Join());
            }

            if (_Menus.Count > 0)
            {
                foreach (var item in _Menus)
                {
                    Console.WriteLine("{0} {1}", item.Key, item.Value.Name);
                }
            }

            Console.WriteLine("0 退出");

            Console.ForegroundColor = color;
        }

        private readonly Dictionary<Char, Menu> _Menus = new Dictionary<Char, Menu>();
        /// <summary>添加菜单</summary>
        /// <param name="key"></param>
        /// <param name="name"></param>
        /// <param name="callbak"></param>
        public void AddMenu(Char key, String name, Action callbak)
        {
            if (!_Menus.ContainsKey(key))
            {
                _Menus.Add(key, new Menu(key, name, callbak));
            }
        }

        class Menu
        {
            public Char Key { get; set; }
            public String Name { get; set; }
            public Action Callback { get; set; }

            public Menu(Char key, String name, Action callback)
            {
                Key = key;
                Name = name;
                Callback = callback;
            }
        }
        #endregion

        #region 服务控制
        /// <summary>开始工作</summary>
        /// <param name="reason"></param>
        protected virtual void StartWork(String reason)
        {
            WriteLog("服务启动 {0}", reason);

            if (_Timer == null) AppDomain.CurrentDomain.ProcessExit += OnProcessExit;

            _Timer = new TimerX(DoCheck, null, 10_000, 10_000, "AM") { Async = true };
        }

        private void OnProcessExit(Object sender, EventArgs e)
        {
            StopWork("ProcessExit");
            Environment.ExitCode = 0;
        }

        /// <summary>停止服务</summary>
        /// <param name="reason"></param>
        protected virtual void StopWork(String reason)
        {
            if (_Timer != null) AppDomain.CurrentDomain.ProcessExit -= OnProcessExit;
            _Timer.TryDispose();
            _Timer = null;

            WriteLog("服务停止 {0}", reason);
        }

        private void Install()
        {
            var exe = GetExeName();

            // 兼容dotnet
            var args = Environment.GetCommandLineArgs();
            if (args.Length >= 1 && Path.GetFileName(exe).EqualIgnoreCase("dotnet", "dotnet.exe"))
                exe += " " + args[0].GetFullPath();
            //else
            //    exe = exe.GetFullPath();

            var bin = $"{exe} -s";
            //RunSC($"create {name} BinPath= \"{bin}\" start= auto DisplayName= \"{svc.DisplayName}\"");

            Host.Install(ServiceName, DisplayName, bin, Description);
        }

        /// <summary>Exe程序名</summary>
        static String GetExeName()
        {
            var p = Process.GetCurrentProcess();
            var filename = p.MainModule.FileName;
            //filename = Path.GetFileName(filename);
            filename = filename.Replace(".vshost.", ".");

            return filename;
        }
        #endregion

        #region 服务维护
        private TimerX? _Timer;

        /// <summary>服务管理线程封装</summary>
        /// <param name="data"></param>
        protected virtual void DoCheck(Object data)
        {
            // 如果某一项检查需要重启服务,则返回true,这里跳出循环,等待服务重启
            if (CheckMemory()) return;
            if (CheckThread()) return;
            if (CheckHandle()) return;
            if (CheckAutoRestart()) return;

            // 检查看门狗
            CheckWatchDog();
        }

        /// <summary>检查内存是否超标</summary>
        /// <returns>是否超标重启</returns>
        protected virtual Boolean CheckMemory()
        {
            var max = Setting.Current.MaxMemory;
            if (max <= 0) return false;

            var cur = GC.GetTotalMemory(false);
            cur = cur / 1024 / 1024;
            if (cur < max) return false;

            // 执行一次GC回收
#if NET4
            GC.Collect(2, GCCollectionMode.Forced);
#else
            GC.Collect(2, GCCollectionMode.Forced, false);
#endif

            // 再次判断内存
            cur = GC.GetTotalMemory(true);
            cur = cur / 1024 / 1024;
            if (cur < max) return false;

            WriteLog("当前进程占用内存 {0:n0}M,超过阀值 {1:n0}M,准备重新启动!", cur, max);

            Restart("MaxMemory");

            return true;
        }

        /// <summary>检查服务进程的总线程数是否超标</summary>
        /// <returns></returns>
        protected virtual Boolean CheckThread()
        {
            var max = Setting.Current.MaxThread;
            if (max <= 0) return false;

            var p = Process.GetCurrentProcess();
            if (p.Threads.Count < max) return false;

            WriteLog("当前进程总线程 {0:n0}个,超过阀值 {1:n0}个,准备重新启动!", p.Threads.Count, max);

            Restart("MaxThread");

            return true;
        }

        /// <summary>检查服务进程的句柄数是否超标</summary>
        /// <returns></returns>
        protected virtual Boolean CheckHandle()
        {
            var max = Setting.Current.MaxHandle;
            if (max <= 0) return false;

            var p = Process.GetCurrentProcess();
            if (p.HandleCount < max) return false;

            WriteLog("当前进程句柄 {0:n0}个,超过阀值 {1:n0}个,准备重新启动!", p.HandleCount, max);

            Restart("MaxHandle");

            return true;
        }

        /// <summary>服务开始时间</summary>
        private readonly DateTime Start = DateTime.Now;

        /// <summary>检查自动重启</summary>
        /// <returns></returns>
        protected virtual Boolean CheckAutoRestart()
        {
            var auto = Setting.Current.AutoRestart;
            if (auto <= 0) return false;

            var ts = DateTime.Now - Start;
            if (ts.TotalMinutes < auto) return false;

            WriteLog("服务已运行 {0:n0}分钟,达到预设重启时间({1:n0}分钟),准备重启!", ts.TotalMinutes, auto);

            Restart("AutoRestart");

            return true;
        }

        /// <summary>重启服务</summary>
        /// <param name="reason"></param>
        public void Restart(String reason)
        {
            WriteLog("重启服务!{0}", reason);

            // 在临时目录生成重启服务的批处理文件
            var filename = "重启.bat".GetFullPath();
            if (File.Exists(filename)) File.Delete(filename);

            File.AppendAllText(filename, "net stop " + ServiceName);
            File.AppendAllText(filename, Environment.NewLine);
            File.AppendAllText(filename, "ping 127.0.0.1 -n 5 > nul ");
            File.AppendAllText(filename, Environment.NewLine);
            File.AppendAllText(filename, "net start " + ServiceName);

            //执行重启服务的批处理
            //RunCmd(filename, false, false);
            var p = new Process();
            var si = new ProcessStartInfo
            {
                FileName = filename,
                UseShellExecute = true,
                CreateNoWindow = true
            };
            p.StartInfo = si;

            p.Start();

            //if (File.Exists(filename)) File.Delete(filename);
        }
        #endregion

        #region 服务高级功能
        public virtual async Task StartAsync(CancellationToken cancellationToken)
        {
            StartWork("StartAsync");
        }

        public virtual async Task StopAsync(CancellationToken cancellationToken)
        {
            StopWork("StopAsync");
        }
        #endregion

        #region 看门狗
        /// <summary>看门狗要保护的服务</summary>
        public static String[] WatchDogs => Setting.Current.WatchDog.Split(",", ";");

        /// <summary>检查看门狗。</summary>
        /// <remarks>
        /// XAgent看门狗功能由管理线程完成,每分钟一次。
        /// 检查指定的任务是否已经停止,如果已经停止,则启动它。
        /// </remarks>
        public static void CheckWatchDog()
        {
            var ss = WatchDogs;
            if (ss == null || ss.Length < 1) return;

            //foreach (var item in ss)
            //{
            //    // 注意:IsServiceRunning返回三种状态,null表示未知
            //    if (ServiceHelper.IsServiceRunning(item) == false)
            //    {
            //        XTrace.WriteLine("发现服务{0}被关闭,准备启动!", item);

            //        ServiceHelper.RunCmd("net start " + item, false, true);
            //    }
            //}
        }
        #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
    }
}