293 lines
12 KiB
Rust
293 lines
12 KiB
Rust
use prometeu_core::debugger_protocol::*;
|
|
use prometeu_core::firmware::{BootTarget, Firmware};
|
|
use prometeu_core::model::CartridgeLoader;
|
|
use prometeu_core::Hardware;
|
|
use std::io::{Read, Write};
|
|
use std::net::{TcpListener, TcpStream};
|
|
|
|
/// Host-side implementation of the PROMETEU DevTools Protocol.
|
|
///
|
|
/// This component acts as a TCP server that allows external tools (like the
|
|
/// Prometeu Debugger) to observe and control the execution of the virtual machine.
|
|
///
|
|
/// Communication is based on JSONL (JSON lines) over TCP.
|
|
pub struct HostDebugger {
|
|
/// If true, the VM will not start execution until a 'start' command is received.
|
|
pub waiting_for_start: bool,
|
|
/// The TCP listener for incoming debugger connections.
|
|
pub(crate) listener: Option<TcpListener>,
|
|
/// The currently active connection to a debugger client.
|
|
pub(crate) stream: Option<TcpStream>,
|
|
/// Sequence tracker to ensure logs are sent only once.
|
|
last_log_seq: u64,
|
|
/// Frame tracker to send telemetry snapshots periodically.
|
|
last_telemetry_frame: u64,
|
|
}
|
|
|
|
impl HostDebugger {
|
|
/// Creates a new debugger interface in an idle state.
|
|
pub fn new() -> Self {
|
|
Self {
|
|
waiting_for_start: false,
|
|
listener: None,
|
|
stream: None,
|
|
last_log_seq: 0,
|
|
last_telemetry_frame: 0,
|
|
}
|
|
}
|
|
|
|
/// Configures the debugger based on the boot target.
|
|
/// If debug mode is enabled, it binds to the specified TCP port.
|
|
pub fn setup_boot_target(&mut self, boot_target: &BootTarget, firmware: &mut Firmware) {
|
|
if let BootTarget::Cartridge { path, debug: true, debug_port } = boot_target {
|
|
self.waiting_for_start = true;
|
|
|
|
// Pre-load cartridge metadata so the Handshake message can contain
|
|
// valid information about the App being debugged.
|
|
if let Ok(cartridge) = CartridgeLoader::load(path) {
|
|
firmware.os.initialize_vm(&mut firmware.vm, &cartridge);
|
|
}
|
|
|
|
match TcpListener::bind(format!("127.0.0.1:{}", debug_port)) {
|
|
Ok(listener) => {
|
|
// Set listener to non-blocking so it doesn't halt the main loop.
|
|
listener.set_nonblocking(true).expect("Cannot set non-blocking");
|
|
self.listener = Some(listener);
|
|
println!("[Debugger] Listening for start command on port {}...", debug_port);
|
|
}
|
|
Err(e) => {
|
|
eprintln!("[Debugger] Failed to bind to port {}: {}", debug_port, e);
|
|
}
|
|
}
|
|
|
|
println!("[Debugger] (Or press D to start execution)");
|
|
}
|
|
}
|
|
|
|
/// Sends a structured response to the connected debugger client.
|
|
fn send_response(&mut self, resp: DebugResponse) {
|
|
if let Some(stream) = &mut self.stream {
|
|
if let Ok(json) = serde_json::to_string(&resp) {
|
|
let _ = stream.write_all(json.as_bytes());
|
|
let _ = stream.write_all(b"\n");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Sends an asynchronous event to the connected debugger client.
|
|
fn send_event(&mut self, event: DebugEvent) {
|
|
if let Some(stream) = &mut self.stream {
|
|
if let Ok(json) = serde_json::to_string(&event) {
|
|
let _ = stream.write_all(json.as_bytes());
|
|
let _ = stream.write_all(b"\n");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Main maintenance method called by the HostRunner every iteration.
|
|
/// It handles new connections and processes incoming commands.
|
|
pub fn check_commands(&mut self, firmware: &mut Firmware, hardware: &mut Hardware) {
|
|
// 1. Accept new client connections.
|
|
if let Some(listener) = &self.listener {
|
|
if let Ok((stream, _addr)) = listener.accept() {
|
|
// Currently, only one debugger client is supported at a time.
|
|
if self.stream.is_none() {
|
|
println!("[Debugger] Connection received!");
|
|
stream.set_nonblocking(true).expect("Cannot set non-blocking on stream");
|
|
|
|
self.stream = Some(stream);
|
|
|
|
// Immediately send the Handshake message to identify the Runtime and App.
|
|
let handshake = DebugResponse::Handshake {
|
|
protocol_version: DEVTOOLS_PROTOCOL_VERSION,
|
|
runtime_version: "0.1".to_string(),
|
|
cartridge: HandshakeCartridge {
|
|
app_id: firmware.os.current_app_id,
|
|
title: firmware.os.current_cartridge_title.clone(),
|
|
app_version: firmware.os.current_cartridge_app_version.clone(),
|
|
app_mode: firmware.os.current_cartridge_app_mode,
|
|
},
|
|
};
|
|
self.send_response(handshake);
|
|
} else {
|
|
println!("[Debugger] Connection refused: already connected.");
|
|
}
|
|
}
|
|
}
|
|
|
|
// 2. Read and process pending commands from the network stream.
|
|
if let Some(mut stream) = self.stream.take() {
|
|
let mut buf = [0u8; 4096];
|
|
match stream.read(&mut buf) {
|
|
Ok(0) => {
|
|
// TCP socket closed by the client.
|
|
println!("[Debugger] Connection closed by remote.");
|
|
self.stream = None;
|
|
// Resume VM execution if it was paused waiting for the debugger.
|
|
firmware.os.paused = false;
|
|
self.waiting_for_start = false;
|
|
}
|
|
Ok(n) => {
|
|
let data = &buf[..n];
|
|
let msg = String::from_utf8_lossy(data);
|
|
|
|
self.stream = Some(stream);
|
|
|
|
// Support multiple JSON messages in a single TCP packet.
|
|
for line in msg.lines() {
|
|
let trimmed = line.trim();
|
|
if trimmed.is_empty() { continue; }
|
|
if let Ok(cmd) = serde_json::from_str::<DebugCommand>(trimmed) {
|
|
self.handle_command(cmd, firmware, hardware);
|
|
}
|
|
}
|
|
}
|
|
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
|
|
// No data available right now, continue.
|
|
self.stream = Some(stream);
|
|
}
|
|
Err(e) => {
|
|
eprintln!("[Debugger] Connection error: {}", e);
|
|
self.stream = None;
|
|
firmware.os.paused = false;
|
|
self.waiting_for_start = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 3. Push events (logs, telemetry) to the client.
|
|
if self.stream.is_some() {
|
|
self.stream_events(firmware);
|
|
}
|
|
}
|
|
|
|
/// Dispatches a specific DebugCommand to the system components.
|
|
fn handle_command(&mut self, cmd: DebugCommand, firmware: &mut Firmware, hardware: &mut Hardware) {
|
|
match cmd {
|
|
DebugCommand::Ok | DebugCommand::Start => {
|
|
if self.waiting_for_start {
|
|
println!("[Debugger] Starting execution...");
|
|
self.waiting_for_start = false;
|
|
}
|
|
firmware.os.paused = false;
|
|
}
|
|
DebugCommand::Pause => {
|
|
firmware.os.paused = true;
|
|
}
|
|
DebugCommand::Resume => {
|
|
firmware.os.paused = false;
|
|
}
|
|
DebugCommand::Step => {
|
|
// Execute exactly one instruction and keep paused.
|
|
firmware.os.paused = true;
|
|
let _ = firmware.os.debug_step_instruction(&mut firmware.vm, hardware);
|
|
}
|
|
DebugCommand::StepFrame => {
|
|
// Execute until the end of the current logical frame.
|
|
firmware.os.paused = false;
|
|
firmware.os.debug_step_request = true;
|
|
}
|
|
DebugCommand::GetState => {
|
|
// Return detailed VM register and stack state.
|
|
let stack_top = firmware.vm.operand_stack.iter()
|
|
.rev().take(10).cloned().collect();
|
|
|
|
let resp = DebugResponse::GetState {
|
|
pc: firmware.vm.pc,
|
|
stack_top,
|
|
frame_index: firmware.os.logical_frame_index,
|
|
app_id: firmware.os.current_app_id,
|
|
};
|
|
self.send_response(resp);
|
|
}
|
|
DebugCommand::SetBreakpoint { pc } => {
|
|
firmware.vm.breakpoints.insert(pc);
|
|
}
|
|
DebugCommand::ListBreakpoints => {
|
|
let pcs = firmware.vm.breakpoints.iter().cloned().collect();
|
|
self.send_response(DebugResponse::Breakpoints { pcs });
|
|
}
|
|
DebugCommand::ClearBreakpoint { pc } => {
|
|
firmware.vm.breakpoints.remove(&pc);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Scans the system for new information to push to the debugger client.
|
|
fn stream_events(&mut self, firmware: &mut Firmware) {
|
|
// 1. Process and send new log entries.
|
|
let new_events = firmware.os.log_service.get_after(self.last_log_seq);
|
|
for event in new_events {
|
|
self.last_log_seq = event.seq;
|
|
|
|
// Map specific internal log tags to protocol events.
|
|
if event.tag == 0xDEB1 {
|
|
self.send_event(DebugEvent::BreakpointHit {
|
|
pc: firmware.vm.pc,
|
|
frame_index: firmware.os.logical_frame_index,
|
|
});
|
|
}
|
|
|
|
// Map Certification tags (0xCA01-0xCA03) to 'Cert' protocol events.
|
|
if event.tag >= 0xCA01 && event.tag <= 0xCA03 {
|
|
let tel = &firmware.os.telemetry_last;
|
|
let cert_config = &firmware.os.certifier.config;
|
|
|
|
let (rule, used, limit) = match event.tag {
|
|
0xCA01 => (
|
|
"cycles_budget".to_string(),
|
|
tel.cycles_used,
|
|
cert_config.cycles_budget_per_frame.unwrap_or(0),
|
|
),
|
|
0xCA02 => (
|
|
"max_syscalls".to_string(),
|
|
tel.syscalls as u64,
|
|
cert_config.max_syscalls_per_frame.unwrap_or(0) as u64,
|
|
),
|
|
0xCA03 => (
|
|
"max_host_cpu_us".to_string(),
|
|
tel.host_cpu_time_us,
|
|
cert_config.max_host_cpu_us_per_frame.unwrap_or(0),
|
|
),
|
|
_ => ("unknown".to_string(), 0, 0),
|
|
};
|
|
|
|
self.send_event(DebugEvent::Cert {
|
|
rule,
|
|
used,
|
|
limit,
|
|
frame_index: firmware.os.logical_frame_index,
|
|
});
|
|
}
|
|
|
|
self.send_event(DebugEvent::Log {
|
|
level: format!("{:?}", event.level),
|
|
source: format!("{:?}", event.source),
|
|
msg: event.msg.clone(),
|
|
});
|
|
}
|
|
|
|
// 2. Send telemetry snapshots at the completion of every frame.
|
|
let current_frame = firmware.os.logical_frame_index;
|
|
if current_frame > self.last_telemetry_frame {
|
|
let tel = &firmware.os.telemetry_last;
|
|
self.send_event(DebugEvent::Telemetry {
|
|
frame_index: tel.frame_index,
|
|
vm_steps: tel.vm_steps,
|
|
syscalls: tel.syscalls,
|
|
cycles: tel.cycles_used,
|
|
cycles_budget: tel.cycles_budget,
|
|
host_cpu_time_us: tel.host_cpu_time_us,
|
|
violations: tel.violations,
|
|
gfx_used_bytes: tel.gfx_used_bytes,
|
|
gfx_inflight_bytes: tel.gfx_inflight_bytes,
|
|
gfx_slots_occupied: tel.gfx_slots_occupied,
|
|
audio_used_bytes: tel.audio_used_bytes,
|
|
audio_inflight_bytes: tel.audio_inflight_bytes,
|
|
audio_slots_occupied: tel.audio_slots_occupied,
|
|
});
|
|
self.last_telemetry_frame = current_frame;
|
|
}
|
|
}
|
|
}
|