Skip to content

fix: reiniciar a depuração sem encerrar a sessão, e breakpoints no servidor novo - #20

Merged
NullSablex merged 8 commits into
masterfrom
fix/restart-sem-encerrar-sessao
Sep 3, 2026
Merged

fix: reiniciar a depuração sem encerrar a sessão, e breakpoints no servidor novo#20
NullSablex merged 8 commits into
masterfrom
fix/restart-sem-encerrar-sessao

Conversation

@NullSablex

Copy link
Copy Markdown
Owner

O que muda

Reiniciar a depuração encerrava a sessão e subia outra. Os breakpoints se perdiam no caminho, e o console vinha com acentos quebrados.

Reiniciar deixa de derrubar a sessão

O restart relançava o adaptador inteiro. Como o servidor é processo filho dele, tudo caía junto — e o editor abria uma sessão nova, com os breakpoints reresolvidos do zero.

Agora o adaptador troca o processo por baixo e mantém a sessão viva. A identidade é o canal nomeado pela session, não o parentesco: matando o servidor e subindo outro com a mesma session, o plugin reabre o mesmo socket e o cliente reconecta pelo retry que já existia.

Breakpoints

  • Não chegavam ao plugin. O editor manda setBreakpoints antes do launch, e a fila que os guardava era descartada quando o PluginClient era trocado.
  • Ficavam no .amx velho. O binário costuma ser recompilado entre uma sessão e outra — é o motivo mais comum de reiniciar — e o mapa linha↔endereço muda junto. Recarregar o bloco de debug e reresolver os marcadores impede a VM de parar no lugar errado.
  • Não armavam no servidor novo. Depois da troca de processo, os breakpoints existiam no adaptador mas nunca eram enviados ao plugin recém-conectado.

Recompilação no restart

Reiniciar com o fonte alterado deixava o servidor subir com o binário velho. O adaptador detecta a diferença de data e pede a recompilação por evento — compilar é atribuição da extensão, que conhece o compilador e as flags. A verificação fica no ponto por onde todo restart passa, venha do botão nativo, da tecla ou da paleta.

Console

A saída do servidor vinha em Windows-1252 e era lida como UTF-8: cada acento virava um caractere de substituição no meio das mensagens.

Identificação

Plugin e adaptador passam a se identificar ao conectar, para o diagnóstico dizer qual versão está de cada lado quando as duas divergem.

O `restart` do DAP marcava a sessão como encerrada e emitia `terminated`
com `restart: true`, delegando ao editor recriar tudo. Na prática a
depuração caía a cada recarga do gamemode — justamente o ciclo mais comum
de quem depura — e o painel do servidor não tinha como reiniciar nada.

A amarra era de desenho, não do protocolo: o servidor é filho do adaptador
e morre no `Drop`, mas o que **identifica** a sessão é o socket nomeado por
`PAWNPRO_DBG_SESSION`, não o parentesco de processo. O plugin escuta nesse
socket e o `PluginClient` já sabia reconectar, com retry de até um minuto.

Então o adaptador passa a trocar o servidor por baixo: mata o filho atual,
sobe outro com a mesma `session` e reconecta. O estado que importa vive no
adaptador — breakpoints de linha, de função e o bloco de debug — e continua
valendo sem o editor reenviar nada.

- `SpawnSpec` do launch fica guardado, e é o que permite repetir o comando.
- `supportsRestartRequest` passa a ser declarado: sem isso o editor nem
  chega a mandar `restart`.
- Sem `spawn_spec` (attach, servidor que não é nosso) o comportamento
  antigo continua: quem subiu o servidor é quem sabe reiniciá-lo.
- `continued` avisa o editor de que não há mais frame parado, já que o
  processo que os produzia acabou de morrer.

Fecha #19.
Ao implementar o restart eu havia criado uma variante `RespawnServer`
separada, com um handler quase idêntico ao do `SpawnServer`. São o mesmo
ato: pôr um servidor no lugar do que houver — no launch não há o que
derrubar, no restart há, e o `Drop` do `ServerChild` cuida disso.

Fica uma variante e um handler. O `launch` também deixa de emitir
`ConnectPlugin` quando tem servidor próprio: quem sobe já conecta, e a
conexão traz retry. No attach, onde o servidor não é nosso, ele continua
sendo o único caminho.

Dois `allow(dead_code)` saem: `resolved_breakpoints` e `set_debug` são
usados pelos testes, e a anotação escondia isso — viram `cfg(test)`, que
diz a verdade e ainda tira o código do binário. Os comentários que os
acompanhavam prometiam um 'Componente 2' e uma 'integração futura' que já
não descrevem o estado do projeto.

90 testes, clippy pedantic e fmt limpos.
Recompilar o gamemode é o motivo mais comum para reiniciar — e muda o mapa
linha↔endereço. Os endereços resolvidos antes passam a apontar para
instruções erradas: a VM pararia no lugar errado, ou em lugar nenhum.

O adaptador passa a guardar os breakpoints como o editor os pediu (linha e
modificadores, antes de virarem endereço). No restart, recarrega o bloco de
debug do .amx e resolve tudo de novo a partir desse pedido, emitindo um
evento `breakpoint` por marcador para o editor reposicionar — a linha pode
ter andado, ou o breakpoint deixado de existir.

