65.9K
CodeProject 正在变化。 阅读更多。
Home

C#调度器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.24/5 (20投票s)

2002年6月24日

15分钟阅读

viewsIcon

402430

downloadIcon

14917

本文演示了如何使用 C# 编写服务以及 .NET 提供的设置选项。

引言

C# 调度程序项目旨在演示如何使用 C# 编写服务以及 .NET 提供的设置选项,而不是将其本身作为主要目标。最终的应用程序更像是一个挂钩,用来放置其他主题。这并不意味着最终项目毫无用处,而是它达到了演示的目的,如果最终结果被证明有用,那就更好了。

该项目包含两部分:第一部分是一个简单的对话框应用程序,它将数据存储在注册表中;第二部分是一个服务,它将使用计时器来检查当前时间,如果有一个程序设置为在该时间运行,则运行它。本文的重点将主要放在服务以及如何让所有功能正常工作上。

调度程序项目包含三个子项目和一个测试项目,该测试项目用于测试在安装程序类中运行的注册表代码。之所以需要测试项目,是因为无法调试通过开发环境生成的安装程序项目。只有在您自己创建类并继承自 installer,并将其作为应用程序的一部分运行,才能调试安装程序中的代码。

三个主要项目是:Schedule 项目,即用于设置作为调度程序运行的应用程序的对话框应用程序;SchedulerService,它是应用程序的核心;以及 SchedulerSetup,它是开发环境生成的设置项目。

该程序设计运行在 Windows XP (家庭版和专业版) 和 Windows 2000 上。

对话框应用程序

上面显示的 ScheduleExample 可执行文件是一个简单的对话框应用程序,它将要调度的信息存储在注册表中。信息以 ScheduleName 的格式存储,ScheduleName 是您为该调度指定的任意名称,以及要调度的文件,该文件存储为要调度的文件的完整路径和可执行文件名。然后,每个计划项都显示在列表框中。

对话框的“运行时间”部分是一个简单的用户控件,允许用户输入计划任务要运行的小时和分钟。应用程序中没有提供秒的选项,因为这需要服务几乎不断地检查是否有需要运行的任务。与其这样做,不如让服务每分钟只检查一次是否有任何任务计划在该分钟内运行。

服务

程序中的 SchedulerService 部分是一个标准的 C# 服务程序,作为 Windows 子系统的一部分运行。服务通常没有用户界面,并执行后台处理。服务继承自 ServiceBase 类。

public class ScheduleService : System.ServiceProcess.ServiceBase	
		

该类允许它覆盖新服务所需的成员。最常用的函数将被覆盖。

protected virtual void OnStart( string[] args )	
		

protected virtual void OnStop()	
		

这些是大多数服务将覆盖以执行其任务的函数。这是因为当服务启动时,会调用 OnStart 函数,而当服务停止时,会调用 OnStop 函数。Windows 系统中执行此操作的部分称为 SCM (我发音为 SCUM) 或服务控制管理器。其他可以覆盖的函数可在 ServiceBase 类帮助文件中找到,因此我不会在此重复,但它们都响应 SCM 指定的不同系统函数。

服务的工作相当简单,它所做的只是每分钟检查一次注册表,查看 ScheduleExample 键下是否有需要运行的程序。主要代码如下。

Process process = new Process();
ProcessStartInfo startInfo = new ProcessStartInfo();
string strHours;
string strMins;
DateTime dtNow = DateTime.Now;

foreach( string strName in subKeys )
{
	subKey = key.OpenSubKey( strName );
	if( subKey == null )
		continue;

	process.StartInfo = startInfo;
	/// GetValue returns an object so to string is required
	startInfo.FileName = subKey.GetValue( "FileToRun" ).ToString();
	startInfo.UseShellExecute = true;

	/// get the time that the app is supposed to run and compare it
	/// to the current time.

	strHours = subKey.GetValue( "Hours" ).ToString();
	strMins = subKey.GetValue( "Mins" ).ToString();

	if( dtNow.Hour == Int32.Parse( strHours ) && dtNow.Minute == Int32.Parse( strMins ) )
	{
		/// start the process
		/// Notice that here I am not keeping track of the processes
		/// just letting them run.
		if( process.Start() == false )
		{
EventLog.WriteEntry( "Unable to start the process " + startInfo.FileName );
		}

	}

}		

