Wait for file to be freed by process
Asked Answered
C

11

45

How do I wait for the file to be free so that ss.Save() can overwrite it with a new one? If I run this twice close together(ish), I get a generic GDI+ error.

///<summary>
/// Grabs a screen shot of the App and saves it to the C drive in jpg
///</summary>
private static String GetDesktopImage(DevExpress.XtraEditors.XtraForm whichForm)
{
    Rectangle bounds = whichForm.Bounds;

    // This solves my problem but creates a clutter issue
    // var timeStamp = DateTime.Now.ToString("ddd-MMM-dd-yyyy-hh-mm-ss");
    // var fileName = "C:\\HelpMe" + timeStamp + ".jpg";

    var fileName = "C:\\HelpMe.jpg";
    File.Create(fileName);
    using (Bitmap ss = new Bitmap(bounds.Width, bounds.Height))
    using (Graphics g = Graphics.FromImage(ss))
    {
        g.CopyFromScreen(whichForm.Location, Point.Empty, bounds.Size);
        ss.Save(fileName, ImageFormat.Jpeg);
    }

    return fileName;
}
Craigie answered 10/9, 2009 at 18:2 Comment(2)
possible duplicate of Is there a way to check if a file is in use?Kakemono
This code has a simple bug with File.Create(fileName). The answers are missing that point. It is not necessary to wait for closure.Missie
N
79

A function like this will do it:

public static bool IsFileReady(string filename)
{
    // If the file can be opened for exclusive access it means that the file
    // is no longer locked by another process.
    try
    {
        using (FileStream inputStream = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.None))
            return inputStream.Length > 0;
    }
    catch (Exception)
    {
        return false;
    }
}

Stick it in a while loop and you have something which will block until the file is accessible:

public static void WaitForFile(string filename)
{
    //This will lock the execution until the file is ready
    //TODO: Add some logic to make it async and cancelable
    while (!IsFileReady(filename)) { }
}
Naumachia answered 10/9, 2009 at 18:12 Comment(4)
-1 because: thedailywtf.com/Comments/…. The right way: https://mcmap.net/q/53247/-is-there-a-way-to-check-if-a-file-is-in-useOrleans
Catching all exceptions is a very bad practice, you should be more precise in what constitutes a fact that the file is simply inaccessible.Mac
Shouldn't it have a sleep() in there somewhere - otherwise the app might become unresponsiveAnkeny
Also another problem with this is you enter in a race condition between returning a response and the other piece of code opening the file another system can come in and lock the file.Vertebrate
A
25

If you check access before writing to the file some other process might snatch the access again before you manage to do your write. Therefor I would suggest one of the following two:

  1. Wrap what you want to do in a retry scope that won't hide any other error
  2. Create a wrapper method that waits until you can get a stream and use that stream

getting a stream

private FileStream GetWriteStream(string path, int timeoutMs)
{
    var time = Stopwatch.StartNew();
    while (time.ElapsedMilliseconds < timeoutMs)
    {
        try
        {
            return new FileStream(path, FileMode.Create, FileAccess.Write);
        }
        catch (IOException e)
        {
            // access error
            if (e.HResult != -2147024864)
                throw;
        }
    }

    throw new TimeoutException($"Failed to get a write handle to {path} within {timeoutMs}ms.");
}

then use it like this:

using (var stream = GetWriteStream("path"))
{
    using (var writer = new StreamWriter(stream))
        writer.Write("test");
}

retry scope

private void WithRetry(Action action, int timeoutMs = 1000)
{
    var time = Stopwatch.StartNew();
    while(time.ElapsedMilliseconds < timeoutMs)
    {
        try
        {
            action();
            return;
        }
        catch (IOException e)
        {
            // access error
            if (e.HResult != -2147024864)
                throw;
        }
    }
    throw new Exception("Failed perform action within allotted time.");
}

and then use WithRetry(() => File.WriteAllText(Path.Combine(_directory, name), contents));

