A HotXLS observa uma pasta de trabalho no disco com TXLSFileMonitor e dispara OnFileChanged quando outro processo salva por cima do arquivo. Definir Monitor.Active := False de dentro desse handler costumava travar para sempre, porque a thread worker ainda estava bloqueada esperando o callback que ela tinha acabado de despachar. A correção é deixar o objeto de thread carregar a identidade do callback que ele está executando no momento
Por que parar um file monitor dentro do próprio handler trava?
Porque a chamada de desativação e o callback estão rodando em duas threads diferentes, e cada uma está esperando a outra terminar. TXLSFileMonitor sai de fábrica com SynchronizeEvent em True, o que significa que a thread de polling não chama seu handler diretamente. Ela chama TThread.Synchronize, entrega o método para a thread principal, e então bloqueia até a thread principal tê-lo rodado e sinalizado a conclusão. Isso é exatamente o que você quer para um handler que recarrega uma grid ou repinta um form, e é também o que torna a chamada óbvia de desativação uma armadilha
procedure TMainForm.HandleFileChanged(Sender: TObject);
begin
// Estamos na thread principal aqui, cortesia de TThread.Synchronize
FBook.LoadFromFile(FMonitor.FileName);
RefreshGrid;
// O usuário só queria um reload, então para de observar
FMonitor.Active := False; // costumava nunca retornar
end;
Trace as duas stacks e o ciclo fica óbvio. A thread principal está dentro de SetActive(False), que termina a worker e então chama Worker.WaitFor para garantir que a thread realmente saiu antes de o objeto ser liberado. A thread worker, enquanto isso, está estacionada dentro de TThread.Synchronize, esperando a thread principal retornar do próprio handler que está chamando SetActive. Nenhum dos lados consegue se mexer. O aplicativo não crasha, não levanta exceção, ele simplesmente para de repintar, que é a pior assinatura de diagnóstico que um bug pode ter
O mesmo deadlock usa uma segunda cara
Desligar a sincronização não evita o problema, só o realoca. Com SynchronizeEvent em False — a escolha sensata num serviço ou numa ferramenta de console sem message loop — TXLSFileMonitor invoca o handler diretamente na thread de polling. Seu handler agora roda na worker, então uma chamada a Monitor.Active := False de dentro dele chega a Worker.WaitFor na própria thread worker. Uma thread não consegue esperar a própria saída, então ela bloqueia até algo de fora matar o processo. As duas variantes são o mesmo bug: SetActive(False) assumia que sempre era chamado de algum lugar fora do callback. Essa suposição vale para um botão Close ou um destructor de form e falha no call site que as pessoas alcançam primeiro, que é por que sobreviveu tanto tempo. Qualquer componente que ao mesmo tempo possui uma thread e despacha código de usuário dela herda o mesmo perigo, e a resolução é a mesma em todo caso — o caminho de teardown tem que conseguir dizer se está sendo executado de dentro de um callback que a thread que ele está prestes a fazer join ainda está segurando
Deixando a thread worker carregar a identidade do callback
A HotXLS resolve gravando, no próprio objeto de thread, qual thread do SO está executando o handler no momento. TXLSFileMonitorThread.FireEvent carimba FCallbackThreadID com GetCurrentThreadId imediatamente antes de invocar o handler e limpa num bloco finally depois. Esse único campo é suficiente para o caminho de desativação distinguir "chamado de código não relacionado" de "chamado de dentro do meu próprio callback", e funciona identicamente nos dois modos de dispatch, porque no caso sincronizado GetCurrentThreadId dentro de FireEvent já reporta a thread principal
procedure TXLSFileMonitorThread.FireEvent;
var
Owner: TXLSFileMonitor;
Handler: TNotifyEvent;
begin
// Um callback enfileirado pode ser despachado por WaitFor durante a desativação
if Terminated then
Exit;
Owner := FOwner;
Handler := Owner.FOnFileChanged;
if not Assigned(Handler) then
Exit;
FCallbackThreadID := GetCurrentThreadId;
try
Handler(Owner);
finally
// O handler pode ter destruído Owner, mas esta thread continua viva
FCallbackThreadID := 0;
end;
end;
O ramo de desativação de SetActive então faz três coisas numa ordem deliberada. Ele desacopla a thread do monitor limpando FThread primeiro, então GetActive reporta False daquele instante em diante e um Active := False reentrante dentro do mesmo handler sai imediatamente em vez de tentar derrubar a thread duas vezes. Ele chama Terminate. Só então decide como reclamar o objeto de thread
Worker := TXLSFileMonitorThread(FThread);
FThread := nil; // o monitor está inativo deste ponto em diante
Worker.Terminate;
FHasBaseline := False;
if Worker.FCallbackThreadID = GetCurrentThreadId then
// Tanto handlers diretos quanto sincronizados precisam retornar antes de a worker
// sair; ela se libera depois de o callback ter desenrolado
Worker.FreeOnTerminate := True
else
begin
Worker.WaitFor;
Worker.Free;
end;
Quando a identidade bate, a HotXLS nunca espera. Ela define FreeOnTerminate := True e retorna, deixando o handler terminar, a chamada Synchronize completar, Execute observar Terminated, e a RTL liberar o objeto de thread uma vez que a stack do callback tenha desenrolado completamente. Quando a identidade não bate — um botão Close, um destructor, um job em background — o clássico WaitFor mais Free ainda é a coisa certa, porque te dá a garantia dura de que nenhum callback pode disparar depois de Active := False retornar. Dois caminhos de saída, um campo para escolher entre eles. Uma consequência merece ser dita por extenso: no caminho de autoliberação a worker sobrevive ao handler. Nada no caminho de retorno de FireEvent pode tocar no objeto do monitor de novo, porque o handler tem o direito de tê-lo liberado. É por isso que FireEvent cacheia Owner e Handler em locals na frente e toca só no próprio campo na cláusula finally. Um FOwner.Something perdido depois da chamada ao handler transformaria um deadlock corrigido em use-after-free, que é um resultado estritamente pior
Por que o FireEvent precisa checar Terminated antes de fazer qualquer coisa?
Porque WaitFor não é uma espera passiva. Na thread principal, a implementação de TThread.WaitFor na RTL continua drenando a fila de sincronização enquanto bloqueia, justamente para que uma thread estacionada em Synchronize possa completar e sair em vez de deadlockar contra o shutdown. Isso é uma feature, e significa que um callback enfileirado microssegundos antes de você chamar Active := False de um botão Close ainda pode ser despachado durante o teardown, depois de o monitor já ter largado a referência FThread. A guarda no topo de FireEvent transforma esse dispatch num no-op. Sem ela, um aplicativo que fecha o form principal enquanto um save está caindo no arquivo observado pode rodar um handler de reload contra estado meio desmontado, e a access violation resultante não se parece em nada com um problema de ordem de shutdown. Se o seu próprio componente despacha trabalho por Synchronize, a regra generaliza: todo método sincronizado precisa de uma checagem de Terminated como primeiro statement, não porque a thread pode ter sumido, mas porque a fila sobrevive à decisão de parar
A mesma forma de bug no caminho de importação CSV
A auditoria que produziu a correção do monitor encontrou um defeito irmão no importador de texto, e vale ler lado a lado porque o erro por baixo é idêntico: uma peça de configuração sendo consumida como se fosse estado de rascunho. TXLSCSVLoader expõe uma propriedade SkipRows para as linhas de banner que ficam acima do header de verdade em relatórios exportados. O parser precisa de uma contagem regressiva enquanto atravessa essas linhas, e a implementação original fazia a contagem regressiva no próprio campo de backing da propriedade. Importe um arquivo e SkipRows vira zero silenciosamente; reutilize o mesmo loader para um segundo arquivo e as linhas de banner caem na grid
Loader := TXLSCSVLoader.Create;
try
Loader.SkipRows := 2; // duas linhas de banner acima do header
Loader.ParseFile('jan-export.csv');
ReadGrid(Loader);
// SkipRows ainda é 2 aqui: FSkipRemaining é reiniciado por parse
Loader.ParseFile('feb-export.csv');
ReadGrid(Loader);
finally
Loader.Free;
end;
A HotXLS agora mantém FSkipRows como a configuração declarada e reinicia um contador separado FSkipRemaining no início de cada parse, nos dois caminhos, delimitado e largura fixa. A regra geral é barata de aplicar e se paga: se um loop faz decremento nele, não é um campo de backing de propriedade. O mesmo batch também apertou XlsCsvTryParseISODateTime, que estava disposto a aceitar um prefixo analisável como data inteira. Ele agora valida o campo inteiro — comprimento, posições dos separadores e cada dígito — para que um valor como uma referência de pedido que só começa com cara de data continue texto em vez de virar um TDateTime errado. Coerção de tipo silenciosa na importação é muito mais cara de debugar a jusante do que um campo rejeitado, um tema que também atravessa como a HotXLS protege uma pasta de trabalho contra leitores e escritores concorrentes
O que essa correção dá e o que ela não dá
Seja preciso sobre a garantia. A mudança de thread ownership torna exatamente um padrão antes fatal seguro: desativar ou liberar um TXLSFileMonitor de dentro do próprio handler OnFileChanged, em qualquer um dos dois modos de dispatch. Não é um design de concorrência geral, e não torna o componente seguro para ser dirigido de threads arbitrárias
- Writes de propriedade ainda não são thread-safe. Defina
FileName,IntervalMseSynchronizeEventde uma thread só, normalmente a que criou o monitor IntervalMsvale a partir da próxima ativação, e a worker dorme em fatias curtas para queTerminateseja honrado prontamente mesmo com período de polling longo- A detecção é por tempo de última escrita mais tamanho, então uma mudança que preserva os dois é invisível; um arquivo que some no meio do save não dispara, e o evento chega quando o conteúdo novo já está no lugar
- Com
SynchronizeEventemFalseseu handler roda na thread worker, e toda regra sobre tocar controles VCL de uma thread em background se aplica a qualquer coisa que você fizer dentro dele - Recarregar uma pasta de trabalho a partir do handler ainda exige disciplina própria, especialmente se outro código segura uma referência ao livro que está sendo substituído
Na prática o handler de reload é a parte que merece mais cuidado, não o monitor. Um evento de mudança costuma chegar enquanto o processo escritor ainda está assentando, então um load que abre o arquivo imediatamente pode encontrar um save parcialmente cometido; tentar de novo uma vez depois de um pequeno atraso é mais robusto do que confiar no primeiro carimbo. Se o seu próprio código é o que escreve o arquivo observado, o temp file em estágios e o rename atômico descritos em como a HotXLS torna todo save à prova de crash também dão ao monitor um evento limpo de transição única para observar em vez de um rio de tamanhos intermediários. E se vários watchers alimentam a mesma rotina de reload, as preocupações de serialização no read gate em volta da inflação ZIP concorrente são a próxima leitura. A lição maior sobrevive ao componente específico. Qualquer classe que possui uma thread e faz callback para código de usuário precisa de um caminho de teardown que reconheça o próprio frame de callback, porque o handler é o lugar mais natural do mundo para um usuário dizer pare. Gravar o id da thread do callback custa um DWORD e transforma um hang inexplicável numa pergunta comum de tempo de vida de objeto. File monitoring, saves atômicos e importação de texto vêm todos no HotXLS Delphi Component para Delphi e C++Builder, sem configuração extra para ativar qualquer um deles