El evento FileSystemWatcher Changed se genera dos veces

336

Tengo una aplicación en la que busco un archivo de texto y, si se realizan cambios en el archivo, estoy usando el OnChangedcontrolador de eventos para manejar el evento. Estoy usando el NotifyFilters.LastWriteTimepero todavía el evento se dispara dos veces. Aquí está el código.

public void Initialize()
{
   FileSystemWatcher _fileWatcher = new FileSystemWatcher();
  _fileWatcher.Path = "C:\\Folder";
  _fileWatcher.NotifyFilter = NotifyFilters.LastWrite;
  _fileWatcher.Filter = "Version.txt";
  _fileWatcher.Changed += new FileSystemEventHandler(OnChanged);
  _fileWatcher.EnableRaisingEvents = true;
}

private void OnChanged(object source, FileSystemEventArgs e)
{
   .......
}

En mi caso, OnChangedse llama dos veces, cuando cambio el archivo de texto version.txty lo guardo.

usuario214707
fuente
2
@BrettRigby: No es de extrañar. Ninguna de estas posibles respuestas proporciona la solución al problema. Todos son soluciones para problemas específicos. De hecho, ninguno de ellos resolvió mi problema específico (debo admitir que no los he probado todos).
Es una solución, pero debe juzgarse por la calidad de la solución. Hacer un seguimiento de los cambios funciona perfectamente, y es simple. OP está pidiendo una forma de suprimir eventos duplicados, y eso es lo que dan las respuestas a continuación. msdn.microsoft.com/en-us/library/… Explica que los eventos múltiples pueden ser causados ​​por un antivirus u otras "cosas complicadas del sistema de archivos" (lo cual suena como una excusa).
Tyler Montney
2
Recientemente abrí este número github.com/Microsoft/dotnet/issues/347
Stephan Ahlf
2
He creado una clase que te ayuda a obtener un solo evento. Puede obtener el código en github.com/melenaos/FileSystemSafeWatcher
Menelaos Vergis

Respuestas:

277

Me temo que este es un error / característica bien conocido de la FileSystemWatcherclase. Esto es de la documentación de la clase:

Puede notar en ciertas situaciones que un solo evento de creación genera múltiples eventos creados que son manejados por su componente. Por ejemplo, si usa un componente FileSystemWatcher para monitorear la creación de nuevos archivos en un directorio, y luego lo prueba usando el Bloc de notas para crear un archivo, puede ver dos eventos creados generados aunque solo se haya creado un solo archivo. Esto se debe a que el Bloc de notas realiza múltiples acciones del sistema de archivos durante el proceso de escritura. El Bloc de notas escribe en el disco en lotes que crean el contenido del archivo y luego los atributos del archivo. Otras aplicaciones pueden funcionar de la misma manera. Debido a que FileSystemWatcher supervisa las actividades del sistema operativo, se recogerán todos los eventos que activen estas aplicaciones.

Ahora, este fragmento de texto trata sobre el Createdevento, pero lo mismo se aplica también a otros eventos de archivo. En algunas aplicaciones, es posible que pueda evitar esto utilizando la NotifyFilterpropiedad, pero mi experiencia dice que a veces también tiene que hacer un filtrado duplicado manual (piratería).

Hace un tiempo reservé una página marcada con algunos consejos de FileSystemWatcher . Quizás quieras revisarlo.

Jørn Schou-Rode
fuente
151

He "solucionado" ese problema usando la siguiente estrategia en mi delegado:

// fsw_ is the FileSystemWatcher instance used by my application.

private void OnDirectoryChanged(...)
{
   try
   {
      fsw_.EnableRaisingEvents = false;

      /* do my stuff once asynchronously */
   }

   finally
   {
      fsw_.EnableRaisingEvents = true;
   }
}
David Brabant
fuente
14
Lo intenté y funcionó si modificaba un archivo a la vez, pero si modificaba dos archivos a la vez (como copia 1.txt y 2.txt a copia de 1.txt y copia de 2.txt) solo aumentaría un evento no dos como se esperaba.
Christopher Painter
2
Han pasado un par de meses, pero creo que lo que terminé haciendo es que el evento llame a un método que pone la lógica de negocios dentro de una declaración de bloqueo. De esa manera, si obtengo eventos adicionales, hacen cola hasta que es su turno y no hay nada que puedan hacer ya que la iteración anterior se encargó de todo.
Christopher Painter
15
Esto parece solucionar el problema, pero no lo hace. Si otro proceso está haciendo cambios, puede perderlos, la razón por la que parece funcionar es porque el IO del otro proceso es asíncrono, y deshabilita la supervisión hasta que haya terminado su procesamiento, creando así una condición de carrera con otros eventos que podrían ser de interés. Por eso @ChristopherPainter observó su problema.
Jf Beaulac
14
-1: ¿Qué sucede si ocurre otro cambio en el que estaría interesado mientras está deshabilitado?
G. Stoynev
2
@cYounes: a menos que hagas tus cosas de forma asincrónica.
David Brabant
107

Cualquier OnChangedevento duplicado del FileSystemWatcherpuede ser detectado y descartado marcando la File.GetLastWriteTimemarca de tiempo en el archivo en cuestión. Al igual que:

DateTime lastRead = DateTime.MinValue;

