Skip to content

fix: corrige servidor local, porta e encerramento controlado (#39) - #40

Open
Rossi-Luciano wants to merge 7 commits into
scieloorg:mainfrom
Rossi-Luciano:fix/issue-39-servidor-porta-encerramento
Open

fix: corrige servidor local, porta e encerramento controlado (#39)#40
Rossi-Luciano wants to merge 7 commits into
scieloorg:mainfrom
Rossi-Luciano:fix/issue-39-servidor-porta-encerramento

Conversation

@Rossi-Luciano

Copy link
Copy Markdown
Contributor

O que esse PR faz?

Corrige o gerenciamento do servidor HTTP local relatado na issue #39: no modo desktop as ações Relatório/HTML/PDF ficavam carregando indefinidamente no Windows, e no modo browser não havia seleção automática de porta livre nem forma confiável de encerrar o processo.

  • Troca wsgiref.simple_server.make_server (síncrono) por werkzeug.serving.make_server(..., threaded=True) nos dois modos.
  • Modo browser passa a escolher porta livre automaticamente quando --port não é informado, e abre o navegador sozinho.
  • Porta explicitamente ocupada mostra um diálogo nativo de erro (tkinter), já que o executável --windowed não tem console em nenhum dos dois modos.
  • Modo desktop: fechar a janela chama server.shutdown() e aguarda a thread terminar (thread.join()).
  • Modo browser: novo botão "Encerrar SPS Validator", com rota POST /shutdown protegida por token gerado por processo.
  • Modo desktop: Relatório e prévia HTML abrem na própria janela do pywebview (sem target="_blank"), com uma barra "Voltar ao histórico" injetada na prévia HTML. PDF continua abrindo externo nos dois modos.

Onde a revisão poderia começar?

spsvalidator/src/spsvalidator/main.py (troca de servidor, seleção de porta, encerramento), depois spsvalidator/src/spsvalidator/web/routes.py (rota POST /shutdown e navegação de volta na prévia HTML).

Como este poderia ser testado manualmente?

  1. spsvalidator --browser sem --port: deve escolher uma porta livre e abrir o navegador automaticamente, mostrando a URL no rodapé.
  2. Rodar duas instâncias com o mesmo --port explícito: a segunda deve mostrar um diálogo de erro claro em vez de travar em silêncio.
  3. No modo browser, validar um pacote e clicar em "Encerrar SPS Validator" no cabeçalho: deve pedir confirmação, encerrar o servidor e mostrar uma mensagem final.
  4. No modo desktop (requer GTK ou Qt instalados para o pywebview), clicar em Relatório/HTML na lista de histórico: deve abrir na própria janela, com botão de volta ao histórico.
  5. pytest tests/test_main.py tests/test_service_and_web.py -v.

Algum cenário de contexto que queira dar?

A issue relata que no Windows, o modo desktop trava indefinidamente ao abrir Relatório/HTML/PDF: o servidor antigo (wsgiref.simple_server) é síncrono, e uma conexão ociosa aberta pelo navegador externo (pywebview abre target="_blank" externamente por padrão) ocupava o único atendimento disponível. O modo browser também não tinha seleção automática de porta livre nem uma forma confiável de encerrar o processo, já que o executável --windowed não tem console para Ctrl+C.

Screenshots

N/A. Mudança majoritariamente de infraestrutura de servidor, validada via terminal e pytest. A parte visual nova (botão "Encerrar SPS Validator" e barra "Voltar ao histórico") foi testada manualmente no modo browser; não foi possível testar o modo desktop neste ambiente por falta dos bindings GTK/Qt do pywebview.

Quais são os tickets relevantes?

Closes #39

Referências


Segurança da informação (NSI.04)

Seção obrigatória. Marque as opções aplicáveis e justifique quando necessário. Referência: NSI.04 - Norma de Desenvolvimento Seguro.

Este PR manipula dados sensíveis ou pessoais (LGPD)?

  • Sim
  • Não

Este PR altera autenticação, autorização, controle de acesso ou gerenciamento de sessão?

  • Sim. Descreva o que mudou e por quê: a nova rota POST /shutdown (modo browser) é protegida por um token aleatório (secrets.token_urlsafe(32)) gerado uma vez por processo e comparado com secrets.compare_digest. Sem o token correto a rota responde 403. Isso não é autenticação de usuário, é um controle de acesso local para impedir que uma página externa derrube o servidor com um POST às cegas.
  • Não

Este PR introduz, atualiza ou remove dependências de terceiros?

  • Sim
  • Não. werkzeug já é dependência transitiva do Flask (usada aqui via werkzeug.serving.make_server); tkinter, webbrowser, secrets e threading são da biblioteca padrão. Nenhuma entrada nova em pyproject.toml.

Este PR foi validado pelo pipeline de segurança (SonarQube / Trivy)?

  • Sim
  • Não aplicável a este PR (justifique): não há dependências novas nem alterações em SQL; o pipeline de CI do repositório roda automaticamente na abertura deste PR.

Este PR concatena, monta ou executa comandos SQL, HTML ou JavaScript a partir de entrada externa?

  • Sim
  • Não. A barra "Voltar ao histórico" injetada na prévia HTML (_inject_back_banner) usa apenas uma URL gerada por url_for (não texto arbitrário de entrada externa). O token de encerramento é enviado via encodeURIComponent no corpo do POST.

Este PR expõe novos endpoints, telas ou serviços?

  • Sim. HTTPS obrigatório está garantido e o acesso segue o princípio de menor privilégio? A nova rota POST /shutdown só é alcançável em 127.0.0.1 (o servidor nunca é exposto fora de localhost, comportamento que já existia e não foi alterado por este PR) e exige o token de processo descrito acima. Não se aplica HTTPS porque não há tráfego de rede externo, é comunicação loopback local.
  • Não

Algum segredo, senha, chave ou token está sendo adicionado ao código-fonte?

  • Não, nenhum segredo foi commitado. O token de encerramento é gerado em tempo de execução (secrets.token_urlsafe(32)) a cada processo, nunca hardcoded.
  • Sim

Rossi-Luciano and others added 7 commits August 3, 2026 20:45
…scieloorg#39)

- Adiciona SHUTDOWN_TOKEN aleatorio (secrets.token_urlsafe) por processo,
  guardado em app.config e usado pela rota web.shutdown (routes.py).
- Expoe execution_mode, shutdown_token e server_url no context_processor
  pra que os templates (index.html, _history_list.html) consigam decidir
  comportamento por modo de execucao e montar o POST de encerramento.

Motivo: a issue scieloorg#39 pede uma acao explicita de encerramento no modo
browser, protegida contra POST vindo de uma pagina externa. O token e
gerado uma vez por processo e so quem carrega a pagina (via template)
consegue le-lo, o que evita que outro site derrube o servidor local as
cegas.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VFZesK9nxJfNr3JAH3TPoC
…previa HTML (issue scieloorg#39)

- Nova rota web.shutdown: valida o token recebido com secrets.compare_digest
  contra SHUTDOWN_TOKEN (403 se nao bater) e agenda server.shutdown() via
  threading.Timer(0.3, ...) em vez de chamar direto, pra nao bloquear a
  resposta HTTP desta propria requisicao.
- view_html_preview passa a se comportar diferente por modo: no browser
  continua servindo o arquivo cru (send_from_directory, sem mudanca); no
  desktop le o HTML, injeta uma barra fixa "Voltar ao historico" logo apos
  <body> (_inject_back_banner) e devolve o conteudo modificado.

Motivo: sem target="_blank" no modo desktop (ver commit dos templates), a
previa HTML passa a substituir o conteudo da propria janela do pywebview;
sem essa barra nao haveria como voltar ao historico. A URL da rota nao
muda, entao os assets relativos (assets/...) continuam resolvendo certo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VFZesK9nxJfNr3JAH3TPoC
…view (issue scieloorg#39)

- target="_blank" nos links de Relatorio e previa HTML passa a ser
  condicional: so aparece quando execution_mode != "desktop".
- Link de PDF mantido sempre com target="_blank" nos dois modos: nao da
  pra injetar uma barra de volta dentro de um PDF bruto, e embutir o PDF
  via <embed> teria risco de compatibilidade entre os backends do
  pywebview (GTK/Qt/WebView2).

Motivo: no Windows, o pywebview abre links target="_blank" no navegador
externo por padrao; essa e a causa raiz do "carregando indefinidamente"
relatado na issue scieloorg#39 (a conexao ociosa do navegador externo travava o
servidor sincrono antigo). No modo browser o comportamento fica igual ao
de antes (abre em nova aba), que nunca foi reportado como problema.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VFZesK9nxJfNr3JAH3TPoC
…cieloorg#39)

- Novo botao no cabecalho, renderizado so quando execution_mode ==
  "browser", com confirm() antes de agir (mesmo padrao ja usado em
  "Limpar marcacoes" no report.html).
- Ao confirmar, faz POST pra web.shutdown com o token embutido no HTML
  (shutdown_token), desabilita o botao e troca o corpo da pagina por uma
  mensagem de encerramento; em caso de falha, reabilita o botao e avisa.
- Rodape mostra a URL do servidor (server_url) quando definida, so
  presente no modo browser.

Motivo: a issue scieloorg#39 pede uma acao explicita de encerramento no modo
browser, ja que fechar so a aba nao e um evento confiavel pra derrubar o
servidor. window.close() nao foi usado porque scripts nao conseguem
fechar uma aba que nao foi aberta por eles, entao a pagina so mostra uma
mensagem confirmando o encerramento.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VFZesK9nxJfNr3JAH3TPoC
…cito (issue scieloorg#39)

- Troca wsgiref.simple_server.make_server (sincrono) por
  werkzeug.serving.make_server(..., threaded=True) nos dois modos; modo
  browser deixa de usar app.run() e passa a compartilhar o mesmo
  _bind_server do modo desktop.
- Modo browser passa a escolher porta livre automaticamente quando --port
  nao e informado (antes usava 5000 fixo), e abre o navegador sozinho
  (webbrowser.open) apos um pequeno delay de aquecimento.
- Nova funcao _bind_server: testa o bind numa porta explicita antes de
  chamar make_server, porque o werkzeug trata EADDRINUSE internamente
  (print em stderr + sys.exit(1)) antes que qualquer except no nosso
  codigo tivesse chance de rodar; em caso de falha mostra um dialogo
  nativo (tkinter.messagebox) com _show_error_dialog, ja que o executavel
  --windowed nao tem console visivel em nenhum dos dois modos.
- Modo desktop: guarda a referencia do server em app.config["SERVER"] e,
  apos webview.start() retornar (janela fechada), chama server.shutdown()
  e thread.join() explicitamente.

Motivo: a issue scieloorg#39 relata que o servidor sincrono trava com uma conexao
ociosa aberta pelo navegador externo no Windows, que a porta fixa 5000 do
modo browser colide com outros servicos, e que nao ha como encerrar o
processo de forma confiavel num executavel --windowed (sem console pra
Ctrl+C).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VFZesK9nxJfNr3JAH3TPoC
…do (issue scieloorg#39)

- Sem token ou com token errado, a rota responde 403.
- Com o token certo, responde 200 e agenda server.shutdown() via
  threading.Timer (nao chama direto): o teste confirma que o fake server
  nao e chamado antes do delay e e chamado depois.

Motivo: cobertura do criterio de aceitacao da issue scieloorg#39 sobre o
encerramento protegido por token no modo browser.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VFZesK9nxJfNr3JAH3TPoC
…vidor (issue scieloorg#39)

- test_bind_server_serves_concurrent_requests: dispara uma requisicao
  lenta (0.5s) e uma rapida contra o servidor real; a rapida precisa
  responder bem antes de 0.4s, provando que threaded=True realmente evita
  o bloqueio que o wsgiref sincrono tinha.
- test_bind_server_port_in_use_shows_dialog_and_exits: ocupa uma porta de
  verdade e confirma que _bind_server levanta SystemExit(1) e chama o
  dialogo de erro exatamente uma vez, com a porta certa na mensagem.
- test_server_shutdown_stops_serve_forever_thread: confirma que
  server.shutdown() encerra de fato a thread do serve_forever
  (thread.join() + is_alive() == False).
- test_free_port_returns_an_available_port: sanidade da selecao de porta
  livre.

Motivo: a issue scieloorg#39 pede explicitamente testes de concorrencia, porta
ocupada e encerramento do servidor como criterio de aceitacao.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VFZesK9nxJfNr3JAH3TPoC
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.

Modo desktop no Windows bloqueia ações e modo browser não possui encerramento controlado

3 participants