Skip to content

fix(server): atalho do Desktop deixa de morrer em silencio — laguna.log, porta checada e navegador so quando o server sobe - #69

Merged
caioross merged 3 commits into
mainfrom
auto/issue-58-startup-visivel
Aug 5, 2026
Merged

caioross merged 3 commits into
mainfrom
auto/issue-58-startup-visivel

Conversation

@caioross

@caioross caioross commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Contexto

O atalho do Desktop é a superfície de distribuição do Laguna para o usuário-alvo. Ele roda pythonw com janela oculta (Laguna.vbs:16): sem console, stdout/stderr vão para o vazio, e o laguna_server.py não escrevia log nenhum. Qualquer falha de startup virava "duplo-clique → nada acontece, para sempre" — e o webbrowser.open() disparava depois de um sleep(1.2) fixo, independentemente de o uvicorn ter 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.ps1 intactos — §7.1 respeitado). Zero dependência nova (só socket, urllib, ctypes, traceback, datetime da stdlib).

O que mudou e por quê

Peça Comportamento
_log(msg, exc) anexa timestamp de start e traceback completo em laguna.log ao lado do script (.gitignore:52 já cobre *.log). Nunca levanta — log é diagnóstico, não pode derrubar o app.
_is_headless() / _notify_error() erro sempre vai pro stderr; sem console (pythonw) vira MessageBoxW (ctypes, stdlib), só no Windows e só no caminho de erro. Detecção por sys.stderr is None e pelo nome do executável — sob o wscript do atalho o stderr é None, mas há ambientes que entregam um stderr válido para o pythonw.
_port_is_free() + _probe_laguna() porta checada antes do uvicorn.run(). Ocupada e respondendo como Laguna (GET /api/status com chave running) → 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 de socket.create_connection a 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() anteparo except BaseException que loga + avisa + devolve exit code; KeyboardInterrupt/SystemExit seguem passando (Ctrl+C continua encerrando limpo). sys.exit(main()) no __main__.

O _probe_laguna fala com 127.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)

  • T1 — verde. 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 em tests_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_status nas duas bordas, os três desfechos de _serve, traceback gravado por main(), Ctrl+C não engolido e _log com caminho inacessível.
  • Ruff igual ao CI (--select E9,F63,F7,F82) → All checks passed!.
  • T2 não se aplica (nada de 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)

Cenário Resultado observado
Caminho feliz uvicorn sobe, BROWSER_OPEN em +1,65s (assim que o socket aceitou) — sem atraso perceptível vs. o sleep(1.2) de antes; GET /api/status → 200 {"running":[]}
Porta ocupada pelo próprio Laguna [laguna] ja esta rodando ... - abrindo o navegador, abre a instância viva, exit 0, nenhum segundo servidor
Porta ocupada por outro programa (listener burro em 7531) aviso claro, linha no laguna.log, exit 1, nenhum BROWSER_OPEN
Falha fatal sob pythonw.exe real is_headless=True, MessageBoxW chamado com flag 0x10 e o texto do erro, traceback completo no laguna.log, exit 1

Nenhum processo ficou vivo: porta 7531 sem LISTENING ao fim da rodada.

Riscos

  • Janela de corrida entre fechar o socket de teste e o uvicorn fazer o bind: irrelevante num app desktop single-user, e se acontecer o uvicorn.run levanta e cai no anteparo do main() (log + MessageBox) em vez de morrer calado — estritamente melhor que hoje.
  • laguna.log cresce: ~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.
  • MessageBox modal bloqueia o processo até o usuário fechar — só no caminho de erro, onde o processo ia morrer de qualquer jeito.
  • Um programa qualquer que sirva {"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

…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
)

`Path(r"C:\Python313\pythonw.exe").name` so devolve o basename num
WindowsPath; no PosixPath da CI (ubuntu) a string inteira volta e a deteccao
de pythonw falhava — o teste versionado pegou. Basename extraido na mao,
mesmo resultado nos dois SOs.
@caioross

caioross commented Aug 5, 2026

Copy link
Copy Markdown
Owner Author

Segundo commit (49daf84): o CI (ubuntu) reprovou o test_is_headless — Path(r"C:\Python313\pythonw.exe").name só devolve o basename num WindowsPath; no PosixPath a string inteira voltava e a detecção de pythonw dava False. Corrigido na função (basename extraído na mão, mesmo resultado nos dois SOs), não no teste. O caso foi exatamente o teste versionado fazendo o trabalho dele.

CI agora verde nos dois jobs; pytest tests_unit/ -q local segue 88 passed.

…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.
@caioross

caioross commented Aug 5, 2026

Copy link
Copy Markdown
Owner Author

Parecer do PR Doctor — quórum concluído, 3× APROVA (SHA 89f37fd)

Classificada em área de quórum (HANDBOOK §7.2): diff no contrato de entrada do laguna_server.py. Diff lido inteiro, CI verde nos dois jobs, MERGEABLE/CLEAN. Nada de §7.1 — nenhum launcher tocado, zero dependência nova.

1ª rodada de lentes (SHA 49daf84): 1 aprova, 2 vetos com vetor concreto

  • 🔴 Privacidade/Robustez — _probe_laguna usava urllib.request.urlopen: o opener default herda o proxy do sistema/http_proxy, e proxy_bypass não isenta 127.0.0.1. Provado empiricamente: com http_proxy setado, o request do startup ia para fora da máquina. Somado a isso, urlopen segue 3xx sozinho — e essa função só roda quando a porta está ocupada por um processo desconhecido, ou seja, o peer é hostil por construção: um squatter local respondendo 302 Location: http://attacker.tld/... transformava o Laguna num beacon de saída. Violação direta do §1.
  • 🔴 Produto/UX — falha de import (o caso do infra: fastapi e uvicorn nao estao no requirements — clone novo seguindo o README nao sobe, e o atalho .vbs falha em silencio #56, o modo de falha mais comum do atalho) acontece antes de qualquer linha de main(): continuava morrendo calada, com o README novo prometendo um laguna.log que não existiria. Mais: o fallback do Laguna.vbs:11-13 roda python.exe com janela oculta — há stderr, ninguém lê, e nenhum MessageBox aparecia.
  • 🟢 Pipeline/Latência — aprovou, mas registrou que o uvicorn sai por sys.exit(1) no bind/startup ASGI, e SystemExit era re-levantado sem log e sem caixa: o caminho mais provável de "o servidor não subiu" seguia silencioso.

Reparo (89f37fd, pelo PR Doctor)

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

Registrado para follow-up (não bloqueia)

  1. NOTIFY_TIMEOUT_MS = 120_000 é longo demais para ambiente automatizado (as três lentes convergiram): 15–20 s bastam.
  2. webbrowser.open() que devolve False (associação de navegador quebrada) é descartado — sobra um caminho de "nada acontece".
  3. Contrato de /api/status: o path e o teto de 4 KB não têm teste; só a forma do payload tem.
  4. Saída do uvicorn some sob pythonw — um log_config com FileHandler em LOG_PATH fecharia o ciclo.
  5. 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 o Laguna.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.

@caioross
caioross merged commit ed8daa4 into main Aug 5, 2026
2 checks passed
@caioross
caioross deleted the auto/issue-58-startup-visivel branch August 5, 2026 21:00
caioross added a commit that referenced this pull request Aug 5, 2026
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.
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.

distribuicao: o atalho do Desktop morre em silencio — pythonw sem console, sem log, e o navegador abre mesmo quando o servidor nao subiu

1 participant