Get-WSManInstance ignores -ErrorAction parameter - powershell

Consider the following script:
$ErrorActionPreference = "SilentlyContinue"
$selectorset = #{
Transport = "HTTPS"
Address = "*"
}
$listener = Get-WSManInstance -ErrorAction Stop `
-ResourceURI 'winrm/config/Listener' -SelectorSet $selectorset
write "Bye!"
For the case when the HTTPS listener is absent, the Get-WSManInstance invocation throws a "cannot find the resource" error (and this is expected). However, this invocation seems to only respect the value of $ErrorActionPreference and completely ignore the value of the -ErrorAction parameter (which is supposed to override the value of $ErrorActionPreference), you may try different combinations of the supported values for these two settings. I have tested the behavior with PowerShell versions 5.1.17763.1490 and 4.0.
If I get it right, the error in this case is a "non-terminating error", otherwise $ErrorActionPreference would have been ignored too.
In contrast, when I test the -ErrorAction parameter with some standard cmdlets like Write-Error or Get-Item, it seems to work as expected.
I am quite new to PowerShell and trying to understand if I am missing something here or it is a bug with Get-WSManInstance (or maybe there are some implementation specifics to this particular cmdlet).

Related

Faulty PowerShell cmdlets filling up $Error automatic variable

In order to be informed when PowerShell Startup / Logon scripts running on remote computers have bugs, I tend to end scripts with the following:
If ($Error) {
(Code that sends a notification email to system administrators attaching the contents of the $Error variable for troubleshooting)
}
This is a great 'tell tale' to pick up edge cases / bugs. However, I've found some basic built-in PowerShell cmdlets dump data into $Error even on successful runs - for example, try:
$Error.Clear()
Get-NetIPConfiguration
$Error
And you'll see a load of errors in $Error that are not shown during normal output but look like:
Get-NetRoute : No matching MSFT_NetRoute objects found by CIM query for instances of the ROOT/StandardCimv2/MSFT_NetRoute class on the CIM server: SELECT * FROM
MSFT_NetRoute WHERE ((DestinationPrefix LIKE '0.0.0.0/0')) AND ((InterfaceAlias LIKE 'OpenVPN Wintun')). Verify query parameters and retry.
Get-NetConnectionProfile : No MSFT_NetConnectionProfile objects found with property 'InterfaceAlias' equal to 'Local Area Connection'. Verify the value of the property and
retry.
or
$Error.Clear()
Get-NetIPAddress
$Error
will return:
“Infinite : The term '“Infinite' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was
included, verify that the path is correct and try again.
(A nice little bug for Microsoft to solve at some point, doubtless!)
Since it's unlikely that the cmdlets will be fixed any time soon, is there a way to run these cmdlets without them clogging up $Error with their useless information?
This is not a duplicate of Powershell: How can I stop errors from being displayed in a script? since that covers errors that actually display in red on the PowerShell console during a 'failed' run of the cmdlet; this is about errors generated by some cmdlets in the background during an apparently 'successful' run of a cmdlet which for some reason only get written to the automatic $Error variable.
Nonetheless I have already tried a number of solutions suggested in that post:
Running the cmdlets with -ErrorAction Ignore
Running the cmdlets with -ErrorAction SilentlyContinue
Running the cmdlets inside try {} catch {}
Running the cmdlets inside try {} catch {} with -ErrorAction Stop
Running the cmdlets with 2>$null following them
Setting $ErrorActionPreference = "SilentlyContinue" before running the cmdlets
I may be asking the impossible, but the way these cmdlets behave does make $Error very hard to use as an actual log, just want to know if I'm missing a trick.
I would like to be able to encapsulate buggy cmdlets in such a way that 'hidden' errors do not go into the automatic $Error variable.
I agree with #zett42' comment: I think you can't really prevent cmdlets from adding to $Error.
Also knowing that these "phantom errors" might already occur with a simple (Try/Catch) statement like:
Try { 1/0 } Catch {}
Anyways, you might consider to mark the last one and remove the errors added after that/ Like:
$HashCode = if ($Error) { $Error[0].GetHashCode() }
Get-NetIPAddress
While($Error -and $Error[0].GetHashCode() -ne $HashCode) { $Error.RemoveAt(0) }
Use the common -ErrorVariable parameter in order to collect only the (non-terminating) errors directly emitted or intentionally passed through by a cmdlet (those that it internally silences or ignores will not be captured):
# $errs is a self-chosen variable; note that it must be specified WITHOUT $
Get-NetIPAddress -ErrorVariable errs
# $errs now contains any (non-terminating) errors emitted by the
# Get-NetIPAddress call, as a [System.Collections.ArrayList] instance.
# (If no errors occurred, the list is empty).
Note: To also silence errors, combine -ErrorVariable errs with -ErrorAction SilentlyContinue (-ErrorAction SilentlyContinue does not work - see below).
The automatic $Error variable is designed to provide a session-wide log of all errors.
However, (script) cmdlets that deliberately ignore errors can avoid logging unnecessary errors by using -ErrorAction Ignore in internal calls - assuming that the errors are not only to be silenced, but also needn't be inspected.
(If errors need to be inspected after having collected them with -ErrorVariable, use of -ErrorAction Ignore is not an option, because it prevents error collection.)
The CDXML-based cmdlets from the NetTCPIP module, such as Get-NetIPAddress unfortunately use -ErrorAction SilentlyContinue in cases where -ErrorAction Ignore would suffice.
Conceivably, the cmdlet-generation code predates v3 of PowerShell, when the Ignore value was introduced.

PowerShell PSDefaultParamterValues

I came across this private error variable example recently.
#hiding errors:
$ErrorActionPreference = 'SilentlyContinue'
#telling all cmdlets to use a private variable for error logging:
$PSDefaultParameterValues.Add('*:ErrorVariable', '+myErrors')
#initializing the variable:
$myErrors = $null
#do stuff:
Stop-Service -Name Spooler
dir c:\gibtsnichtabcs
#check errors at end USING PRIVATE VARIABLE:
$myErrors
I just want to understand the $PSDefaultParameterValues.Add line above as to how prepending + to the myErrors variable name makes it cumulative. Thanks.
The $PSDefaultParameterValues line causes PowerShell to pass argument -ErrorVariable +myErrors to all cmdlets or advanced functions.
The documentation for the -ErrorVariable common parameter says:
By default, new error messages overwrite error messages that are
already stored in the variable. To append the error message to the
variable content, type a plus sign (+) before the variable name.
So the "cumulative" behaviour is implemented in PowerShell's handling of the common parameters.

PowerShell ignoring Write-Verbose while running Import-Module

For presenting the problem, I have this simple script saved as PowerShell module (test.psm1)
Write-Verbose 'Verbose message'
In real life, it includes command to import additional functions, but that is irrelevant at the moment.
If I run Import-Module .\test.psm1 -Verbose -Force I get only
VERBOSE: Loading module from path 'C:\tmp\test.psm1'.
My Write-Verbose is ignored 😟
I tried adding cmdletbinging but it also did not work.
[cmdletbinding()]
param()
Write-Verbose 'Verbose message'
Any clue how to provide Verbose output while importing the PowerShell module?
P.S. I do not want to display Verbose information always, but only if -Verbose is specified. Here would be my expected output for these two different cases:
PS C:\> Import-Module .\test.psm1 -Verbose -Force # with verbose output
VERBOSE: Loading module from path 'C:\tmp\test.psm1'.
VERBOSE: Verbose message
PS C:\> Import-Module .\test.psm1 -Force # without verbose output
PS C:\>
That is an interesting situation. I have a theory, but if anyone can prove me wrong, I would be more than happy.
The short answer: you probably cannot do what you want by playing with -Verbose only. There may be some workarounds, but the shortest path could be setting $VerbosePreference.
First of all, we need to understand the lifetime of a module when it is imported:
When a module is imported, a new session state is created for the
module, and a System.Management.Automation.PSModuleInfo object is
created in memory. A session-state is created for each module that is
imported (this includes the root module and any nested modules). The
members that are exported from the root module, including any members
that were exported to the root module by any nested modules, are then
imported into the caller's session state. [..] To send output to the host, users should run the Write-Host cmdlet.
The last line is the first hint that pointed me to a solution: when a module is imported, a new session state is created, but only exported elements are attached to the global session state. This means that test.psm1 code is executed in a session different than the one where you run Import-Module, therefore the -Verbose option, related to that single command, is not propagated.
Instead, and this is an assumption of mine, since I did not find it on the documentation, configurations from the global session state are visible to all the child sessions. Why is this important? Because there are two ways to turn on verbosity:
-Verbose option, not working in this case because it is local to the command
$VerbosePreference, that sets the verbosity for the entire session using a preference variable.
I tried the second approached and it worked, despite not being so elegant.
$VerbosePreference = "Continue" # print all the verbose messages, disabled by default
Import-Module .\test.psm1 -Force
$VerbosePreference = "SilentlyContinue" # restore default value
Now some considerations:
Specifying -Verbose on the Import-Module command is redundant
You can still override the verbosity configuration inside your module script, by using
Write-Verbose -Message "Verbose message" -Verbose:$false
As #Vesper pointed out, $false will always suppress the Write-Verbose output. Instead, you may want to parameterized that option with a boolean variable assigned in a previous check, perhaps. Something like:
if (...)
{
$forceVerbose=$true
}
else
{
$forceVerbose=$false
}
Write-Verbose -Message "Verbose message" -Verbose:$forceVerbose
There might be other less invasive workarounds (for instance centered on Write-Host), or even a real solution. As I said, it is just a theory.
Marco Luzzara's answer is spot on (and deserves the bounty in my opinion) in regards to the module being run in its own session state, and that by design you can't access those variables.
An alternative solution to setting $VerbosePreference and restoring it, is to have your module take a parameter specifically for this purpose. You touched on this a little bit by trying to add [CmdletBinding()] to your module; the problem is you have no way to pass in named parameters, only unnamed arguments, via Import-Module -ArgumentList, so you can't specifically pass in a $true for -Verbose.
Instead you can specify your own parameter and use it.
(psm1)
[CmdletBinding()]param([bool]$myverbose)
Write-Verbose "Message" -Verbose:$myverbose
followed with:
Import-Module test.psm1 -Force -ArgumentList $true
In the above example, it would apply only to a specific command, where you were setting -Verbose:$myverbose every time.
But you could apply it to the module's $VerbosePreference:
[CmdletBinding()]param([bool]$myverbose)
$VerbosePreference = if ($myverbose) { 'Continue' } else { 'SilentlyContinue' }
Write-Verbose "Message"
That way it applies throughout.
At this point I should mention the drawback of what I'm showing: you might notice I didn't include -Verbose in the Import-Module call, and that's because, it doesn't change the behavior inside the module. The verbose messages from inside will be shown purely based on the argument you passed in, regardless of the -Verbose setting on Import-Module.
An all-in-one solution then goes back to Marco's answer: manipulating $VerbosePreference on the caller's side. I think it's the only way to get both behaviors aligned, but only if you don't use -Verbose switch on Import-Module to override.
On the other hand, within a scope, like within an advanced function that can take -Verbose, setting the switch changes the local value of $VerbosePreference. That can lead us to wrap Import-Module in our own function:
function Import-ModuleVerbosely {
[CmdletBinding()]
param($Name, [Switch]$Force)
Import-Module $Name -Force:$Force
}
Great! Now we can call Import-ModuleVerbosely test.psm1 -Force -Verbose. But... it didn't work. Import-Module did recognize the verbose setting but it didn't make it down into the module this time.
Although I haven't been able to find a way to see it, I suspect it's because the variable is set to Private (even though Get-Variable seems to say otherwise) and so that value doesn't make it this time. Whatever the reason.. we could go back to making our module accept a value. This time let's make it the same type for ease of use:
(psm1)
[CmdletBinding()]param([System.Management.Automation.ActionPreference]$myverbose)
if ($myverbose) { $VerbosePreference = $myverbose }
Write-Verbose "message"
Then let's change the function:
function Import-ModuleVerbosely {
[CmdletBinding()]
param($Name, [Switch]$Force)
Import-Module $Name -Force:$Force -ArgumentList $VerbosePreference
}
Hey now we're getting somewhere! But.. it's kind of clunky isn't it?
You could go farther with it, making a full on proxy function for Import-Module, then making an alias to it called Import-Module to replace the real one.
Ultimately you're trying to do something not really supported, so it depends how far you want to go.

How can I capture the Powershell informational console output to a variable?

I can't seem to figure out how to capture the quoted text below for parsing. I've tried setting it to a variable as well as redirecting all streams with *>&1 . How can I grab the output text? The *>&1 works for errors, but not for the text below for some reason? Writing to a file is not an option.
Connect-Mailbox -Identity $guidT -user $targetDNT -Alias $aliasT -Database $databaseT -DomainController $domainSpecificDCsSourceAccountT[0]
I've also tried
$outValue = Connect-Mailbox -Identity $guidT -user $targetDNT -Alias $aliasT -Database $databaseT -DomainController $domainSpecificDCsSourceAccountT[0] *>&1
WARNING: The operation completed successfully but the change will not
become effective until Active Directory replication occurs.
[edit]
For repro, this command appears to fall into the same situation, and is easier to setup/configure
$var = Set-Mailbox $sourceUserT -LitigationHoldEnabled $false
[edit2]
Some commands like set-mailbox will work if you change the type from a string to an array such as but connect-mailbox is not able to use that?
So this was kind of dumb luck, it works for the set-mailbox command but not the connect-mailbox command?
PS> Set-Mailbox "first.last" -LitigationHoldEnabled $false -WarningVariable wv
WARNING: The command completed successfully but no settings of 'xxx/last, first' have been modified.
PS> $wv
PS>
However when it did it this way
[System.Collections.ArrayList]$wvar = #();
Set-Mailbox "onprem.detailee1" -LitigationHoldEnabled $false -WarningVariable +wvar
WARNING: The command completed successfully but no settings of 'xxxx/last, first' have been modified.
PS> write-host $wvar
The command completed successfully but no settings of 'xxxx/last, first' have been modified.
So Powershell cannot cast some outputs (warning, error, etc) to a string, however they can add the object to an array. Strangely enough the non-typing portion of the Powershell language is not applicable here.
You should be able to use the -WarningVariable Common Parameter to capture messages destined for the warning stream.
Connect-Mailbox -Identity $guidT -user $targetDNT -Alias $aliasT -Database $databaseT -DomainController $domainSpecificDCsSourceAccountT[0] -WarningVariable WarningVar
$WarningVar
The common parameters include variable options for messages sent to the error, information, and warning streams.
One way I found to do this was to set erroraction explicitly to stop (-ea stop) for the cmdlet in a try catch statement, then I could get the exception value with $_.Exception.Message in the catch statement as it forces the cmdlet to have a terminating error.
This is something I've found to be frustrating coming from c# where a try/catch statement works without having to explicitly define an error as terminating or not. If you do not do that for some cmdlets, the try/catch will not work and it will never catch the terminating error.
Hopefully this helps others, more info
https://www.vexasoft.com/blogs/powershell/7255220-powershell-tutorial-try-catch-finally-and-error-handling-in-powershell
The problem most likely stems from buggy behavior with respect to streams from implicitly remoting commands, such as created via Import-PSSession; this GitHub issue is presumably related.
You have already found one workaround, as demonstrated in your question: if you explicitly create the target variable to pass to -WarningVariable as a System.Collections.ArrayList instance and pass the variable name prefixed with + (-WarningVariable +var) - normally intended for appending to a preexisting variable value - capturing the warning(s) is possible.
A slightly simpler workaround is to call the command via Invoke-Command and apply -WarningVariable to the latter:
Invoke-Command -WarningVariable warnings {
Connect-Mailbox -Identity $guidT -user $targetDNT -Alias $aliasT -Database $databaseT -DomainController $domainSpecificDCsSourceAccountT[0]
}
# $warnings now contains any warnings emitted by the command.
Did you try the following?
$var = myCommand | out-string

Capturing Non-Standard Powershell CmdLet Output for Flow Control

Currently trying to build a script utilizing cmdlets from the MS released "Team Foundation Server Power Tools" package.
I'm attempting to flow command logic from the success or failure of the "Update-TfsWorkspace" cmdlet however I can't seem get a return code out of the call nor can I capture the output using Out-String. I'm using Powershell v1.
update-tfsworkspace "C:\doesnotexist\" -recurse -version T
Yields a message of "Unable to determine the workspace." which is the error I'm trying to catch.
$ret = update-tfsworkspace "C:\doesnotexist\" -recurse -version T
Is expected to give me a $true/$false indicating success/fail but doesn't work.
update-tfsworkspace "C:\doesnotexist\" -recurse -version T | Out-Null
Is expected to prevent the cmdlet from writing the message but doesn't work.
trap{echo "fail"}
update-tfsworkspace $workspace_path -recurse -version T
Is expected to catch an error and write "fail" but doesn't work.
$msg = update-tfsworkspace $workspace_path -recurse -version T | Out-String
Is expected to populate a $msg variable with the host output but doesn't work.
I'm totally out of ideas here. Help would be appreciated!
Little hacky, but since I don't have TFS to try to figure something else out, see if this helps.
I would say that this cmdlet wasn't written correctly. First, since it didn't succeed it should have emitted an error object which would have caused $? to return false which you could have checked or trapped. Second, you can't suppress the error message using -ea 0. It looks like this snapin is using the Host api to write an error string to the host console. That's a spew!! For now, you could do what EBGreen suggests:
$msg = powershell.exe -nologo update-tfsworkspace "C:\doesnotexist\" -recurse -version T 2>&1
Just watch out for all the text your profile script spits out when a new instance of PowerShell starts up.
Your problem here is that the cmdlet is writing an error (Non-Terminating Error), but not throwing an exception (Terminating Error). You can make it throw an exception by adding the ErrorAction parameter:
trap{echo "fail"}
update-tfsworkspace $workspace_path -recurse -version T -ErrorAction "Stop"
This will cause the cmdlet to make all errors terminating (throwing an exception if it writes to the error stream).