Attendi che il file venga sbloccato in .NET


103

Qual è il modo più semplice per bloccare un thread fino a quando un file non è stato sbloccato ed è accessibile per la lettura e la ridenominazione? Ad esempio, c'è un WaitOnFile () da qualche parte in .NET Framework?

Ho un servizio che utilizza un FileSystemWatcher per cercare i file che devono essere trasmessi a un sito FTP, ma l' evento di file creato si attiva prima che l'altro processo abbia finito di scrivere il file.

La soluzione ideale avrebbe un periodo di timeout in modo che il thread non si blocchi per sempre prima di arrendersi.

Modifica: dopo aver provato alcune delle soluzioni di seguito, ho finito per cambiare il sistema in modo che tutti i file scrivessero Path.GetTempFileName(), quindi eseguissi un File.Move()nella posizione finale. Non appena si è verificato l' FileSystemWatcherevento, il file era già completo.


4
Dal rilascio di .NET 4.0, esiste un modo migliore per risolvere questo problema?
jason

Risposte:


40

Questa è stata la risposta che ho dato a una domanda correlata :

    /// <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
Trovo questa brutta ma l'unica soluzione possibile
knoopx

6
Funzionerà davvero nel caso generale? se apri il file in una clausola using (), il file viene chiuso e sbloccato quando l'ambito using termina. Se è presente un secondo processo che utilizza la stessa strategia di questo (riprovare ripetutamente), dopo l'uscita da WaitForFile (), c'è una condizione di competizione per quanto riguarda se il file sarà apribile o meno. No?
Cheeso

75
Cattiva idea! Sebbene il concetto sia corretto, una soluzione migliore sarà restituire FileStream invece di un bool. Se il file viene bloccato di nuovo prima che l'utente abbia la possibilità di ottenere il suo blocco sul file, riceverà un'eccezione anche se la funzione ha restituito "false"
Nissim

2
dov'è il metodo di Fero?
Vbp

1
Il commento di Nissim è esattamente quello che stavo pensando anch'io, ma se hai intenzione di usare quella ricerca, non dimenticare di reimpostarla a 0 dopo aver letto il byte. fs.Seek (0, SeekOrigin.Begin);
WHol

73

Partendo dalla risposta di Eric, ho incluso alcuni miglioramenti per rendere il codice molto più compatto e riutilizzabile. Spero sia utile.

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
Vengo dal futuro per dire che questo codice funziona ancora come un incantesimo. Grazie.
OnoSendai

6
@PabloCosta Esattamente! Non può chiuderlo, perché se lo facesse, un altro thread potrebbe entrare e aprirlo, vanificando lo scopo. Questa implementazione è corretta perché la mantiene aperta! Lascia che il chiamante se ne preoccupi, è sicuro usingsu un valore nullo, controlla semplicemente se nullo all'interno del usingblocco.
doug65536

2
"FileStream fs = null;" dovrebbe essere dichiarato all'esterno della try ma all'interno del for. Quindi assegna e usa fs all'interno del try. Il blocco catch dovrebbe fare "if (fs! = Null) fs.Dispose ();" (o semplicemente fs? .Dispose () in C # 6) per garantire che il FileStream che non viene restituito venga pulito correttamente.
Bill Menees

1
È davvero necessario leggere un byte? Nella mia esperienza, se hai aperto il file per l'accesso in lettura, ce l'hai, non devi provarlo. Anche se con il design qui non stai forzando l'accesso esclusivo, quindi è anche possibile che tu possa essere in grado di leggere il primo byte, ma nessun altro (blocco a livello di byte). È probabile che dalla domanda originale si apra con il livello di condivisione di sola lettura, quindi nessun altro processo può bloccare o modificare il file. In ogni caso, ritengo che fs.ReadByte () sia uno spreco completo o non abbastanza, a seconda dell'uso.
eselk

8
Utente quale circostanza può fsnon essere nulla nel catchblocco? Se il FileStreamcostruttore lancia, alla variabile non verrà assegnato un valore e non c'è nient'altro dentro tryche può lanciare un IOException. A me sembra che dovrebbe essere giusto farlo return new FileStream(...).
Matti Virkkunen

18

Ecco un codice generico per farlo, indipendente dall'operazione sul file stesso. Questo è un esempio su come usarlo:

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

o

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

È inoltre possibile definire il numero di tentativi e il tempo di attesa tra i tentativi.

NOTA: Sfortunatamente, l'errore Win32 sottostante (ERROR_SHARING_VIOLATION) non è esposto con .NET, quindi ho aggiunto una piccola funzione di hack ( IsSharingViolation) basata su meccanismi di riflessione per verificarlo.

    /// <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
Avrebbero potuto davvero fornire un file SharingViolationException. In effetti, possono ancora, compatibilmente all'indietro, purché discenda da IOException. E dovrebbero davvero, davvero.
Roman Starkov


9
In .NET Framework 4.5, .NET Standard e .NET Core, HResult è una proprietà pubblica della classe Exception. La riflessione non è più necessaria per questo. Da 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

Ho organizzato una lezione di aiuto per questo genere di cose. Funzionerà se hai il controllo su tutto ciò che potrebbe accedere al file. Se ti aspetti una contesa da un mucchio di altre cose, allora questo è abbastanza inutile.

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
}

