Last active
March 31, 2025 14:56
-
-
Save Bill-Stewart/ce137f690775720036ac63664aee2ba3 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Start-ScheduledTaskCommand.ps1 | |
# Written by Bill Stewart (bstewart AT iname.com) | |
# This script provides a simple way to execute a command one one or more remote | |
# computers using the Task Scheduler service. Uses the Task Scheduler scripting | |
# objects: | |
# https://learn.microsoft.com/en-us/windows/win32/taskschd/task-scheduler-objects | |
# If you're using the Windows firewall, you'll need the 'Remove Scheduled Tasks | |
# Management' inbound rules (or equivalent) in place on remote machines. | |
#requires -version 3 | |
<# | |
.SYNOPSIS | |
Schedules a command to run once using the Task Scheduler. | |
.DESCRIPTION | |
Schedules a command to run once using the Task Scheduler. | |
.PARAMETER TaskCredential | |
Specifies a PSCredential object that contains the credentials to run the task. | |
.PARAMETER TaskServiceAccountName | |
Specifies the name of a service account to run the task. This can be a SID if you specify a '*' as the first character (e.g., '*S-1-5-19' specifies the 'LOCAL SERVICE' account). You can specify a Managed Service Account (MSA) name by appending '$' to the account name (e.g. 'AppService$'). | |
.PARAMETER Command | |
Specifies the command to run. Note that the command will run without a visible user interface, so it should run to completion without user intervention (e.g., a quiet application installation). | |
.PARAMETER Parameters | |
Specifies the command's command-line parameters. Specify an empty string ('') if the command doesn't need any parameters. (NOTE: It is strongly recommended not to specify any sensitive information, such as a password, in this parameter.) | |
.PARAMETER ComputerName | |
Specifies the names of one or more computers on which to start the task. You can specify a dot ('.') or the name 'localhost' to schedule the task to run on the current computer. | |
.PARAMETER TaskName | |
Specifies the name of the scheduled task. If you omit this parameter, the task name will automatically be generated based on the command name and the current date and time. If the task already exists, it will not be overwritten unless you also specify -Force. | |
.PARAMETER Description | |
Specifies a description for the scheduled task. | |
.PARAMETER StartTime | |
Specifies when the task should start. If not specified, or if the specified start time has already passed, the task will be scheduled for 2 minutes from the current date and time. | |
.PARAMETER ConnectionCredential | |
Specifies credentials to connect to the computer(s). If not specified, the current credentials are used to connect. | |
.PARAMETER RunElevated | |
Specifies to run the task using highest privileges. If not specified, the task will run with limited privileges. | |
.PARAMETER DeleteTask | |
Configures the scheduled task to be automatically deleted one minute after its trigger expires. If not specified, the scheduled task will not be automaticaally deleted. | |
.PARAMETER Force | |
If you specify -TaskName, this parameter forces overwriting of the task if it already exists. | |
.INPUTS | |
System.String | |
.OUTPUTS | |
PSObjects with the following properties: | |
ComputerName Computer on which task is scheduled | |
AccountName Account used to run the scheduled task | |
TaskName Scheduled task name | |
Command Command to run | |
Parameters Parameters for the command | |
Elevated Whether the task runs with highest privileges | |
StartTime Start time for scheduled task | |
DeleteAfter If -DeleteTask specified, when task can be deleted | |
ErrorCode Task Scheduler result: 0 for success, or hexadecimal error code | |
Status Result of Task Scheduler request | |
#> | |
[CmdletBinding(DefaultParameterSetName = "ServiceAccount")] | |
param( | |
[Parameter(ParameterSetName = "Credential",Position = 0,Mandatory)] | |
[ValidateNotNullOrEmpty()] | |
[Management.Automation.PSCredential] | |
$TaskCredential, | |
[Parameter(ParameterSetName = "ServiceAccount",Position = 0,Mandatory)] | |
[ValidateNotNullOrEmpty()] | |
[String] | |
$TaskServiceAccountName, | |
[Parameter(ParameterSetName = "Credential",Position = 1,Mandatory)] | |
[Parameter(ParameterSetName = "ServiceAccount",Position = 1,Mandatory)] | |
[ValidateNotNullOrEmpty()] | |
[String] | |
$Command, | |
[Parameter(ParameterSetName = "Credential",Position = 2)] | |
[Parameter(ParameterSetName = "ServiceAccount",Position = 2)] | |
[String] | |
$Parameters, | |
[Parameter(ParameterSetName = "Credential",Position = 3,Mandatory,ValueFromPipeline)] | |
[Parameter(ParameterSetName = "ServiceAccount",Position = 3,Mandatory,ValueFromPipeline)] | |
[ValidateNotNullOrEmpty()] | |
[String[]] | |
$ComputerName, | |
[Parameter(ParameterSetName = "Credential")] | |
[Parameter(ParameterSetName = "ServiceAccount")] | |
[String] | |
$TaskName, | |
[Parameter(ParameterSetName = "Credential")] | |
[Parameter(ParameterSetName = "ServiceAccount")] | |
[String] | |
$Description, | |
[Parameter(ParameterSetName = "Credential")] | |
[Parameter(ParameterSetName = "ServiceAccount")] | |
[DateTime] | |
$StartTime, | |
[Parameter(ParameterSetName = "Credential")] | |
[Parameter(ParameterSetName = "ServiceAccount")] | |
[Management.Automation.PSCredential] | |
$ConnectionCredential, | |
[Parameter(ParameterSetName = "Credential")] | |
[Parameter(ParameterSetName = "ServiceAccount")] | |
[Switch] | |
$RunElevated, | |
[Parameter(ParameterSetName = "Credential")] | |
[Parameter(ParameterSetName = "ServiceAccount")] | |
[Switch] | |
$DeleteTask, | |
[Parameter(ParameterSetName = "Credential")] | |
[Parameter(ParameterSetName = "ServiceAccount")] | |
[Switch] | |
$Force | |
) | |
begin { | |
# Constants | |
$SCHED_S_TASK_READY = 0x00041300 | |
$SCHED_E_ACCOUNT_NAME_NOT_FOUND = 0x80041310 | |
$SCHED_E_TASK_NOT_V1_COMPAT = 0x80041327 | |
$TASK_ACTION_EXEC = 0 | |
$TASK_CREATE = 2 | |
$TASK_CREATE_OR_UPDATE = 6 | |
$TASK_LOGON_PASSWORD = 1 | |
$TASK_LOGON_SERVICE_ACCOUNT = 5 | |
$TASK_RUNLEVEL_LUA = 0 | |
$TASK_RUNLEVEL_HIGHEST = 1 | |
$TASK_TRIGGER_TIME = 1 | |
if ( [Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT ) { | |
throw "Windows platform required" | |
} | |
# Abort if we can't instantiate the COM object | |
try { | |
$TaskService = New-Object -ComObject "Schedule.Service" | |
} | |
catch { | |
Write-Error -Exception $_.Exception | |
exit $_.Exception.HResult | |
} | |
# "Immediate if" - if $testExpr is $true, run $trueExpr; else run $falseExpr | |
function iif { | |
param( | |
[ScriptBlock] | |
$testExpr, | |
[ScriptBlock] | |
$trueExpr, | |
[ScriptBlock] | |
$falseExpr | |
) | |
if ( & $testExpr ) { & $trueExpr } else { & $falseExpr } | |
} | |
# Attempts to convert an account name to a | |
# System.Security.Principal.NTAccount object; if the first character of the | |
# name is '*', attempt to evaluate as a SID; if identity translation fails, | |
# abort script | |
function ConvertTo-IdentityReference { | |
param( | |
[String] | |
$accountName | |
) | |
$sid = New-Object Security.Principal.SecurityIdentifier ` | |
([Security.Principal.WellKnownSidType]::NullSid,$null) | |
if ( ($accountName[0] -ne '*') ) { | |
try { | |
$sid = ([Security.Principal.NTAccount] ` | |
$accountName).Translate([Security.Principal.SecurityIdentifier]) | |
} | |
catch { | |
} | |
} | |
elseif ( $accountName.Length -gt 1 ) { | |
try { | |
$sid = [Security.Principal.SecurityIdentifier] ` | |
$accountName.Substring(1) | |
} | |
catch { | |
} | |
} | |
if ( $sid.IsWellKnown([Security.Principal.WellKnownSidType]::NullSid) ) { | |
$exception = New-Object Security.Principal.IdentityNotMappedException ` | |
("{0} - '$accountName'" -f ([ComponentModel.Win32Exception] ` | |
$SCHED_E_ACCOUNT_NAME_NOT_FOUND).Message) | |
Write-Error -Exception $exception | |
exit $SCHED_E_ACCOUNT_NAME_NOT_FOUND | |
} | |
$sid.Translate([Security.Principal.NTAccount]) | |
} | |
# Based on an account name or PSCredential object, outputs an object with | |
# the following properties: | |
# * FullAccountName: full account name (e.g., authority\accountname) | |
# * AuthorityName: authority name without account name | |
# * AccountName: account name without authority name | |
# * SID: SecurityIdentifier object representing account | |
# * Password: | |
# * PSCredential object's Password property (if -credential parameter) | |
# * $null (if -accountName parameter) | |
# (Requires ConvertTo-IdentityReference function) | |
function Get-IdentityInfo { | |
[CmdletBinding(DefaultParameterSetName = "AccountName")] | |
param( | |
[Parameter(ParameterSetName = "AccountName",Position = 0,Mandatory)] | |
[ValidateNotNullOrEmpty()] | |
[String] | |
$accountName, | |
[Parameter(ParameterSetName = "Credential",Position = 0,Mandatory)] | |
[Management.Automation.PSCredential] | |
$credential | |
) | |
if ( $PSCmdlet.ParameterSetName -eq "AccountName" ) { | |
$identity = ConvertTo-IdentityReference $accountName | |
$password = $null | |
} | |
else { | |
$identity = ConvertTo-IdentityReference $credential.UserName | |
$password = $credential.Password | |
} | |
# Split the account name if it contains '\' character | |
$p = $identity.Value.IndexOf('\') | |
if ( $p -ne -1 ) { | |
$authorityName = $identity.Value.Substring(0,$p) | |
$accountName = $identity.Value.Substring($p + 1) | |
} | |
else { | |
$accountName = $identity.Value | |
} | |
[PSCustomObject] @{ | |
"FullAccountName" = $identity.Value | |
"AuthorityName" = $authorityName | |
"AccountName" = $accountName | |
"SID" = $identity.Translate([Security.Principal.SecurityIdentifier]) | |
"Password" = $password | |
} | |
} | |
# For local system, local service, or network service SIDs, outputs | |
# $TASK_LOGON_SERVICE account; otherwise, outputs $TASK_LOGON_PASSWORD | |
# (Note: MSAs use $TASK_LOGON_PASSWORD even though they're service accounts) | |
function Get-TaskSchedulerLogonType { | |
param( | |
[Security.Principal.SecurityIdentifier] | |
$sid | |
) | |
$serviceAccountLogon = ` | |
$sid.IsWellKnown([Security.Principal.WellKnownSidType]::LocalSystemSid) -or | |
$sid.IsWellKnown([Security.Principal.WellKnownSidType]::LocalServiceSid) -or | |
$sid.IsWellKnown([Security.Principal.WellKnownSidType]::NetworkServiceSid) | |
iif { $serviceAccountLogon } { $TASK_LOGON_SERVICE_ACCOUNT } ` | |
{ $TASK_LOGON_PASSWORD } | |
} | |
# Outputs a SecureString object as plain-text | |
function ConvertTo-String { | |
param( | |
[Security.SecureString] | |
$secureStr | |
) | |
try { | |
$bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureStr) | |
[Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr) | |
} | |
finally { | |
if ( $bstr -ne [IntPtr]::Zero ) { | |
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) | |
} | |
} | |
} | |
# Outputs a DateTime object as a Task Scheduler formatted string | |
function Get-TimeString { | |
param( | |
[DateTime] | |
$time | |
) | |
"{0:yyyy-MM-dd}T{0:HH:mm}" -f $time | |
} | |
$BoundParameters = $PSBoundParameters | |
$TaskIdentityInfo = iif { $PSCmdlet.ParameterSetName -eq "Credential" } ` | |
{ Get-IdentityInfo $TaskCredential } ` | |
{ Get-IdentityInfo $TaskServiceAccountName } | |
$TaskLogonType = Get-TaskSchedulerLogonType $TaskIdentityInfo.SID | |
$TaskRunLevel = iif { $RunElevated } { $TASK_RUNLEVEL_HIGHEST } ` | |
{ $TASK_RUNLEVEL_LUA } | |
$TaskFlags = iif { $Force } { $TASK_CREATE_OR_UPDATE } { $TASK_CREATE } | |
$StartTime = iif { $BoundParameters.ContainsKey("StartTime") } ` | |
{ [DateTime] (Get-TimeString $StartTime) } ` | |
{ [DateTime] (Get-TimeString (Get-Date).AddMinutes(2)) } | |
$ConnectIdentityInfo = iif { $BoundParameters.ContainsKey("ConnectionCredential") } ` | |
{ Get-IdentityInfo $ConnectionCredential } { $null } | |
# Uses the following globals (all start with UPPERCASE letter): | |
# * TaskService (connects to the Task Scheduler service) | |
# * Script parameters: Command, Parameters, RunElevated, TaskName, StartTime, | |
# and DeleteTask | |
# * ConnectIdentityInfo and TaskIdentityInfo objects | |
# * Description, TaskLogonType, TaskRunLevel, and TaskFlags | |
function Start-ScheduledTaskCommand { | |
param( | |
[String] | |
$computerName | |
) | |
if ( ($computerName -eq ".") -or ($computerName -eq "localhost") ) { | |
$computerName = [Net.Dns]::GetHostName() | |
} | |
# Construct output object | |
$outputObject = [PSCustomObject] @{ | |
"ComputerName" = $computerName | |
"AccountName" = [NullString]::Value | |
"TaskName" = [NullString]::Value | |
"Command" = [NullString]::Value | |
"Parameters" = [NullString]::Value | |
"Elevated" = [NullString]::Value | |
"StartTime" = $null | |
"DeleteAfter" = $null | |
"ErrorCode" = "0" | |
"Status" = [NullString]::Value | |
} | |
# Try to connect to the Task Scheduler service on the computer | |
try { | |
if ( $null -eq $ConnectIdentityInfo ) { | |
$TaskService.Connect($computerName) | |
} | |
else { | |
$TaskService.Connect($computerName,$ConnectIdentityInfo.AccountName, | |
$ConnectIdentityInfo.AuthorityName, | |
(ConvertTo-String $ConnectIdentityInfo.Password)) | |
} | |
} | |
catch { | |
# Failed to connect; update output object and return it | |
$outputObject.ErrorCode = "0x{0:X8}" -f $_.Exception.HResult | |
$outputObject.Status = $_.Exception.Message | |
return $outputObject | |
} | |
# Sanity check Task Scheduler service version | |
if ( $TaskService.HighestVersion -lt 0x00010002 ) { | |
$outputObject.ErrorCode = "0x{0:X8}" -f $SCHED_E_TASK_NOT_V1_COMPAT | |
$outputObject.Status = ([ComponentModel.Win32Exception] ` | |
$SCHED_E_TASK_NOT_V1_COMPAT).Message | |
return $outputObject | |
} | |
$name = iif { $TaskName } { $TaskName } ` | |
{ "{0} [{1:yyyyMMdd_HHmmss}]" -f (Split-Path $Command -Leaf),(Get-Date) } | |
# Create an empty TaskDefinition object to populate | |
$taskDefinition = $TaskService.NewTask(0) | |
$taskDefinition.RegistrationInfo.Description = $Description | |
# Create an ExecAction object and specify command and parameters | |
$execAction = $taskDefinition.Actions.Create($TASK_ACTION_EXEC) | |
$execAction.Path = $Command | |
$execAction.Arguments = $Parameters | |
# If requested start time has already passed, schedule 2 minutes ahead | |
$startBoundary = iif { (Get-Date) -ge $StartTime } ` | |
{ (Get-Date).AddMinutes(2) } { $StartTime } | |
# Create a TimeTrigger object and specify when task should start | |
$timeTrigger = $taskDefinition.Triggers.Create($TASK_TRIGGER_TIME) | |
$timeTrigger.StartBoundary = Get-TimeString $startBoundary | |
# If -DeleteTask specified, add a trigger expiration | |
if ( $DeleteTask ) { | |
$endBoundary = $startBoundary.AddMinutes(1) | |
$timeTrigger.EndBoundary = Get-TimeString $endBoundary | |
$taskDefinition.Settings.DeleteExpiredTaskAfter = "PT0S" | |
} | |
else { | |
$endBoundary = $null | |
} | |
# Configure the Principal object | |
$principal = $taskDefinition.Principal | |
$principal.UserId = $TaskIdentityInfo.FullAccountName | |
$principal.LogonType = $TaskLogonType | |
$principal.RunLevel = $TaskRunLevel | |
$taskPassword = iif { $null -eq $TaskIdentityInfo.Password } { $null } ` | |
{ ConvertTo-String $TaskIdentityInfo.Password } | |
# Register the task definition | |
try { | |
$taskFolder = $TaskService.GetFolder("\") | |
# Void cast discards RegisteredTask output object | |
[Void] $TaskFolder.RegisterTaskDefinition($name,$taskDefinition, | |
$TaskFlags,$TaskIdentityInfo.FullAccountName,$taskPassword, | |
$TaskLogonType) | |
$outputObject.Status = ([ComponentModel.Win32Exception] ` | |
$SCHED_S_TASK_READY).Message | |
} | |
catch { | |
$outputObject.ErrorCode = "0x{0:X8}" -f $_.Exception.HResult | |
$outputObject.Status = $_.Exception.Message | |
} | |
# Update remainder of output object and output it | |
$outputObject.AccountName = $TaskIdentityInfo.FullAccountName | |
$outputObject.TaskName = $name | |
$outputObject.Command = $execAction.Path | |
$outputObject.Parameters = $execAction.Arguments | |
$outputObject.Elevated = $RunElevated.IsPresent | |
$outputObject.StartTime = $startBoundary | |
$outputObject.DeleteAfter = $endBoundary | |
$outputObject | |
} | |
} | |
process { | |
foreach ( $ComputerNameItem in $ComputerName ) { | |
Start-ScheduledTaskCommand $ComputerNameItem | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment