Wait until file is unlocked in .NET

Viewed 89532

What's the simplest way of blocking a thread until a file has been unlocked and is accessible for reading and renaming? For example, is there a WaitOnFile() somewhere in the .NET 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.

16 Answers

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

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.

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!

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.

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

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

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

Here is a similar answer to the above except I added a check to see if the file exists.

bool WaitForFile(string fullPath)
        {
            int numTries = 0;
            while (true)
            {
                //need to add this line to prevent infinite loop
                if (!File.Exists(fullPath))
                {
                    _logger.LogInformation("WaitForFile {0} returning true - file does not exist", fullPath);
                    break;
                }
                ++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)
                {
                    _logger.LogInformation(
                       "WaitForFile {0} failed to get an exclusive lock: {1}",
                        fullPath, ex.ToString());

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

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

            _logger.LogInformation("WaitForFile {0} returning true after {1} tries",
                fullPath, numTries);
            return true;
        }

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.

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.

Related