fix(server): atalho do Desktop deixa de morrer em silencio — laguna.log, porta checada e navegador so quando o server sobe - #69
Conversation
…em de porta e navegador so quando o server sobe (#58) O atalho do Desktop roda `pythonw` (sem console): qualquer falha de start sumia sem rastro e o navegador abria por `sleep(1.2)` fixo, mesmo quando o servidor nao tinha subido. Agora, tudo dentro de `laguna_server.py` — nenhum launcher (.vbs/.bat/.ps1) foi tocado: - `_log()` grava start e traceback de falha fatal em `laguna.log` (ja coberto pelo `.gitignore`); nunca levanta. - `_notify_error()` sinaliza no stderr e, quando nao ha console (pythonw), num MessageBoxW (ctypes, stdlib) — so no caminho de erro e so no Windows. - `_port_is_free()` + `_probe_laguna()` antes do `uvicorn.run`: porta ocupada pelo proprio Laguna abre o navegador na instancia viva e sai 0; ocupada por outro programa registra, avisa e sai 1 SEM abrir navegador. - `_open_browser_when_ready()` troca o `sleep(1.2)` por poll do socket (100ms ate 15s): navegador so abre com o servidor de fato ouvindo. - `main()` envolve tudo num anteparo que loga+avisa e devolve exit code (Ctrl+C e SystemExit seguem passando). Acompanham: `tests_unit/test_server_startup.py` (27 casos de logica pura, sem audio/modelo/GPU) e um bloco de solucao de problemas no README (PT e EN). Closes #58
|
Segundo commit ( CI agora verde nos dois jobs; |
…a rastro (#58) Reparos do quorum adversarial da PR #69 (2 vetos com vetor concreto): - `_probe_laguna` trocou `urllib.request.urlopen` por `http.client`: o opener default do urllib herda o proxy do sistema/`http_proxy` (que NAO faz bypass de loopback) e segue redirect 3xx para host arbitrario. Como a funcao so roda quando a porta esta ocupada por um processo desconhecido, os dois caminhos tiravam trafego da maquina e feriam a promessa 100% local (HANDBOOK §1). Agora o socket e 127.0.0.1 e ponto final, com o corpo lido limitado a 4 KB (servidor hostil nao prende o startup por drip-feed). - Guarda nos imports pesados: `ModuleNotFoundError` no topo do modulo (o caso do #56, o modo de falha mais comum do atalho) acontecia antes de qualquer linha de `main()` e sumia sem log. Os tres helpers de diagnostico passaram para antes dos imports; a excecao original segue subindo. - `main()` trata `SystemExit` com codigo != 0: o uvicorn sai por `sys.exit(1)` quando o bind ou o startup ASGI falha — o caminho mais provavel de "o servidor nao subiu" voltava a morrer calado sob pythonw. - `_is_headless` ganhou `console_hidden`: o fallback do atalho (Laguna.vbs:11-13 roda `python.exe` com window style 0) tem console e stderr, mas escondido — nao mostrava MessageBox nenhum. - MessageBox agora e `MessageBoxTimeoutW` (120s): sem ninguem para clicar (sessao 0, tarefa agendada) a caixa prendia o processo — trocar "morre calado" por "trava calado" seria pior. Verificado: retorna em 0,83s com prazo de 800ms. - Mensagens ao usuario com acentuacao (padrao do `static/i18n.js`); README PT/EN em paridade e agora honesto sobre dependencia faltando. Testes: +17 (105 passed), incluindo regressao de proxy e de redirect (nenhum byte chega ao proxy/alvo), contrato entre o corpo real de `/api/status` e `_is_laguna_status`, e os dois ramos novos de `SystemExit`. Gate: compileall OK, ruff (E9,F63,F7,F82) OK, pytest tests_unit/ 105 passed.
Parecer do PR Doctor — quórum concluído, 3× APROVA (SHA
|
| Veto/ressalva | Correção |
|---|---|
| proxy + redirect (§1) | http.client.HTTPConnection no lugar de urlopen — sem opener, sem proxy, sem redirect; corpo lido com teto de 4 KB |
| import morre calado | 3 helpers de diagnóstico movidos para antes dos imports pesados; try/except loga, avisa e re-levanta a exceção original |
SystemExit do uvicorn |
main() trata código != 0 (loga + avisa); SystemExit(0) e Ctrl+C seguem passando intactos |
| console oculto | _is_headless(..., console_hidden) + _console_is_hidden() (GetConsoleWindow/IsWindowVisible) |
| achado do próprio reparo | o MessageBoxW modal pendurou o processo na verificação manual (sessão não-interativa). Virou MessageBoxTimeoutW — trocar "morre calado" por "trava calado" seria pior |
| acentuação / README | mensagens ao usuário acentuadas; blocos PT e EN em paridade e honestos sobre dependência faltando |
Testes: 88 → 105, incluindo regressão de proxy e de redirect (o gravador não recebe byte nenhum), contrato entre o corpo real de /api/status e _is_laguna_status, e os dois ramos de SystemExit.
Gate local na worktree (C:\Python313\python.exe): compileall OK · ruff E9,F63,F7,F82 OK · pytest tests_unit/ -q 105 passed. T2/T3 não se aplicam (caminho quente e static/ intocados). Verificado à mão: com fastapi ausente, o traceback vai para o laguna.log e a mensagem para o stderr; o MessageBox com prazo de 800 ms retornou em 0,83 s.
2ª rodada (re-convocação única) — 3× APROVA
- 🟢 Pipeline/Latência: ordem de import intacta (o
_register_cuda_dlls()dolaguna_pipeline.pynão é afetado); caminho quente com zero toques;http.clientainda reduz ~9 ms do startup vs.urllib. - 🟢 Privacidade/Robustez: refez os dois ataques — proxy gravador recebeu
[], alvo do302recebeu[].MessageBoxTimeoutWconfirmada emuser32.dll. - 🟢 Produto/UX: V1–V6 fechados; contrato REST/WS intacto; os 6 acceptance criteria da distribuicao: o atalho do Desktop morre em silencio — pythonw sem console, sem log, e o navegador abre mesmo quando o servidor nao subiu #58 atendidos →
Closes #58procede.
Registrado para follow-up (não bloqueia)
NOTIFY_TIMEOUT_MS = 120_000é longo demais para ambiente automatizado (as três lentes convergiram): 15–20 s bastam.webbrowser.open()que devolveFalse(associação de navegador quebrada) é descartado — sobra um caminho de "nada acontece".- Contrato de
/api/status: o path e o teto de 4 KB não têm teste; só a forma do payload tem. - Saída do uvicorn some sob
pythonw— umlog_configcomFileHandleremLOG_PATHfecharia o ciclo. - Fora do alcance desta PR: o 4º cenário da tabela da distribuicao: o atalho do Desktop morre em silencio — pythonw sem console, sem log, e o navegador abre mesmo quando o servidor nao subiu #58 — "Python fora de
C:\Python313" — é insolúvel sem tocar oLaguna.vbs, que hardcoda o caminho. Isso é núcleo irredutível (§7.1) e precisa de decisão do @caioross; a distribuicao: o atalho do Desktop morre em silencio — pythonw sem console, sem log, e o navegador abre mesmo quando o servidor nao subiu #58 fecha sem esse item, e ele está registrado abaixo para não sumir.
Squash-merge autorizado pelo quórum.
O merge de #69 moveu os imports pesados de laguna_server.py para dentro do guarda de import; o `reinit_portaudio` desta PR foi para junto deles. Sem mudanca de comportamento dos dois lados. Gate na uniao: compileall OK, ruff (E9,F63,F7,F82) OK, imports OK, pytest tests_unit/ 111 passed, node --check em static/app.js e i18n.js.
Contexto
O atalho do Desktop é a superfície de distribuição do Laguna para o usuário-alvo. Ele roda
pythonwcom janela oculta (Laguna.vbs:16): sem console,stdout/stderrvão para o vazio, e olaguna_server.pynão escrevia log nenhum. Qualquer falha de startup virava "duplo-clique → nada acontece, para sempre" — e owebbrowser.open()disparava depois de umsleep(1.2)fixo, independentemente de ouvicornter conseguido ouvir na porta.Correção inteira dentro de
laguna_server.py, como a issue exige: nenhum launcher tocado (Laguna.vbs,Laguna.bat,install_shortcuts.ps1intactos — §7.1 respeitado). Zero dependência nova (sósocket,urllib,ctypes,traceback,datetimeda stdlib).O que mudou e por quê
_log(msg, exc)laguna.logao lado do script (.gitignore:52já cobre*.log). Nunca levanta — log é diagnóstico, não pode derrubar o app._is_headless()/_notify_error()stderr; sem console (pythonw) viraMessageBoxW(ctypes, stdlib), só no Windows e só no caminho de erro. Detecção porsys.stderr is Nonee pelo nome do executável — sob owscriptdo atalho o stderr é None, mas há ambientes que entregam um stderr válido para opythonw._port_is_free()+_probe_laguna()uvicorn.run(). Ocupada e respondendo como Laguna (GET /api/statuscom chaverunning) → abre o navegador na instância viva e sai 0. Ocupada por outro programa → loga, avisa e sai 1, sem abrir navegador._open_browser_when_ready()sleep(1.2)fixo → poll desocket.create_connectiona cada 100ms por até 15s. Navegador só abre com o servidor de fato ouvindo; se não subiu, não abre e o motivo vai pro log.main()except BaseExceptionque loga + avisa + devolve exit code;KeyboardInterrupt/SystemExitseguem passando (Ctrl+C continua encerrando limpo).sys.exit(main())no__main__.O
_probe_lagunafala com127.0.0.1(constante do módulo) — loopback, não rede: nada sai da máquina, a promessa 100% local (§1) segue intacta.Gate (resultado real)
python -m compileall -q .→ OK;import fase0_poc, laguna_core, laguna_server→IMPORTS_OK(C:\Python313\python.exe).pytest tests_unit/ -q— verde: 88 passed, dos quais 27 novos emtests_unit/test_server_startup.py(lógica pura: sem áudio, modelo, GPU ou rede externa — os únicos sockets são loopback em porta efêmera criados pelo próprio teste). Cobre_is_headless, porta livre/ocupada,_wait_until_listening(acha o listener / desiste no timeout),_is_laguna_statusnas duas bordas, os três desfechos de_serve, traceback gravado pormain(), Ctrl+C não engolido e_logcom caminho inacessível.--select E9,F63,F7,F82) →All checks passed!.laguna_core.py/laguna_pipeline.py/VAD/defaults de modelo — latência do caminho quente não é tocada). T3 não se aplica (static/intocado).Verificação manual dos 4 cenários da issue (servidor real, browser interceptado)
BROWSER_OPENem +1,65s (assim que o socket aceitou) — sem atraso perceptível vs. osleep(1.2)de antes;GET /api/status→200 {"running":[]}[laguna] ja esta rodando ... - abrindo o navegador, abre a instância viva, exit 0, nenhum segundo servidorlaguna.log, exit 1, nenhumBROWSER_OPENpythonw.exerealis_headless=True,MessageBoxWchamado com flag0x10e o texto do erro, traceback completo nolaguna.log, exit 1Nenhum processo ficou vivo: porta 7531 sem
LISTENINGao fim da rodada.Riscos
uvicornfazer o bind: irrelevante num app desktop single-user, e se acontecer ouvicorn.runlevanta e cai no anteparo domain()(log + MessageBox) em vez de morrer calado — estritamente melhor que hoje.laguna.logcresce: ~1 linha por start (≈80 bytes) + traceback só em falha. Sem rotação, de propósito (diff mínimo); se virar incômodo, é uma issue de 5 linhas.{"running": [...]}na 7531 seria confundido com o Laguna. Cenário implausível num app local; o pior caso é abrir o navegador nele em vez de avisar.Solicito quórum (HANDBOOK §7)
Motivo:
laguna_server.pyé área sensível e este diff muda o comportamento de entrada do app para o usuário não-técnico (exit codes novos, MessageBox, navegador condicional). O contrato REST/WS em si está intocado — nenhuma rota adicionada, removida ou alterada.Closes #58