public/Invoke-OctaScheduler.ps1
|
function Invoke-OctaScheduler { <# .SYNOPSIS Creates/lists/removes a Windows Task Scheduler entry that runs an Octa category or Quick Clean non-interactively on a schedule. 007 US5. Every scheduled run invokes the exported cmdlets directly (never Invoke-Octa/the interactive menu), consistent with the non-interactive guard already in Show-OctaMenu. Runs elevated (RunLevel Highest) always, disclosed here before creating the task, since most categories require it anyway and a scheduled run has no one present to grant elevation on demand. #> [CmdletBinding()] param( [switch]$Create, [string]$Category, [ValidateSet('Daily', 'Weekly', 'Monthly')] [string]$Frequency, [string]$Remove ) $taskPath = '\Octa\' if ($Remove) { $existing = Get-ScheduledTask -TaskPath $taskPath -TaskName $Remove -ErrorAction SilentlyContinue if (-not $existing) { return [pscustomobject]@{ Status = 'NotFound'; Message = "No scheduled Octa run named: $Remove" } } Unregister-ScheduledTask -TaskPath $taskPath -TaskName $Remove -Confirm:$false return [pscustomobject]@{ Status = 'Success'; Message = "Removed scheduled run: $Remove" } } if ($Create) { if (-not $Category -or -not $Frequency) { return [pscustomobject]@{ Status = 'Error'; Message = 'Both -Category and -Frequency are required to create a scheduled run.' } } # ponytail: validate against the real catalog before registering anything. Without this, # an unknown/misspelled category happily created a scheduled task that could only ever # fail, silently, forever - the user would never see the failure because the task runs # hidden and non-interactively. It also keeps $Category (which gets interpolated into the # -Command string below) constrained to known-good ids instead of arbitrary text. $validCategories = @(Get-OctaCategory -SkipRiskScan | Select-Object -ExpandProperty Id) + 'quick-clean' if ($Category -notin $validCategories) { return [pscustomobject]@{ Status = 'UnknownCategory' Message = "Unknown category: $Category. Valid values: $($validCategories -join ', ')" } } $cmdletCall = if ($Category -eq 'quick-clean') { 'Invoke-OctaQuickClean -Apply -Yes -Quiet' } else { "Invoke-OctaCategory -CategoryId '$Category' -Apply -Yes -Quiet" } $command = "Import-Module Octa -Force; $cmdletCall" Write-Host 'This scheduled task will run elevated (Administrator), since most categories require it.' $taskName = "Octa-$Category-$Frequency-$([guid]::NewGuid().ToString('N').Substring(0, 8))" $arguments = "-NoProfile -WindowStyle Hidden -Command `"$command`"" $startBoundary = (Get-Date -Hour 3 -Minute 0 -Second 0).ToString('yyyy-MM-ddTHH:mm:ss') try { if ($Frequency -eq 'Monthly') { # ponytail: registered from task XML, not from an MSFT_TaskMonthlyTrigger CIM # instance. New-ScheduledTaskTrigger genuinely has no -Monthly parameter set # (only Once/Daily/Weekly/Startup/Logon exist in PS 5.1), but the CIM route that # replaced it never actually worked: Register-ScheduledTask rejected every single # variant with "El parametro no es correcto" (E_INVALIDARG) - plain assignment, # explicit [uint16] casts (the class really does declare DaysOfMonth/MonthOfYear # as UInt16), RunOnLastDayOfMonth set, timezone-qualified StartBoundary, and # New-ScheduledTask + -InputObject. All five failed 100% of the time, which is # why the "flaky" Monthly test was never flaky at all - it was reporting a real, # permanent break. Registering the same schedule as task XML works, and is still # a genuine calendar-monthly trigger (day 1 of all 12 months), not a drifting # "every 4 weeks" approximation. $escapedArguments = [System.Security.SecurityElement]::Escape($arguments) $escapedUser = [System.Security.SecurityElement]::Escape($env:USERNAME) $xml = @" <?xml version="1.0" encoding="UTF-16"?> <Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task"> <Triggers> <CalendarTrigger> <StartBoundary>$startBoundary</StartBoundary> <Enabled>true</Enabled> <ScheduleByMonth> <DaysOfMonth><Day>1</Day></DaysOfMonth> <Months><January/><February/><March/><April/><May/><June/><July/><August/><September/><October/><November/><December/></Months> </ScheduleByMonth> </CalendarTrigger> </Triggers> <Principals> <Principal id="Author"> <UserId>$escapedUser</UserId> <LogonType>S4U</LogonType> <RunLevel>HighestAvailable</RunLevel> </Principal> </Principals> <Settings> <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy> <Enabled>true</Enabled> <StartWhenAvailable>true</StartWhenAvailable> </Settings> <Actions Context="Author"> <Exec> <Command>powershell.exe</Command> <Arguments>$escapedArguments</Arguments> </Exec> </Actions> </Task> "@ Register-ScheduledTask -TaskName $taskName -TaskPath $taskPath -Xml $xml -ErrorAction Stop | Out-Null } else { $action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument $arguments $trigger = switch ($Frequency) { 'Daily' { New-ScheduledTaskTrigger -Daily -At 3am } 'Weekly' { New-ScheduledTaskTrigger -Weekly -DaysOfWeek Sunday -At 3am } } $principal = New-ScheduledTaskPrincipal -UserId $env:USERNAME -LogonType S4U -RunLevel Highest Register-ScheduledTask -TaskName $taskName -TaskPath $taskPath -Action $action -Trigger $trigger -Principal $principal -ErrorAction Stop | Out-Null } } catch { # ponytail: -ErrorAction Stop + this catch, because Register-ScheduledTask only raises # a NON-terminating error on failure - execution used to continue straight to the # "Success" return below, so a task that was never created was reported as created # (exactly what the Monthly break looked like from the outside: Status = 'Success' # with a TaskName that could not be found or removed afterwards). return [pscustomobject]@{ Status = 'Error'; Message = "Could not register the scheduled task: $($_.Exception.Message)" } } return [pscustomobject]@{ Status = 'Success'; TaskName = $taskName } } $tasks = @(Get-ScheduledTask -TaskPath $taskPath -ErrorAction SilentlyContinue) foreach ($t in $tasks) { Write-Host ("{0,-10} {1}" -f $t.State, $t.TaskName) } return $tasks } |