Windows with C++

Task Scheduler 2.0

Kenny Kerr

Code download available at:WindowsWithC++2007_10.exe(156 KB)

Contents

Task Service and Storage
Using the Task Service
Task Definitions
Task Actions
Task Triggers
What's Next?

Task Scheduler received a complete overhaul in Windows Vista®. Although there are some similarities, the new Task Scheduler (dubbed Task Scheduler 2.0) is far more powerful than the original, which has been around since Windows® 98. It is no longer just a simple tool for end users, but a powerful platform for designing and managing complex background operations—so much so that it can remove the need for Windows service development in many cases.

Imagine your project needs to check for updates automatically. You might think to write a Windows service that runs in the background and checks for updates every few days. Instead of a service that has to run all the time, you can design a scheduled task that only runs every few days, checks for updates, and then stops. Even better, you can ensure that it runs only when a user is logged in so that resources aren't needlessly consumed when no one is around to perform an update.

It is evident that Microsoft developers consider the new Task Scheduler ready for prime time by the simple fact that it is used by many parts of Windows Vista to manage background tasks. This has the added benefit of reducing the number of new services that would inevitably have been created to handle all those tasks. It also simplifies Windows administration and diagnostics since it is far simpler to interrogate a task to see what it's doing than it is to figure out what a black-box Window service is up to. For tasks that form part of Windows itself, the Task Scheduler also acts as a task host, consolidating many task-specific processes into one and thereby further reducing the resources that are required to host those operations.

In this column I am going to explore the main concepts and building blocks that make up the Task Scheduler so you can get up to speed as quickly as possible and start benefiting from this great new service right away.

Task Service and Storage

The story begins with the Task Scheduler service, the service responsible for the actual scheduling of tasks. Since Task Scheduler uses the file system to store task information, it is possible to enumerate and prepare tasks without the help of the service, but you won't get very far without it. Indeed, future versions of the Task Scheduler may well change the storage format and location, so relying on this is not advised. In fact, it is critical that you not rely on accessing the storage directly as the Task Scheduler may not recognize tasks that are written directly to the file system and will likely refuse to run tasks that have been tampered with. Since Task Scheduler is such an integral part of Windows Vista, it is no longer possible for administrators to simply disable it. This is good news for developers because it means you can safely rely on it to perform your background operations and abstract the task storage.

By convention, tasks are grouped by company, product, and component name. For example, the Windows Disk Defragmenter stores its tasks in the \Microsoft\Windows\Defrag subfolder. Each task is stored as an XML document defined by the Task Scheduler schema. You can then create tasks directly with an XML parser like XmlLite (see my previous MSDN® Magazine article at msdn.microsoft.com/msdnmag/issues/07/04/Xml/), or use the extensive COM interfaces provided by the Task Scheduler API. The COM interfaces also allow you to provide and query the XML definitions for tasks directly. In that way you get the benefit of using XML to author and query tasks while being shielded from their actual storage. Task Scheduler is also accessible remotely.

Using the Task Service

The ITaskService interface is the gateway into the Task Scheduler API. All of the definitions you will need are located within the taskschd.h header file. Of course, you will need the latest Windows SDK that includes support for Windows Vista. Start by creating a local, in-process instance as follows:

CComPtr<ITaskService> service; 
HR(service.CoCreateInstance(__uuidof(TaskScheduler)));

(I use the HR macro in these samples to clearly identify where methods return an HRESULT that needs to be checked; you can replace this with appropriate error handling—whether that is throwing an exception or returning the HRESULT yourself.)

Next, call the Connect method to establish a connection to the task scheduler on the computer of your choice. The Connect method is defined as follows:

HRESULT Connect( VARIANT computer, 
  VARIANT user, VARIANT domain, VARIANT password);

The use of the VARIANT type is unfortunate, but easy enough to deal with using the handy Active Template Library (ATL) CComVariant derived class. To connect to the local computer, simply omit the computer name. To connect using the caller's identity or effective token, don't specify a user name. To use the current domain for the local computer, don't specify a domain name. Finally, only specify a password if you also specified a user name. So, in its simplest form, you might call the Connect method as follows to connect to the local computer's Task Scheduler service:

HR(service->Connect( CComVariant(), // local computer 
  CComVariant(), // current user 
  CComVariant(), // current domain 
  CComVariant())); // no password

