How to capture warnings to a log - powershell

I have a script to manage hyper-v virtual machines. When I execute the command to shut down a machine, I need the warning to be saved in the log if it is shut down.
The script I have only captures the errors, I need to get both errors and warnings:
Try {
Stop-VM $Machine -ErrorAction Stop
} Catch {
Write-Host "Error: $_"
Add-Content -Path $Log -Value "`n$_"
}
Thanks!

Assuming that Stop-VM issues non-terminating errors:
Stop-VM $Machine *> output.log
Note: This redirects all of PowerShell's output streams to file output.log, including success output, if any, and it would work with passing an array of VM names in $Machine.
As Abraham Zinala points out, you can selectively capture (some of the) output streams, in variables, using the the common -WarningVariable parameter as well as -ErrorVariable, which you can later send to a file as needed. Note that using these variables still produces the original stream output, but you can silence that with -WarningAction SilentlyContinue and -ErrorAction SilentlyContinue. See the answer linked below for details.
As Santiago Squarzon points out, you could extend your original approach by adding -WarningAction Stop, but the limitation, as
explained in this answer, is that only the first error or warning emitted by the call is then captured, and, perhaps more importantly, the command is terminated at that point - even if multiple VMs were specified.

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.

How to save powershell hyper-v errors to file

I am making a script to turn virtual machines on and off in hyper-v.
Sometimes the Stop-VM command fails and I need to save the bug or reflect it in some way in a log file
I tried putting the command in a trycath but it didn't work.
Command:
Stop-VM $VMapagar
Sometimes the command gives me this error and does not turn off the machine
Stop-VM: Could not stop.
I would like to be able to reflect the failure in some way in a log.txt
Thanks!
Use Try..Catch to trap the error by telling PS to treat it as a terminating error, then process it as you require:
# Rest of your script
Try {
# Run your command, but tell PS to stop if it find an error
# You can explore the effects of the other possible values for -ErrorAction in PS documentation.
Stop-VM $VMpagar -ErrorAction Stop
# If it's got this far, then there can't have been an error so write a success message to console
Write-Host "OK"
}
Catch {
# This code will process if there was an error in the "Try" block
# By default, within the "Catch" block, the "$_" variable contains the error message
Write-Host "Error: $_"
# Write the error to a log file - "`n" tells PS to write a newline before the subsequent text
Add-Content -Path 'c:\temp\log.txt' -Value "`n$_"
# You could stop the script here using "Throw" or "Exit" commands if you want the whole script to stop on ANY error
}
# Your script will continue from this point if you haven't stopped it
Scepticalist's helpful answer shows how to capture a terminating error, by using the common -ErrorAction (-ea) parameter with value 'Stop' in order to promote non-terminating errors (the most common kind) to terminating ones, which allows them to be trapped with a try/ catch / finally statement.
Note that this approach limits you to capturing the first non-terminating error (whereas a single cmdlet call may emit multiple ones), because it - thanks to -ErrorAction Stop - then instantly terminates the statement and transfers control the catch block (where the automatic $_ variable reflects the triggering error in the form of an [ErrorRecord] instance).
Also note that execution continues after a catch block by default - unless you explicitly use throw to re-throw the terminating error (or use a statement such as exit to exit the script).
To capture - potentially multiple - non-terminating errors you have two options:
Redirect them directly to a file, using the redirection operator > with the number of the error stream, 2:
Stop-Vm $vms 2>errs.txt
This sends any errors quietly to file errs.txt; that is, you won't see them in the console. If no errors occur, an empty file is created.
Note: This technique is the only option for directly redirecting an external program's errors (stderr output); however, using redirection 2>&1 you can capture success output (stdout) and errors (stderr) combined, and split them by their source stream later - see the bottom section of this answer.
Use the common -ErrorVariable (-ev) parameter to collect any non-terminating errors in a variable - note that the target variable must be specified without the $:
Stop-Vm $vms -ErrorVariable errs
By default, the errors are still output as well and therefore print to the console (host) by default, but you can add -ErrorAction SilentlyContinue to prevent that. Caveat: Do not use -ErrorAction Ignore, as that will categorically suppress errors and prevent their collection.
You can then inspect the $errs array (list), which is empty if no errors occurred and otherwise contains one or more [ErrorRecord] instances, and send the collected errors to a file on demand; e.g.:
if ($errs) { $errs > errs.txt }
See also:
This answer for information about PowerShell's two fundamental error types.
GitHub docs issue #1583 for a comprehensive overview of PowerShell's surprisingly complex error handling.

