6

I need to open a file but if it's currently not available I need to wait until it's ready. What's the best approach to take?

SCENARIO

I'm using files as a persistent caching mechanism for application data. This data needs to be read and deserialized often (written only once, and deleted occasionally). I have a cleanup process that runs on a separate thread that determines which files are no longer needed and deletes them. Opening and reading of files may happen concurrently (rarely, but could happen) and I want the process to wait and try to read the data again.

Thanks!

Micah
  • 111,873
  • 86
  • 233
  • 325

4 Answers4

9

I'm not a huge fan of the try/catch IOException because:

  1. The reason for the exception is unknown.
  2. I dislike 'expected' exceptions as I often run with break on exception.

You can do this without exceptions by calling CreateFile and returning a stream when/if it finally returns a handle:

public static System.IO.Stream WaitForExclusiveFileAccess(string filePath, int timeout)
{
    IntPtr fHandle;
    int errorCode;
    DateTime start = DateTime.Now;

    while(true)
    {
        fHandle = CreateFile(filePath, EFileAccess.GenericRead | EFileAccess.GenericWrite, EFileShare.None, IntPtr.Zero,
                             ECreationDisposition.OpenExisting, EFileAttributes.Normal, IntPtr.Zero);

        if (fHandle != IntPtr.Zero && fHandle.ToInt64() != -1L)
            return new System.IO.FileStream(fHandle, System.IO.FileAccess.ReadWrite, true);

        errorCode = Marshal.GetLastWin32Error();

        if (errorCode != ERROR_SHARING_VIOLATION)
            break;
        if (timeout >= 0 && (DateTime.Now - start).TotalMilliseconds > timeout)
            break;
        System.Threading.Thread.Sleep(100);
    }


    throw new System.IO.IOException(new System.ComponentModel.Win32Exception(errorCode).Message, errorCode);
}

#region Win32
const int ERROR_SHARING_VIOLATION = 32;

[Flags]
enum EFileAccess : uint
{
    GenericRead = 0x80000000,
    GenericWrite = 0x40000000
}

[Flags]
enum EFileShare : uint
{
    None = 0x00000000,
}

enum ECreationDisposition : uint
{
    OpenExisting = 3,
}

[Flags]
enum EFileAttributes : uint
{
    Normal = 0x00000080,
}

[DllImport("kernel32.dll", EntryPoint = "CreateFileW", SetLastError = true, CharSet = CharSet.Unicode)]
static extern IntPtr CreateFile(
   string lpFileName,
   EFileAccess dwDesiredAccess,
   EFileShare dwShareMode,
   IntPtr lpSecurityAttributes,
   ECreationDisposition dwCreationDisposition,
   EFileAttributes dwFlagsAndAttributes,
   IntPtr hTemplateFile);

#endregion
InteXX
  • 6,135
  • 6
  • 43
  • 80
csharptest.net
  • 62,602
  • 11
  • 71
  • 89
4

The more generic version of the csharptest.net's method could look like this (also, used SafeFileHandle and removed exception throwing on timeout, you can get enum values at http://www.pinvoke.net/default.aspx/kernel32.createfile):

    public static FileStream WaitForFileAccess(string filePath, FileMode fileMode, FileAccess access, FileShare share, TimeSpan timeout)
    {
        int errorCode;
        DateTime start = DateTime.Now;

        while (true)
        {
            SafeFileHandle fileHandle = CreateFile(filePath, ConvertFileAccess(access), ConvertFileShare(share), IntPtr.Zero,
                                                   ConvertFileMode(fileMode), EFileAttributes.Normal, IntPtr.Zero);

            if (!fileHandle.IsInvalid)
            {
                return new FileStream(fileHandle, access);
            }

            errorCode = Marshal.GetLastWin32Error();

            if (errorCode != ERROR_SHARING_VIOLATION)
            {
                break;
            }

            if ((DateTime.Now - start) > timeout)
            {
                return null; // timeout isn't an exception
            }

            Thread.Sleep(100);
        }

        throw new IOException(new Win32Exception(errorCode).Message, errorCode);
    }

    private static EFileAccess ConvertFileAccess(FileAccess access)
    {
        return access == FileAccess.ReadWrite ? EFileAccess.GenericRead | EFileAccess.GenericWrite : access == FileAccess.Read ? EFileAccess.GenericRead : EFileAccess.GenericWrite;
    }

    private static EFileShare ConvertFileShare(FileShare share)
    {
        return (EFileShare) ((uint) share);
    }

    private static ECreationDisposition ConvertFileMode(FileMode mode)
    {
        return mode == FileMode.Open ? ECreationDisposition.OpenExisting : mode == FileMode.OpenOrCreate ? ECreationDisposition.OpenAlways : (ECreationDisposition) (uint) mode;
    }

    [DllImport("kernel32.dll", EntryPoint = "CreateFileW", SetLastError = true, CharSet = CharSet.Unicode)]
    private static extern SafeFileHandle CreateFile(
       string lpFileName,
       EFileAccess dwDesiredAccess,
       EFileShare dwShareMode,
       IntPtr lpSecurityAttributes,
       ECreationDisposition dwCreationDisposition,
       EFileAttributes dwFlagsAndAttributes,
       IntPtr hTemplateFile);
Gman
  • 1,781
  • 1
  • 23
  • 38
2

It depends on who controls the file. If one part of your application needs to wait until another part of the application finishes preparing the file, then you can use a ManualResetEvent. That is, at startup, your program creates a new event:

public ManualResetEvent FileEvent = new ManualResetEvent(false);

Now, the part of the program that's waiting for the file has this code:

FileEvent.WaitOne();

And the part of the program that's creating the file does this when the file's ready:

FileEvent.Set();

If your application has to wait for a file that is being used by another application that you have no control over, your only real solution is to continually try to open the file.

FileStream f = null;
while (f == null)
{
    try
    {
        f = new FileStream(...);
    }
    catch (IOException)
    {
        // wait a bit and try again
        Thread.Sleep(5000);
    }
}

Of course, you probably wouldn't want to unconditionally catch IOException. You'd likely want to catch the specific exceptions that you know how to handle (for example, you wouldn't want to try again if you got a DirectoryNotFoundException). The I/O functions document which exceptions they're expected to throw, and under what circumstances.

Jim Mischel
  • 131,090
  • 20
  • 188
  • 351
0

Like all "what is the best approach" questions, this one depends on your needs. Some options that come easily to mind:

  1. Abort the attempt
  2. Loop until the file becomes unlocked
  3. Ask the user what to do about it

Which one you chose depends on how you can deal with it.

Tergiver
  • 14,171
  • 3
  • 41
  • 68
  • 1
    1) is not a very effective means of waiting for the lock to become available. 3) is not a programmatic solution, though perhaps the correct one. And 2) is, I suspect, the default solution upon which Micah is trying to improve. – Kirk Woll Sep 27 '10 at 23:00
  • I would point out that the original question looked nothing like its present form. – Tergiver Sep 28 '10 at 00:11