Подождите, пока файл не будет разблокирован в .NET

103

Какой самый простой способ заблокировать поток до тех пор, пока файл не будет разблокирован и доступен для чтения и переименования? Например, есть ли где-нибудь в .NET Framework WaitOnFile ()?

У меня есть служба, которая использует FileSystemWatcher для поиска файлов, которые должны быть переданы на FTP-сайт, но событие создания файла срабатывает до того, как другой процесс завершит запись файла.

Идеальное решение было бы иметь период тайм-аута, чтобы поток не зависал навсегда, прежде чем сдаться.

Изменить: опробовав некоторые из приведенных ниже решений, я изменил систему так, чтобы все файлы записывались Path.GetTempFileName(), а затем выполняли File.Move()их в окончательном месте. Как только FileSystemWatcherсобытие сработало, файл уже был готов.

Крис Уэнам
источник
4
Есть ли лучший способ решить эту проблему с момента выпуска .NET 4.0?
Джейсон

Ответы:

40

Это был ответ, который я дал на связанный с этим вопрос :

    /// <summary>
    /// Blocks until the file is not locked any more.
    /// </summary>
    /// <param name="fullPath"></param>
    bool WaitForFile(string fullPath)
    {
        int numTries = 0;
        while (true)
        {
            ++numTries;
            try
            {
                // Attempt to open the file exclusively.
                using (FileStream fs = new FileStream(fullPath,
                    FileMode.Open, FileAccess.ReadWrite, 
                    FileShare.None, 100))
                {
                    fs.ReadByte();

                    // If we got this far the file is ready
                    break;
                }
            }
            catch (Exception ex)
            {
                Log.LogWarning(
                   "WaitForFile {0} failed to get an exclusive lock: {1}", 
                    fullPath, ex.ToString());

                if (numTries > 10)
                {
                    Log.LogWarning(
                        "WaitForFile {0} giving up after 10 tries", 
                        fullPath);
                    return false;
                }

                // Wait for the lock to be released
                System.Threading.Thread.Sleep(500);
            }
        }

        Log.LogTrace("WaitForFile {0} returning true after {1} tries",
            fullPath, numTries);
        return true;
    }
Эрик Зи Берд
источник
8
Я считаю это уродливым, но единственно возможным решением
knoopx
6
Это действительно сработает в общем случае? если вы открываете файл в предложении using (), файл закрывается и разблокируется при завершении области действия using. Если есть второй процесс, использующий ту же стратегию, что и этот (повторять несколько раз), то после выхода из WaitForFile () возникает состояние гонки, определяющее, будет ли файл открываться или нет. Нет?
Cheeso,
75
Плохая идея! Хотя концепция верна, лучшим решением будет возврат FileStream вместо bool. Если файл снова заблокирован до того, как пользователь получит возможность заблокировать файл, он получит исключение, даже если функция вернула «false»
Ниссим
2
где метод Феро?
Vbp
1
Комментарий Ниссима - это именно то, о чем я тоже думал, но если вы собираетесь использовать этот поиск, не забудьте сбросить его на 0 после чтения байта. fs.Seek (0, SeekOrigin.Begin);
WHol
73

Начиная с ответа Эрика, я включил некоторые улучшения, чтобы сделать код более компактным и многоразовым. Надеюсь, это будет полезно.

