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

适用于 .NET 的新任务计划程序类库

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (224投票s)

2002年6月10日

CPOL

15分钟阅读

viewsIcon

3956225

downloadIcon

63484

David Hall 的任务计划程序类库的修订版。

引言

任务计划程序是用于计划和自动启动程序的 Windows 服务。当您浏览 %WINDIR%\TASKS 文件夹(通常通过控制面板的快捷方式)时,Windows Explorer 会向该服务提供用户界面。从命令行来看,schtasks 命令和旧的 at 命令也能完成同样的操作。程序员有一个文档记录良好的 COM 接口,但 .NET 框架并未提供任何包装器。本库提供了这个 .NET 包装器。

注意:自从创建此库以来,Microsoft 已为 Windows Vista 引入了新的任务计划程序 (Task Scheduler 2.0)。此库是对 Task Scheduler 1.0 接口的包装,该接口在 Vista 中仍然可用,并与 Windows XP、Windows Server 2003 和 Windows 2000 兼容。

该库是 CodeProject.com 会员 David Hall 编写的一个软件包的扩展版本。请参阅 他的文章。在最初的作品中,David 展示了如何将一组 COM 接口整合成 .NET 风格的逻辑类库。新版本提供了许多改进,包括消除了 COM 内存泄漏。修复内存泄漏问题需要采用不兼容的类层次结构,但是,对于正在迁移的客户端,原始层次结构仍然可用,作为同一对象的一种不同组织方式。然而,由于存在泄漏,它已被弃用。稍后关于兼容性的部分将提供更多详细信息。

文档包含在 MSDN 风格的 HTML 帮助文件中,该文件与库一起下载。该文档旨在自成一体,足以供任何客户端使用该库。MSDN 记录了该库所基于的 COM 接口。当库的文档不足时,查阅 MSDN 仍然是值得的。

第二个下载包含库的源代码以及一个 C# 测试应用程序,该应用程序也用作示例代码。该测试应用程序是一个简单的命令行解释器,用于操作计划任务。可以轻松修改它以插入您可能想进行的任何测试。

本文介绍了库的类层次结构,并描述了与原始版本进行的一些更改。

目录

示例代码
与版本 1 的更改
与版本 1 的兼容性
内部更改
历史

完整的类层次结构如图所示,该图遵循 David Hall 在其文章中设定的标准。抽象类 StartableTrigger 使层次结构比我希望的要复杂一些,但已包含在内。出于完整性考虑。对于大多数用途,客户端可以更简单地将各种具体的触发器类视为 Trigger 的直接子类。

Class Diagram

ScheduledTasks

ScheduledTasks 对象表示特定计算机上的计划任务文件夹。这可以是本地计算机,也可以是调用者具有管理权限的网络上的命名计算机。在计算机的计划任务文件夹中,Windows 将每个计划任务的信息保存在一个扩展名为 *.job 的文件中。文件夹中的所有任务都被称为“已计划”,无论它们是否实际运行。

// Get a ScheduledTasks object for the computer named "DALLAS"
ScheduledTasks st = new ScheduledTasks(@"\\DALLAS");

您可以使用 ScheduledTasks 对象来访问各个任务。每个任务都有一个名称,与不带扩展名的文件名相同。从 ScheduledTasks 对象中,您可以获取当前计划的所有任务的名称。有创建、打开和删除任务的方法,所有这些都使用任务的名称。

// Get an array of all the task names
string[] taskNames = st.GetTaskNames();

ScheduledTasks 对象包含一个 COM 接口。当您完成使用该对象后,请调用其 Dispose() 方法来释放 COM 接口。

// Dispose the ScheduledTasks object to release COM resources.
st.Dispose();

任务

Task 对象代表一个已打开以供访问的单个计划任务。其属性决定了任务运行的应用程序以及您可以在 Explorer 用户界面中看到的各种其他项。其中一个属性是其 TriggerList,这是下面将讨论的另一个类。在创建 Task 或打开和修改 Task 后,必须保存它以将其新状态记录在其文件中。您可以将其保存在当前名称下,也可以提供一个新名称。使用新名称保存类似于 Windows 应用程序中的“另存为”:打开的 Task 仍与新名称关联。Task 没有公共构造函数。Task 只能由 ScheduledTasks 对象使用其 Open()Create() 方法创建。

// Open a task named "foo" from the local computer's scheduled tasks
ScheduledTasks st = new ScheduledTasks();
Task t = st.OpenTask("foo");

Task 保留 COM 接口。当您完成使用 Task 后,请调用其 Close() 方法来释放接口。

// Close the Task object to release COM resources.
t.Close();

TriggerList

