web-dev-qa-db-fra.com

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

Je rencontre un problème avec 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. Généralement, le premier fichier est analysé correctement, 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. Finalement, ce processus s'exécutera sur plusieurs ordinateurs et ils surveilleront un répertoire partagé, mais un seul serveur peut analyser chaque fichier lorsque les données sont importées dans une base de données et qu'il n'y a pas de clé primaire.

Voici le code 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, il 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 elle fonctionnait sur plusieurs ordinateurs, mais elle ne fonctionne 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 les copie dans le répertoire lorsque l'application est en cours d'exécution.

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

37
Tai Squared

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

En guise de solution de contournement, vous pouvez d'abord copier le fichier, puis le renommer et écouter l'événement de changement de nom.

Ou une autre option consisterait à avoir une boucle while vérifiant si le fichier peut être ouvert avec un accès en écriture. Si vous le pouvez, vous saurez que la copie est terminée. Le code C # pourrait ressembler à ceci (dans un système de production, vous souhaiterez peut-être un nombre maximal d'essais ou de délai d'attente au lieu d'une 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 consisterait à placer un petit fichier de déclencheur dans le dossier une fois la copie terminée. FileSystemWatcher n'écouterait que le fichier de déclencheur.

51
Dirk Vollmar

J'aurais 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 ressemblant à 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 avec l’utilisation du paramètre FileShare.ReadWrite est qu’il demande l’accès au fichier en disant "Je veux lire/écrire dans ce fichier, mais d’autres peuvent aussi y lire/écrire". Cette approche a échoué dans notre situation. Le processus qui recevait le transfert à distance n'a pas verrouillé le fichier, mais il y écrivait activement. Notre code en aval (SharpZipLib) échouait avec l'exception "fichier en cours d'utilisation" car il essayait d'ouvrir le fichier avec un FileShare.Read ("Je veux que le fichier soit lu, et que les autres processus soient également lus"). Parce que le processus qui avait le fichier ouvert était déjà en train de l'écrire, cette demande a échoué.

Toutefois, le code dans la réponse ci-dessus est trop souple. En utilisant FileShare.ReadWrite, il a réussi à obtenir l'accès au fichier (car il demandait une restriction de partage pouvant être respectée), mais l'appel en aval a continué à échouer.

Le paramètre de partage dans l'appel à File.Open doit être soit FileShare.Read, soit FileShare.None, etNOTFileShare.ReadWrite.

9
G-Mac

Lorsque vous ouvrez le fichier dans votre méthode OnChanged, vous spécifiez FileShare.None qui, selon la documentation , entraînera l'échec de toute autre tentative d'ouverture du fichier tant que vous l'avez ouvert. Puisque tout ce que vous (et votre observateur) faites est en train de lire, essayez plutôt d'utiliser FileShare.Read.

4
Mike Powell

FileSystemWatcher déclenche l'observateur watcher.Created deux fois pour chaque création de fichier 1ce lorsque la copie du fichier est démarrée et la deuxième 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 pour la deuxième fois.

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

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;
}
2
AjitChahal

Une solution simple consisterait à disposer du système de fichiers une fois la notification reçue. Avant de copier le fichier, faites en sorte que le thread actuel attende qu'il reçoive l'événement disposé filesystemwatcher. alors vous pouvez continuer à copier le fichier modifié sans problèmes d'accès. J'ai eu la même exigence et je l'ai fait exactement comme ce que j'ai mentionné. ça a marché.

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);
}
2
Aravind Kathiroju

Je pense qu'un bon exemple de ce que vous voulez est le ConfigureAndWatchHandler dans log4net. Ils utilisent une minuterie pour déclencher l'événement du gestionnaire de fichiers. Je pense que cela finit par être une implémentation plus propre de la boucle while dans le message de 0xA3. Pour ceux d'entre vous qui ne veulent pas utiliser dotPeek pour examiner le fichier, je vais essayer de vous donner un extrait de code ici basé sur le code OP:

private System.Threading.Timer _timer;    

public void Run() {
  //setup filewatcher
  _timer = new System.Threading.Timer(new TimerCallback(OnFileChange), (object) null, -1, -1);
}

private void OnFileChange(object state)
{
    try 
    {
    //handle files
    }
    catch (Exception ex) 
    {
        //log exception
        _timer.Change(500, -1);
    }
}
1
Druegor

J'ai eu le même problème. C'est juste à cause de FileSystemWatcher. Je viens d'utiliser
Thread.Sleep (); 

Et cela fonctionne très bien maintenant . Lorsque le fichier arrive dans le répertoire, il appelle onCreated deux fois. donc une fois lorsque le fichier est copié et une deuxième fois lorsque la copie est terminée. Pour cela, j'ai utilisé Thread.Sleep (); Donc, il faudra attendre avant que j'appelle ReadFile ();

private static void OnCreated(object source, FileSystemEventArgs e)
    {
        try
        {
            Thread.Sleep(5000);
            var data = new FileData();
            data.ReadFile(e.FullPath);                
        }
        catch (Exception ex)
        {
            WriteLogforError(ex.Message, String.Empty, filepath);
        }
    }
1
Nikhil Patel
public static BitmapSource LoadImageNoLock(string path)
{
    while (true)
    {
        try
        {
            var memStream = new MemoryStream(File.ReadAllBytes(path));
            var img = new BitmapImage();
            img.BeginInit();
            img.StreamSource = memStream;
            img.EndInit();
            return img;
            break;
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
    }
}
0
Andreas

J'ai eu le même problème au sein de DFS. Ma résolution a été obtenue en ajoutant deux lignes vides à chaque fichier. Ensuite, mon code attend deux lignes vides dans le fichier. Ensuite, j'ai la certitude de lire des données entières à partir d'un fichier.

0
dariol