FileStream WaitForFile (string fullPath, FileMode mode, FileAccess access, FileShare share)
{
    for (int numTries = 0; numTries < 10; numTries++) {
        FileStream fs = null;
        try {
            fs = new FileStream (fullPath, mode, access, share);
            return fs;
        }
        catch (IOException) {
            if (fs != null) {
                fs.Dispose ();
            }
            Thread.Sleep (50);
        }
    }

    return null;
}
мафу
источник
17
Я пришел из будущего, чтобы сказать, что этот код по-прежнему работает как шарм. Спасибо.
OnoSendai
6
@PabloCosta Совершенно верно! Он не может закрыть его, потому что в противном случае другой поток мог бы ворваться и открыть его, нарушив цель. Эта реализация верна, потому что она остается открытой! Позвольте вызывающему абоненту беспокоиться об этом, это безопасно для usingнуля, просто проверьте наличие null внутри usingблока.
doug65536 03
2
"FileStream fs = null;" следует объявить вне try, но внутри for. Затем назначьте и используйте fs внутри try. Блок catch должен выполнять "if (fs! = Null) fs.Dispose ();" (или просто fs? .Dispose () в C # 6), чтобы гарантировать, что FileStream, который не возвращается, очищен должным образом.
Билл Менес
1
Неужели нужно читать байт? По моему опыту, если вы открыли файл для чтения, он у вас есть, не нужно его проверять. Несмотря на то, что здесь вы не используете монопольный доступ, вы даже можете прочитать первый байт, но не другие (блокировка байтового уровня). Исходя из исходного вопроса, вы, вероятно, откроете уровень общего доступа только для чтения, поэтому никакой другой процесс не может заблокировать или изменить файл. В любом случае, я считаю, что fs.ReadByte () либо бесполезная трата, либо недостаточная, в зависимости от использования.
eselk
8
Пользователь, какое обстоятельство не может fsбыть нулевым в catchблоке? Если FileStreamконструктор выбрасывает, переменной не будет присвоено значение, и нет ничего другого внутри, tryчто могло бы вызвать IOException. Мне кажется, что это нормально - просто делать return new FileStream(...).
Матти Вирккунен
18

Вот общий код для этого, не зависящий от самой операции с файлом. Это пример того, как его использовать:

WrapSharingViolations(() => File.Delete(myFile));

или

WrapSharingViolations(() => File.Copy(mySourceFile, myDestFile));

Вы также можете определить количество повторов и время ожидания между повторами.

ПРИМЕЧАНИЕ. К сожалению, основная ошибка Win32 (ERROR_SHARING_VIOLATION) не отображается в .NET, поэтому я добавил небольшую функцию взлома ( IsSharingViolation), основанную на механизмах отражения, чтобы проверить это.

    /// <summary>
    /// Wraps sharing violations that could occur on a file IO operation.
    /// </summary>
    /// <param name="action">The action to execute. May not be null.</param>
    public static void WrapSharingViolations(WrapSharingViolationsCallback action)
    {
        WrapSharingViolations(action, null, 10, 100);
    }

    /// <summary>
    /// Wraps sharing violations that could occur on a file IO operation.
    /// </summary>
    /// <param name="action">The action to execute. May not be null.</param>
    /// <param name="exceptionsCallback">The exceptions callback. May be null.</param>
    /// <param name="retryCount">The retry count.</param>
    /// <param name="waitTime">The wait time in milliseconds.</param>
    public static void WrapSharingViolations(WrapSharingViolationsCallback action, WrapSharingViolationsExceptionsCallback exceptionsCallback, int retryCount, int waitTime)
    {
        if (action == null)
            throw new ArgumentNullException("action");

        for (int i = 0; i < retryCount; i++)
        {
            try
            {
                action();
                return;
            }
            catch (IOException ioe)
            {
                if ((IsSharingViolation(ioe)) && (i < (retryCount - 1)))
                {
                    bool wait = true;
                    if (exceptionsCallback != null)
                    {
                        wait = exceptionsCallback(ioe, i, retryCount, waitTime);
                    }
                    if (wait)
                    {
                        System.Threading.Thread.Sleep(waitTime);
                    }
                }
                else
                {
                    throw;
                }
            }
        }
    }

    /// <summary>
    /// Defines a sharing violation wrapper delegate.
    /// </summary>
    public delegate void WrapSharingViolationsCallback();

    /// <summary>
    /// Defines a sharing violation wrapper delegate for handling exception.
    /// </summary>
    public delegate bool WrapSharingViolationsExceptionsCallback(IOException ioe, int retry, int retryCount, int waitTime);

    /// <summary>
    /// Determines whether the specified exception is a sharing violation exception.
    /// </summary>
    /// <param name="exception">The exception. May not be null.</param>
    /// <returns>
    ///     <c>true</c> if the specified exception is a sharing violation exception; otherwise, <c>false</c>.
    /// </returns>
    public static bool IsSharingViolation(IOException exception)
    {
        if (exception == null)
            throw new ArgumentNullException("exception");

        int hr = GetHResult(exception, 0);
        return (hr == -2147024864); // 0x80070020 ERROR_SHARING_VIOLATION

    }

    /// <summary>
    /// Gets the HRESULT of the specified exception.
    /// </summary>
    /// <param name="exception">The exception to test. May not be null.</param>
    /// <param name="defaultValue">The default value in case of an error.</param>
    /// <returns>The HRESULT value.</returns>
    public static int GetHResult(IOException exception, int defaultValue)
    {
        if (exception == null)
            throw new ArgumentNullException("exception");

        try
        {
            const string name = "HResult";
            PropertyInfo pi = exception.GetType().GetProperty(name, BindingFlags.NonPublic | BindingFlags.Instance); // CLR2
            if (pi == null)
            {
                pi = exception.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.Instance); // CLR4
            }
            if (pi != null)
                return (int)pi.GetValue(exception, null);
        }
        catch
        {
        }
        return defaultValue;
    }
