Domanda

Qual è il modo più semplice per bloccare un thread finché un file non viene sbloccato ed è accessibile per la lettura e la ridenominazione?Ad esempio, esiste un WaitOnFile() da qualche parte nel .NET Framework?

Ho un servizio che utilizza FileSystemWatcher per cercare file da trasmettere a un sito FTP, ma il file file creato l'evento 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.

Modificare:Dopo aver provato alcune delle soluzioni seguenti, ho finito per modificare il file sistema in modo che tutti i file vengano scritti Path.GetTempFileName(), quindi eseguito a File.Move() alla posizione finale.Appena il FileSystemWatcher evento attivato, il file era già completo.

È stato utile?

Soluzione

Questa è stata la risposta che ho dato su a 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;
    }

Altri suggerimenti

Partendo dalla risposta di Eric ho inserito alcune migliorie per rendere il codice molto più compatto e riutilizzabile.Spero che 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;
}

Ecco un codice generico per fare ciò, indipendente dall'operazione stessa sul file.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 viene esposto con .NET, quindi ho aggiunto una piccola funzione di hacking (IsSharingViolation) sulla base di 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;
    }

Ho organizzato un corso di aiuto per questo genere di cose.Funzionerà se hai il controllo su tutto ciò che accede al file.Se ti aspetti contesa da un sacco 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 le "\" trasformate in "/").Puoi utilizzare Open(), che si bloccherà finché il mutex non sarà accessibile oppure puoi utilizzare TryOpen(TimeSpan), che tenta di acquisire il mutex per la durata specificata e restituisce false se non può acquisirlo entro l'intervallo di tempo.Molto probabilmente dovrebbe essere utilizzato all'interno di un blocco using, per garantire che i blocchi vengano rilasciati correttamente e il flusso (se aperto) verrà eliminato correttamente quando questo oggetto verrà eliminato.

Ho eseguito un rapido test con circa 20 cose da eseguire in varie letture/scritture del file e non ho riscontrato alcun danneggiamento.Ovviamente non è molto avanzato, ma dovrebbe funzionare per la maggior parte dei casi semplici.

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.Guarda solo il file finale ma elabora il txt.
  • FTP un file ma rinominalo una volta terminato.Ad esempio, invia important.wait e chiedi al mittente di rinominarlo in important.txt una volta terminato.

Buona fortuna!

Una delle tecniche che ho usato qualche tempo fa era scrivere la mia funzione.Fondamentalmente rileva l'eccezione e riprova utilizzando un timer che puoi attivare per una durata specifica.Se esiste un modo migliore, condividilo.

Da MSDN:

L'evento oncreated viene sollevato non appena viene creato un file.Se un file viene copiato o trasferito in una directory osservata, l'evento oncreated verrà sollevato immediatamente, seguito da uno o più eventi on -changed.

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

  1. Spande un thread che interroga lo stato del file fino a quando non viene bloccato (usando 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

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

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

AD per trasferimento del processo TRIGGER File samenameastrasferedfile.trg che viene creato dopo il completamento della trasmissione dei file.

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

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

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

        // check whether it's a lock problem

        Thread.Sleep( 100 );
    }
}

Una possibile soluzione sarebbe quella di combinare un filesystemwatcher con qualche polling,

Ricevi una notifica per ogni modifica su un file e quando viene notificato, controlla se è bloccato come indicato nella risposta attualmente accettata: https://stackoverflow.com/a/50800/6754146Il codice per aprire il 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 verificare la presenza di un file se è bloccato e ricevere una notifica quando viene chiuso durante il callback specificato, in questo modo eviti il ​​polling eccessivamente aggressivo e fai il lavoro solo quando potrebbe essere effettivamente chiuso

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

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

Usa semplicemente il Cambiato evento con NotifyFilter NotifyFilters.LastWrite:

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

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

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

Che ne dici di questa 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 al momento della creazione otterresti un falso positivo.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top