以上代码大部分应该不言自明。有趣的点是 Process 和 EventLog。先处理 EventLog,Event Log 是一个系统范围的日志记录系统,已经存在很长时间了,但往往被大多数用户忽略,原因很简单:大多数计算机用户甚至不知道它的存在,因此,如果应用程序不断地向其写入数据,事件日志就会变得非常大,并开始占用大量空间,因此在写入时通常需要谨慎。在这段代码中,我一直坚持只在出错时写入 Event Log 的规则,尽管在调试期间,其中有一些代码告诉我正在发生什么,但这些代码已被删除。服务已经自带了一个 Event Log 成员,默认情况下会写入 Event Log 的应用程序部分 (在 Windows XP 上可通过“控制面板\管理工具\事件查看器”访问)。但使用服务安装程序时,您需要添加自己的 EventLog 对象,该对象可在 System.Diagnostics 中找到。

Process 是计算机上运行的任何应用程序。Process 类可用于获取有关正在运行的程序的信息,并随时对其进行操作。它们也可以像上面的代码一样,用于在计算机上启动新进程。这与在 NT 4 上使用 C++ 启动进程的方式几乎相同。会创建一个 StartInfo 对象,在本例中是 ProcessStartInfo 类型的对象,并用与要启动的程序相关的数据填充,然后将其赋给 Process 自己的 ProcessStartInfo 成员。您也可以只创建 ProcessStartInfo 对象并进行填充,然后调用静态函数 Process.Start(ProcessStartInfo startInfo)

在服务中使用计时器

在 .NET 中,有三种不同类型的计时器可供使用。标准 Timer 用于窗体,是早期 Windows 版本中可用的标准计时器。System.Threading.Timer 是一种新型计时器,它不像旧计时器那样只调用预定义的函数,而是调用在新线程上启动的函数;而 System.Timer 在这里被使用,它与其他计时器不同,它专门设计用于不需要始终有用户界面的服务器类型应用程序。

timer = new System.Timers.Timer();
timer.Interval = 60000;
timer.Elapsed += new ElapsedEventHandler( ServiceTimer_Tick  );	

在这里您可以看到计时器的设置代码,它实例化计时器并将间隔设置为每 60 秒触发一次。Elapsed 事件处理程序与远程函数调用的设置方式完全相同,并且函数必须以相同的方式定义。

private void ServiceTimer_Tick(object sender, System.Timers.ElapsedEventArgs e)		

这就像您期望的远程函数定义方式一样,带有 ElapsedEventArgs 对象,该对象继承自 EventArgs 类。

访问注册表

注册表访问在 .Net 编程提到的几乎所有地方都已介绍,所以我不会在此过多赘述。主要的注册表项在 .NET 中已预定义,并用作注册表对象的静态对象,

Registry.CurrentUser 

Registry.LocalMachine		

这些在 MSDN 的 Registry 类下有很好的文档。为了打开一个键,您有两个 OpenSubKey 函数的重载:一个只接受键的名称,另一个接受要打开的键的名称和一个布尔值,如果为 false,则表示该键是只读的;如果为 true,则表示需要对该键进行写入访问。

我只想指出我发现的一个小问题,那就是最初代码中的注册表访问都使用了 CurrentUser 键。一切正常,因为安装程序在工作,对话框应用程序也在工作,只有服务表现得很奇怪。我发现当在服务中运行时,一行读取的代码:

RegistryKey reg = Registry.CurrentUser.OpenSubKey( "Software", 
                                false ).OpenSubKey( "ScheduleExample", false );

