В операционных системах семейства Windows NT изначально существовал встроенный планировщик задач. В процессе развития запускаемый из командной строки планировщик At в Windows NT превратился в более мощную службу Task Scheduler в Windows 2000. В Windows Server 2008 и Windows Vista служба Task Scheduler была переработана и дополнена новыми функциями.

В Windows 2000 не существовало объекта сценария или интерфейса командной строки для доступа к службе Task Scheduler, за исключением неудобной для использования утилиты командной строки Jt.exe из пакета ресурсов Microsoft Windows NT Server Resource Kit (но не в операционной системе). В Windows XP ситуация изменилась благодаря утилите Schtasks.

.

Проблема Schtasks

Как отмечалось выше, в состав XP и более новых версий входит утилита Schtasks, которая предоставляет интерфейс командной строки для службы Task Scheduler. При использовании команды Schtasks с параметром /query выводится список запланированных на компьютере задач. Например, команда

schtasks/query/s server1/fo CSV

выводит запланированные задачи на компьютере с именем server1 с разделением запятыми (CSV), пригодном для импорта в электронную таблицу или базу данных. Команда Schtasks/query прекрасно работает в XP и Windows 2003, но при использовании в более поздних версиях возникают затруднения.

Vista, Server 2008 и более новые версии поддерживают папки задач, и, к сожалению, команда Schtasks выводит отдельную строку заголовка CSV для каждой из них, даже если папка не содержит никаких задач. На приведенном экране показан образец данных CSV, импортированных из команды Schtasks/query/fo CSV, с выделенными повторяющимися заголовками CSV. Нетрудно удалить лишние строки из вывода одного компьютера, но задача усложняется, если необходимо подготовить отчет о запланированных задачах на многих компьютерах.

 

Повторяющиеся строки заголовков в выводе команды Schtasks
Экран. Повторяющиеся строки заголовков в выводе команды Schtasks

У других форматов вывода Schtasks свои проблемы. Формат List (/fo List) выводит список с разделением символами новой строки, но его трудно анализировать. Формат Table (/fo Table) поддерживает параметр /nh (no headers — без заголовков), но в выводе присутствуют пустые строки, и он разделен по папкам задач, что также затрудняет анализ. Чтобы сформировать пригодный для использования отчет в формате XML (/XML), требуется подготовить программный код синтаксического анализа XML.

Вместо того чтобы заниматься синтаксическим разбором вывода команды Schtasks, я решил поискать более эффективный способ. Так, мы можем воспользоваться объектами для создания сценариев (msdn.microsoft.com/en-us/library/aa383607.aspx), чтобы получить информацию о запланированной задаче. Я решил подготовить сценарий PowerShell, Get-ScheduledTask.ps1, в котором эти объекты применяются для вывода запланированных задач на одном или нескольких компьютерах.

Знакомимся с Get-ScheduledTask.ps1

Для Get-ScheduledTask.ps1 требуется Vista, Server 2008 или более новая версия, так как объект Task Service недоступен в предыдущих версиях операционной системы. Необходимо также запускать сценарий из сеанса PowerShell с расширенными правами. Для этого щелкните правой кнопкой мыши на пиктограмме PowerShell и выберите пункт Run as administrator. Синтаксис сценария следующий:

Get-ScheduledTask
[[-TaskName] ]
[[-computerName] ]
[-Subfolders]
[-connectioncredential ]

Параметр -TaskName указывает имя одной или нескольких запланированных задач. Допускаются универсальные символы (* и? ). Также можно задать список имен задач, разделенных запятыми, или переменную, содержащую массив. Если опустить этот параметр, значение по умолчанию будет «*» (то есть выводятся все задачи). Можно пропустить имя параметра -TaskName, если этот аргумент первый в командной строке.

Параметр -ComputerName задает имя одного или нескольких компьютеров. С этим параметром нельзя употреблять универсальные символы, но можно указать текстовый файл, содержащий список имен компьютеров (по одному имени на строке). Возможен и конвейерный вывод. Если не указать имя компьютера, то по умолчанию выбирается имя текущего. Имя параметра -ComputerName можно опустить, если его аргумент — второй в командной строке.

Параметр -Subfolders указывает, распознаются ли в сценарии вложенные папки задач. Без этого параметра сценарий работает только с задачами в корневой папке задач («\»). В графическом интерфейсе Task Scheduler корневая папка задач — верхняя папка в иерархии. Если удаленный компьютер не поддерживает папки задач, этот параметр игнорируется.

