HotXLS vigila un workbook en disco con TXLSFileMonitor y dispara OnFileChanged cuando otro proceso guarda sobre el archivo. Poner Monitor.Active := False desde dentro de ese handler solía colgarse para siempre, porque el worker thread seguía bloqueado esperando al callback que acababa de despachar. El arreglo es dejar que el objeto del thread lleve la identidad del callback que está ejecutando en ese momento
¿Por qué se cuelga parar un file monitor dentro de su propio handler?
Porque la llamada de desactivación y el callback corren en dos hilos distintos, y cada uno espera a que el otro termine. TXLSFileMonitor viene con SynchronizeEvent en True, lo que significa que el hilo de polling no llama a tu handler él mismo. Llama a TThread.Synchronize, entrega el método al hilo principal, y se bloquea hasta que el hilo principal lo ha ejecutado y ha señalado la finalización. Eso es exactamente lo que quieres para un handler que recarga un grid o repinta un formulario, y también es lo que convierte la llamada de desactivación obvia en una trampa
procedure TMainForm.HandleFileChanged(Sender: TObject);
begin
// Estamos en el hilo principal, cortesía de TThread.Synchronize
FBook.LoadFromFile(FMonitor.FileName);
RefreshGrid;
// El usuario solo quería una recarga, así que deja de vigilar
FMonitor.Active := False; // solía no devolver nunca
end;
Traza las dos pilas y el ciclo es obvio. El hilo principal está dentro de SetActive(False), que termina el worker y luego llama a Worker.WaitFor para asegurarse de que el hilo ha salido de verdad antes de liberar el objeto. El worker thread, mientras tanto, está aparcado dentro de TThread.Synchronize, esperando a que el hilo principal vuelva del mismo handler que está llamando a SetActive. Ningún lado puede moverse. La aplicación no crashea, no lanza excepción, simplemente deja de repintar, que es la firma de diagnóstico más mala que puede tener un bug
El mismo deadlock tiene una segunda cara
Apagar la sincronización no evita el problema, solo lo reubica. Con SynchronizeEvent en False — la elección sensata en un servicio o una herramienta de consola sin message loop — TXLSFileMonitor invoca el handler directamente en el hilo de polling. Tu handler ahora corre en el worker, así que una llamada a Monitor.Active := False desde dentro llega a Worker.WaitFor en el propio hilo del worker. Un hilo no puede esperar su propia salida, así que se bloquea hasta que algo externo mate el proceso. Las dos variantes son el mismo bug: SetActive(False) asumía que siempre se llamaba desde fuera del callback. Esa suposición se cumple para un botón Close o un destructor de formulario y falla en el único punto de llamada al que la gente acude primero, que es por lo que sobrevivió tanto tiempo. Cualquier componente que a la vez posea un hilo y despache código de usuario desde él hereda el mismo peligro, y la resolución es la misma en todos los casos — el path de teardown tiene que poder distinguir si se está ejecutando desde dentro de un callback que el hilo que va a esperar sigue sosteniendo
Dejar que el worker thread lleve la identidad del callback
HotXLS lo resuelve registrando, en el propio objeto del hilo, qué hilo del SO está ejecutando el handler en ese momento. TXLSFileMonitorThread.FireEvent marca FCallbackThreadID con GetCurrentThreadId justo antes de invocar el handler y lo limpia después en un bloque finally. Ese único campo basta para que el path de desactivación distinga entre "llamado desde código no relacionado" y "llamado desde dentro de mi propio callback", y funciona igual en los dos modos de despacho, porque en el caso sincronizado GetCurrentThreadId dentro de FireEvent ya reporta el hilo principal
procedure TXLSFileMonitorThread.FireEvent;
var
Owner: TXLSFileMonitor;
Handler: TNotifyEvent;
begin
// Un callback en cola puede ser despachado por WaitFor durante la desactivación
if Terminated then
Exit;
Owner := FOwner;
Handler := Owner.FOnFileChanged;
if not Assigned(Handler) then
Exit;
FCallbackThreadID := GetCurrentThreadId;
try
Handler(Owner);
finally
// El handler puede haber destruido a Owner, pero este hilo sigue vivo
FCallbackThreadID := 0;
end;
end;
La rama de desactivación de SetActive hace entonces tres cosas en un orden deliberado. Desacopla el hilo del monitor limpiando FThread primero, así que GetActive reporta False desde ese instante y un Active := False reentrante dentro del mismo handler sale al momento en vez de intentar derribar el hilo dos veces. Llama a Terminate. Solo entonces decide cómo reclamar el objeto del hilo
Worker := TXLSFileMonitorThread(FThread);
FThread := nil; // el monitor queda inactivo desde este punto
Worker.Terminate;
FHasBaseline := False;
if Worker.FCallbackThreadID = GetCurrentThreadId then
// Tanto los handlers directos como los sincronizados deben devolver antes
// de que el worker pueda salir; se libera solo tras desenrollarse el callback
Worker.FreeOnTerminate := True
else
begin
Worker.WaitFor;
Worker.Free;
end;
Cuando la identidad coincide, HotXLS nunca espera. Pone FreeOnTerminate := True y devuelve, dejando que el handler termine, que la llamada a Synchronize se complete, que Execute observe Terminated, y que el RTL libere el objeto del hilo una vez que la pila del callback se ha desenrollado del todo. Cuando la identidad no coincide — un botón Close, un destructor, un trabajo en segundo plano — el clásico WaitFor más Free sigue siendo lo correcto, porque te da una garantía dura de que ningún callback más puede dispararse después de que Active := False devuelva. Dos paths de salida, un campo para elegir entre ellos. Una consecuencia merece decirse con todas las letras: en el path de auto-liberación, el worker sobrevive al handler. Nada en el path de retorno de FireEvent puede tocar el objeto del monitor otra vez, porque el handler tiene derecho a haberlo liberado. Por eso FireEvent cachea Owner y Handler en locales desde el principio y solo toca su propio campo en la cláusula finally. Un FOwner.Something suelto después de la llamada al handler convertiría un deadlock arreglado en un use-after-free, que es un resultado estrictamente peor
¿Por qué FireEvent debe comprobar Terminated antes de hacer nada?
Porque WaitFor no es una espera pasiva. En el hilo principal, la implementación del RTL de TThread.WaitFor sigue vaciando la cola de sincronización mientras se bloquea, precisamente para que un hilo aparcado en Synchronize pueda completar y salir en vez de deadlockear contra el apagado. Eso es una feature, y significa que un callback encolado microsegundos antes de que llamaras a Active := False desde un botón Close puede seguir despachándose durante el teardown, después de que el monitor ya haya soltado su referencia a FThread. El guard al principio de FireEvent convierte ese despacho en un no-op. Sin él, una aplicación que cierra su formulario principal mientras un guardado está cayendo sobre el archivo vigilado puede ejecutar un handler de recarga contra un estado medio liberado, y la access violation resultante no se parece en nada a un problema de orden de apagado. Si tu propio componente despacha trabajo por Synchronize, la regla se generaliza: todo método sincronizado necesita un chequeo de Terminated como primera sentencia, no porque el hilo pueda haber desaparecido, sino porque la cola sobrevive a la decisión de parar
La misma forma de bug en el path de importación CSV
La auditoría que produjo el arreglo del monitor encontró un defecto hermano en el importador de texto, y merece la pena leerlo en paralelo porque el error subyacente es idéntico: una pieza de configuración consumida como si fuera estado de trabajo. TXLSCSVLoader expone una propiedad SkipRows para las líneas de banner que están encima de la cabecera real en los informes exportados. El parser necesita una cuenta atrás mientras recorre esas líneas, y la implementación original descontaba sobre el propio campo de respaldo de la propiedad. Importa un archivo y SkipRows se volvía cero en silencio; reutiliza el mismo loader para un segundo archivo y las filas de banner aterrizaban en el grid
Loader := TXLSCSVLoader.Create;
try
Loader.SkipRows := 2; // dos líneas de banner sobre la cabecera
Loader.ParseFile('jan-export.csv');
ReadGrid(Loader);
// SkipRows sigue siendo 2 aquí: FSkipRemaining se resetea por parse
Loader.ParseFile('feb-export.csv');
ReadGrid(Loader);
finally
Loader.Free;
end;
HotXLS ahora mantiene FSkipRows como el ajuste declarado y resetea un contador FSkipRemaining aparte al inicio de cada parse, tanto en el path delimitado como en el de ancho fijo. La regla general es barata de aplicar y se paga sola: si un bucle lo decrementa, no es un campo de respaldo de propiedad. El mismo lote también apretó XlsCsvTryParseISODateTime, que estaba dispuesto a aceptar un prefijo parseable como fecha completa. Ahora valida el campo entero — longitud, posiciones de separador y cada dígito — para que un valor como una referencia de pedido que solo empieza con algo con forma de fecha siga siendo texto en vez de convertirse en un TDateTime equivocado. La coerción de tipos silenciosa en la importación es mucho más cara de depurar aguas abajo que un campo rechazado, un tema que también recorre cómo protege HotXLS un workbook contra lectores y escritores concurrentes
Qué te da este arreglo y qué no
Sé preciso sobre la garantía. El cambio de thread ownership hace seguro exactamente un patrón antes fatal: desactivar o liberar un TXLSFileMonitor desde dentro de su propio handler OnFileChanged, en cualquiera de los dos modos de despacho. No es un diseño de concurrencia general, y no hace que el componente sea seguro de manejar desde hilos arbitrarios
- Las escrituras de propiedades siguen sin ser thread-safe. Pon
FileName,IntervalMsySynchronizeEventdesde un solo hilo, normalmente el que creó el monitor IntervalMsse aplica desde la siguiente activación, y el worker duerme en tramos cortos para queTerminatese respete con prontitud incluso con un periodo de poll largo- La detección es por hora de última escritura más tamaño, así que un cambio que preserve ambas cosas es invisible; un archivo que desaparece a mitad de guardado no dispara el evento, y este llega una vez que el contenido nuevo está en su sitio
- Con
SynchronizeEventenFalsetu handler corre en el worker thread, y todas las reglas sobre tocar controles VCL desde un hilo de fondo se aplican a lo que hagas dentro - Recargar un workbook desde el handler sigue necesitando su propia disciplina, sobre todo si otro código tiene una referencia al libro que se está reemplazando
En la práctica, el handler de recarga es la parte que merece más cuidado, no el monitor. Un evento de cambio suele llegar mientras el proceso que escribe sigue asentándose, así que una carga que abra el archivo de inmediato puede encontrarse un guardado a medio commitear; reintentar una vez tras una pequeña espera es más robusto que fiarse del primer sello. Si tu propio código es el que escribe el archivo vigilado, el archivo temporal por fases y el rename atómico descritos en cómo hace HotXLS que cada guardado sea crash-safe también dan al monitor un evento limpio de transición única que observar en vez de un chorro de tamaños intermedios. Y si varios watchers alimentan la misma rutina de recarga, las preocupaciones de serialización de el read gate alrededor de la inflación ZIP concurrente son lo siguiente que leer. La lección amplia sobrevive al componente concreto. Cualquier clase que posea un hilo y llame de vuelta a código de usuario necesita un path de teardown que pueda reconocer su propio frame de callback, porque el handler es el lugar más natural del mundo para que un usuario diga para. Registrar el id del hilo del callback cuesta un DWORD y convierte un cuelgue inexplicable en una pregunta ordinaria de ciclo de vida de objetos. El file monitoring, los guardados atómicos y la importación de texto viajan todos en el HotXLS Delphi Component para Delphi y C++Builder, sin configuración extra para activar nada de eso