Skip to content
102 changes: 86 additions & 16 deletions crates/dap-adapter/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,35 @@ fn main() -> io::Result<()> {
for outgoing in session.handle(&req) {
match outgoing {
Outgoing::Response(_) | Outgoing::Event(_) => emit(&out, &outgoing),
Outgoing::SpawnServer(spec) => match spawn_server(&spec, &out) {
Ok(child) => server = Some(child),
Err(e) => out.event(
"output",
serde_json::json!({
"category": "stderr",
"output": format!("Falha ao iniciar o servidor: {e}\n"),
}),
),
},
// Subir e reiniciar são o mesmo ato: derrubar o que houver e
// pôr um servidor no lugar. No `launch` não há o que derrubar;
// no `restart` há, e o `Drop` do `ServerChild` cuida disso ao
// atribuir `None`. Reconectar depois é inofensivo no primeiro
// caso — o `launch` emite `ConnectPlugin` logo em seguida — e
// necessário no segundo, porque o canal morreu com o processo.
Outgoing::SpawnServer(spec) => {
server = None;
match spawn_server(&spec, &out) {
Ok(child) => {
server = Some(child);
// Só conecta se ainda não há cliente. Trocar o
// `PluginClient` jogaria fora a fila de comandos que
// ele acumulou — e o editor manda `setBreakpoints`
// ANTES do `launch`, então é justamente ali que os
// breakpoints estão esperando.
if plugin.is_none() {
plugin = Some(PluginClient::connect(&spec.session, out.clone()));
}
}
Err(e) => out.event(
"output",
serde_json::json!({
"category": "stderr",
"output": format!("Falha ao iniciar o servidor: {e}\n"),
}),
),
}
}
Outgoing::ConnectPlugin(id) => {
// A conexão é assíncrona (com retry e feedback próprios);
// não bloqueia o loop nem falha de imediato.
Expand Down Expand Up @@ -158,10 +177,40 @@ fn forward_stream<R: io::Read + Send + 'static>(stream: R, category: &'static st
std::thread::spawn(move || pump_stream(stream, category, &out));
}

/// Bytes do console do servidor em texto.
///
/// O SA-MP/open.mp escreve o console em Windows-1252, não em UTF-8: em
/// português `ção` sai como `\xe7\xe3o`, que `from_utf8_lossy` trocaria por `�`.
/// Tentamos UTF-8 primeiro — é o que um gamemode moderno pode emitir — e só
/// caímos no cp1252 quando a sequência não é UTF-8 válida.
///
/// A conversão é direta e sem dependência: em cp1252 os bytes 0xA0–0xFF já são
/// os mesmos code points Unicode (herança do Latin-1); apenas 0x80–0x9F têm
/// tabela própria.
fn decodificar_console(bytes: &[u8]) -> String {
/// Os 32 code points de 0x80–0x9F, onde cp1252 difere do Latin-1.
const ALTOS: [char; 32] = [
'\u{20AC}', '\u{81}', '\u{201A}', '\u{192}', '\u{201E}', '\u{2026}', '\u{2020}',
'\u{2021}', '\u{2C6}', '\u{2030}', '\u{160}', '\u{2039}', '\u{152}', '\u{8D}', '\u{17D}',
'\u{8F}', '\u{90}', '\u{2018}', '\u{2019}', '\u{201C}', '\u{201D}', '\u{2022}', '\u{2013}',
'\u{2014}', '\u{2DC}', '\u{2122}', '\u{161}', '\u{203A}', '\u{153}', '\u{9D}', '\u{17E}',
'\u{178}',
];
if let Ok(s) = std::str::from_utf8(bytes) {
return s.to_owned();
}
bytes
.iter()
.map(|&b| match b {
0x80..=0x9F => ALTOS[usize::from(b - 0x80)],
_ => char::from(b),
})
.collect()
}

/// Lógica síncrona de `forward_stream`, isolada para ser testável sem thread.
/// Lê `stream` linha a linha e emite cada linha como `output`. `read_until('\n')`
/// (em vez de `lines()`) preserva a quebra de linha e a última linha sem `\n`, e
/// não falha com bytes não-UTF-8 (lidos com substituição).
/// (em vez de `lines()`) preserva a quebra de linha e a última linha sem `\n`.
fn pump_stream<R: io::Read>(stream: R, category: &'static str, out: &DapOut) {
use std::io::BufRead;
let mut reader = BufReader::new(stream);
Expand All @@ -170,7 +219,7 @@ fn pump_stream<R: io::Read>(stream: R, category: &'static str, out: &DapOut) {
if n == 0 {
break; // EOF — o servidor fechou a pipe
}
let text = String::from_utf8_lossy(&buf).into_owned();
let text = decodificar_console(&buf);
out.event(
"output",
serde_json::json!({ "category": category, "output": text }),
Expand Down Expand Up @@ -237,6 +286,26 @@ mod tests {
use super::*;
use std::sync::{Arc, Mutex};

/// O console do SA-MP/open.mp é Windows-1252: `from_utf8_lossy` trocava
/// cada acento por `\u{FFFD}` no CONSOLE DE DEPURAÇÃO.
#[test]
fn console_decodifica_windows1252() {
// Bytes como o servidor os escreve: "ção" = 0xE7 0xE3 0x6F.
let cru = b"deslocamento=4 (acentua\xe7\xe3o: cora\xe7\xe3o)\n";
assert_eq!(
decodificar_console(cru),
"deslocamento=4 (acentuação: coração)\n"
);
}

/// UTF-8 válido tem prioridade: um gamemode moderno pode emitir UTF-8, e
/// interpretá-lo como cp1252 daria mojibake ao contrário.
#[test]
fn console_preserva_utf8_valido() {
assert_eq!(decodificar_console("ação".as_bytes()), "ação");
assert_eq!(decodificar_console(b"plain ascii"), "plain ascii");
}

#[test]
fn base64_encode_matches_rfc() {
assert_eq!(base64_encode(b""), "");
Expand Down Expand Up @@ -276,8 +345,9 @@ mod tests {
assert!(raw.contains("alpha\\n"));
assert!(raw.contains("beta")); // última linha sem `\n` ainda é emitida
assert!(raw.contains("\"category\":\"stdout\""));
// O byte inválido não derruba nada (substituído por U+FFFD, emitido como
// UTF-8 cru pelo serde, não escapado).
assert!(raw.contains('\u{fffd}'));
// Byte que não é UTF-8 válido não derruba nada e não vira `\u{FFFD}`:
// cai na leitura cp1252, onde 0xFF é `ÿ`.
assert!(raw.contains('ÿ'));
assert!(!raw.contains('\u{fffd}'));
}
}
1 change: 0 additions & 1 deletion crates/dap-adapter/src/messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ impl Response {
}
}

#[allow(dead_code)] // usado quando os handlers passarem a falhar explicitamente
pub fn fail(seq: i64, req: &Request, message: impl Into<String>) -> Self {
Self {
seq,
Expand Down
65 changes: 65 additions & 0 deletions crates/dap-adapter/src/plugin_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use std::thread;
use std::time::Duration;

use interprocess::local_socket::traits::Stream as _;
use pawnpro_dbg_protocol::messages::{self, Locale, MsgKey};
use pawnpro_dbg_protocol::transport::{self, LocalStream};
use pawnpro_dbg_protocol::{self as wire, Command, Event};
use serde_json::json;
Expand Down Expand Up @@ -86,6 +87,27 @@ pub struct PluginClient {
writer: Arc<Mutex<Writer>>,
}

/// Avisa no console quando o plugin do servidor e o adaptador têm versões
/// diferentes.
///
/// Versões diferentes conversam até a primeira mensagem que um dos lados não
/// entende, e o sintoma é a depuração simplesmente não fazer nada. Dizer qual é
/// a diferença troca uma falha muda por uma instrução.
fn avisar_se_versao_difere(plugin: &str, out: &DapOut) {
let minha = env!("CARGO_PKG_VERSION");
if plugin == minha {
return;
}
// Mesmo locale que o adaptador propaga ao plugin: a fonte já existe.
let loc = std::env::var("PAWNPRO_DBG_LOCALE")
.map_or_else(|_| Locale::default(), |t| Locale::from_tag(&t));
let texto = messages::format(loc, MsgKey::PluginVersaoDiferente, &[plugin, minha, minha]);
out.event(
"output",
json!({ "category": "important", "output": format!("{texto}\n") }),
);
}

impl PluginClient {
/// Inicia a conexão com o plugin (socket da sessão `id`) numa thread, com
/// retry. Retorna imediatamente — os comandos enviados nesse meio-tempo são
Expand Down Expand Up @@ -194,6 +216,7 @@ impl PluginClient {
let _ = tx.send(bytes);
}
}
Ok(Event::Hello { version }) => avisar_se_versao_difere(&version, &out),
Ok(Event::Exited) => {
out.event("terminated", serde_json::Value::Null);
break;
Expand Down Expand Up @@ -303,3 +326,45 @@ pub fn update_array_elem(frame: usize, var_index: usize, elem: usize, value: &st
child.value = value.to_string();
}
}

#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Arc, Mutex};

#[derive(Clone)]
struct Sink(Arc<Mutex<Vec<u8>>>);
impl std::io::Write for Sink {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0.lock().unwrap().extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}

/// Versões iguais são o caso normal: avisar ali seria ruído em toda sessão.
#[test]
fn versao_igual_nao_avisa() {
let buf = Arc::new(Mutex::new(Vec::new()));
let out = DapOut::new(Box::new(Sink(Arc::clone(&buf))), 1);
avisar_se_versao_difere(env!("CARGO_PKG_VERSION"), &out);
assert!(buf.lock().unwrap().is_empty());
}

/// Versão diferente é o caso de #85: sem o aviso, a depuração falha em
/// silêncio e nada no editor explica por quê.
#[test]
fn versao_diferente_avisa_com_as_duas() {
let buf = Arc::new(Mutex::new(Vec::new()));
let out = DapOut::new(Box::new(Sink(Arc::clone(&buf))), 1);
avisar_se_versao_difere("0.1.0", &out);
let saida = String::from_utf8_lossy(&buf.lock().unwrap()).into_owned();
assert!(saida.contains("0.1.0"), "a versão do plugin");
assert!(
saida.contains(env!("CARGO_PKG_VERSION")),
"e a do adaptador"
);
}
}
Loading