powershell 2.0 redirection file handle exception

2020-02-28 07:35发布

I'm looking for a solution to the The OS handle's position is not what FileStream expected. Do not use a handle simultaneously in one FileStream and in Win32 code or another FileStream. exception that would also work on scripts called within the script containing "the fix".

For the purposes of this question, say that I have two scripts:

foo.ps1

# <fix>
$bindingFlags = [Reflection.BindingFlags] "Instance,NonPublic,GetField"
$objectRef = $host.GetType().GetField( "externalHostRef", $bindingFlags ).GetValue( $host )
$bindingFlags = [Reflection.BindingFlags] "Instance,NonPublic,GetProperty"
$consoleHost = $objectRef.GetType().GetProperty( "Value", $bindingFlags ).GetValue( $objectRef, @() )
[void] $consoleHost.GetType().GetProperty( "IsStandardOutputRedirected", $bindingFlags ).GetValue( $consoleHost, @() )
$bindingFlags = [Reflection.BindingFlags] "Instance,NonPublic,GetField"
$field = $consoleHost.GetType().GetField( "standardOutputWriter", $bindingFlags )
$field.SetValue( $consoleHost, [Console]::Out )
$field2 = $consoleHost.GetType().GetField( "standardErrorWriter", $bindingFlags )
$field2.SetValue( $consoleHost, [Console]::Out )
# </fix>

write-host "normal"
write-error "error"
write-host "yay"
.\bar.ps1

bar.ps1

write-host "normal"
write-error "error"
write-host "yay"

And foo.ps1 is being run like this:

powershell .\foo.ps1 > C:\temp\redirecct.log 2>&1

The expected output should be:

normal
C:\foo.ps1 : error
At line:1 char:10
+ .\foo.ps1 <<<<
    + CategoryInfo          : NotSpecified: (:) [Write-Error], WriteErrorException
    + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,foo.ps1

yay
normal
C:\bar.ps1 : error
At C:\foo.ps1:17 char:6
+ .\bar <<<<  2>&1
    + CategoryInfo          : NotSpecified: (:) [Write-Error], WriteErrorException
    + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,bar.ps1

yay

However, due to the known bug, the output is actually:

normal
C:\foo.ps1 : error
At line:1 char:10
+ .\foo.ps1 <<<< 
    + CategoryInfo          : NotSpecified: (:) [Write-Error], WriteErrorException
    + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,foo.ps1

yay
normal
out-lineoutput : The OS handle's position is not what FileStream expected. Do not use a handle simultaneously in one FileStream and in Win3
2 code or another FileStream. This may cause data loss.
    + CategoryInfo          : NotSpecified: (:) [out-lineoutput], IOException
    + FullyQualifiedErrorId : System.IO.IOException,Microsoft.PowerShell.Commands.OutLineOutputCommand

So the observed behavior is that the changes made by "the fix" aren't being inherited by the 'child' script (bar.ps1, in this case). When bar.ps1 tries to write, it crashes hard. If I don't guard against it somehow in foo.ps1, it will also crash hard. What can I do before/in calling bar.ps1 to prevent bar.ps1 from crashing when it tries to write?

Constraints:

  • Powershell 2.0
  • The script must be run as above
  • I can't modify bar.ps1 (and it should not crash when writing to stderr).

UPDATE

Below is a half-acceptable solution. I say half because it only prevents the 'parent' script from crashing. The 'child' script still fails hard when it tries to write. On the plus side, it can go as far as recognizing that bar failed.

foo.ps1:

function savepowershellfromitself {
    $bindingFlags = [Reflection.BindingFlags] "Instance,NonPublic,GetField"
    $objectRef = $host.GetType().GetField( "externalHostRef", $bindingFlags ).GetValue( $host )
    $bindingFlags = [Reflection.BindingFlags] "Instance,NonPublic,GetProperty"
    $consoleHost = $objectRef.GetType().GetProperty( "Value", $bindingFlags ).GetValue( $objectRef, @() )
    [void] $consoleHost.GetType().GetProperty( "IsStandardOutputRedirected", $bindingFlags ).GetValue( $consoleHost, @() )
    $bindingFlags = [Reflection.BindingFlags] "Instance,NonPublic,GetField"
    $field = $consoleHost.GetType().GetField( "standardOutputWriter", $bindingFlags )
    $field.SetValue( $consoleHost, [Console]::Out )
    $field2 = $consoleHost.GetType().GetField( "standardErrorWriter", $bindingFlags )
    $field2.SetValue( $consoleHost, [Console]::Out )
}

savepowershellfromitself
write-host "normal"
write-error "error"
write-host "yay"
$output = .\bar.ps1 2>&1
savepowershellfromitself
write-host "$output"
if( $errors = $output | ?{$_.gettype().Name -eq "ErrorRecord"} ){
    write-host "there were errors in bar!"
}
write-error "error2"
write-host "done"

1条回答
冷血范
2楼-- · 2020-02-28 08:14

If you do this in foo.ps1 it solves the problem:

# <fix>
$bindingFlags = [Reflection.BindingFlags] "Instance,NonPublic,GetField"
$objectRef = $host.GetType().GetField( "externalHostRef", $bindingFlags ).GetValue( $host )
$bindingFlags = [Reflection.BindingFlags] "Instance,NonPublic,GetProperty"
$consoleHost = $objectRef.GetType().GetProperty( "Value", $bindingFlags ).GetValue(     $objectRef, @() )
[void] $consoleHost.GetType().GetProperty( "IsStandardOutputRedirected", $bindingFlags).GetValue( $consoleHost, @() )
$bindingFlags = [Reflection.BindingFlags] "Instance,NonPublic,GetField"
$field = $consoleHost.GetType().GetField( "standardOutputWriter", $bindingFlags )
$field.SetValue( $consoleHost, [Console]::Out )
$field2 = $consoleHost.GetType().GetField( "standardErrorWriter", $bindingFlags )
$field2.SetValue( $consoleHost, [Console]::Out )
# </fix>

write-host "normal"
write-error "error"
write-host "yay"

powershell .\bar.ps1 2>&1 | more

Piping the output through more hides the fact that it is ultimately going to a file from the child instance of Powershell, bypassing the bug.

In fact, if you create a grandparent script foobar.ps1 which just runs foo.ps1:

powershell .\foo.ps1 2>&1 | more

Then you don't need "the fix" at all, and foo.ps1 can just be

write-host "normal"
write-error "error"
write-host "yay"

.\bar.ps1

because the piping solves the problem for all descendent scripts.

查看更多
登录 后发表回答