这段代码是从对话框应用程序复制粘贴过来的,但并没有按预期工作。它明显无法打开正确的注册表键,所以我使用了 CreateSubKey 调用,如果找不到该键,它就会创建它。然后我搜索了注册表以查找 ScheduleExample 键。我发现当从服务运行时,上面的代码行并没有在 HKEY_CURRENT_USER 键下打开注册表键,而是使用了 HKEY_USER 键并在那里创建了 ScheduleExample 键。我只是想提及这一点,以提醒那些不留意的人。

服务安装程序

要为服务添加自定义安装程序,请双击包含该服务的 .cs 文件,然后单击上面“Misc”面板上会出现的“Add Installer”选项。这将向项目中添加 ProjectInstaller.cs 文件,该文件包含两个项:一个 ServiceProcessInstaller 和一个 ServiceInstaller。

ServiceProcessInstaller 包含系统选项,它控制服务将如何运行,在当前情况下,服务将在 LocalSystem 帐户下运行;而 ServiceInstaller 控制服务本身,它设置 ServiceName 和服务的 StartType,通常为自动,除非您想从另一个程序启动它,在这种情况下,您将其设置为手动。我暂时想不出为什么您想将新服务安装为禁用,但我确定有人会这样做。最重要的是,服务中设置的 ServiceName 必须与 ServiceInstaller 中给出的 ServiceName 完全匹配。所有设置安装程序所需的选项都可以通过用户界面中的选项来完成。

安装选项

该项目有两种安装方式,不能简单地复制和运行,因为它需要设置一个服务才能在运行的计算机上运行。第一种是使用 .NET 框架自带的 InstallUtil.exe 应用程序,第二种是利用开发环境创建安装项目并将其添加到代码中的能力。

通过 InstallUtil.exe 安装应用程序

要安装服务,当项目解压后,在 debug 目录中有一个 install.bat 文件。它使用 InstallUtil.exe 程序,该程序作为 .NET Framework 的一部分分发。要使用它,我将我的 Windows 路径设置为包含 .NET Framework 目录,但如果您愿意,您可以将文件复制到所需文件夹,或者在 install.bat 中使用完整路径。我推测使用框架的安装程序会找到 .NET Framework 的位置,但目前这样就可以了。应该注意的是,您必须在文件名末尾包含 .exe。InstallUtil 不会自动执行此操作,因此如果您不指定它,您将收到文件未找到的错误消息。

一旦文件路径设置好,installutil 就会开始工作。在我 Windows XP 家庭电脑上,这会导致弹出对话框,请求服务运行所需的登录参数。只有当服务选择在用户帐户下运行时才会出现此对话框。理论上,这需要用户输入当前帐户的登录信息,尽管我发现在 Windows XP 家庭版和 Windows 2000 专业版上,即使输入了正确的帐户信息,该对话框也会报告错误。

Running a transacted installation.

Beginning the Install phase of the installation.
See the contents of the log file for the 
     c:\freepr~1\schedu~1\schedu~1\bin\debug\schedulerservice.exe assembly's progress.
The file is located at Service.log.
Installing assembly 'c:\freepr~1\schedu~1\schedu~1\bin\debug\schedulerservice.exe'.
Affected parameters are:
   assemblypath = c:\freepr~1\schedu~1\schedu~1\bin\debug\schedulerservice.exe
   logfile = Service.log
Installing service SchedulerService...
Creating EventLog source SchedulerService in log Application...

The Install phase completed successfully, and the Commit phase is beginning.
See the contents of the log file for the 
       c:\freepr~1\schedu~1\schedu~1\bin\debug\schedulerservice.exe assembly's progress.
The file is located at Service.log.
Committing assembly 'c:\freepr~1\schedu~1\schedu~1\bin\debug\schedulerservice.exe'.
Affected parameters are:
   assemblypath = c:\freepr~1\schedu~1\schedu~1\bin\debug\schedulerservice.exe
   logfile = Service.log

The Commit phase completed successfully.

The transacted install has completed.		
		

上面是项目安装在我电脑上的日志文件的输出。

通过安装项目安装应用程序。

要为您的应用程序创建安装项目,请单击“文件\新建\项目”,然后选择“Setup and Deployment Projects”,再选择一个 Setup 项目,然后单击“添加到当前解决方案”单选按钮。完成后,项目将与 Detected Dependencies 文件夹一起创建。

