Warten Sie, bis die Datei in .NET entsperrt ist

103

Was ist der einfachste Weg, einen Thread zu blockieren, bis eine Datei entsperrt wurde und zum Lesen und Umbenennen zugänglich ist? Gibt es beispielsweise irgendwo in .NET Framework ein WaitOnFile ()?

Ich habe einen Dienst, der einen FileSystemWatcher verwendet, um nach Dateien zu suchen, die an eine FTP-Site übertragen werden sollen. Das Ereignis " Datei erstellt" wird jedoch ausgelöst, bevor der andere Prozess das Schreiben der Datei abgeschlossen hat.

Die ideale Lösung hätte eine Zeitüberschreitung, damit der Thread nicht für immer hängt, bevor er aufgibt.

Bearbeiten: Nachdem ich einige der folgenden Lösungen ausprobiert hatte, änderte ich das System so, dass alle Dateien darauf geschrieben wurden, Path.GetTempFileName()und führte dann eine File.Move()bis zum endgültigen Speicherort durch. Sobald das FileSystemWatcherEreignis ausgelöst wurde, war die Datei bereits vollständig.

Chris Wenham
quelle
4
Gibt es seit der Veröffentlichung von .NET 4.0 einen besseren Weg, um dieses Problem zu lösen?
Jason

Antworten:

40

Dies war die Antwort, die ich auf eine verwandte Frage gab :

    /// <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 Bart
quelle
7
Ich finde das hässlich, aber die einzig mögliche Lösung
Knoopx
5
Wird das im allgemeinen Fall wirklich funktionieren? Wenn Sie die Datei in einer using () -Klausel öffnen, wird die Datei geschlossen und entsperrt, wenn der using-Bereich endet. Wenn es einen zweiten Prozess gibt, der dieselbe Strategie wie diese verwendet (wiederholt wiederholen), gibt es nach dem Beenden von WaitForFile () eine Race-Bedingung, ob die Datei geöffnet werden kann oder nicht. Nein?
Cheeso
74
Schlechte Idee! Während das Konzept richtig ist, besteht eine bessere Lösung darin, den FileStream anstelle eines Bools zurückzugeben. Wenn die Datei erneut gesperrt wird, bevor der Benutzer die Möglichkeit hat, die Datei zu sperren, wird er eine Ausnahme erhalten, selbst wenn die Funktion "false"
zurückgibt
2
Wo ist Feros Methode?
Vbp
1
Nissims Kommentar ist genau das, was ich auch gedacht habe, aber wenn Sie diese Suche verwenden möchten, vergessen Sie nicht, sie nach dem Lesen des Bytes auf 0 zurückzusetzen. fs.Seek (0, SeekOrigin.Begin);
Ganz
73

Ausgehend von Erics Antwort habe ich einige Verbesserungen vorgenommen, um den Code weitaus kompakter und wiederverwendbarer zu machen. Hoffe es ist nützlich.

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
quelle
16
Ich bin aus der Zukunft gekommen, um zu sagen, dass dieser Code immer noch wie ein Zauber wirkt. Vielen Dank.
OnoSendai
6
@PabloCosta Genau! Es kann es nicht schließen, denn wenn dies der Fall ist, könnte ein anderer Thread hineinrasen und es öffnen, wodurch der Zweck zunichte gemacht wird. Diese Implementierung ist korrekt, weil sie offen bleibt! Lassen Sie den Anrufer sich darüber Sorgen machen, es ist sicher, usingeine Null zu verwenden. Überprüfen Sie einfach den usingBlock auf Null .
Doug65536
2
"FileStream fs = null;" sollte außerhalb des Versuchs deklariert werden, aber innerhalb des for. Weisen Sie dann fs im Versuch zu und verwenden Sie sie. Der catch-Block sollte "if (fs! = Null) fs.Dispose ();" (oder nur fs? .Dispose () in C # 6), um sicherzustellen, dass der nicht zurückgegebene FileStream ordnungsgemäß bereinigt wird.
Bill Menees
1
Ist es wirklich notwendig, ein Byte zu lesen? Wenn Sie die Datei für den Lesezugriff geöffnet haben, müssen Sie sie meiner Erfahrung nach nicht testen. Obwohl Sie mit dem Design hier keinen exklusiven Zugriff erzwingen, ist es sogar möglich, dass Sie das erste Byte lesen können, aber keine anderen (Sperren auf Byte-Ebene). Ab der ursprünglichen Frage werden Sie wahrscheinlich mit schreibgeschützter Freigabestufe geöffnet, sodass kein anderer Prozess die Datei sperren oder ändern kann. Auf jeden Fall halte ich fs.ReadByte () für eine völlige Verschwendung oder nicht genug, je nach Verwendung.
Eselk
8
Benutzer, welcher Umstand kann fsim catchBlock nicht null sein ? Wenn der FileStreamKonstruktor auslöst, wird der Variablen kein Wert zugewiesen, und es gibt nichts anderes in der try, das einen auslösen kann IOException. Mir scheint, es sollte in Ordnung sein, es einfach zu tun return new FileStream(...).
Matti Virkkunen
18

Hier ist ein generischer Code, um dies zu tun, unabhängig von der Dateioperation selbst. Dies ist ein Beispiel für die Verwendung:

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

oder

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

Sie können auch die Anzahl der Wiederholungen und die Wartezeit zwischen den Wiederholungen definieren.

HINWEIS: Leider ist der zugrunde liegende Win32-Fehler (ERROR_SHARING_VIOLATION) in .NET nicht verfügbar. Daher habe ich eine kleine Hack-Funktion ( IsSharingViolation) hinzugefügt, die auf Reflexionsmechanismen basiert, um dies zu überprüfen.

    /// <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
quelle
5
Sie hätten wirklich eine zur Verfügung stellen können SharingViolationException. Tatsächlich können sie immer noch abwärtskompatibel sein, solange es abstammt IOException. Und sie sollten es wirklich, wirklich.
Roman Starkov
6
Marshal.GetHRForException msdn.microsoft.com/en-us/library/…
Steven T. Cramer
9
In .NET Framework 4.5, .NET Standard und .NET Core ist HResult eine öffentliche Eigenschaft für die Exception-Klasse. Reflexion ist dafür nicht mehr erforderlich. Von 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

Ich habe eine Helferklasse für solche Dinge zusammengestellt. Es funktioniert, wenn Sie die Kontrolle über alles haben, was auf die Datei zugreifen würde. Wenn Sie Streit von einer Reihe anderer Dinge erwarten, dann ist dies ziemlich wertlos.

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
}