Funziona utilizzando un mutex denominato. Coloro che desiderano accedere al file tentano di acquisire il controllo del mutex denominato, che condivide il nome del file (con la "\" trasformata in "/"). È possibile utilizzare Open (), che si bloccherà fino a quando il mutex non è accessibile oppure è possibile utilizzare TryOpen (TimeSpan), che tenta di acquisire il mutex per la durata specificata e restituisce false se non può acquisire entro l'intervallo di tempo. Questo dovrebbe molto probabilmente essere usato all'interno di un blocco using, per garantire che i blocchi vengano rilasciati correttamente e il flusso (se aperto) verrà smaltito correttamente quando questo oggetto viene eliminato.

Ho fatto un rapido test con ~ 20 cose per fare varie letture / scritture del file e non ho visto alcun danneggiamento. Ovviamente non è molto avanzato, ma dovrebbe funzionare per la maggior parte dei casi semplici.


5

Per questa particolare applicazione l'osservazione diretta del file porterà inevitabilmente a un bug difficile da rintracciare, soprattutto quando la dimensione del file aumenta. Ecco due diverse strategie che funzioneranno.

  • Ftp due file ma guardane solo uno. Ad esempio, invia i file important.txt e important.finish. Controlla solo il file finale ma elabora il txt.
  • FTP un file ma rinominalo quando hai finito. Ad esempio, invia important.wait e chiedi al mittente di rinominarlo important.txt al termine.

In bocca al lupo!


Questo è l'opposto dell'automatico. È come ottenere manualmente il file, con più passaggi.
HackSlash

4

Una delle tecniche che ho usato tempo fa era scrivere la mia funzione. Fondamentalmente cattura l'eccezione e riprova utilizzando un timer che puoi attivare per una durata specificata. Se c'è un modo migliore, per favore condividi.


3

Da MSDN :

L'evento OnCreated viene generato non appena viene creato un file. Se un file viene copiato o trasferito in una directory controllata, l'evento OnCreated verrà generato immediatamente, seguito da uno o più eventi OnChanged.

Il tuo FileSystemWatcher potrebbe essere modificato in modo che non esegua la sua lettura / rinomina durante l'evento "OnCreated", ma piuttosto:

  1. Espande un thread che esegue il polling dello stato del file finché non viene bloccato (utilizzando un oggetto FileInfo)
  2. Richiama il servizio per elaborare il file non appena determina che il file non è più bloccato ed è pronto per essere utilizzato

1
La generazione del thread del filesystemwatcher può portare all'overflow del buffer sottostante, perdendo così molti file modificati. Un approccio migliore sarà creare una coda consumatore / produttore.
Nissim,

2

Nella maggior parte dei casi un approccio semplice come quello suggerito da @harpo funzionerà. Puoi sviluppare codice più sofisticato usando questo approccio:

  • Trova tutti gli handle aperti per il file selezionato utilizzando SystemHandleInformation \ SystemProcessInformation
  • Sottoclasse WaitHandle classe per ottenere l'accesso al suo handle interno
  • Passa gli handle trovati racchiusi nella sottoclasse WaitHandle al metodo WaitHandle.WaitAny

2

Annuncio per trasferire il file di attivazione del processo SameNameASTrasferedFile.trg creato dopo il completamento della trasmissione del file.

Quindi imposta FileSystemWatcher che genererà l'evento solo sul file * .trg.


1

Non so cosa stai usando per determinare lo stato di blocco del file, ma qualcosa del genere dovrebbe farlo.

mentre (vero)
{
    provare {
        stream = File.Open (fileName, fileMode);
        rompere;
    }
    catch (FileIOException) {

        // controlla se si tratta di un problema di blocco

        Thread.Sleep (100);
    }
}

1
Un po 'tardi, ma quando il file è bloccato in qualche modo non uscirai mai dal tuo ciclo. Dovresti aggiungere un contatore (vedi prima risposta).
Peter

0

Una possibile soluzione sarebbe combinare un file systemwatcher con alcuni polling,

ricevi una notifica per ogni modifica su un file e quando ricevi una notifica controlla se è bloccata come indicato nella risposta attualmente accettata: https://stackoverflow.com/a/50800/6754146 Il codice per l'apertura del filestream viene copiato dalla risposta e leggermente modificato:

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

In questo modo puoi controllare se un file è bloccato e ricevere una notifica quando viene chiuso per il callback specificato, in questo modo eviti il ​​polling eccessivamente aggressivo e fai il lavoro solo quando potrebbe essere effettivamente chiuso


-1

Lo faccio allo stesso modo di Gulzar, continua a provare con un loop.

In effetti non mi preoccupo nemmeno del controllo del file system. Il polling di un'unità di rete per nuovi file una volta al minuto è economico.


2
Può essere economico, ma una volta al minuto è troppo lungo per molte applicazioni. Il monitoraggio in tempo reale a volte è essenziale. Invece di dover implementare qualcosa che ascolterà i messaggi del file system in C # (non il linguaggio più conveniente per queste cose) usi FSW.
ThunderGr

-1

Usa semplicemente l' evento Changed con 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 non notifica solo quando un file è stato scritto. Spesso ti avviserà più volte per una "singola" scrittura logica e se provi ad aprire il file dopo aver ricevuto la prima notifica otterrai un'eccezione.
Ross

-1

Ho riscontrato un problema simile durante l'aggiunta di un allegato di Outlook. "Usare" ha salvato la giornata.

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

Che ne dici di questo come opzione:

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

Ovviamente se la dimensione del file è preallocata sulla creazione, otterrai un falso positivo.


1
Se il processo di scrittura nel file si interrompe per più di un secondo o si interrompe in memoria per più di un secondo, si otterrà un altro falso positivo. Non credo che questa sia una buona soluzione in nessuna circostanza.
Chris Wenham,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.