Poczekaj, aż plik zostanie odblokowany w .NET

103

Jaki jest najprostszy sposób blokowania wątku, dopóki plik nie zostanie odblokowany i będzie można go odczytać i zmienić jego nazwę? Na przykład, czy gdzieś w .NET Framework istnieje WaitOnFile ()?

Mam usługę, która używa FileSystemWatcher do wyszukiwania plików, które mają zostać przesłane do witryny FTP, ale zdarzenie utworzone w pliku jest wyzwalane, zanim inny proces zakończy zapisywanie pliku.

Idealne rozwiązanie miałoby limit czasu, aby wątek nie zawieszał się na zawsze, zanim się poddaje.

Edycja: po wypróbowaniu niektórych z poniższych rozwiązań skończyło się na zmianie systemu tak, aby wszystkie pliki były zapisywane Path.GetTempFileName(), a następnie przeprowadziłem File.Move()do ostatecznej lokalizacji. Zaraz po uruchomieniu FileSystemWatcherzdarzenia plik był już kompletny.

Chris Wenham
źródło
4
Czy od czasu wydania platformy .NET 4.0 istnieje lepszy sposób rozwiązania tego problemu?
jason

Odpowiedzi:

40

Oto odpowiedź, której udzieliłem na powiązane pytanie :

    /// <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;
    }
Eric Z Beard
źródło
8
Uważam, że to brzydkie, ale jedyne możliwe rozwiązanie
knoopx
6
Czy to naprawdę zadziała w ogólnym przypadku? jeśli otworzysz plik w klauzuli using (), plik zostanie zamknięty i odblokowany po zakończeniu zakresu using. Jeśli istnieje drugi proces korzystający z tej samej strategii (ponawiaj wielokrotne próby), po zakończeniu WaitForFile () wystąpi wyścig określający, czy plik będzie można otworzyć, czy nie. Nie?
Cheeso
75
Kiepski pomysł! Chociaż koncepcja jest słuszna, lepszym rozwiązaniem będzie zwrócenie FileStream zamiast bool. Jeśli plik zostanie ponownie zablokowany, zanim użytkownik będzie miał szansę na zablokowanie pliku - otrzyma wyjątek, nawet jeśli funkcja zwróci „false”
Nissim
2
gdzie jest metoda Fero?
Vbp
1
Komentarz Nissima jest dokładnie tym, o czym myślałem, ale jeśli zamierzasz użyć tego wyszukiwania, nie zapomnij zresetować go do 0 po przeczytaniu bajtu. fs.Seek (0, SeekOrigin.Begin);
CAŁY
73

Zaczynając od odpowiedzi Erica, dodałem kilka ulepszeń, aby kod był znacznie bardziej zwarty i wielokrotnego użytku. Mam nadzieję, że to przydatne.

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;
}
mafu
źródło
16
Przybyłem z przyszłości, aby powiedzieć, że ten kod nadal działa jak urok. Dzięki.
OnoSendai,
6
@PabloCosta Dokładnie! Nie może go zamknąć, ponieważ gdyby tak się stało, inny wątek mógłby wpaść i otworzyć go, pokonując cel. Ta implementacja jest poprawna, ponieważ utrzymuje ją otwartą! Niech dzwoniący się o to martwi, można bezpiecznie usingustawić wartość null, po prostu sprawdź, czy w usingbloku nie ma wartości null .
doug65536
2
"FileStream fs = null;" powinno być zadeklarowane poza try, ale wewnątrz for. Następnie przypisz i użyj fs w try. Blok catch powinien wykonać "if (fs! = Null) fs.Dispose ();" (lub po prostu fs? .Dispose () w C # 6), aby upewnić się, że FileStream, który nie jest zwracany, jest prawidłowo czyszczony.
Bill Menees
1
Czy naprawdę trzeba czytać bajt? Z mojego doświadczenia wynika, że ​​jeśli otworzyłeś plik do odczytu, masz go, nie musisz go testować. Chociaż w przypadku projektu tutaj nie wymuszasz wyłącznego dostępu, więc jest nawet możliwe, że możesz odczytać pierwszy bajt, ale nie ma innych (blokowanie na poziomie bajtów). Z pierwotnego pytania, które prawdopodobnie otworzysz z poziomem udostępniania tylko do odczytu, więc żaden inny proces nie może zablokować ani zmodyfikować pliku. W każdym razie uważam, że fs.ReadByte () jest albo całkowitym marnotrawstwem, albo niewystarczającym, w zależności od zastosowania.
eselk
8
Użytkownik, która okoliczność nie może fsbyć zerowa w catchbloku? Jeśli FileStreamkonstruktor zgłosi, zmiennej nie zostanie przypisana wartość, a wewnątrz elementu nie ma nic innego, tryco mogłoby zgłosić IOException. Wydaje mi się, że powinno to być w porządku return new FileStream(...).
Matti Virkkunen
18

Oto ogólny kod, który to robi, niezależnie od samej operacji na plikach. Oto przykład, jak go używać:

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

lub

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

Możesz również zdefiniować liczbę ponownych prób i czas oczekiwania między kolejnymi próbami.

UWAGA: Niestety, podstawowy błąd Win32 (ERROR_SHARING_VIOLATION) nie jest ujawniany w .NET, więc dodałem małą funkcję hackowania ( IsSharingViolation) opartą na mechanizmach odbicia, aby to sprawdzić.

    /// <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;
    }
