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

namespace NewLife.Agent
{
    /// <typeparam name="TService">服务类型</typeparam>
    public abstract class AgentServiceBase<TService> : AgentServiceBase
         where TService : AgentServiceBase<TService>, new()
    {
        /// <summary>服务主函数</summary>
        public static void ServiceMain() => new TService().Main();
    }

    /// <summary>服务程序基类</summary>
    public abstract class AgentServiceBase : ServiceBase
    {
        #region 属性
        /// <summary>显示名</summary>
        public virtual String DisplayName { get; set; } = "";

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

        #region 构造
        /// <summary>初始化</summary>
        public AgentServiceBase()
        {
            CanStop = true;
            CanShutdown = true;
            CanPauseAndContinue = false;
            CanHandlePowerEvent = true;
            CanHandleSessionChangeEvent = true;
            AutoLog = true;
        }
        #endregion

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

            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;

            var name = service.ServiceName;
            var args = Environment.GetCommandLineArgs();

            if (args.Length > 1)
            {
                #region 命令
                var cmd = args[1].ToLower();
                if (cmd == "-s")  //启动服务
                {

                    try
                    {
                        ServiceBase.Run(new[] { service });
                    }
                    catch (Exception ex)
                    {
                        XTrace.WriteException(ex);
                    }
                }
                else if (cmd == "-i") //安装服务
                    service.Install(true);
                else if (cmd == "-u") //卸载服务
                    service.Install(false);
                else if (cmd == "-start") //启动服务
                    service.ControlService(true);
                else if (cmd == "-stop") //停止服务
                    service.ControlService(false);
                #endregion
            }
            else
            {
                Console.Title = service.DisplayName;

                #region 命令行
                // 输出状态
                service.ShowStatus();

                while (true)
                {
                    //输出菜单
                    service.ShowMenu();
                    Console.Write("请选择操作(-x是命令行参数):");

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

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

                            break;
                        case '2':
                            if (ServiceHelper.IsInstalled(name) == true)
                                service.Install(false);
                            else
                                service.Install(true);
                            break;
                        case '3':
                            if (ServiceHelper.IsRunning(name) == true)
                                service.ControlService(false);
                            else
                                service.ControlService(true);
                            break;
                        case '5':
                            #region 循环调试
                            try
                            {
                                Console.WriteLine("正在循环调试……");
                                service.StartWork("循环开始");

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

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

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

            var service = this;
            var name = service.ServiceName;

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

            var install = ServiceHelper.IsInstalled(name);
            if (install == null)
                Console.WriteLine("未知");
            else if (install == false)
                Console.WriteLine("未安装");
            else
            {
                var run = ServiceHelper.IsRunning(name);
                if (run == null)
                    Console.WriteLine("未知");
                else if (run == false)
                    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 ShowMenu()
        {
            var service = this;
            var name = service.ServiceName;

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

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

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

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

            if (run != true)
            {
                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);

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

        /// <summary>停止服务</summary>
        /// <param name="reason"></param>
        protected virtual void StopWork(String reason)
        {
            _Timer.TryDispose();

            WriteLog("服务停止 {0}", reason);
        }
        #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("重启服务!");

            // 在临时目录生成重启服务的批处理文件
            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 服务高级功能
        /// <summary>服务启动事件</summary>
        /// <param name="args"></param>
        protected override void OnStart(String[] args) => StartWork(nameof(OnStart));

        /// <summary>服务停止事件</summary>
        protected override void OnStop() => StopWork(nameof(OnStop));

        /// <summary>在系统关闭时执行。 指定在系统关闭之前应该发生什么。</summary>
        protected override void OnShutdown() => StopWork(nameof(OnShutdown));

        /// <summary>在计算机的电源状态已发生更改时执行。 这适用于便携式计算机,当他们进入挂起模式,这不是系统关闭相同。</summary>
        /// <param name="powerStatus"></param>
        /// <returns></returns>
        protected override Boolean OnPowerEvent(PowerBroadcastStatus powerStatus)
        {
            WriteLog(nameof(OnPowerEvent) + " " + powerStatus);

            return true;
        }

        /// <summary>在终端服务器会话中接收的更改事件时执行</summary>
        /// <param name="changeDescription"></param>
        protected override void OnSessionChange(SessionChangeDescription changeDescription) => WriteLog(nameof(OnSessionChange) + " SessionId={0} Reason={1}", changeDescription.SessionId, changeDescription.Reason);
        #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>
        /// <param name="isinstall">是否安装</param>
        public void Install(Boolean isinstall = true)
        {
            var name = ServiceName;
            if (String.IsNullOrEmpty(name)) throw new Exception("未指定服务名!");

            if (name.Length < name.GetBytes().Length) throw new Exception("服务名不能是中文!");

            name = name.Replace(" ", "_");
            // win7及以上系统时才提示
            if (Environment.OSVersion.Version.Major >= 6) ServiceHelper.WriteLine("在win7/win2008及更高系统中,可能需要管理员权限执行才能安装/卸载服务。");
            if (isinstall)
            {
                var exe = ServiceHelper.ExeName;

                // 兼容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 = GetBinPath(exe);
                ServiceHelper.RunSC($"create {name} BinPath= \"{bin}\" start= auto DisplayName= \"{DisplayName}\"");
                if (!Description.IsNullOrEmpty()) ServiceHelper.RunSC($"description {name} \"{Description}\"");
            }
            else
            {
                this.ControlService(false);

                ServiceHelper.RunSC("Delete " + name);
            }
        }

        /// <summary>获取安装服务的命令参数</summary>
        /// <param name="exe"></param>
        /// <returns></returns>
        protected virtual String GetBinPath(String exe) => $"{exe} -s";
        #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)
        {
            if (Log != null && Log.Enable) Log.Info(format, args);
        }
        #endregion
    }
}
#endif