功能特点

NewLife.Agent是一个服务管理框架,用于开发随系统自动启动的长时间运行后台应用程序,支持Windows/Linux。

在Windows上注册为Windows服务,在Linux上注册为Systemd守护进程。

Agent支持netstandard2.0/net45/net40/mono,旧版本还支持net20(已不再维护)。

源码库:https://github.com/NewLifeX/NewLife.Agent


Agent常用于各种后台应用,如aspnetcore应用、RPC网络服务器、MQTT服务器、数据处理应用、数据消费应用(Redis/RocketMQ)等。同类软件有 NSSM、srvany,但并不一样,Agent是框架,而它们是具体软件,更像星尘代理 StarAgent。


NewLife.Agent主要功能:

  1. 注册应用为系统服务或守护进程,随系统自动启动
  2. 支持控制台菜单控制安装、卸载、启动、停止,以及查看状态
  3. 支持控制台调试应用,解决Windows服务难以调试的问题
  4. 支持健康检测,限制内存、线程数、句柄数,超限时重启应用服务
  5. 支持应用服务定时重启,通过配置指定
  6. 支持看门狗WatchDog,通过配置指定要守护的目标应用服务,如果目标停止则启动
  7. 支持配置文件修改服务名,一个应用程序可在多个目录上部署为不同的系统服务


服务控制

一个服务代理示例跑起来的样子

image.png

image.png

这是Agent的标准控制台(Windows和Centos)。上面是该服务的状态信息,下面是控制菜单。

示例分析:

  • 服务名 XAgent/StarAgent,可以命令启动停止,Windows是net start XAgent/net stop XAgent,Linux是systemctl start StarAgent/systemctl stop StarAgent
  • 显示名“新生命服务代理”是在windows服务控制板里面看到的名字
  • 下一段信息给出了NewLife.Agent和当前应用的版本信息和编译时间
  • 黄色菜单可通过按键选择相应操作,内置012345,可自定义其它按键操作
  • 菜单1,显示状态,按下1后刷新状态信息
  • 菜单2,安装服务或卸载服务,安装成功后,显示信息变为卸载服务,反之亦然
  • 菜单3,启动服务或停止服务,安装后才可以看见
  • 菜单4,重启服务,安装且运行后可以看见
  • 菜单5,循环调试,在当前进程启动应用主逻辑,用于业务逻辑调试,等同于Windows服务调用
  • 菜单0,退出应用服务

!!!注意,服务安装、卸载、启动、停止,在Windows上需要管理员权限运行




写一个应用服务

新建.netcore3.1或.net4.5控制台项目,从nuget引用NewLife.Agent。

image.pngimage.png



Agent需要接管主线程循环

class Program
{
    static void Main(String[] args) => new MyService().Main(args);
}


这里的MyService就是自己的后台应用

/// <summary>代理服务例子。自定义服务程序可参照该类实现。</summary>
public class MyService : ServiceBase
{
    #region 属性
    #endregion

    #region 构造函数
    /// <summary>实例化一个代理服务</summary>
    public MyService()
    {
        // 一般在构造函数里面指定服务名
        ServiceName = "XAgent";

        DisplayName = "新生命服务代理";
        Description = "用于承载各种服务的服务代理!";
    }
    #endregion

    #region 核心
    private TimerX _timer;
    private TimerX _timer2;
    /// <summary>开始工作</summary>
    /// <param name="reason"></param>
    protected override void StartWork(String reason)
    {
        WriteLog("业务开始……");

        // 5秒开始,每60秒执行一次
        _timer = new TimerX(DoWork, null, 5_000, 60_000) { Async = true };
        // 每天凌晨执行一次
        _timer2 = new TimerX(DoWork, null, DateTime.Today, 24 * 3600 * 1000) { Async = true };

        base.StartWork(reason);
    }

    private void DoWork(Object state)
    {
        XTrace.WriteLine("定时任务");
    }

    /// <summary>停止服务</summary>
    /// <param name="reason"></param>
    protected override void StopWork(String reason)
    {
        WriteLog("业务结束!");

        _timer.Dispose();
        _timer2.Dispose();

        base.StopWork(reason);
    }
    #endregion
}

构造函数里面指定服务名和显示名等信息,但最终会优先使用配置文件指定值,如果配置文件为空,则使用构造函数这里的默认值写入配置文件。

!!!注意:代码里面的ServiceName只用于创建配置文件Agent.config里面的ServiceName默认值,此后服务名以配置文件为准!

这是一个非常典型的应用模型,在任务开始时,实例化定时器,任务停止时关闭定时器。业务逻辑在定时处理函数里面实现。


启动后按5,进入循环调试,可以看到定时器启动后的运行效果

image.png