Саймон Мурье
источник
5
Они действительно могли предоставить SharingViolationException. Фактически, они могут быть обратно совместимыми, если они происходят от IOException. И они действительно должны.
Роман Старков
6
Marshal.GetHRForException msdn.microsoft.com/en-us/library/…
Стивен Т. Крамер
9
В .NET Framework 4.5, .NET Standard и .NET Core HResult является общедоступным свойством класса Exception. Отражение для этого больше не нужно. Из MSDN:Starting with the .NET Framework 4.5, the HResult property's setter is protected, whereas its getter is public. In previous versions of the .NET Framework, both getter and setter are protected.
NightOwl888,
13

Я собрал вспомогательный класс для таких вещей. Это сработает, если вы контролируете все, что имеет доступ к файлу. Если вы ожидаете разногласий от множества других вещей, то это бесполезно.

using System;
using System.IO;
using System.Threading;

/// <summary>
/// This is a wrapper aroung a FileStream.  While it is not a Stream itself, it can be cast to
/// one (keep in mind that this might throw an exception).
/// </summary>
public class SafeFileStream: IDisposable
{
    #region Private Members
    private Mutex m_mutex;
    private Stream m_stream;
    private string m_path;
    private FileMode m_fileMode;
    private FileAccess m_fileAccess;
    private FileShare m_fileShare;
    #endregion//Private Members

    #region Constructors
    public SafeFileStream(string path, FileMode mode, FileAccess access, FileShare share)
    {
        m_mutex = new Mutex(false, String.Format("Global\\{0}", path.Replace('\\', '/')));
        m_path = path;
        m_fileMode = mode;
        m_fileAccess = access;
        m_fileShare = share;
    }
    #endregion//Constructors

    #region Properties
    public Stream UnderlyingStream
    {
        get
        {
            if (!IsOpen)
                throw new InvalidOperationException("The underlying stream does not exist - try opening this stream.");
            return m_stream;
        }
    }

    public bool IsOpen
    {
        get { return m_stream != null; }
    }
    #endregion//Properties

    #region Functions
    /// <summary>
    /// Opens the stream when it is not locked.  If the file is locked, then
    /// </summary>
    public void Open()
    {
        if (m_stream != null)
            throw new InvalidOperationException(SafeFileResources.FileOpenExceptionMessage);
        m_mutex.WaitOne();
        m_stream = File.Open(m_path, m_fileMode, m_fileAccess, m_fileShare);
    }

    public bool TryOpen(TimeSpan span)
    {
        if (m_stream != null)
            throw new InvalidOperationException(SafeFileResources.FileOpenExceptionMessage);
        if (m_mutex.WaitOne(span))
        {
            m_stream = File.Open(m_path, m_fileMode, m_fileAccess, m_fileShare);
            return true;
        }
        else
            return false;
    }

    public void Close()
    {
        if (m_stream != null)
        {
            m_stream.Close();
            m_stream = null;
            m_mutex.ReleaseMutex();
        }
    }

    public void Dispose()
    {
        Close();
        GC.SuppressFinalize(this);
    }