A resolução vira um método só, usado pelo setBreakpoints e pelo restart:
antes o laço vivia inline no handler e não havia como repeti-lo.

Remove o último allow(dead_code) do adaptador: Response::fail tem 7 usos
reais, a anotação era resíduo.
Regressão que introduzi ao unificar subir/reiniciar: o handler criava um
PluginClient novo a cada spawn, jogando fora o anterior — e com ele a fila
de comandos que já estava acumulada. Agora só conecta se ainda não há
cliente.

Mas a causa de fundo é anterior e maior: o editor manda `setBreakpoints`
**antes** do `launch`, quando ainda não existe canal com o plugin. Comando
sem canal é descartado em silêncio (`if let Some(c) = &plugin`), então os
breakpoints pedidos antes de a sessão subir simplesmente nunca chegavam.

O `configurationDone` passa a reenviar o conjunto real antes de liberar a
VM — é o ponto do protocolo em que a configuração terminou e o servidor já
está subindo, e portanto o lugar certo para isso.
O restart reresolvia os endereços contra o .amx recompilado e emitia os
eventos `breakpoint` para o editor — o marcador ia para a linha certa —,
mas nada disso chegava ao plugin. O servidor novo sobe com um plugin novo,
sem breakpoint nenhum: o estado morreu junto com o processo anterior. A VM
rodava sem saber onde parar e a execução passava direto.

É o mesmo buraco que 7742b90 fechou no launch, no outro caminho. Faltavam
três coisas:

- `ConnectPlugin`: o canal antigo morreu com o processo, e o `PluginClient`
  só tem retry na conexão inicial — reusá-lo mandava comandos para um
  socket morto. A guarda `plugin.is_none()` no `main`, criada para proteger
  a fila do launch, impedia a reconexão justamente onde ela era necessária.
- `SetBreakpoints`: os endereços reresolvidos precisam chegar ao plugin.
- `Configured`: o plugin bloqueia a VM na carga esperando este sinal, com
  timeout de 10 s. Sem ele o servidor novo travaria até o timeout, e
  breakpoints em OnGameModeInit se perderiam.

O teste `restart_reresolve_os_breakpoints` só olhava o estado interno e o
evento para o editor; passa a exigir também o que chega ao plugin.
O SA-MP/open.mp escreve o console em Windows-1252, e o adaptador lia a
saída com `from_utf8_lossy`: em português `ção` chega como `\xe7\xe3o`, que
não é UTF-8 válido, e cada acento virava U+FFFD no CONSOLE DE DEPURAÇÃO.

Passa a tentar UTF-8 primeiro — um gamemode moderno pode emitir UTF-8, e
interpretá-lo como cp1252 daria mojibake ao contrário — e só cair no cp1252
quando a sequência é inválida. Sem dependência nova: de 0xA0 a 0xFF o
cp1252 já coincide com Unicode (herança do Latin-1), e apenas os 32 bytes
de 0x80–0x9F precisam de tabela.

Um teste afirmava que 0xFF vira U+FFFD — a asserção codificava o bug. Em
cp1252 esse byte é `ÿ`, um caractere legítimo.
Recompilar é o motivo mais comum para reiniciar, mas o `restart` do DAP vai
direto ao adaptador: ele relê o `.amx` do disco e nunca era regerado. Quem
editasse o código e reiniciasse subiria o binário velho, e os breakpoints se
resolveriam contra o mapa antigo — parando em linha errada, sem aviso.

A verificação entra no `on_restart` porque é por onde TODO restart passa: o
botão nativo do editor, a tecla e a paleta. Assim não é preciso comando
próprio nem interceptar o do editor — o que exigiria registrar
`workbench.action.debug.restart` e tornaria a extensão responsável pelo
restart de toda sessão de depuração, inclusive de outras linguagens.

Compilar continua sendo atribuição da extensão, onde vivem o compilador, as
flags e a configuração do projeto. Quando o `.pwn` é mais novo que o `.amx`,
o adaptador emite `pawnproRebuild` e para — sem subir o servidor. A extensão
compila e reenvia o `restart`, que então segue o caminho normal.

`rebuild_pedido` guarda contra laço: um fonte cuja data continuasse à frente
(compilação falhou, relógio adiantado) pediria rebuild para sempre.
O protocolo não tinha versionamento: plugin e adaptador de versões diferentes
conversavam até a primeira mensagem que um dos lados não entendia, e o
sintoma era a depuração não fazer nada — nenhum painel se popular, nenhum
breakpoint pausar, nada indicando erro. Foi o que ocorreu com um plugin 0.1.0
e um adaptador 0.2.0 (PawnPro#85).

O plugin passa a mandar `Hello { version }` como primeira mensagem ao aceitar
a conexão. O adaptador compara com a própria versão e, se diferirem, escreve
no console qual é qual e o que atualizar.

Compatível com o que já está instalado nos dois sentidos: um adaptador antigo
ignora o evento que não conhece (`Err(_) => {}` no parser), e um plugin antigo
simplesmente não anuncia — o adaptador novo segue sem avisar, como antes.
@NullSablex
NullSablex merged commit a188ee5 into master Sep 3, 2026
5 checks passed
@NullSablex
NullSablex deleted the fix/restart-sem-encerrar-sessao branch September 3, 2026 11:04
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