views:

2856

answers:

12

What's the simplest way of blocking a thread until a file has been unlocked and is accessible for reading and renaming? EG: Is there a WaitOnFile() somewhere in the framework?

I have a service that uses a FileSystemWatcher to look for files that are to be transmitted to an FTP site, but the File Created event fires before the other process has finished writing the file.

The ideal solution would have a timeout period so the thread doesn't hang forever before giving up.

Edit: After trying out some of the solutions below, I ended up changing the system so that all files wrote to Path.GetTempFileName(), then performed a File.Move() to the final location. As soon as the FileSystemWatcher event fired, the file was already complete.

+4  A: 

One of the techniques I used some time back was to write my own function. Basically catch the exception and retry using a timer which you can fire for a specified duration. If there is a better way, please share.

Gulzar
+1  A: 

I don't know what you're using to determine the file's lock status, but something like this should do it.

while (true)
{
    try {
        stream = File.Open( fileName, fileMode );
        break;
    }
    catch( FileIOException ) {

        // check whether it's a lock problem

        Thread.Sleep( 100 );
    }
}
harpo
+2  A: 

From MSDN:

The OnCreated event is raised as soon as a file is created. If a file is being copied or transferred into a watched directory, the OnCreated event will be raised immediately, followed by one or more OnChanged events.

Your FileSystemWatcher could be modified so that it doesn't do its read/rename during the "OnCreated" event, but rather:

  1. Spanws a thread that polls the file status until it is not locked (using a FileInfo object)
  2. Calls back into the service to process the file as soon as it determines the file is no longer locked and is ready to go
Guy Starbuck
Spawning the thread of the filesystemwatcher can lead the underlying buffer to overflow, thus missing a lot of changed files. A better approach will be to create a consumer/producer queue.
Nissim
+7  A: 

This was the answer I gave on a related question:

    /// <summary>
    /// Blocks until the file is not locked any more.
    /// </summary>
    /// <param name="fullPath"></param>
    bool WaitForFile(string fullPath)
    {
        int numTries = 0;
        while (true)
        {
            ++numTries;
            try
            {
                // Attempt to open the file exclusively.
                using (FileStream fs = new FileStream(fullPath,
                    FileMode.Open, FileAccess.ReadWrite, 
                    FileShare.None, 100))
                {
                    fs.ReadByte();

                    // If we got this far the file is ready
                    break;
                }
            }
            catch (Exception ex)
            {
                Log.LogWarning(
                   "WaitForFile {0} failed to get an exclusive lock: {1}", 
                    fullPath, ex.ToString());

                if (numTries > 10)
                {
                    Log.LogWarning(
                        "WaitForFile {0} giving up after 10 tries", 
                        fullPath);
                    return false;
                }

                // Wait for the lock to be released
                System.Threading.Thread.Sleep(500);
            }
        }

        Log.LogTrace("WaitForFile {0} returning true after {1} tries",
            fullPath, numTries);
        return true;
    }
Eric Z Beard
I find this ugly but the only possible solution
knoopx
Is this really going to work in the general case? if you open the file in a using() clause, the file is closed and unlocked when the using scope ends. If there is a second process using the same strategy as this (retry repeatedly), then after exit of WaitForFile(), there is a race condition regarding whether the file will be openable or not. No?
Cheeso
Are you talking about 2 threads in the same app calling WaitForFile on the same file? Hmm, not sure, since I mostly use this to wait for other processes to let go of the file. I've had this code in production for a long time and it has worked well for me. Should be pretty simple to write a an app to test your theory.
Eric Z Beard
Bad idea! While the concept is right, a better solution will be to return the FileStream instead of a bool. If the file is locked again before the user got a chance to get his lock on the file - he will get an exception even if the function returned "false"
Nissim
Worked a treat for me.
Aim Kai
I also think this is ugly, and find Fero's answer below to do everything that this one does, elegantly, and in a fraction of the code. Anyone care to comment on any pitfalls in Fero's method? I am about to implement something similar myself.
Mikey Cee
A: 

I do it the same way as Gulzar, just keep trying with a loop.

In fact I don't even bother with the file system watcher. Polling a network drive for new files once a minute is cheap.

Jonathan Allen
+1  A: 

In most cases simple approach like @harpo suggested will work. You can develop more sophisticated code using this approach:

  • Find all opened handles for selected file using SystemHandleInformation\SystemProcessInformation
  • Subclass WaitHandle class to gain access to it's internal handle
  • Pass found handles wrapped in subclassed WaitHandle to WaitHandle.WaitAny method
aku
+3  A: 

For this particular application directly observing the file will inevitably lead to a hard to trace bug, especially when the file size increases. Here are two different strategies that will work.

  • Ftp two files but only watch one. For example send the files important.txt and important.finish. Only watch for the finish file but process the txt.
  • FTP one file but rename it when done. For example send important.wait and have the sender rename it to important.txt when finished.

Good luck!

jms
A: 

This might be a good example of where multiple answers to a question should be accepted in SO. I'm going to try out aku's suggestion to monitor the file handles, but Eric's code sample fits what I originally had in mind.