Simon Mourier
źródło
5
Naprawdę mogli zapewnić SharingViolationException. W rzeczywistości nadal mogą, w sposób zgodny z poprzednimi wersjami, o ile pochodzi z IOException. I naprawdę, naprawdę powinni.
Roman Starkov
6
Marshal.GetHRForException msdn.microsoft.com/en-us/library/…
Steven T. Cramer
9
W programie .NET Framework 4.5, .NET Standard i .NET Core HResult jest właściwością publiczną klasy Exception. Nie jest już do tego potrzebna refleksja. Z 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

Zorganizowałem razem zajęcia pomocnicze na tego typu rzeczy. Będzie działać, jeśli masz kontrolę nad wszystkim, co ma dostęp do pliku. Jeśli spodziewasz się sporu z wielu innych rzeczy, to jest to bezwartościowe.

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
}

Działa przy użyciu nazwanego muteksu. Ci, którzy chcą uzyskać dostęp do pliku, próbują przejąć kontrolę nad nazwanym muteksem, który ma wspólną nazwę pliku (z '\' zamienionymi na \ '). Możesz użyć Open (), która będzie się zatrzymywać, dopóki mutex nie będzie dostępny, lub możesz użyć TryOpen (TimeSpan), który próbuje uzyskać muteks na określony czas i zwraca false, jeśli nie może uzyskać w określonym przedziale czasu. Najprawdopodobniej powinno to być używane wewnątrz bloku using, aby zapewnić prawidłowe zwolnienie blokad, a strumień (jeśli jest otwarty) zostanie prawidłowo usunięty, gdy ten obiekt zostanie usunięty.

Zrobiłem szybki test z ~ 20 rzeczami, aby wykonać różne odczyty / zapisy pliku i nie zauważyłem żadnych uszkodzeń. Oczywiście nie jest bardzo zaawansowany, ale powinien działać w większości prostych przypadków.

user152791
źródło
5

W przypadku tej konkretnej aplikacji bezpośrednia obserwacja pliku nieuchronnie prowadzi do trudnego do wyśledzenia błędu, zwłaszcza gdy zwiększa się rozmiar pliku. Oto dwie różne strategie, które będą działać.

  • Ftp dwa pliki, ale oglądaj tylko jeden. Na przykład wyślij pliki ważne.txt i ważne.finish. Uważaj tylko na plik końcowy, ale przetwórz plik txt.
  • Prześlij jeden plik na FTP, ale po zakończeniu zmień jego nazwę. Na przykład wyślij ważne.wait i poproś nadawcę o zmianę nazwy na ważne.txt po zakończeniu.

Powodzenia!

jason saldo
źródło
To jest przeciwieństwo automatycznego. To tak, jakby ręcznie pobrać plik, z większą liczbą kroków.
HackSlash
4

Jedną z technik, których użyłem kiedyś, było napisanie własnej funkcji. Zasadniczo złap wyjątek i spróbuj ponownie, używając timera, który możesz odpalić przez określony czas. Jeśli istnieje lepszy sposób, udostępnij.

Gulzar Nazim
źródło
3

Z MSDN :

Zdarzenie OnCreated jest zgłaszane zaraz po utworzeniu pliku. Jeśli plik jest kopiowany lub przesyłany do obserwowanego katalogu, zdarzenie OnCreated zostanie zgłoszone natychmiast, po którym nastąpi jedno lub więcej zdarzeń OnChanged.

Twój FileSystemWatcher może zostać zmodyfikowany, aby nie wykonywał odczytu / zmiany nazwy podczas zdarzenia „OnCreated”, ale raczej:

  1. Obejmuje wątek, który sonduje stan pliku, dopóki nie zostanie zablokowany (przy użyciu obiektu FileInfo)
  2. Wzywa z powrotem do usługi w celu przetworzenia pliku, gdy tylko stwierdzi, że plik nie jest już zablokowany i jest gotowy do pracy
