We are currently refactoring our administration scripts.
It had just appeared that a combination of WinRM, error handling and ScriptMethod dramatically decreases available recursion depth.
See the following example:
Invoke-Command -ComputerName . -ScriptBlock {
$object = New-Object psobject
$object | Add-Member ScriptMethod foo {
param($depth)
if ($depth -eq 0) {
throw "error"
}
else {
$this.foo($depth - 1)
}
}
try {
$object.foo(5) # Works fine, the error gets caught
} catch {
Write-Host $_.Exception
}
try {
$object.foo(6) # Failure due to call stack overflow
} catch {
Write-Host $_.Exception
}
}
Just six nested calls are enough to overflow the call stack!
Indeed, more than 200 local nested calls work fine, and without the try-catch the available depth doubles. Regular functions are also not that limited in recursion.
Note: I used recursion only to reproduce the problem, the real code contains many different functions on different objects in different modules. So trivial optimizations as "use functions not ScriptMethod" require architectural changes
Is there a way to increase the available stack size? (I have an administrative account.)
You have two problems that conspire to make this difficult. Neither is most effectively solved by increasing your stack size, if such a thing is possible (I don't know if it is).
First, as you've experienced, remoting adds overhead to calls that reduces the available stack. I don't know why, but it's easily demonstrated that it does. This could be due to the way runspaces are configured, or how the interpreter is invoked, or due to increased bookkeeping -- I don't know the ultimate cause(s).
Second and far more damningly, your method produces a bunch of nested exceptions, rather than just one. This happens because the script method is, in effect, a script block wrapped in another exception handler that rethrows the exception as a MethodInvocationException. As a result, when you call foo(N), a block of nested exception handlers is set up (paraphrased, it's not actually PowerShell code that does this):
try {
try {
...
try {
throw "error"
} catch {
throw [System.Management.Automation.MethodInvocationException]::new(
"Exception calling ""foo"" with ""1"" argument(s): ""$($_.Exception.Message)""",
$_.Exception
)
}
...
} catch {
throw [System.Management.Automation.MethodInvocationException]::new(
"Exception calling ""foo"" with ""1"" argument(s): ""$($_.Exception.Message)""",
$_.Exception
)
}
} catch {
throw [System.Management.Automation.MethodInvocationException]::new(
"Exception calling ""foo"" with ""1"" argument(s): ""$($_.Exception.Message)""",
$_.Exception
)
}
This produces a massive stack trace that eventually overflows all reasonable boundaries. When you use remoting, the problem is exacerbated by the fact that even if the script executes and produces this huge exception, it (and any results the function does produce) can't be successfully remoted -- on my machine, using PowerShell 5, I don't get a stack overflow error but a remoting error when I call foo(10).
The solution here is to avoid this particular deadly combination of recursive script methods and exceptions. Assuming you don't want to get rid of either recursion or exceptions, this is most easily done by wrapping a regular function:
$object = New-Object PSObject
$object | Add-Member ScriptMethod foo {
param($depth)
function foo($depth) {
if ($depth -eq 0) {
throw "error"
}
else {
foo ($depth - 1)
}
}
foo $depth
}
While this produces much more agreeable exceptions, even this can quite quickly run out of stack when you're remoting. On my machine, this works up to foo(200); beyond that I get a call depth overflow. Locally, the limit is far higher, though PowerShell gets unreasonably slow with large arguments.
As a scripting language, PowerShell wasn't exactly designed to handle recursion efficiently. Should you need more than foo(200), my recommendation is to bite the bullet and rewrite the function so it's not recursive. Classes like Stack<T> can help here:
$object = New-Object PSObject
$object | Add-Member ScriptMethod foo {
param($depth)
$stack = New-Object System.Collections.Generic.Stack[int]
$stack.Push($depth)
while ($stack.Count -gt 0) {
$item = $stack.Pop()
if ($item -eq 0) {
throw "error"
} else {
$stack.Push($item - 1)
}
}
}
Obviously foo is trivially tail recursive and this is overkill, but it illustrates the idea. Iterations could push more than one item on the stack.
This not only eliminates any problems with limited stack depth but is a lot faster as well.
Might be worth checking this out if you are overrunning the available memory in your remote session: Running Java remotely using PowerShell
I know it's for running a Java app but the solution updates the max memory available to a remote WinRM session.
Related
Updated Comment: I'm attempting to use PDFCreator to convert pdf files into txt files via PowerShell but it still doesn't seem to be working.
Any help is appreciated!
$PDFCreator = New-Object -ComObject PDFCreator.JobQueue
$PDF = 'C:\Users\userName\Downloads\SampleACORD.pdf'
$TXT = 'C:\Users\userName\Downloads\SampleACORD.txt'
try {
$PDFCreator.initialize()
if($PDFCreator.WaitForJob(5)){
$PJ = $PDFCreator.NextJob
}
if($PJ){
$PJ.PrintFile($PDF)
$PJ.ConvertTo($TXT)
}
} catch {
$_
Break
}
finally {
if($PDFCreator){
$PDFCreator.ReleaseCom()
}
}
You are getting that because $PJ is $null. NextJob isn't returning anything.
To guard against this, WaitForJob(int) returns a bool, $true if a job arrived and $false if not, so you should know after WaitForJob completes whether there is a job to get or not:
if( $PDFCreator.WaitForJob(5) ){
$PJ = $PDFCreator.NextJob
$PJ.allowDefaultPrinterSwitch('C:\Users\userName\Downloads\SampleACORD.txt', $true)
$PJ.ConvertTo($TXT)
} else {
# Handle the no jobs case here
}
You could also do a null check against $PJ before trying to call $PJ.allowDefaultPrinterSwitch:
if( $PJ ){
$PJ.allowDefaultPrinterSwitch('C:\Users\userName\Downloads\SampleACORD.txt', $true)
$PJ.ConvertTo($TXT)
}
Here is some more information on the PDFCreator.JobQueue API, which you may find useful.
To address your issue in the comments, where the file is not being produced, this page of the documentation explains the logical flow of how the conversion process should work:
Call the Initialize() method with your COM Object.
Call WaitForJob(timeOut) if you are waiting for one print job to get in the queue. The parameter timeOut specifies the maximum time the queue waits for the print job to arrive.
Now you are able to get the next job in the queue by calling the property NextJob.
Setup the profile of the job with the method SetProfileByGuid(guid). The guid parameter is used to assign the appropriate conversion profile.
Start the conversion on your print job with ConvertTo(path). The path parameter includes the full path to the location where the converted file should be saved and its full name.
The property IsFinished informs about the conversion state. If the print job is done, IsFinished returns true.
If you want to know whether the job was successfully done, consider the property IsSuccessful. It returns true if the job was converted successfully otherwise false.
In your case, I'm not sure how essential the profile would be, but it does look like your code fails to wait for completion. The following code will wait for the conversion job to finish (and check for success if you need to):
# Wait for completion
while( -Not $PJ.IsFinished ){
Start-Sleep -Seconds 5
}
# Check for success
if( $PJ.IsSuccessful ){
# success case
} else {
# failure case
}
Unrelated, but good practice, wrap your code in a try/finally block, and put your COM release in that block. This way your COM connection closes cleanly even in the event of a terminating error:
$PDFCreator = New-Object -ComObject PDFCreator.JobQueue
try {
# Handle PDF creator calls
} finally {
if( $PDFCreator ){
$PDFCreator.ReleaseCom()
}
}
The finally block is guaranteed to execute before returning to a parent scope, so whether the code succeeds or fails, the finally block will be run.
I have a job that is throwing an exception and I'd like to have the calling process report the exception call stack in a debug log while presenting a useful error to the user.
Contents of test.ps1:
Function foo {
throw "bar"
}
Try {
foo
} catch {
throw
}
Calling code:
Start-job test -filepath test.ps1
Receive-job test
$error[0] | select *
I'd like the output to indicate that the error occurred in test.ps1, but it just says that it occurred in the scriptblock in the exception output for the call stack.
I could see this in 2 ways:
1) updating the call stack shown to indicate the script name instead of just scriptblock, or
2) updating the exception message to have the script name in the exception message while keeping the existing call stack in the exception.
it is not an exact answer, I know.
Here are some throws that helped me a lot so far. See if you can use them:
$PSItem.InvocationInfo
contains additional information collected by PowerShell about the function or script where the exception was thrown. EG:
$PSItem.InvocationInfo | Format-List *
Others:
$PSItem.Exception.StackTrace
$PSItem.Exception.Message
$PSItem.Exception.InnerException
update: to throw a custom message i did:
catch {
$message = "Something bad happened in the inner script. See stack: $PSItem.Exception.InnerException"
throw $message
}
I have an existing PowerShell module that runs against Windows PowerShell 5.1. It depends heavily on the Invoke-WebRequest and Invoke-RestMethod cmdlets which have some fairly significant changes between 5.1 and PowerShell Core 6.
I've already managed to make most of the code work between editions (Desktop/Core), but the last thing I'm having trouble with is handling the exceptions thrown by HTTP failure responses. The existing code looks more or less like this.
try {
$response = Invoke-WebRequest #myparams -EA Stop
# do more stuff with the response
} catch [System.Net.WebException] {
# parse the JSON response body for error details
}
I don't necessarily want to trap any exceptions other than the ones generated by failing HTTP response codes from the server. The exception type returned on Core edition is different than Desktop edition and requires separate code paths to parse the response. So initially I tried this:
try {
$response = Invoke-WebRequest #myparams -EA Stop
# do more stuff with the response
} catch [System.Net.WebException] {
# Desktop: parse the JSON response body for error details
} catch [Microsoft.PowerShell.Commands.HttpResponseException] {
# Core: parse the JSON response body for error details
}
This works fine when running in Core. But when running in Desktop, I get a Unable to find type [Microsoft.PowerShell.Commands.HttpResponseException] error.
What's the best way to work around this? Do I need to just catch all exceptions, string match on the type, and re-throw what I don't want? Is there a more elegant way I'm missing that doesn't involve releasing separate versions of the module for Desktop and Core versions of PowerShell?
I might recommend that you make thin wrapper around the cmdlets that does exception handling, and maybe you create a consistent exception class of your own and each version throws the same one.
At run time, you determine your version/edition, and set an alias to which one you want to use.
Example:
function Invoke-PS51WebRequest {
[CmdletBinding()]
param(...)
try {
Invoke-WebRequest #PSBoundParameters -EA Stop
# return the response
} catch [System.Net.WebException] {
# create a new exception
throw [My.Custom.Exception]$_
}
}
function Invoke-PS6XWebRequest {
[CmdletBinding()]
param(...)
try {
Invoke-WebRequest #PSBoundParameters -EA Stop
# return the response
} catch [System.Net.WebException] {
# Desktop
throw [My.Custom.Exception]$_
} catch [Microsoft.PowerShell.Commands.HttpResponseException] {
# Core
throw [My.Custom.Exception]$_
}
}
switch ($PSVersionTable)
{
{ $_.Is51 } { Set-Alias -Name Invoke-WebRequest -Value Invoke-PS51WebRequest -Force }
{ $_.Is6X } { Set-Alias -Name Invoke-WebRequest -Value Invoke-PS6XWebRequest -Force }
}
try {
Invoke-WebRequest #myParams -ErrorAction Stop
} catch [My.Custom.Exception] {
# do something
}
This needs a lot of work (parsing the exceptions properly, maybe making more than 2 of these variations, determining which platform you're on for real as $PSversionTable.Is51 and .Is6X aren't real, creating your own exception class, creating a proper instance of it instead of casting $_ to it, etc.).
I also demonstrated overriding the actual name Invoke-WebRequest, but I recommend using your own name Invoke-MyCustomWebRequest and using that throughout your code instead. It will keep things more manageable.
I'm wondering why I get the following behaviour when running this script. I have the script loaded in PowerShell ISE (v4 host) and have the Pester module loaded. I run the script by pressing F5.
function Test-Pester {
throw("An error")
}
Describe "what happens when a function throws an error" {
Context "we test with Should Throw" {
It "Throws an error" {
{ Test-Pester } | Should Throw
}
}
Context "we test using a try-catch construct" {
$ErrorSeen = $false
try {
Test-Pester
}
catch {
$ErrorSeen = $true
}
It "is handled by try-catch" {
$ErrorSeen | Should Be $true
}
}
Context "we test using trap" {
trap {
$ErrorSeen = $true
}
$ErrorSeen = $false
Test-Pester
It "is handled by trap" {
$ErrorSeen | Should Be $true
}
}
}
I then get the following output:
Describing what happens when a function throws an error
Context we test with Should Throw
[+] Throws an error 536ms
Context we test using a try-catch construct
[+] is handled by try-catch 246ms
Context we test using trap
An error
At C:\Test-Pester.ps1:2 char:7
+ throw("An error")
+ ~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (An error:String) [], RuntimeException
+ FullyQualifiedErrorId : An error
[-] is handled by trap 702ms
Expected: {True}
But was: {False}
at line: 40 in C:\Test-Pester.ps1
40: $ErrorSeen | Should Be $true
Question
Why is the trap{} apparently not running in the final test?
Here are two solutions to the problem based on the comments/suggested answers from #PetSerAl and #Eris. I have also read and appreciated the answer given to this question:
Why are variable assignments within a Trap block not visible outside it?
Solution 1
Although variables set in the script can be read within the trap, whatever you do within the trap happens to a copy of that variable, i.e. only in the scope that is local to the trap. In this solution we evaluate a reference to $ErrorSeen so that when we set the value of the variable, we are actually setting the value of the variable that exists in the parent scope.
Adding a continue to the trap suppresses the ErrorRecord details, cleaning up the output of the test.
Describe "what happens when a function throws an error" {
Context "we test using trap" {
$ErrorSeen = $false
trap {
Write-Warning "Error trapped"
([Ref]$ErrorSeen).Value = $true
continue
}
Test-Pester
It "is handled by trap" {
$ErrorSeen | Should Be $true
}
}
}
Solution 2
Along the same lines as the first solution, the problem can be solved by explicitly setting the scope of the $ErrorSeen variable (1) when it is first created and (2) when used within the trap {}. I have used the Script scope here, but Global also seems to work.
Same principle applies here: we need to avoid the problem where changes to the variable within the trap only happen to a local copy of the variable.
Describe "what happens when a function throws an error" {
Context "we test using trap" {
$Script:ErrorSeen = $false
trap {
Write-Warning "Error trapped"
$Script:ErrorSeen = $true
continue
}
Test-Pester
It "is handled by trap" {
$ErrorSeen | Should Be $true
}
}
}
According to this blog, you need to tell your trap to do something to the control flow:
The [...] thing you notice is that when you run this as script, you will receive both your error message and the red PowerShell error message.
. 'C:\Scripts\test.ps1'
Something terrible happened!
Attempted to divide by zero.
At C:\Scripts\test.ps1:2 Char:3
+ 1/ <<<< null
This is because your Trap did not really handle the exception. To handle an exception, you need to add the "Continue" statement to your trap:
trap { 'Something terrible happened!'; continue }
1/$null
Now, the trap works as expected. It does whatever you specified in the trap script block, and PowerShell does not get to see the exception anymore. You no longer get the red error message.
I am making several calls to .NET classes in order in my PS script like this:
[class1]::MethodA()
[class1]::MethodB()
if(/*check if last method threw an error*/)
{
"MethodB failed! Exiting."
return
}
[class2]::MethodC()
[class2]::MethodD()
I really want to check whether there was an exception from MethodB before moving on. How do check for this?
Use a try/catch block:
try
{
[class1]::MethodB()
}
catch
{
Write-Host "MethodB failed!"
exit
}