371 lines
14 KiB
Rust

use prometeu_drivers::hardware::Hardware;
use prometeu_firmware::{BootTarget, Firmware};
use prometeu_hal::cartridge_loader::CartridgeLoader;
use prometeu_hal::debugger_protocol::*;
use prometeu_hal::telemetry::{CertificationConfig, TelemetryFrame};
use prometeu_system::CrashReport;
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.
/// Detailed inspection and certification events are synthesized in the host
/// from telemetry snapshots and host/runtime integration state.
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,
/// Last fault summary sent to the debugger client.
last_fault_summary: Option<String>,
}
impl Default for HostDebugger {
fn default() -> Self {
Self::new()
}
}
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,
last_fault_summary: None,
}
}
/// 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) {
let _ = 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
&& 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
&& 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
&& 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);
self.last_fault_summary = None;
// 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_top(10);
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.insert_breakpoint(pc);
}
DebugCommand::ListBreakpoints => {
let pcs = firmware.vm.breakpoints_list();
self.send_response(DebugResponse::Breakpoints { pcs });
}
DebugCommand::ClearBreakpoint { pc } => {
firmware.vm.remove_breakpoint(pc);
}
}
}
pub(crate) fn cert_event_from_snapshot(
tag: u16,
telemetry: TelemetryFrame,
cert_config: &CertificationConfig,
frame_index: u64,
) -> Option<DebugEvent> {
let (rule, used, limit) = match tag {
0xCA01 => (
"cycles_budget".to_string(),
telemetry.cycles_used,
cert_config.cycles_budget_per_frame.unwrap_or(0),
),
0xCA02 => (
"max_syscalls".to_string(),
telemetry.syscalls as u64,
cert_config.max_syscalls_per_frame.unwrap_or(0) as u64,
),
0xCA03 => (
"max_host_cpu_us".to_string(),
telemetry.host_cpu_time_us,
cert_config.max_host_cpu_us_per_frame.unwrap_or(0),
),
0xCA04 => (
"max_glyph_slots_used".to_string(),
telemetry.glyph_slots_used as u64,
cert_config.max_glyph_slots_used.unwrap_or(0) as u64,
),
0xCA05 => (
"max_sound_slots_used".to_string(),
telemetry.sound_slots_used as u64,
cert_config.max_sound_slots_used.unwrap_or(0) as u64,
),
0xCA06 => (
"max_heap_bytes".to_string(),
telemetry.heap_used_bytes as u64,
cert_config.max_heap_bytes.unwrap_or(0) as u64,
),
0xCA07 => (
"max_logs_per_frame".to_string(),
telemetry.logs_count as u64,
cert_config.max_logs_per_frame.unwrap_or(0) as u64,
),
_ => return None,
};
Some(DebugEvent::Cert { rule, used, limit, frame_index })
}
pub(crate) fn telemetry_event_from_snapshot(telemetry: TelemetryFrame) -> DebugEvent {
DebugEvent::Telemetry {
frame_index: telemetry.frame_index,
vm_steps: telemetry.vm_steps,
syscalls: telemetry.syscalls,
cycles: telemetry.cycles_used,
cycles_budget: telemetry.cycles_budget,
host_cpu_time_us: telemetry.host_cpu_time_us,
violations: telemetry.violations,
glyph_slots_used: telemetry.glyph_slots_used,
glyph_slots_total: telemetry.glyph_slots_total,
sound_slots_used: telemetry.sound_slots_used,
sound_slots_total: telemetry.sound_slots_total,
scene_slots_used: telemetry.scene_slots_used,
scene_slots_total: telemetry.scene_slots_total,
}
}
/// Scans the system for new information to push to the debugger client.
fn stream_events(&mut self, firmware: &mut Firmware) {
if let Some(report) = firmware.os.last_crash_report.as_ref() {
self.stream_fault(report);
} else {
self.last_fault_summary = None;
}
// 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,
});
}
if (0xCA01..=0xCA07).contains(&event.tag)
&& let Some(cert_event) = Self::cert_event_from_snapshot(
event.tag,
firmware.os.atomic_telemetry.snapshot(),
&firmware.os.certifier.config,
firmware.os.logical_frame_index,
)
{
self.send_event(cert_event);
}
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.atomic_telemetry.snapshot();
self.send_event(Self::telemetry_event_from_snapshot(tel));
self.last_telemetry_frame = current_frame;
}
}
fn stream_fault(&mut self, report: &CrashReport) {
let summary = report.summary();
if self.last_fault_summary.as_deref() == Some(summary.as_str()) {
return;
}
let (trap_code, opcode) = match report {
CrashReport::VmTrap { trap } => (Some(trap.code), Some(trap.opcode)),
CrashReport::VmPanic { .. } | CrashReport::VmInit { .. } => (None, None),
};
self.send_event(DebugEvent::Fault {
kind: report.kind().to_string(),
summary: summary.clone(),
pc: report.pc(),
trap_code,
opcode,
});
self.last_fault_summary = Some(summary);
}
}