IOException despite blocking an IOException lock

We have a Windows Forms application that connects to some web services. It lists the documents in the system, and when the user double-clicks one, we upload the file to the local computer and open the document for editing. As soon as the user closes the document, we upload it back to the system.

For this process, we track the file lock in the document. Once the file lock is released, we will upload the document.

The IsFileLocked method is as follows:

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

We call this in a 5 second sleep cycle between attempts. This seems to work most of the time, but sometimes we see an IOException from this method. I do not see how this can be ruled out.

The exception is:

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

The other odd part is the stack trace. This applies to GetOutlook , which is completely different from the system (not related to document processing). There are two code paths in IsFileLocked , and none of them are accessible through the GetOutlookInternal method. It looks like the stack is corrupted.

Why not use FileSystemWatcher ?

As a note, we considered using FileSystemWatcher to monitor file changes, but abandoned this approach because the user can leave the document open and continue making further changes to it. Our web services will unlock the document as soon as we download it, so we won’t be able to do this until the user finishes working with it.

We deal only with documents that are blocked by their application. I appreciate that there are some applications that do not block their files, but we do not need to consider them here.

Outlook Methods

Below is the GetOutlookInternal method, which appears on the stack - as you can see, it works only with Outlook Interop and is not associated with opening a document. It does not call 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; } 
+2
c #
May 09 '16 at 16:26
source share
1 answer

I accidentally stumbled upon this article that helped find the cause of my problem: Marshal.GetHRForException does more than just Get-HR-for-Exception

It turns out we had two threads: one Marshal.GetHRForException(...) on an IOException to determine if the file was locked (Win32 error code 32 or 33). Another thread called Marshal.GetActiveObject(...) to connect to an Outlook instance using Interop.

If GetHRForException is GetHRForException first and then GetActiveObject is thrown second, but throws a COMException , then you get a completely incorrect exception and stack trace. This is because GetHRForException effectively "sets" an exception, and GetActiveObject will GetActiveObject this instead of a real COMException .

Example code for playback:

This problem can be reproduced using the following code. Create a new console application, import the Outlook COM link, and paste the code. Make sure that Outlook does not start when the application starts:

  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 expect to see a 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 that there is a DirectoryNotFoundException exception, and the GetActiveObject called in IsFileLocked is incorrectly offered on the IsFileLocked .

Decision:

The solution to this problem was to simply use the Exception.HResult property instead of GetHRForException . This property was previously protected, but now it is available 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 behavior will be as expected. Now the console 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: Do not use Marshal.GetHRForException if you are also using COM components.

+3
Oct. 25 '16 at 13:58
source share



All Articles