Public/Set-AtmdScheduledTask.ps1

function Set-AtmdScheduledTask {
    <#
    .SYNOPSIS
        Создает задачу в Планировщике заданий Windows.
    .DESCRIPTION
        Создает задачу на основании представленных параметрв в виде объектов / хеш таблиц (см. пример).
    .PARAMETER BaseParams
        Объект, содержащий имя, путь и описание задачи.
    .PARAMETER Actions
        Объект, содержащий массив выполняемых действий.
    .PARAMETER Triggers
        Объект, содержащий массив триггеров задачи.
    .PARAMETER Settings
        Объект, содержащий перечень параметров задачи (см. пример).
    .PARAMETER Security
        Объект, содержащий информацию об имени пользователя и пароле для запуска задачи, если требуется запускать задачу от имени другого пользователя.
    .PARAMETER Force
        Set-AtmdScheduledTask создает задачу, если она не существует. Чтобы пересоздать существующую, необходимо указать параметр -Force.
    .PARAMETER PassThru
        Используйте параметр -PassThru для того, чтобы функция вернула CIMInstance созданной задачи.
 
    .EXAMPLE
        $BaseParams = @{
            name = "Имя задачи"
            taskPath = "\ATMD User Tasks\ATMD-ilichev\"
            description = "Описание задачи"
        }
 
        $Action = @{
            commandToExecute = "C:\Tools\BGInfo\Bginfo.exe"
            workingDirectory = "C:\Tools"
        }
 
        $Trigger = @{
            periodicity = "Daily"
            relativeAt = "10"
            relativeEndBoundary = "15"
            repetition = @{
                duration = "P35D"
                interval = "P33D"
            }
        }
 
        $Settings = @{
            allowDemandStart = "true"
            allowHardTerminate = "false"
            deleteExpiredTaskAfter = "PT0S"
            disallowStartIfOnBatteries = "true"
            enabled = "true"
            executionTimeLimit = "PT1H30M"
            hidden = "false"
            multipleInstances = "IgnoreNew"
            priority = "100"
            restartCount = "0"
            restartInterval = "PT5M"
            startWhenAvailable = "true"
        }
 
        Import-Module ArielAuxFn
 
        Set-AtmdScheduledTask -BaseParams $BaseParams -Actions $Action -Triggers $Trigger -Settings $Settings -Force -PassThru -Verbose
 
        TaskPath TaskName State
        -------- -------- -----
        \ATMD User Tasks\ATMD-ilichev\ Имя задачи Ready
    .INPUTS
        Функция получает 5 объектов, описыавающих основные параметры создаваемой задачи.
    .OUTPUTS
        Взвращает CIMInstance, получаемую в результате вызова Get-ScheduledTask
    .NOTES
        Цель написания этой функции - возможность быстро создавать задачи на основании их описания, которое будте храниться в JSON-файле.
        Фактически, данная функция просто собирает вместе несколько командлетов Powershell, необходимых для создания задачи и позволяет менять параметры задаче на основании
        конфигурационного файла, не меняя при этом формат вызова командлетов.
    #>

    [CmdletBinding()]
    [OutputType([System.Object])]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $false, Position = 0)]
        [System.Object]
        $BaseParams,

        [Parameter(Mandatory = $true, ValueFromPipeline = $false, Position = 1)]
        [System.Object]
        $Actions,

        [Parameter(Mandatory = $true, ValueFromPipeline = $false, Position = 2)]
        [System.Object]
        $Triggers,

        [Parameter(Mandatory = $false, ValueFromPipeline = $false, Position = 3)]
        [System.Object]
        $Settings,

        [Parameter(Mandatory = $false, ValueFromPipeline = $false, Position = 4)]
        [System.Object]
        $Security,

        [Parameter(Mandatory = $false, ValueFromPipeline = $false, Position = 5)]
        [switch]
        $Force = $false,

        [Parameter(Mandatory = $false, ValueFromPipeline = $false, Position = 6)]
        [switch]
        $PassThru = $false

    )

    begin {
        # Это волшебные цифры. Надо просто принят...
        $MagicKey = (4, 8, 15, 16, 23, 42, 4, 8, 15, 16, 23, 42, 4, 8, 15, 16, 23, 42, 4, 8, 15, 16, 23, 42)
        if (-not($BaseParams.Description)) {
            $BaseParams.Description = 'При создании задачи использовался PSC.'
        }
    }

    process {
        try {
            if ($IsLinux) {
                throw [System.Configuration.ConfigurationException]::New('This operation system does not supported.')
            }

            Write-Verbose -Message "<Set-AtmdScheduledTask> Проверяем наличие задачи $($BaseParams.Name) в Планировщике заданий."
            $ScheduledTask = Get-ScheduledTask -TaskName $BaseParams.Name -ErrorAction SilentlyContinue
            # Если задача уже есть, но указан параметр -Force - удаляем задачу.
            if ($ScheduledTask -and $Force) {
                Write-Verbose -Message "<Set-AtmdScheduledTask> Удаляем задачу $($BaseParams.Name), так как указан параметр -Force."
                Unregister-ScheduledTask -InputObject $ScheduledTask -Confirm:$false
                Start-Sleep -Seconds 2
                $ScheduledTask = $null
            }
            # Задача не найдена или удалена. Создаем новую.
            if (-not($ScheduledTask)) {

                #region Определяем "Действия" - запуск програмы с аргумнтами или без.
                Write-Verbose -Message '<Set-AtmdScheduledTask> Определяем выполняемые действия (см. вкладку "Действия").'
                $TaskActions = @()
                foreach ($Action in $Actions) {
                    $TaskAction = New-ScheduledTaskAction -Execute $Action.commandToExecute
                    if ($Action.argument) {
                        $TaskAction.Arguments = $Action.argument
                    }
                    if ($Action.workingDirectory) {
                        $TaskAction.WorkingDirectory = $Action.workingDirectory
                    }
                    $TaskActions += $TaskAction
                }
                #endregion Определяем "Действия" - запуск програмы с аргумнтами или без.

                #region Определяем триггеры запуска задачи.
                Write-Verbose -Message '<Set-AtmdScheduledTask> Определяем триггеры запуска (см. вкладку "Триггеры").'
                $TaskTriggers = @()
                foreach ($Trigger in $Triggers) {
                    # Для триггеров поддерживается относительно время запуска.
                    # Это значит, что в конфигурационном файле может быть указано как точно время запуска, так и количество минут тносительно текущего времени.
                    if ($Trigger.relativeAt) {
                        $StartTime = Get-Date
                        $StartTime = $StartTime.AddSeconds($([System.Xml.XmlConvert]::ToTimeSpan($Trigger.relativeAt)).TotalSeconds)
                    }
                    elseif ($Trigger.At) {
                        $StartTime = Get-Date $Trigger.At
                    }

                    # Создавать задачу, время запуска которой в прошлом - дурная затея.
                    # Поэтому, если прозевали время запуска сегодня - запустим завтра.
                    if (($StartTime) -and ($StartTime -lt (Get-Date))) {
                        $StartTime = $StartTime.AddDays(1)
                    }

                    switch ($Trigger.Periodicity) {
                        # TimeTrigger object
                        'Once' {
                            $TaskTrigger = New-ScheduledTaskTrigger -Once -At $StartTime
                            break
                        }
                        # DailyTrigger object
                        'Daily' {
                            $TaskTrigger = New-ScheduledTaskTrigger -Daily -At $StartTime
                            break
                        }
                        # WeeklyTrigger object
                        'Weekly' {
                            $TaskTrigger = New-ScheduledTaskTrigger -Weekly -WeeksInterval $Trigger.WeeksInterval -DaysOfWeek $Trigger.DaysOfWeek -At $StartTime
                            break
                        }
                        # BootTrigger object
                        'AtStartup' {
                            $TaskTrigger = New-ScheduledTaskTrigger -AtStartup
                            break
                        }
                        # - RegistrationTrigger object
                        'AtRegistration' {
                            $CIMCLass = Get-CimClass -ClassName 'MSFT_TaskRegistrationTrigger' -Namespace 'Root/Microsoft/Windows/TaskScheduler'
                            $TaskTrigger = New-CimInstance -CimClass $CIMCLass -ClientOnly
                            $TaskTrigger.Enabled = $true
                            break
                        }
                        ## TODO Реализовать тригер "AtLogOn"
                        # # LogonTrigger object
                        # 'AtLogOn' {
                        # break
                        # }
                        # Типы триггеров. которые существуют, но не реализованы тут:
                        # - EventTrigger object
                        # - IdleTrigger object
                        # - MonthlyDOWTrigger object
                        # - MonthlyTrigger object
                        # - SessionStateChangeTrigger object
                        Default {
                            throw 'Данный тип триггера ещё не поддерживается скриптом.'
                        }
                    }
                    # Срок действия (EndBoundary), как и время запуска, может быть указан в минутах относительно времени запуска.
                    if ($Trigger.relativeEndBoundary) {
                        $EndBoundary = $StartTime.AddSeconds($([System.Xml.XmlConvert]::ToTimeSpan($Trigger.relativeEndBoundary)).TotalSeconds)
                    }
                    elseif ($Trigger.EndBoundary) {
                        $EndBoundary = Get-Date $Trigger.EndBoundary
                    }
                    if ($EndBoundary) {
                        # Срок действия не должен истекать раньше первого запуска. Если облажались с этим - срок действия будте 10 минут.
                        if ($StartTime -ge $EndBoundary) {
                            $EndBoundary = $StartTime.AddMinutes(10)
                        }
                        Write-Verbose -Message '<Set-AtmdScheduledTask> Для триггера запуска указан срок действия.'
                        # Параметр '-Formar s' небоходим, чтобы формат даты "попал" в формат хранения EndBoundary.
                        $TaskTrigger.EndBoundary = Get-Date -Date $EndBoundary -Format s
                    }

                    # Параметры порторения (Галочка "Повторять задачу каждые:") - интервал и продолжительность.
                    if ($Trigger.repetition.interval) {
                        Write-Verbose -Message "<Set-AtmdScheduledTask> Для триггера запуска указан интервал повторения - $($Trigger.repetition.interval)"
                        # Так как у нас продолжительность указана в строках вида PnYnMnDTnHnMnS, преобразуем это в TimeSpan для сравнения.
                        $intervalTimeSpan = [System.Xml.XmlConvert]::ToTimeSpan($Trigger.repetition.interval)

                        # Значение Interval не может быть больше значения Duration
                        if ($Trigger.repetition.duration) {
                            # Так как у нас продолжительность указана в строках вида PnYnMnDTnHnMnS, преобразуем это в TimeSpan для сравнения.
                            $durationTimeSpan = [System.Xml.XmlConvert]::ToTimeSpan($Trigger.repetition.duration)
                            if ($intervalTimeSpan -gt $durationTimeSpan) {
                                $Trigger.repetition.interval = $null
                                $Trigger.repetition.duration = $null
                                Write-Warning -Message 'Значение интервала повторения задачи не может привышать дилтельность порторения. Параметры проигнорированы.'
                            }
                        }

                        # Значение интервала не может превышать 31 день и не может быть меньше минуты.
                        if (($intervalTimeSpan -gt $(New-TimeSpan -Days 31)) -or ($intervalTimeSpan -lt $(New-TimeSpan -Minutes 1))) {
                            # Если "не попали" в допустимые значения - не будет ничего.
                            $Trigger.repetition.interval = $null
                        }

                        # Устанавливаем значение. Если ещё есть, что устанавливать.
                        if ($Trigger.repetition.interval) {
                            $CIMCLass = Get-CimClass -ClassName 'MSFT_TaskRepetitionPattern' -Namespace 'Root/Microsoft/Windows/TaskScheduler'
                            $Repetition = New-CimInstance -CimClass $CIMCLass -ClientOnly
                            # Возможно, этот параметр можно вынести в конфиг.
                            $Repetition.StopAtDurationEnd = $False
                            $Repetition.Interval = $Trigger.repetition.interval
                            $TaskTrigger.Repetition = $Repetition

                            # К этому моменту значения $Trigger.repetition.duration может быть $null
                            if ($Trigger.repetition.duration) {
                                # Так как у нас продолжительность указана в строках вида PnYnMnDTnHnMnS, преобразуем это в TimeSpan для сравнения.
                                $durationTimeSpan = [System.Xml.XmlConvert]::ToTimeSpan($Trigger.repetition.duration)

                                # Значение продолжительности не может быть меньше минуты.
                                if ($durationTimeSpan -lt $(New-TimeSpan -Minutes 1)) {
                                    # Если "не попали" в допустимые значения - не будет ничего.
                                    $Trigger.repetition.duration = $null
                                }
                            }

                            # Устанавливаем значение.
                            $TaskTrigger.Repetition.Duration = $Trigger.repetition.duration
                        }
                    } # end of 'if ($Trigger.repetition.interval) {...'

                    $TaskTriggers += $TaskTrigger
                } # end of 'foreach ($Trigger in $Triggers) {...'
                #endregion Определяем триггеры запуска задачи.

                #region Определяем "Параметры".

                # Параметры (с примерами значений). которые можно задавать, но это не риализованы тут:
                # - Compatibility : Vista
                # - IdleSettings : MSFT_TaskIdleSettings
                # - NetworkSettings : MSFT_TaskNetworkSettings
                # - RunOnlyIfIdle : False
                # - RunOnlyIfNetworkAvailable : False
                # - StopIfGoingOnBatteries : True
                # - WakeToRun : False
                # - DisallowStartOnRemoteAppSession : False
                # - UseUnifiedSchedulingEngine : False
                # - MaintenanceSettings :
                # - volatile : False

                Write-Verbose -Message '<Set-AtmdScheduledTask> Приступаем к созданию настроек для задачи.'
                $TaskSettings = New-ScheduledTaskSettingsSet

                # Выполнять задачу по требованию
                if ($Settings.allowDemandStart) {
                    $TaskSettings.AllowDemandStart = [System.Convert]::ToBoolean($Settings.allowDemandStart)
                }
                # Принудительная остановка задачи, если она не прекращается по запросу
                if ($Settings.allowHardTerminate) {
                    $TaskSettings.AllowHardTerminate = [System.Convert]::ToBoolean($Settings.allowHardTerminate)
                }
                # Если повтор задачи не запланирован, удалять через
                if ($Settings.deleteExpiredTaskAfter) {
                    $TaskSettings.DeleteExpiredTaskAfter = $Settings.deleteExpiredTaskAfter
                }
                # Запускать только при питании от электросети
                if ($Settings.disallowStartIfOnBatteries) {
                    $TaskSettings.DisallowStartIfOnBatteries = [System.Convert]::ToBoolean($Settings.disallowStartIfOnBatteries)
                }
                # Отключить / Включить
                if ($Settings.enabled) {
                    $TaskSettings.Enabled = [System.Convert]::ToBoolean($Settings.enabled)
                }
                # Останавливать задачу, выполняемую дольше
                if ($Settings.executionTimeLimit) {
                    if ($Settings.executionTimeLimit -eq '') {
                        $Settings.executionTimeLimit = $null
                    }
                    $TaskSettings.ExecutionTimeLimit = $Settings.executionTimeLimit
                }
                # Скрытая
                if ($Settings.hidden) {
                    $TaskSettings.Hidden = [System.Convert]::ToBoolean($Settings.hidden)
                }
                # Если задача уже выполняется, то применять правило
                if ($Settings.multipleInstances) {
                    $AllowedValues = @('IgnoreNew', 'Parallel', 'Queue', '')
                    if ($Settings.multipleInstances -in $AllowedValues) {
                        if ($Settings.multipleInstances -eq '') {
                            $Settings.multipleInstances = $null
                        }
                        $TaskSettings.MultipleInstances = $Settings.multipleInstances
                    }
                    else {
                        $TaskSettings.MultipleInstances = 'IgnoreNew'
                    }
                }
                # Не нашел в интерфейсе...
                if ($Settings.priority) {
                    # Если триоритет не попадает в допустимы диапазон от 0 до 10, то используется значение по умолчанию - 7.
                    if ($Settings.priority -notin @(0..10)) {
                        $Settings.priority = '7'
                    }
                    $TaskSettings.Priority = $Settings.priority
                }
                # Количество попыток перезапуска
                if ($Settings.restartCount) {
                    if ($Settings.restartCount -lt 0) {
                        $Settings.restartCount = 0
                    }
                    if ($Settings.restartCount -gt 0) {
                        # При сбое выполнять перезапуск через
                        if ($Settings.restartInterval) {
                            # Так как у нас продолжительность указана в строках вида PnYnMnDTnHnMnS, преобразуем это в TimeSpan для сравнения.
                            $intervalTimeSpan = [System.Xml.XmlConvert]::ToTimeSpan($Settings.restartInterval)

                            # Значение интервала не может превышать 31 день и не может быть меньше минуты.
                            if (($intervalTimeSpan -gt $(New-TimeSpan -Days 31)) -or ($intervalTimeSpan -lt $(New-TimeSpan -Minutes 1))) {
                                # Если "не попали" в допустимые значения - будет 5 минут.
                                $Settings.restartInterval = 'PT5M'
                            }

                            # Устанавливаем значение.
                            $TaskSettings.RestartInterval = $Settings.restartInterval
                        }
                        else {
                            $Settings.restartCount = 0
                        }
                    }
                    $TaskSettings.RestartCount = $Settings.restartCount
                }
                # Немедленно запускать задачу, если пропущен плановый запук
                if ($Settings.startWhenAvailable) {
                    $TaskSettings.StartWhenAvailable = [System.Convert]::ToBoolean($Settings.startWhenAvailable)
                }
                #endregion Определяем "Параметры".

                #region Определяем пользователя для запуска и создаем задачу.
                Write-Verbose -Message '<Set-AtmdScheduledTask> Указываем параметры запуска и регистрируем задачу.'
                if ($Security.UserID) {
                    $TaskUserID = $Security.UserID
                    # Если домен пользователя не указан, берем домен текущего компьютера.
                    if (($TaskUserID.IndexOf('\') -lt 0) -and ($TaskUserID.IndexOf('@') -lt 0)) {
                        $ComputerSystem = Get-CimInstance -ClassName Win32_ComputerSystem
                        $TaskUserID = $TaskUserID + '@' + $ComputerSystem.Domain
                    }
                    ## TODO Вынести определение Run Level в конфиг
                    # Run Level could be Limited or Highest
                    $TaskPrincipal = New-ScheduledTaskPrincipal -UserId $TaskUserID -RunLevel Highest -LogonType Password

                    $SecurityStringPassword = ConvertTo-SecureString -String $Security.userPassword -Key $MagicKey
                    $BinaryStringPassword = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecurityStringPassword)
                    $TaskPassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BinaryStringPassword)

                    # Создаем задачу.
                    $ScheduledTask = New-ScheduledTask -Action $TaskActions -Trigger $TaskTriggers -Settings $TaskSettings -Description $BaseParams.Description -Principal $TaskPrincipal
                    # Регистрируем задачу.
                    Register-ScheduledTask -TaskName $BaseParams.Name -TaskPath $BaseParams.TaskPath -InputObject $ScheduledTask -User $TaskUserID -Password $TaskPassword | Out-Null
                }
                else {
                    # Создаем задачу.
                    $ScheduledTask = New-ScheduledTask -Action $TaskActions -Trigger $TaskTriggers -Settings $TaskSettings -Description $BaseParams.Description
                    # Регистрируем задачу.
                    Register-ScheduledTask -TaskName $BaseParams.Name -TaskPath $BaseParams.TaskPath -InputObject $ScheduledTask | Out-Null
                }
                #endregion Определяем пользователя для запуска и создаем задачу.

                Start-Sleep -Seconds 2
                $Result = Get-ScheduledTask -TaskName $BaseParams.Name -TaskPath $BaseParams.TaskPath

            } # end of 'if (-not($ScheduledTask)) {...'
            else {
                Write-Verbose -Message '<Set-AtmdScheduledTask> Задача с указанным именем уже существует.'
                $Result = $ScheduledTask
            }
        }
        catch {
            # $PSCmdlet.ThrowTerminatingError($PSitem)
            Write-Error -Exception $PSItem.Exception
        }
    }

    end {
        if ($PassThru) {
            return $Result
        }
    }
}