Improve output format of caught exception, using Write-Error - powershell

say I have the following script test-exception.ps1
[CmdletBinding()]
param()
process {
1/0
}
if I run it, I get
Tentative de division par zéro.
Au caractère U:\test-exception.ps1:5 : 2
+ 1/0
+ ~~~
+ CategoryInfo : NotSpecified: (:) [], RuntimeException
+ FullyQualifiedErrorId : RuntimeException
if now I try/catch the error,
[CmdletBinding()]
param()
process {
try {
1/0
} catch {
write-error $_
}
}
I get
Au caractère Ligne:1 : 1
+ U:\test-exception.ps1
+ ~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,test-exception.ps1
where I lose the line number and the line text. I tried many variants but the only way I've found to get the first error message in the error stream of a surrounding CMD while try/catching the code (I want to be able to exit with some custom exit code, not 1) is
[CmdletBinding()]
param()
process {
try {
1/0
} catch {
[Console]::Error.WriteLine(($_ | Out-String))
exit 99
}
}
did I miss something or is this the only solution?

As you have noticed, when you call Write-Error in a catch block, the error message only shows the origin of the Write-Error call, not the origin of the exception.
You could get the origin of the exception from the PositionMessage property of ErrorRecord.InvocationInfo, but a far easier way is to just pipe the ErrorRecord object ($_) to Out-String.
In addition you may show the script stack trace by querying the ErrorRecord.ScriptStackTrace property, which contains the chain of function calls that led to the error.
Finally, set the ErrorRecord.ErrorDetails property to override the default error message so all the information about the original error will be shown.
try {
1/0
} catch {
$_.ErrorDetails = ($_ | Out-String) +
"`n------- SCRIPT STACKTRACE -------`n$($_.ScriptStackTrace)" +
"`n `n------- CAUGHT AT -------"
Write-Error $_
}
Output:
C:\Test\ExceptionTest.ps1 : Attempted to divide by zero.
In C:\Test\ExceptionTest.ps1:4 Char:2
+ 1/0
+ ~~~
+ CategoryInfo : NotSpecified: (:) [], RuntimeException
+ FullyQualifiedErrorId : RuntimeException
------- SCRIPT STACKTRACE -------
at <ScriptBlock>, C:\Test\ExceptionTest.ps1: Line 4
at <ScriptBlock>, <no file>: Line 1
------- CAUGHT AT -------
In Line:1 Char:1
+ . 'C:\Test\ExceptionTest.ps1'
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,ExceptionTest.ps1
The information shown after the "CAUGHT AT" line is still pretty useless, but I don't think we can do anything about that, as it is added automatically by PowerShell. At least the relevant information is shown at the top now.

Related

runspace: EndInvoke() fails to return all the scriptblocks output , only the last exception

The script block
$sb = {
write-output "Testing 1"
write-output "Testing 2"
throw " Exception in sb"
}
Calling EndInvoke() only returns the below. My runspace tasks are in some case hours long. I can not lose all the output except the last exception. I do not have control over the script blocks as they get passed into my cmdlets.
How do I resolve that?
Exception calling "EndInvoke" with "1" argument(s): " Exception in sb"
At line:1 char:1
+ $j.PowerShell.EndInvoke($j.Job)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : RuntimeException
By the time you call EndInvoke(), you are already too late. There is no way to receive the data from the output stream as all that data is discarded and the only thing you will get is the thrown exception message. Instead, you have to change how you do your initial BeginInvoke() call to allow you to capture the output.
Using an overloaded BeginInvoke(TInput,TOutput) call, you can both pass your input commands (if needed, or blank), as well as you have to supply a buffer for the output to be stored (of type System.Management.Automation.PSDataCollection[psobject]). So your code looks like this:
$sb = {
write-output "Testing 1"
write-output "Testing 2"
throw " Exception in sb"
}
$PowerShell = [powershell]::Create()
[void]$PowerShell.AddScript($sb)
$InputObject = New-Object 'System.Management.Automation.PSDataCollection[psobject]'
$OutputObject = New-Object 'System.Management.Automation.PSDataCollection[psobject]'
$Handle = $PowerShell.BeginInvoke($InputObject,$OutputObject)
Calling EndInvoke() will give you the error message:
PS C:\> $PowerShell.EndInvoke($Handle)
Exception calling "EndInvoke" with "1" argument(s): " Exception in sb"
At line:1 char:1
+ $PowerShell.EndInvoke($Handle)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : RuntimeException
But the output is stored in the $OutputObject buffer:
PS C:\> $InputObject
PS C:\> $OutputObject
Testing 1
Testing 2