    public static explicit operator Stream(SafeFileStream sfs)
    {
        return sfs.UnderlyingStream;
    }
    #endregion//Functions
}

Он работает с использованием именованного мьютекса. Те, кто желает получить доступ к файлу, пытаются получить контроль над именованным мьютексом, который разделяет имя файла (с символами '\' превращенными в '/'). Вы можете либо использовать Open (), который будет останавливаться до тех пор, пока мьютекс не станет доступным, либо вы можете использовать TryOpen (TimeSpan), который пытается получить мьютекс в течение заданного времени и возвращает false, если он не может быть получен в течение указанного промежутка времени. Скорее всего, это следует использовать внутри блока using, чтобы гарантировать, что блокировки снимаются должным образом, и поток (если он открыт) будет правильно удален при удалении этого объекта.

Я провел быстрый тест с ~ 20 объектами для различных операций чтения / записи файла и не обнаружил повреждений. Очевидно, что это не очень продвинутый вариант, но он должен работать в большинстве простых случаев.

user152791
источник
5

Для этого конкретного приложения непосредственное наблюдение за файлом неизбежно приведет к трудно отслеживаемой ошибке, особенно при увеличении размера файла. Вот две разные стратегии, которые будут работать.

  • Ftp два файла а смотреть только один. Например, отправьте файлы important.txt и important.finish. Следите только за файлом финиша, но обрабатывайте txt.
  • FTP один файл, но переименуйте его, когда закончите. Например, отправьте important.wait и попросите отправителя переименовать его в important.txt, когда закончите.

Удачи!

Джейсон Сальдо
источник
Это противоположно автоматическому. Это похоже на получение файла вручную с дополнительными шагами.
HackSlash
4

Некоторое время назад я использовал один из приемов - написание собственной функции. В основном перехватите исключение и повторите попытку, используя таймер, который вы можете запустить в течение определенного времени. Если есть способ получше, поделитесь пожалуйста.

Гульзар Назим
источник
3

Из MSDN :

Событие OnCreated возникает сразу после создания файла. Если файл копируется или переносится в отслеживаемый каталог, немедленно возникает событие OnCreated, за которым следует одно или несколько событий OnChanged.

Ваш FileSystemWatcher можно изменить так, чтобы он не выполнял чтение / переименование во время события «OnCreated», а:

  1. Охватывает поток, который опрашивает состояние файла, пока он не будет заблокирован (с помощью объекта FileInfo)
  2. Вызывает обратно службу для обработки файла, как только определяет, что файл больше не заблокирован и готов к работе.
Гай Старбак
источник
1
Создание потока наблюдателя файловой системы может привести к переполнению нижележащего буфера, что приведет к потере большого количества измененных файлов. Лучшим подходом будет создание очереди потребителя / производителя.
Nissim
2

В большинстве случаев будет работать простой подход, например, предложенный @harpo. Используя такой подход, вы можете разработать более сложный код:

  • Найдите все открытые дескрипторы для выбранного файла с помощью SystemHandleInformation \ SystemProcessInformation
  • Подкласс WaitHandle, чтобы получить доступ к его внутреннему дескриптору
  • Передача найденных дескрипторов, заключенных в подкласс WaitHandle, в метод WaitHandle.WaitAny
аку
источник
2

Объявление для передачи файла триггера процесса SameNameASTrasferFile.trg, который создается после завершения передачи файла.

Затем настройте FileSystemWatcher, который будет запускать событие только для файла * .trg.

Руди
источник
1

Я не знаю, что вы используете для определения статуса блокировки файла, но что-то вроде этого должно это сделать.

пока (правда)
{
    пытаться {
        stream = File.Open (имя файла, режим файла);
        перерыв;
    }
    catch (FileIOException) {

        // проверяем, не проблема ли это с блокировкой

        Thread.Sleep (100);
    }
}
гарпо
источник
1
Немного поздно, но когда файл каким-то образом заблокирован, вы никогда не выйдете из цикла. Вам следует добавить счетчик (см. 1-й ответ).
Питер
0

Возможным решением было бы объединить наблюдатель файловой системы с некоторым опросом,

получать уведомление для каждого изменения в файле, и при получении уведомления проверьте, заблокирован ли он, как указано в принятом в настоящее время ответе: https://stackoverflow.com/a/50800/6754146 Код для открытия файлового потока копируется из ответа и немного изменено:

public static void CheckFileLock(string directory, string filename, Func<Task> callBack)
{
    var watcher = new FileSystemWatcher(directory, filename);
    FileSystemEventHandler check = 
        async (sender, eArgs) =>
    {
        string fullPath = Path.Combine(directory, filename);
        try
        {
            // Attempt to open the file exclusively.
            using (FileStream fs = new FileStream(fullPath,
                    FileMode.Open, FileAccess.ReadWrite,
                    FileShare.None, 100))
            {
                fs.ReadByte();
                watcher.EnableRaisingEvents = false;
                // If we got this far the file is ready
            }
            watcher.Dispose();
            await callBack();
        }
        catch (IOException) { }
    };
    watcher.NotifyFilter = NotifyFilters.LastWrite;
    watcher.IncludeSubdirectories = false;
    watcher.EnableRaisingEvents = true;
    //Attach the checking to the changed method, 
    //on every change it gets checked once
    watcher.Changed += check;
    //Initially do a check for the case it is already released
    check(null, null);
}

Таким образом вы можете проверить файл, если он заблокирован, и получить уведомление, когда он закрыт по указанному обратному вызову, таким образом вы избегаете чрезмерно агрессивного опроса и выполняете работу только тогда, когда он может быть фактически закрыт

Флориан К
источник
-1

Я делаю это так же, как Гульзар, просто продолжаю пробовать петлей.

На самом деле я даже не беспокоюсь о наблюдателе файловой системы. Опрашивать сетевой диск о новых файлах раз в минуту - это дешево.

Джонатан Аллен
источник
2
Это может быть дешево, но раз в минуту слишком долго для многих приложений. Иногда необходим мониторинг в реальном времени. Вместо того, чтобы реализовать что-то, что будет прослушивать сообщения файловой системы на C # (не самый удобный язык для этих вещей), вы используете FSW.
ThunderGr
-1

Просто используйте событие Changed с NotifyFilter NotifyFilters.LastWrite :

var watcher = new FileSystemWatcher {
      Path = @"c:\temp\test",
      Filter = "*.xml",
      NotifyFilter = NotifyFilters.LastWrite
};
watcher.Changed += watcher_Changed; 
watcher.EnableRaisingEvents = true;
Бернхард Хохгаттерер
источник
1
FileSystemWatcher не только уведомляет о завершении записи в файл. Он часто уведомляет вас несколько раз об «одиночной» логической записи, и если вы попытаетесь открыть файл после получения первого уведомления, вы получите исключение.
Росс
-1

Я столкнулся с аналогичной проблемой при добавлении вложения Outlook. «Использование» спасло положение.

string fileName = MessagingBLL.BuildPropertyAttachmentFileName(currProp);

                //create a temporary file to send as the attachment
                string pathString = Path.Combine(Path.GetTempPath(), fileName);

                //dirty trick to make sure locks are released on the file.
                using (System.IO.File.Create(pathString)) { }

                mailItem.Subject = MessagingBLL.PropertyAttachmentSubject;
                mailItem.Attachments.Add(pathString, Outlook.OlAttachmentType.olByValue, Type.Missing, Type.Missing);
Джахмал23
источник
-3

Как насчет этого как варианта:

private void WaitOnFile(string fileName)
{
    FileInfo fileInfo = new FileInfo(fileName);
    for (long size = -1; size != fileInfo.Length; fileInfo.Refresh())
    {
        size = fileInfo.Length;
        System.Threading.Thread.Sleep(1000);
    }
}

Конечно, если размер файла предварительно выделен при создании, вы получите ложное срабатывание.

Ральф Шиллингтон
источник
1
Если процесс записи в файл приостанавливается более чем на секунду или буферизуется в памяти более чем на секунду, вы получите еще одно ложное срабатывание. Я не думаю, что это хорошее решение ни при каких обстоятельствах.
Крис Уэнам,