3

I have 2 programs: a C# GUI application and a C# windows service accessing the same text file;

a) the C# GUI application will write/append to the text file
b) the windows service will copy the file to a network location every 20 mins.

When the action happened concurrently, I got error message like below:

2014/09/08 21:15:56 mscorlib
The process cannot access the file 'C:\09082014.log' because it is being used by another process.
   at System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)
   at System.IO.FileStream.Init(String path, FileMode mode, FileAccess access, Int32 rights, Boolean useRights, FileShare share, Int32 bufferSize, FileOptions options, SECURITY_ATTRIBUTES secAttrs, String msgPath, Boolean bFromProxy)
   at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share, Int32 bufferSize, FileOptions options)
   at System.IO.StreamWriter.CreateFile(String path, Boolean append)
   at System.IO.StreamWriter..ctor(String path, Boolean append, Encoding encoding, Int32 bufferSize)
   at System.IO.StreamWriter..ctor(String path, Boolean append)
   at DataloggerUI.DataHelper.WriteDataLog(String msg, Int64& downTimeSince)
   at DataloggerUI.Form1.ReceiveData(IAsyncResult asyncResult)

---- the C# windows service part is like below ----------

  if (File.Exists(destination + @"\" + fi.Name))
            {
                FileInfo fi_dest = new FileInfo(destination + @"\" + fi.Name);
                if (fi.LastWriteTime > fi_dest.LastWriteTime)
                {
                    File.Copy(fi.FullName, destination + @"\" + fi.Name, true);
                    WriteLog("Send " + fi.FullName + " to server");
                }
            }
            else
            {
                File.Copy(fi.FullName, destination + @"\" + fi.Name, true);
                WriteLog("Send " + fi.FullName + " to server");
            }  
}

------- the C# windows GUI application code is like below -------

    string logfile = DataHelper.GetAppConfigString("MPRS_LogDir") + @"\" + DateTime.Now.ToString("MMddyyyy") + ".log";
    using (StreamWriter sw = new StreamWriter(logfile, true))
    {
        sw.WriteLine(tick + " " + "KYEC" + Environment.MachineName + " " + msg);
        sw.Close();
    }

The error message is thrown out by the GUI application. Was there any error or bad practice in my code?

------------ modified code to the following as per Peter's advice --------------

    try
    {
        using (StreamWriter sw = new StreamWriter(logfile, true))
        {
            sw.WriteLine(tick + " " + "KYEC" + Environment.MachineName + " " + msg);
        }
    }
    catch (IOException ex)
    {
        WriteErrorLog("IOException " + ex.Message);
        System.Threading.Thread.Sleep(2000); //2 secs                
        using (StreamWriter sw = new StreamWriter(logfile, true))
        {
            sw.WriteLine(tick + " " + "KYEC" + Environment.MachineName + " " + msg);
        }
    }
ROMANIA_engineer
  • 54,432
  • 29
  • 203
  • 199
sqr
  • 365
  • 2
  • 12
  • 29
  • How does the windows-service decide when to start copying the file ? Please explain the procedure or show more code. – Paul Weiland Oct 15 '14 at 07:04
  • The error says, the file was being used. Was it? Maybe it was being copied when GUI wanted to add something else. – luk32 Oct 15 '14 at 07:05
  • Hi luk32, yes, the error happened when GUI is appending to the file while windows service tried to copy it somewhere. Can I avoid this exception somehow? like detecting whether the file is being used before appending? also, by the C# 'using' statement, the exception would be handled already? – sqr Oct 15 '14 at 07:15
  • This is why I ask when does the service start copying ? Is there a trigger event or something ? Also the using statement automatically closes and disposes the streamwriter so you do not need sw.Close(); if I'm not mistaken. – Paul Weiland Oct 15 '14 at 07:51
  • @MeAndSomeRandoms: you are correct, for Stream calling Close() is the same as calling Dispose(), so the "using" is sufficient to ensure the Stream instance is closed (and does so equally well whether the code succeeds or an exception occurs). – Peter Duniho Oct 15 '14 at 07:54
  • @MeAndSomeRandoms: the service is triggered by a timer to run every 15 mins. – sqr Oct 15 '14 at 08:03

3 Answers3

5

You can use FileStream in Shared ReadWrite mode to write and copy file simultaneously.

Try the below code:

 //To write file use
    using (FileStream fs = new FileStream(fileToReadPath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite)) 
    {
        using (StreamWriter StreamWriter = new StreamWriter(fs)) 
        {
            StreamWriter.WriteLine(tick + " " + "KYEC" + Environment.MachineName + " " + msg);
            StreamWriter.Close();
        }
    }

//To copy file use
    using (FileStream inStream = new FileStream(fileToReadPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) 
    {
        using (FileStream outStream = File.Create(fileToWritePath)) 
        {
            while (inStream.Position < inStream.Length) 
            {
                outStream.WriteByte((byte)inStream.ReadByte());
            }
        }
    }

In this way you can achieve your task without any used by another process error.

prem
  • 3,348
  • 1
  • 25
  • 57
  • While file is being copied its locked so other process can't read/write at the same time. So the example I have gives uses the ReadWrite FileShare in which multiple process can simultaneously Read/Write the file. Hence instead of using File.Copy to copy the file you can use this technique to copy the file contents. – prem Oct 16 '14 at 05:37
  • thanks for the explanation! one more question, in order for this to work, I must make sure both applications implemented the FIleShare? – sqr Oct 16 '14 at 08:44
  • Yes, both the applications must implement ReadWrite FileShare. – prem Oct 16 '14 at 09:27
  • @sqr: you never accepted any answer, so I don't know if you wound up using any of these options. But please be aware that if you use the `FileShare` mode for this purpose, then without additional synchronization (e.g. named mutex as I mentioned before), you have no way to guarantee that the file that wants to read the file will get a complete, coherent view of the data. – Peter Duniho Jan 09 '15 at 04:48
  • thanks, Peter. I marked @abto's reply as answer. However, many thanks for you and others' reply! i didn't know how to mark a reply as answer until just now. :) I should do this long time ago. thanks for the reminder. – sqr Jan 09 '15 at 08:04
3

Based on Peter Duniho's answer and your own edits this should be the right approach:

// try to write maximum of 3 times
var maxRetry = 3;
for (int retry = 0; retry < maxRetry; retry++)
{
    try
    {
        using (StreamWriter sw = new StreamWriter(logfile, true))
        {
            sw.WriteLine("{0} KYEC{1} {2}", tick, Environment.MachineName, msg);

            break; // you were successfull so leave the retry loop
        }
    }
    catch (IOException)
    {
        if(retry < maxRetry - 1)
        {
            System.Threading.Thread.Sleep(2000); // Wait some time before retry (2 secs)
        }
        else
        {
            // handle unsuccessfull write attempts or just ignore.
        }
    }
}

this gives you the opportunity to specify how long you would retry your write attempts.

Community
  • 1
  • 1
abto
  • 1,583
  • 1
  • 12
  • 31
  • thanks, you have not only showed me the right code, but also elegant and robust code. – sqr Oct 15 '14 at 13:52
1

You have at least a couple of options here. The simplest conceptually is to just wrap your file I/O in try/catch and if an IOException occurs (and only if an IOException occurs), delay the operation briefly (e.g. Thread.Sleep(), set a timer), and try again.

An alternative approach would be to use a named mutex to allow the service and GUI process to coordinate access. Each would acquire the mutex prior to attempting their respective operation; if the other is currently accessing the file, then the attempt to acquire the mutex will cause the process to wait until the other is done.

Retry code can sometimes get messy and verbose, so while I think the retry approach is easier to understand, IMHO the mutex approach is actually more readable and simpler to get correct (once you've gone to the trouble to learn how to create a named mutex…it's not hard and MSDN has examples).

Peter Duniho
  • 68,759
  • 7
  • 102
  • 136
  • hi Peter, thanks. Do you mean to convert the below code into a try/catch block? using (StreamWriter sw = new StreamWriter(logfile, true)) { sw.WriteLine(tick + " " + "KYEC" + Environment.MachineName + " " + msg); sw.Close(); } – sqr Oct 15 '14 at 07:57
  • http://stackoverflow.com/questions/4590490/try-catch-using-right-syntax @Peter Duniho: I was reading the above links just now. I had always believed that the 'using' keyword is equivalent to try/catch. I guess I got it wrong. the using statement doesn't catch exception arising from calling streamwriter constructor. so should i change it to the following: try { using (var myObject = new MyClass()) { // something here... } } catch(Exception ex) { // Handle exception } – sqr Oct 15 '14 at 08:04
  • I mean you would wrap that entire block of code in a try/catch. Doing so is in general a good idea for any I/O code anyway. I/O almost always has the potential to throw exceptions, so it's worth taking the time to think about what you want your program to do if that happens, rather than just leaving it to .NET. As far as the referenced discussion goes, in this case you want the entire "using" block completely contained within the try/catch block, because the "new StreamWriter..." expression can throw an exception (indeed, that's where it likely would). – Peter Duniho Oct 15 '14 at 08:08
  • thanks, I learned something today. I modified the code as in the question statement; not tested online yet. Let me know whether it is correct. – sqr Oct 15 '14 at 08:25
  • @sqr: see abto's response. Your original edit actually should work fine, assuming you are confident that the service will never take more than two seconds to accomplish its task. But a retry loop is more reliable. Mutex is even more reliable. :) Note that on the service code side, if not a mutex you still need a retry, and it will _have_ to be in a loop because the GUI process can be writing at random times, not just momentarily every 20 minutes. – Peter Duniho Oct 15 '14 at 08:44