TriggerListTrigger 对象的集合。每个 Task 都有一个可以通过只读属性获取的 TriggerListTriggerList 没有公共构造函数。每个 TriggerList 都与一个 Task 相关联,并在创建 Task 时一同创建,因此 TriggerList 只能从其 Task 对象中获取。

// Get the triggers for the local task "foo".
ScheduledTasks st = new ScheduledTasks();
Task t = st.OpenTask("foo");
TriggerList tl = t.Triggers;

当任务关闭时,TriggerList 会失效。(进一步访问会导致错误。)

触发器

Trigger 是当满足条件时导致任务运行的条件。在 Explorer UI 中,触发器称为“计划”。有几种不同类型的触发器,并且不同类型的数据是适当的。因此,Trigger 类实际上是一个抽象类,从中派生出一系列具体类。每个具体的 Trigger 类都有唯一的构造函数,用于指定它所代表的条件的特定类型。要为任务设置新触发器,请构造一个适当种类的 Trigger,然后将其添加到任务的 TriggerList 中。

// Add a trigger to run task "foo" at 4:30 pm every day
Trigger tg = new DailyTrigger(16, 30);  // hour and minute
ScheduledTasks st = new ScheduledTasks();
Task t = st.OpenTask("foo");
t.Triggers.Add(tg);
t.Save();

当创建新的 Trigger 时,它不与任何 TaskTriggerList 相关联,被称为未绑定。未绑定的 Trigger 不包含任何 COM 资源,也没有特殊的用法协议。当 Trigger 被添加到 TriggerList 时,它就被称为已绑定,并且除非从该集合中移除,否则它将一直保持绑定状态。已绑定的 Trigger 包含一个 COM 接口。当相应的 Task 关闭时,COM 接口将被释放,因此客户端无需采取特别的注意事项。未绑定和已绑定 Trigger 之间的区别在客户端代码中很少出现,但以下几点是关键:

  • 一个 Trigger 一次只能在一个 TriggerList 中。因此,已绑定的 Trigger 不能添加到 TriggerList 或分配给它,因为它已经在其中了。要将同一个 Trigger 放入第二个 TriggerList,请使用 Clone() 创建一个未绑定的副本。
  • 任务关闭后,无法使用已绑定的 Trigger 对象。(进一步访问会导致错误。)

示例代码

几个示例应该足以展示与任务计划程序的交互方式。有关类的完整文档,请参阅随附的 MSDN 风格的帮助文件。

列出计算机上的计划任务

此示例连接到名为“DALLAS”的计算机,并在控制台上打印其计划任务的摘要。如果无法访问 DALLAS,或者运行代码的用户帐户在 DALLAS 上没有管理员权限,则构造函数将抛出异常。示例代码中未处理此类异常。

// Get a ScheduledTasks object for the computer named "DALLAS"
ScheduledTasks st = new ScheduledTasks(@"\\DALLAS");

// Get an array of all the task names
string[] taskNames = st.GetTaskNames();

// Open each task, write a descriptive string to the console
foreach (string name in taskNames) {
    Task t = st.OpenTask(name);
    Console.WriteLine("  " + t.ToString());
    t.Close();
}

// Dispose the ScheduledTasks object to release COM resources.
st.Dispose();

计划运行新任务

创建一个名为“D checker”的新任务,该任务在 D: 驱动器上运行 chkdsk。

//Get a ScheduledTasks object for the local computer.
ScheduledTasks st = new ScheduledTasks();

// Create a task
Task t;
try {
    t = st.CreateTask("D checker");
} catch (ArgumentException) {
    Console.WriteLine("Task name already exists");
    return;
}

// Fill in the program info
t.ApplicationName = "chkdsk.exe";
t.Parameters = "d: /f";
t.Comment = "Checks and fixes errors on D: drive";

// Set the account under which the task should run.
t.SetAccountInformation(@"THEDOMAIN\TheUser", "HisPasswd");

// Declare that the system must have been idle for ten minutes before 
// the task will start
t.IdleWaitMinutes = 10;

// Allow the task to run for no more than 2 hours, 30 minutes.
t.MaxRunTime = new TimeSpan(2, 30, 0);

// Set priority to only run when system is idle.
t.Priority = System.Diagnostics.ProcessPriorityClass.Idle;

// Create a trigger to start the task every Sunday at 6:30 AM.
t.Triggers.Add(new WeeklyTrigger(6, 30, DaysOfTheWeek.Sunday));

// Save the changes that have been made.
t.Save();
// Close the task to release its COM resources.
t.Close();
// Dispose the ScheduledTasks to release its COM resources.
st.Dispose();

更改任务的运行时间

此代码打开一个特定任务,然后更新具有开始时间的任何触发器,将时间更改为凌晨 4:15。这利用了 StartableTrigger 抽象类,因为只有这些触发器才有开始时间。