You can now perform various operations, such as creating tasks and enumerating preexisting folders and tasks. The first step is to get to know the ITaskFolder interface since tasks cannot be used until they have been stored in a folder. ITaskService's GetFolder method returns an ITaskFolder interface pointer for a given folder. A backslash represents the root task folder as illustrated here:

CComPtr<ITaskFolder> folder; 
HR(service->GetFolder(CComBSTR(L"\\"), &folder));

You can then create a folder for your application as follows:

CComPtr<ITaskFolder> newFolder; 
HR(folder->CreateFolder( CComBSTR(L"Company\\Product"), 
  CComVariant(), &newFolder));

The optional second parameter to CreateFolder specifies the security descriptor for the newly created file system folder. If omitted, the folder will simply inherit the security descriptor from its parent. You can override the default access control using the security descriptor definition language (SDDL). For example, to allow administrators, as well as the local system, full control, you might use the following SDDL:

HR(folder->CreateFolder( CComBSTR(L"Company\\Product"), 
  CComVariant(L"D:(A;;FA;;;BA)(A;;FA;;;SY)"), &newFolder));

Briefly, D: indicates that the subsequent access control entries (ACE) form part of the discretionary access control list (DACL). The first entry indicates that access is being allowed (A) with file all (FA) being granted to built-in administrators (BA). The second entry indicates the same access is being granted to the local system (SY). Keep in mind that the security descriptor definition is only used in creating the Product folder, while the parent Company folder will still be created with an inherited security descriptor. Also, make sure you don't deny access to the Local System account since that is the identity used by the Task Scheduler service and unexpected behavior is sure to follow when the scheduler cannot access the tasks it is meant to schedule!

Given a preexisting folder, you can use ITaskService's GetFolder method to open a folder relative to the root task folder. Note that ITaskFolder provides the same method for opening folders relative to the folder represented by the interface instance:

CComPtr<ITaskFolder> productFolder; 
HR(service->GetFolder( CComBSTR(L"Company\\Product"), &productFolder)); 
CComPtr<ITaskFolder> componentFolder; 
HR(productFolder->GetFolder( CComBSTR(L"Component"), &componentFolder));

Figure 1 illustrates the Task Scheduler Explorer sample application included with the download for this column. It provides a handy view of your tasks and is helpful as you start using the task scheduler API.

Figure 1 Task Scheduler Explorer

Figure 1** Task Scheduler Explorer **(Click the image for a larger view)

Task Definitions

Before a task can be created, it needs to be modeled by a task definition. Figure 2 describes the components that make up a task definition. Due to the complexity of a task definition and its necessity in creating tasks, I don't recommend jumping right into task creation. It helps to first grasp the fundamental building blocks. Before I dive into the specifics of defining actions and triggers, let's briefly look at how a task definition is composed and created.

Figure 2 Task Definition Components

Task Definition
Actions One or more actions define the work the task will perform.
Triggers One or more triggers indicate when to start the task.
Principal Authorization and auditing is based on the security context.
Settings These settings control the runtime behavior and limits of the task.
Data A string made available to actions.
RegistrationInfo Administrative bookkeeping information.

Start by asking the Task Scheduler service to create an empty task definition that you can populate:

