Powershell Wait for Specified Key to Be Pressed Otherwise Timeout - powershell

I would like to let the user to enter a specified key (e.g.: $ ) within 5 seconds.
If the user cannot enter the specified key, it will become time out.
$host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
This method allows the user to enter a key, but the console will continue to wait for input, which is not time out. How to achieve that?

You need to check [Console]::KeyAvailable in a while loop and timeout manually.
function Read-KeyWithTimeout {
param([int] $Timeout = 1000)
end {
$stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
while (-not [Console]::KeyAvailable) {
if ($stopwatch.ElapsedMilliseconds -gt $Timeout) {
throw 'Timeout hit'
}
Start-Sleep -Milliseconds 50
}
$Host.UI.RawUI.ReadKey('NoEcho, IncludeKeyDown')
}
}
When [Console]::KeyAvailable returns true there is input in the buffer waiting to be read.
Note: On Unix this will still echo the input because of how System.Console is implemented.

Related

Subscribe Powershell Information, Warning and Error stream AddData event

I'm building a wrapper around PowerShell scripts to handle logging in one unified way. The idea is to intercept messages from information/warning/error streams and log them in chronological order using different sinks (console/file/db etc). For that purpose, I'd like to utilize events that occur when some data is added to a corresponding stream. That's where I'm now:
$ps = [PowerShell]::Create("CurrentRunspace")
$ps.AddScript({
Write-Information 'Information'
Write-Warning 'Warning'
Write-Error 'Error'
exit -1
}) # in reality here should be script that is wrapped
$output = [System.Management.Automation.PSDataCollection[object]]::new()
$ps.Streams.Information.Add_DataAdded({
# $output.Add($_) is for example, in reality, any handler could be added
$ps.Streams.Information.ReadAll().ForEach{ $output.Add($_) }
})
$ps.Streams.Warning.Add_DataAdded({
# $output.Add($_) is for example, in reality, any handler could be added
$ps.Streams.Warning.ReadAll().ForEach{ $output.Add($_) }
})
$ps.Streams.Error.Add_DataAdded({
# $output.Add($_) is for example, in reality, any handler could be added
$ps.Streams.Error.ReadAll().ForEach{ $output.Add($_) }
})
# run script
$results = $ps.Invoke()
$exitCode = $LASTEXITCODE
# these lines are just for the test
Write-Host $exitCode
Write-Host "$([string]::Join(', ', $output))"
Expected content of $output:
-1
Information
Warning
Error
Actual content of $output:
-1
Error
Do you guys know why a subscription to the Warning and Information DataAdded event doesn't work?

How can I ensure Dispose() is called on an advanced function's local variable on stop signal?

