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

StackOverflow https://stackoverflow.com/questions/50744

  •  09-06-2019
  •  | 
  •  

Вопрос

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

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

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

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

Это было полезно?

Решение

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

    /// <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;
    }

Другие советы

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

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;
}

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

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;
    }

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

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
}

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

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

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

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

Удачи вам!

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

От MSDN:

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

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

  1. Запускает поток, который опрашивает состояние файла до тех пор, пока он не будет заблокирован (используя объект FileInfo).
  2. Обратный вызов службы для обработки файла, как только она определит, что файл больше не заблокирован и готов к работе

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

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

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

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

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

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

        // check whether it's a lock problem

        Thread.Sleep( 100 );
    }
}

Возможным решением было бы объединить filesystemwatcher с некоторым опросом,

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

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);
}

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

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

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

Просто используйте Измененный событие с помощью NotifyFilter Фильтры уведомлений.Последняя запись:

var watcher = new FileSystemWatcher {
      Path = @"c:\temp\test",
      Filter = "*.xml",
      NotifyFilter = NotifyFilters.LastWrite
};
watcher.Changed += watcher_Changed; 
watcher.EnableRaisingEvents = true;

Я столкнулся с аналогичной проблемой при добавлении вложения 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);

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

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);
    }
}

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top