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


103

Який найпростіший спосіб блокувати потік, поки файл не розблокований і не буде доступний для читання та перейменування? Наприклад, чи є WaitOnFile () десь у .NET Framework?

У мене є служба, яка використовує 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
Це справді буде працювати в загальному випадку? якщо ви відкриєте файл за допомогою () пункту, він закривається та розблокується, коли закінчується область використання. Якщо є другий процес, що використовує ту саму стратегію, що і ця (повторно повторити), то після виходу WaitForFile () виникає перегонова умова щодо того, буде файл відкритим чи ні. Немає?
Cheeso

75
Погана ідея! Хоча концепція правильна, кращим рішенням буде повернення FileStream замість bool. Якщо файл буде заблокований ще до того, як користувач отримав шанс отримати його замок у файлі - він отримає виняток, навіть якщо функція повернеться "помилково"
Nissim

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

16
Я прийшов з майбутнього, щоб сказати, що цей код все ще працює як шарм. Дякую.
OnoSendai

6
@PabloCosta Точно! Він не може його закрити, тому що якщо це зробиться, інша нитка може забігти і відкрити її, переможивши мету. Ця реалізація правильна, оскільки вона тримає її відкритою! Нехай абонент потурбується про це, це безпечно usingна нуль, просто перевірте наявність нуля всередині usingблоку.
doug65536

2
"FileStream fs = null;" слід оголосити за межами спроби, але всередині для. Потім призначте та використовуйте fs всередині спробу. Блок захоплення повинен робити "if (fs! = Null) fs.Dispose ();" (або просто fs? .isispose () в C # 6), щоб переконатися, що FileStream, який не повертається, очищено належним чином.
Білл Меніес

1
Чи справді потрібно читати байт? На мій досвід, якщо ви відкрили файл для доступу до читання, у вас його є, не потрібно його перевіряти. Хоча при дизайні тут ви не вимушені ексклюзивного доступу, тому навіть можливо, ви можете прочитати перший байт, але ніякий інший (блокування рівня байтів). З початкового питання ви, ймовірно, відкриєте рівень загального доступу лише для читання, тому жоден інший процес не може заблокувати або змінити файл. У будь-якому випадку, я вважаю, що fs.ReadByte () є повним відходом, або його недостатньо, залежно від використання.
eselk

8
Користувач, яка обставина може fsбути недійсною в catchблоці? Якщо FileStreamконструктор кидає, змінній не буде присвоєно значення, а всередині нічого іншого, tryщо може кинути an 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. І вони справді, справді повинні.
Роман Старков


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
}

Він працює за допомогою названого mutex. Ті, хто бажає отримати доступ до файлу, намагаються отримати контроль над іменованим mutex, який розділяє ім'я файлу (з '\' s перетворився на '/' s). Ви можете або скористатися Open (), який зупиниться, поки mutex не стане доступним, або ви можете використати TryOpen (TimeSpan), який намагається придбати mutex протягом заданої тривалості та поверне помилковий, якщо він не може придбати протягом періоду часу. Це, швидше за все, слід використовувати всередині використовуючого блоку, щоб забезпечити належне звільнення блокувань, а потік (якщо він відкритий) буде належним чином розміщений, коли цей об’єкт розміщений.

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


5

У цьому конкретному додатку безпосередньо спостереження за файлом неминуче призведе до важко відстежити помилку, особливо коли розмір файлу збільшується. Ось дві різні стратегії, які будуть працювати.

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

Удачі!


Це протилежне автоматичному. Це як вручну отримати файл, виконавши більше кроків.
HackSlash

4

Один із прийомів, який я використовував деякий час назад, - це написати власну функцію. В основному виберіть виняток і повторіть спробу, використовуючи таймер, який ви можете стріляти протягом визначеної тривалості. Якщо є кращий спосіб, будь ласка, поділіться.


3

Від MSDN :

Подія OnCreate піднімається, як тільки створюється файл. Якщо файл копіюється або передається в каталог, який переглядається, подія OnCreate буде негайно піднято, а за ним слід одне або кілька подій OnChanged.

Ваш FileSystemWatcher може бути змінено таким чином, щоб він не робив його читання / перейменування під час події "OnCreate", а скоріше:

  1. Запускає потік, який опитує стан файлу, поки він не буде заблокований (використовуючи об'єкт FileInfo)
  2. Передзвонює в службу для обробки файлу, як тільки він визначить файл більше не заблокований і готовий до роботи

1
Нерест потоку файлової системи Watcher може призвести до переповнення основного буфера, тим самим пропустивши безліч змінених файлів. Кращим підходом буде створення черги споживача / виробника.
Нісім

2

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

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

2

Файл тригера процесу передачі оголошення SameNameASTrasferedFile.trg, який створюється після завершення передачі файлу.

Потім встановіть FileSystemWatcher, який запускатиме події лише у файлі * .trg.


1

Я не знаю, що ви використовуєте для визначення стану блокування файлу, але щось подібне повинно це робити.

поки (правда)
{
    спробуйте {
        stream = File.Open (fileName, fileMode);
        перерва;
    }
    лов (FileIOException) {

        // перевірте, чи це проблема із замком

        Нитка.Спляча (100);
    }
}

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
Це може бути дешевим, але раз на хвилину занадто довгий для багатьох застосувань. Моніторинг у реальному часі іноді важливий. Замість того, щоб вам потрібно було реалізувати щось, що буде слухати повідомлення Filesystem у C # (не найзручніша мова для цих речей), ви використовуєте FSW.
Грім

-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

Я зіткнувся з подібною проблемою, додаючи додаток до прогнозу. "Використання" врятувало день.

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

-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
Якщо процес запису у файл призупиняється більше секунди або буферизується в пам'яті більше секунди, ви отримаєте ще один помилковий позитив. Я не думаю, що це є гарним рішенням ні за яких обставин.
Chris Wenham
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.