Thursday, July 12, 2012

Add Description to Scheduled Task Using Powershell

You want to create a Scheduled Task?  From the Scheduled Task GUI, it's simple.

You want to do so from the Scheduled Task command line (schtasks.exe)?  A bit more involved, but possible.

You want to set the Description field in a Scheduled Task from the command line in Powershell?  It CAN be done, with some clever use of schtasks.exe command line options, and Powershell's ability to manipulate XML.

In this post, I'll show you how to set the Description field of a Scheduled Task.

You certainly do not have to use Powershell, but its xml manipulation commands make it easier.  For those of you who don't use Powershell, you'll just have to cobble something together for some of these steps.

Here's a summary:
Step 1. Set a variable to be the XML export file.
Step 2. Set a variable to be the task name.
Step 3. Set a variable to be the Description.
Step 4. Set a variable to be the executable and arguments.
Step 5. Create the scheduled task.
Step 6. Export the task to an XML file.
Step 7. Delete the task.
Step 8. Load the XML file into a variable.
Step 9. In the XML variable, add a Description field, and set the Description field to that variable.
Step 10. Write the XML variable to a file.
Step 11. Create a scheduled task, this time from the XML file.
Step 12. You're done!

Step 1. Set a variable to be the XML export file.
Let's call the variable "$strOutputXMLFile".
$strOutputXMLFile="c:\scheduled_task.xml"

Step 2. Set a variable to be the task name.
Let's call the variable "$strScheduledTaskName".
$strScheduledTaskName="New Scheduled Task"

Step 3. Set a variable to be the Description.
Let's call the variable "$strDescription".
$strDescription="Here is content for the Description field."

Step 4. Set a variable to be the executable and arguments.
Let's call the variable "$strExecuteableAndArguments".
$strExecuteableAndArguments="backup.exe /all" # - totally bogus command, but you get the idea

Step 5. Create the scheduled task.
schtasks.exe /rl limited /create /tn "$strScheduledTaskName" /tr "e:\ScheduledTaskUpdateTools\run-scheduled-processes.bat daily_10_min_import_processes" /ru DOMAINNAME\DOMAINUSERNAME /rp "$3cr3t_4a$$w0rd" /sc weekly /d sun /st 02:00 /sd 07/12/2012

Let me dissect the command above for you:
schtasks.exe: Scheduled Task executable
'/rl limited': run with limited priveges. This is the default value, and you can omit it if you wish.
Use '/rl highest' to run with highest privileges.  I'm not really sure what this means, but I think it is the equivalent to "run as administrator."
'/create': Make a new scheduled task.
'/tn "$strScheduledTaskName"': This will be the name of your new scheduled task.
The name must be unique. If you're automating this, you'll need to delete the existing task that matches this name. You'll see how to do this.
'/tr "e:\ScheduledTaskUpdateTools\run-scheduled-processes.bat daily_10_min_import_processes"': This will be the executable the task should run, and the parameters to pass to the executable.
'/ru DOMAINNAME\DOMAINUSERNAME': This will be the domain account to run the executable under.
'/rp "$3cr3t_4a$$w0rd"': This will be the account password that works for DOMAINNAME\DOMAINUSERNAME.
Just to make sure the task will run as it should, I use "runas /profile /user:DOMAINNAME\DOMAINUSERNAME cmd.exe", enter the account password, then in the new command window, "e:\ScheduledTaskUpdateTools\run-scheduled-processes.bat daily_10_min_import_processes".
'/sc weekly /d sun /st 02:00 /sd 07/12/2012': Run the task every Sunday at 2am, starting on July 12, 2012.
If you omit the '/sd' parameter, Task Scheduler will assume today's date.
You may need to adjust the date format for your locale.

