Having worked with Win32 API functions enough in
PowerShell using P/Invoke and reflection, I was constantly annoyed by the fact
that I was often unable to correctly capture the correct error code from a
function that sets its error code (by calling SetLastError) prior to returning
to the caller despite setting SetLastError to True in the DllImportAttribute.
Consider the following, simple code that calls CopyFile
within kernel32.dll:
$MethodDefinition = @'
[DllImport("kernel32.dll", CharSet = CharSet.Unicode,
SetLastError = true)]
public static extern bool CopyFile(string lpExistingFileName,
string lpNewFileName, bool bFailIfExists);
'@
$Kernel32 = Add-Type -MemberDefinition
$MethodDefinition -Name
'Kernel32' -Namespace
'Win32' -PassThru
# Perform an invalid copy
$CopyResult = $Kernel32::CopyFile('C:\foo2', 'C:\foo1', $True)
# Retrieve the last error for CopyFile. The following error is
expected:
# "The system cannot find the file specified"
$LastError = [ComponentModel.Win32Exception][Runtime.InteropServices.Marshal]::GetLastWin32Error()
# An incorrect error is retrieved:
# "The system could not find the environment option that
was entered"
# Grrrrrrrrrrrrrrrrr...
$LastError
I knew that you needed to retrieve the last error code
immediately after a call to a Win32 function so naturally, I would have
expected the correct error code. The one returned was consistently nonsensical,
however. I don’t really know how I thought to try the following but I finally
figured out how to properly capture the correct error code after an unmanaged
function call – capture the error code on the same line (i.e. immediately after a
semicolon). Apparently, the simple act of progressing to the next line in a
PowerShell console is enough for your thread to set a different error code…
The following code demonstrates how to accurately capture
the last set error code:
$MethodDefinition = @'
[DllImport("kernel32.dll", CharSet = CharSet.Unicode,
SetLastError = true)]
public static extern bool CopyFile(string lpExistingFileName,
string lpNewFileName, bool bFailIfExists);
'@
$Kernel32 = Add-Type -MemberDefinition
$MethodDefinition -Name
'Kernel32' -Namespace
'Win32' -PassThru
# Perform an invalid copy
$CopyResult = $Kernel32::CopyFile('C:\foo2', 'C:\foo1', $True);$LastError =
[ComponentModel.Win32Exception][Runtime.InteropServices.Marshal]::GetLastWin32Error()
# The correct error is retrieved:
# "The system cannot find the file specified"
# Yayyyyyyyyyyy....
$LastError
That’s all. I felt it was necessary to share this as I’m
sure others have encountered this issue and were unable to find any solution on the Internet as
it pertained to PowerShell.
Happy New Year!
Tricky!. PowerShell does some work on the main thread in between statements (invoking event subscriber blocks, etc). With this in mind, maybe it's best to just write a C# wrapper around the Win32 method, and have that method be responsible for handling the error code and throwing an exception if needed. Then there's no opportunity for PowerShell to sneak in some background work, even if behavior changes in a future version and this "same line" trick stops working.
ReplyDeleteDepending upon the use case (e.g. needed to stick to pure reflection), implementing a C# wrapper isn't always possible. C# would definitely solve the issue, however. Yep, I can definitely envision those race condition scenario in between statements. Hopefully, this workaround works out for some time. Fingers crossed...
DeleteFinally! Nice work Matt!
ReplyDelete