Guy Starbuck
źródło
1
Odrodzenie wątku obserwatora systemu plików może doprowadzić do przepełnienia podstawowego bufora, a tym samym do utraty wielu zmienionych plików. Lepszym podejściem będzie utworzenie kolejki konsument / producent.
Nissim
2

W większości przypadków proste podejście, takie jak sugerowane przez @harpo, będzie działać. Możesz opracować bardziej wyrafinowany kod, stosując następujące podejście:

  • Znajdź wszystkie otwarte dojścia dla wybranego pliku przy użyciu SystemHandleInformation \ SystemProcessInformation
  • Podklasa WaitHandle, aby uzyskać dostęp do jej wewnętrznego uchwytu
  • Przekaż znalezione uchwyty zawinięte w podklasę WaitHandle do metody WaitHandle.WaitAny
aku
źródło
2

Reklama do przesłania pliku wyzwalającego proces SameNameASTrasferedFile.trg, który jest tworzony po zakończeniu transmisji pliku.

Następnie skonfiguruj FileSystemWatcher, który będzie uruchamiał zdarzenie tylko w pliku * .trg.

Rudi
źródło
1

Nie wiem, czego używasz do określenia stanu blokady pliku, ale coś takiego powinno wystarczyć.

podczas gdy (prawda)
{
    próbować {
        stream = Plik.Open (nazwa_pliku, tryb_pliku);
        przerwa;
    }
    catch (FileIOException) {

        // sprawdź, czy to problem z blokadą

        Thread.Sleep (100);
    }
}
harpo
źródło
1
Trochę późno, ale kiedy plik zostanie w jakiś sposób zablokowany, nigdy nie wyjdziesz z pętli. Powinieneś dodać licznik (patrz pierwsza odpowiedź).
Peter,
0

Możliwym rozwiązaniem byłoby połączenie obserwatora systemu plików z pewnym odpytywaniem,

otrzymuj powiadomienie o każdej zmianie w pliku, a po otrzymaniu powiadomienia sprawdź, czy jest zablokowane zgodnie z aktualnie zaakceptowaną odpowiedzią: https://stackoverflow.com/a/50800/6754146 Kod do otwarcia strumienia pliku jest kopiowany z odpowiedzi i lekko zmodyfikowane:

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

W ten sposób możesz sprawdzić, czy plik jest zablokowany i otrzymać powiadomienie po jego zamknięciu przez określone wywołanie zwrotne, w ten sposób unikniesz zbyt agresywnego odpytywania i wykonujesz pracę tylko wtedy, gdy może być faktycznie zamknięty

Florian K
źródło
-1

Robię to tak samo, jak Gulzar, po prostu próbuj dalej z pętlą.

W rzeczywistości nawet nie zawracam sobie głowy obserwatorem systemu plików. Sondowanie dysku sieciowego w poszukiwaniu nowych plików raz na minutę jest tanie.

Jonathan Allen
źródło
2
To może być tanie, ale jedna minuta to za długo dla wielu aplikacji. Czasami niezbędne jest monitorowanie w czasie rzeczywistym. Zamiast implementować coś, co będzie nasłuchiwać komunikatów systemu plików w C # (nie jest to najwygodniejszy język do tych rzeczy), używasz FSW.
ThunderGr
-1

Po prostu użyj zdarzenia Changed z NotifyFilter NotifyFilters.LastWrite :

var watcher = new FileSystemWatcher {
      Path = @"c:\temp\test",
      Filter = "*.xml",
      NotifyFilter = NotifyFilters.LastWrite
};
watcher.Changed += watcher_Changed; 
watcher.EnableRaisingEvents = true;
Bernhard Hochgatterer
źródło
1
FileSystemWatcher nie tylko powiadamia o zakończeniu zapisu pliku. Często powiadamia Cię kilka razy o „pojedynczym” logicznym zapisie, a jeśli spróbujesz otworzyć plik po otrzymaniu pierwszego powiadomienia, pojawi się wyjątek.
Ross
-1

Napotkałem podobny problem podczas dodawania załącznika Outlook. „Korzystanie” uratowało dzień.

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);
Jahmal23
źródło
-3

Co powiesz na to jako opcję:

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

Oczywiście, jeśli rozmiar pliku jest wstępnie przydzielony do tworzenia, otrzymasz fałszywie dodatni wynik.

Ralph Shillington
źródło
1
Jeśli proces zapisywania do pliku zatrzymuje się na dłużej niż sekundę lub buforuje pamięć na dłużej niż sekundę, otrzymasz kolejny fałszywie dodatni wynik. Nie sądzę, żeby to było dobre rozwiązanie w żadnych okolicznościach.
Chris Wenham,