tags:

views:

452

answers:

4

I have a library that handles reading and writing a cache file. This library is used by a Windows Service and several instances of a console application on the same machine. The console application runs when a user logs in.

I am getting occasional IO errors saying the cache file is in use by another process. I assume that collisions are occurring between the different application instances and service trying to read and write at the same time.

Is there a way to lock the file when it is in use and force all other requests to "wait in line" to access the file?

    private void SaveCacheToDisk(WindowsUser user) {
        string serializedCache = SerializeCache(_cache);
        //encryt
        serializedCache = AES.Encrypt(serializedCache);

        string path = user == null ? ApplicationHelper.CacheDiskPath() :
            _registry.GetCachePath(user);
        string appdata = user == null ? ApplicationHelper.ClientApplicationDataFolder() :
            _registry.GetApplicationDataPath(user);

        if (Directory.Exists(appdata) == false) {
            Directory.CreateDirectory(appdata);
        }

        if (File.Exists(path) == false) {
            using (FileStream stream = File.Create(path)) { }
        }

        using (FileStream stream = File.Open(path, FileMode.Truncate)) {
            using (StreamWriter writer = new StreamWriter(stream)) {
                writer.Write(serializedCache);
            }
        }
    }

    private string ReadCacheFromDisk(WindowsUser user) {
        //cache file path
        string path = user == null ? ApplicationHelper.CacheDiskPath() :
            _registry.GetCachePath(user);

        using (FileStream stream = File.Open(path, FileMode.Open)) {
            using (StreamReader reader = new StreamReader(stream)) {
                string serializedCache = reader.ReadToEnd();
                //decrypt
                serializedCache = AES.Decrypt(serializedCache);

                return serializedCache;
            }
        }
    }
+5  A: 

Sure, you could use a mutex and permit access only when holding the mutex.

Jason
A: 

Check out the TextWriter.Synchronized method.

http://msdn.microsoft.com/en-us/library/system.io.textwriter.synchronized.aspx

This should let you do this:

TextWriter.Synchronized(writer).Write(serializedCache);
Michael Meadows
+1: This looks like it does exactly what the OP needs. In fact, the MSDN on the `StreamWriter` class explicitly suggests replacing `StreamWriter` with `TextWriter.Synchronized` if thread-safe output is needed.
Brian
Synchronized is a static method.
Michael Meadows
I believe this will only work for *in-process* synchronization. The question is actually about *inter-process* synchronization, and only a Mutex has OS-level scope.
Aaronaught
This did not work. I applied it to every StreamReader and StreamWriter. Thanks for the suggestion though.
modernzombie
@Aaronaught: Oh yeah, you're right. I wasn't paying careful enough attention to what the OP wanted.
Brian
+1  A: 

You could use a cross-process EventWaitHandle. This lets you create and use a WaitHandle that's identified across processes by name. A thread is notified when it's its turn, does some work, and then indicates it's done allowing another thread to proceed.

Note that this only works if every process/thread is referring to the same named WaitHandle.

The EventWaitHandle constructors with strings in their signature create named system synchronization events.

Corbin March
+1  A: 

One option you could consider is having the console applications route their file access through the service, that way there's only one process accessing the file and you can synchronise access to it there.

One way of implementing this is by remoting across an IPC channel (and here's another example from weblogs.asp.net). We used this technique in a project for the company I work for and it works well, with our specific case providing a way for a .net WebService to talk to a Windows Service running on the same machine.

Sample based on the weblogs.asp.net example

Basically what you need to do with the code below is create a Solution, add two Console Apps (one called "Server" and the other called "Client" and one Library to it. Add a reference to the Library to both console apps, paste the code below in and add a reference to System.Runtime.Remoting to both Server & Console.

Run the Server app, then run the client app. Observe the fact that the server app has a message passed to it by the client. You can extend this to any number of messages/tasks

// Server:
using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Ipc;
namespace RemotingSample
{
    public class Server
    {
        public Server()
        { 
        }
        public static int Main(string[] args)
        {
            IpcChannel chan = new IpcChannel("Server");
            //register channel
            ChannelServices.RegisterChannel(chan, false);
            //register remote object
            RemotingConfiguration.RegisterWellKnownServiceType(
                        typeof(RemotingSample.RemoteObject),
                   "RemotingServer",
                   WellKnownObjectMode.SingleCall);

            Console.WriteLine("Server Activated");
            Console.ReadLine();
            return 0;
        }
    }
}

// Client:
using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Ipc;
using RemotingSample;
namespace RemotingSample
{
    public class Client
    {
        public Client()
        { 
        }
        public static int Main(string[] args)
        {
            IpcChannel chan = new IpcChannel("Client");
            ChannelServices.RegisterChannel(chan);
            RemoteObject remObject = (RemoteObject)Activator.GetObject(
                        typeof(RemotingSample.RemoteObject),
                        "ipc://Server/RemotingServer");
            if (remObject == null)
            {
                Console.WriteLine("cannot locate server");
            }
            else
            {
                remObject.ReplyMessage("You there?");
            }
            return 0;
        }
    }
}

// Shared Library:
using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;

namespace RemotingSample
{
    public class RemoteObject : MarshalByRefObject
    {
        public RemoteObject()
        {
            Console.WriteLine("Remote object activated");
        }
        public String ReplyMessage(String msg)
        {
            Console.WriteLine("Client : " + msg);//print given message on console
            return "Server : I'm alive !";
        }
    }
}
Rob
@Rob this sounds interesting could you please explain a bit more or provide some psuedo code? Sorry I've never done anything like that before.
modernzombie
@modernzombie - The best I can suggest is that you follow the example on one of the two referenced pages, I'll have a go at putting together a simpler sample, so watch this space :)
Rob
@Rob thank you. I didn't see those 2 examples when I posted the comment.
modernzombie
Annoyingly the sample from weblogs.asp.net didn't work straight off. I'm going to drop a code sample into the answer that I've compiled and run (based on it) that works for me =)
Rob
@Rob awesome. I really appreciate this! I'm trying it out now.
modernzombie
@modernzombie, no worries - glad to help :) It is a bit heavy going to start with, but you'll probably get going pretty quickly once you've got your head round the base implementation above :)
Rob
@Rob I really like this approach. I wish I had this when I was starting out with the project. Unfortunately to do this would require a reworking of the project's architecture which I spent several weeks working on.
modernzombie
@modernzombie, there's always v2 ;)
Rob