Just to help you out, here are parameters for some other schedules I use:
'/sc daily /st 06:00 /ri 5 /du 0013:00': Daily, between 6am and 7pm, every 5 minutes. /ri 5 ("run interval") means "run every 5 minutes". /du 0013:00 (duration 13 hrs) means run the task for 13 hours, and 6am+13hrs=19 -> 7pm.
'/sc daily /st 04:00': Daily at 4am
'/sc daily /st 22:00': Daily at 10pm
'/sc monthly /mo third /d sun /st 03:00': On the third Sunday of every month at 3am

Step 6. Export the task to an XML file.
schtasks.exe /query /tn "$strScheduledTaskName" /xml > "$strOutputXMLFile"
'/query': Look at a task, but don't change it.
'/tn "$strScheduledTaskName"': Name of the task to be exported
'/xml > "$strOutputXMLFile"': output the task as XML into c:\scheduled_task.xml.
Note that '> "c:\scheduled_task.xml"' must be the last parameter.

If you're not using Powershell, review the "c:\scheduled_task.xml" file.
It should read something like this:

<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
  <RegistrationInfo>
    <Date>2012-07-12T09:58:09</Date>
    <Author>DOMAINNAME\DOMAINUSERNAME</Author>
  </RegistrationInfo>
  <Triggers>
    <CalendarTrigger>
      <StartBoundary>2012-07-15T04:00:00</StartBoundary>
      <Enabled>true</Enabled>
      <ScheduleByDay>
        <DaysInterval>1</DaysInterval>
      </ScheduleByDay>
    </CalendarTrigger>
  </Triggers>
  <Settings>
    <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
    <DisallowStartIfOnBatteries>true</DisallowStartIfOnBatteries>
    <StopIfGoingOnBatteries>true</StopIfGoingOnBatteries>
    <AllowHardTerminate>true</AllowHardTerminate>
    <StartWhenAvailable>false</StartWhenAvailable>
    <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
    <IdleSettings>
      <Duration>PT10M</Duration>
      <WaitTimeout>PT1H</WaitTimeout>
      <StopOnIdleEnd>true</StopOnIdleEnd>
      <RestartOnIdle>false</RestartOnIdle>
    </IdleSettings>
    <AllowStartOnDemand>true</AllowStartOnDemand>
    <Enabled>true</Enabled>
    <Hidden>false</Hidden>
    <RunOnlyIfIdle>false</RunOnlyIfIdle>
    <WakeToRun>false</WakeToRun>
    <ExecutionTimeLimit>PT72H</ExecutionTimeLimit>
    <Priority>7</Priority>
  </Settings>
  <Actions Context="Author">
    <Exec>
      <Command>x:\ScheduledTaskUpdateTools\run-scheduled-processes.bat</Command>
      <Arguments>daily_multihour_update_processes</Arguments>
    </Exec>
  </Actions>
  <Principals>
    <Principal id="Author">
      <UserId>DOMAINNAME\DOMAINUSERNAME</UserId>
      <LogonType>Password</LogonType>
      <RunLevel>LeastPrivilege</RunLevel>
    </Principal>
  </Principals>
</Task>

Step 7. Delete the task.
Why?  Because you will re-create the task soon. You can't have two Scheduled Tasks with the same name.
schtasks.exe /delete /tn "$strScheduledTaskName" /f
'/delete': delete the task
'/tn "New Scheduled Task"': Name of the task to be deleted
'/f': Do not confirm; delete the task with no warning message.

Step 8. Load the XML file into a variable.
If you're using Powershell, use this command:
[xml] $xmlScheduledTask = get-content "$strOutputXMLFile"
'[xml] $xmlScheduledTask' typecasts the text content from "c:\scheduled_task.xml" as an xml variable.

If you're not using Powershell, and your language cannot create an xml variable, load the file as a string.