如果您在解决方案资源管理器窗格中选择 Setup 项目,您会注意到顶部有多个编辑器选项。它们是“文件系统编辑器”,用于设置要包含在项目中的文件。“注册表编辑器”,允许您创建项并为要设置的任何程序设置注册表。这里应该注意的是,我通过安装程序添加注册表项,原因很简单。在我编写安装程序时我不知道它能做到这一点,而且项目中的注册表访问代码仍然需要保留,所以我决定保持原样。

接下来是“文件类型编辑器”,它允许您设置要与您的应用程序关联的自定义文件类型。然后是“用户界面编辑器”,它允许您以仅通过 InstallShield 等产品才能获得的方式添加和删除安装过程中的对话框。下一个是“自定义操作编辑器”,稍后将使用它来启动 SchedulerService 程序集中的安装程序。最后一个是启动条件编辑器,它允许您检查注册表中的某些条件,或者检查要设置代码的计算机已安装 .NET。

要将所需文件添加到安装项目,请单击“文件系统编辑器”按钮并选择“应用程序”文件夹。右键单击“应用程序”文件夹并选择“添加\项目输出”。这将弹出一个对话框,其中列出了可以添加到安装中的文件。有趣的是,其中一个选项是包含项目的源代码,我觉得这是一种分发此类演示项目的好方法。对于普通应用程序,目前我们感兴趣的是“Primary Output”选项,它将包含我们项目的核心输出,并且还将包括运行项目所需的已检测到的依赖项。

我遇到的问题是,尽管文件被安装了,但项目没有安装,因为 SchedulerService 中的安装程序没有被安装程序自动运行。这个问题的答案很简单:告诉它安装。通过“自定义操作编辑器”可以做到这一点。选择它会显示一个包含四个文件夹的屏幕,这些文件夹是用于自定义安装程序的重载函数,即 install、rollback、commit 和 uninstall。选择您在创建的安装类中重载的函数,右键单击并选择“添加自定义操作”,这将弹出一个对话框,允许您从“文件系统编辑器”中选择任何选项。选择“应用程序文件夹”,这将为您提供已设置为安装的应用程序列表。通过选择包含自定义安装程序的应用程序,您可以告诉安装程序在运行安装时要执行应用程序中的安装程序。

还有一个问题需要注意,那就是使用“添加\项目输出”时,如果在一个被设置为项目输出的应用程序中运行安装程序,该文件将被设置为从将项目文件添加到安装程序的目录运行。这意味着在测试中,我发现尽管安装程序工作正常,但服务本身实际上是从开发文件夹运行的,而不是从安装文件夹运行的。解决这个问题的方法是将可执行文件放在应用程序文件夹中,而不是项目输出。

运行服务

SchedulerService 程序配置为自动启动,但这并不意味着它会在安装后立即启动,所以需要一些手动帮助。安装完成后,转到“控制面板\管理工具\服务”并双击“服务”。这将打开服务对话框,找到 SchedulerService 并双击它。这将打开服务属性对话框,从中单击“登录”选项卡并勾选“允许服务与桌面进行交互”。这将授予服务启动具有图形用户界面的程序的权限。需要手动执行此操作的原因是 System.ServiceProcess 命名空间中存在一个 ServiceType 枚举,但它仅被 ServiceController 类使用,并且其访问权限仅限于 get,这意味着虽然您可以使用 ServiceController 来检查服务是否可以访问桌面,但目前无法直接将服务设置为使用 ServiceType 枚举,因为在任何其他服务类中都没有提及它。我只能推测我遗漏了什么,或者此领域的功能将在 .NET 框架的后续版本中扩展。

勾选“允许服务与桌面进行交互”复选框后,选择“常规”选项卡,然后单击“启动”按钮。最后单击“应用”按钮,服务将会在每次计算机重启时正常启动,然后您只需要运行应用程序的对话框部分来维护调度程序。

© . All rights reserved.