Attendez que le fichier soit déverrouillé dans .NET

103

Quel est le moyen le plus simple de bloquer un thread jusqu'à ce qu'un fichier ait été déverrouillé et soit accessible pour la lecture et le changement de nom? Par exemple, y a-t-il un WaitOnFile () quelque part dans le .NET Framework?

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

La solution idéale aurait un délai d'expiration pour que le fil ne se bloque pas indéfiniment avant d'abandonner.

Edit: Après avoir essayé certaines des solutions ci-dessous, j'ai fini par changer le système afin que tous les fichiers soient écrits Path.GetTempFileName(), puis exécutés File.Move()à l'emplacement final. Dès que l' FileSystemWatcherévénement s'est déclenché, le fichier était déjà complet.

Chris Wenham
la source
4
Depuis la sortie de .NET 4.0, existe-t-il un meilleur moyen de résoudre ce problème?
jason

Réponses:

40

Voici la réponse que j'ai donnée à une 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;
    }
Eric Z Beard
la source
8
Je trouve cela moche mais la seule solution possible
knoopx
6
Cela va-t-il vraiment fonctionner dans le cas général? si vous ouvrez le fichier dans une clause using (), le fichier est fermé et déverrouillé à la fin de la portée using. S'il y a un deuxième processus utilisant la même stratégie que celle-ci (réessayer à plusieurs reprises), puis après la sortie de WaitForFile (), il y a une condition de concurrence pour savoir si le fichier sera ouvert ou non. Non?
Cheeso
75
Mauvaise idée! Bien que le concept soit correct, une meilleure solution sera de renvoyer le FileStream au lieu d'un bool. Si le fichier est à nouveau verrouillé avant que l'utilisateur n'ait une chance d'obtenir son verrou sur le fichier - il obtiendra une exception même si la fonction a renvoyé "faux"
Nissim
2
où est la méthode de Fero?
Vbp le
1
Le commentaire de Nissim est exactement ce à quoi je pensais aussi, mais si vous comptez utiliser cette recherche, n'oubliez pas de la remettre à 0 après avoir lu l'octet. fs.Seek (0, SeekOrigin.Begin);
WHol
73

À 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;
}
mafu
la source
16
Je suis venu du futur pour dire que ce code fonctionne toujours comme un charme. Merci.
OnoSendai
6
@PabloCosta Exactement! Il ne peut pas le fermer, car s'il le faisait, un autre thread pourrait entrer et l'ouvrir, ce qui irait à l'encontre de l'objectif. Cette implémentation est correcte car elle la maintient ouverte! Laisser l'appelant s'inquiéter à ce sujet, il est prudent de usingsur un null, vérifiez simplement la présence de null à l'intérieur du usingbloc.
doug65536
2
"FileStream fs = null;" doit être déclaré en dehors de l'essai mais à l'intérieur du for. Ensuite, attribuez et utilisez fs à l'intérieur du try. Le bloc catch doit faire "if (fs! = Null) fs.Dispose ();" (ou juste fs? .Dispose () en C # 6) pour s'assurer que le FileStream qui n'est pas renvoyé est nettoyé correctement.
Bill Menees
1
Est-il vraiment nécessaire de lire un octet? D'après mon expérience, si vous avez ouvert le fichier pour un accès en lecture, vous l'avez, vous n'avez pas à le tester. Bien qu'avec la conception ici, vous ne forcez pas l'accès exclusif, il est donc même possible que vous puissiez lire le premier octet, mais pas les autres (verrouillage au niveau des octets). À partir de la question d'origine, vous êtes susceptible d'ouvrir avec un niveau de partage en lecture seule, de sorte qu'aucun autre processus ne peut verrouiller ou modifier le fichier. En tout cas, je pense que le fs.ReadByte () est soit un gaspillage complet, soit pas assez, selon l'utilisation.
eselk
8
Utilisateur quelle circonstance ne peut fsêtre nulle dans le catchbloc? Si le FileStreamconstructeur lance, la variable ne recevra pas de valeur, et il n'y a rien d'autre à l'intérieur du tryqui puisse lancer un IOException. Pour moi, il semble que ce devrait être juste de le faire return new FileStream(...).
Matti Virkkunen
18

Voici un code générique pour ce faire, indépendant de l'opération de fichier elle-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.

REMARQUE: 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ée 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;
    }
Simon Mourier
la source
5
Ils auraient vraiment pu fournir un fichier SharingViolationException. En fait, ils le peuvent encore, de manière rétrocompatible, tant qu'il descend de IOException. Et ils devraient vraiment, vraiment.
Roman Starkov
6
Marshal.GetHRForException msdn.microsoft.com/en-us/library/…
Steven T. Cramer
9
Dans .NET Framework 4.5, .NET Standard et .NET Core, HResult est une propriété publique de la classe Exception. La réflexion n'est plus nécessaire pour cela. De 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

