Skip to content

fix(startup): a primeira execução travava num seletor de idioma invisível - #563

Merged
MarkSant merged 2 commits into
mainfrom
claude/first-run-startup-hang
Sep 8, 2026
Merged

MarkSant merged 2 commits into
mainfrom
claude/first-run-startup-hang

Conversation

@MarkSant

@MarkSant MarkSant commented Sep 8, 2026

Copy link
Copy Markdown
Owner

Teste de instalação do zero em C:\DRerio\DRerio-LogAI depois do #561: o install.bat correu inteiro, mas o duplo-clique no ícone não fazia absolutamente nada. Pelo terminal, o app parava aqui e nunca mais saía:

logging.session.start
logging.configured  cwd=C:\DRerio\DRerio-LogAI

O atalho estava correto. O app é que travava — e só numa máquina limpa.

O defeito

Depois daquela linha vêm Tk(), root.withdraw() e o seletor de idioma de primeira execução. Ele chamava dialog.transient(root).

Uma janela transient herda o estado mapeado do master. Com o master withdrawn ela também fica withdrawn — e nem deiconify() nem lift() revertem. Medido no Windows:

transient only                 viewable=False state=withdrawn 1x1
transient + deiconify          viewable=False state=withdrawn 1x1
transient + deiconify + lift   viewable=False state=withdrawn 1x1
no transient                   viewable=True  state=normal    167x61

O chooser era criado, nunca aparecia, e o root.wait_window(dialog) seguinte bloqueava para sempre numa janela que ninguém podia ver, focar ou fechar.

O withdraw() não é o defeito e não foi mexido. Ele existe porque a pergunta de idioma tem de ser feita antes de load_settings() — é ela que escreve ui.language — e a janela principal não pode piscar antes disso. O que muda é o transient, agora aplicado só quando o master está visível: o caso de Configurações → Idioma, onde ele vale (empilhamento correto sobre a janela principal, sem segundo botão na barra de tarefas). Um TclError do root degrada para "sem transient" — perder o comportamento transient é cosmético, perder o diálogo para o app.

Só acontece com config.local.yaml ausente, ou seja só numa máquina limpa — precisamente a do operador, e nunca a de quem desenvolve. Foi por isso que passou despercebido até o teste de instalação real.

Conferido que o splash não tem o mesmo problema: ele usa overrideredirect(True), que não herda o estado do master.

Por que os testes não pegaram

Todos os 9 casos de tests/ui/test_language_dialog.py recebem um Toplevel visível da fixture tkinter_root. Nenhum reproduzia o root withdrawn do arranque real. Os três testes novos reproduzem, e o primeiro falha contra o código antigo com a mensagem exata do defeito:

the chooser was never mapped: state='withdrawn' size=(1, 1);
wait_window would block on it forever

Segundo commit: nada mais pode falhar em silêncio

Enquanto diagnosticava isto, ficou claro que o problema real não era o travamento em si — era não haver como saber qual era.

run_app converte falhas em diálogo, mas só a partir do ponto em que a raiz Tk existe. O parsing de argumentos, o _setup_logging e o próprio Tk() rodam antes disso, e uma falha do próprio handler de erro também escapa. Nesses casos o traceback vai para sys.stderr — e sob pythonw, escolhido no #561 justamente para não deixar um console atrás da janela, sys.stderr é None. O traceback vai para lugar nenhum e o processo morre calado.

Para quem tem terminal isso é um contratempo. Para o operador que só tem o ícone, é o pior desfecho possível.

__main__.main() agora envolve run_app e, quando não há stderr, mostra o traceback num message box junto com o caminho de logs/analysis.log e o comando que reproduz o erro. Com console é no-op deliberado: ali o traceback do próprio Python é melhor que um modal, e a suíte afirma sobre ele. SystemExit passa direto — o caminho de pesos ausentes já mostra um diálogo que nomeia a pasta e o remédio, e um segundo modal genérico só pioraria.

Validado sob pythonw de verdade, iniciado destacado como o Explorer faz (stderr=None confirmado no processo).

Verificação

  • Suíte rápida contra o main atual: 6737 passed, 4 skipped
  • GUI (-m gui -n0): 1209 passed
  • ruff limpo; mypy limpo em 903 arquivos; pre-commit --all-files verde
  • Cada teste novo conferido revertendo a metade correspondente da correção — os sete discriminam

🤖 Generated with Claude Code

MarkSant and others added 2 commits September 7, 2026 22:56
…e sem pista

Relato de um teste de instalacao do zero (`C:\DRerio\DRerio-LogAI`): o
`install.bat` correu inteiro, mas o duplo-clique no icone nao fazia
absolutamente nada. As propriedades do atalho estavam corretas --
`.venv\Scripts\pythonw.exe -m zebtrack` -- e rodar exatamente isso sob
`pythonw` numa maquina ja configurada chega ao `mainloop` normalmente.

