Hey, Scripting Guy! Question

Hey, Scripting Guy! I need to be able to run a script when my computer is idle. But only when it is idle. I have looked in WMI and everywhere else and cannot find anything that will detect when the computer is idle. Can you tell me how to do it?

- NM

SpacerHey, Scripting Guy! Answer

Hi NM,

You know, our computers are busy plotting against us every minute of the day. Of this we are sure. However, if our computers decide they do not have anything better to do, they might condescend to perform tasks for us. That is right; we can create a scheduled task to run when the computer is idle. This is, like, soooo cool!

This week we are talking about scheduled tasks. There are at least three different ways that you can work with scheduled tasks. This “Hey, Scripting Guy!” article provides an example of working with scheduled tasks via WMI. This script lists scheduled tasks created via WMI. We also have a variety of scheduled task scripts from the Community-Submitted Scripts Center. The Schedule.Service API is discussed in two articles. The first article talks about creating a task and setting the trigger. The second article discusses organizing tasks. This article discusses executing scheduled tasks via the Schedule.Service API. And the Task Scheduler is documented on MSDN.

NM, I could not just create a single script that runs when the computer is idle and leave it at that. That would be lame. Instead, I decided to borrow some functions from yesterday's script and give you the ability to list scheduled tasks, delete scheduled tasks, and create scheduled tasks that run when the computer is idle. I did give it the name CreateScheduledTasks.ps1, but you can rename it to whatever you wish. Maybe something like List_Delete_Create_ScheduledTasks.ps1 would be more accurate, but I did not want to do all that typing.

Function Get-ScheduleService
{
  New-Object -ComObject schedule.service
} #end Get-ScheduleService
Function Get-Tasks($folder)
{ #returns a task object
 $folder.GetTasks(1)
} #end Get-Tasks

Function Get-Task($folder,$name)
{ #returns a task object
 $folder.GetTask($name)
} #end Get-Tasks