Es funktioniert mit einem benannten Mutex. Diejenigen, die auf die Datei zugreifen möchten, versuchen, die Kontrolle über den benannten Mutex zu erlangen, der den Namen der Datei teilt (wobei die '\' in '/' umgewandelt werden). Sie können entweder Open () verwenden, das blockiert, bis auf den Mutex zugegriffen werden kann, oder TryOpen (TimeSpan), das versucht, den Mutex für die angegebene Dauer abzurufen und false zurückgibt, wenn er nicht innerhalb der Zeitspanne erfasst werden kann. Dies sollte höchstwahrscheinlich in einem using-Block verwendet werden, um sicherzustellen, dass Sperren ordnungsgemäß freigegeben werden und der Stream (falls geöffnet) ordnungsgemäß entsorgt wird, wenn dieses Objekt entsorgt wird.

Ich habe einen kurzen Test mit ~ 20 Dingen durchgeführt, um verschiedene Lese- / Schreibvorgänge für die Datei durchzuführen, und keine Korruption festgestellt. Natürlich ist es nicht sehr weit fortgeschritten, aber es sollte für die meisten einfachen Fälle funktionieren.

user152791
quelle
5

Bei dieser speziellen Anwendung führt das direkte Beobachten der Datei unweigerlich zu einem schwer zu verfolgenden Fehler, insbesondere wenn die Dateigröße zunimmt. Hier sind zwei verschiedene Strategien, die funktionieren werden.

  • FTP zwei Dateien, aber nur eine. Senden Sie beispielsweise die Dateien "wichtig.txt" und "wichtig.finish". Achten Sie nur auf die Enddatei, aber verarbeiten Sie den Text.
  • FTP eine Datei, aber benenne sie um, wenn du fertig bist. Senden Sie beispielsweise "Important.wait" und lassen Sie den Absender nach Abschluss in "Important.txt" umbenennen.

Viel Glück!

Jason Saldo
quelle
Das ist das Gegenteil von automatisch. Das ist wie das manuelle Abrufen der Datei mit mehr Schritten.
HackSlash
4

Eine der Techniken, die ich vor einiger Zeit verwendet habe, war das Schreiben meiner eigenen Funktion. Fangen Sie die Ausnahme ab und versuchen Sie es erneut mit einem Timer, den Sie für eine bestimmte Dauer auslösen können. Wenn es einen besseren Weg gibt, teilen Sie ihn bitte mit.

Gulzar Nazim
quelle
3

Von MSDN :

Das OnCreated-Ereignis wird ausgelöst, sobald eine Datei erstellt wird. Wenn eine Datei kopiert oder in ein überwachtes Verzeichnis übertragen wird, wird das OnCreated-Ereignis sofort ausgelöst, gefolgt von einem oder mehreren OnChanged-Ereignissen.

Ihr FileSystemWatcher kann so geändert werden, dass er während des Ereignisses "OnCreated" nicht gelesen / umbenannt wird, sondern:

  1. Spannt einen Thread, der den Dateistatus abfragt, bis er nicht mehr gesperrt ist (mithilfe eines FileInfo-Objekts).
  2. Ruft den Dienst zurück, um die Datei zu verarbeiten, sobald festgestellt wird, dass die Datei nicht mehr gesperrt und einsatzbereit ist
Guy Starbuck
quelle
1
Das Laichen des Threads des Dateisystemwatchers kann dazu führen, dass der zugrunde liegende Puffer überläuft und viele geänderte Dateien fehlen. Ein besserer Ansatz besteht darin, eine Verbraucher- / Erzeugerwarteschlange zu erstellen.
Nissim
2

In den meisten Fällen funktioniert ein einfacher Ansatz wie der von @harpo vorgeschlagene. Mit diesem Ansatz können Sie komplexeren Code entwickeln:

  • Suchen Sie mit SystemHandleInformation \ SystemProcessInformation nach allen geöffneten Handles für die ausgewählte Datei
  • Unterklasse WaitHandle-Klasse, um Zugriff auf das interne Handle zu erhalten
  • Übergeben Sie gefundene Handles, die in WaitHandle der Unterklasse eingeschlossen sind, an die WaitHandle.WaitAny-Methode
aku
quelle
2

Anzeige zum Übertragen des Prozessauslösers SameNameASTrasferedFile.trg, die nach Abschluss der Dateiübertragung erstellt wird.

Richten Sie dann FileSystemWatcher ein, das ein Ereignis nur für die * .trg-Datei auslöst.

Rudi
quelle
1

Ich weiß nicht, was Sie verwenden, um den Sperrstatus der Datei zu bestimmen, aber so etwas sollte es tun.

während (wahr)
{
    Versuchen {
        stream = File.Open (Dateiname, Dateimodus);
        brechen;
    }}
    catch (FileIOException) {

        // Überprüfen Sie, ob es sich um ein Sperrproblem handelt

        Thread.Sleep (100);
    }}
}}
Harpo
quelle
1
Ein bisschen spät, aber wenn die Datei irgendwie gesperrt ist, werden Sie Ihre Schleife nie verlassen. Sie sollten einen Zähler hinzufügen (siehe 1. Antwort).
Peter
0

