fixes on dev-tools

This commit is contained in:
Nilton Constantino 2026-01-18 07:46:04 +00:00
parent 47a2f9971d
commit 6b9858d481
No known key found for this signature in database
4 changed files with 209 additions and 25 deletions

View File

@ -105,9 +105,9 @@ impl PrometeuRunner {
} }
fn check_debug_commands(&mut self) { fn check_debug_commands(&mut self) {
if self.debug_waiting_for_start { if let Some(listener) = &self.debug_listener {
if let Some(listener) = &self.debug_listener { if let Ok((stream, _addr)) = listener.accept() {
if let Ok((stream, _addr)) = listener.accept() { if self.debug_stream.is_none() {
println!("[Debugger] Connection received!"); println!("[Debugger] Connection received!");
stream.set_nonblocking(true).expect("Cannot set non-blocking on stream"); stream.set_nonblocking(true).expect("Cannot set non-blocking on stream");
@ -125,6 +125,8 @@ impl PrometeuRunner {
}, },
}; };
self.send_debug_response(handshake); self.send_debug_response(handshake);
} else {
println!("[Debugger] Connection refused: already connected.");
} }
} }
} }
@ -135,17 +137,25 @@ impl PrometeuRunner {
Ok(0) => { Ok(0) => {
println!("[Debugger] Connection closed by remote."); println!("[Debugger] Connection closed by remote.");
self.debug_stream = None; self.debug_stream = None;
self.firmware.os.paused = false;
self.debug_waiting_for_start = false;
} }
Ok(n) => { Ok(n) => {
let data = &buf[..n]; let data = &buf[..n];
// Processar múltiplos comandos se houver \n // Processar múltiplos comandos se houver \n
let msg = String::from_utf8_lossy(data); let msg = String::from_utf8_lossy(data);
self.debug_stream = Some(stream); // Coloca de volta antes de processar comandos
for line in msg.lines() { for line in msg.lines() {
if let Ok(cmd) = serde_json::from_str::<DebugCommand>(line) { let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if let Ok(cmd) = serde_json::from_str::<DebugCommand>(trimmed) {
self.handle_debug_command(cmd); self.handle_debug_command(cmd);
} }
} }
self.debug_stream = Some(stream);
} }
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
self.debug_stream = Some(stream); self.debug_stream = Some(stream);
@ -153,6 +163,8 @@ impl PrometeuRunner {
Err(e) => { Err(e) => {
eprintln!("[Debugger] Connection error: {}", e); eprintln!("[Debugger] Connection error: {}", e);
self.debug_stream = None; self.debug_stream = None;
self.firmware.os.paused = false;
self.debug_waiting_for_start = false;
} }
} }
} }
@ -169,7 +181,6 @@ impl PrometeuRunner {
if self.debug_waiting_for_start { if self.debug_waiting_for_start {
println!("[Debugger] Starting execution..."); println!("[Debugger] Starting execution...");
self.debug_waiting_for_start = false; self.debug_waiting_for_start = false;
self.debug_listener = None;
} }
self.firmware.os.paused = false; self.firmware.os.paused = false;
} }
@ -190,9 +201,9 @@ impl PrometeuRunner {
} }
DebugCommand::GetState => { DebugCommand::GetState => {
let stack_top = self.firmware.vm.operand_stack.iter() let stack_top = self.firmware.vm.operand_stack.iter()
.rev().take(10).filter_map(|v| v.as_integer()).collect(); .rev().take(10).cloned().collect();
let resp = DebugResponse::State { let resp = DebugResponse::GetState {
pc: self.firmware.vm.pc, pc: self.firmware.vm.pc,
stack_top, stack_top,
frame_index: self.firmware.os.logical_frame_index, frame_index: self.firmware.os.logical_frame_index,
@ -441,7 +452,6 @@ impl ApplicationHandler for PrometeuRunner {
if is_down && code == KeyCode::KeyD && self.debug_waiting_for_start { if is_down && code == KeyCode::KeyD && self.debug_waiting_for_start {
self.debug_waiting_for_start = false; self.debug_waiting_for_start = false;
self.debug_listener = None; // Fecha o socket
println!("[Debugger] Execution started!"); println!("[Debugger] Execution started!");
} }
@ -703,7 +713,7 @@ mod tests {
// Processa o comando recebido // Processa o comando recebido
runner.check_debug_commands(); runner.check_debug_commands();
assert!(!runner.debug_waiting_for_start, "Execução deve ter iniciado após comando start"); assert!(!runner.debug_waiting_for_start, "Execução deve ter iniciado após comando start");
assert!(runner.debug_listener.is_none(), "Listener deve ter sido fechado"); assert!(runner.debug_listener.is_some(), "Listener deve permanecer aberto para reconexões");
} }
// Agora que o stream saiu do escopo do teste, o runner deve detectar o fechamento no próximo check // Agora que o stream saiu do escopo do teste, o runner deve detectar o fechamento no próximo check
@ -711,4 +721,150 @@ mod tests {
runner.check_debug_commands(); runner.check_debug_commands();
assert!(runner.debug_stream.is_none(), "Stream deve ter sido fechado após o cliente desconectar"); assert!(runner.debug_stream.is_none(), "Stream deve ter sido fechado após o cliente desconectar");
} }
#[test]
fn test_debug_reconnection() {
let mut runner = PrometeuRunner::new(None, None);
let port = 9998;
runner.set_boot_target(BootTarget::Cartridge {
path: "dummy.bin".to_string(),
debug: true,
debug_port: port,
});
// 1. Conecta e inicia
{
let mut stream = TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Deve conectar 1");
std::thread::sleep(std::time::Duration::from_millis(50));
runner.check_debug_commands();
assert!(runner.debug_stream.is_some());
stream.write_all(b"{\"type\":\"start\"}\n").expect("Deve escrever start");
std::thread::sleep(std::time::Duration::from_millis(50));
runner.check_debug_commands();
assert!(!runner.debug_waiting_for_start);
// Atualmente o listener é fechado aqui.
}
// 2. Desconecta (limpa o stream no runner)
std::thread::sleep(std::time::Duration::from_millis(50));
runner.check_debug_commands();
assert!(runner.debug_stream.is_none());
// 3. Tenta reconectar - DEVE FALHAR atualmente, mas queremos que FUNCIONE
let stream2 = TcpStream::connect(format!("127.0.0.1:{}", port));
assert!(stream2.is_ok(), "Deve aceitar nova conexão mesmo após o start");
std::thread::sleep(std::time::Duration::from_millis(50));
runner.check_debug_commands();
assert!(runner.debug_stream.is_some(), "Stream deve ter sido aceito na reconexão");
}
#[test]
fn test_debug_refuse_second_connection() {
let mut runner = PrometeuRunner::new(None, None);
let port = 9997;
runner.set_boot_target(BootTarget::Cartridge {
path: "dummy.bin".to_string(),
debug: true,
debug_port: port,
});
// 1. Primeira conexão
let mut _stream1 = TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Deve conectar 1");
std::thread::sleep(std::time::Duration::from_millis(50));
runner.check_debug_commands();
assert!(runner.debug_stream.is_some());
// 2. Segunda conexão
let mut stream2 = TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Deve conectar 2 (SO aceita)");
std::thread::sleep(std::time::Duration::from_millis(50));
runner.check_debug_commands(); // Deve aceitar e fechar stream2
// Verifica se stream2 foi fechado pelo servidor
let mut buf = [0u8; 10];
stream2.set_read_timeout(Some(std::time::Duration::from_millis(100))).unwrap();
let res = stream2.read(&mut buf);
assert!(matches!(res, Ok(0)) || res.is_err(), "Segunda conexão deve ser fechada pelo servidor");
assert!(runner.debug_stream.is_some(), "Primeira conexão deve continuar ativa");
}
#[test]
fn test_get_state_returns_response() {
let mut runner = PrometeuRunner::new(None, None);
let port = 9996;
runner.set_boot_target(BootTarget::Cartridge {
path: "dummy.bin".to_string(),
debug: true,
debug_port: port,
});
let mut stream = TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Deve conectar");
std::thread::sleep(std::time::Duration::from_millis(100));
runner.check_debug_commands();
// Limpa o buffer do handshake
let mut buf = [0u8; 2048];
let n = stream.read(&mut buf).expect("Deve ler handshake");
assert!(n > 0);
// Envia getState
stream.write_all(b"{\"type\":\"getState\"}\n").expect("Deve escrever getState");
std::thread::sleep(std::time::Duration::from_millis(100));
runner.check_debug_commands();
// Verifica se recebeu resposta
stream.set_read_timeout(Some(std::time::Duration::from_millis(500))).unwrap();
let mut full_resp = Vec::new();
let mut temp_buf = [0u8; 1024];
loop {
match stream.read(&mut temp_buf) {
Ok(0) => break,
Ok(n) => {
full_resp.extend_from_slice(&temp_buf[..n]);
if full_resp.contains(&b'\n') {
break;
}
}
Err(_) => break,
}
}
assert!(!full_resp.is_empty(), "Resposta deve ter conteúdo");
let resp: serde_json::Value = serde_json::from_slice(&full_resp).expect("Resposta deve ser JSON válido");
assert_eq!(resp["type"], "getState");
}
#[test]
fn test_debug_resume_on_disconnect() {
let mut runner = PrometeuRunner::new(None, None);
let port = 9995;
runner.set_boot_target(BootTarget::Cartridge {
path: "dummy.bin".to_string(),
debug: true,
debug_port: port,
});
// 1. Conecta e pausa
{
let mut stream = TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Deve conectar");
std::thread::sleep(std::time::Duration::from_millis(50));
runner.check_debug_commands();
stream.write_all(b"{\"type\":\"pause\"}\n").expect("Deve escrever pause");
std::thread::sleep(std::time::Duration::from_millis(50));
runner.check_debug_commands();
assert!(runner.firmware.os.paused, "VM deve estar pausada");
}
// 2. Desconecta (stream sai de escopo)
std::thread::sleep(std::time::Duration::from_millis(50));
runner.check_debug_commands();
// 3. Verifica se despausou
assert!(!runner.firmware.os.paused, "VM deve ter despausado após desconexão");
assert!(!runner.debug_waiting_for_start, "VM deve ter saído do estado waiting_for_start");
}
} }

View File

@ -1,6 +1,8 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::model::AppMode; use crate::model::AppMode;
use crate::virtual_machine::Value;
pub const DEVTOOLS_PROTOCOL_VERSION: u32 = 1; pub const DEVTOOLS_PROTOCOL_VERSION: u32 = 1;
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
@ -37,10 +39,10 @@ pub enum DebugResponse {
runtime_version: String, runtime_version: String,
cartridge: HandshakeCartridge, cartridge: HandshakeCartridge,
}, },
#[serde(rename = "state")] #[serde(rename = "getState")]
State { GetState {
pc: usize, pc: usize,
stack_top: Vec<i64>, stack_top: Vec<Value>,
frame_index: u64, frame_index: u64,
app_id: u32, app_id: u32,
}, },
@ -87,3 +89,26 @@ pub enum DebugEvent {
frame_index: u64, frame_index: u64,
}, },
} }
#[cfg(test)]
mod tests {
use super::*;
use crate::virtual_machine::Value;
#[test]
fn test_get_state_serialization() {
let resp = DebugResponse::GetState {
pc: 42,
stack_top: vec![Value::Integer(10), Value::String("test".into()), Value::Boolean(true)],
frame_index: 5,
app_id: 1,
};
let json = serde_json::to_string(&resp).unwrap();
assert!(json.contains("\"type\":\"getState\""));
assert!(json.contains("\"pc\":42"));
assert!(json.contains("\"stack_top\":[10,\"test\",true]"));
assert!(json.contains("\"frame_index\":5"));
assert!(json.contains("\"app_id\":1"));
}
}