Step 9. In the XML variable, add a Description field, and set the Description variable to that field.
If you're using Powershell, use these commands:
# - the "NamespaceURI" part prevents adding the "xmlns" attribute to the "Description" tag
# - leaving the "xmlns" attribute will let the "create scheduled task" command below fail silently
$xmlDescription=$xmlScheduledTask.CreateElement("Description", $xmlScheduledTask.DocumentElement.NamespaceURI)
$xmlDescription.set_InnerXML("$strDescription")
$xmlScheduledTask.Task.RegistrationInfo.AppendChild($xmlDescription) | out-null

If you're not using Powershell, and your language cannot manipulate xml variables, you'll most likely need to add a <Description> tag to the XML string using string functions. To see where to add the <Description> tag, create a new scheduled task with a Description using the GUI, then use the '/query /xml' parameters of the schdtasks.exe command to output the text, then review that XML.  XML is very particular about hierarchy and tag names.

For example, in my case, the updated XML file (with the new Description tag) read something like this:

<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
  <RegistrationInfo>
    <Date>2012-07-12T09:58:09</Date>
    <Author>DOMAINNAME\DOMAINUSERNAME</Author>
    <Description>Here is content for the Description field.</Description>
  </RegistrationInfo>
  <Triggers>
...

Step 10. Write the XML variable to a file.
If you're using Powershell, use this command:
$xmlScheduledTask.save("$strOutputXMLFile")

Step 11. Create a scheduled task, this time from the XML file.
schtasks.exe /create /tn "$strScheduledTaskName" /ru DOMAINNAME\DOMAINUSERNAME /rp "$3cr3t_4a$$w0rd" /xml "$strOutputXMLFile"

Step 12. You're done!
Your scheduled task now has a Description!

Here is code I use, along with some extras. Please note that I had to change some parts of it for security reasons, but the essentials are there.

# - I use this function to run executables in my powershell scripts.
# - I like it because I have the option of waiting for the executable
# - to finish or not.  Use "-waitforexit $true" to wait until the
# - executable is finished.
function Start-Proc  {
    param (
        [string]$exe = $(Throw "An executable must be specified"),
        [string]$arguments="",
        [switch]$hidden=$false,
        [switch]$waitforexit
    )

    # Build Startinfo and set options according to parameters

    $startinfo = new-object System.Diagnostics.ProcessStartInfo
    $startinfo.FileName = $exe

    $startinfo.Arguments = $arguments

    if ($hidden -eq $true) {
        $startinfo.WindowStyle = "Hidden"
        $startinfo.CreateNoWindow = $true
    }

    $process = [System.Diagnostics.Process]::Start($startinfo)

    if ($waitforexit) { $process.WaitForExit() }
}

$strScheduledTaskName="New Scheduled Task"
$strEnabled="true" # - set to "false" to create, but disable this scheduled task. helpful if you want to create the task now, but you want to enable it manually later.
$strDescription="This is the description for this task."
$strOutputXMLFile="c:\scheduled_task.xml"
$strScheduledTaskAccount="DOMAINNAME\DOMAINUSERNAME"
$strScheduledTaskPassword="$3cr3t_4a$$w0rd"
$strExecuteableAndArguments="backup.exe /all" # - totally bogus command, but you get the idea
$strRLValue="limited" # - use "limited" or "highest"
$strScheduledTaskArgs="/sc daily /st 22:00 /sd {MM/DD/YYYY}" # - daily at 10pm, starting today
# $strScheduledTaskArgs="/sc weekly /d sun /st 01:30 /sd {MM/DD/YYYY_PLUS_7_DAYS}" # - Every Sunday at 1:30am, starting 7 days from today
# $strScheduledTaskArgs="/sc monthly /mo third /d sun /st 03:00 /sd {MM/DD/YYYY_PLUS_3_DAYS}" # - On the third Sunday of every month at 3am, starting 3 days from today

$strHH=(get-date)
$strHH=(get-date $strHH -uformat "%H")

$strHHPlusOne=(get-date) + (new-timespan -hours 1)
$strHHPlusOne=(get-date $strHHPlusOne -uformat "%H")

