尽管存在IOException catch块,仍会引发IOException

时间:2016-05-09 16:26:19

标签: c# .net

我们有一个连接到某些Web服务的Windows窗体应用程序。它列出了系统中的文档,当用户双击文档时,我们将文件下载到本地计算机并打开文档供他们编辑。用户关闭文档后,我们会将其上传回系统。

对于此过程,我们一直在监视文档上的文件锁定。一旦文件锁被释放,我们就上传文件。

IsFileLocked方法如下所示:

private const int ErrorLockViolation = 33;
private const int ErrorSharingViolation = 32;

private static bool IsFileLocked(string fileName)
{
    Debug.Assert(!string.IsNullOrEmpty(fileName));

    try
    {
        if (File.Exists(fileName))
        {
            using (FileStream fs = File.Open(fileName, FileMode.Open, FileAccess.Read, FileShare.None))
            {
                fs.ReadByte();
            }
        }

        return false;
    }
    catch (IOException ex)
    {
        // get the HRESULT for this exception
        int errorCode = Marshal.GetHRForException(ex) & 0xFFFF;

        return errorCode == ErrorSharingViolation || errorCode == ErrorLockViolation;
    }
}

我们称这是一个循环,尝试之间有5秒的睡眠。这似乎在大多数情况下都很好用,但偶尔我们会从这个方法中看到IOException。我无法看到抛出此异常的可能性。

例外是:

IOException: The process cannot access the file 'C:\Users\redacted\AppData\Roaming\redacted\Jobs\09c39a4c-c1a3-4bb9-a5b5-54e00bb6c747\4b5c4642-8ede-4881-8fa9-a7944852d93e\CV abcde abcdef.docx' because it is being used by another process.
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, Int32 bufferSize, FileOptions options, String msgPath, Boolean bFromProxy)
at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share)
at redacted.Helpers.IsFileLocked(String fileName)
at System.Runtime.InteropServices.Marshal.GetActiveObject(Guid& rclsid, IntPtr reserved, Object& ppunk)
at System.Runtime.InteropServices.Marshal.GetActiveObject(String progID)
at redacted.OutlookHelper.GetOutlookInternal()
at redacted.OutlookHelper.GetOutlook()
...

另一个奇怪的部分是堆栈跟踪。这指的是GetOutlook,它完全是系统的不同部分(与文档处理无关)。 IsFileLocked有两个代码路径,并且都不能通过GetOutlookInternal方法访问。这几乎就像堆栈正在腐败一样。

为什么不使用FileSystemWatcher

作为旁注,我们确实考虑使用FileSystemWatcher来监控文件更改,但对此方法打了折扣,因为用户可能会保持文档处于打开状态并继续对其进行进一步更改。我们的网络服务会在我们上传后立即解锁文档,因此我们无法在用户完成文档之前完成此操作。

我们只关注其应用程序锁定的文档。我感谢有些应用程序没有锁定他们的文件,但我们不需要在这里考虑它们。

Outlook方法

下面是堆栈中出现的GetOutlookInternal方法 - 正如您所看到的,它只处理Outlook Interop并且与文档打开无关。它不会调用IsFileLocked

    private static Application GetOutlookInternal()
    {
        Application outlook;

        // Check whether there is an Outlook process running.
        if (Process.GetProcessesByName("OUTLOOK").Length > 0)
        {
            try
            {
                // If so, use the GetActiveObject method to obtain the process and cast it to an Application object.
                outlook = (Application)Marshal.GetActiveObject("Outlook.Application");
            }
            catch (COMException ex)
            {
                if (ex.ErrorCode == -2147221021)    // HRESULT: 0x800401E3 (MK_E_UNAVAILABLE)
                {
                    // Outlook is running but not ready (not in Running Object Table (ROT) - http://support.microsoft.com/kb/238610)
                    outlook = CreateOutlookSingleton();
                }
                else
                {
                    throw;
                }
            }
        }
        else
        {
            // If not running, create a new instance of Outlook and log on to the default profile.
            outlook = CreateOutlookSingleton();
        }
        return outlook;
    }

    private static Application CreateOutlookSingleton()
    {
        Application outlook = new Application();

        NameSpace nameSpace = null;
        Folder folder = null;
        try
        {
            nameSpace = outlook.GetNamespace("MAPI");

            // Create an instance of the Inbox folder. If Outlook is not already running, this has the side
            // effect of initializing MAPI. This is the approach recommended in http://msdn.microsoft.com/en-us/library/office/ff861594(v=office.15).aspx
            folder = (Folder)nameSpace.GetDefaultFolder(OlDefaultFolders.olFolderInbox);
        }
        finally
        {
            Helpers.ReleaseComObject(ref folder);
            Helpers.ReleaseComObject(ref nameSpace);
        }

        return outlook;
    }

1 个答案:

答案 0 :(得分:5)

我偶然发现了这篇文章,这有助于找出问题的原因:Marshal.GetHRForException does more than just Get-HR-For-Exception

事实证明我们有两个线程,一个在IOException上调用Marshal.GetHRForException(...)来确定文件是否被锁定(Win32错误代码32或33)。另一个线程是使用Interop调用Marshal.GetActiveObject(...)连接到Outlook实例。

如果先调用GetHRForException,然后GetActiveObject被调用第二个但抛出COMException,则会得到完全错误的异常和堆栈跟踪。这是因为GetHRForException实际上是"设置"异常和GetActiveObject会抛出异常,而不是真正的COMException

要重现的示例代码:

可以使用以下代码重现此问题。创建一个新的控制台应用程序,导入Outlook COM引用,并粘贴代码。启动应用程序时确保Outlook未运行:

    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;
        }
    }

您希望在控制台中看到COMException,但这就是您所看到的

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()

请注意例外DirectoryNotFoundException,并且堆栈错误地建议将GetActiveObject调入IsFileLocked

<强>解决方案:

此问题的解决方案只是使用Exception.HResult属性而不是GetHRForException。以前这个属性受到保护,但现在可以访问它,因为我们将项目升级到.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
    }
}

通过此更改,行为符合预期。控制台现在显示:

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: 如果您还在使用COM组件,请不要使用Marshal.GetHRForException