IOException raised despite IOException catch block

后端 未结 1 669
灰色年华
灰色年华 2021-01-07 06:24

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

相关标签:
1条回答
  • 2021-01-07 07:20

    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.

    0 讨论(0)
提交回复
热议问题