fix: corrige servidor local, porta e encerramento controlado (#39) - #40
Open
Rossi-Luciano wants to merge 7 commits into
Open
fix: corrige servidor local, porta e encerramento controlado (#39)#40Rossi-Luciano wants to merge 7 commits into
Rossi-Luciano wants to merge 7 commits into
Conversation
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
wsgiref.simple_server.make_server(síncrono) porwerkzeug.serving.make_server(..., threaded=True)nos dois modos.--portnão é informado, e abre o navegador sozinho.--windowednão tem console em nenhum dos dois modos.server.shutdown()e aguarda a thread terminar (thread.join()).POST /shutdownprotegida por token gerado por processo.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), depoisspsvalidator/src/spsvalidator/web/routes.py(rotaPOST /shutdowne navegação de volta na prévia HTML).Como este poderia ser testado manualmente?
spsvalidator --browsersem--port: deve escolher uma porta livre e abrir o navegador automaticamente, mostrando a URL no rodapé.--portexplícito: a segunda deve mostrar um diálogo de erro claro em vez de travar em silêncio.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 abretarget="_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--windowednão tem console paraCtrl+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)
Este PR manipula dados sensíveis ou pessoais (LGPD)?
Este PR altera autenticação, autorização, controle de acesso ou gerenciamento de sessão?
POST /shutdown(modo browser) é protegida por um token aleatório (secrets.token_urlsafe(32)) gerado uma vez por processo e comparado comsecrets.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.Este PR introduz, atualiza ou remove dependências de terceiros?
werkzeugjá é dependência transitiva do Flask (usada aqui viawerkzeug.serving.make_server);tkinter,webbrowser,secretsethreadingsão da biblioteca padrão. Nenhuma entrada nova empyproject.toml.Este PR foi validado pelo pipeline de segurança (SonarQube / Trivy)?
Este PR concatena, monta ou executa comandos SQL, HTML ou JavaScript a partir de entrada externa?
_inject_back_banner) usa apenas uma URL gerada porurl_for(não texto arbitrário de entrada externa). O token de encerramento é enviado viaencodeURIComponentno corpo do POST.Este PR expõe novos endpoints, telas ou serviços?
POST /shutdownsó é alcançável em127.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.Algum segredo, senha, chave ou token está sendo adicionado ao código-fonte?
secrets.token_urlsafe(32)) a cada processo, nunca hardcoded.