I have noticed that objects implementing IDisposable in advanced functions aren't reliably disposed of when a "stop" signal (eg. pressing CTRL+C) is sent during execution. This is a pain when the object holds a handle to, for example, a file. If the stop signal is received at an inopportune time, the handle doesn't get closed and the file remains locked until the PowerShell session is closed.
Consider the following class and functions:
class f : System.IDisposable {
Dispose() { Write-Host 'disposed' }
}
function g {
param( [Parameter(ValueFromPipeline)]$InputObject )
begin { $f = [f]::new() }
process {
try {
$InputObject
}
catch {
$f.Dispose()
throw
}
}
end {$f.Dispose()}
}
function throws {
param ( [Parameter(ValueFromPipeline)] $InputObject )
process { throw 'something' }
}
function blocks {
param ( [Parameter(ValueFromPipeline)] $InputObject )
process { Wait-Event 'bogus' }
}
Imagine $f holds a handle to a file and releases it when its Dispose() method is called. My goal is that the lifetime of $f matches the lifetime of g. $f is disposed correctly when g is invoked in each the following ways:
g
'o' | g
'o' | g | throws
I can tell as much because each of these outputs disposed.
When the stop signal is sent while execution is occuring downstream of g, however, $f is not disposed. To test that, I invoked
'o' | g | blocks
which blocks at the Wait-Event inside blocks, then I pressed Ctrl+C to stop execution. In that case, Dispose() does not seem to get called (or, at least disposed is not written to the console).
In C# implementations of such functions it is my understanding that StopProcessing() gets called on a stop signal to do such cleanup. However, there seems to be no analog to StopProcessing available for PowerShell implementations of advanced functions.
How can I ensure that $f is disposed in all cases including a stop signal?
You can't if the function accepts pipeline input.
I don't think a robust way of achieving this is possible if the function accepts pipeline input. The reason is that any of the following could occur while code is executing upstream in the pipeline:
break, continue, or throw
terminating error
stop signal received
When these occur upstream, no part of the function can be caused to intervene. The begin{} and process{} blocks have either run to completion or not run at all, and the end{} block may or may not be run. The closest to an on-point solution I have found is the following:
function g {
param (
[Parameter(ValueFromPipeline)]
$InputObject
)
begin { $f = [f]::new() } # The local IDisposable is created when the pipeline is established.
process {
try
{
# flags to keep track of why finally was run
$success = $false
$caught = $false
$InputObject # output an object to exercise the pipeline downstream
# if we get here, nothing unusual happened downstream
$success = $true
}
catch
{
# we get here if an exception was thrown
$caught = $true
# !!!
# This is bad news. It's possible the exception will be
# handled by an upstream process{} block. The pipeline would
# survive and the next invocation of process{} would occur
# after $f is disposed.
# !!!
$f.Dispose()
# rethrow the exception
throw
}
finally
{
# !!!
# This finally block is not invoked when the PowerShell instance receives
# a stop signal while executing code upstream in the pipeline. In that
# situation cleanup $f.Dispose() is not invoked.
# !!!
if ( -not $success -and -not $caught )
{
# dispose only if finally{} is the only block remaining to run
$f.Dispose()
}
}
}
end {$f.Dispose()}
}
However, per the comments there are still cases where $f.Dispose() is not invoked. You can step through this working example that includes such cases.
Consider a pattern like usingObject {} instead.
If we limit usage to the case where the function responsible for cleanup does not accept pipeline input, then we can factor out the lifetime-management logic into a helper function similar to C#'s using block. Here is a proof-of-concept that implements such a helper function called usingObject. This is an example of how g could be substantially simplified when using usingObject to achieve robust invokation of .Dispose():
# refactored function g
function g {
param (
[Parameter(ValueFromPipeline)]
$InputObject,
[Parameter(Mandatory)]
[f]
$f
)
process {
$InputObject
}
}
# usages of function g
usingObject { [f]::new() } {
g -f $_
}
usingObject { [f]::new() } {
'o' | g -f $_
}
try
{
usingObject { [f]::new() } {
'o' | g -f $_ | throws
}
}
catch {}
usingObject { [f]::new() } {
'o' | g -f $_ | blocks
}
Seems like you can just add a finally{} block and dispose it there. You might also want to consider setting your ErrorActionPreference, since you are doing custom error handling.
$ErrorActionPreference = 'SilentlyContinue'
try
{
try
{
1/0
}
catch
{
throw New-Object System.Exception("Exception!")
}
finally
{
"You can dispose here!"
}
}
catch
{
$_.Exception.Message | Write-Output
}

How do I correctly mock my Function to return a custom property with Pester?