Eine mögliche Lösung wäre, einen Dateisystemwatcher mit einigen Abfragen zu kombinieren.

Bei jeder Änderung an einer Datei benachrichtigt werden. Wenn Sie benachrichtigt werden, überprüfen Sie, ob diese gemäß der aktuell akzeptierten Antwort gesperrt ist: https://stackoverflow.com/a/50800/6754146 Der Code zum Öffnen des Dateistreams wird aus der Antwort kopiert und leicht modifiziert:

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

Auf diese Weise können Sie nach einer gesperrten Datei suchen und benachrichtigt werden, wenn sie über den angegebenen Rückruf geschlossen wird. Auf diese Weise vermeiden Sie übermäßig aggressive Abfragen und erledigen die Arbeit nur, wenn sie möglicherweise tatsächlich geschlossen wird

Florian K.
quelle
-1

Ich mache es genauso wie Gulzar, versuche es einfach weiter mit einer Schleife.

Tatsächlich kümmere ich mich nicht einmal um den Dateisystem-Watcher. Das Abfragen eines Netzwerklaufwerks nach neuen Dateien einmal pro Minute ist günstig.

Jonathan Allen
quelle
2
Es mag billig sein, aber einmal pro Minute ist zu lang für viele Anwendungen. Manchmal ist eine Echtzeitüberwachung unerlässlich. Anstatt etwas implementieren zu müssen, das auf Dateisystemnachrichten in C # wartet (nicht die bequemste Sprache für diese Dinge), verwenden Sie FSW.
ThunderGr
-1

Verwenden Sie einfach das geänderte Ereignis mit dem 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
quelle
1
FileSystemWatcher benachrichtigt nicht nur, wenn das Schreiben einer Datei abgeschlossen ist. Sie werden häufig mehrmals über einen "einzelnen" logischen Schreibvorgang benachrichtigt. Wenn Sie versuchen, die Datei nach Erhalt der ersten Benachrichtigung zu öffnen, wird eine Ausnahme angezeigt.
Ross
-1

Beim Hinzufügen eines Outlook-Anhangs ist ein ähnliches Problem aufgetreten. "Using" hat den Tag gerettet.

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
quelle
-3

Wie wäre es damit als Option:

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

Wenn die Dateigröße beim Erstellen vorab zugewiesen wird, erhalten Sie natürlich ein falsches Positiv.

Ralph Shillington
quelle
1
Wenn der Prozess, der in die Datei schreibt, länger als eine Sekunde pausiert oder länger als eine Sekunde im Speicher puffert, erhalten Sie ein weiteres falsches Positiv. Ich denke nicht, dass dies unter keinen Umständen eine gute Lösung ist.
Chris Wenham