// Get a ScheduledTasks object for the local computer.
ScheduledTasks st = new ScheduledTasks();

// Open a task we're interested in
Task task = st.OpenTask("D checker");

// Be sure the task was found before proceeding
if (task != null) {
    // Enumerate each trigger in the TriggerList of this task
    foreach (Trigger tr in task.Triggers) {
        // If this trigger has a start time, change it to 4:15 AM.
        if (tr is StartableTrigger) {
            (tr as StartableTrigger).StartHour = 4;
            (tr as StartableTrigger).StartMinute = 15;
        }
    }
    task.Save();
    task.Close();
}
st.Dispose();

常见问题解答

为什么我收到访问异常?

这个问题通常会出现在希望从 ASP.NET 代码中使用任务计划程序的客户端身上。通常,此类代码在 ASPNET 帐户下运行,该帐户权限较低,无法使用任务计划程序。解决方案是将您的代码设置为在另一个更具特权的帐户下运行。这称为模拟,您可以在 web.config 文件中进行设置。

任务计划程序不需要客户端以管理员权限运行,但如果不是,则在可执行操作方面会有所限制。我还没有找到这些文档记录得很好。然而,直到最近,非管理员似乎可以查看和操作他们创建的任务,但不能查看其他任务。在 Windows XP SP2 中,似乎有一些通用化。在 Explorer 中,任务属性对话框上有一个新的“安全”选项卡。还有一个简短的文档说明任务的文件权限将决定其他用户如何使用它们。Read = 查看它,Read/Execute = 运行任务,Write = 修改任务。

我的任务是否必须有一个帐户和密码?

计划任务必须被指定一个特定的帐户来运行,或者可以设置为在本地系统帐户下运行。本地系统帐户是许多标准服务使用的伪帐户。它对本地系统具有广泛的访问权限,但它有时无法直接与用户交互,也没有网络特权。要将任务设置为在本地系统帐户下运行,客户端必须已经在该帐户或管理员帐户下运行。

如果任务需要与用户交互,您需要设置一个特定的用户帐户,任务将只与该用户交互。如果您的客户端根据用户不同而运行在不同的帐户下,您可以安排任务而不必知道用户的密码。为此,您可以设置一个特定的任务标志 RunOnlyIfLoggedOn,并提供用户名和空密码。

为什么我在远程机器上收到拒绝访问?

您必须在本地计算机和远程计算机上都拥有足够特权的用户帐户下运行。如果没有域控制器,您需要运行在一个在两台计算机上都具有相同名称和相同密码的用户帐户下。这有很多种方式会出错,但您可以尝试先跨两台计算机访问私有文件并使其正常工作来纠正它。

我正在打开远程计算机上的计划任务,但其中没有任务。

这通常是因为您命名机器时没有在开头包含两个反斜杠。出于某种原因,任务计划程序会找到机器并似乎已打开它,但其中没有任务。

如何将最长运行时间设置为无限?

底层服务要求将一个特殊值作为 MaxRunTime 传递。这在库的早期版本中有效,但很不方便。现在有一个名为 MaxRunTimeLimited 的新属性。将其设置为 False 以获得无限时间。

您是如何制作酷炫文档的?

MSDN 风格的帮助文件是使用 Microsoft 的 Sandcastle(预览版)和 Eric Woodruff 的 GUI 前端 Sandcastle Help File Builder 构建的。

该库在 Vista 中可以使用吗?

Vista 引入了具有新程序接口 (2.0) 的新任务计划程序。该库访问旧的 1.0 接口,因此相同的代码可以在 Vista、XP、Server 2003 或 Windows 2000 中运行。兼容性的代价是您无法获得新任务计划程序的任何功能。

我在 Vista 上尝试了该库,它似乎运行正常。不过,我还没有弄清楚所有的特权要求,所以要小心。Vista 的用户帐户控制 (UAC) 意味着即使是包中包含的测试应用程序也可能遇到问题。任务计划程序允许您仅使用用户特权执行某些操作,而其他操作则需要您以管理员身份运行。

在 2007 年对项目的更新中,我修改了测试应用程序,使其自动以管理员特权运行。当然,您必须进行确认。这是通过向项目添加清单文件并执行自动化的生成后步骤将清单添加到可执行文件来完成的。这使用了 mt.exe 工具,因此该工具必须在您的路径中的某个位置。我的 mt.exe 在 Windows SDK 中。如果您使用 Visual Studio 2008,则无需执行生成后步骤即可添加清单。只需将其添加到 TestApp 项目中。

与版本 1 的更改

这个新库的主要目标是提供对 COM 资源的控制。这需要改变访问模型,以便客户端能够清楚地知道资源何时被分配和释放。新模型类似于文档应用程序。客户端打开和关闭任务,并且有相当于保存和另存为的操作,使更改永久化。当 Task 打开时,它持有 COM 接口等,然后在 Task 关闭时释放。这避免了原始设计中的内存泄漏,至少在客户端正确关闭其任务的程度上。