Для параметра -ConnectionCredential требуется объект PSCredential (созданный с использованием команды Get-Credential), который содержит учетные данные для подключения к объекту TaskService на указанных компьютерах. Следует отметить, что это потенциально небезопасная операция. Сценарий должен получить экземпляр пароля объекта PSCredential в виде простого текста, так как метод Connect объекта TaskService не поддерживает шифрование учетных данных.

Get-ScheduledTask.ps1 выводит пользовательские объекты (PSObjects) для каждой задачи. В таблице 1 показаны свойства объекта по умолчанию, выводимые сценарием. Если набор свойств по умолчанию содержит слишком много информации, можно использовать команду Select-Object, чтобы выбрать только нужные свойства. Если свойство не поддерживается (например, его не существует в старой операционной системе), оно будет пустым. Кроме того, если ActionType имеет значение, отличное от Execute, свойство Action будет пустым.

 

Таблица 1. Свойства по умолчанию выходного объекта в Get-ScheduledTask.ps1
Свойства по умолчанию выходного объекта в Get-ScheduledTask.ps1

В таблице 2 показаны образцы команд для запуска Get-ScheduledTask.ps1. Обратите внимание, что, хотя в таблице команды переносятся на другую строку, в консоли PowerShell их следует вводить одной строкой.

 

Таблица 2. Образцы команд для запуска Get-ScheduledTask.ps1
Образцы команд для запуска Get-ScheduledTask.ps1

Как функционирует сценарий

Сценарий Get-ScheduledTask.ps1 предназначен для функционирования, подобно команде, благодаря возможности конвейерного ввода вместо параметра -ComputerName. Для этого в сценарии используются блоки begin и process. В блоке begin определяется ряд глобальных в масштабах сценария переменных, затем предпринимается попытка создать экземпляр объекта TaskService, как показано в листинге 1. Если создать объект не удается, выдается сообщение об ошибке и работа сценария завершается. После создания объекта TaskService определяются все вспомогательные функции. Рабочая функция сценария — get-scheduledtask2, она будет описана ниже.

В блоке process каждое имя компьютера передается функции get-scheduledtask2. Переменная $PIPELINEINPUT, определенная в верхней части блока begin, активирует блок process, чтобы определить, следует ли использовать параметр ComputerName или извлекать из конвейера входные данные.

Функция get-scheduledtask2

Как отмечалось выше, get-scheduledtask2 — рабочая функция сценария. Она исполняется в блоке process для каждого имени компьютера. Если используется параметр -ConnectionCredential, функция извлекает имя домена, имя пользователя и текстовую копию пароля из объекта PSCredential для передачи методу Connect объекта TaskService. Если метод Connect заканчивается неудачей, функция выводит предупреждение с использованием команды Write-Warning, и ее выполнение завершается.

После этого функция get-scheduledtask2 проверяет свойство HighestVersion объекта TaskService, чтобы определить версию службы Task Scheduler. На основании номера версии службы определяется, поддерживаются ли папки задач и значение свойства Elevated выходного объекта (см. таблицу 1).

Затем с помощью метода GetFolder объекта TaskService извлекается корневая папка задач и применяется функция get-task для получения списка всех задач на компьютере. Далее функция get-scheduledtask2 сравнивает имя задачи с именем в параметре -TaskName. Если совпадения нет, функция переходит к следующей задаче. В случае совпадения функция присваивает переменной $actionCount нулевое значение и обрабатывает коллекцию Actions задачи.

Для каждого действия задачи в функции get-scheduledtask2 с помощью команды New-Object создается пустой пользовательский объект (PSObject) и свойства задачи добавляются в PSObject. После того как все свойства добавлены, функция выводит объект.

Определяем задачи, запланированные на компьютере

С помощью утилиты Schtasks компании Microsoft можно составить отчет о запланированных задачах, но ее форматы вывода ограничены, трудны для анализа и плохо масштабируются. Эти ограничения устранены в сценарии Get-ScheduledTask.ps1. Не составляет труда подготовить отчет о запланированных задачах для любого числа компьютеров. Сценарий приведен в листинге 2.

Листинг 1. Исходный текст для создания объекта TaskService

try {
  $TaskService = new-object -comobject "Schedule.Service"
}
catch [System.Management.Automation.PSArgumentException] {
  throw $_
}

Листинг 2. Исходный текст листинга Get-ScheduledTask.ps1

