上一篇文章我们讲了需求的分解,以及如何抓取网页,链接在这里:https://www.jianshu.com/p/46d523bed44f。
这篇的主题是,如何启用Windows 服务来定时执行任务。
细分需求
- Windows服务,如何创建并管理(安装、卸载、启动、暂停)一个Windows 服务。
- 定时任务,如何让一个任务按规定时间执行。
Windows服务
本文开发环境使用VS 2015进行开发,首先新建项目,建一个Windows 服务项目:
打开服务类(原来默认名称是Service1,重命名为Service):
在服务类上右键,添加安装程序:
这时候发现新建了一个ProjectInstaller类:
打开该类后,发现有两个控件在上面,我们需要做一些配置:
-
配置ServiceName,查看serviceInstaller1的属性,修改ServiceName:
-
配置服务账号,查看serviceProcessInstaller1的属性,修改Account为LocalSystem:
好了,到目前位置,我们的服务已经准备好了,为了做记录,我们打上日志,在Service.cs的代码里面, 我们写上日志如下:
protected override void OnStart(string[] args)
{
Log.Info("OnStart");
}
protected override void OnStop()
{
Log.Info("OnStop");
}
服务项目配置好之后,我们接下来需要再创建一个WPF项目,用于管理服务(安装、卸载、启动、暂停)。新建一个WPF项目,然后在界面上添加4个按钮:
有了按钮,并添加点击事件后,接下来是核心的代码,如何安装、卸载、启动、暂停服务,这里写了一个专门用于管理服务的Class:
public class ServiceHelper
{
// 安装服务
public static void InstallService(string serviceName, string strServiceInstallPath)
{
IDictionary mySavedState = new Hashtable();
try
{
if (!ServiceIsExisted(serviceName))
{
// 安装服务
AssemblyInstaller assemblyInstaller = new AssemblyInstaller();
mySavedState.Clear();
assemblyInstaller.Path = strServiceInstallPath;
assemblyInstaller.UseNewContext = true;
assemblyInstaller.Install(mySavedState);
assemblyInstaller.Commit(mySavedState);
assemblyInstaller.Dispose();
// 将服务设置为自动启动
ChangeServiceStartType(2, serviceName);
}
}
catch (Exception ex)
{
throw new Exception("安装服务出错:" + ex.Message);
}
}
// 卸载服务
public static void UnInstallService(string serviceName, string strServiceInstallPath)
{
try
{
if (ServiceIsExisted(serviceName))
{
using (AssemblyInstaller assemblyInstaller = new AssemblyInstaller())
{
assemblyInstaller.UseNewContext = true;
assemblyInstaller.Path = strServiceInstallPath;
assemblyInstaller.Uninstall(null);
assemblyInstaller.Dispose();
}
Thread.Sleep(1000);
}
}
catch (Exception ex)
{
throw new Exception("卸载服务时出错:" + ex.Message);
}
}
// 卸载服务时结束主程序进程
public static void KillProcess(string exeName)
{
//后缀起始位置
int startpos = -1;
try
{
if (exeName != "")
{
if (exeName.ToLower().EndsWith(".exe"))
{
startpos = exeName.ToLower().IndexOf(".exe");
}
if (startpos < 0) return;
Process[] processes = Process.GetProcessesByName(exeName.Remove(startpos));
if (processes == null) return;
foreach (Process p in processes)
{
p.Kill();
Thread.Sleep(1000);
return;
}
}
}
catch (Exception ex)
{
throw new Exception(string.Format("强制关闭进程 {0} 出错:{1}", exeName, ex.Message));
}
}
// 判断服务是否存在
public static bool ServiceIsExisted(string serviceName)
{
ServiceController[] services = ServiceController.GetServices();
foreach (ServiceController s in services)
{
if (s.ServiceName == serviceName)
{
return true;
}
}
return false;
}
public static bool IsServiceRunning(string serviceName)
{
if (!ServiceIsExisted(serviceName))
{
return false;
}
ServiceController service = new ServiceController(serviceName);
if (service == null)
{
return false;
}
return service.Status == ServiceControllerStatus.Running || service.Status == ServiceControllerStatus.StartPending;
}
// 启动服务
public static void StartService(string serviceName)
{
if (ServiceIsExisted(serviceName))
{
System.ServiceProcess.ServiceController service = new System.ServiceProcess.ServiceController(serviceName);
if (service.Status != ServiceControllerStatus.Running && service.Status != ServiceControllerStatus.StartPending)
{
service.Start();
service.Refresh();
}
}
}
// 停止服务
public static void StopService(string serviceName)
{
if (ServiceIsExisted(serviceName))
{
System.ServiceProcess.ServiceController service = new System.ServiceProcess.ServiceController(serviceName);
if (service.Status == ServiceControllerStatus.Running)
{
service.Stop();
service.Refresh();
}
}
}
/// <summary>
/// 修改服务的启动项
/// </summary>
/// <param name="startType">1:自动(延迟启动,)2:自动,3:手动,4:禁用 </param>
/// <param name="serviceName">服务名称</param>
/// <returns></returns>
public static bool ChangeServiceStartType(int startType, string serviceName)
{
try
{
RegistryKey regist = Registry.LocalMachine;
RegistryKey sysReg = regist.OpenSubKey("SYSTEM");
RegistryKey currentControlSet = sysReg.OpenSubKey("CurrentControlSet");
RegistryKey services = currentControlSet.OpenSubKey("Services");
RegistryKey servicesName = services.OpenSubKey(serviceName, true);
servicesName.SetValue("Start", startType);
servicesName.SetValue("Type", 0x00000110);
}
catch (Exception ex)
{
throw new Exception("设置启动项失败:" + ex.Message);
}
return true;
}
}
在按钮事件里,就好处理了,直接调用相对应的函数即可(篇幅有限,只列出安装和启动的事件):
private void BtnInstall_Click(object sender, RoutedEventArgs e)
{
Log.Info("开始安装服务");
// 卸载服务
ServiceHelper.UnInstallService(SvcName, SvcInstallPath);
ServiceHelper.KillProcess(AppExeName);
// 安装服务
ServiceHelper.InstallService(SvcName, SvcInstallPath);
this.CheckStatus();
Log.Info("完成安装服务");
}
private void BtnStart_Click(object sender, RoutedEventArgs e)
{
Log.Info("开始启动服务");
ServiceHelper.StartService(SvcName);
this.CheckStatus();
Log.Info("完成启动服务");
}
让我们来看下运行截图:
Steven.WindowsService服务正常运行。
定时任务
定时任务采用大名鼎鼎的 quartznet,关于quartz的说明网上已经有很多了,简单来说,就是可以定制任意事件执行任务的类库,通过对时间的配置,可以很方便的调整任务的执行时间,以及执行什么任务都是通过配置文件来实现。
这里不对quartz进行详细说明, 只是说关键的几个知识点:
IJob
具体的任务实现类,需要继承IJob,实现Execute函数即可。在上一节中我们已经由抓取网页的代码了,这里就不展开。
cronExpression
任务的执行时间表达式,先来看下一条配置:
<Task name="BeiLin.Service.Tasks.Jobs.TaskManagerJob" cronExpression="0/5 * * * * ?" />
可以看到cronExpression里面由一下几个部分组成,从左到右一次是:
名称 | 是否必须 | 允许值 | 特殊字符 |
---|---|---|---|
秒 | 是 | 0-59 | , - * / |
分 | 是 | 0-59 | , - * / |
时 | 是 | 0-23 | , - * / |
日 | 是 | 1-31 | , - * ? / L W C |
月 | 是 | 1-12 或 JAN-DEC | , - * / |
周 | 是 | 1-7 或 SUN-SAT | , - * ? / L C # |
年 | 否 | 空 或 1970-2099 | , - * / |
通过表达式我们可以算出每天晚上10点的表达式是:0 0 22 * * ?