Question

Quelle est la manière la plus simple de bloquer un fil de discussion jusqu'à ce qu'un fichier soit déverrouillé et soit accessible en lecture et en renommage ?Par exemple, existe-t-il un WaitOnFile() quelque part dans le .NET Framework ?

J'ai un service qui utilise FileSystemWatcher pour rechercher les fichiers à transmettre vers un site FTP, mais le fichier créé L'événement se déclenche avant que l'autre processus n'ait fini d'écrire le fichier.

La solution idéale aurait un délai d'attente afin que le thread ne se bloque pas éternellement avant d'abandonner.

Modifier:Après avoir essayé certaines des solutions ci-dessous, j'ai fini par changer le système pour que tous les fichiers soient écrits dans Path.GetTempFileName(), puis effectué un File.Move() jusqu'à l'emplacement final.Aussi tôt que le FileSystemWatcher événement déclenché, le fichier était déjà complet.

Était-ce utile?

La solution

C'est la réponse que j'ai donnée lors d'un question connexe:

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

Autres conseils

À partir de la réponse d'Eric, j'ai inclus quelques améliorations pour rendre le code beaucoup plus compact et réutilisable.J'espère que c'est 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;
}

Voici un code générique pour ce faire, indépendant de l'opération sur le fichier lui-même.Voici un exemple d'utilisation :

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

ou

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

Vous pouvez également définir le nombre de tentatives et le temps d'attente entre les tentatives.

NOTE:Malheureusement, l'erreur Win32 sous-jacente (ERROR_SHARING_VIOLATION) n'est pas exposée avec .NET, j'ai donc ajouté une petite fonction de hack (IsSharingViolation) basé sur des mécanismes de réflexion pour vérifier cela.

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

J'ai créé une classe d'aide pour ce genre de choses.Cela fonctionnera si vous contrôlez tout ce qui accède au fichier.Si vous vous attendez à des conflits à partir d'un tas d'autres choses, alors cela ne vaut rien.

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
}

Cela fonctionne en utilisant un mutex nommé.Ceux qui souhaitent accéder au fichier tentent d'acquérir le contrôle du mutex nommé, qui partage le nom du fichier (avec les '\' transformés en '/').Vous pouvez soit utiliser Open(), qui s'arrêtera jusqu'à ce que le mutex soit accessible, soit utiliser TryOpen(TimeSpan), qui tente d'acquérir le mutex pour la durée donnée et renvoie false s'il ne peut pas acquérir dans le délai imparti.Cela devrait très probablement être utilisé à l'intérieur d'un bloc using, pour garantir que les verrous sont correctement libérés et que le flux (s'il est ouvert) sera correctement supprimé lorsque cet objet est supprimé.

J'ai fait un test rapide avec environ 20 choses pour effectuer diverses lectures/écritures du fichier et je n'ai vu aucune corruption.Ce n’est évidemment pas très avancé, mais cela devrait fonctionner pour la majorité des cas simples.

Pour cette application particulière, l'observation directe du fichier entraînera inévitablement un bug difficile à retracer, surtout lorsque la taille du fichier augmente.Voici deux stratégies différentes qui fonctionneront.

  • FTP deux fichiers mais n'en regarder qu'un.Par exemple envoyez les fichiers important.txt et important.finish.Surveillez uniquement le fichier de finition mais traitez le txt.
  • FTP un fichier mais renommez-le une fois terminé.Par exemple, envoyez important.wait et demandez à l'expéditeur de le renommer important.txt une fois terminé.

Bonne chance!

L'une des techniques que j'ai utilisées il y a quelque temps consistait à écrire ma propre fonction.Fondamentalement, interceptez l'exception et réessayez en utilisant une minuterie que vous pouvez déclencher pour une durée spécifiée.S'il existe une meilleure façon, partagez-la.

Depuis MSDN:

L'événement sur surnature est soulevé dès qu'un fichier est créé.Si un fichier est copié ou transféré dans un répertoire regardé, l'événement sur place sera soulevé immédiatement, suivi par un ou plusieurs événements inchangés.

Votre FileSystemWatcher pourrait être modifié pour qu'il ne fasse pas sa lecture/renommage lors de l'événement "OnCreated", mais plutôt :

  1. Spanws un thread qui interroge l'état du fichier jusqu'à ce qu'il ne soit pas verrouillé (à l'aide d'un objet FileInfo)
  2. Rappelle le service pour traiter le fichier dès qu'il détermine que le fichier n'est plus verrouillé et qu'il est prêt à être utilisé

Dans la plupart des cas, une approche simple comme celle suggérée par @harpo fonctionnera.Vous pouvez développer du code plus sophistiqué en utilisant cette approche :

  • Recherchez tous les handles ouverts pour le fichier sélectionné à l’aide de SystemHandleInformation\SystemProcessInformation
  • Sous-classe WaitHandle pour accéder à son handle interne
  • Transmettre les handles trouvés enveloppés dans la sous-classe WaitHandle à la méthode WaitHandle.WaitAny

AD pour transférer le fichier de déclenchement du processus SAmenameasstrasferedfile.trg qui est créé une fois la transmission de fichiers terminée.

Configurez ensuite FileSystemWatcher qui déclenchera l'événement uniquement sur le fichier *.trg.

Je ne sais pas ce que vous utilisez pour déterminer l'état de verrouillage du fichier, mais quelque chose comme ça devrait le faire.

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

        // check whether it's a lock problem

        Thread.Sleep( 100 );
    }
}

Une solution possible serait de combiner un observateur de système de fichiers avec des interrogations,

Soyez avisé pour chaque modification d'un fichier, et lorsque vous êtes averti, vérifiez s'il est verrouillé comme indiqué dans la réponse actuellement acceptée: https://stackoverflow.com/a/50800/6754146Le code d'ouverture du filestream est copié de la réponse et légèrement modifié :

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

De cette façon, vous pouvez vérifier qu'un fichier est verrouillé et être averti lorsqu'il est fermé via le rappel spécifié. De cette façon, vous évitez les interrogations trop agressives et n'effectuez le travail que lorsqu'il est réellement fermé.

Je le fais de la même manière que Gulzar, continuez simplement à essayer avec une boucle.

En fait, je ne me soucie même pas de l'observateur du système de fichiers.Interroger un lecteur réseau à la recherche de nouveaux fichiers une fois par minute est peu coûteux.

Utilisez simplement le Modifié événement avec le NotifyFilter NotifyFilters.LastWrite:

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

J'ai rencontré un problème similaire lors de l'ajout d'une pièce jointe Outlook."Utiliser" a sauvé la mise.

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

Que diriez-vous de ceci en 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);
    }
}

Bien sûr, si la taille du fichier est pré-attribuée lors de la création, vous obtiendrez un faux positif.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top