Quartz.NET是一个开源的作业调度框架,是OpenSymphony 的 Quartz API的.NET移植,它用C#写成,可用于winform和asp.net应用中。它提供了巨大的灵活性而不牺牲简单性。你能够用它来为执行一个作业而创建简单的或复杂的调度。它有很多特征,如:数据库支持,集群,插件,支持cron-like表达式等等
以上介绍是从博客园张善友(http://www.cnblogs.com/shanyou/archive/2007/08/25/quartznettutorial.html)的博客摘录,可登录他博客具体了解quartz.net。
我在这里只讲具体在项目中的实现:
通过Windows服务调用Quartz.net,然后Quartz.net 调用WinForm消息窗口,实现计划任务消息推送。
【Windows服务】-->【Quartz.net】 --> 【Winform .exe】 --> 【程序打包】
项目解决方案,如图所示:
一、创建Windows服务
1、打开vs2010--新建项目--选择"Windows服务",我这里命名为"QuartzService". 创建好后首先映入我们眼帘的是QuartzService.cs[设计]视图,右键点设计视图选择"添加安装程序",如下图:
注释:(创建一个Windows服务,仅用InstallUtil程序去安装这个服务是不够的。你必须还要把一个服务安装程序添加到你的Windows服务当中,这样便于InstallUtil或是任何别的安装程序知道应用你服务的是怎样的配置设置)
2、切换到刚被添加的ProjectInstaller的设计视图, 设置serviceInstaller1组件的属性: StartType = Automatic;ServiceName = QuartzService;
设置serviceProcessInstaller1组件的属性 Account = LocalSystem; 如下图:
3、QuartzService中添加Quartz.dll ,log4net.dll引用,在QuartzService.cs文件中引用命名空间 Quartz;Quartz.Impl; log4net;
右键点击QuartzService项目,属性-目标框架 ,选择.net Framwork 4,如下图:
4、添加app.config配置文件,具体配置如下:
<?xml version="1.0"?>
<configuration>
<configSections>
<section name="quartz" type="System.Configuration.NameValueSectionHandler, System, Version=1.0.5000.0,Culture=neutral, PublicKeyToken=b77a5c561934e089"/>
<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net"/>
<sectionGroup name="common">
<section name="logging" type="Common.Logging.ConfigurationSectionHandler, Common.Logging"/>
</sectionGroup>
</configSections>
<common>
<logging>
<factoryAdapter type="Common.Logging.Log4Net.Log4NetLoggerFactoryAdapter, Common.Logging.Log4net">
<arg key="configType" value="INLINE"/>
</factoryAdapter>
</logging>
</common>
<log4net>
<appender name="InfoFileAppender" type="log4net.Appender.RollingFileAppender">
<file value="log/" />
<appendToFile value="true" />
<param name="DatePattern" value="yyyyMMdd".txt"" />
<rollingStyle value="Date" />
<maxSizeRollBackups value="" />
<maximumFileSize value="1024KB" />
<staticLogFileName value="false" />
<Encoding value="UTF-8" />
<filter type="log4net.Filter.LevelRangeFilter">
<param name="LevelMin" value="INFO" />
<param name="LevelMax" value="INFO" />
</filter>
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date %-5level %logger - %message%newline" />
</layout>
</appender>
<appender name="ErrorFileAppender" type="log4net.Appender.RollingFileAppender">
<file value="log/error.txt" />
<appendToFile value="true" />
<rollingStyle value="Size" />
<maxSizeRollBackups value="" />
<maximumFileSize value="10240KB" />
<staticLogFileName value="true" />
<Encoding value="UTF-8" />
<filter type="log4net.Filter.LevelRangeFilter">
<param name="LevelMin" value="WARN" />
<param name="LevelMax" value="FATAL" />
</filter>
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date %-5level %logger - %message%newline" />
</layout>
</appender>
<root>
<level value="INFO" />
<appender-ref ref="InfoFileAppender" />
<appender-ref ref="ErrorFileAppender" />
</root>
</log4net> <!--
We use quartz.config for this server, you can always use configuration section if you want to.
Configuration section has precedence here.
-->
<appSettings>
<!-- YYC.WebService URL地址 -->
<add key="URL" value="http://localhost:43093/WebService.asmx" />
</appSettings>
<!--
<quartz >
</quartz>
-->
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/>
</startup>
</configuration>
二、创建Quartz.Net.JobLibrary并配置Job
1、在解决方案中添加新项-选择"类库",我这里命名为Quartz.Net.JobLibrary,Quartz.Net.JobLibrary用来实现多个"作业"类。Quartz.Net.JobLibrary类库中添加Quartz.dll ,log4net.dll引用。
Quartz.Net.JobLibrary类库中添加一个类Interop.cs,这个类是为了解决在Win7中出现【交互式检测】弹窗,博客园李敬然(http://www.cnblogs.com/gnielee/archive/2010/04/07/session0-isolation-part1.html)的博客详细谈到 穿透Session 0 隔离,具体代码如下:
using System;
using System.Runtime.InteropServices; namespace Quartz.Net.JobLibrary
{
public class Interop
{
public static IntPtr WTS_CURRENT_SERVER_HANDLE = IntPtr.Zero;
public static void ShowMessageBox(string message, string title)
{
int resp = ;
WTSSendMessage(
WTS_CURRENT_SERVER_HANDLE,
WTSGetActiveConsoleSessionId(),
title, title.Length,
message, message.Length,
, , out resp, false);
}
[DllImport("kernel32.dll", SetLastError = true)]
public static extern int WTSGetActiveConsoleSessionId();
[DllImport("wtsapi32.dll", SetLastError = true)]
public static extern bool WTSSendMessage(
IntPtr hServer,
int SessionId,
String pTitle,
int TitleLength,
String pMessage,
int MessageLength,
int Style,
int Timeout,
out int pResponse,
bool bWait); public static void CreateProcess(string app, string path)
{
IntPtr hToken;
IntPtr hDupedToken = IntPtr.Zero; PROCESS_INFORMATION pi = new PROCESS_INFORMATION();
SECURITY_ATTRIBUTES sa = new SECURITY_ATTRIBUTES();
sa.Length = Marshal.SizeOf(sa); STARTUPINFO si = new STARTUPINFO();
si.cb = Marshal.SizeOf(si); int dwSessionID = WTSGetActiveConsoleSessionId();
bool result = WTSQueryUserToken(dwSessionID, out hToken); if (!result)
{
ShowMessageBox("WTSQueryUserToken failed", "AlertService Message");
} result = DuplicateTokenEx(
hToken,
GENERIC_ALL_ACCESS,
ref sa,
(int)SECURITY_IMPERSONATION_LEVEL.SecurityIdentification,
(int)TOKEN_TYPE.TokenPrimary,
ref hDupedToken
); if (!result)
{
ShowMessageBox("DuplicateTokenEx failed", "AlertService Message");
} IntPtr lpEnvironment;
result = CreateEnvironmentBlock(out lpEnvironment, hDupedToken, false); if (!result)
{
ShowMessageBox("CreateEnvironmentBlock failed", "AlertService Message");
} result = CreateProcessAsUser(
hDupedToken,
path + app,
String.Empty,
ref sa, ref sa,
false, , IntPtr.Zero,
path, ref si, ref pi); if (!result)
{
int error = Marshal.GetLastWin32Error();
string message = String.Format("CreateProcessAsUser Error: {0}", error);
ShowMessageBox(message, "AlertService Message");
} if (pi.hProcess != IntPtr.Zero)
CloseHandle(pi.hProcess);
if (pi.hThread != IntPtr.Zero)
CloseHandle(pi.hThread);
if (hDupedToken != IntPtr.Zero)
CloseHandle(hDupedToken);
} [StructLayout(LayoutKind.Sequential)]
public struct STARTUPINFO
{
public Int32 cb;
public string lpReserved;
public string lpDesktop;
public string lpTitle;
public Int32 dwX;
public Int32 dwY;
public Int32 dwXSize;
public Int32 dwXCountChars;
public Int32 dwYCountChars;
public Int32 dwFillAttribute;
public Int32 dwFlags;
public Int16 wShowWindow;
public Int16 cbReserved2;
public IntPtr lpReserved2;
public IntPtr hStdInput;
public IntPtr hStdOutput;
public IntPtr hStdError;
} [StructLayout(LayoutKind.Sequential)]
public struct PROCESS_INFORMATION
{
public IntPtr hProcess;
public IntPtr hThread;
public Int32 dwProcessID;
public Int32 dwThreadID;
} [StructLayout(LayoutKind.Sequential)]
public struct SECURITY_ATTRIBUTES
{
public Int32 Length;
public IntPtr lpSecurityDescriptor;
public bool bInheritHandle;
} public enum SECURITY_IMPERSONATION_LEVEL
{
SecurityAnonymous,
SecurityIdentification,
SecurityImpersonation,
SecurityDelegation
} public enum TOKEN_TYPE
{
TokenPrimary = ,
TokenImpersonation
} public const int GENERIC_ALL_ACCESS = 0x10000000; [DllImport("kernel32.dll", SetLastError = true,
CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
public static extern bool CloseHandle(IntPtr handle); [DllImport("advapi32.dll", SetLastError = true,
CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
public static extern bool CreateProcessAsUser(
IntPtr hToken,
string lpApplicationName,
string lpCommandLine,
ref SECURITY_ATTRIBUTES lpProcessAttributes,
ref SECURITY_ATTRIBUTES lpThreadAttributes,
bool bInheritHandle,
Int32 dwCreationFlags,
IntPtr lpEnvrionment,
string lpCurrentDirectory,
ref STARTUPINFO lpStartupInfo,
ref PROCESS_INFORMATION lpProcessInformation); [DllImport("advapi32.dll", SetLastError = true)]
public static extern bool DuplicateTokenEx(
IntPtr hExistingToken,
Int32 dwDesiredAccess,
ref SECURITY_ATTRIBUTES lpThreadAttributes,
Int32 ImpersonationLevel,
Int32 dwTokenType,
ref IntPtr phNewToken); [DllImport("wtsapi32.dll", SetLastError = true)]
public static extern bool WTSQueryUserToken(
Int32 sessionId,
out IntPtr Token); [DllImport("userenv.dll", SetLastError = true)]
static extern bool CreateEnvironmentBlock(
out IntPtr lpEnvironment,
IntPtr hToken,
bool bInherit);
}
}
Quartz.Net.JobLibrary类库中再分别添加两个"作业"类JobTest1.cs,JobTest2.cs:
代码如下
JobTest1.cs
using System;
using System.Configuration;
using System.Reflection;
using Common.Logging; namespace Quartz.Net.JobLibrary
{
public class JobTest1 : IJob
{
//使用Common.Logging.dll日志接口实现日志记录
private static readonly ILog logger =
LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); private static string URL = ConfigurationManager.AppSettings["URL"]; #region IJob 成员 public void Execute(IJobExecutionContext context)
{
try
{
logger.Info("JobTest1 任务开始运行");
//ShowMsgBox msgBox = new ShowMsgBox();
//msgBox.ShowMsg(100, "AAAA", "计划任务提醒");
//WebServiceSoapClient client = new WebServiceSoapClient(new BasicHttpBinding(), new EndpointAddress(URL));
//client.Shake();
//ShowMsg(100, "AAAA", "计划任务提醒");
//Interop.ShowMessageBox("This a message from AlertService.",
// "AlertService Message");
string path = System.Windows.Forms.Application.StartupPath;
Interop.CreateProcess("Quartz.Net.WinForm.exe", path + "\\");
logger.Info("JobTest1 任务运行结束");
}
catch (Exception ex)
{
logger.Error("JobTest1 运行异常", ex);
}
} #endregion }
}
2、在Quartz.Net.JobLibrary中,添加配置文件quartz_jobs.xml,配置信息如下:
<?xml version="1.0" encoding="UTF-8"?> <!-- This file contains job definitions in schema version 2.0 format -->
<job-scheduling-data xmlns="http://quartznet.sourceforge.net/JobSchedulingData" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.0">
<processing-directives>
<overwrite-existing-data>true</overwrite-existing-data>
</processing-directives> <schedule>
<!--定义JobTest1-->
<job>
<name>JobTest1</name>
<group>JobGroup</group>
<description>测试任务1</description>
<job-type>Quartz.Net.JobLibrary.JobTest1,Quartz.Net.JobLibrary</job-type>
<durable>true</durable>
<recover>false</recover>
</job> <!--定义示JobTest2-->
<job>
<name>JobTest2</name>
<group>JobGroup</group>
<description>测试任务2</description>
<job-type>Quartz.Net.JobLibrary.JobTest2,Quartz.Net.JobLibrary</job-type>
<durable>true</durable>
<recover>false</recover>
</job> <!--定义JobTest1 触发器 每30秒执行一次任务-->
<trigger>
<cron>
<name>JobTestTrigger1</name>
<group>TriggerGroup</group>
<job-name>JobTest1</job-name>
<job-group>JobGroup</job-group>
<cron-expression>/ * * * * ?</cron-expression>
</cron>
</trigger> <!--定义JobTest2 触发器 每分钟执行一次任务-->
<trigger>
<cron>
<name>JobTestTrigger2</name>
<group>TriggerGroup</group>
<job-name>JobTest2</job-name>
<job-group>JobGroup</job-group>
<cron-expression> * * * * ?</cron-expression>
</cron>
</trigger> <!--定义JobTest2 触发器 每天凌晨01:00执行一次任务-->
<trigger>
<cron>
<name>JobTestTrigger3</name>
<group>TriggerGroup</group>
<job-name>JobTest2</job-name>
<job-group>JobGroup</job-group>
<cron-expression> * * ?</cron-expression>
</cron>
</trigger>
</schedule> <!-- Cron Expressions——Cron 表达式
Cron表达式被用来配置CronTrigger实例。Cron表达式是一个由7个子表达式组成的字符串。每个子表达式都描述了一个单独的日程细节。这些子表达式用空格分隔,分别表示:
. Seconds 秒
. Minutes 分钟
. Hours 小时
. Day-of-Month 月中的天
. Month 月
. Day-of-Week 周中的天
. Year (optional field) 年(可选的域)
一个cron表达式的例子字符串为"0 0 12 ? * WED",这表示“每周三的中午12:”。
单个子表达式可以包含范围或者列表。例如:前面例子中的周中的天这个域(这里是"WED")可以被替换为"MON-FRI", "MON, WED, FRI"或者甚至"MON-WED,SAT"。
通配符('*')可以被用来表示域中“每个”可能的值。因此在"Month"域中的*表示每个月,而在Day-Of-Week域中的*则表示“周中的每一天”。
所有的域中的值都有特定的合法范围,这些值的合法范围相当明显,例如:秒和分域的合法值为0到59,小时的合法范围是0到23,Day-of-Month中值得合法凡范围是0到31,
但是需要注意不同的月份中的天数不同。月份的合法值是0到11。或者用字符串JAN,FEB MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV 及DEC来表示。
Days-of-Week可以用1到7来表示(=星期日)或者用字符串SUN, MON, TUE, WED, THU, FRI 和SAT来表示.
'/'字符用来表示值的增量,例如, 如果分钟域中放入'0/15',它表示“每隔15分钟,从0开始”,如果在份中域中使用'3/20',则表示“小时中每隔20分钟,
从第3分钟开始”或者另外相同的形式就是'3,23,43'。
'?'字符可以用在day-of-month及day-of-week域中,它用来表示“没有指定值”。这对于需要指定一个或者两个域的值而不需要对其他域进行设置来说相当有用。
'L'字符可以在day-of-month及day-of-week中使用,这个字符是"last"的简写,但是在两个域中的意义不同。例如,在day-of-month域中的"L"表示这个月的最后一天,
即,一月的31日,非闰年的二月的28日。如果它用在day-of-week中,则表示""或者"SAT"。但是如果在day-of-week域中,这个字符跟在别的值后面,
则表示"当月的最后的周XXX"。例如:"6L" 或者 "FRIL"都表示本月的最后一个周五。当使用'L'选项时,最重要的是不要指定列表或者值范围,否则会导致混乱。
'W' 字符用来指定距离给定日最接近的周几(在day-of-week域中指定)。例如:如果你为day-of-month域指定为"15W",则表示“距离月中15号最近的周几”。
'#'表示表示月中的第几个周几。例如:day-of-week域中的"6#3" 或者 "FRI#3"表示“月中第三个周五”。
下面是一些表达式以及它们的含义。
Example Cron Expressions ——Cron表达式的例子
CronTrigger
例1 – 一个简单的每隔5分钟触发一次的表达式
"0 0/5 * * * ?" CronTrigger
例2 – 在每分钟的10秒后每隔5分钟触发一次的表达式(例如. :: am, ::10等.)。
"10 0/5 * * * ?" CronTrigger
例3 – 在每个周三和周五的10:,:,:30触发的表达式。
"0 30 10-13 ? * WED,FRI" CronTrigger
例4 – 在每个月的5号,20号的8点和10点之间每隔半个小时触发一次且不包括10点,只是8:,:00和9:30的表达式。
"0 0/30 8-9 5,20 * ?" 注意,对于单独触发器来说,有些日程需求可能过于复杂而不能用表达式表述,例如::00到10:00之间每隔5分钟触发一次,
下午1:00到10点每隔20分钟触发一次。这个解决方案就是创建两个触发器,两个触发器都运行相同的任务。
-->
</job-scheduling-data>
三、创建Quartz.Net.WinForm消息窗口
1、在解决方案中添加新项-选择"windows 应用程序",我这里命名为Quartz.Net.WinForm,具体实现从右下角逐渐显示气泡式窗口如图所示:
点击【确定】从右下角逐渐消失
通过JobTest1调用消息窗口,
string path = System.Windows.Forms.Application.StartupPath;
Interop.CreateProcess("Quartz.Net.WinForm.exe", path + "\\");
实现例如每一个小时检测是否有计划任务,一旦有任务,消息窗口弹出提醒!
四、注册Windows服务
1、cmd打开命令行工具:如果你系统是C盘,一般命令应该如下:
C:\WINDOWS\Microsoft.NET\Framework\v4.0.30319\InstallUtil.exe "你的windows服务路径"
反注册如下:
C:\WINDOWS\Microsoft.NET\Framework\v4.0.30319\InstallUtil.exe /u "你的windows服务路径"
当然也可以用sc命令
sc create windows服务名称 binPath= "你的windows服务路径"
删除服务
sc delete windows服务名称
这样注册就完了。
2、打开服务:
运行 下输入services.msc打开服务管理。如图:
刷新,可以查看到QuartzService.如下图:
3、 启动QuartzService,回到windows服务 应用程序所在目录
以上四部分大体实现了类似一些新闻消息推送,下一篇讲讲具体程序打包、安装等实现过程。希望大家多多推荐一下