CComPtr<ITaskDefinition> definition; 
HR(service->NewTask(0, // reserved &definition));

Before registering the task definition in a specific folder, you need to populate it with at least one action, and optionally with triggers and other information. Once it's ready you can register it in a folder using the RegisterTaskDefinition method as follows:

CComPtr<IRegisteredTask> registeredTask; 
HR(folder->RegisterTaskDefinition( CComBSTR(L"Task"), 
  definition, TASK_CREATE_OR_UPDATE, CComVariant(), // user name 
  CComVariant(), // password 
  TASK_LOGON_INTERACTIVE_TOKEN, 
  CComVariant(), // sddl 
  &registeredTask));

An IRegisteredTask interface pointer is returned representing the newly registered task. This is the interface used to start or stop tasks as well as to retrieve runtime information about a task.

RegisterTaskDefinition's first parameter specifies the name of the new task. Since this value is also used to name the file saved in the folder, it must comply with the naming conventions of the file system. You can set this parameter to NULL and the method will generate a GUID to use as the name, but this is discouraged since it isn't very meaningful or helpful to administrators of the computer. The second parameter specifies the task definition describing the task to be created. I will dig into that in more detail in the following sections. The third parameter specifies a value from the TASK_CREATION enumeration. The most common values are TASK_ CREATE and TASK_UPDATE, while TASK_CREATE_OR_UPDATE is just a bitwise OR of both.

The next three parameters specify the registration credentials. RegisterTaskDefinition will ensure that the specified user will at least have read access to the registered task even if the DACL you specify in the parameter that follows does not specifically grant it. Task Scheduler supports a number of different logon techniques. The previous example uses TASK_LOGON_INTERACTIVE_TOKEN indicating that the task will only run given an interactive logon session for the specified user. If a user name and password is not provided it will assume the caller's identity. A password is not stored since an interactive logon session is required.

TASK_LOGON_PASSWORD is an alternative that instructs Task Scheduler to create a batch logon session for the task. In this case, both a user name and password must be provided and the account must be granted the "Log on as a batch job" privilege.

TASK_LOGON_S4U is yet another option that provides a more secure alternative. It takes advantage of a service for user (S4U) logon to run the task on behalf of the specified user, but without having to store the password. Since the Task Scheduler runs within the local system account, it can create a S4U logon session and receive a token that can not only be used for identification, but also for impersonation on the local computer. Normally a S4U token is only good for identification.

In addition to the credentials supplied at the time you register a task, the IPrincipal interface offers further control over the security context that will be provided for the task. In particular, IPrincipal provides a convenient way of controlling the execution level for the process hosting the task scheduler engine for your task. Start by querying the task definition for the associated principal object, then set the RunLevel property as needed. This needs to be done before the task definition is registered. Here is an example:

CComPtr<IPrincipal> principal; 
HR(definition->get_Principal(&principal)); 
HR(principal->put_RunLevel(TASK_RUNLEVEL_HIGHEST));

TASK_RUNLEVEL_HIGHEST indicates that the task should be run in the most unrestrictive security context for the particular user. Assuming the user is an administrator, it will provide the process with a token that is not restricted by User Account Control (UAC). The alternative is TASK_RUNLEVEL_LUA, which provides a restricted, or non-elevated, token.

Task Actions

Task Scheduler .NET

Although a Microsoft® .NET Framework wrapper for Task Scheduler is not available, it is not hard to use thanks to a feature of the common language runtime (CLR) known as runtime callable wrappers (RCW).

Since Task Scheduler provides scripting-friendly COM interfaces, it is possible to use the API from managed code with little effort. Simply reference the type library using the Visual Studio® Add Reference dialog. It is a bit misleading since the type library for Task Scheduler 2.0 is named TaskScheduler1.1 Type Library. Nevertheless, Visual Studio will create an interop assembly that provides all of the managed definitions for the COM interfaces so you can construct and reference the various COM objects directly in managed code using RCW.

Here is an example illustrating the C# equivalent of the code samples from the first section of this column, connecting to the Task Scheduler service and creating a folder for your product's tasks:

ITaskService service = new TaskSchedulerClass(); 
service.Connect(null, // local computer 
  null, // current user 
  null, // current domain 
  null); // no password
 ITaskFolder folder = service.GetFolder("\\"); 
ITaskFolder newFolder = folder.CreateFolder( "Company\\Product", 
  "D:(A;;FA;;;BA)(A;;FA;;;SY)");

From here, you can go on to create task definitions, actions, triggers, and much more, all from the comfort of your favorite language that targets the .NET Framework.

With the groundwork out of the way, let's take a look at the variety of action types provided by the task scheduler. Actions are created and added to a task definition using the IActionCollection interface pointer returned by the task definition's Actions property:

CComPtr<ITaskDefinition> definition; 
HR(service->NewTask(0, // reserved &definition)); 
CComPtr<IActionCollection> actions; 
HR(definition->get_Actions(&actions));

IActionCollection provides a Create method that creates a new action object and adds it to the collection. The actions themselves are exposed through interfaces deriving from IAction. Here's an example of this:

CComPtr<IAction> action; 
HR(actions->Create(TASK_ACTION_EXEC, &action));

TASK_ACTION_EXEC is by far the most common action type as it describes a command-line operation and can be used to launch all kinds of programs. It can even launch documents and other file types registered with the Windows shell. To set the action-specific properties, you need to first query for the type-specific interface and CComQIPtr elegantly takes care of doing just that:

CComQIPtr<IExecAction> execAction(action);

Given an IExecAction interface pointer, you can now configure the action to run the command of your choice. Here's an example that kicks off the Sysinternals Contig tool to defragment my virtual hard drive images:

HR(execAction->put_Path(CComBSTR(L"g:\\Tools\\contig.exe"))); 
HR(execAction->put_Arguments(CComBSTR(L"-s g:\\VirtualMachines")));

Although running a command is convenient for administrators, it is not always the most appropriate solution for developers since the command doesn't have any awareness of the action it represents within the task or that it is even part of a scheduled task. Fortunately, Task Scheduler provides another action type that is designed specifically to address this issue. The TASK_ACTION_COM_HANDLER action type creates a COM server and queries it for the ITaskHandler interface that you must implement. This establishes a communications channel between the action and the Task Scheduler engine allowing cleaner integration of application-specific operations with Task Scheduler.

Here's how you create a COM action:

CComPtr<IAction> action; 
HR(actions->Create(TASK_ACTION_COM_HANDLER, &action)); 
CComQIPtr<IComHandlerAction> comAction(action); 
HR(comAction->put_ClassId(CComBSTR( L"{25C6DB11-4ADC-4e89-BA47-04576C7AA46A}")));

You will of course need a COM server that is registered with the specified Class Identifier (CLSID) and that implements the ITaskHandler interface. Figure 3 provides an example of a COM class implementing ITaskHandler with the help of ATL.

Figure 3 COM Action Class

class DECLSPEC_UUID("25C6DB11-4ADC-4e89-BA47-04576C7AA46A") 
  DECLSPEC_NOVTABLE CoSampleTask : public CComObjectRootEx<CComMultiThreadModel>, 
  public CComCoClass<CoSampleTask, &__uuidof(CoSampleTask)>, 
  public ITaskHandler { 
    public: DECLARE_REGISTRY_RESOURCEID(IDR_SAMPLETASK) 
    BEGIN_COM_MAP(CoSampleTask) 
      COM_INTERFACE_ENTRY(ITaskHandler) 
    END_COM_MAP() 
    private: 
      STDMETHODIMP Start(IUnknown* taskScheduler, BSTR data); 
      STDMETHODIMP Stop(HRESULT* actionResult); 
      STDMETHODIMP Pause(); STDMETHODIMP Resume(); 
};

There is an additional step you need to perform that, as of this writing, is not documented anywhere but without which your COM action will fail to load. The Task Scheduler engine expects to load your COM server into a separate process using the CLSCTX_LOCAL_SERVER context when it calls the CoCreateInstance function with your CLSID (unless your action is part of Windows itself, in which case it may be loaded in-process). For this to succeed, your COM server needs to be configured to allow activation in a surrogate process. Technically, you could use a traditional out-of-process component, but that is less common and not very practical in most cases. This is easily solved by updating your registration code. Update your ATL registration to include the DllSurrogate value. Here's an example:

HKCR { 
  NoRemove AppID { 
    '%APPID%' = s 'SampleTask' { val DllSurrogate = s '' } 'SampleTask.DLL' { val AppID = s '%APPID%' }
   } 
}

COM actions are superior in a number of ways to the other action types. Notably, a COM action is the only action type that allows an action to be stopped in a graceful and predictable manner. After the Task Scheduler engine creates an instance of your COM class, it calls the Start method, providing an interface pointer you can use to communicate with the Task Scheduler. The Start method can then query for the ITaskHandlerStatus interface, providing a simple mechanism for the task to notify the task scheduler of its progress and eventual completion. The action can end in two ways. A user may decide to end the task using a variety of methods, all of which result in your COM class's Stop method being called. Alternatively, your COM class may report that it has completed its duties by calling ITaskHandlerStatus's TaskCompleted method.

Another useful action type is provided that allows a task to send e-mail messages. Unlike the previous action types, this one is less useful for performing operations or administrative tasks, but it is ideally suited for use as a notification mechanism. The next section deals with triggers, but for now it is enough to say that tasks can be started by events other than traditional calendar- or time-based triggers. Here's how you create an e-mail action:

CComPtr<IAction> action; 
HR(actions->Create(TASK_ACTION_SEND_EMAIL, &action)); 
CComQIPtr<IEmailAction> emailAction(action); 
HR(emailAction->put_From(CComBSTR(L"kenny@example.com"))); 
HR(emailAction->put_To(CComBSTR(L"karin@example.com"))); 
HR(emailAction->put_Subject(CComBSTR(L"subject"))); 
HR(emailAction->put_Body(CComBSTR(L"body"))); 
HR(emailAction->put_Server(CComBSTR(L"mail.example.com")));

Task Triggers

Task Scheduler provides a variety of triggers that allow you to kick off your tasks without user intervention. There are the usual time- and calendar-based triggers as well as a number of useful event-based triggers.

Tasks are created and added to a task definition using the ITriggerCollection interface pointer returned by the task definition's Triggers property:

CComPtr<ITriggerCollection> triggers;
HR(definition->get_Triggers(&triggers));

ITriggerCollection provides a Create method that creates a new trigger object and adds it to the collection. The triggers themselves are exposed through interfaces deriving from ITrigger. Here's an example of a trigger that might be suitable for the example from the previous section to start a defrag operation:

CComPtr<ITrigger> trigger; 
HR(triggers->Create(TASK_TRIGGER_WEEKLY, &trigger));
 CComQIPtr<IWeeklyTrigger> weeklyTrigger(trigger); 
HR(weeklyTrigger->put_StartBoundary(CComBSTR( L"2007-01-01T02:00:00-08:00"))); 
HR(weeklyTrigger->put_DaysOfWeek(0x01)) // Sunday

TASK_TRIGGER_WEEKLY is one of 11 trigger types that are available in the initial release of the new Task Scheduler. Figure 4 provides a summary of the trigger types that are available to you as well as the associated COM interface for each. The StartBoundary property indicates the earliest date that the task will be triggered as well as the time of day that the task will be started. The DaysOfWeek property specifies the particular days of the week to run the task. It uses a bitmask so that you can combine any days you wish. The trigger in the example above will cause the task to run every Sunday at 2 A.M. PST starting on the first day of 2007.

Figure 4 Trigger Types

Type Interface Usage and Options
TASK_TRIGGER_EVENT IEventTrigger Events defined by Windows event log.
TASK_TRIGGER_TIME ITimeTrigger Time of day; optional random delay.
TASK_TRIGGER_DAILY IDailyTrigger Every n days; optional random delay.
TASK_TRIGGER_WEEKLY IWeeklyTrigger Specified days of week; every n weeks; optional random delay.
TASK_TRIGGER_MONTHLY IMonthlyTrigger Specified days of month; specified months; optional last day of month; optional random delay.
TASK_TRIGGER_MONTHLYDOW IMonthlyDOWTrigger Specified days of week; specified weeks of month; specified months of year; optional last week of month; optional random delay.
TASK_TRIGGER_IDLE IIdleTrigger When computer is idle.
TASK_TRIGGER_REGISTRATION IRegistrationTrigger When task is created or updated; optional delay.
TASK_TRIGGER_BOOT IBootTrigger When computer is booted; optional delay.
TASK_TRIGGER_LOGON ILogonTrigger When user logs on; optional delay.
TASK_TRIGGER_SESSION_STATE_CHANGE ISessionStateChangeTrigger Various session events.

What's Next?

There are many compelling reasons to use the new Task Scheduler in Windows Vista and beyond. If you have relied on Task Scheduler in previous versions of Windows, you will undoubtedly be pleased by all the new features and capabilities.In fact, Task Scheduler may soon replace many custom-built schedulers written for applications today.

I haven't come close to covering all of Task Scheduler's features in this column. Take a moment to explore the documentation in the Windows SDK (msdn2.microsoft.com/en-us/library/aa383614.aspx) and you will discover some additional features, including the ability to author tasks directly in XML, greater control over the security context that tasks are executed in, the ability to enumerate and manage running tasks, and much more!

Send your questions for Kenny to mmwincpp.

Kenny Kerr is a software craftsman specializing in software development for Windows. He has a passion for writing and teaching developers about programming and software design. Reach Kenny at weblogs.asp.net/kennykerr.