后台服务程序的核心是 StartWork/StopWork,分别指示服务的开始和结束。

  • 在StartWork中实例化应用对象以及加载配置,例如实例化并配置打开网络服务 NetServer,主要应用对象应作为类成员字段,而不是StartWork本地变量,避免被GC收割;
  • 不允许长时间阻塞StartWork或抛出异常,否则服务启动失败;
  • 在StopWork中关闭应用并销毁资源,例如 NetServer 的Close或Dispose;


产品级例程

一个产品级例程的服务编写如下(星尘代理StarAgent):

   class Program
    {
        static void Main(String[] args) => new MyService().Main(args);
    }

    /// <summary>服务类。名字可以自定义</summary>
    class MyService : ServiceBase
    {
        public MyService()
        {
            ServiceName = "StarAgent";

            var set = Setting.Current;
            if (set.IsNew)
            {
#if DEBUG
                set.Server = "http://localhost:6600";
#endif

                set.Save();
            }

            // 注册菜单,在控制台菜单中按 t 可以执行Test函数,主要用于临时处理数据
            AddMenu('t', "测试", Test);
        }

        TimerX _timer;
        StarClient _Client;
        ServiceManager _Manager;
        private void StartClient()
        {
            var set = Setting.Current;
            var server = set.Server;
            if (server.IsNullOrEmpty()) return;

            WriteLog("初始化服务端地址:{0}", server);

            var client = new StarClient(server)
            {
                Code = set.Code,
                Secret = set.Secret,
                Log = XTrace.Log,
            };

            // 登录后保存证书
            client.OnLogined += (s, e) =>
            {
                var inf = client.Info;
                if (inf != null && !inf.Code.IsNullOrEmpty())
                {
                    set.Code = inf.Code;
                    set.Secret = inf.Secret;
                    set.Save();
                }
            };

            // 可能需要多次尝试
            _timer = new TimerX(TryConnectServer, client, 0, 5_000) { Async = true };

            _Client = client;
        }

        private void TryConnectServer(Object state)
        {
            var client = state as StarClient;
            var set = Setting.Current;
            client.Login().Wait();
            CheckUpgrade(client, set.Channel);

            // 登录成功,销毁定时器
            //TimerX.Current.Period = 0;
            _timer.TryDispose();
            _timer = null;
        }

        /// <summary>服务启动</summary>
        /// <remarks>
        /// 安装Windows服务后,服务启动会执行一次该方法。
        /// 控制台菜单按5进入循环调试也会执行该方法。
        /// </remarks>
        protected override void StartWork(String reason)
        {
            var set = Setting.Current;

            StartClient();

            // 应用服务管理
            _Manager = new ServiceManager
            {
                Services = set.Services,

                Log = XTrace.Log,
            };
            _Manager.Start();

            base.StartWork(reason);
        }

        /// <summary>服务停止</summary>
        /// <remarks>
        /// 安装Windows服务后,服务停止会执行该方法。
        /// 控制台菜单按5进入循环调试,任意键结束时也会执行该方法。
        /// </remarks>
        protected override void StopWork(String reason)
        {
            base.StopWork(reason);

            _Manager.Stop(reason);
            //_Manager.TryDispose();

            _Client.TryDispose();
            _Client = null;
        }

        private void CheckUpgrade(StarClient client, String channel)
        {
            // 检查更新
            var ur = client.Upgrade(channel).Result;
            if (ur != null)
            {
                var rs = client.ProcessUpgrade(ur);

                // 强制更新时,马上重启
                if (rs && ur.Force)
                {
                    StopWork("Upgrade");

                    var p = Process.GetCurrentProcess();
                    p.Close();
                    p.Kill();
                }
            }
        }

        /// <summary>数据测试,菜单t</summary>
        public void Test()
        {
        }
    }

这里借助AddMenu注册了自定义菜单。


重点难点

再次提醒,在Windows上安装、卸载、启动、停止服务,需要在文件上右键以管理员运行,否则权限不足报错;同理,在Linux上也需要sudo权限。


系统服务没有界面,面临的最大困难就是不知道系统的工作状况。

这里正是X组件日志大显身手的时候,当前目录下Log子目录存放有应用系统的全部工作日志,可供用户查看分析。

另外,借助任务管理器,可以查看系统服务是否在工作中,以及CPU内存等资源占用。


分布式部署

服务代理开发的应用只能手工部署于单个计算机上,如果要在多台服务器上部署以及更新应用,有些折腾。

这里,推荐您关注我们的产品级项目《星尘Stardust》!

星尘,轻量级分布式服务框架。配置中心、集群管理、远程自动发布、服务治理。服务自动注册和发现,负载均衡,动态伸缩,故障转移,性能监控。


星尘代理 StarAgent 是一个基于NewLife.Agent开发的产品级应用,它可通过配置拉起多个目标应用进程,并且目标应用不需要依赖NewLife.Agent开发。StarAgent 也可以通过控制中心向多台节点服务器推送应用程序包,星尘代理节点负责拉起并守护进程!


如果对你有帮助,给个赞呗!

image.pngimage.png