How do I retain ScriptStackTrace in an exception thrown from within an Invoke-Command on a remote computer?

I'm writing a Powershell script which executes one of the steps in my build/deploy process, and it needs to run some actions on a remote machine. The script is relatively complex, so if an error occurs during that remote activity I want a detailed stack trace of where in the script the error occurred (over and above the logging that is already produced).
The problem arises in that Invoke-Command loses stack trace information when relaying terminating exceptions from a remote machine. If a script block is invoked on the local machine:
Invoke-Command -ScriptBlock {
throw "Test Error";
}
The required exception detail is returned:
Test Error
At C:\ScriptTest\Test2.ps1:4 char:2
+ throw "Test Error";
+ ~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (Test Error:String) [], RuntimeException
+ FullyQualifiedErrorId : Test Error
But if run remotely:
Invoke-Command -ComputerName $remoteComputerName -ScriptBlock {
throw "Test Error";
}
The exception stack trace points to the whole Invoke-Command block:
Test Error
At C:\ScriptTest\Test2.ps1:3 char:1
+ Invoke-Command -ComputerName $remoteComputerName -ScriptBlock {
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (Test Error:String) [], RuntimeException
+ FullyQualifiedErrorId : Test Error
I can transport the exception back to the local machine manually:
$exception = Invoke-Command -ComputerName $remoteComputerName -ScriptBlock {
try
{
throw "Test Error";
}
catch
{
return $_;
}
}
throw $exception;
But re-throwing it loses the stack trace:
Test Error
At C:\ScriptTest\Test2.ps1:14 char:1
+ throw $exception;
+ ~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (Test Error:PSObject) [], RuntimeException
+ FullyQualifiedErrorId : Test Error
If I write the exception to Output:
$exception = Invoke-Command -ComputerName $remoteComputerName -ScriptBlock {
try
{
throw "Test Error";
}
catch
{
return $_;
}
}
Write-Output $exception;
I get the correct stack trace information:
Test Error
At line:4 char:3
+ throw "Test Error";
+ ~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (Test Error:String) [], RuntimeException
+ FullyQualifiedErrorId : Test Error
But as it's not on the Error stream it isn't picked up correctly by my build tools. If I try Write-Error, I have a similar problem to re-throwing the exception and the stack trace points to the wrong part of the script.
So my question is - how do I get Powershell to report the exception from a remote machine as if it had been raised locally, with the same stack trace information and on the Error stream?
When you run some code and it fails, you receive an ErrorRecord that reflects the code you (local computer) executed. So when you use throw "error" you can access the invocationinfo and exception for that code.
When you use Invoke-Command, you are not executing throw "error" anymore, the remote computer is. You (local computer) are executing Invoke-Command ...., which is why the ErrorRecord you get reflects that (and not the real exception like you wanted). This is the way it has to be since an exception may be coming from the scriptblock the remote comptuer executed, but it could just as well be an exception from Invoke-Command itself because it couldn't connect to the remote computer or something similar.
When the exception is originally thrown on the remote computer, Invoke-Command/PowerShell throws a RemoteException on the local computer.
#Generate errors
try { Invoke-Command -ComputerName localhost -ScriptBlock { throw "error" } }
catch { $remoteexception = $_ }
try { throw "error" }
catch { $localexception = $_ }
#Get exeception-types
$localexception.Exception.GetType().Name
RuntimeException
$remoteexception.Exception.GetType().Name
RemoteException
This exception-type has a few extra properties, including SerializedRemoteException and SerializedRemoteInvocationInfo which contains the information from the exception that was thrown in the remote session. Using these, you can receive the "internal" exception.
SerializedRemoteException: Gets the original exception that was thrown by the remote instance of Windows PowerShell.
SerializedRemoteInvocationInfo: Gets the invocation information for the remote instance of Windows PowerShell.
Sample:
#Show command that threw error
$localexception.InvocationInfo.PositionMessage
At line:4 char:7
+ try { throw "error" }
+ ~~~~~~~~~~~~~
$remoteexception.Exception.SerializedRemoteInvocationInfo.PositionMessage
At line:1 char:2
+ throw "error"
+ ~~~~~~~~~~~~~
You can then write a simple function to extract the information dynamically, ex:
function getExceptionInvocationInfo ($ex) {
if($ex.Exception -is [System.Management.Automation.RemoteException]) {
$ex.Exception.SerializedRemoteInvocationInfo.PositionMessage
} else {
$ex.InvocationInfo.PositionMessage
}
}
function getException ($ex) {
if($ex.Exception -is [System.Management.Automation.RemoteException]) {
$ex.Exception.SerializedRemoteException
} else {
$ex.Exception
}
}
getExceptionInvocationInfo $localexception
At line:4 char:7
+ try { throw "error" }
+ ~~~~~~~~~~~~~
getExceptionInvocationInfo $remoteexception
At line:1 char:2
+ throw "error"
+ ~~~~~~~~~~~~~
Be aware that the SerializedRemoteExpcetion is shown as PSObject because of the serialization/deserialization during network transfer, so if you're going to check the exception-type you need to extract it from psobject.TypeNames.
$localexception.Exception.GetType().FullName
System.Management.Automation.ItemNotFoundException
$remoteexception.Exception.SerializedRemoteException.GetType().FullName
System.Management.Automation.PSObject
#Get TypeName from psobject
$remoteexception.Exception.SerializedRemoteException.psobject.TypeNames[0]
Deserialized.System.Management.Automation.ItemNotFoundException
I am sure someone with more experience can help but I would like to give you something to chew on in the mean time. Sounds like you want to be using throw since you are looking for terminating exceptions. Write-Error does write to the error stream but it is not terminating. Regardless of your choice there my suggestion is still the same.
Capturing the exception into a variable is a good start for this so I would recommend this block from your example:
$exception = Invoke-Command -ComputerName $remoteComputerName -ScriptBlock {
try
{
throw "Test Error";
}
catch
{
return $_;
}
}
$exception in this case should be a Deserialized.System.Management.Automation.ErrorRecord. You can send custom objects to throw...
You can also throw an ErrorRecord object or a Microsoft .NET Framework exception.
but in this case it does not work which is likely due to the deserialization. At one point I tried to create my own error object but some of the needed properties were read only so I skipped that.
PS M:\Scripts> throw $exception
Test Error
At line:1 char:1
+ throw $return
+ ~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (Test Error:PSObject) [], RuntimeException
+ FullyQualifiedErrorId : Test Error
However just writing to the output stream gives you the correct information as you have seen.
PS M:\Scripts> $exception
Test Error
At line:4 char:9
+ throw "Test Error";
+ ~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (Test Error:String) [], RuntimeException
+ FullyQualifiedErrorId : Test Error
$exception is an object will properties that contain all of this information so a possible way to get what you need would be to make a custom error string from the desired properties. However that turned out to be more work that it was worth. A simple compromise would be to use Out-String to convert that useful output so that it can be returned as an error.
PS M:\Scripts> throw ($return | out-string)
Test Error
At line:4 char:9
+ throw "Test Error";
+ ~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (Test Error:String) [], RuntimeException
+ FullyQualifiedErrorId : Test Error
At line:1 char:1
+ throw ($return | out-string)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (Test Error
At ...Test Error
:String) [], RuntimeException
+ FullyQualifiedErrorId : Test Error
At line:4 char:9
+ throw "Test Error";
+ ~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (Test Error:String) [], RuntimeException
+ FullyQualifiedErrorId : Test Error
So now we have a proper terminating error with information from both relative to the scriptblock and to where in the calling code the error is generated. You will see some repetition obviously but maybe this will be a start for you.
If there is other information you need specifically I would delve into the $exception object with Get-Member to see if you can find something specific that you are looking for. Some notable properties here would be
$exception.ErrorCategory_Reason
$exception.PSComputerName
$exception.InvocationInfo.Line
$exception.InvocationInfo.CommandOrigin
The last one would read something like this.
PS M:\Scripts> $exception.InvocationInfo.PositionMessage
At line:4 char:9
+ throw "Test Error";
+ ~~~~~~~~~~~~~~~~~~
Hoping someone with more experience can comment on this, my comment seems to have gone overlooked.
I found this article which offers some insight into using Enter-PSSession
Or even better, create a persistent session
$session = New-PSSession localhost
Invoke-Command $session {ping example.com}
Invoke-Command $session {$LASTEXITCODE}
To do debugging using tracing, remember that you need to set the VerbosePreference, DebugPreference etc in the remote session
Invoke-Command $session {$VerbosePreference = ‘continue’}
Invoke-Command $session {Write-Verbose hi}

How to get a proper error line number with $ErrorActionPreference = "Stop"

Consider the following script
#requires -version 2.0
[CmdletBinding()]
param
(
)
$script:ErrorActionPreference = "Stop"
Set-StrictMode -Version Latest
function PSScriptRoot { $MyInvocation.ScriptName | Split-Path }
function ThrowFunction($i)
{
"ThrowFunction $i"
$someNonExistingVariable
}
#(1, 2, 3) | ForEach-Object -Process { ThrowFunction $_ }
When we run it we get
C:\dev> .\MyScript.ps1
ThrowFunction 1
ForEach-Object : The variable '$someNonExistingVariable' cannot be retrieved because it has not been set.
At C:\dev\MyScript.ps1:18 char:28
+ #(1, 2, 3) | ForEach-Object <<<< -Process { ThrowFunction $_ }
+ CategoryInfo : InvalidOperation: (someNonExistingVariable:Token) [ForEach-Object], RuntimeException
+ FullyQualifiedErrorId : VariableIsUndefined,Microsoft.PowerShell.Commands.ForEachObjectCommand
As you see it reports the problem in line #18
But the actual problem is in the line #15
I found that if we change Line 8:
$script:ErrorActionPreference = "Continue"
We get
C:\dev> .\MyScript.ps1
ThrowFunction 1
The variable '$someNonExistingVariable' cannot be retrieved because it has not been set.
At C:\dev\MyScript.ps1:15 char:29
+ $someNonExistingVariable <<<<
+ CategoryInfo : InvalidOperation: (someNonExistingVariable:Token) [], RuntimeException
+ FullyQualifiedErrorId : VariableIsUndefined
ThrowFunction 2
The variable '$someNonExistingVariable' cannot be retrieved because it has not been set.
At C:\dev\MyScript.ps1:15 char:29
+ $someNonExistingVariable <<<<
+ CategoryInfo : InvalidOperation: (someNonExistingVariable:Token) [], RuntimeException
+ FullyQualifiedErrorId : VariableIsUndefined
ThrowFunction 3
The variable '$someNonExistingVariable' cannot be retrieved because it has not been set.
At C:\dev\MyScript.ps1:15 char:29
+ $someNonExistingVariable <<<<
+ CategoryInfo : InvalidOperation: (someNonExistingVariable:Token) [], RuntimeException
+ FullyQualifiedErrorId : VariableIsUndefined
And you see that now line 15 is reported as expected.
Now the question is how to get the proper line and have “Stop” behavior.
I tried many approaches and none of them worked for me.
I tried
trap { throw $_ }
trap { $_.InvocationInfo }
trap { Get-PSCallStack }
but none of them gets the proper line
Then I tried to switch
$script:ErrorActionPreference = "Continue"
and found that as soon as I add any trap, the wrong line is being reported again.
So I am still looking for a working solution...
Thanks to #Keith Hill, I found a solution
The magic line is
trap { throw $Error[0] }
This script
#requires -version 2.0
[CmdletBinding()]
param
(
)
$script:ErrorActionPreference = "Stop"
Set-StrictMode -Version Latest
function PSScriptRoot { $MyInvocation.ScriptName | Split-Path }
trap { throw $Error[0] }
function ThrowFunction($i)
{
"ThrowFunction $i"
$someNonExistingVariable
}
#(1, 2, 3) | ForEach-Object -Process { ThrowFunction $_ }
returns
C:\Dev> .\MyScript.ps1
ThrowFunction 1
The variable '$someNonExistingVariable' cannot be retrieved because it has not been set.
At C:\Dev\MyScript.ps1:17 char:29
+ $someNonExistingVariable <<<<
+ CategoryInfo : InvalidOperation: (someNonExistingVariable:Token) [], RuntimeException
+ FullyQualifiedErrorId : VariableIsUndefined
Great!
Even though you've set strictmode to Latest, the $someNonExistingVariable does not generate a terminating error. It just writes to the Error stream. By setting ErrorActionPreference to Stop, you now have Foreach-Object converting the non-terminating error into a terminating error. That is why the error indicates line 18 - the Foreach-Object cmdlet.
If you look in the $Error collection, assuming you Clear() it first, you would see two errors. The latest is in $Error[0] - this is the terminating error thrown by Foreach-Object. $Error[1] is the non-terminating error written by the attempt to access an undefined variable. It has the right line number - 15.
You can access the ScriptStackTrace like so:
PS C:\> $error[0].errorRecord.ScriptStackTrace
at ThrowFunction, C:\Users\hillr\ErrorLine.ps1: line 15
at <ScriptBlock>, C:\Users\hillr\ErrorLine.ps1: line 18
at <ScriptBlock>, C:\Users\hillr\ErrorLine.ps1: line 18
You can you Try {} Catch {} instead :
#requires -version 2.0
[CmdletBinding()]
param
(
)
$script:ErrorActionPreference = "Stop"
Set-StrictMode -Version Latest
function PSScriptRoot { $MyInvocation.ScriptName | Split-Path }
function ThrowFunction($i)
{
"ThrowFunction $i"
Try {
$someNonExistingVariable
}
Catch { # The variable $_ represents the error that is caught
Write-Output $_
}
}
#(1, 2, 3) | ForEach-Object -Process { ThrowFunction $_ }
And the resulting output gives the correct line number :
ThrowFunction 1
The variable '$someNonExistingVariable' cannot be retrieved because it has not been set.
At line:16 char:9
+ $someNonExistingVariable
+ ~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (someNonExistingVariable:String) [], RuntimeException
+ FullyQualifiedErrorId : VariableIsUndefined
ThrowFunction 2
The variable '$someNonExistingVariable' cannot be retrieved because it has not been set.
At line:16 char:9
+ $someNonExistingVariable
+ ~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (someNonExistingVariable:String) [], RuntimeException
+ FullyQualifiedErrorId : VariableIsUndefined
ThrowFunction 3
The variable '$someNonExistingVariable' cannot be retrieved because it has not been set.
At line:16 char:9
+ $someNonExistingVariable
+ ~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (someNonExistingVariable:String) [], RuntimeException
+ FullyQualifiedErrorId : VariableIsUndefined

Get a line number where Write-Error occurred

Consider the script
$someComplexCondition = $false
if ($someComplexCondition)
{
Write-Error -Message "Some complex condition"
}
else
{
Write-Error -Message "Other complex condition"
}
When I run it it says
C:\Dev> .\MyScript.ps1
C:\Dev\MyScript.ps1 : Other complex condition
At line:1 char:15
+ .\MyScript.ps1 <<<<
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,MyScript.ps1
And I noticed that Write-Error always reports itself as at line:1
According to the stack trace it looks like it's line #1 because it was called from the shell and more interesting char:15 because
".\MyScript.ps1 ".Length -eq 15
If we change file name, this char:15 will be changed accordingly.
The question is how to get the actual line when the error occurred.
In our case I would like to get line:9
When there is an actual error, generated by Powershell you get an object of the type ErrorRecord :
$error[0] | Get-Member
TypeName: System.Management.Automation.ErrorRecord
When you use this same ErrorRecord with Write-Error, you don't get an object :
$myError = Write-Error -ErrorRecord $Error[0]
PS C:\> $MyError | Get-Member
gm : You must specify an object for the Get-Member cmdlet.
At line:1 char:12
+ $MyError | gm
+ ~~
+ CategoryInfo : CloseError: (:) [Get-Member], InvalidOperationException
+ FullyQualifiedErrorId : NoObjectInGetMember,Microsoft.PowerShell.Commands.GetMemberCommand
So now, we know that Write-Error will not give us anything that we can reuse later.
You can use the "Throw" statement, instead :
$someComplexCondition = $false
if ($someComplexCondition)
{
Throw "Some complex condition"
}
else
{
Throw "Other complex condition"
}
Then, when you run the script, the error gives you the line number and character number of the start of the "Throw" statement :
C:\Test-Error.ps1
Other complex condition
At C:\Test-Error.ps1:9 char:5
+ Throw "Other complex condition"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (Other complex condition:String) [], RuntimeException
+ FullyQualifiedErrorId : Other complex condition
Here, it is line number 9.

Release file handle on script terminate

How to force powershell to release file handle on script termination CTRL-C. While partially testing script I have to restart powershell in order to execute script again because of file handle. Is there any way to release it when I terminate script?
$outFile = 'testfile'
$stream = [System.IO.StreamWriter] $outFile
$stream.WriteLine("Some txt")
... // here is body of script which can take a lot of time
... // this calls external REST services and output some info to
... // opened file.
$stream.close()
Exit
When I terminate script during working on REST part of script, next time I execute the script I've got this error message:
Cannot convert value "testfile" to type "System.IO.StreamWriter". Error: "The process cannot access the file 'C:\Users\j33nn\testfile' because it is being used by another process.
"At C:\Users\j33nn\Documents\work\testscript.ps1:62 char:44
+ $stream = [System.IO.StreamWriter] $outFile <<<<
+ CategoryInfo : NotSpecified: (:) [], RuntimeException
+ FullyQualifiedErrorId : RuntimeException
You cannot call a method on a null-valued expression.
At C:\Users\j33nn\Documents\work\testscript.ps1:63 char:18
+ $stream.WriteLine <<<< ("Some txt")
+ CategoryInfo : InvalidOperation: (WriteLine:String) [], RuntimeException
+ FullyQualifiedErrorId : InvokeMethodOnNull
You cannot call a method on a null-valued expression.
At C:\Users\j33nn\Documents\work\testscript.ps1:64 char:18
+ $stream.WriteLine <<<< ("")
+ CategoryInfo : InvalidOperation: (WriteLine:String) [], RuntimeException
+ FullyQualifiedErrorId : InvokeMethodOnNull
Wrap file IO statements within a try...finally block and close() and dispose() the streamreader in the finalizer block.