38 votes

Erreur d'accès aux fichiers avec FileSystemWatcher lorsque plusieurs fichiers sont ajoutés à un répertoire

Je rencontre un problème avec un FileSystemWatcher lorsque plusieurs fichiers sont placés dans le répertoire surveillé. Je veux analyser le fichier dès qu'il est placé dans le répertoire. En général, le premier fichier est bien analysé, mais l'ajout d'un deuxième fichier au répertoire pose un problème d'accès. Parfois, le premier fichier n'est même pas analysé. Une seule application est en cours d'exécution et surveille ce répertoire. À terme, ce processus sera exécuté sur plusieurs machines qui surveilleront un répertoire partagé, mais un seul serveur pourra analyser chaque fichier, car les données sont importées dans une base de données et il n'y a pas de clés primaires.

Voici le code du FileSystemWatcher :

public void Run() {
  FileSystemWatcher watcher = new FileSystemWatcher("C:\\temp");
  watcher.NotifyFilter = NotifyFilters.FileName;
  watcher.Filter = "*.txt";

  watcher.Created += new FileSystemEventHandler(OnChanged);

  watcher.EnableRaisingEvents = true;
  System.Threading.Thread.Sleep(System.Threading.Timeout.Infinite);
}

Puis la méthode qui analyse le fichier :

