Eigenverft.Manifested.Drydock.ScheduledTask.ps1
function New-CompatScheduledTask { <# .SYNOPSIS Create or update a Windows Scheduled Task (Win7/10/11, PS5) via COM with clear scope semantics, minimal prompting, and helpful guidance. .DESCRIPTION - Run context (who the task runs as) via -RunAsAccount: CurrentUser (default), SpecificUser 'DOMAIN\User' or 'User@Domain', or System. - Background mode via -Background ("Run whether user is logged on or not"). - -DoNotStorePassword (S4U): no stored password, typically needs elevation; local-only resources at runtime. - -Credential (PASSWORD): stored credential; allows network access; avoids interactive prompt. - Triggers: -LogonThisUser, -LogonAnyUser, -Startup, -DailyAtTime. - Safety: StartWhenAvailable, IgnoreNew (no overlaps), optional -WakeComputer. - Fast-win improvements: * If -DoNotStorePassword set without -Background -> auto-enable -Background (info message). * If -LogonAnyUser with interactive principal (not System and not -Background) -> warn about behavior. * -DailyAtTime uses invariant TryParseExact "HH:mm" (also accepts [DateTime]). * If S4U chosen, warn when arguments imply UNC/SMB use (no false alarms on local drives). * ActionPath validation with -ForceRegister escape hatch. * Richer HRESULT decoding and remediation hints. * Return a useful object; -Quiet suppresses chatter; -Json outputs JSON. .PARAMETER TaskName Leaf name of the task. .PARAMETER TaskFolder Task folder (e.g. '\MyCompany\MyApp'). Created if missing. Default: '\'. .PARAMETER ActionPath Executable to run (e.g., 'powershell.exe' or a full program/script path). .PARAMETER ActionArguments Arguments for the action. .PARAMETER WorkingDirectory Working directory for the action (prevents relative-path issues). .PARAMETER RunAsAccount Run context: 'CurrentUser' (default), 'SpecificUser', or 'System'. (Alias: -RunAs) .PARAMETER SpecificUser User for SpecificUser context. Accepts 'DOMAIN\User' or 'User@Domain'. .PARAMETER Background Run even when the user is not logged on. (Alias: -RunWhetherUserLoggedOn) .PARAMETER DoNotStorePassword Use S4U ("Do not store password"). Implies -Background. Commonly needs elevation. (Alias: -NoStorePassword) .PARAMETER Credential PSCredential for PASSWORD mode (avoids prompt; enables network access). .PARAMETER NoPrompt If a password is needed and -Credential is not supplied, throw instead of prompting. (Alias: -NonInteractive) .PARAMETER Highest Request "Run with highest privileges" for the run context user. .PARAMETER LogonThisUser Trigger at logon for the run-as user (CurrentUser or SpecificUser). (Alias: -AtLogon) .PARAMETER LogonAnyUser Trigger at logon of ANY user. (Alias: -AtLogonAnyUser) .PARAMETER Startup Trigger at system startup/boot. (Alias: -AtStartup) .PARAMETER DailyAtTime Daily time ('HH:mm' or [DateTime]). Invariant parsing; no repetition is set for daily mode. (Alias: -DailyAt) .PARAMETER WakeComputer Attempt to wake the computer to run (policy/hardware permitting). Opt-in. (Alias: -WakeToRun) .PARAMETER ForceRegister Register even if ActionPath or referenced script does not exist (disables path guard). .PARAMETER Quiet Suppress Write-Host info/hints (errors still thrown). .PARAMETER Json Emit the returned summary object as JSON (also returns the object). .PARAMETER Description Optional description. # ... [examples and the rest of your header remain unchanged] ... #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$TaskName, [string]$TaskFolder = '\', [Parameter(Mandatory)] [string]$ActionPath, [string]$ActionArguments = '', [string]$WorkingDirectory, [Alias('RunAs')] [ValidateSet('CurrentUser','SpecificUser','System')] [string]$RunAsAccount = 'CurrentUser', [string]$SpecificUser, [Alias('RunWhetherUserLoggedOn')] [switch]$Background, [Alias('NoStorePassword')] [switch]$DoNotStorePassword, [System.Management.Automation.PSCredential]$Credential, [Alias('NonInteractive')] [switch]$NoPrompt, [switch]$Highest, [Alias('AtLogon')] [switch]$LogonThisUser, [Alias('AtLogonAnyUser')] [switch]$LogonAnyUser, [Alias('AtStartup')] [switch]$Startup, [Alias('DailyAt')] [object]$DailyAtTime, [Alias('WakeToRun')] [switch]$WakeComputer, [switch]$ForceRegister, [switch]$Quiet, [switch]$Json, [string]$Description ) function _writeInfo($m){ if(-not $Quiet){ Write-Host "[INFO] $m" } } function _writeHint($m){ if(-not $Quiet){ Write-Host "[HINT] $m" -ForegroundColor Yellow } } function _writeErrT($m){ Write-Host "[ERROR] $m" -ForegroundColor Red } function _releaseCom($o){ if($o){ try{ [void][Runtime.InteropServices.Marshal]::ReleaseComObject($o) }catch{} } } $id = [Security.Principal.WindowsIdentity]::GetCurrent() $pri = [Security.Principal.WindowsPrincipal]$id $IsElevated = $pri.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) $CurrentUser = $id.Name if([string]::IsNullOrWhiteSpace($TaskName)){ _writeErrT "TaskName cannot be empty." throw "Invalid TaskName." } if($TaskName -match '[\/:\*\?"<>|]'){ _writeErrT ("TaskName contains illegal characters: {0}" -f $TaskName) _writeHint "Disallowed: / : * ? "" < > |" throw "Invalid TaskName." } if($RunAsAccount -eq 'SpecificUser' -and [string]::IsNullOrWhiteSpace($SpecificUser)){ _writeErrT "When -RunAsAccount SpecificUser is used, you must provide -SpecificUser." _writeHint "Accepted formats: DOMAIN\User or user@domain." throw "SpecificUser is required." } if($RunAsAccount -eq 'SpecificUser' -and -not [string]::IsNullOrWhiteSpace($SpecificUser)){ if($SpecificUser -notmatch '^(?:[^\\\/:\*\?"<>|]+\@[^\s@]+|[^\\\/:\*\?"<>|]+\\[^\\\/:\*\?"<>|]+)$'){ _writeHint ("SpecificUser format looks unusual: {0}. Expected DOMAIN\User or user@domain." -f $SpecificUser) } } if($DoNotStorePassword -and -not $Background){ _writeInfo "-DoNotStorePassword implies background mode; enabling -Background." $Background = $true } if (-not ($LogonThisUser -or $LogonAnyUser -or $Startup -or $DailyAtTime)) { _writeErrT "No trigger specified." _writeHint "Add -LogonThisUser, -LogonAnyUser, -Startup, or -DailyAtTime 'HH:mm'." throw "At least one trigger is required." } if($RunAsAccount -eq 'System' -and $LogonThisUser){ _writeErrT "LogonThisUser cannot be used with -RunAsAccount System. Use -LogonAnyUser or -Startup." throw "Invalid trigger combination." } $looksBareExe = ($ActionPath -match '^[^\\/]+\.(?i:exe)$') if (-not (Test-Path -LiteralPath $ActionPath)) { if (-not $looksBareExe -and -not $ForceRegister) { _writeErrT ("ActionPath not found: {0}" -f $ActionPath) _writeHint "Provide a full path or an .exe name on PATH (e.g., powershell.exe), or pass -ForceRegister." throw "ActionPath not found." } else { _writeHint ("Continuing with non-resolved ActionPath '{0}' (command lookup at runtime)." -f $ActionPath) } } if(-not $IsElevated -and $RunAsAccount -eq 'System'){ _writeErrT "System principal requires an elevated PowerShell." _writeHint "Relaunch as Administrator or use -Background with -Credential for user context." throw "Elevation required." } if(-not $IsElevated -and $RunAsAccount -eq 'SpecificUser' -and $SpecificUser -and $SpecificUser -ne $CurrentUser){ _writeErrT ("Cannot create a task for another user '{0}' from a non-elevated session." -f $SpecificUser) _writeHint "Run elevated, or use -RunAsAccount CurrentUser." throw "Elevation required." } if($Background -and $DoNotStorePassword -and -not $IsElevated){ _writeErrT "S4U (Do not store password) commonly requires elevation." _writeHint "Run elevated or switch to PASSWORD mode with -Credential." throw "Elevation recommended for S4U." } if($LogonAnyUser -and $RunAsAccount -ne 'System' -and -not $Background){ _writeHint "LogonAnyUser + interactive principal runs only when the run-as user logs in. For true 'any user' execution use -Background (PASSWORD/S4U) or -RunAsAccount System." } $dailyStart = $null if($DailyAtTime){ if($DailyAtTime -is [datetime]){ $dailyStart = [datetime]$DailyAtTime } else { $fmt = 'HH:mm' $ci = [System.Globalization.CultureInfo]::InvariantCulture $styles = [System.Globalization.DateTimeStyles]::None $parsed = [datetime]::MinValue $ok = [datetime]::TryParseExact([string]$DailyAtTime,$fmt,$ci,$styles,[ref]$parsed) if(-not $ok){ _writeErrT ("Could not parse -DailyAtTime '{0}'." -f $DailyAtTime) _writeHint "Use 24h format 'HH:mm' (e.g., '09:30') or pass a [DateTime]." throw "Invalid DailyAtTime." } $dailyStart = $parsed } } if($Background -and $DoNotStorePassword){ $arguments = $ActionArguments if($null -eq $arguments){ $arguments = '' } if($ActionPath -like '\\*' -or $arguments -match '(?i)(^|[^A-Za-z0-9_])\\\\[A-Za-z0-9._-]+\\|(?i)\bsmb:'){ _writeHint "S4U selected: background token has no network access. If you need UNC/mapped shares, use -Credential instead." } } function Test-UserMatches($expect, $actual){ if(-not $actual){ return $false } if($expect -eq $actual){ return $true } try{ $a1 = (New-Object System.Security.Principal.NTAccount($actual)).Translate([System.Security.Principal.SecurityIdentifier]).Translate([System.Security.Principal.NTAccount]).Value if($a1 -eq $expect){ return $true } }catch{} return $false } $svc = $null; $folder = $null; $def=$null; $trigs=$null; $act=$null try{ $svc = New-Object -ComObject 'Schedule.Service' $svc.Connect() function Resolve-TaskFolder([__comobject]$service,[string]$path){ $path = ($path -replace '/','\'); if([string]::IsNullOrWhiteSpace($path)){ $path='\' } if($path -eq '\'){ return $service.GetFolder('\') } $parts = $path.Trim('\').Split('\'); $cur = $service.GetFolder('\') foreach($p in $parts){ try{ $cur = $cur.GetFolder("\$p") } catch { $cur = $cur.CreateFolder($p) } } return $cur } $folder = Resolve-TaskFolder -service $svc -path $TaskFolder $def = $svc.NewTask(0) $def.RegistrationInfo.Description = $Description $def.Settings.Enabled = $true $def.Settings.AllowDemandStart = $true $def.Settings.MultipleInstances = 0 $def.Settings.StopIfGoingOnBatteries = $false $def.Settings.DisallowStartIfOnBatteries = $false $def.Settings.RunOnlyIfNetworkAvailable = $false $def.Settings.StartWhenAvailable = $true $def.Settings.ExecutionTimeLimit = 'PT24H' if($WakeComputer){ $def.Settings.WakeToRun = $true } $TaskLogon = @{ Password=1; S4U=2; Interactive=3; Service=5 } $p = $def.Principal if($Highest){ $p.RunLevel = 1 } $RegUser=$null; $RegPwd=$null; $RegLogon=$null switch($RunAsAccount){ 'System'{ $p.UserId='SYSTEM'; $p.LogonType=$TaskLogon.Service $RegUser='SYSTEM'; $RegLogon=$TaskLogon.Service } 'CurrentUser'{ $p.UserId=$CurrentUser if($Background){ if($DoNotStorePassword){ $p.LogonType=$TaskLogon.S4U; $RegUser=$CurrentUser; $RegLogon=$TaskLogon.S4U } else { if(-not $Credential){ if($NoPrompt){ throw "Credentials required; supply -Credential or use -DoNotStorePassword (elevated)." } $Credential = Get-Credential -Message "Enter password for $CurrentUser to run when not logged on" } elseif(-not (Test-UserMatches -expect $CurrentUser -actual $Credential.UserName)){ _writeHint ("Credential user '{0}' does not match current user '{1}'. This can cause 0x8007052E." -f $Credential.UserName, $CurrentUser) } $p.LogonType=$TaskLogon.Password $RegUser=$Credential.UserName $RegPwd =$Credential.GetNetworkCredential().Password $RegLogon=$TaskLogon.Password } } else { $p.LogonType=$TaskLogon.Interactive; $RegLogon=$TaskLogon.Interactive } } 'SpecificUser'{ $p.UserId=$SpecificUser if($Background){ if($DoNotStorePassword){ $p.LogonType=$TaskLogon.S4U; $RegUser=$SpecificUser; $RegLogon=$TaskLogon.S4U } else { if(-not $Credential -or -not (Test-UserMatches -expect $SpecificUser -actual $Credential.UserName)){ if($NoPrompt){ throw "Credentials for $SpecificUser required; username must match the run-as account." } $Credential = Get-Credential -UserName $SpecificUser -Message "Enter password for $SpecificUser to run when not logged on" } $p.LogonType=$TaskLogon.Password $RegUser=$Credential.UserName $RegPwd =$Credential.GetNetworkCredential().Password $RegLogon=$TaskLogon.Password } } else { $p.LogonType=$TaskLogon.Interactive; $RegLogon=$TaskLogon.Interactive } } } $act = $def.Actions.Create(0) $act.Path = $ActionPath if($ActionArguments){ $act.Arguments = $ActionArguments } if($WorkingDirectory){ $act.WorkingDirectory = $WorkingDirectory } $trigs = $def.Triggers if($Startup){ [void]$trigs.Create(8); _writeInfo "Added Startup trigger." } if($LogonThisUser){ $lt = $trigs.Create(9) if($RunAsAccount -eq 'CurrentUser'){ $lt.UserId = $CurrentUser } elseif($RunAsAccount -eq 'SpecificUser'){ $lt.UserId = $SpecificUser } _writeInfo "Added Logon trigger for specific user." } if($LogonAnyUser){ $la = $trigs.Create(9); $la.UserId = $null _writeInfo "Added Logon trigger for ANY user." } # ----- DAILY: once per day, no repetition ----- if ($dailyStart) { $start = [datetime]::Today.AddHours($dailyStart.Hour).AddMinutes($dailyStart.Minute) if ($start -lt (Get-Date)) { $start = $start.AddDays(1) } $dt = $trigs.Create(2) # DAILY $dt.StartBoundary = $start.ToString('s') $dt.DaysInterval = 1 # once per day # Do not set $dt.Repetition.* at all _writeInfo ("Added Daily trigger at {0}." -f $start.ToShortTimeString()) } $TASK_CREATE_OR_UPDATE = 6 $taskPath = ("{0}{1}" -f $TaskFolder, $TaskName) try{ $null = $folder.RegisterTaskDefinition($TaskName, $def, $TASK_CREATE_OR_UPDATE, $RegUser, $RegPwd, $RegLogon, $null) if(-not $Quiet){ Write-Host ("[OK] Task '{0}' created/updated." -f $taskPath) if($WakeComputer){ _writeHint "Wake timers depend on firmware/policy; may be ignored on some devices." } } } catch { $hr = ('0x{0:X8}' -f $_.Exception.HResult) _writeErrT ("Task registration failed (HRESULT={0}). {1}" -f $hr, $_.Exception.Message) switch($hr){ '0x80070005' { _writeHint "Access denied. Elevate for System/other-user, or use a delegated -TaskFolder." } '0x8007052E' { _writeHint "Logon failure (bad credentials). Verify -Credential username matches the run-as account." } '0x80070002' { _writeHint "File not found. Check ActionPath and any script paths in -ActionArguments." } '0x80041316' { _writeHint "One or more properties are invalid (e.g., logon type vs. principal). Review S4U/PASSWORD choices." } '0x80041314' { _writeHint "Account information not set. PASSWORD mode requires valid -Credential." } '0x80041309' { _writeHint "Invalid task name. Avoid special characters." } default { _writeHint "Verify elevation (if needed), credentials, and folder ACLs." } } throw } $logonTypeName = switch($RegLogon){ 1 {'Password'} 2 {'S4U'} 3 {'Interactive'} 5 {'Service'} default {"$RegLogon"} } $bgMode = if($RunAsAccount -eq 'System'){'Service'} elseif($Background -and $DoNotStorePassword){'S4U'} elseif($Background){'Password'} else{'Interactive'} $trigList = @() if($Startup){ $trigList += 'Startup' } if($LogonThisUser){ $trigList += 'Logon-ThisUser' } if($LogonAnyUser){ $trigList += 'Logon-AnyUser' } if($dailyStart){ $trigList += ('Daily@' + ($dailyStart.ToString('HH:mm'))) } $result = [pscustomobject]@{ TaskPath = $taskPath TaskFolder = $TaskFolder TaskName = $TaskName Principal = $RunAsAccount PrincipalUser = if($RunAsAccount -eq 'CurrentUser'){$CurrentUser} elseif($RunAsAccount -eq 'SpecificUser'){$SpecificUser} else {'SYSTEM'} LogonType = $logonTypeName Background = $bgMode Triggers = $trigList ActionPath = $ActionPath ActionArgs = $ActionArguments WorkingDir = $WorkingDirectory Elevated = $IsElevated WakeComputer = [bool]$WakeComputer } if($Json){ $json = $result | ConvertTo-Json -Depth 4 if(-not $Quiet){ $json } return $result } else { return $result } } finally{ _releaseCom $act _releaseCom $trigs _releaseCom $def _releaseCom $folder _releaseCom $svc } } # 1) Current user at logon (visible; no password; no elevation) # Use when your script needs the user's interactive desktop. #New-CompatScheduledTask -TaskName 'MyApp-UserLogon' -ActionPath 'C:\Windows\regedit.exe' -LogonThisUser #New-CompatScheduledTask -TaskName 'MyDaily-System-0200' `-RunAsAccount System -Highest -DailyAtTime '02:00' -ActionPath "$env:WINDIR\System32\WindowsPowerShell\v1.0\powershell.exe" -ActionArguments '-NoProfile -ExecutionPolicy Bypass -File "C:\Scripts\job.ps1"' -WorkingDirectory 'C:\Scripts' |