Friday, January 1, 2016

Properly Retrieving Win32 API Error Codes in PowerShell

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!

3 comments:

  1. 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.

    ReplyDelete
    Replies
    1. Depending 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...

      Delete