另一个目标是扩展 XML 文档,从而扩展帮助文件。我在测试该库的过程中学到了很多关于任务计划程序的知识,并试图包含我所学到的内容。还做了许多其他改进,但以下几点最值得注意:

  • Save() 的一个带名称参数的新重载版本。这允许您更改现有任务的名称。
  • Trigger 对象上许多以前只读的属性现在也可以设置了。
  • 现在有一个用户界面可以像 Windows 任务计划程序一样编辑任务。
  • Trigger 添加了 ICloneable。这允许复制 Trigger 以便在多个 Task 中使用。

与版本 1 的兼容性

如上所述,David Hall 库中的原始类仍然可用以供过渡使用,因此该库基本上是“即插即用”兼容的。还有其他几个小问题可能会影响一些用户:

  1. TaskHidden 属性在 Save() 时生效,就像所有其他属性一样。在原始版本中,它立即生效。Hidden 属性实际上仅为了与版本 1 兼容,因为任务标志提供了相同的功能。
  2. 版本 1 将触发器的类型作为特殊的 TriggerType 枚举值提供,但该功能现已删除。使用 GetType() 方法来确定 Trigger 的类型,就像您对任何其他类型一样。

内部

源文件易于浏览,并且按照高标准的可读性编写。嗯,可读性是见仁见智的。我只能说我试图写好并记录好一切。在 Visual Studio 中打开解决方案并浏览。

通过阅读任何代码都可以学到很多东西,但我可以推荐该库的一点是学习如何为 COM 接口编写包装器。有很多设计决策,但真正棘手的部分在于您实际上如何从 C# 与 COM 进行交互。如果您必须这样做,这段代码可能会很有趣,特别是从资源分配和释放的角度来看。

任务计划程序通过 COM 接口返回的字符串属性通常在 COM 任务内存中分配,并返回字符串的指针。如果封送处理程序处理了从指针到 System.String 的转换,则内存不会被释放。我将其更改为封送为 System.IntPtr,并编写了代码在返回到客户端之前释放内存。

历史

06/10/02

  • 首次发布

06/17/02

  • 添加了 SchedulerTaskList 类,以使库向上兼容 David Hall 的原始库。这些类在其他方面是不需要的。
  • 扩展了文章以解释兼容性问题。

08/09/02

  • 更新了库以修复 Mark Stafford 报告的 Trigger 属性中的错误。
  • 更新了 SetAccountInformation 方法的文档。
  • taskscheduler.xml 文件包含在库和文档下载中。这在对象浏览器中提供了文档。
  • 修复了一些格式,并将此历史记录部分添加到文章中。

08/20/02

  • 更新了库以修复 Ashley Barton 指出的 Save(name) 问题。

10/25/02

  • Task 添加了方法 DisplayPropertySheet()。与现有的 DisplayForEdit 一样,它显示用于编辑任务的属性对话框。然而,新参数可以控制对话框中显示哪些页面。它实际上使 DisplayForEdit 过时,但为了兼容性,DisplayForEdit 仍然可用。这项工作由 CodeProject.com 会员 **Ingram Leedy** 资助。感谢 Ingram,让这项增强功能能够提供给社区。
  • Task 添加了方法 NextRunTimeAfter()。现有的 NextRunTime 属性返回任务的下一次运行时间,但新方法允许调用者指定一个用于计算下一次运行的时间。这允许所有未来的运行时间都可以交互式地计算。
  • 将程序集版本提高到 1.1。
  • 更新了文章,添加了一些信息并提高了可读性(我希望)。

9/8/04

  • 更新了 HTML 帮助文档,进行了一些小的更正,特别是关于任务标志的描述。
  • 对文章进行了一些小的更正,并添加了一个 FAQ。
  • 添加了一个任务属性 MaxTimeLimited,以简化时间限制的设置。
  • 修复了 Hidden 属性和 Hidden 任务标志,使其能够取消隐藏以及隐藏(bug 修复)。
  • 将程序集版本提高到 1.2

11/30/07

  • 使用 Visual Studio 2005/.NET 2.0 进行编译的微小更改。也适用于 VS 2008。
  • SetAccountInformation 添加了一个重载,该重载接受密码作为 SecureString(由 CodeProject.com 会员“aule”贡献)。
  • 通过切换到 Microsoft 的 Sandcastle 工具和 Eric Woodruff 的 Sandcastle Help File Builder 来改进帮助文件。
  • 添加了生成后步骤,将管理员级别的提升清单添加到测试应用程序。
  • 将程序集版本提高到 1.3
© . All rights reserved.