void OnChanged(object source, FileSystemEventArgs a)
{
    DateTime lastWriteTime = File.GetLastWriteTime(uri);
    if (lastWriteTime != lastRead)
    {
        doStuff();
        lastRead = lastWriteTime;
    }
    // else discard the (duplicated) OnChanged event
}
Babu
fuente
13
Me gusta esa solución, pero he usado Rx para hacer lo "correcto" (cambiar "Rename"el nombre del evento que le interesa):Observable.FromEventPattern<FileSystemEventArgs>(fileSystemWatcher, "Renamed") .Select(e => e.EventArgs) .Distinct(e => e.FullPath) .Subscribe(onNext);
Kjellski
44
¿Me estoy perdiendo de algo? No entiendo cómo funcionará esto. Por lo que he visto, los eventos se disparan simultáneamente, por lo que si ambos ingresan al evento anterior al mismo tiempo, ambos comenzarán a ejecutarse antes de configurar lastRead.
Peter Jamsmenson
Como DateTimesolo tiene una resolución de milisegundos, este método funciona incluso si lo reemplaza File.GetLastWriteTimecon DateTime.Now. Dependiendo de su situación, también puede usar el a.FullNameen una variable global para detectar eventos duplicados.
Roland
@PeterJamsmenson Los eventos no se disparan exactamente simultáneamente. Por ejemplo, el Bloc de notas puede generar varios eventos al guardar modificaciones en el disco, pero estos eventos se activan secuencialmente, uno tras otro, durante los diversos pasos que el Bloc de notas debe realizar para guardar. El método de Babu funciona muy bien.
Roland
10
No funciona ya que los eventos disparados están separados: Última hora de escritura: 636076274162565607 Última hora de escritura: 636076274162655722
Asheh
23

Aquí está mi solución que me ayudó a evitar que el evento se genere dos veces:

watcher.NotifyFilter = NotifyFilters.FileName | NotifyFilters.Size;

Aquí he establecido la NotifyFilterpropiedad con solo Nombre de archivo y tamaño.
watcheres mi objeto de FileSystemWatcher. Espero que esto ayude.

Deepashri
fuente
99
Además, en el Bloc de notas, creé un archivo con cuatro caracteres: abcd. Luego abrí una nueva instancia de Bloc de notas e ingresé los mismos cuatro caracteres. Elegí Archivo | Guardar como y eligió el mismo archivo. El archivo es idéntico y el tamaño y el nombre del archivo no cambian, ya que el archivo tiene las mismas cuatro letras, por lo que no se activa.
Rhyous
30
Es posible que se pueda hacer un cambio genuino que no altere el tamaño del archivo, por lo tanto, esta técnica podría fallar en esa situación.
Lee Grissom el
3
Supongo que es un caso bastante común en el que sabes que cualquier cambio significativo modificará el tamaño del archivo (por ejemplo, mi caso se estaba agregando a un archivo de registro). Si bien cualquiera que use esta solución debe tener en cuenta (y documentar) esa suposición, esto era exactamente lo que necesitaba.
GrandOpener
1
@GrandOpener: Esto no siempre es cierto. En mi caso, estoy viendo archivos donde su contenido consta de un solo carácter que es 0 o 1.
8

Mi escenario es que tengo una máquina virtual con un servidor Linux. Estoy desarrollando archivos en el host de Windows. Cuando cambio algo en una carpeta en el host, quiero que se carguen todos los cambios, sincronizados en el servidor virtual a través de Ftp. Así es como elimino el evento de cambio duplicado cuando escribo en un archivo (que marca la carpeta que contiene el archivo que se modificará también):

private Hashtable fileWriteTime = new Hashtable();

private void fsw_sync_Changed(object source, FileSystemEventArgs e)
{
    string path = e.FullPath.ToString();
    string currentLastWriteTime = File.GetLastWriteTime( e.FullPath ).ToString();

    // if there is no path info stored yet
    // or stored path has different time of write then the one now is inspected
    if ( !fileWriteTime.ContainsKey(path) ||
         fileWriteTime[path].ToString() != currentLastWriteTime
    )
    {
        //then we do the main thing
        log( "A CHANGE has occured with " + path );

        //lastly we update the last write time in the hashtable
        fileWriteTime[path] = currentLastWriteTime;
    }
}

Principalmente creo una tabla hash para almacenar información de tiempo de escritura de archivos. Luego, si la tabla hash tiene la ruta de archivo modificada y su valor de tiempo es el mismo que el cambio del archivo notificado actualmente, entonces sé que es el duplicado del evento e ignórelo.

Icono
fuente
Supongo que vacias la tabla hash periódicamente.
ThunderGr
Esto sería preciso para el segundo, pero si el período entre los dos cambios es lo suficientemente largo como para pasar un segundo, fallará. Además, si desea más precisión, puede usarlo, ToString("o")pero prepárese para más fallas.
Pragmateek
55
No compare cadenas, use DateTime.Equals ()
Phillip Kamikaze
No, no lo hagas. No son iguales En el caso de mi proyecto actual, están separados por una milésima de segundo. Yo uso (newtime-oldtime) .TotalMilliseconds <(umbral arbitrario, generalmente 5ms).
Flynn1179
8

Prueba con este código:

class WatchPlotDirectory
{
    bool let = false;
    FileSystemWatcher watcher;
    string path = "C:/Users/jamie/OneDrive/Pictures/Screenshots";