C. Lawrence Wenham
A: 

How about this as an option:

private void WaitOnFile(string fileName)
{
    FileInfo fileInfo = new FileInfo(fileName);
    for (long size = -1; size != fileInfo.Length; fileInfo.Refresh())
    {
        size = fileInfo.Length;
        System.Threading.Thread.Sleep(1000);
    }
}

Of course if the filesize is preallocated on the create you'd get a false positive.

Ralph Shillington
If the process writing to the file pauses for more than a second, or buffers in memory for more than a second, then you will get another false positive. I don't think this is a good solution under any circumstance.
C. Lawrence Wenham
+2  A: 

I threw together a helper class for these sorts of things. It will work if you have control over everything that would access the file. If you're expecting contention from a bunch of other things, then this is pretty worthless.

using System;
using System.IO;
using System.Threading;

/// <summary>
/// This is a wrapper aroung a FileStream.  While it is not a Stream itself, it can be cast to
/// one (keep in mind that this might throw an exception).
/// </summary>
public class SafeFileStream: IDisposable
{
    #region Private Members
    private Mutex m_mutex;
    private Stream m_stream;
    private string m_path;
    private FileMode m_fileMode;
    private FileAccess m_fileAccess;
    private FileShare m_fileShare;
    #endregion//Private Members

    #region Constructors
    public SafeFileStream(string path, FileMode mode, FileAccess access, FileShare share)
    {
     m_mutex = new Mutex(false, String.Format("Global\\{0}", path.Replace('\\', '/')));
     m_path = path;
     m_fileMode = mode;
     m_fileAccess = access;
     m_fileShare = share;
    }
    #endregion//Constructors

    #region Properties
    public Stream UnderlyingStream
    {
     get
     {
      if (!IsOpen)
       throw new InvalidOperationException("The underlying stream does not exist - try opening this stream.");
      return m_stream;
     }
    }

    public bool IsOpen
    {
     get { return m_stream != null; }
    }
    #endregion//Properties

    #region Functions
    /// <summary>
    /// Opens the stream when it is not locked.  If the file is locked, then
    /// </summary>
    public void Open()
    {
     if (m_stream != null)
      throw new InvalidOperationException(SafeFileResources.FileOpenExceptionMessage);
     m_mutex.WaitOne();
     m_stream = File.Open(m_path, m_fileMode, m_fileAccess, m_fileShare);
    }

    public bool TryOpen(TimeSpan span)
    {
     if (m_stream != null)
      throw new InvalidOperationException(SafeFileResources.FileOpenExceptionMessage);
     if (m_mutex.WaitOne(span))
     {
      m_stream = File.Open(m_path, m_fileMode, m_fileAccess, m_fileShare);
      return true;
     }
     else
      return false;
    }

    public void Close()
    {
     if (m_stream != null)
     {
      m_stream.Close();
      m_stream = null;
      m_mutex.ReleaseMutex();
     }
    }

    public void Dispose()
    {
     Close();
     GC.SuppressFinalize(this);
    }

    public static explicit operator Stream(SafeFileStream sfs)
    {
     return sfs.UnderlyingStream;
    }
    #endregion//Functions
}

It works using a named mutex. Those wishing to access the file attempt to acquire control of the named mutex, which shares the name of the file (with the '\'s turned into '/'s). You can either use Open(), which will stall until the mutex is accessible or you can use TryOpen(TimeSpan), which tries to acquire the mutex for the given duration and returns false if it cannot acquire within the time span. This should most likely be used inside a using block, to ensure that locks are released properly, and the stream (if open) will be properly disposed when this object is disposed.

I did a quick test with ~20 things to do various reads/writes of the file and saw no corruption. Obviously it's not very advanced, but it should work for the majority of simple cases.

Great solution... thanks!
Nissim
A: 

Starting from Eric's answer, I included some improvements to make the code far more compact and reusable. Hope it's useful.

FileStream WaitForFile (string fullPath, FileMode mode, FileAccess access, FileShare share)
{
    for (int numTries = 0; numTries < 10; numTries++) {
        try {
            FileStream fs = new FileStream (fullPath, mode, access, share);

            fs.ReadByte ();
            fs.Seek (0, SeekOrigin.Begin);

            return fs;
        }
        catch (IOException) {
            Thread.Sleep (50);
        }
    }

    return null;
}
mafutrct
+1  A: 

Try to use ReaderWriterLockSlim

Sample usage.

public static class Samle
{
    private static ReaderWriterLockSlim rwl = new ReaderWriterLockSlim();

    public static void WriteLog(string message, string fullFilePath)
    {
        rwl.EnterWriteLock();
        try
        {
            using (StreamWriter w = File.AppendText(fullFilePath))
            {

                // Write here

                w.Close();
            }
        }
        finally
        {
            rwl.ExitWriteLock();
        }
    }
}
Fero
I think this is easily the best answer on this page. Anyone else agree?
Mikey Cee
You can also call the method TryEnterWriteLock and pass a timeout value. This simulates the numTries in the accepted answer above.
Mikey Cee