利用任务计划程序
在应用程序中使用任务计划程序接口可能会很棘手,因为它需要对 COM 技术有详细的了解。本文基于简化与接口的通信,提出了一个实用的解决方案。
本文由 Rafal Piotrowski 撰写,最初发表于 2005 年 3 月的 Software 2.0 杂志。您可以在 SDJ 网站上找到更多文章。
引言
Windows 操作系统系列为我们提供了一个名为“任务计划程序”的有用机制。利用它,我们可以指示系统多次执行任务,而无需手动运行,也无需记住它们。适用于任务计划程序的典型示例是定期扫描系统以查找病毒或木马。
我们可以通过两种方式使用任务计划程序,一种是通过在“控制面板”中运行“任务计划程序”,并使用对话框窗口设置所需的选项。从我们的角度来看,更有趣的方法是利用任务计划程序 API,即一系列对象和 COM 接口,它们允许在我们的程序中安排任务。但是,在我们开始使用此机制之前,每次想要安排任务时,都必须确保检查任务计划程序是否正在运行。在 Windows 95 和 NT5 下,它默认未安装,因此您可能需要将其打开。在 Win98 及更新版本中,计划程序默认安装并处于活动状态,但也会有程序可以将其禁用。
任务计划程序 API
使用任务计划程序 API,我们可以
- 创建新任务,
- 设置任务的执行时间和频率,
- 调整任务的执行时间和频率,
- 定义任务应如何执行(参数),
- 停止正在运行的任务。
我们通过创建适当的对象和 COM 接口,并执行它们的适当方法来添加任务。
创建任务对象
为了创建新任务,我们使用接口 ITaskScheduler
、ITask
和 IPersistFile
。为了成功创建,应向 ITaskScheduler::NewWorkItem()
方法提供一个唯一的任务名称。如果该名称已在使用中,该方法将返回 ERROR_FILE_EXISTS
。
任务创建涉及一系列操作。要获取 TaskScheduler
类对象,必须调用函数 CoInitialize()
(初始化 COM 库)和 CoCreateInstance()
。然后,调用 ITaskScheduler::NewWorkItem()
来创建新任务;此方法返回指向 ITask
接口的指针。通过调用 IPersistFile::Save()
方法将新创建的任务写入磁盘 — IPersistFile
是 ITask
接口支持的标准 COM 接口)。最后,通过调用 ITask::Release()
方法,我们释放获得的资源(Release()
是 IUnknown
接口的方法,ITask
继承自该接口)。
现在我们有了一个任务,但系统不知道何时应运行它。要实现这一点,我们需要创建一个触发器 — 一个与特定任务绑定的对象,指示何时以及多久执行一次。
创建触发器
在创建触发器时,我们利用三个接口:IScheduledWorkItem
、ITaskTrigger
和 IPersistFile
。我们可以区分执行任务的触发器(在给定时间段内)和执行任务的触发器(在用户选择不活动一段时间后)。由于可以将多个触发器分配给单个任务对象,因此我们可以例如让任务每天运行一次,并在用户不活动 5 分钟后运行。
要创建触发器,首先必须调用 CoInitialize()
或 CoCreateInstance()
;如果紧随创建任务之后设置触发器,则此步骤不是必需的。接下来,我们调用 ITaskScheduler::Activate
方法来获取 ITask
接口,然后调用 IScheduledWorkItem::CreateTrigger()
来创建触发器。我们定义一个 TASK_TRIGGER
结构(wBeginDay
、wBeginMonth
和 wBeginYear
变量必须具有可接受的值),然后执行 ITaskTrigger::SetTrigger
来设置触发器的参数。最后,我们使用 IPersistFile::Save()
将新创建的触发器保存到磁盘,并使用 ITask::Release()
释放资源。
Listing 1 展示了将任务添加到任务计划程序的示例。
Listing 1. 将任务添加到计划程序
HRESULT hr = ERROR_SUCCESS; ITaskScheduler *pITS; hr = CoInitialize(NULL); if (SUCCEEDED(hr)){ hr = CoCreateInstance(CLSID_CTaskScheduler, NULL,CLSCTX_INPROC_SERVER, IID_ITaskScheduler,(void **) &pITS); } else { return 1; } // Calling ITaskScheduler::NewWorkItem() // to create a new task LPCWSTR pwszTaskName; ITask *pITask; IPersistFile *pIPersistFile; pwszTaskName = L"Test Task1"; hr = pITS->NewWorkItem(pwszTaskName,// task name CLSID_CTask, // class identifier IID_ITask, // interface identifier (IUnknown**)&pITask); // address of a pointer // to the ITask interface // Call IUnknown::QueryInterface // to obtain a pointer to // IPersistFile and IPersistFile::Save, // to write the task to disk hr = pITask->QueryInterface(IID_IPersistFile, (void **)&pIPersistFile); hr = pIPersistFile->Save(NULL,TRUE); // creating a trigger ITaskTrigger* pITaskTrig = NULL; IPersistFile* pIFile = NULL; TASK_TRIGGER rTrigger; WORD wTrigNumber = 0; hr = pITask->CreateTrigger ( &wTrigNumber, &pITaskTrig ); //filling the TASK_TRIGGER structure ZeroMemory ( &rTrigger, sizeof (TASK_TRIGGER) ); rTrigger.cbTriggerSize = sizeof (TASK_TRIGGER); rTrigger.wBeginYear = 2004; rTrigger.wBeginMonth = 4; rTrigger.wBeginDay = 10; rTrigger.wStartHour = 10; rTrigger.wStartMinute = 0; // associate the trigger with the task rTrigger.TriggerType = TASK_TIME_TRIGGER_ONCE; hr = pITaskTrig->SetTrigger ( &rTrigger ); hr = pITask->QueryInterface ( IID_IPersistFile, (void **) &pIFile ); hr = pIFile->Save ( NULL, FALSE ); printf("Succesfully created task and trigger.\n");
CTask 类
由于在 C++ 中编写基于 COM 技术程序并非易事,因此我创建了一个名为 CTask
的类来包装 COM 调用。使用该类,可以快速有效地将任务添加到任务计划程序。
为了使用 CTask
类创建新任务,我们需要指定
- 程序的名称和完整路径(必需),
- 我们希望传递给程序的命令行参数(可选),
- 程序的启动目录(可选),
- 帐户名和密码(NT 必需),
- 开始日期和时间(可选,对于仅运行一次的任务不可用),
- 结束日期(见上文,可选),
- 执行频率(一次、每天、每周、每月,必需),
- 注释(将在任务计划程序小程序的对话框中显示的文本 - 可选)。
上述每个任务参数都与相应命名的类方法相关联,用于设置正确的值
void SetProgram ( LPCTSTR szProgram )
void SetParameters ( LPCTSTR szParams )
void SetStartingDir ( LPCTSTR szDir )
void SetAccountName ( LPCTSTR szAccount )
void SetPassword ( LPCTSTR szPassword )
void SetStartDateTime ( const CTime& timeStart )
void SetStartDateTime ( const SYSTEMTIME& timeStart )
void SetEndDate ( const CTime& timeEnd )
void SetEndDate ( const SYSTEMTIME& timeEnd )
void SetFrequency ( CScheduledTask::ETaskFrequency freq )
void SetComment ( LPCTSTR szComment )
此外,还可以通过使用前缀为 Get_
的方法读取每个成员值,因此程序名称可以通过 GetProgram()
获取,而 GetParameters()
则检查参数等。
在 Listing 2 中,可以看到一个使用 CTask
类将任务添加到任务计划程序的简单控制台应用程序的源代码。警告:链接之前,您必须将运行时库更改为多线程的,方法是选择:项目->设置,C++ 选项卡,代码生成类别,然后在“使用运行时库”下拉列表中选择“Debug Multithreaded”。
Listing 2. CTask
类的一个简单应用
#include "CTask.h" int main(int /*argc*/, char* /*argv[]*/) { CTask task; //constructing a CTask class object CTime time(2004, 04, 01, 10, 10, 0); //date of creation LPCTSTR sTaskName("TaskName"); //name //replace if such a task already exists BOOL bReplace = TRUE; //full path to the programme task.SetProgram ( "" ); //execution parameters task.SetParameters ( "" ); //starting directory task.SetStartingDir ( "" ); //account name task.SetAccountName ( "" ); //account password task.SetPassword ( "" ); //comment task.SetComment ( "" ); task.SetStartDateTime ( time ); //start date //frequency adding the task to the scheduler task.SetFrequency ( CTask::freqOnce ); if ( S_OK == task.SaveTask ( sTaskName, bReplace )){ MessageBox(GetActiveWindow(), "The task has been added!", "", MB_OK); return 0; } else { MessageBox(GetActiveWindow(), "Failed to create a task!", "", MB_OK); return 1; } }
添加 GUI
现在是时候构建一个更高级的应用程序来管理任务计划程序,并配备图形用户界面。为此,我们将使用 MS Visual C++ IDE。
构建用户界面
让我们创建一个新项目,并为其指定一个我们选择的名称,例如 MyTaskDemo。在向导中选择 MFC Application 作为应用程序类型。选择“Dialog – Based”选项,将其他选项保留为默认值,然后选择“Next”再选择“Finish”。在“Resources Editor”中,选择向导为我们创建的对话框窗口。现在我们将为我们的应用程序构建一个用户界面,其目标外观如图 1 所示。
Figure 1. MyTaskDemo 应用程序的用户界面
“Options”组不需要太多讨论。它由四个“Edit”字段、四个“Static”类型的控件和两个按钮组成。在“Frequency”组中,不要忘记在“Radio”按钮的选项中选择“Group”。开始日期和结束日期是 DateTimePicker
控件。开始日期和结束日期的格式为“Short Date”,开始时间为“Time”格式。为结束日期选择“Show Null”以允许任务无限期运行。在“Password”字段(一个“Edit”控件)的属性中,应选择“Password”字段。
准备 CTask
类的应用程序
将 CTask.cpp 和 CTask.h 文件添加到项目中(Project->Add To Project->Files...)。您应该会在类视图窗口中看到 CTask
类。暂时,我们将将其放在一边,切换到资源编辑器,然后选择我们构建了 GUI 的对话框。按键盘上的 [Ctrl+W],或选择 Class Wizard。我们将被询问是否要为我们的对话框创建新类或使用现有类;我们选择现有类 CMyTaskDemoDlg
。下一个任务是为按钮添加“Event Handlers” - 处理事件的函数,例如鼠标点击;最简单的方法是在“Resources Editor”中双击按钮,然后通过将成员变量与相应控件绑定来将它们添加到 CMyTaskDemoDlg
类;我们可以通过从列表中选择控件并单击“Add Variable”按钮来完成此操作。
添加实际程序代码
现在是时候添加源代码来执行必要的初始化并使用 CTask
类了。比较下载中的两个源项目Step 2 和 Step 3,在这方面可能会非常有启发性。
在 MyTaskDemo.cpp 的 InitInstance()
方法中,使用 AfxOleInit()
函数初始化 OLE。
大部分代码将放入 MyTaskDemoDlg.cpp。我只会简要讨论需要添加的内容,更多细节可以在下载的 Step 3 项目的源代码中找到。
首先,提供以下指令
#include CTask.h, //CTask 类的声明
#include <shlobj.h> //用于 SHBrowseForFolder()
- 在
OnInitDialog()
方法中,提供必要的初始化(参见源代码), - 在
OnBrowseProgram()
方法中,创建一个CFileDialog
类对象,它将允许我们选择所需的程序, - 在
OnBrowsePath()
方法中,我们将使用 Shell API 函数SHBrowseForFolder()
,它可以轻松选择程序的执行目录, - 在
OnDeleteTask()
方法中,我们将最终让我们的类发挥作用并使用其方法Ctask::DeleteTask()
, - 在
OnAddTask()
方法中,将完成大部分工作:此方法读取控件的值,检查它们的有效性,并在数据正确时设置CTask
的适当成员变量并调用Ctask::SaveTask()
。
我们的应用程序现在可以向任务计划程序添加任务。上面的示例可以成功用于编写防病毒软件、个人日历等等。请注意,如果我们为任务提供了名称,但没有提供帐户名或密码,任务将被添加但永远不会执行。我将此方面的改进留给读者作为练习。
可能的改进
另一种可能的改进是创建处理 ANSI 和 UNICODE 的函数对。XXX 不是最佳解决方案,因为从 2000 版本开始,Windows 仅使用 UNICODE,因此所有 ANSI 处理函数都会转换为 UNICODE。任务计划程序 API 还允许列出计划程序中设置的所有任务、浏览它们或编辑其内容;因此,本文绝非详尽无遗。