2016-05-09 2 views
2

У нас есть приложение Windows Forms, которое подключается к некоторым веб-службам. Он перечисляет документы в системе, и когда пользователь дважды щелкает один, мы загружаем файл на локальный компьютер и открываем документ для редактирования. Как только пользователь закрывает документ, мы загружаем его обратно в систему.Исключение IOException, несмотря на блокирование блокировки IOException

Для этого процесса мы отслеживаем блокировку файла в документе. Как только блокировка файла будет выпущена, мы загрузим документ.

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 для мониторинга изменений файлов, но уклонились от этого подхода, поскольку пользователь может оставить документ открытым и продолжить внесение в него дальнейших изменений. Наши веб-службы разблокируют документ, как только мы его выгружаем, поэтому мы не можем этого сделать, пока пользователь не завершит работу с ним.

Мы имеем дело только с документами, которые заблокированы их применением. Я ценю, что есть некоторые приложения, которые не блокируют их файлы, но мы не должны их рассматривать здесь.

методы Перспективы

Ниже 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; 
    } 
+0

Не можете ли вы отладить и пройти трассировку стека? Это кажется довольно странным, и я сомневаюсь, что трассировка стека будет повреждена (по крайней мере, с 'IOException') ... можете ли вы опубликовать код для' GetOutlookInternal'? – Jcl

+0

Никогда не рекомендуется делиться файлами в окнах. Похоже, вы используете Outlook для доступа к файлам. Я думаю, что oledb используется для просмотра файлов. Oledb не предназначен для работы в многопользовательской среде и неправильно обрабатывает блокировку файлов. Даже если вы можете подумать, что вы блокируете файлы в C#, вы предотвращаете одновременный доступ двух файлов к файлам. это не означает, что другой процесс Windows не может получить доступ к файлу. Используете ли вы какие-либо процессы Async? – jdweng

+0

@Jcl, к сожалению, проблема возникает только на некоторых клиентских ПК, и даже тогда это спорадически. Я не могу воспроизвести в отладчике. Я обновил, чтобы включить метод Outlook. –

ответ

3

я случайно наткнулся на эту статью, которая помогла найти причину моего вопроса: Marshal.GetHRForException does more than just Get-HR-For-Exception

Оказывается, мы два потока, один звали Marshal.GetHRForException(...) на IOException чтобы определить, заблокирован ли файл (код ошибки Win32 32 или 33). Другой поток вызывал Marshal.GetActiveObject(...) для подключения к экземпляру Outlook с помощью Interop.

Если сначала вызывается GetHRForException, а затем GetActiveObject называется вторым, но выбрасывает COMException, тогда вы получаете совершенно неправильное исключение и трассировку стека. Это связано с тем, что GetHRForException эффективно «устанавливает» исключение, а GetActiveObject будет выбрасывать это вместо реального COMException.

Пример кода для воспроизведения:

Эта проблема может быть воспроизведен с помощью следующего кода. Создайте новое консольное приложение, импортируйте ссылку Outlook COM и вставьте код. Убедитесь, что прогноз не работает при запуске приложения:

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: Не используйте Marshal.GetHRForException, если вы также используете COM компонентов.

+0

Спасибо, что вернулись с решением. Я видел подобную вещь, поэтому мой код теперь выглядит так: 'int errorCode = (int) typeof (IOException) .InvokeMember (« HResult », BindingFlags.GetProperty | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public, null, ex , null); ' – adrianm

Смежные вопросы