Appointive answered 11/5, 2016 at 6:26 Comment(9)
I also created a gist for a class wrapping this behavior. Bear in mind of course that doing this might mean that your architecture has issues if several classes are reading and writing to the same file in a conflicting manner. You may end up losing data this way. gist.github.com/ddikman/667f309706fdf4f68b9fab2827b1bccaAppointive
I don't know why this isn't the accepted answer. The code is much safer; calling IsFileReady in a while loop, as Gordon Thompson's answer suggests could potentially fail. Another process could lock the file between when the loop condition checks if its available and your process tries to actually access it. Only thing, e.HResult is inaccessible because it is protected.Cackle
Thanks for the support although my suggested solution is pretty cluttered in comparison. I don't much like the look of it however since there's no built in support in the framework you're left with few options. I was using the HResult though, might be different between framework versions maybe, I'm sure there's some other property that can be used to detect which error the IOException contains though.Appointive
I know you can use the Message property and do string comparison but that's kinda ugly IMOCackle
I agree, I wouldn't check the message unless I have to. I double checked MSDN about the HResult though and in the later versions of the framework (after 3.5) it's public. msdn.microsoft.com/en-us/library/…Appointive
Hmm, really? I copied your code verbatim at first (I ended up having to slightly tweak it for my use case anyway) and Visual Studio's IntelliSense threw a warning/error that HResult was inaccessible because it was protected, and I'm on .NET 4.0.Cackle
You're right, I need to revise that, it's public in the ".NET Framework (current version)" as stated in the page. Even 4.0 is protected.Appointive
This solution works, though I don't follow where the WithRetry fits into the first part of your example.. You get a StreamWriter object but then use File.WriteAllText(...) as WithRetry's Action param. Are these two separate solutions, or should WithRetry replace using (var writer = new StreamWriter(stream)) writer.Write("test");Spinozism
@Spinozism sorry, this was probably written in a confusing way. Reading it now (I wrote it years ago) I think there are two solutions there. Either you use the GetWriteStream which will retry creating the stream until you get a file lock, or, you use the WithRetry and don't rely on a stream at all, instead just write all the contents in one go. It depends on whether you need the stream or not.Appointive
V
8

Here is a solution that may be overkill for some users. I've created a new static class which has an event which is triggered only when the file finishes copying.

The user registers files which they would like to watch by calling FileAccessWatcher.RegisterWaitForFileAccess(filePath). If the file is not already being watched a new task is started which repeatedly checks the file to see if it can be opened. Each time it checks it also reads the file size. If the file size does not increase in a pre-defined time (5 minutes in my example) the loop is exited.

When the loop exits from the file being accessible or from the timeout the FileFinishedCopying event is triggered.

public class FileAccessWatcher
{
    // this list keeps track of files being watched
    private static ConcurrentDictionary<string, FileAccessWatcher> watchedFiles = new ConcurrentDictionary<string, FileAccessWatcher>();

    public static void RegisterWaitForFileAccess(string filePath)
    {
        // if the file is already being watched, don't do anything
        if (watchedFiles.ContainsKey(filePath))
        {
            return;
        }
        // otherwise, start watching it
        FileAccessWatcher accessWatcher = new FileAccessWatcher(filePath);
        watchedFiles[filePath] = accessWatcher;
        accessWatcher.StartWatching();
    }

    /// <summary>
    /// Event triggered when the file is finished copying or when the file size has not increased in the last 5 minutes.
    /// </summary>
    public static event FileSystemEventHandler FileFinishedCopying;

    private static readonly TimeSpan MaximumIdleTime = TimeSpan.FromMinutes(5);

    private readonly FileInfo file;

    private long lastFileSize = 0;

    private DateTime timeOfLastFileSizeIncrease = DateTime.Now;

    private FileAccessWatcher(string filePath)
    {
        this.file = new FileInfo(filePath);
    }

    private Task StartWatching()
    {
        return Task.Factory.StartNew(this.RunLoop);
    }

    private void RunLoop()
    {
        while (this.IsFileLocked())
        {
            long currentFileSize = this.GetFileSize();
            if (currentFileSize > this.lastFileSize)
            {
                this.lastFileSize = currentFileSize;
                this.timeOfLastFileSizeIncrease = DateTime.Now;
            }

            // if the file size has not increased for a pre-defined time limit, cancel
            if (DateTime.Now - this.timeOfLastFileSizeIncrease > MaximumIdleTime)
            {
                break;
            }
        }

        this.RemoveFromWatchedFiles();
        this.RaiseFileFinishedCopyingEvent();
    }

    private void RemoveFromWatchedFiles()
    {
        FileAccessWatcher accessWatcher;
        watchedFiles.TryRemove(this.file.FullName, out accessWatcher);
    }

    private void RaiseFileFinishedCopyingEvent()
    {
        FileFinishedCopying?.Invoke(this,
            new FileSystemEventArgs(WatcherChangeTypes.Changed, this.file.FullName, this.file.Name));
    }

    private long GetFileSize()
    {
        return this.file.Length;
    }

    private bool IsFileLocked()
    {
        try
        {
            using (this.file.Open(FileMode.Open)) { }
        }
        catch (IOException e)
        {
            var errorCode = Marshal.GetHRForException(e) & ((1 << 16) - 1);

            return errorCode == 32 || errorCode == 33;
        }

        return false;
    }
}

Example usage:

// register the event
FileAccessWatcher.FileFinishedCopying += FileAccessWatcher_FileFinishedCopying;

// start monitoring the file (put this inside the OnChanged event handler of the FileSystemWatcher
FileAccessWatcher.RegisterWaitForFileAccess(fileSystemEventArgs.FullPath);

Handle the FileFinishedCopyingEvent:

