We have a Windows Forms application which connects to some web services. It lists the documents in the system, and when the user double-clicks one we download the file to th
I accidentally stumbled across this article which helped to find the cause of my issue: Marshal.GetHRForException does more than just Get-HR-For-Exception
It turns out we had two threads, one was calling Marshal.GetHRForException(...) on an IOException
to determine if a file is locked (Win32 error code 32 or 33). Another thread was calling Marshal.GetActiveObject(...) to connect to an Outlook instance using Interop.
If GetHRForException
is called first, and then GetActiveObject
is called second but throws a COMException
, then you get completely the wrong exception and stack trace. This is because GetHRForException
is effectively "setting" the exception and GetActiveObject
will throw that instead of the real COMException
.
Example code to reproduce:
This issue can be reproduced using the following code. Create a new console application, import the Outlook COM reference, and paste in the code. Ensure Outlook is not running when you start the application:
public static void Main(string[] args)
{
bool isLocked = IsFileLocked();
Console.WriteLine("IsLocked = " + isLocked);
ShowOutlookWindow();
}
private static bool IsFileLocked()
{
try
{
using (FileStream fs = File.Open(@"C:\path\to\non_existant_file.docx", FileMode.Open, FileAccess.Read, FileShare.None))
{
fs.ReadByte();
return false;
}
}
catch (IOException ex)
{
int errorCode = Marshal.GetHRForException(ex) & 0xFFFF;
return errorCode == 32 || errorCode == 33; // lock or sharing violation
}
}
private static void ShowOutlookWindow()
{
try
{
Application outlook = (Application)Marshal.GetActiveObject("Outlook.Application");
// ^^ causes COMException because Outlook is not running
MailItem mailItem = outlook.CreateItem(OlItemType.olMailItem);
mailItem.Display();
}
catch (System.Exception ex)
{
Console.WriteLine(ex);
throw;
}
}
You would expect to see the COMException
in the console, but this is what you see
IsLocked = False
System.IO.DirectoryNotFoundException: Could not find a part of the path 'C:\path\to\non_existant_file.docx'.
at System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)
at System.IO.FileStream.Init(String path, FileMode mode, FileAccess access, Int32 rights, Boolean useRights, FileShare share, Int32 bufferSize, FileOptions options, SECURITY_ATTRIBUTES secAttrs, String msgPath, Boolean bFromProxy, Boolean useLongPath, Boolean checkHost)
at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share)
at System.IO.File.Open(String path, FileMode mode, FileAccess access, FileShare share)
at MyProject.Program.IsFileLocked()
at System.Runtime.InteropServices.Marshal.GetActiveObject(Guid& rclsid, IntPtr reserved, Object& ppunk)
at System.Runtime.InteropServices.Marshal.GetActiveObject(String progID)
at MyProject.Program.ShowOutlookWindow()
Note how the exception is DirectoryNotFoundException
, and the stack incorrectly suggests GetActiveObject
called into IsFileLocked
.
Solution:
The solution to this problem was simply to use the Exception.HResult property instead of GetHRForException
. Previously this property was protected but it is now accessible since we upgraded the project to .NET 4.5
private static bool IsFileLocked()
{
try
{
using (FileStream fs = File.Open(@"C:\path\to\non_existant_file.docx", FileMode.Open, FileAccess.Read, FileShare.None))
{
fs.ReadByte();
return false;
}
}
catch (IOException ex)
{
int errorCode = ex.HResult & 0xFFFF;
return errorCode == 32 || errorCode == 33; // lock or sharing violation
}
}
With this change, the behaviour is as expected. The console now shows:
IsLocked = False
System.Runtime.InteropServices.COMException (0x800401E3): Operation unavailable (Exception from HRESULT: 0x800401E3 (MK_E_UNAVAILABLE))
at System.Runtime.InteropServices.Marshal.GetActiveObject(Guid& rclsid, IntPtr reserved, Object& ppunk)
at System.Runtime.InteropServices.Marshal.GetActiveObject(String progID)
at MyProject.Program.ShowOutlookWindow()
TL;DR: Don't use Marshal.GetHRForException
if you are also using COM components.