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 the local computer and open the document for them to edit. Once the user closes the document then we upload it back to the system.
For this process we have been monitoring the file lock on the document. As soon as the file lock is released we upload the document.
The IsFileLocked
method looks like this:
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 loop with a 5 second sleep between attempts. This seems to work great most of the time but occasionally we see an IOException
from this method. I cannot see how it is possible for this exception to be thrown.
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 refers to GetOutlook
which is a different part of the system entirely (unrelated to document handling). There are two code paths into IsFileLocked
and neither are reachable via the GetOutlookInternal
method. It is almost as if the stack is getting corrupt.
Why not use a FileSystemWatcher?
As a side-note, we did consider using a FileSystemWatcher
to monitor file changes but discounted this approach because the user may keep the document open and continue making further changes to it. Our web services unlock the document as soon as we upload it so we cannot do that until the user has completed finished with it.
We are only concerned with documents that are locked by their application. I appreciate there are some applications which do not lock their files but we do not need to consider them here.
The Outlook methods
Below is the GetOutlookInternal
method that appears in the stack - as you can see, it is only dealing with Outlook Interop and is unrelated to the document opening. It does not call into 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;
}
IOException
at least)... can you post the code forGetOutlookInternal
? – Funda