# Get-ScheduledTask.ps1
# Written by Bill Stewart (bstewart@iname.com)
#requires -version 2
<#
.SYNOPSIS
Outputs scheduled task information.
.DESCRIPTION
Outputs scheduled task information. Requires Windows Vista/Server 2008 or later.
.PARAMETER TaskName
The name of a scheduled task to output. Wildcards are supported. The default value is * (i.e., output all tasks).
.PARAMETER ComputerName
A computer or list of computers on which to output scheduled tasks.
.PARAMETER Subfolders
Specifies whether to support task subfolders (Windows Vista/Server 2008 or later only).
.PARAMETER ConnectionCredential
The connection to the task scheduler service will be made using these credentials. If you don't specify this parameter, the currently logged on user's credentials are assumed. This parameter only supports connecting to the scheduler service on remote computers running Windows Vista/Server 2008 or later.
.OUTPUTS
PSObjects containing information about scheduled tasks.
.EXAMPLE
PS C:\> Get-ScheduledTask
This command outputs the scheduled tasks in the root tasks folder on the current computer.
.EXAMPLE
PS C:\> Get-ScheduledTask -Subfolders
This command outputs all scheduled tasks on the current computer, including those in subfolders.
.EXAMPLE
PS C:\> Get-ScheduledTask -TaskName \Microsoft\* -Subfolders
This command outputs all scheduled tasks in the \Microsoft task subfolder and its subfolders on the current computer.
.EXAMPLE
PS C:\> Get-ScheduledTask -ComputerName SERVER1
This command outputs scheduled tasks in the root tasks folder on the computer SERVER1.
.EXAMPLE
PS C:\> Get-ScheduledTask -ComputerName SERVER1 -ConnectionCredential (Get-Credential) | Export-CSV Tasks.csv -NoTypeInformation
This command prompts for credentials to connect to SERVER1 and exports the scheduled tasks in the computer's root tasks folder to the file Tasks.csv.
.EXAMPLE
PS C:\> Get-Content Computers.txt | Get-ScheduledTask
This command outputs all scheduled tasks for each computer listed in the file Computers.txt.
#>
[CmdletBinding()]
param(
  [parameter(Position=0)] [String[]] $TaskName="*",
  [parameter(Position=1,ValueFromPipeline=$TRUE)] [String[]]
  $ComputerName=$ENV:COMPUTERNAME,
  [switch] $Subfolders,
  [System.Management.Automation.PSCredential] $ConnectionCredential
)
begin {
  $PIPELINEINPUT = (-not $PSBOUNDPARAMETERS.ContainsKey("ComputerName")) -and (-not $ComputerName)
  $MIN_SCHEDULER_VERSION = "1.2"
  $TASK_STATE = @{0 = "Unknown"; 1 = "Disabled"; 2 = "Queued";
  3 = "Ready"; 4 = "Running"}
  $ACTION_TYPE = @{0 = "Execute"; 5 = "COMhandler"; 6 = "Email";
  7 = "ShowMessage"}
  # Try to create the TaskService object on the local computer; throw an error on failure
  try {
    $TaskService = new-object -comobject "Schedule.Service"
  }
  catch [System.Management.Automation.PSArgumentException] {
    throw $_
  }
  # Returns the specified PSCredential object's password as a plain-text string
  function get-plaintextpwd($credential) {
    $credential.GetNetworkCredential().Password
  }
  # Returns a version number as a string (x.y); e.g. 65537 (10001 hex) returns "1.1"
  function convertto-versionstr([Int] $version) {
    $major = [Math]::Truncate($version / [Math]::Pow(2, 0x10)) -band 0xFFFF
    $minor = $version -band 0xFFFF
    "$($major).$($minor)"
  }
  # Returns a string "x.y" as a version number; e.g., "1.3" returns
  65539 (10003 hex)
  function convertto-versionint([String] $version) {
    $parts = $version.Split(".")
    $major = [Int] $parts[0] * [Math]::Pow(2, 0x10)
    $major -bor [Int] $parts[1]
  }
  # Returns a list of all tasks starting at the specified task folder
  function get-task($taskFolder) {
    $tasks = $taskFolder.GetTasks(0)
    $tasks | foreach-object { $_ }
    if ($SubFolders) {
      try {
        $taskFolders = $taskFolder.GetFolders(0)
        $taskFolders | foreach-object { get-task $_ $TRUE }
      }
      catch [System.Management.Automation.MethodInvocationException] {
      }
    }
  }
# Returns a date if greater than 12/30/1899 00:00; otherwise,
  returns nothing
  function get-OLEdate($date) {
    if ($date -gt [DateTime] "12/30/1899") { $date }
  }

  function get-scheduledtask2($computerName) {
    # Assume $NULL for the schedule service connection parameters
   unless -ConnectionCredential used
    $userName = $domainName = $connectPwd = $NULL
    if ($ConnectionCredential) {
      # Get user name, domain name, and plain-text copy of password
      from PSCredential object
      $userName = $ConnectionCredential.UserName.Split("\")[1]
      $domainName = $ConnectionCredential.UserName.Split("\")[0]
     $connectPwd = get-plaintextpwd $ConnectionCredential
    }
    try {
      $TaskService.Connect($ComputerName, $userName, $domainName,
      $connectPwd)
    }
    catch [System.Management.Automation.MethodInvocationException] {
      write-warning "$computerName - $_"
      return
    }
    $serviceVersion = convertto-versionstr $TaskService.HighestVersion
    $vistaOrNewer = (convertto-versionint $serviceVersion) -ge
    (convertto-versionint $MIN_SCHEDULER_VERSION)
    $rootFolder = $TaskService.GetFolder("\")
    $taskList = get-task $rootFolder
    if (-not $taskList) { return }
    foreach ($task in $taskList) {
      foreach ($name in $TaskName) {
        # Assume root tasks folder (\) if task folders supported
        if ($vistaOrNewer) {
          if (-not $name.Contains("\")) { $name = "\$name" }
        }
        if ($task.Path -notlike $name) { continue }
        $taskDefinition = $task.Definition
        $actionCount = 0
        foreach ($action in $taskDefinition.Actions) {
          $actionCount += 1
          $output = new-object PSObject
          # PROPERTY: ComputerName
          $output | add-member NoteProperty ComputerName
          $computerName
          # PROPERTY: ServiceVersion
          $output | add-member NoteProperty ServiceVersion
           $serviceVersion
          # PROPERTY: TaskName
          if ($vistaOrNewer) {
            $output | add-member NoteProperty TaskName $task.Path
          } else {
            $output | add-member NoteProperty TaskName $task.Name
          }
          #PROPERTY: Enabled
          $output | add-member NoteProperty Enabled ([Boolean]
          $task.Enabled)
          # PROPERTY: ActionNumber
          $output | add-member NoteProperty ActionNumber $actionCount
          # PROPERTIES: ActionType and Action
          # Old platforms return null for the Type property
          if ((-not $action.Type) -or ($action.Type -eq 0)) {
            $output | add-member NoteProperty ActionType
            $ACTION_TYPE[0]
            $output | add-member NoteProperty Action "$($action.Path)
            $($action.Arguments)"
          } else {
            $output | add-member NoteProperty ActionType
            $ACTION_TYPE[$action.Type]
            $output | add-member NoteProperty Action $NULL
          }
          # PROPERTY: LastRunTime
          $output | add-member NoteProperty LastRunTime (get-OLEdate
          $task.LastRunTime)
          # PROPERTY: LastResult
          if ($task.LastTaskResult) {
            # If negative, convert to DWORD (UInt32)
            if ($task.LastTaskResult -lt 0) {
              $lastTaskResult = «0x{0:X}» -f [UInt32] ($task.LastTaskResult +
              [Math]::Pow(2, 32))
            } else {
              $lastTaskResult = "0x{0:X}" -f $task.LastTaskResult
            }
          }
          $output | add-member NoteProperty LastResult $lastTaskResult
          # PROPERTY: NextRunTime
          $output | add-member NoteProperty NextRunTime
          (get-OLEdate $task.NextRunTime)
          # PROPERTY: State
          if ($task.State) {
            $taskState = $TASK_STATE[$task.State]
          }
          $output | add-member NoteProperty State $taskState
          $regInfo = $taskDefinition.RegistrationInfo
          # PROPERTY: Author
          $output | add-member NoteProperty Author $regInfo.Author
          # The RegistrationInfo object's Date property, if set, is a string
          if ($regInfo.Date) {
            $creationDate = [DateTime]::Parse($regInfo.Date)
          }
          $output | add-member NoteProperty Created $creationDate
          # PROPERTY: RunAs
          $principal = $taskDefinition.Principal
          $output | add-member NoteProperty RunAs $principal.UserId
          # PROPERTY: Elevated
          if ($vistaOrNewer) {
            if ($principal.RunLevel -eq 1) { $elevated = $TRUE } else {
           $elevated = $FALSE }
          }
          $output | add-member NoteProperty Elevated $elevated
          # Output the object
          $output
        }
      }
    }
  }
}

process {
  if ($PIPELINEINPUT) {
    get-scheduledtask2 $_
  }
  else {
    $ComputerName | foreach-object {
      get-scheduledtask2 $_
    }
  }
}

Билл Стюарт (bill.stewart@frenchmortuary.com) — системный и сетевой администратор в компании French Mortuary, Нью-Мехико