private void FileAccessWatcher_FileFinishedCopying(object sender, FileSystemEventArgs e)
{
    Console.WriteLine("File finished copying: " + e.FullPath);
}
Vorlage answered 14/10, 2016 at 4:35 Comment(3)
This is a beautiful solution. In my opion is the only bulletproof solution.Other solutions dont work on all situations. For example this solution works when: open 2 windows folders, copy file from Dir a to watched dir. This solutions gives me correctly a single event call on FileFinishedCopying. Other solutions like opening the file/reading the file still give multiple hits in this scenario. Thank you verry mutch for this solution!Voltmeter
From my preliminary tests this appears to be working well with the exception that the class instance needs to remove itself from the watchedFiles dictionary using the original file path specified when the instance was created instead of 'this.file.FullName'. Other potential enhancements would be to include a short delay within the 'while' loop of RunLoop to ensure other processes can get to use the CPU as well as implementing a cancellation token to immediately stop processing.Wherewith
Also, the FileAccessWatcher class instance should remove itself from the dictionary only after RaiseFileFinishedCopyingEvent has completed in case its processing causes the file to be re-added to the dictionary.Wherewith
S
5

There is no function out there which will allow you to wait on a particular handle / file system location to be available for writing. Sadly, all you can do is poll the handle for writing.

Sago answered 10/9, 2009 at 18:6 Comment(0)
E
4

Taking the top answer I wrote a similar one, but it's async, non-blocking, awaitable, cancelable (just stop the task) and checks the exception thrown.

public static async Task IsFileReady(string filename)
    {
        await Task.Run(() =>
        {
            if (!File.Exists(path))
            {
                throw new IOException("File does not exist!");
            }

            var isReady = false;

            while (!isReady)
            {
                // If the file can be opened for exclusive access it means that the file
                // is no longer locked by another process.
                try
                {
                    using (FileStream inputStream =
                        File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.None))
                        isReady = inputStream.Length > 0;
                }
                catch (Exception e)
                {
                    // Check if the exception is related to an IO error.
                    if (e.GetType() == typeof(IOException))
                    {
                        isReady = false;
                    }
                    else
                    {
                        // Rethrow the exception as it's not an exclusively-opened-exception.
                        throw;
                    }
                }
            }
        });
    }

You can use it in this fashion:

Task ready = IsFileReady(path);

ready.Wait(1000);

if (!ready.IsCompleted)
{
    throw new FileLoadException($"The file {path} is exclusively opened by another process!");
}

File.Delete(path);

If you have to really wait for it, or in a more JS-promise-way:

IsFileReady(path).ContinueWith(t => File.Delete(path));
Edris answered 21/10, 2018 at 11:35 Comment(0)
S
3

You can let the System wait, until the process is closed.

Just as simple as this:

Process.Start("the path of your text file or exe").WaitForExit();

Severable answered 5/10, 2015 at 8:23 Comment(1)
And who is supposed to close (exit) the newly started process?Stephi
P
1
bool isLocked = true;
while (isLocked)
 try {
  System.IO.File.Move(filename, filename2);
  isLocked = false;
 }
 catch { }
 System.IO.File.Move(filename2, filename);
Parks answered 10/9, 2009 at 18:8 Comment(2)
Moving a file to find out if it is locked, regardless of contextual knowledge of the file's purpose, is not a good approach.Sogdian
You might don't want to do that ^. Some different process may check if file exists at the same time and create an another one or something else.Errand
T
1

You could use a lock statement with a Dummy variable, and it seems to work great.

Check here.

Tody answered 28/5, 2017 at 10:8 Comment(0)
P
1

The problem is that your code is already opening the file by calling File.Create, which returns an open file stream. Depending on timing, the garbage collector may have noticed that the returned stream is unused and put it on the finalizer queue, and then the finalizer thread may have cleaned things up up already before you start writing to the file again. But this is not guarantueed, as you noticed.

To fix it, you can either close the file again immediately like File.Create(...).Dispose(). Alternatively, wrap the stream in a using statement, and write to it.

using (FileStream stream = File.Create(fileName))
using (Bitmap ss = new Bitmap(bounds.Width, bounds.Height))
using (Graphics g = Graphics.FromImage(ss))
{
    g.CopyFromScreen(whichForm.Location, Point.Empty, bounds.Size);
    ss.Save(stream, ImageFormat.Jpeg);
}
Pennyworth answered 6/7, 2020 at 20:32 Comment(0)
L
0

One practice I use is to write a specific word at the end of the string on the file. Type "Exit". Then checking if the string read ends with the word "Exit" means that the file has been read completely.

Laky answered 10/9, 2009 at 18:2 Comment(1)
Adding return statement also saves the problem why using Exit instead?Ralphralston
E
0

Extending some of the other solutions here, this is what I ended up with:

private static async Task<FileStream> OpenLockFile(string path) {
  for (int i = 0; ; ++i) {
    try {
      return File.Open(path, FileMode.Create, FileAccess.ReadWrite, FileShare.None);
    } catch (Exception ex) {
      // wait up to 30 seconds, before giving up
      if (i < 30 && ex.HResult == -2147024864) {
        await Task.Delay(1000);
      } else {
        throw ex;
      }
    }
  }
}
Exhume answered 27/9, 2022 at 17:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.