Classes/ExpectHandler.ps1
class ExpectHandler { [System.Diagnostics.Process]$process = [System.Diagnostics.Process]::new() hidden [System.Collections.Generic.List[string]] $output = [System.Collections.Generic.List[string]]::new() hidden [int] $timeoutSeconds = $null hidden [int] $EventID = $null [void] StartProcess([int]$timeout) { # If a timeout was provided, override the global timeout if ($timeout -gt 0) { $this.timeoutSeconds = $timeout } $PowerShellExe = if ($(Get-Command "pwsh.exe" -ErrorAction SilentlyContinue)) { Get-Command "pwsh.exe" | Select-Object -ExpandProperty Path } else { Get-Command "powershell.exe" | Select-Object -ExpandProperty Path } # Configure the process $this.process.StartInfo.FileName = $PowerShellExe $this.process.StartInfo.UseShellExecute = $false $this.process.StartInfo.Arguments = "-NoLogo" $this.process.StartInfo.RedirectStandardInput = $true $this.process.StartInfo.RedirectStandardOutput = $true $this.process.StartInfo.CreateNoWindow = $false $this.process.EnableRaisingEvents = $true # Attach an asynchronous event handler to the output $stdEvent = Register-ObjectEvent -InputObject $this.process -EventName OutputDataReceived -Action { param([Object]$PSSender, [System.Diagnostics.DataReceivedEventArgs]$DataArgs) if ($null -ne $DataArgs.Data) { # Set the max length of the output list to 100 items $global:processHandler.AppendOutput($DataArgs.Data, 100) } }.GetNewClosure() # Save the EventID so we can unregister the event later $this.EventID = $stdEvent.Id # Start the process $this.process.Start() # Start reading the output asynchronously $this.process.BeginOutputReadLine() } [void] StopProcess() { # Stop reading the process output so we can remove the event handler $this.process.CancelOutputRead() Get-EventSubscriber | Where-Object { $_.SubscriptionId -like $this.EventID } | Unregister-Event # Assuming process has not already exited, destroy the process if (-not $this.process.HasExited) { $this.process.Kill() } Write-Information -MessageData "Closing process" -Tags "Close", "Process" $this.process.Close() } [void] ExpectRegex([string] $regexString, [int] $timeoutMs, [bool] $continueOnTimeout, [bool]$EOF) { # If user is expecting end of automation process, close the process. if ($EOF) { $this.StopProcess() } else { [bool]$IsMatched = $false [int] $timeout = 0 # If a timeout was provided specifically to this expect, override any global settings if ($timeoutMs -gt 0) { $timeout = $timeoutMs } elseif ($this.timeoutSeconds -gt 0) { $timeout = $this.timeoutSeconds } # Calculate the max timestamp we can reach before the expect times out [long] $maxTimestamp = [DateTimeOffset]::Now.ToUnixTimeSeconds() + $timeout # While no match is found (or no timeout occurs), continue to evaluate output until match is found do { $this.output | ForEach-Object { $line = $_ if ($line -match $regexString) { Write-Information -MessageData "Match found: $line" -Tags "Match", "Found" $IsMatched = $true break } } # Clear the output to keep the buffer nice and lean $this.output.Clear() # If a timeout is set and we've exceeded the max time, throw timeout error and stop the loop if ($timeout -gt 0 -and [DateTimeOffset]::Now.ToUnixTimeSeconds() -ge $maxTimestamp) { [string]$timeoutMessage = "Timed out waiting for: '$($regexString)'" $IsMatched = $true if (-not $continueOnTimeout) { $this.StopProcess() throw [Exception]::new($timeoutMessage) } else { Write-Information -MessageData $timeoutMessage -Tags "Timeout" } break } # TODO: Evaluate if this timeout is too much or if we should attempt to evaluate matches as they arrive. [System.Threading.Thread]::Sleep(500) } while (-not $IsMatched) } } [void] ExpectSimple([string] $SimpleMatchString, [int] $timeoutMs, [bool] $continueOnTimeout, [bool]$EOF) { # If user is expecting end of automation process, close the process. if ($EOF) { $this.StopProcess() } else { [bool]$IsMatched = $false [int] $timeout = 0 # If a timeout was provided specifically to this expect, override any global settings if ($timeoutMs -gt 0) { $timeout = $timeoutMs } elseif ($this.timeoutSeconds -gt 0) { $timeout = $this.timeoutSeconds } # Calculate the max timestamp we can reach before the expect times out [long] $maxTimestamp = [DateTimeOffset]::Now.ToUnixTimeSeconds() + $timeout # While no match is found (or no timeout occurs), continue to evaluate output until match is found do { $this.output | ForEach-Object { $line = $_ if ($line -like $SimpleMatchString) { Write-Information -MessageData "Match found: $line" -Tags "Match", "Found" $IsMatched = $true break } } # Clear the output to keep the buffer nice and lean $this.output.Clear() # If a timeout is set and we've exceeded the max time, throw timeout error and stop the loop if ($timeout -gt 0 -and [DateTimeOffset]::Now.ToUnixTimeSeconds() -ge $maxTimestamp) { [string]$timeoutMessage = "Timed out waiting for: '$($SimpleMatchString)'" $IsMatched = $true if (-not $continueOnTimeout) { $this.StopProcess() throw [Exception]::new($timeoutMessage) } else { Write-Information -MessageData $timeoutMessage -Tags "Timeout" } break } # TODO: Evaluate if this timeout is too much or if we should attempt to evaluate matches as they arrive. [System.Threading.Thread]::Sleep(500) } while (-not $IsMatched) } } [void] Send([string]$command, [bool]$noNewline) { $this.process.StandardInput.Write($command + $(if ($noNewline) { "" }else { "`n" })) | Out-Null } [void] AppendOutput([string]$data, [int]$maxLength) { Write-Host $data # If there are too many items in the array, truncate items starting from the oldest. if ($this.output.Count -gt $maxLength) { [int]$removeCount = $this.output.Count - $maxLength $this.output.RemoveRange(0, $removeCount) } $this.output.Add($data) } } |