2

I need to be able to open a file to read it, maintaining a lock that denies other instances of the same application write access until I have written the amended file back to disk. The file is in a shared location on a network, and the app instances can be on any machine on the network.

I have tried using a FileStream with FileAccess.ReadWrite and FileShare.Read, with a Streamreader to read and then a StreamWriter (on the same FileStream) to write, but this corrupts the file. Other permutations of the FileAccess and FileShare don't seem to solve my basic problem either.

So I tried closing the StreamReader before opening the StreamWriter, but that changes the CanRead and CanWrite properties of the FileStream, so I still can't write.

Clearly I am taking the wrong approach, so can someone tell me how I should be approaching this? It seems a common enough thing to want to do - edit a file and block write access until the edited file is saved.

Yuval Itzchakov
  • 146,575
  • 32
  • 257
  • 321
haughtonomous
  • 4,602
  • 11
  • 34
  • 52

3 Answers3

0

If you want to write to a file, you need to take exclusive file access, otherwise other programs can read partially written data (your writes aren't atomic). There are solutions to this, but they are quite complex.

A solution could be something like this:

static bool Read(FileStream fs, byte[] data, long position)
{
    fs.Seek(position, SeekOrigin.Begin);

    if (fs.ReadByte() == 0)
    {
        // Block of data not finished writing
        return false;
    }

    fs.Read(data, 0, data.Length);
    return true;
}

static bool Write(FileStream fs, byte[] data, long position)
{
    try
    {
        fs.Lock(position, data.Length + 1);
        fs.Seek(position, SeekOrigin.Begin);
        fs.WriteByte(0);
        fs.Write(data, 0, data.Length);
        fs.Seek(position, SeekOrigin.Begin);
        fs.WriteByte(1);
        fs.Unlock(position, data.Length + 1);
        return true;
    }
    catch (IOException)
    {
        return false;
    }
}

static bool Append(FileStream fs, byte[] data)
{
    return Write(fs, data, fs.Length);
}

where you always keep open the FileStream as

FileStream fs1 = new FileStream("Test.txt", FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);

Before the data there is a "guard byte" that tells if the data is being written. If it's being written then reads on it will fail. The file is locked "where it's needed" using FileStream.Lock.

This clearly works better with binary fixed-length data. If you have variable length data (or you need to atomically update more "regions" of a file) then it becomes more complex. Normally you use DBs for this reason :-)

xanatos
  • 109,618
  • 12
  • 197
  • 280
  • Thank. In the end I adopted a 'lock file' approach. I know that this still isn't atomic, but I figure that the risk of a collision is small. – haughtonomous Oct 28 '11 at 09:29
  • Thanks. In the end I adopted a 'lock file' approach. I know that this still isn't atomic, but I figure that the risk of a collision is small. eg test if lock file exists. If it does, block editing operation. If it doesn't, create the lock file, do the editing, save the file then delete the lock file. There is a potential window between testing for the lock file and creating it in which another user could potentially save over the first user's changes, but the window is small and this is an occasional operation so I'm not too worried about that. – haughtonomous Oct 28 '11 at 09:59
  • Alternatively, how do I take exclusive access so the first user can read/write? – haughtonomous Oct 28 '11 at 09:59
  • It isn't clear if you are using a "binary" file or a "text" file. – xanatos Oct 29 '11 at 05:31
  • @haughtonomous Written an example – xanatos Oct 29 '11 at 05:46
0

Keep the file open with write access. That should deny all other writes to the file.

Cees Timmerman
  • 17,623
  • 11
  • 91
  • 124
  • Yes, but I need others to be able to read the file in the meantime. It's necessary for the app to start up. – haughtonomous Oct 28 '11 at 09:38
  • new FileStream(file_name, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read); – Cees Timmerman Oct 28 '11 at 10:19
  • Thanks. Unfortunately that prevents me from reading the file. I need to read then write, all without allowing instances of the app write access (but they should have read-only access) – haughtonomous Oct 28 '11 at 10:45
  • FileAccess.ReadWrite then. The readers can check sentinels (e.g. newline) and use Seek() in case of incomplete data. – Cees Timmerman Oct 28 '11 at 10:56
0

As you don't specify what OS you're using nor whether access is time critical or order critical then I think that your best bet is to use a directory to indicate that the file is locked.

On most OS, creating a directory is pretty much instant, whereas creating a file isn't. So each time your application wants access to the data file in question it must try and create a subdirectory in the same location as the file. If the directory already exists then the create will fail and your application must wait, trying again until it succeeds.

When it succeeds in creating the directory it can access the file and write it. Once all data has been written and file access closed then it should wait for a bit (to allow the actual write to complete) before deleting the directory.

You will, however, need to write some code that can handle an instance of your application crashing and leaving the lock directory present.

ChrisBD
  • 9,104
  • 3
  • 22
  • 35
  • This is Windows XP and 7. Do the same comments apply re speed of creation? I thought that a directory was simply a special kind of file (or is that Unix?) – haughtonomous Oct 28 '11 at 09:36
  • Interestingly I have just written a little test, creating 100 files (File.Create(...)) and creating 100 directories (Directory.CreateDirectory(...)), and the files took 1/4 of the time taken to create the directories. Files = 4 times the speed of directories. This seems at odds with your assertion about relative speed. – haughtonomous Oct 28 '11 at 09:58