View File

@ -1,4 +1,7 @@
#[derive(Debug, Clone)] use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Value { pub enum Value {
Integer(i64), Integer(i64),
Float(f64), Float(f64),

View File

@ -11,50 +11,50 @@
}, },
"requests": [ "requests": [
{ {
"name": "pause", "type": "pause",
"params": [] "params": []
}, },
{ {
"name": "resume", "type": "resume",
"params": [] "params": []
}, },
{ {
"name": "step", "type": "step",
"params": [] "params": []
}, },
{ {
"name": "stepFrame", "type": "stepFrame",
"params": [] "params": []
}, },
{ {
"name": "getState", "type": "getState",
"params": [], "params": [],
"response": ["pc", "stack_top", "frame_index", "app_id"] "response": ["pc", "stack_top", "frame_index", "app_id"]
}, },
{ {
"name": "setBreakpoint", "type": "setBreakpoint",
"params": ["pc"] "params": ["pc"]
}, },
{ {
"name": "clearBreakpoint", "type": "clearBreakpoint",
"params": ["pc"] "params": ["pc"]
} }
], ],
"events": [ "events": [
{ {
"name": "breakpointHit", "event": "breakpointHit",
"fields": ["pc", "frame_index"] "fields": ["pc", "frame_index"]
}, },
{ {
"name": "log", "event": "log",
"fields": ["level", "source", "msg"] "fields": ["level", "source", "msg"]
}, },
{ {
"name": "telemetry", "event": "telemetry",
"fields": ["frame_index", "vm_steps", "syscalls", "cycles"] "fields": ["frame_index", "vm_steps", "syscalls", "cycles"]
}, },
{ {
"name": "cert", "event": "cert",
"fields": ["rule", "used", "limit", "frame_index"] "fields": ["rule", "used", "limit", "frame_index"]
} }
] ]