J'ai organisé une classe d'aide pour ce genre de choses. Cela fonctionnera si vous avez le contrôle sur tout ce qui accède au fichier. Si vous vous attendez à une controverse de la part d'autres choses, cela ne vaut pratiquement 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 bloquera jusqu'à ce que le mutex soit accessible, soit vous pouvez utiliser TryOpen (TimeSpan), qui essaie d'acquérir le mutex pour la durée donnée et retourne false s'il ne peut pas acquérir dans le laps de temps. Cela devrait très probablement être utilisé à l'intérieur d'un bloc using, pour garantir que les verrous sont libérés correctement et que le flux (s'il est ouvert) sera correctement éliminé lorsque cet objet sera supprimé.

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

user152791
la source
5

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

  • Ftp deux fichiers mais n'en regardez qu'un. Par exemple, envoyez les fichiers important.txt et important.finish. Surveillez uniquement le fichier final 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 en important.txt lorsque vous avez terminé.

Bonne chance!

Jason Saldo
la source
C'est le contraire de l'automatique. C'est comme obtenir manuellement le fichier, avec plus d'étapes.
HackSlash
4

L'une des techniques que j'ai utilisées il y a quelque temps était d'écrire ma propre fonction. En gros, interceptez l'exception et réessayez à l'aide d'un minuteur que vous pouvez déclencher pendant une durée spécifiée. S'il y a un meilleur moyen, veuillez partager.

Gulzar Nazim
la source
3

Depuis MSDN :

L'événement OnCreated est déclenché dès qu'un fichier est créé. Si un fichier est copié ou transféré dans un répertoire surveillé, l'événement OnCreated sera déclenché immédiatement, suivi d'un ou plusieurs événements OnChanged.

Votre FileSystemWatcher pourrait être modifié afin qu'il ne fasse pas sa lecture / renommée pendant 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 au service pour traiter le fichier dès qu'il détermine que le fichier n'est plus verrouillé et est prêt à être utilisé
Guy Starbuck
la source
1
La création du thread de l'observateur de système de fichiers peut entraîner un débordement de la mémoire tampon sous-jacente, manquant ainsi beaucoup de fichiers modifiés. Une meilleure approche sera de créer une file d'attente consommateur / producteur.
Nissim
2

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

  • Trouvez toutes les poignées ouvertes pour le fichier sélectionné à l'aide de SystemHandleInformation \ SystemProcessInformation
  • Sous-classe WaitHandle classe pour accéder à son handle interne
  • Passer les handles trouvés encapsulés dans la sous-classe WaitHandle à la méthode WaitHandle.WaitAny
aku
la source
2

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

Ensuite, configurez FileSystemWatcher qui déclenchera l'événement uniquement sur le fichier * .trg.

Rudi
la source
1

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.

tandis que (vrai)
{
    essayez {
        stream = File.Open (fileName, fileMode);
        Pause;
    }
    catch (FileIOException) {

        // vérifie s'il s'agit d'un problème de verrouillage

        Thread.Sleep (100);
    }
}
harpo
la source
1
Un peu tard, mais lorsque le fichier est verrouillé, vous ne quitterez jamais votre boucle. Vous devez ajouter un compteur (voir 1ère réponse).
Peter
0

Une solution possible serait de combiner un observateur de système de fichiers avec une interrogation,

être notifié pour chaque changement sur un fichier, et lorsque vous recevez une notification, vérifiez s'il est verrouillé comme indiqué dans la réponse actuellement acceptée: https://stackoverflow.com/a/50800/6754146 Le code d'ouverture du flux de fichiers est copié à partir 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 si un fichier est verrouillé et être averti lorsqu'il est fermé au cours du rappel spécifié, de cette façon, vous évitez l'interrogation trop agressive et ne faites le travail que lorsqu'il est effectivement fermé.

Florian K
la source
-1

Je le fais de la même manière que Gulzar, continuez d'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 pour les nouveaux fichiers une fois par minute n'est pas cher.

Jonathan Allen
la source
2
Cela peut être bon marché mais une fois par minute est trop long pour de nombreuses applications. La surveillance en temps réel est parfois essentielle. Au lieu de devoir implémenter quelque chose qui écoutera les messages du système de fichiers en C # (pas le langage le plus pratique pour ces choses), vous utilisez FSW.
ThunderGr
-1

Utilisez simplement l' événement Changed avec 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
la source
1
FileSystemWatcher ne notifie pas seulement quand un fichier est terminé en cours d'écriture. Il vous notifiera souvent plusieurs fois pour une écriture logique «unique», et si vous essayez d'ouvrir le fichier après avoir reçu la première notification, vous obtiendrez une exception.
Ross
-1

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

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
la source
-3

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éallouée lors de la création, vous obtiendrez un faux positif.

Ralph Shillington
la source
1
Si le processus d'écriture dans le fichier s'arrête pendant plus d'une seconde ou si les tampons en mémoire pendant plus d'une seconde, vous obtiendrez un autre faux positif. Je ne pense pas que ce soit une bonne solution en aucune circonstance.
Chris Wenham