fix: reiniciar a depuração sem encerrar a sessão, e breakpoints no servidor novo - #20
Merged
Merged
Conversation
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.
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 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 mesmasession, o plugin reabre o mesmo socket e o cliente reconecta pelo retry que já existia.Breakpoints
setBreakpointsantes dolaunch, e a fila que os guardava era descartada quando oPluginClientera trocado..amxvelho. 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.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.