尽管有IOException catch块,但仍引发IOException

Als*_*sty 2 .net c#

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

在此过程中,我们一直在监视文档上的文件锁定。释放文件锁后,我们就会上载文档。

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

我们将其称为循环,两次尝试之间有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()
...
Run Code Online (Sandbox Code Playgroud)

另一个奇怪的部分是堆栈跟踪。这指的GetOutlook是完全是系统的另一部分(与文档处理无关)。该方法有两个代码路径,IsFileLocked并且均无法通过该GetOutlookInternal方法到达。几乎就像堆栈已损坏一样。

为什么不使用FileSystemWatcher

附带说明一下,我们确实考虑过使用a FileSystemWatcher来监视文件更改,但是由于用户可以保持文档打开并继续对其进行进一步更改,因此不建议使用此方法。上载文档后,我们的Web服务便会对其进行解锁,因此只有在用户完成文档处理后,我们才能这样做。

我们只关心被其应用程序锁定的文档。我很欣赏有些应用程序不锁定其文件,但是我们在这里不需要考虑它们。

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;
    }
Run Code Online (Sandbox Code Playgroud)

Als*_*sty 5

我无意间偶然发现了这篇文章,该文章有助于查找我的问题的原因:Marshal.GetHRForException所做的不只是Get-HR-For-Exception

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

如果GetHRForException先调用if ,然后再GetActiveObject调用if ,但抛出COMException,则您将得到完全错误的异常和堆栈跟踪。这是因为GetHRForException有效地“设置”了异常GetActiveObject并将抛出该异常而不是real 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;
        }
    }
Run Code Online (Sandbox Code Playgroud)

您可能希望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()
Run Code Online (Sandbox Code Playgroud)

请注意异常情况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
    }
}
Run Code Online (Sandbox Code Playgroud)

进行此更改后,行为将达到预期的效果。控制台现在显示:

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()
Run Code Online (Sandbox Code Playgroud)

TL; DR:Marshal.GetHRForException如果还使用COM组件,则不要使用。

  • 感谢您回来提供解决方案。我见过类似的事情,所以我的代码现在看起来像: `int errorCode = (int)typeof(IOException).InvokeMember("HResult", BindingFlags.GetProperty|BindingFlags.Instance|BindingFlags.NonPublic|BindingFlags.Public, null, ex ,空);` (2认同)