    public WatchPlotDirectory()
    {
        watcher = new FileSystemWatcher();
        watcher.Path = path;
        watcher.NotifyFilter = NotifyFilters.LastAccess | NotifyFilters.LastWrite
                               | NotifyFilters.FileName | NotifyFilters.DirectoryName;
        watcher.Filter = "*.*";
        watcher.Changed += new FileSystemEventHandler(OnChanged);
        watcher.Renamed += new RenamedEventHandler(OnRenamed);
        watcher.EnableRaisingEvents = true;
    }



    void OnChanged(object sender, FileSystemEventArgs e)
    {
        if (let==false) {
            string mgs = string.Format("File {0} | {1}",
                                       e.FullPath, e.ChangeType);
            Console.WriteLine("onchange: " + mgs);
            let = true;
        }

        else
        {
            let = false;
        }


    }

    void OnRenamed(object sender, RenamedEventArgs e)
    {
        string log = string.Format("{0} | Renamed from {1}",
                                   e.FullPath, e.OldName);
        Console.WriteLine("onrenamed: " + log);

    }

    public void setPath(string path)
    {
        this.path = path;
    }
}
Jamie Krcmar
fuente
1
Esta es la mejor solución, utilizando un semáforo en lugar de un temporizador.
Aaron Blenkush
1
¿Qué semáforo? Veo solo una variable booleana aquí. Además, el problema principal no está resuelto: FileSystemEventHandler sigue disparando múltiples eventos. ¿Y qué efectividad tiene este código? if (let==false) { ... } else { let = false; }? Increíble cómo esto obtuvo votos positivos, esto debe ser solo una cuestión de insignias StackOverflow.
sɐunıɔ ןɐ qɐp
8

Aquí está mi enfoque:

// Consider having a List<String> named _changedFiles

private void OnChanged(object source, FileSystemEventArgs e)
{
    lock (_changedFiles)
    {
        if (_changedFiles.Contains(e.FullPath))
        {
            return;
        }
        _changedFiles.Add(e.FullPath);
    }

    // do your stuff

    System.Timers.Timer timer = new Timer(1000) { AutoReset = false };
    timer.Elapsed += (timerElapsedSender, timerElapsedArgs) =>
    {
        lock (_changedFiles)
        {
            _changedFiles.Remove(e.FullPath);
        }
    };
   timer.Start();
}

Esta es la solución que usé para resolver este problema en un proyecto donde estaba enviando el archivo como archivo adjunto en un correo. Evitará fácilmente el evento que se disparó dos veces, incluso con un intervalo de temporizador más pequeño, pero en mi caso 1000 estuvo bien, ya que estaba más feliz con la falta de pocos cambios que con inundar el buzón con> 1 mensaje por segundo. Al menos funciona bien en caso de que se cambien varios archivos al mismo tiempo.

Otra solución que he pensado sería reemplazar la lista con un archivo de mapeo de diccionario a su MD5 respectivo, para que no tenga que elegir un intervalo arbitrario ya que no tendría que eliminar la entrada pero actualizar su valor, y cancela tus cosas si no ha cambiado. Tiene la desventaja de tener un Diccionario creciendo en la memoria a medida que se monitorean los archivos y consumiendo más y más memoria, pero he leído en alguna parte que la cantidad de archivos monitoreados depende del búfer interno del FSW, por lo que tal vez no sea tan crítico. No sé cómo el tiempo de computación MD5 afectaría el rendimiento de su código tampoco, cuidado = \

Rémy Esmery
fuente
Tu solución funciona muy bien para mí. Solo que olvidó agregar el archivo a la lista _changedFiles. La primera parte del código debería verse así:lock (_changedFiles) { if (_changedFiles.Contains(e.FullPath)) { return; } _changedFiles.Add(e.FullPath); // add this! } // do your stuff
davidthegrey
Voté a favor 4 respuestas anteriores y voté a favor esta. Su respuesta es la primera que hace lo que debería hacer tomando el ÚLTIMO evento y no la primera. Como explicó @Jorn, el problema es que los archivos se escriben en lotes. Otras soluciones no funcionaron para mí.
CodingYourLife
Su solución no es segura para subprocesos. Se _changedFilesaccede al desde múltiples hilos. Una forma de solucionarlo es usar un en ConcurrentDictionarylugar de List. Otra forma es asignar la corriente Forma la Timer.SynchronizingObjectpropiedad, así como a la FileSystemWatcher.SynchronizingObjectpropiedad.
Theodor Zoulias
5

He creado un repositorio de Git con una clase que se extiende FileSystemWatcherpara desencadenar los eventos solo cuando se realiza la copia. Descarta todos los eventos modificados excepto el último y lo genera solo cuando el archivo está disponible para leer.

Descargue FileSystemSafeWatcher y agréguelo a su proyecto.

Luego, úselo normalmente FileSystemWatchery controle cuándo se desencadenan los eventos.

var fsw = new FileSystemSafeWatcher(file);
fsw.EnableRaisingEvents = true;
// Add event handlers here
fsw.Created += fsw_Created;
Menelaos Vergis
fuente
Esto parece fallar cuando se genera un evento en un directorio. Lo puse a trabajar envolviendo una verificación de directorio antes de abrir el archivo
Sam
A pesar del error tipográfico en el ejemplo, esta parece ser una solución viable para mí. Sin embargo, en mi caso puede haber una docena de actualizaciones en un segundo, por lo que tuve que bajar _consolidationInterval drásticamente para no perder ningún cambio. Si bien 10 ms parece estar bien, todavía pierdo alrededor del 50% de las actualizaciones si configuro _consolidationInterval en 50 ms. Todavía tengo que ejecutar algunas pruebas para encontrar el valor que mejor se ajuste.
_consolidationInterval parece funcionar bien para mí. Me gustaría que alguien bifurque esto y lo convierta en un paquete NuGet.
zumalifeguard
1
Gracias :) Resolvió mi problema. Espero que los eventos creados y copiados funcionen correctamente con un solo observador para resolver bien este problema. stackoverflow.com/questions/55015132/…
techno
1
Esto es excelente. Lo he implementado en mi proyecto y ha superado todos los intentos que he hecho para intentar romperlo. Gracias.
Christh
4