I'm a bit new to PowerShell and specifically Pester testing. I can't seem to recreate a scenario for the function I am making Pester test.
Here is the code:
$State = Get-Status
if(State) {
switch ($State.Progress) {
0 {
Write-Host "Session for $Name not initiated. Retrying."
}
100{
Write-Host "Session for $Name at $($State.Progress) percent"
}
default {
Write-Host "Session for $Name in progress (at $($State.Progress)
percent)."
}
}
I've mocked Get-Status to return true so that the code path would go inside the if block, but then the result doesn't have any value for $State.Progress.
My test would always go into the default block in terms of code path. I tried
creating a custom object $State = [PSCustomObject]#{Progress = 0} to no avail.
Here is part of my Pester test:
Context 'State Progress returns 0' {
mock Get-Status {return $true} -Verifiable
$State = [PSCustomObject]#{Progress = 0}
$result = Confirm-Session
it 'should be' {
$result | should be "Session for $Name not initiated. Retrying."
}
}
There are a couple of issues:
Per 4c's comments, your Mock may not be being called because of scoping (unless you have a describe block around your context not shown). If you change Context to Describe and then use Assert-VerifiableMocks you can see that the Mock does then get called.
You can't verify the output of code that uses Write-Host because this command doesn't write to the normal output stream (it writes to the host console). If you remove Write-Host so that the strings are returned to the standard output stream, the code works.
You can use [PSCustomObject]#{Progress = 0} to mock the output of a .Progress property as you suggested, but I believe this should be inside the Mock of Get-Status.
Here's a minimal/verifiable example that works:
$Name = 'SomeName'
#Had to define an empty function in order to be able to Mock it. You don't need to do this in your code as you have the real function.
Function Get-Status { }
#I assumed based on your code all of this code was wrapped as a Function called Confirm-Session
Function Confirm-Session {
$State = Get-Status
if ($State) {
switch ($State.Progress) {
0 {
"Session for $Name not initiated. Retrying."
}
100{
"Session for $Name at $($State.Progress) percent"
}
default {
"Session for $Name in progress (at $($State.Progress) percent)."
}
}
}
}
#Pester tests for the above code:
Describe 'State Progress returns 0' {
mock Get-Status {
[PSCustomObject]#{Progress = 0}
} -Verifiable
#$State = [PSCustomObject]#{Progress = 0}
$result = Confirm-Session
it 'should be' {
$result | should be "Session for $Name not initiated. Retrying."
}
it 'should call the verifiable mocks' {
Assert-VerifiableMocks
}
}
Returns:
Describing State Progress returns 0
[+] should be 77ms
[+] should call the verifiable mocks 7ms

PowerShell Try/Catch and Retry