$strMMDDYYYY=(get-date)
$strMMDDYYYY=(get-date $strMMDDYYYY -uformat "%m/%d/%Y")

$strMMDDYYYYPlus3Days=(get-date) + (new-timespan -days 3)
$strMMDDYYYYPlus3Days=(get-date $strMMDDYYYYPlus3Days -uformat "%m/%d/%Y")

$strMMDDYYYYPlus7Days=(get-date) + (new-timespan -days 7)
$strMMDDYYYYPlus7Days=(get-date $strMMDDYYYYPlus7Days -uformat "%m/%d/%Y")

$strHHMMMinusOneMinute=(get-date) - (new-timespan -minutes 1)
$strHHMMMinusOneMinute=(get-date $strHHMMMinusOneMinute -uformat "%H:%M")

$strScheduledTaskArgs=$strScheduledTaskArgs.replace("{HH}", $strHH)

$strScheduledTaskArgs=$strScheduledTaskArgs.replace("{HH_PLUS_ONE}", $strHHPlusOne)

$strScheduledTaskArgs=$strScheduledTaskArgs.replace("{HH:MM_MINUS_ONE_MINUTE}", $strHHMMMinusOneMinute)

$strScheduledTaskArgs=$strScheduledTaskArgs.replace("{MM/DD/YYYY}", $strMMDDYYYY)

$strScheduledTaskArgs=$strScheduledTaskArgs.replace("{MM/DD/YYYY_PLUS_3_DAYS}", $strMMDDYYYYPlus3Days)

$strScheduledTaskArgs=$strScheduledTaskArgs.replace("{MM/DD/YYYY_PLUS_7_DAYS}", $strMMDDYYYYPlus7Days)

# - create the scheduled task only so we can fetch the xml later
$strArguments="/c schtasks.exe /rl $strRLValue /create /tn `"$strScheduledTaskName`" /tr `"$strExecuteableAndArguments`" /ru $strScheduledTaskAccount /rp `"$strScheduledTaskPassword`" $strScheduledTaskArgs"
$strModifiedArguments=$strArguments
$strModifiedArguments=$strModifiedArguments.replace($strScheduledTaskPassword, "*****") # - output $strModifiedArguments to show the command line without the password
start-proc "cmd.exe" -arguments $strArguments -waitforexit

# - create the xml file
$strArguments="/c schtasks.exe /query /tn `"$strScheduledTaskName`" /xml > `"$strOutputXMLFile`""
start-proc "cmd.exe" -arguments $strArguments -waitforexit

# - delete the task so we can re-create it using the xml file
$strArguments="/c schtasks.exe /delete /tn `"$strScheduledTaskName`" /f"
start-proc "cmd.exe" -arguments $strArguments -waitforexit

# - fetch the xml file
[xml] $xmlScheduledTask = get-content "$strOutputXMLFile"

# - add a description element to the xml object
# - the "NamespaceURI" part prevents adding the "xmlns" attribute to the "Description" tag
# - leaving the "xmlns" attribute will let the "create scheduled task" command below fail silently
$xmlDescription=$xmlScheduledTask.CreateElement("Description", $xmlScheduledTask.DocumentElement.NamespaceURI)
$xmlDescription.set_InnerXML("$strDescription")
$xmlScheduledTask.Task.RegistrationInfo.AppendChild($xmlDescription) | out-null
$xmlScheduledTask.Task.Settings.Enabled=$strEnabled

# - write the xml file
$xmlScheduledTask.save($strOutputXMLFile)

$strArguments="/c schtasks.exe /create /tn `"$strScheduledTaskName`" /ru $strScheduledTaskAccount /rp `"$strScheduledTaskPassword`" /xml `"$strOutputXMLFile`""
$strModifiedArguments=$strArguments
$strModifiedArguments=$strModifiedArguments.replace($strScheduledTaskPassword, "*****")
start-proc "cmd.exe" -arguments $strArguments -waitforexit