Sé que este es un problema antiguo, pero tenía el mismo problema y ninguna de las soluciones anteriores realmente resolvió el problema que estaba enfrentando. He creado un diccionario que asigna el nombre del archivo con LastWriteTime. Entonces, si el archivo no está en el diccionario, continuará con el proceso; de lo contrario, verifique cuándo fue la última vez que se modificó y si es diferente de lo que está en el diccionario, ejecute el código.

    Dictionary<string, DateTime> dateTimeDictionary = new Dictionary<string, DateTime>(); 

        private void OnChanged(object source, FileSystemEventArgs e)
            {
                if (!dateTimeDictionary.ContainsKey(e.FullPath) || (dateTimeDictionary.ContainsKey(e.FullPath) && System.IO.File.GetLastWriteTime(e.FullPath) != dateTimeDictionary[e.FullPath]))
                {
                    dateTimeDictionary[e.FullPath] = System.IO.File.GetLastWriteTime(e.FullPath);

                    //your code here
                }
            }
Zardaloop
fuente
Esta es una solución sólida, pero le falta una línea de código. en la your code heresección, debe agregar o actualizar dateTimeDictionary. dateTimeDictionary[e.FullPath] = System.IO.File.GetLastWriteTime(e.FullPath);
DiamondDrake
No funciono para mi. Mi controlador de cambios se llama dos veces y el archivo tiene una marca de tiempo diferente la segunda vez. Podría ser porque es un archivo grande y la escritura estaba en progreso la primera vez. Encontré un temporizador para colapsar eventos duplicados que funcionó mejor.
michael
3

Un posible 'pirateo' sería limitar los eventos usando Extensiones reactivas, por ejemplo:

var watcher = new FileSystemWatcher("./");

Observable.FromEventPattern<FileSystemEventArgs>(watcher, "Changed")
            .Throttle(new TimeSpan(500000))
            .Subscribe(HandleChangeEvent);

watcher.EnableRaisingEvents = true;

En este caso, estoy acelerando a 50 ms, en mi sistema eso fue suficiente, pero los valores más altos deberían ser más seguros. (Y como dije, sigue siendo un 'hack').

TimothyP
fuente
He usado el .Distinct(e => e.FullPath)que me parece mucho más intuitivo. Y tiene el comportamiento restaurado que se esperaría de la API.
Kjellski
3

Tengo una solución muy rápida y simple aquí, funciona para mí, y no importa que el evento se desencadene una o dos veces o más veces ocasionalmente, échale un vistazo:

private int fireCount = 0;
private void inputFileWatcher_Changed(object sender, FileSystemEventArgs e)
    {
       fireCount++;
       if (fireCount == 1)
        {
            MessageBox.Show("Fired only once!!");
            dowork();
        }
        else
        {
            fireCount = 0;
        }
    }
}
Xiaoyuvax
fuente
Al principio pensé que esto funcionaría para mí, pero no es así. Tengo una situación en la que el contenido de los archivos a veces se sobrescribe y otras veces el archivo se elimina y se vuelve a crear. Si bien su solución parece funcionar en caso de que el archivo se sobrescriba, no siempre funciona en caso de que el archivo se vuelva a crear. En este último caso, los eventos a veces se pierden.
Intente ordenar los diferentes tipos de eventos y tratarlos por separado, solo ofrezco una posible solución. buena suerte.
Xiaoyuvax
aunque no lo pruebo, no estoy seguro de que esto no funcione para la creación y eliminación. debería ser teóricamente aplicable también. Dado que la instrucción fireCount ++ y if () son atómicos y no se esperarán. incluso con dos eventos disparados que compiten entre sí. Supongo que debe haber algo más que cause problemas. (¿por perdido? ¿qué quieres decir?)
Xiaoyuvax
3

Aquí hay una nueva solución que puedes probar. Funciona bien para mi En el controlador de eventos para el evento modificado, elimine mediante programación el controlador del diseñador, envíe un mensaje si lo desea y luego vuelva a agregar el controlador mediante programación. ejemplo:

public void fileSystemWatcher1_Changed( object sender, System.IO.FileSystemEventArgs e )
    {            
        fileSystemWatcher1.Changed -= new System.IO.FileSystemEventHandler( fileSystemWatcher1_Changed );
        MessageBox.Show( "File has been uploaded to destination", "Success!" );
        fileSystemWatcher1.Changed += new System.IO.FileSystemEventHandler( fileSystemWatcher1_Changed );
    }