Function New-TaskObject($path)
{ #returns a taskfolder object
 $taskObject = Get-ScheduleService
 $taskObject.Connect()
 if(-not $path) { $path = "\" }
 $taskObject.GetFolder($path)
} #end New-TaskObject

Function New-Task($path,$Name,$description,$author,$command,$user,$password,$sddl)
{ 
  New-Variable -Name TASK_TRIGGER_IDLE -Value 6 -Option constant
  New-Variable -Name TASK_ACTION_EXEC -Value 0 -Option constant
  New-Variable -Name CREATE_OR_UPDATE -Value 6 -Option constant
  New-Variable -Name TASK_LOGON_NONE -Value 0 -Option constant
  $user=$password=$sddl=$null
  $taskObject = Get-ScheduleService
  $taskObject.Connect()
  if(-not $path) { $path = "\" }
  $rootFolder = $taskObject.GetFolder($path)
  $taskdefinition = $taskObject.NewTask($null)
  $regInfo = $taskdefinition.RegistrationInfo
  if(-not $description) { $description = "Created by script" }
  $regInfo.Description = $description
  if(-not $author) { $author = $env:username }
  $regInfo.Author = $author
  $settings = $taskdefinition.Settings
  $settings.StartWhenAvailable = $true
  $settings.Hidden = $false
  $triggers = $taskdefinition.Triggers
  $trigger = $triggers.Create($TASK_TRIGGER_IDLE)
  $action = $taskdefinition.Actions.Create($TASK_ACTION_EXEC)
  $action.Path = $command
  $rootFolder.RegisterTaskDefinition($Name,$taskdefinition,$CREATE_OR_UPDATE,
                                     $user,$password,$TASK_LOGON_NONE,$sddl)

} #end New-Task

Function Remove-Task($folder,$name)
{
 $folder.DeleteTask($name,$null)
} #end Remove-Task

# *** entry point to script ***

# Get-Tasks -folder (New-taskObject -path "\microsoft\windows\defrag")
# Returns a collection of task objects representing each task in the defrag folder

# Get-Task -folder (New-taskObject -path "\") -name "Test idle trigger"
# returns a task object representing the Test idle trigger task in the root folder  

# New-Task -command "C:\Windows\System32\notepad.exe" -name "test idle trigger"
# Creates task to run notepad. task is stored in root and named test idle trigger

# New-Task -command "C:\Windows\System32\notepad.exe" -path "\hsg" `
#  -name "test idle trigger"

# Creates task to run notepad. task is named test idle trigger and 
# is stored in root\hsg folder.

# Remove-Task -folder (New-taskObject) -name "test idle trigger"
# Deletes a scheduled task named "test idle trigger" from the root folder

# Remove-Task -folder (New-taskObject -path "\hsg") -name "test idle trigger"
# Deletes a scheduled task named "test idle trigger" from the root\hsg folder

The first thing that we need to do is to create the function that will create an instance of the schedule.service object. The Get–ScheduleService function is the same function that we used in yesterday's script and therefore will not be discussed here. The code is seen here:

Function Get-ScheduleService
{
  New-Object -ComObject schedule.service
} #end Get-ScheduleService

We also need to create the Get–Tasks function, which will return a task object. This is the same function that we used yesterday and therefore will not be discussed. This code is seen here:

Function Get-Tasks($folder)
{ #returns task object
$folder.GetTasks(1)
} #end Get-Tasks

The Get-Task function is seen here and is the same function that we used yesterday:

Function Get-Task($folder,$name)
{ #returns a task object
 $folder.GetTask($name)
} #end Get-Tasks

We also need to use the New-TaskObject function to create a folder object for us to use. This is the same function we used just yesterday:

Function New-TaskObject($path)
{ #returns a taskfolder object
$taskObject = Get-ScheduleService
$taskObject.Connect()
if(-not $path) { $path = "\" }
$taskObject.GetFolder($path)
} #end New-TaskObject

Now we come to the New-Task function. This is a new function and we will need to spend some time examining it. Following the Function keyword and the name, we define eight input parameters. Because of the way that the script is written, not all eight of these input parameters are required to be supplied when the script is run. This code is shown here:

Function New-Task($path,$Name,$description,$author,$command,$user,$password,$sddl)
{

We create a few constants. To create a new constant, we use the New-Variable cmdlet while supplying a value and a name for the variable. The thing that makes the difference between a variable and a constant is the fact that a constant can neither be changed nor deleted. We did not need to use a constant here; we could in fact simply use a variable and assign a value to it. If we did not assign a new value to the variable, it would in effect be constant. The constant names are the same names that are defined in the Windows Software Development Kit (SDK). We did not need to use these names, but it will make the script easier to read and easier to maintain, particularly when we need to return to the SDK in order to identify additional options for the script. As a matter of fact, we do not even need to use a variable to hold the value. We could simply use literal values directly in the method call. The price for this efficiency is readability. The constants are shown here:

New-Variable -Name TASK_TRIGGER_IDLE -Value 6 -Option constant
  New-Variable -Name TASK_ACTION_EXEC -Value 0 -Option constant
  New-Variable -Name CREATE_OR_UPDATE -Value 6 -Option constant
  New-Variable -Name TASK_LOGON_NONE -Value 0 -Option constant

Because we do not wish to always be required to supply a user name and a password when running the script to create a new scheduled task, we assign the variables $user, $password, and $sddl to be equal to null. This is shown here:

$user=$password=$sddl=$null

It is time to obtain the TaskService object. To do this we call the Get–ScheduleService function and assign the resulting object to the $taskObject variable. We then call the Connect method:

$taskObject = Get-ScheduleService
  $taskObject.Connect()

Now we need to connect to the TaskFolder object. The first thing that we need to do is to verify that a value was supplied for the $path variable. If it was not, we connect to the root. We use the GetFolder method from the TaskService object and assign the resulting TaskFolder object to the $rootFolder variable. This is seen here:

if(-not $path) { $path = "\" }
  $rootFolder = $taskObject.GetFolder($path)

Next we call the NewTask method from the TaskService object and assign the NewTask object to the $taskdefinition variable. The reason we supply the $null value to the method call is because the flag that would normally be placed here is not defined in the API. We next need to obtain registration information and assign it to the $regInfo variable. This is shown here:

$taskdefinition = $taskObject.NewTask($null)
  $regInfo = $taskdefinition.RegistrationInfo

I hate it when I am required to type in a description for the task if it is not something that I intend to keep around for a very long time. If a description is not supplied when the function is called, a default value of “Created by script” is assigned to the $description variable. The value contained inside the $description variable is then assigned to the description property as shown here:

if(-not $description) { $description = "Created by script" }
  $regInfo.Description = $description

The same goes for the author, so if a value for $author is not assigned when the function is called, we read the username value from the system environment and assign the value to the author :

if(-not $author) { $author = $env:username }
  $regInfo.Author = $author

Now we get to the TaskSettings object. The TestSettings object allows us to configure the items seen here:

Image of the items configurable by the TestSettings object

 

To do this, we need to first retrieve the TestSettings object by querying the settings property from the TaskDefinition object and storing the results in the $settings variable. In our script we select two properties from the TaskDefinition object to work with. The first property, StartWhenAvailable, allows the scheduled task to begin after it becomes available. The second property, Hidden, allows the task to be visible. This is seen here:

$settings = $taskdefinition.Settings
  $settings.StartWhenAvailable = $true
  $settings.Hidden = $false

We need to create a Trigger object. The Trigger object is obtained by querying the triggers property from the TaskDefinition object. We store the returned object in the $triggers variable. Now we need to decide what kind of trigger we're going to use. This is where we can really begin to see some of the power of the Task Scheduler 2.0 API. In this example, we want the scheduled task to run only when the computer is not doing anything. To do this we will create an idle trigger. The $TASK_TRIGGER_IDLE constant is used to define the kind of trigger that we will use. We store the idle trigger in the $trigger variable. This is seen here:

$triggers = $taskdefinition.Triggers
  $trigger = $triggers.Create($TASK_TRIGGER_IDLE)

Now we need an action object. The action object is used to define the action that the scheduled task will take when the trigger fires. We use the actions property from the $taskdefinition object and call the create method to create the action object. The create method is given the $TASK_ACTION_EXEC constant to tell it what kind of action is to be created. We store the returned action object in the $action variable. We then use the path property of the action object and give it the path to the command that will be executed. This is stored in the $command variable. This code is seen here:

$action = $taskdefinition.Actions.Create($TASK_ACTION_EXEC)
  $action.Path = $command

It is time to actually register the scheduled task. This part is actually a little surprising; we use the folder object to give us access to the RegisterTaskDefinition method. This method has seven parameters. Each of the parameters for the method is supplied values via the variables and constants that we defined earlier. This makes it easy for us to understand what the task is supposed to do:

$rootFolder.RegisterTaskDefinition($Name,$taskdefinition,$CREATE_OR_UPDATE,
                                     $user,$password,$TASK_LOGON_NONE,$sddl)
} #end New-Task

We may want to delete a previously scheduled task. To do this we use the DeleteTask method from the folder object. We create a Remove-Task function to allow us to easily call this method. This is shown here:

Function Remove-Task($folder,$name)
{
$folder.DeleteTask($name,$null)
} #end Remove-Task

The entry point to the script is the section of the script that allows us to call the functions contained within the script. There are a number of things that we could do such as returning a collection of task objects that list each of the tasks within a specific folder. If we were to do this, the syntax might look something like this:

Get-Tasks -folder (New-taskObject -path "\microsoft\windows\defrag")

We may decide that we would like to look at a specific task. To do this we would call the Get-Task function, pass it the folder object that we obtain via the New-TaskObject function. We would need of course to specify the name of the task as seen here:

Get-Task -folder (New-TaskObject -path "\") -name "Test idle trigger"

If we want to create a new task, the only things which must be supplied are the command to execute and the name of the task. We could even create a function that would generate a random name for the task and allow us to skip the problem of making up names for one-off scheduled tasks, but we have not done so in this particular script. You could use the technique seen in this “Hey, Scripting Guy!” article to give you some ideas for creating random task names. The code to create the new task is shown here:

New-Task -command "C:\Windows\System32\notepad.exe" -path "\hsg" `
  -name "test idle trigger"

If you read the article yesterday and learned how to create your own folders, you probably want to create your scheduled tasks inside a new folder. To do so you use the New-Task function, and specify the command, the name, and the path parameters as seen here:

New-Task -command "C:\Windows\System32\notepad.exe" -path "\hsg" `  -name "test idle trigger"

If you need to delete a task, we specify a folder object that contains the task as well as the name of the task that is to be removed. We need to use the New-TaskObject function because the method will not accept a string as a path to the folder. It requires an actual folder object. Keep in mind that we must supply a folder object even if the task is stored in the root of the scheduled tasks folder. This code is seen here:

Remove-Task -folder (New-TaskObject) -name "test idle trigger"

If the task that you wish to remove is stored in a different folder, we use the New-Task object function and specify the path parameter to that function. We place this inside a set of parentheses and then supply that to the folder parameter of the Remove-Task function. We then specify the name of the task as we did earlier. This is shown here:

Remove-Task -folder (New-taskObject -path "\hsg") -name "test idle trigger"

This concludes our article detailing the process for creating scheduled tasks and deleting scheduled tasks. It also brings to a conclusion our Scheduled Tasks Week. We hope that you have enjoyed this series of articles and are somewhat intrigued by the new and exciting possibilities offered by the Scheduled Task 2.0 API. Join us tomorrow for Quick-Hits Friday. Until then, peace.

 

Ed Wilson and Craig Liebendorfer, Scripting Guys