Suppress and handle stderr error output in PowerShell script

I wanted to capture SMB shares using PowerShell, but this doesn't work
# Cannot use CIM as it throws up WinRM errors.
# Could maybe use if WinRM is configured on all clients, but this is not a given.
$cim = New-CimSession -ComputerName $hostname
$sharescim = Get-SmbShare -CimSession $cim
So this led me to another method using net view and this is fairly ok if the host is Windows
# This method uses net view to collect the share names (including hidden shares like C$) into an array
# https://www.itprotoday.com/powershell/view-all-shares-remote-machine-powershell
try { $netview = $(net view \\$hostname /all) | select -Skip 7 | ?{$_ -match 'disk*'} | %{$_ -match '^(.+?)\s+Disk*'|out-null ; $matches[1]} }
catch { $netview = "No shares found" }
So, if the host is Linux, I get an error, and as you can see, I am trying above to suppress that error with try / catch, but this fails.
Obviously, this is because 'net view' is CMD so is not controllable by try / catch. So my question is: how can I a) suppress the system error below?, and b) handle this error when it happens (i.e. throw a "This host is not responding to 'net view'" or something instead of the error)?
System error 53 has occurred.
The network path was not found.
Stderr (standard error) output from external programs is not integrated with PowerShell's error handling, primarily because this stream is not only used to communicate errors, but also status information.
(You should therefore only infer success vs. failure of an external-program call from its exit code, as reflected in $LASTEXTICODE[1]).
However, you can redirect stderr output, and redirecting it to $null (2>$null) silences it[2]:
$netview = net view \\$hostname /all 2>$null | ...
if (-not $netview) { $netview = 'No shares found' }
[1] Acting on a nonzero exit code, which by convention signals failure, is also not integrated into PowerShell's error handling as of v7.1, but fixing that is being proposed in this RFC.
[2] Up to PowerShell 7.1.x, any 2> redirection unexpectedly also records the stderr lines in the automatic $Error collection. This problem has been corrected in v7.1
As an unfortunate side effect, in versions up to v7.0, a 2> redirection can also throw a script-terminating error if $ErrorActionPreference = 'Stop' happens to be in effect, if at least one stderr line is emitted.

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

PowerShell Receive-Job Output To Variable Or File But Not Screen

I am trying to get the job details without outputting the data to the screen. However, regardless of what option I try, the job logs always get sent to the console. Any ideas on how to save the logs in a variable or file without outputting that data to console?
Receive-Job -Id $id -Keep -ErrorAction Continue > C:\Temp\Transcript-$VM.txt
$info = Receive-Job -Id $id -Keep -ErrorAction Continue
You state that your job uses Write-Host output and that you're running Windows PowerShell v5.1.
In order to also capture Write-Host output - which in v5+ is sent to the information stream (stream number 6) - use redirection 6>&1:
# Capture both success output and information-stream output
# (Write-Host) output in $info.
$info = Receive-Job -Id $id -Keep -ErrorAction Continue 6>&1
Unfortunately, due to a known bug, you'll still get console output as well (bug is still present in PowerShell Core 7.0.0-preview.5).
Catch-all redirection *>&1 normally routes all streams through the success output stream.
Unfortunately, due to the bug linked to above, the following streams cannot be captured or redirected at all when using background jobs or remoting:
verbose messages (4)
debug messages (5)
The only workaround is to capture the streams inside the job and save them to a file from there, and then access the files from the caller later.
Of course, this requires that you have control over how the jobs are created.
A simplified example:
# Redirect all output streams *inside* the job to a file...
Start-Job {
& {
# The job's commands go here.
# Note that for any *verbose* output to be captured,
# verbose output must explicitly turned on, such as with
# the -Verbose common parameter here.
# You can also set $VerbosePreference = 'Continue', which
# cmdlets (including advanced functions/scripts) will honor.
'success'; write-verbose -Verbose 'verbose'; write-host 'host'
} *> $HOME/out.txt
} | Receive-Job -Wait -AutoRemove
# ... then read the resulting file.
Get-Content $HOME/out.txt
Note that I've used a full path as the redirection target, because, unfortunately, in v6- versions of PowerShell script blocks executed in background jobs do not inherit the caller's current location. This will change in PowerShell Core v7.0.
Try placing it in a pipeline, and see if that works:
Receive-Job -Id $id -Keep -ErrorAction Continue | Set-Content 'C:\Temp\Transcript-$VM.txt'