Fancy_Mammoth
fuente
1
No necesita invocar el constructor del tipo delegado. this.fileSystemWatcher1.Changed -= this.fileSystemWatcher1_Changed;Debería hacer lo correcto.
bartonjs
@bartonjs Gracias por eso. No estoy seguro de por qué invoqué todo el constructor. Honestamente, es muy probable que sea un error de novato. A pesar de todo, parece que mi truco de una solución funcionó bastante bien.
Fancy_Mammoth
2

La razón principal fue que la última hora de acceso del primer evento fue la hora actual (escritura de archivo o hora cambiada). entonces el segundo evento fue el último tiempo de acceso original del archivo. Lo resuelvo bajo código.

        var lastRead = DateTime.MinValue;

        Watcher = new FileSystemWatcher(...)
        {
            NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite,
            Filter = "*.dll",
            IncludeSubdirectories = false,
        };
        Watcher.Changed += (senderObject, ea) =>
        {
            var now = DateTime.Now;
            var lastWriteTime = File.GetLastWriteTime(ea.FullPath);

            if (now == lastWriteTime)
            {
                return;
            }

            if (lastWriteTime != lastRead)
            {
                // do something...
                lastRead = lastWriteTime;
            }
        };

        Watcher.EnableRaisingEvents = true;
Kim Ki Won
fuente
igual que esta respuesta
Jean-Paul
2

Pasé una cantidad significativa de tiempo usando FileSystemWatcher, y algunos de los enfoques aquí no funcionarán. Realmente me gustó el enfoque de eventos de desactivación, pero desafortunadamente, no funciona si se cae> 1 archivo, el segundo archivo se perderá la mayoría, si no todas las veces. Entonces uso el siguiente enfoque:

private void EventCallback(object sender, FileSystemEventArgs e)
{
    var fileName = e.FullPath;

    if (!File.Exists(fileName))
    {
        // We've dealt with the file, this is just supressing further events.
        return;
    }

    // File exists, so move it to a working directory. 
    File.Move(fileName, [working directory]);

    // Kick-off whatever processing is required.
}
Gino
fuente
2

Este código me funcionó.

        private void OnChanged(object source, FileSystemEventArgs e)
    {

        string fullFilePath = e.FullPath.ToString();
        string fullURL = buildTheUrlFromStudyXML(fullFilePath);

        System.Diagnostics.Process.Start("iexplore", fullURL);

        Timer timer = new Timer();
        ((FileSystemWatcher)source).Changed -= new FileSystemEventHandler(OnChanged);
        timer.Interval = 1000;
        timer.Elapsed += new ElapsedEventHandler(t_Elapsed);
        timer.Start();
    }

    private void t_Elapsed(object sender, ElapsedEventArgs e)
    {
        ((Timer)sender).Stop();
        theWatcher.Changed += new FileSystemEventHandler(OnChanged);
    }
Neo
fuente
2

principalmente para mi futuro :)

Escribí un contenedor usando Rx:

 public class WatcherWrapper : IDisposable
{
    private readonly FileSystemWatcher _fileWatcher;
    private readonly Subject<FileSystemEventArgs> _infoSubject;
    private Subject<FileSystemEventArgs> _eventSubject;