Ou seja: o atalho nao e o defeito. O defeito e nao haver como saber qual e.

`run_app` converte falhas em dialogo, mas **so a partir do ponto em que a raiz
Tk existe**. O parsing de argumentos, o `_setup_logging` e o proprio
`tk_module.Tk()` rodam antes disso, e uma falha do proprio handler de erro
tambem escapa. Nesses casos o traceback vai para o `sys.excepthook` padrao, que
escreve em `sys.stderr` -- e sob `pythonw`, escolhido justamente para nao deixar
um console atras da janela, `sys.stderr` e `None`. O traceback vai para lugar
nenhum e o processo morre calado.

Para quem tem terminal isso e um contratempo. Para o operador que so tem o
icone, e o pior desfecho possivel: nada aconteceu, e nada explica por que.

`__main__.main()` agora envolve `run_app` e, **quando nao ha stderr**, mostra o
traceback num message box junto com o caminho de `logs/analysis.log` e o comando
de terminal que reproduz o erro. Com console e no-op deliberado: ali o traceback
do proprio Python e melhor que um modal, e a suite afirma sobre ele.

`SystemExit` passa direto sem ser reportado -- o caminho de pesos ausentes sai
assim depois de ja ter mostrado um dialogo que nomeia a pasta e o remedio, e um
segundo modal generico so pioraria. Se o proprio Tk for o que esta quebrado, a
mensagem cai no log, e o erro ORIGINAL e o que chega ao chamador, nao o do
reporter.

Verificado sob `pythonw` de verdade, iniciado destacado como o Explorer faz
(`stderr=None` confirmado no processo): o dialogo e montado com o traceback
completo e o caminho do log.

Os quatro testes novos discriminam -- conferido revertendo cada metade:
remover a rede faz falhar o dialogo; remover a guarda de console faz o modal
aparecer para quem tem terminal; remover o repasse de `SystemExit` faz o caminho
de pesos ausentes ganhar um segundo dialogo.

Suite rapida: 6734 passed, 5 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ivel

Teste de instalacao do zero, pelo terminal, parava aqui:

    logging.session.start
    logging.configured  cwd=C:\DRerio\DRerio-LogAI

e nunca mais saia dali. Pelo icone, o duplo-clique nao fazia nada.

O que vem depois dessa linha e `Tk()`, `root.withdraw()` e o seletor de idioma
de primeira execucao. Ele chamava `dialog.transient(root)` -- e **uma janela
transient herda o estado mapeado do master**. Com o root withdrawn a janela
tambem fica withdrawn, e nem `deiconify()` nem `lift()` revertem. Medido no
Windows:

    transient only                 viewable=False state=withdrawn 1x1
    transient + deiconify          viewable=False state=withdrawn 1x1
    transient + deiconify + lift   viewable=False state=withdrawn 1x1
    no transient                   viewable=True  state=normal    167x61

O chooser era criado, nunca aparecia, e o `root.wait_window(dialog)` seguinte
bloqueava para sempre numa janela que ninguem podia ver, focar ou fechar.

O `withdraw()` nao e o defeito e nao foi mexido: ele existe porque a pergunta de
idioma tem de ser feita ANTES de `load_settings()`, e a janela principal nao
pode piscar antes disso. O `transient` e que passa a ser aplicado so quando o
master esta visivel -- o caso de **Configuracoes -> Idioma**, onde ele vale
(empilhamento correto sobre a janela principal, sem segundo botao na barra de
tarefas). Um `TclError` do root degrada para "sem transient": perder o
comportamento transient e cosmetico, perder o dialogo para o app.

Isto so acontece com `config.local.yaml` ausente, ou seja **so numa maquina
limpa** -- precisamente a do operador, e nunca a de quem desenvolve.

Escapou dos testes porque todos os 9 casos de `test_language_dialog.py` recebem
um Toplevel visivel da fixture `tkinter_root`; nenhum reproduzia o root
withdrawn do arranque real. Os tres testes novos reproduzem, e o primeiro falha
contra o codigo antigo com a mensagem exata do defeito:

    the chooser was never mapped: state='withdrawn' size=(1, 1);
    wait_window would block on it forever

Conferido tambem que o splash NAO tem o mesmo problema: ele usa
`overrideredirect(True)`, que nao herda o estado do master.

GUI: 1209 passed. ruff e mypy limpos.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@codecov

codecov Bot commented Sep 8, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.11765% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/zebtrack/__main__.py 92.00% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@MarkSant
MarkSant merged commit e286931 into main Sep 8, 2026
6 checks passed
@MarkSant
MarkSant deleted the claude/first-run-startup-hang branch September 8, 2026 20:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant