371 lines
14 KiB
Rust
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);
|
|
}
|
|
}
|