    public WatcherWrapper(string path, string nameFilter = "*.*", NotifyFilters? notifyFilters = null)
    {
        _fileWatcher = new FileSystemWatcher(path, nameFilter);

        if (notifyFilters != null)
        {
            _fileWatcher.NotifyFilter = notifyFilters.Value;
        }

        _infoSubject = new Subject<FileSystemEventArgs>();
        _eventSubject = new Subject<FileSystemEventArgs>();

        Observable.FromEventPattern<FileSystemEventArgs>(_fileWatcher, "Changed").Select(e => e.EventArgs)
            .Subscribe(_infoSubject.OnNext);
        Observable.FromEventPattern<FileSystemEventArgs>(_fileWatcher, "Created").Select(e => e.EventArgs)
            .Subscribe(_infoSubject.OnNext);
        Observable.FromEventPattern<FileSystemEventArgs>(_fileWatcher, "Deleted").Select(e => e.EventArgs)
            .Subscribe(_infoSubject.OnNext);
        Observable.FromEventPattern<FileSystemEventArgs>(_fileWatcher, "Renamed").Select(e => e.EventArgs)
            .Subscribe(_infoSubject.OnNext);

        // this takes care of double events and still works with changing the name of the same file after a while
        _infoSubject.Buffer(TimeSpan.FromMilliseconds(20))
            .Select(x => x.GroupBy(z => z.FullPath).Select(z => z.LastOrDefault()).Subscribe(
                infos =>
                {
                    if (infos != null)
                        foreach (var info in infos)
                        {
                            {
                                _eventSubject.OnNext(info);
                            }
                        }
                });

        _fileWatcher.EnableRaisingEvents = true;
    }

    public IObservable<FileSystemEventArgs> FileEvents => _eventSubject;


    public void Dispose()
    {
        _fileWatcher?.Dispose();
        _eventSubject.Dispose();
        _infoSubject.Dispose();
    }
}

Uso:

var watcher = new WatcherWrapper(_path, "*.info");
// all more complicated and scenario specific filtering of events can be done here    
watcher.FileEvents.Where(x => x.ChangeType != WatcherChangeTypes.Deleted).Subscribe(x => //do stuff)
Krzysztof Skowronek
fuente
1

He cambiado la forma en que superviso los archivos en los directorios. En lugar de usar FileSystemWatcher, sondeo ubicaciones en otro hilo y luego miro el LastWriteTime del archivo.

DateTime lastWriteTime = File.GetLastWriteTime(someFilePath);

Utilizando esta información y manteniendo un índice de una ruta de archivo y su último tiempo de escritura, puedo determinar los archivos que han cambiado o que se han creado en una ubicación particular. Esto me elimina de las rarezas del FileSystemWatcher. El principal inconveniente es que necesita una estructura de datos para almacenar LastWriteTime y la referencia al archivo, pero es confiable y fácil de implementar.

Wil P
fuente
99
así como también debe grabar ciclos de fondo en lugar de ser notificado por un evento del sistema.
Matthew Whited
1

Podría intentar abrirlo para escribir, y si tiene éxito, podría suponer que la otra aplicación se ha completado con el archivo.

private void OnChanged(object source, FileSystemEventArgs e)
{
    try
    {
        using (var fs = File.OpenWrite(e.FullPath))
        {
        }
        //do your stuff
    }
    catch (Exception)
    {
        //no write access, other app not done
    }
}

Simplemente abrirlo para escribir parece no generar el evento modificado. Entonces debería ser seguro.

MatteKarla
fuente
1
FileReadTime = DateTime.Now;

private void File_Changed(object sender, FileSystemEventArgs e)
{            
    var lastWriteTime = File.GetLastWriteTime(e.FullPath);
    if (lastWriteTime.Subtract(FileReadTime).Ticks > 0)
    {
        // code
        FileReadTime = DateTime.Now;
    }
}
prasad
fuente
1
Si bien esta puede ser la mejor solución a la pregunta, siempre es bueno agregar algunos comentarios sobre por qué eligió este enfoque y por qué cree que funciona. :)
waka
1

Perdón por la excavación de tumbas, pero he estado luchando contra este problema por un tiempo y finalmente encontré una manera de manejar estos eventos disparados múltiples. Me gustaría agradecer a todos en este hilo, ya que lo he usado en muchas referencias al luchar contra este problema.

Aquí está mi código completo. Utiliza un diccionario para rastrear la fecha y la hora de la última escritura del archivo. Compara ese valor y, si es el mismo, suprime los eventos. Luego establece el valor después de comenzar el nuevo hilo.

using System.Threading; // used for backgroundworker
using System.Diagnostics; // used for file information
private static IDictionary<string, string> fileModifiedTable = new Dictionary<string, string>(); // used to keep track of our changed events

private void fswFileWatch_Changed( object sender, FileSystemEventArgs e )
    {
        try
        {
           //check if we already have this value in our dictionary.
            if ( fileModifiedTable.TryGetValue( e.FullPath, out sEmpty ) )
            {              
                //compare timestamps      
                if ( fileModifiedTable[ e.FullPath ] != File.GetLastWriteTime( e.FullPath ).ToString() )
                {        
                    //lock the table                
                    lock ( fileModifiedTable )
                    {
                        //make sure our file is still valid
                        if ( File.Exists( e.FullPath ) )
                        {                               
                            // create a new background worker to do our task while the main thread stays awake. Also give it do work and work completed handlers
                            BackgroundWorker newThreadWork = new BackgroundWorker();
                            newThreadWork.DoWork += new DoWorkEventHandler( bgwNewThread_DoWork );
                            newThreadWork.RunWorkerCompleted += new RunWorkerCompletedEventHandler( bgwNewThread_RunWorkerCompleted );

                            // capture the path
                            string eventFilePath = e.FullPath;
                            List<object> arguments = new List<object>();

                            // add arguments to pass to the background worker
                            arguments.Add( eventFilePath );
                            arguments.Add( newEvent.File_Modified );

                            // start the new thread with the arguments
                            newThreadWork.RunWorkerAsync( arguments );

                            fileModifiedTable[ e.FullPath ] = File.GetLastWriteTime( e.FullPath ).ToString(); //update the modified table with the new timestamp of the file.
                            FILE_MODIFIED_FLAG.WaitOne(); // wait for the modified thread to complete before firing the next thread in the event multiple threads are being worked on.
                        }
                    }
                }
            }
        }
        catch ( IOException IOExcept )
        {
            //catch any errors
            postError( IOExcept, "fswFileWatch_Changed" );
        }
    }
Asalto Binario
fuente
Usé esto en uno de mis proyectos. ¡Funciona genial!
Tyler Montney
No funciona ya que los eventos disparados están separados: Último tiempo de escritura: 636076274162565607 Último tiempo de escritura: 636076274162655722
Profesor de programación
1

Evento si no se pregunta, es una pena que no haya muestras de solución listas para F #. Arreglar esto aquí es mi receta, solo porque puedo y F # es un maravilloso lenguaje .NET.

Los eventos duplicados se filtran usando el FSharp.Control.Reactivepaquete, que es solo un contenedor F # para extensiones reactivas. Todo lo que puede orientarse a un marco completo o netstandard2.0:

let createWatcher path filter () =
    new FileSystemWatcher(
        Path = path,
        Filter = filter,
        EnableRaisingEvents = true,
        SynchronizingObject = null // not needed for console applications
    )

let createSources (fsWatcher: FileSystemWatcher) =
    // use here needed events only. 
    // convert `Error` and `Renamed` events to be merded
    [| fsWatcher.Changed :> IObservable<_>
       fsWatcher.Deleted :> IObservable<_>
       fsWatcher.Created :> IObservable<_>
       //fsWatcher.Renamed |> Observable.map renamedToNeeded
       //fsWatcher.Error   |> Observable.map errorToNeeded
    |] |> Observable.mergeArray

let handle (e: FileSystemEventArgs) =
    printfn "handle %A event '%s' '%s' " e.ChangeType e.Name e.FullPath 

let watch path filter throttleTime =
    // disposes watcher if observer subscription is disposed
    Observable.using (createWatcher path filter) createSources
    // filter out multiple equal events
    |> Observable.distinctUntilChanged
    // filter out multiple Changed
    |> Observable.throttle throttleTime
    |> Observable.subscribe handle

[<EntryPoint>]
let main _args =
    let path = @"C:\Temp\WatchDir"
    let filter = "*.zip"
    let throttleTime = TimeSpan.FromSeconds 10.
    use _subscription = watch path filter throttleTime
    System.Console.ReadKey() |> ignore
    0 // return an integer exit code
python_kaa
fuente
1

En mi caso, necesito obtener la última línea de un archivo de texto que se inserta por otra aplicación, tan pronto como se realiza la inserción. Aquí está mi solución. Cuando se genera el primer evento, desactivo al observador para que no genere otros, luego llamo al temporizador TimeElapsedEvent porque cuando se llama a mi función de control OnChanged necesito el tamaño del archivo de texto, pero el tamaño en ese momento no es el tamaño real, es el tamaño del archivo inmediatamente antes de la inserción. Así que espero un momento para proceder con el tamaño de archivo correcto.

private FileSystemWatcher watcher = new FileSystemWatcher();
...
watcher.Path = "E:\\data";
watcher.NotifyFilter = NotifyFilters.LastWrite ;
watcher.Filter = "data.txt";
watcher.Changed += new FileSystemEventHandler(OnChanged);
watcher.EnableRaisingEvents = true;

...

private void OnChanged(object source, FileSystemEventArgs e)
   {
    System.Timers.Timer t = new System.Timers.Timer();
    try
    {
        watcher.Changed -= new FileSystemEventHandler(OnChanged);
        watcher.EnableRaisingEvents = false;

        t.Interval = 500;
        t.Elapsed += (sender, args) => t_Elapsed(sender, e);
        t.Start();
    }
    catch(Exception ex) {
        ;
    }
}

private void t_Elapsed(object sender, FileSystemEventArgs e) 
   {
    ((System.Timers.Timer)sender).Stop();
       //.. Do you stuff HERE ..
     watcher.Changed += new FileSystemEventHandler(OnChanged);
     watcher.EnableRaisingEvents = true;
}
André Washington
fuente
1

Prueba esto, está funcionando bien