private void OnChanged(object source, FileSystemEventArgs e) {
  string line = null;

  try {
    using (FileStream fs = new FileStream(e.FullPath, FileMode.Open, FileAccess.Read, FileShare.None)) {
      using (StreamReader sr = new StreamReader(fs)) {
        while (sr.EndOfStream == false) {
          line = sr.ReadLine();
          //parse the line and insert into the database
        }
      }
    }
  }
  catch (IOException ioe) {
    Console.WriteLine("OnChanged: Caught Exception reading file [{0}]", ioe.ToString());
  }

En déplaçant le deuxième fichier, on attrape

System.IO.IOException : Le processus ne peut pas accéder au fichier ' C:\Temp\TestFile.txt car il est utilisé par un autre processus.

Je m'attendrais à voir cette erreur si le programme était exécuté sur plusieurs machines, mais il n'est exécuté que sur un seul serveur pour le moment. Il ne devrait pas y avoir d'autre processus utilisant ce fichier - je les ai créés et je les copie dans le répertoire lorsque l'application fonctionne.

Est-ce la bonne façon de configurer le FileSystemWatcher ? Comment puis-je voir ce qui a le verrou sur ce fichier ? Pourquoi les deux fichiers ne sont-ils pas analysés ? Dois-je fermer le FileStream ? Je souhaite conserver l'option FileShare.None car je veux qu'un seul serveur analyse le fichier - le serveur qui accède au fichier l'analyse en premier.

52voto

0xA3 Points 73439

Un problème typique de cette approche est que le fichier est toujours en cours de copie pendant que l'événement est déclenché. Évidemment, vous obtiendrez une exception car le fichier est verrouillé pendant la copie. Une exception est particulièrement probable pour les gros fichiers.

Comme solution de rechange, vous pouvez d'abord copier le fichier, puis le renommer et écouter l'événement de renommage.

Une autre option serait d'avoir une boucle while vérifiant si le fichier peut être ouvert avec un accès en écriture. Si c'est le cas, vous saurez que la copie est terminée. Le code C# pourrait ressembler à ceci (dans un système de production, il est préférable d'avoir un nombre maximal de tentatives ou un délai d'attente au lieu d'un code C#). while(true) ) :

/// <summary>
/// Waits until a file can be opened with write permission
/// </summary>
public static void WaitReady(string fileName)
{
    while (true)
    {
        try
        {
            using (Stream stream = System.IO.File.Open(fileName, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite))
            {
                if (stream != null)
                {
                    System.Diagnostics.Trace.WriteLine(string.Format("Output file {0} ready.", fileName));
                    break;
                }
            }
        }
        catch (FileNotFoundException ex)
        {
            System.Diagnostics.Trace.WriteLine(string.Format("Output file {0} not yet ready ({1})", fileName, ex.Message));
        }
        catch (IOException ex)
        {
            System.Diagnostics.Trace.WriteLine(string.Format("Output file {0} not yet ready ({1})", fileName, ex.Message));
        }
        catch (UnauthorizedAccessException ex)
        {
            System.Diagnostics.Trace.WriteLine(string.Format("Output file {0} not yet ready ({1})", fileName, ex.Message));
        }
        Thread.Sleep(500);
    }
}

Une autre approche encore consisterait à placer un petit fichier déclencheur dans le dossier une fois la copie terminée. Votre FileSystemWatcher n'écouterait que le fichier déclencheur.

10voto

G-Mac Points 366

J'aurais bien laissé un commentaire ci-dessus, mais je n'ai pas encore assez de points.

La réponse la mieux notée à cette question comporte un bloc de code qui ressemble à ceci :

using (Stream stream = System.IO.File.Open(fileName, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite))
{
    if (stream != null)
    {
        System.Diagnostics.Trace.WriteLine(string.Format("Output file {0} ready.", fileName));
        break;
    }
}

Le problème de l'utilisation de FileShare.ReadWrite est qu'il demande l'accès au fichier en disant "Je veux lire/écrire dans ce fichier, mais les autres peuvent aussi le lire/écrire". Cette approche a échoué dans notre situation. Le processus qui recevait le transfert à distance n'a pas mis de verrou sur le fichier, mais il y écrivait activement. Notre code en aval (SharpZipLib) échouait avec l'exception "fichier en cours d'utilisation" parce qu'il essayait d'ouvrir le fichier avec un verrou de type FileShare.Read ("Je veux le fichier pour la lecture, et seulement laisser les autres processus lire aussi"). Comme le processus qui avait ouvert le fichier était déjà en train d'y écrire, cette demande a échoué.

Cependant, le code dans la réponse ci-dessus est trop relâché. En utilisant FileShare.ReadWrite En effet, l'appel en aval a réussi à obtenir l'accès au fichier (parce qu'il demandait une restriction Share qui pouvait être respectée), mais l'appel en aval a continué à échouer.

La fixation de l'action dans l'appel à File.Open devrait être soit FileShare.Read ou FileShare.None et PAS FileShare.ReadWrite .

4voto

Mike Powell Points 2913

Quand vous ouvrez le fichier dans votre méthode OnChanged, vous spécifiez FileShare.None qui, selon la documentation Les autres tentatives d'ouverture du fichier échoueront tant que vous l'aurez ouvert. Puisque tout ce que vous (et votre observateur) faites est de lire, essayez d'utiliser la commande FileShare.Read à la place.

2voto

Une solution simple serait de disposer du filesystemwatcher une fois que vous recevez la notification. Avant de copier le fichier, faites en sorte que le thread actuel attende jusqu'à ce qu'il reçoive l'événement disposed du filesystemwatcher. ensuite vous pouvez continuer à copier le fichier modifié sans problèmes d'accès. J'avais la même exigence et j'ai fait exactement ce que j'ai mentionné. cela a fonctionné.

Exemple de code :

    public void TestWatcher()
    {
        using (var fileWatcher = new FileSystemWatcher())
        {

            string path = @"C:\sv";
            string file = "pos.csv";

                fileWatcher.Path = path;
                fileWatcher.NotifyFilter = NotifyFilters.CreationTime | NotifyFilters.LastWrite;
                fileWatcher.Filter = file;

                System.EventHandler onDisposed = (sender,args) =>
                {
                   eve.Set();
                };

                FileSystemEventHandler onFile = (sender, fileChange) =>
                {
                   fileWatcher.EnableRaisingEvents = false;
                   Thread t = new Thread(new ParameterizedThreadStart(CopyFile));
                   t.Start(fileChange.FullPath);
                   if (fileWatcher != null)
                   {
                       fileWatcher.Dispose();
                   }
                   proceed = false;
                };

                fileWatcher.Changed += onFile;
                fileWatcher.Created += onFile;
                fileWatcher.Disposed+= onDisposed;
                fileWatcher.EnableRaisingEvents = true;

                while (proceed)
                {
                    if (!proceed)
                    {
                        break;
                    }
                }
        }
    }

    public void CopyFile(object sourcePath)
    {
        eve.WaitOne();
        var destinationFilePath = @"C:\sv\Co";
        if (!string.IsNullOrEmpty(destinationFilePath))
        {
            if (!Directory.Exists(destinationFilePath))
            {
                Directory.CreateDirectory(destinationFilePath);
            }
            destinationFilePath = Path.Combine(destinationFilePath, "pos.csv");
        }           

        File.Copy((string)sourcePath, destinationFilePath);
    }

2voto

AjitChahal Points 28

FileSystemWatcher déclenche l'événement watcher.Created deux fois pour chaque création de fichier 1 fois lorsque la copie du fichier est lancée et 2 fois lorsque la copie du fichier est terminée. Tout ce que vous avez à faire est d'ignorer le premier événement et de traiter l'événement la deuxième fois.

Un exemple simple de gestionnaire d'événements :

private bool _fileCreated = false;
    private void FileSystemWatcher_FileCreated(object sender, FileSystemEventArgs e)
    {
        if (_fileCreated)
        {
            ReadFromFile();//just an example method call to access the new file
        }

        _fileCreated = !_fileCreated;
    }

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X