ZService.Manager 1.1.3

dotnet add package ZService.Manager --version 1.1.3
NuGet\Install-Package ZService.Manager -Version 1.1.3
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="ZService.Manager" Version="1.1.3" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add ZService.Manager --version 1.1.3
#r "nuget: ZService.Manager, 1.1.3"
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install ZService.Manager as a Cake Addin
#addin nuget:?package=ZService.Manager&version=1.1.3

// Install ZService.Manager as a Cake Tool
#tool nuget:?package=ZService.Manager&version=1.1.3

ZService

介绍

插件化,基于BackgroundService、Quartz的任务调度服务。自安装,支持插件模式和独立模式,支持Windows和Linux。

软件架构

整体架构包括以下四部分,且均已在nuget.org上传相关包。

  • ZService.Core:支持Windows以及Linux的系统服务核心组件
  • ZService.Install:支持Windows以及Linux的系统服务安装组件
  • ZService.Manager:支持Windows以及Linux的系统服务管理组件
  • ZService.Job.Shell:支持Windows以及Linux的系统服务示例插件 - 命令行执行

安装教程

可以在VS 2022 的扩展管理中下载模板插件 ZService.Template (https://marketplace.visualstudio.com/items?itemName=zhaohuiyingxue.ZServiceTemplate)快捷创建服务管理端和插件。 使用模板创建项目

也可以创建项目后,手动从nuget中安装对应的包。 nuget包管理器操作

当你只需要调度相关功能,需引用包:ZService.Core

当你需要给项目增加安装为服务的功能,需引用包:ZService.Install

当你需要完整的功能,需引用包:ZService.Manager

当示例插件可以满足你的使用需求,可引用包:ZService.Job.Shell

框架中最基本的类是ZService.BaseJob,当你把鼠标放置到其名字上,会弹出框架的使用方法: 输入图片说明

简要使用说明
调度任务基类。调试前必须先重新生成!
在Program.cs中写入`await PluginRunner.Run(args);`可脱离框架运行或调试
【ㅤ先决条件ㅤ】

1、插件项目必须创建为 可执行项目 编辑项目文件加入以下节以便生成时包含依赖引用。

<PropertyGroup>
ㅤㅤ<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>

2、插件项目必须编辑项目文件加入以下节以便将生成文件复制到ServiceManager生成目录下(便于调试)。

	
	<Target Name="PostBuild" AfterTargets="PostBuildEvent">
		<ItemGroup>
			<_PostBuildFiles Include="$(TargetDir)**\*" />
		</ItemGroup>
		<Copy SourceFiles="@(_PostBuildFiles)" DestinationFolder="$(SolutionDir)$(SolutionName)\$(OutDir)Plugins\$(TargetName)\%(RecursiveDir)\" SkipUnchangedFiles="True" />
	</Target>
【ㅤ用法ㅤ】

1、根据需要添加特性(依赖Quartz)

[PersistJobDataAfterExecution]//共享数据
[DisallowConcurrentExecution] //不允许并发
public class ClassName : BaseJob

2、重写ExecuteJob方法,在其中写入任务逻辑。

public override Task ExecuteJob(IJobExecutionContext context)

3、(可选)声明存储配置信息的属性,并从配置文件中获取值

public string HostPort { get; set; }
HostPort = Config.GetValue<string>("HostPort");

4、(可选)根据必要重写BeforeExecuteJob或AfterExecuteJob方法。

protected override Task BeforeExecuteJob(IJobExecutionContext context){ }
protected override Task AfterExecuteJob(IJobExecutionContext context){ }

5、(可选)加入日志便于调试分析。

Log.Info("开始执行XXX计划任务...");

使用说明

核心组件 ZService.Core

该包为框架核心组件,必须引用。当只引用此包时,即为简单模式。

组件 ZService.Core 实现了基本的任务调度功能,其中任务调度模块基于组件Quartz,你可以根据经验扩展其他的功能。

在此框架下,所有调度任务,均应当继承自类 ZService.BaseJob,其定义大致如下(其中IJob.Execute实现是内部的,用于将Quartz方法拆分成三个步骤):

public abstract class BaseJob : IJob
{
    /// <summary>
    /// 从Config.json获取到的插件相关的配置信息。
    /// </summary>
    public IConfiguration Config { get; set; }
    /// <summary>
    /// 插件标识符
    /// </summary>
    public string PluginID { get; set; }
    /// <summary>
    /// 插件名称
    /// </summary>
    public string PluginName { get; set; }
    /// <summary>
    /// 插件描述
    /// </summary>
    public string PluginDescription { get; set; }
    /// <summary>
    /// 插件Cron表达式
    /// </summary>
    public List<string> PluginCron { get; set; }
    /// <summary>
    /// 插件错失策略
    /// </summary>
    public string PluginMisFire { get; set; }
    /// <summary>
    /// 日志记录
    /// </summary>
    public ILogger Log { get; set; }
    /// <summary>
    /// 构造函数
    /// </summary>
    public BaseJob() { }
    /// <summary>
    /// 运行任务(内部方法)。
    /// </summary>
    /// <param name="context">Quartz上下文</param>
    /// <returns>一般返回Task.CompletedTask</returns>
    async Task IJob.Execute(IJobExecutionContext context)
    {
        PluginID = context.JobDetail.Key.Name;
        Log = Logger.Factory.GetLogger($"{GetType().FullName}[{PluginID}]");
        try
        {
            Log.Trace($"{PluginName}[{PluginID}] 于 {DateTime.Now} 启动运行...");
            await BeforeExecuteJob(context);
            await ExecuteJob(context);
            await AfterExecuteJob(context);
        }
        catch (Exception err) { Log.Error($"{PluginName}[{PluginID}]运行异常:{err}"); }//JobExecutionException
    }
    /// <summary>
    /// 运行任务之前。
    /// </summary>
    protected virtual Task BeforeExecuteJob(IJobExecutionContext context)=>Task.CompletedTask;
    /// <summary>
    /// 运行任务。
    /// </summary>
    /// <param name="context">Quartz上下文</param>
    /// <returns>一般返回Task.CompletedTask</returns>
    protected abstract Task ExecuteJob(IJobExecutionContext context);
    /// <summary>
    /// 运行任务之后。
    /// </summary>
    protected virtual Task AfterExecuteJob(IJobExecutionContext context)=>Task.CompletedTask;
    /// <summary>
    /// 对象销毁处理。
    /// </summary>
    protected virtual Task Dispose()=>Task.CompletedTask;
    /// <summary>
    /// 获取直接运行或调试时配置文件路径。要求配置文件结构与服务模型相同。
    /// <para>不需要直接运行或调试可不重写,优先级大于Init方法。</para>
    /// </summary>
    /// <returns>配置文件路径</returns>
    public virtual Task<string> ConfigFilePath() => null;
    /// <summary>
    /// 获取直接运行或调试时的配置信息。
    /// <para>不需要直接运行或调试可不重写,优先级小于ConfigFilePath方法。</para>
    /// </summary>
    /// <returns>配置信息</returns>
    public virtual Task<ZJobDetail> Init() => null;
}

当你实现基于BaseJob的调度任务后,这意味着你直接拥有了以下功能:

  1. 从Config.json中直接获取配置信息
  2. 获取插件的各项定义,如Cron表达式、错失策略等
  3. 继承了NLog的日志功能
  4. 简化的任务调度生命周期
  5. 最外层异常处理机制
如何实现一个调度任务?

你可以通过 “安装教程”中的模板插件 ZService.Template 来创建 一个ZService.Template.Plugin 项目,模版会自动应用到你的项目中。

你也可以手动一步一步进行创建,大致步骤如下:

  • 首先,创建项目,引用包 ZService.Core
  • 创建一个继承自 ZService.BaseJob 的类,比如:
using Quartz;
using ZService;

namespace QuartzNetJobs
{
    [PersistJobDataAfterExecution]//运行后缓存数据
    [DisallowConcurrentExecution]//单例模式
    public class 插件名称 : BaseJob//集成基类
    {
        public override async Task<ZJobDetail> Init() =>
            //调度配置,至少需要一个Cron表达式
            new ZJobDetail() { CronList = new List<string> { "*/5 * * * * ? *" } };
        protected override Task ExecuteJob(IJobExecutionContext context)
        {
            Log.Info("开始执行...");
            try
            {
                //你自己的逻辑
            }
            catch (Exception err)
            {
                Log.Fatal(err, $"运行过程中发生异常:{err.Message}。");
                throw;
            }
            finally
            {
                Log.Info($"于 {DateTime.Now} 结束运行...");
            }
            return Task.CompletedTask;
        }
    }
}
  • 更改入口函数
await ZService.PluginRunner.Run(args);
  • 按F5运行 运行界面

是不是很简单?不过,相信你也看到了,运行界面中包含了两个警告: 开始独立运行插件...此模式下不具备热重载、配置更新等功能,建议使用完整的插件模式... ,包括上面的代码,也是将任务的配置(Cron表达式等)硬编码到代码里了,这不是太好吧?

对于硬编码的问题,有一个比较折中的处理方法,就是我们不使用 Init 来返回任务的配置,改用 ConfigFilePath 来返回配置文件的路径,这样不就好了? 况且这个方法优先级是大于Init方法。

public override async Task<string> ConfigFilePath() => "MyConfig.json";

这样,我们就可以把对任务的配置写在文件里,方便我们去维护。

那么,为什么不使用 IHostBuilder.ConfigureAppConfiguration 来设置配置文件呢?原因是,目前我们的入口函数并未包含 IHostBuilder 的环境。并且,整体的框架,我们是想让其具备热重载、配置更新、服务安装等功能的,所以,需要更多的功能的话请关注ZService.Manager。

如果你想将调度任务安装为服务,请参看 ZService.Install 部分 将 PluginRunner 对象设置为服务

关于配置文件

以下是一个配置文件的示例:

//最全配置
{
  //服务标识,安装服务时需要,默认ZService.Manager
  "ServiceName": "ZService.Manager",
  //服务名称,安装服务时需要,默认ZService 服务管理器
  "DisplayName": "系统服务名称",
  //服务描述,安装服务时需要,默认插件化的系统服务,支持Windows和Linux。
  "Description": "系统服务功能说明,或其他的详细描述。",
  //任务列表【必填】
  "Jobs": {
    //任务标识,唯一标识不能重复,可以是中文:-;
    "MusicDownload": {
      //任务插件完全限定名【必填,仅与任务标识一致时可以省略】
      "PluginName": "Job.MusicDownload",
      //任务名称
      "Name": "天擎数据下载",
      //任务描述
      "Description": "天擎数据下载",
      //Cron计划表达式,字符串或字符串数组。【必填】
      //字符串数组一般不用,主要用来间接实现类似每90分钟运行的设定
      "Cron": [ "10 * * * * ? *", "50 * * * * ? *" ],
      //错过运行的动作:ALL全部运行,ONCE运行一次,NONE不运行,默认NONE
      "Misfire": "NONE",
      //是否立即启动一次,默认false
      "StartNow": true,
      //各任务独有的配置【仅当前任务可使用】
      "Config": {

      }
    }
  },
  //全局通用配置,比如用来存放共用的配置信息如数据库连接等。【所有任务均可使用】
  "CommonConfig": {
    "DbConn": "server=localhost;uid=sa;pwd=123456;database=Renying",
  },
  //插件的通用配置。各插件名称为完全限定名。【使用同一插件的任务可使用】
  "Job.MusicDownload": {
    "SavePath": "../../Test"
  }
}

其中有几个部分是配置的必填项,那么将其摘出来,形成了最小配置,当我们不需要很复杂的配置时可以借鉴:

//最小配置示例
{
  "Jobs": {                                       //任务列表
    "Job.MusicDownload": {                        //    任务标识且同时为插件完全限定名
      "Cron": "10 * * * * ? *"                    //        Cron计划表达式
    }
  }
}

可以看到,最小配置和我们上面示例中硬编码的部分是基本差不多的。

当使用配置文件时,如果我们要在任务内部获取自定义的参数时,可以重写 BeforeExecuteJob 方法,并使用 Config.GetValue<T> 实现

/// <summary>
/// 任务执行前,可以获取插件配置等信息
/// </summary>
protected override void BeforeExecuteJob(IJobExecutionContext context)
{
    //获取配置
    var isAsync = Config.GetValue<bool>("IsAsync", true);
}

使用方法和微软自带的类似,需要注意的有以下几条:

  1. 调度任务的自定义参数,其优先级为:插件独立配置 > 插件的通用配置 > 全局通用配置,同名的配置会按优先级进行覆盖
  2. 建议在BeforeExecuteJob方法中读取配置
关于 NLog

框架集成了NLog日志系统作为基本日志系统,目前并无计划支持其他日志组件。

NLog的默认配置以代码的形式集成于系统当中,等同于以下NLog配置:

<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
	<variable name="layout" value="${date:format=HH\:mm\:ss}[${level:uppercase=true:padding=-5}]${logger:shortName=true} : ${message} ${onexception:${exception:format=tostring} ${newline} ${stacktrace} ${newline}" />
	<targets>
		
		<target name="console" xsi:type="ColoredConsole" layout="${layout}" useDefaultRowHighlightingRules="false">
			<highlight-row condition="level == LogLevel.Trace" foregroundColor="DarkGray" />
			<highlight-row condition="level == LogLevel.Debug" foregroundColor="Gray" />
            <highlight-row condition="level == LogLevel.Info" foregroundColor="DarkCyan" />
            <highlight-row condition="level == LogLevel.Warn" foregroundColor="DarkYellow" />
            <highlight-row condition="level == LogLevel.Error" foregroundColor="Red" />
            <highlight-row condition="level == LogLevel.Fatal" foregroundColor="DarkRed" backgroundColor="DarkYellow" />
		</target>
		
		<target name="debugger" xsi:type="Debugger" layout="${layout}" />
		
		<target name="file" xsi:type="File" maxArchiveFiles="30" fileName="${basedir}/Logs/${shortdate}.log" layout="${layout}" />
	</targets>
	<rules>
		<logger name="*" minlevel="Trace" writeTo="console" />
		<logger name="*" minlevel="Debug" writeTo="debugger" />
		<logger name="*" minlevel="Info" writeTo="file" />
	</rules>
</nlog>

但是,你也可以使用文件配置如NLog.config去覆盖它。之后在 IHostBuilder.ConfigureLogging 或合适的位置调用 NLog.LogManager.LoadConfiguration(configPath) 使其生效。当然,对于引用了ZService.Manager包的项目,你可以在入口处直接调用扩展方法 AddNLog 来实现。

using Microsoft.Extensions.Hosting;
using ZService;

await Host.CreateDefaultBuilder(args)
    .SetConfig()
    .AddNLog("Config/NLog.config")  //当参数为空,使用内置配置
    .AddService(args)
    .Build()
    .RunAsync();
管理组件 ZService.Manager

ZService.Manager 间接引用了包 ZService.Base,并在其基础上实现了 插件化、热重载 的任务调度,如果要同时承载多个调度任务,并希望能对任务逻辑进行热更新,建议直接引用此包,同时ZService.Manager也实现了 将自身安装为Windows系统服务或Linux守护程序 的功能。

当然,你也可以在引用ZService.Manager包的情况下,依然采用ZService.Base中介绍的办法来实现你的功能,代价是不能实现插件化和热重载,但服务安装功能依然保留。

你可以在引用了ZService.Manager的项目中定义插件,但我们不建议你这样做,并会给出如下警告:

自身程序集中定义有插件,开始载入自身程序集
不建议在自身程序集中定义插件!该方式不支持热重载,且抛出异常时影响主程序...
如何建立一套完整的调度任务集合

建议的方式是,建立如下图所示的解决方案

解决方案示例

主程序

其中,示例中名为ZService的项目为主程序,其仅包括了入口函数和配置文件,不包含任何调度任务逻辑。

在这个项目中,配置文件位于文件夹Config内,包含了主配置文件Config.json和NLog.config,以及你自己其他的配置信息。

如果你是使用模板插件 ZService.Template 来创建的 ZService.Template.Manager 项目,模版会自动应用到你的项目中。

代码方面比较简单,示例如下:

using Microsoft.Extensions.Hosting;
using ZService;
//启用HOST容器
await Host.CreateDefaultBuilder(args)
    .SetConfig()		//设置配置文件
    .AddNLog()		        //设置NLog配置文件
    .AddService(args)		//添加服务
    .Build()
    .RunAsync();		//开始运行

仅此而已,它承载的任务仅仅是载入包含了调度任务的插件,并运行它们。

如果你需要安装为系统服务,运行 XXX.exe -install 即可,更多的用法请参考ZService.Install部分

如果你在主程序或其引用的程序集内定义了调度任务,我们会给出如下警告:

自身程序集中定义有插件,开始载入自身程序集
不建议在自身程序集中定义插件!该方式不支持热重载,且抛出异常时影响主程序...
插件集

在Plugins解决方案文件夹下的各个项目均为调度任务插件,每个项目中可以包含多个基于ZService.BaseJob类的调度任务。你可以按照ZService.Base介绍部分创建各个插件。

理论上,插件只需要引用包ZService.Base即可

如果你是使用模板插件 ZService.Template 来创建的 ZService.Template.Plugin 项目,模版会自动应用到你的项目中

如果你决定 手动创建你的调度任务插件集合 ,那么你必须注意以下几点:

📕 所有调度任务插件,其编译后的可执行包,必须复制到主程序根目录下的Plugins文件夹下,即可能的结构如下

主程序.exe                     =>主程序
Logs                           =>日志文件夹
Config                         =>配置文件夹
    Config.json                =>    主配置文件
    NLog.config                =>    NLog配置文件
    XXX.config                 =>    其他配置文件
Plugins                        =>插件主目录
    ZService.Job.Shell         =>    插件目录,名称必须为主dll相同
        ZService.Job.Shell.dll =>        插件编译内容主dll
        XXX                    =>        插件编译内容其他文件

📗 为了减少手动复制编译目录,可以给各个插件项目的项目文件中增加以下节以便每次编译后自动复制到主程序的输出目录下

	
	<Target Name="PostBuild" AfterTargets="PostBuildEvent">
		<ItemGroup>
			<_PostBuildFiles Include="$(TargetDir)**\*" />
		</ItemGroup>
		<Copy SourceFiles="@(_PostBuildFiles)" DestinationFolder="$(SolutionDir)$(SolutionName)\$(OutDir)Plugins\$(TargetName)\%(RecursiveDir)\" SkipUnchangedFiles="True" />
	</Target>

📗 为了可以单独运行及调试插件,建议插件均为控制台程序而非运行库。而且运行库的编译输出一般只包括自身直接引用,为确保插件编译输出的完整性,控制台程序是最好的选择

安装组件 ZService.Install

如果你的项目需要安装为Windows系统服务或Linux守护程序,可以引用此包,方便快捷地给项目增加系统安装功能。

ZService.Manager 默认已包含此包,不需要单独引用。

引用ZService.Install包后,需更改入口代码:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ZService.Installer;

await Host.CreateDefaultBuilder(args)
    //支持Windows服务
    .UseWindowsService()
    //支持Linux守护进程
    .UseSystemd()
    //添加QuartzService主服务
    .ConfigureServices((context, services) =>
    {
        //获取服务配置信息
        var id = "服务标识符";
        var name = "服务显示名称";
        var description = "服务描述";
        //服务安装选项构造
        var serviceOptions = new ServiceOptions {
            Description = description,
            WorkingDirectory = AppDomain.CurrentDomain.BaseDirectory
        };
        serviceOptions.Linux.Service.Restart = "always";
        serviceOptions.Linux.Service.RestartSec = "10";
        serviceOptions.Windows.DisplayName = name;
        serviceOptions.Windows.FailureActionType = WindowsServiceActionType.Restart;
        //当UseServiceInstaller返回true时,运行你的服务逻辑
        if (Service.UseServiceInstaller(args, id, serviceOptions))
            services.AddHostedService<你自己的服务类>();
        else Environment.Exit(0);
    })
    .Build()
    .RunAsync();

如果你引用的包中,包含了ZService.Manager,则可以用以下简化代码

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ZService;

await Host.CreateDefaultBuilder(args)
    //以下三种方式选择一种
    .AddService(args) 		        //插件模式
    .AddServiceRunner(args)		//简单模式
    .AddService<你自己的服务类>(args)	//其他想安装的服务类
    .Build()
    .RunAsync();

如果你不想在代码中指定服务的相关信息,可以将其写入配置文件Config.json,支持的配置项如下:

{
  //服务标识,安装服务时需要,默认ZService.Manager
  "ServiceName": "ZService.Manager",
  //服务名称,安装服务时需要,默认ZService 服务管理器
  "DisplayName": "ZService 服务管理器",
  //服务描述,安装服务时需要,默认插件化的系统服务,支持Windows和Linux。
  "Description": "插件化的系统服务,支持Windows和Linux。"
}

之后,你可以在命令行(需要具有权限)中将程序安装为服务,例如:XXX.exe -install。支持的命令包括:install/uninstall/start/stop/help。

如需在Linux环境下安装,请运行 dotnet XXX.dll -install

如已编译为Linux可执行文件,也可执行 ./XXX -install

服务安装命令行

示例插件 ZService.Job.Shell

该示例插件是支持Windows以及Linux的命令行执行,如直接满足需求,可按以下方法在项目中使用。

  1. 创建插件项目,引用此包;
  2. 创建新的Class并继承ZService.Job.Shell
  3. 按以下规则编写配置文件Config.json中的插件配置Jobs的配置项,json路径: Jobs:插件ID:Config
//完整配置
"Config": {
  //是否异步执行,不等待前一命令结束。默认true。false则为顺序执行。非必填。
  "IsAsync": true,
  //等待所有进程结束,默认true等待,对于一些可同时运行的任务可设置false。非必填。
  "WaitFor": true,
  //重定向显示执行结果,默认false不显示。非必填。
  "Redirect": true,
  //每个命令行之间延迟毫秒数。默认200毫秒。非必填。
  "Delay": 200,
  //需要执行的命令行【必填】
  //规则1:可填写多个,也可只有一个,至少有一个
  //规则2:如果命令以$开头,会根据操作系统选用shell
  //规则3:命令是dll文件,会添加dotnet
  "Command": ["$F:\\Gateway\\Gateway.dll"]
}
//最小配置
"Config": { "Command": "$F:\\Gateway\\Gateway.dll" }
Product Compatible and additional computed target framework versions.
.NET net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
1.1.3 124 12/20/2023
1.1.2 83 12/16/2023
1.1.1 104 12/16/2023
1.1.0 142 5/9/2023
1.0.1.1 161 4/21/2023
1.0.1 157 4/21/2023
1.0.0.4 154 4/21/2023
1.0.0.3 152 4/21/2023
1.0.0.2 155 4/21/2023
1.0.0.1 159 4/21/2023
1.0.0 157 4/21/2023