  private static readonly FileSystemWatcher Watcher = new FileSystemWatcher();
    static void Main(string[] args)
    {
        Console.WriteLine("Watching....");

        Watcher.Path = @"D:\Temp\Watcher";
        Watcher.Changed += OnChanged;
        Watcher.EnableRaisingEvents = true;
        Console.ReadKey();
    }

    static void OnChanged(object sender, FileSystemEventArgs e)
    {
        try
        {
            Watcher.Changed -= OnChanged;
            Watcher.EnableRaisingEvents = false;
            Console.WriteLine($"File Changed. Name: {e.Name}");
        }
        catch (Exception exception)
        {
            Console.WriteLine(exception);
        }
        finally
        {
            Watcher.Changed += OnChanged;
            Watcher.EnableRaisingEvents = true;
        }
    }
Conocido
fuente
1

Quería reaccionar solo en el último evento, por si acaso, también en un cambio de archivo de Linux, parecía que el archivo estaba vacío en la primera llamada y luego se rellenaba en la siguiente y no me importaba perder algo de tiempo en caso de que el sistema operativo decidió hacer algún cambio de archivo / atributo.

Estoy usando .NET async aquí para ayudarme a hacer el subproceso.

    private static int _fileSystemWatcherCounts;
    private async void OnChanged(object sender, FileSystemEventArgs e)
    {
        // Filter several calls in short period of time
        Interlocked.Increment(ref _fileSystemWatcherCounts);
        await Task.Delay(100);
        if (Interlocked.Decrement(ref _fileSystemWatcherCounts) == 0)
            DoYourWork();
    }
Guillermo Ruffino
fuente
1

Creo que la mejor solución para resolver el problema es usar extensiones reactivas. Cuando transforma el evento en observable, simplemente puede agregar Throttling (..) (originalmente llamado Debounce (..))

Código de muestra aquí

        var templatesWatcher = new FileSystemWatcher(settingsSnapshot.Value.TemplatesDirectory)
        {
            NotifyFilter = NotifyFilters.LastWrite,
            IncludeSubdirectories = true
        };

        templatesWatcher.EnableRaisingEvents = true;

        Observable.FromEventPattern<FileSystemEventHandler, FileSystemEventArgs>(
                addHandler => templatesWatcher.Changed += addHandler,
                removeHandler => templatesWatcher.Changed -= removeHandler)
            .Throttle(TimeSpan.FromSeconds(5))
            .Subscribe(args =>
            {
                _logger.LogInformation($"Template file {args.EventArgs.Name} has changed");
                //TODO do something
            });
Cek Szy
fuente
0

Pude hacer esto agregando una función que busca duplicados en una matriz de búfer.

Luego realice la acción después de que la matriz no haya sido modificada por tiempo X usando un temporizador: - Restablezca el temporizador cada vez que se escriba algo en el búfer - Realice una acción al marcar

Esto también atrapa otro tipo de duplicación. Si modifica un archivo dentro de una carpeta, la carpeta también genera un evento Change.

Function is_duplicate(str1 As String) As Boolean
    If lb_actions_list.Items.Count = 0 Then
        Return False
    Else
        Dim compStr As String = lb_actions_list.Items(lb_actions_list.Items.Count - 1).ToString
        compStr = compStr.Substring(compStr.IndexOf("-") + 1).Trim