I have a fairly large powershell scripts with many (20+) functions which perform various actions.
Right now all of the code doesn't really have any error handling or retry functionality. If a particular task/function fails it just fails and continues on.
I would like to improve error handling and implement retries to make it more robust.
I was thinking something similar to this:
$tries = 0
while ($tries -lt 5) {
try{
# Do Something
# No retries necessary
$tries = 5;
} catch {
# Report the error
# Other error handling
}
}
The problem is that I have many many steps where I would need to do this.
I don't think it make sense to implement the above code 20 times. That seems really superfluous.
I was thinking about writing an "TryCatch" function with a single parameter that contains the actual function I want to call?
I'm not sure that's the right approach either though. Won't I end up with a script that reads something like:
TryCatch "Function1 Parameter1 Parameter2"
TryCatch "Function2 Parameter1 Parameter2"
TryCatch "Function3 Parameter1 Parameter2"
Is there a better way to do this?
If you frequently need code that retries an action a number of times you could wrap your looped try..catch in a function and pass the command in a scriptblock:
function Retry-Command {
[CmdletBinding()]
Param(
[Parameter(Position=0, Mandatory=$true)]
[scriptblock]$ScriptBlock,
[Parameter(Position=1, Mandatory=$false)]
[int]$Maximum = 5,
[Parameter(Position=2, Mandatory=$false)]
[int]$Delay = 100
)
Begin {
$cnt = 0
}
Process {
do {
$cnt++
try {
# If you want messages from the ScriptBlock
# Invoke-Command -Command $ScriptBlock
# Otherwise use this command which won't display underlying script messages
$ScriptBlock.Invoke()
return
} catch {
Write-Error $_.Exception.InnerException.Message -ErrorAction Continue
Start-Sleep -Milliseconds $Delay
}
} while ($cnt -lt $Maximum)
# Throw an error after $Maximum unsuccessful invocations. Doesn't need
# a condition, since the function returns upon successful invocation.
throw 'Execution failed.'
}
}
Invoke the function like this (default is 5 retries):
Retry-Command -ScriptBlock {
# do something
}
or like this (if you need a different amount of retries in some cases):
Retry-Command -ScriptBlock {
# do something
} -Maximum 10
The function could be further improved e.g. by making script termination after $Maximum failed attempts configurable with another parameter, so that you can have have actions that will cause the script to stop when they fail, as well as actions whose failures can be ignored.
I adapted #Victor's answer and added:
parameter for retries
ErrorAction set and restore (or else exceptions do not get caught)
exponential backoff delay (I know the OP didn't ask for this, but I use it)
got rid of VSCode warnings (i.e. replaced sleep with Start-Sleep)
# [Solution with passing a delegate into a function instead of script block](https://stackoverflow.com/a/47712807/)
function Retry()
{
param(
[Parameter(Mandatory=$true)][Action]$action,
[Parameter(Mandatory=$false)][int]$maxAttempts = 3
)
$attempts=1
$ErrorActionPreferenceToRestore = $ErrorActionPreference
$ErrorActionPreference = "Stop"
do
{
try
{
$action.Invoke();
break;
}
catch [Exception]
{
Write-Host $_.Exception.Message
}
# exponential backoff delay
$attempts++
if ($attempts -le $maxAttempts) {
$retryDelaySeconds = [math]::Pow(2, $attempts)
$retryDelaySeconds = $retryDelaySeconds - 1 # Exponential Backoff Max == (2^n)-1
Write-Host("Action failed. Waiting " + $retryDelaySeconds + " seconds before attempt " + $attempts + " of " + $maxAttempts + ".")
Start-Sleep $retryDelaySeconds
}
else {
$ErrorActionPreference = $ErrorActionPreferenceToRestore
Write-Error $_.Exception.Message
}
} while ($attempts -le $maxAttempts)
$ErrorActionPreference = $ErrorActionPreferenceToRestore
}
# function MyFunction($inputArg)
# {
# Throw $inputArg
# }
# #Example of a call:
# Retry({MyFunction "Oh no! It happened again!"})
# Retry {MyFunction "Oh no! It happened again!"} -maxAttempts 10
Solution with passing a delegate into a function instead of script block:
function Retry([Action]$action)
{
$attempts=3
$sleepInSeconds=5
do
{
try
{
$action.Invoke();
break;
}
catch [Exception]
{
Write-Host $_.Exception.Message
}
$attempts--
if ($attempts -gt 0) { sleep $sleepInSeconds }
} while ($attempts -gt 0)
}
function MyFunction($inputArg)
{
Throw $inputArg
}
#Example of a call:
Retry({MyFunction "Oh no! It happend again!"})
Error handling is always going to add more to your script since it usually has to handle many different things. A Try Catch function would probably work best for what you are describing above if you want to have each function have multiple tries. A custom function would allow you to even set things like a sleep timer between tries by passing in a value each time, or to vary how many tries the function will attempt.

Powershell - Inexplicable timeout for HttpRequests

I have a powershell script that monitors several pages on a handful of servers. It gives the HTTP status code from each to determine the server's health.
This has been working perfectly for a number of weeks but today, without making any changes to the script, I started receiving timeout errors ("The operation has timed out"). A telnet on port 80 to the server let me through instantly and I could connect happily via every other mechanism I could think of.
Finally I updated the script by changing the timeout period from 1000ms to 2000ms and the script worked again, instantly. And it still worked after I changed it back to 1000ms.
This is the second time something like this has happened with the same resolution. What is happening and how can I avoid it?
This script may help, it may be a bit overkill for your situation.
function HTTP-Get() {
param
(
[string] $target
)
try
{
$webRequest = [System.Net.WebRequest]::Create($target)
$webRequest.Method = "GET"
[System.Net.WebResponse] $resp = $webRequest.GetResponse();
$rs = $resp.GetResponseStream();
[System.IO.StreamReader] $sr = New-Object System.IO.StreamReader -argumentList $rs;
[string] $results = $sr.ReadToEnd();
}
catch [System.Net.WebException]
{
Write-Warning $_
if ($_.response)
{
$_.response.Close();
}
}
finally
{
if ($sr)
{
$sr.close();
$sr.dispose();
}
if ($resp)
{
$resp.close();
}
}
return $results;
}
As I mentioned in the comments: I had this issue with powershell and none of the .NET http tricks worked (httprequest, webclient). The site I used worked fine with postman and other http helpers.
It seemed like the issue is related to the .NET version, sp, etc. I changed my code to use COM instead of .NET and now it works fine.
......
$HTTPREQUEST_SETCREDENTIALS_FOR_SERVER = 0
$Http = new-object -com "WinHttp.WinHttpRequest.5.1"
$Http.open("GET", "http://....", $false)
$Http.SetCredentials($userName, $password, $HttpREQUEST_SETCREDENTIALS_FOR_SERVER)
$Http.send()
$status = [int]$Http.Status
$responseText = $Http.responseText
.....
I hope it helps :)