Подождите, пока файл не будет разблокирован в .NET - PullRequest
87 голосов
/ 09 сентября 2008

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

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

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

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

Ответы [ 15 ]

57 голосов
/ 09 сентября 2010

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

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;
}
37 голосов
/ 09 сентября 2008

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

    /// <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;
    }
15 голосов
/ 26 марта 2011

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

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;
    }
13 голосов
/ 08 августа 2009

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

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, если он не может получить в течение промежутка времени. Скорее всего, это следует использовать внутри используемого блока, чтобы обеспечить правильное снятие блокировок, а поток (если он открыт) будет правильно удален при удалении этого объекта.

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

5 голосов
/ 09 сентября 2008

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

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

Удачи!

4 голосов
/ 09 сентября 2008

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

3 голосов
/ 09 сентября 2008

С MSDN :

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

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

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

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

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

2 голосов
/ 09 сентября 2008

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

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

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

while (true)
{
    try {
        stream = File.Open( fileName, fileMode );
        break;
    }
    catch( FileIOException ) {

        // check whether it's a lock problem

        Thread.Sleep( 100 );
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...