        If compStr <> str1 AndAlso compStr.parentDir <> str1 & "\" Then
            Return False
        Else
            Return True
        End If
    End If
End Function

Public Module extentions
<Extension()>
Public Function parentDir(ByVal aString As String) As String
    Return aString.Substring(0, CInt(InStrRev(aString, "\", aString.Length - 1)))
End Function
End Module
ciego
fuente
0

Esta solución me funcionó en la aplicación de producción:

Ambiente:

VB.Net Framework 4.5.2

Establecer manualmente las propiedades del objeto: NotifyFilter = Tamaño

Luego usa este código:

Public Class main
    Dim CalledOnce = False
    Private Sub FileSystemWatcher1_Changed(sender As Object, e As IO.FileSystemEventArgs) Handles FileSystemWatcher1.Changed
            If (CalledOnce = False) Then
                CalledOnce = True
                If (e.ChangeType = 4) Then
                    ' Do task...
                CalledOnce = False
            End If
        End Sub
End Sub
wpcoder
fuente
Utiliza el mismo concepto que @Jamie Krcmar pero para VB.NET
wpcoder
0

¡Prueba esto!

string temp="";

public void Initialize()
{
   FileSystemWatcher _fileWatcher = new FileSystemWatcher();
  _fileWatcher.Path = "C:\\Folder";
  _fileWatcher.NotifyFilter = NotifyFilters.LastWrite;
  _fileWatcher.Filter = "Version.txt";
  _fileWatcher.Changed += new FileSystemEventHandler(OnChanged);
  _fileWatcher.EnableRaisingEvents = true;
}

private void OnChanged(object source, FileSystemEventArgs e)
{
   .......
if(temp=="")
{
   //do thing you want.
   temp = e.name //name of text file.
}else if(temp !="" && temp != e.name)
{
   //do thing you want.
   temp = e.name //name of text file.
}else
{
  //second fire ignored.
}

}
LT
fuente
0

Tuve que combinar varias ideas de las publicaciones anteriores y agregar la comprobación de bloqueo de archivos para que funcione:

FileSystemWatcher fileSystemWatcher;

private void DirectoryWatcher_Start()
{
    FileSystemWatcher fileSystemWatcher = new FileSystemWatcher
    {
        Path = @"c:\mypath",
        NotifyFilter = NotifyFilters.LastWrite,
        Filter = "*.*",
        EnableRaisingEvents = true
    };

    fileSystemWatcher.Changed += new FileSystemEventHandler(DirectoryWatcher_OnChanged);
}

private static void WaitUntilFileIsUnlocked(String fullPath, Action<String> callback, FileAccess fileAccess = FileAccess.Read, Int32 timeoutMS = 10000)
{
    Int32 waitMS = 250;
    Int32 currentMS = 0;
    FileInfo file = new FileInfo(fullPath);
    FileStream stream = null;
    do
    {
        try
        {
            stream = file.Open(FileMode.Open, fileAccess, FileShare.None);
            stream.Close();
            callback(fullPath);
            return;
        }
        catch (IOException)
        {
        }
        finally
        {
            if (stream != null)
                stream.Dispose();
        }
        Thread.Sleep(waitMS);
        currentMS += waitMS;
    } while (currentMS < timeoutMS);
}    

private static Dictionary<String, DateTime> DirectoryWatcher_fileLastWriteTimeCache = new Dictionary<String, DateTime>();

private void DirectoryWatcher_OnChanged(Object source, FileSystemEventArgs ev)
{
    try
    {
        lock (DirectoryWatcher_fileLastWriteTimeCache)
        {
            DateTime lastWriteTime = File.GetLastWriteTime(ev.FullPath);
            if (DirectoryWatcher_fileLastWriteTimeCache.ContainsKey(ev.FullPath))
            {
                if (DirectoryWatcher_fileLastWriteTimeCache[ev.FullPath].AddMilliseconds(500) >= lastWriteTime)
                    return;     // file was already handled
            }

            DirectoryWatcher_fileLastWriteTimeCache[ev.FullPath] = lastWriteTime;
        }

        Task.Run(() => WaitUntilFileIsUnlocked(ev.FullPath, fullPath =>
        {
            // do the job with fullPath...
        }));

    }
    catch (Exception e)
    {
        // handle exception
    }
}
HarryP
fuente
0

Me acerqué al problema de doble creación como este, que ignora el primer evento:

Private WithEvents fsw As New System.IO.FileSystemWatcher
Private complete As New List(Of String)

Private Sub fsw_Created(ByVal sender As Object, _
    ByVal e As System.IO.FileSystemEventArgs) Handles fsw.Created

    If Not complete.Contains(e.FullPath) Then
        complete.Add(e.FullPath)

    Else
        complete.Remove(e.FullPath)
        Dim th As New Threading.Thread(AddressOf hprocess)
        th.Start(e)

    End If

End Sub
Simon Barnett
fuente