Implement GDB/LLDB compatible server #3
|
@ -151,6 +151,7 @@ pub struct Emulator {
|
|||
linked: Arc<AtomicBool>,
|
||||
renderers: HashMap<SimId, TextureSink>,
|
||||
messages: HashMap<SimId, mpsc::Sender<Toast>>,
|
||||
debuggers: HashMap<SimId, DebugInfo>,
|
||||
eye_contents: Vec<u8>,
|
||||
audio_samples: Vec<f32>,
|
||||
}
|
||||
|
@ -174,6 +175,7 @@ impl Emulator {
|
|||
linked,
|
||||
renderers: HashMap::new(),
|
||||
messages: HashMap::new(),
|
||||
debuggers: HashMap::new(),
|
||||
eye_contents: vec![0u8; 384 * 224 * 2],
|
||||
audio_samples: vec![0.0; EXPECTED_FRAME_SIZE],
|
||||
})
|
||||
|
@ -288,6 +290,48 @@ impl Emulator {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn start_debugging(&mut self, sim_id: SimId, sender: DebugSender) {
|
||||
let debug = DebugInfo {
|
||||
sender,
|
||||
stop_reason: Some(DebugStopReason::Trapped),
|
||||
};
|
||||
self.debuggers.insert(sim_id, debug);
|
||||
self.state
|
||||
.store(EmulatorState::Debugging, Ordering::Release);
|
||||
}
|
||||
|
||||
fn stop_debugging(&mut self, sim_id: SimId) {
|
||||
self.debuggers.remove(&sim_id);
|
||||
if self.debuggers.is_empty() {
|
||||
self.state.store(EmulatorState::Running, Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
fn debug_interrupt(&mut self, sim_id: SimId) {
|
||||
let Some(debugger) = self.debuggers.get_mut(&sim_id) else {
|
||||
self.stop_debugging(sim_id);
|
||||
return;
|
||||
};
|
||||
if !matches!(debugger.stop_reason, Some(DebugStopReason::Trapped)) {
|
||||
debugger.stop_reason = Some(DebugStopReason::Trapped);
|
||||
if debugger
|
||||
.sender
|
||||
.send(DebugEvent::Stopped(DebugStopReason::Trapped))
|
||||
.is_err()
|
||||
{
|
||||
self.stop_debugging(sim_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn debug_continue(&mut self, sim_id: SimId) {
|
||||
let Some(debugger) = self.debuggers.get_mut(&sim_id) else {
|
||||
self.stop_debugging(sim_id);
|
||||
return;
|
||||
};
|
||||
debugger.stop_reason = None;
|
||||
}
|
||||
|
||||
pub fn run(&mut self) {
|
||||
loop {
|
||||
let idle = self.tick();
|
||||
|
@ -320,8 +364,14 @@ impl Emulator {
|
|||
let p1_state = self.sim_state[SimId::Player1.to_index()].load(Ordering::Acquire);
|
||||
let p2_state = self.sim_state[SimId::Player2.to_index()].load(Ordering::Acquire);
|
||||
let state = self.state.load(Ordering::Acquire);
|
||||
let p1_running = state == EmulatorState::Running && p1_state == SimState::Ready;
|
||||
let p2_running = state == EmulatorState::Running && p2_state == SimState::Ready;
|
||||
// Don't emulate if the state is "paused", or if any sim is paused in the debugger
|
||||
let running = match state {
|
||||
EmulatorState::Paused => false,
|
||||
EmulatorState::Running => true,
|
||||
EmulatorState::Debugging => self.debuggers.values().all(|d| d.stop_reason.is_none()),
|
||||
};
|
||||
let p1_running = running && p1_state == SimState::Ready;
|
||||
let p2_running = running && p2_state == SimState::Ready;
|
||||
let mut idle = !p1_running && !p2_running;
|
||||
if p1_running && p2_running {
|
||||
Sim::emulate_many(&mut self.sims);
|
||||
|
@ -405,6 +455,18 @@ impl Emulator {
|
|||
EmulatorCommand::Resume => {
|
||||
self.resume_sims();
|
||||
}
|
||||
EmulatorCommand::StartDebugging(sim_id, debugger) => {
|
||||
self.start_debugging(sim_id, debugger);
|
||||
}
|
||||
EmulatorCommand::StopDebugging(sim_id) => {
|
||||
self.stop_debugging(sim_id);
|
||||
}
|
||||
EmulatorCommand::DebugInterrupt(sim_id) => {
|
||||
self.debug_interrupt(sim_id);
|
||||
}
|
||||
EmulatorCommand::DebugContinue(sim_id) => {
|
||||
self.debug_continue(sim_id);
|
||||
}
|
||||
EmulatorCommand::ReadRegister(sim_id, register, done) => {
|
||||
let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
|
||||
return;
|
||||
|
@ -476,6 +538,10 @@ pub enum EmulatorCommand {
|
|||
StopSecondSim,
|
||||
Pause,
|
||||
Resume,
|
||||
StartDebugging(SimId, DebugSender),
|
||||
StopDebugging(SimId),
|
||||
DebugInterrupt(SimId),
|
||||
DebugContinue(SimId),
|
||||
ReadRegister(SimId, VBRegister, oneshot::Sender<u32>),
|
||||
ReadMemory(SimId, Range<u32>, Vec<u8>, oneshot::Sender<Vec<u8>>),
|
||||
SetAudioEnabled(bool, bool),
|
||||
|
@ -499,6 +565,24 @@ pub enum SimState {
|
|||
pub enum EmulatorState {
|
||||
Paused,
|
||||
Running,
|
||||
Debugging,
|
||||
}
|
||||
|
||||
type DebugSender = tokio::sync::mpsc::UnboundedSender<DebugEvent>;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum DebugStopReason {
|
||||
// The debugger told us to pause
|
||||
Trapped,
|
||||
}
|
||||
|
||||
struct DebugInfo {
|
||||
sender: DebugSender,
|
||||
stop_reason: Option<DebugStopReason>,
|
||||
}
|
||||
|
||||
pub enum DebugEvent {
|
||||
Stopped(DebugStopReason),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
|
248
src/gdbserver.rs
248
src/gdbserver.rs
|
@ -1,22 +1,19 @@
|
|||
use anyhow::{bail, Result};
|
||||
use registers::REGISTERS;
|
||||
use request::{Request, RequestKind};
|
||||
use request::{Request, RequestKind, RequestSource};
|
||||
use response::Response;
|
||||
use std::{
|
||||
sync::{Arc, Mutex},
|
||||
thread,
|
||||
};
|
||||
use tokio::{
|
||||
io::{AsyncWriteExt as _, BufReader, BufWriter},
|
||||
net::{
|
||||
tcp::{OwnedReadHalf, OwnedWriteHalf},
|
||||
TcpListener, TcpStream,
|
||||
},
|
||||
io::{AsyncWriteExt as _, BufReader},
|
||||
net::{TcpListener, TcpStream},
|
||||
select,
|
||||
sync::oneshot,
|
||||
sync::{mpsc, oneshot},
|
||||
};
|
||||
|
||||
use crate::emulator::{EmulatorClient, EmulatorCommand, SimId};
|
||||
use crate::emulator::{DebugEvent, DebugStopReason, EmulatorClient, EmulatorCommand, SimId};
|
||||
|
||||
mod registers;
|
||||
mod request;
|
||||
|
@ -81,8 +78,8 @@ async fn run_server(
|
|||
let Some(stream) = try_connect(port, status).await else {
|
||||
return;
|
||||
};
|
||||
let connection = GdbConnection::new(sim_id, client, stream);
|
||||
match connection.run().await {
|
||||
let mut connection = GdbConnection::new(sim_id, client);
|
||||
match connection.run(stream).await {
|
||||
Ok(()) => {
|
||||
*status.lock().unwrap() = GdbServerStatus::Stopped;
|
||||
}
|
||||
|
@ -130,122 +127,151 @@ impl GdbServerStatus {
|
|||
struct GdbConnection {
|
||||
sim_id: SimId,
|
||||
client: EmulatorClient,
|
||||
stream_in: BufReader<OwnedReadHalf>,
|
||||
stream_out: BufWriter<OwnedWriteHalf>,
|
||||
ack_messages: bool,
|
||||
request_buf: Vec<u8>,
|
||||
stop_reason: Option<DebugStopReason>,
|
||||
response_buf: Option<Vec<u8>>,
|
||||
memory_buf: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl GdbConnection {
|
||||
fn new(sim_id: SimId, client: EmulatorClient, stream: TcpStream) -> Self {
|
||||
let (rx, tx) = stream.into_split();
|
||||
fn new(sim_id: SimId, client: EmulatorClient) -> Self {
|
||||
Self {
|
||||
sim_id,
|
||||
client,
|
||||
stream_in: BufReader::new(rx),
|
||||
stream_out: BufWriter::new(tx),
|
||||
ack_messages: true,
|
||||
request_buf: vec![],
|
||||
stop_reason: None,
|
||||
response_buf: None,
|
||||
memory_buf: None,
|
||||
}
|
||||
}
|
||||
async fn run(mut self) -> Result<()> {
|
||||
println!("Connected for {}", self.sim_id);
|
||||
self.client.send_command(EmulatorCommand::Pause);
|
||||
async fn run(&mut self, stream: TcpStream) -> Result<()> {
|
||||
let (debug_sink, mut debug_source) = mpsc::unbounded_channel();
|
||||
let (rx, mut tx) = stream.into_split();
|
||||
let mut request_source = RequestSource::new(BufReader::new(rx));
|
||||
self.client
|
||||
.send_command(EmulatorCommand::StartDebugging(self.sim_id, debug_sink));
|
||||
loop {
|
||||
let mut req = Request::read(&mut self.stream_in, &mut self.request_buf).await?;
|
||||
let response = select! {
|
||||
maybe_event = debug_source.recv() => {
|
||||
let Some(event) = maybe_event else {
|
||||
// debugger has stopped running
|
||||
break;
|
||||
};
|
||||
self.handle_event(event)
|
||||
}
|
||||
maybe_request = request_source.recv() => {
|
||||
let req = maybe_request?;
|
||||
self.handle_request(req)?
|
||||
}
|
||||
};
|
||||
if let Some(res) = response {
|
||||
let buffer = res.finish();
|
||||
match std::str::from_utf8(&buffer) {
|
||||
Ok(text) => println!("response: {text}"),
|
||||
Err(_) => println!("response: {buffer:02x?}"),
|
||||
}
|
||||
tx.write_all(&buffer).await?;
|
||||
self.response_buf = Some(buffer);
|
||||
tx.flush().await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_event(&mut self, event: DebugEvent) -> Option<Response> {
|
||||
let res = match event {
|
||||
DebugEvent::Stopped(reason) => {
|
||||
if self.stop_reason.is_some_and(|r| r == reason) {
|
||||
return None;
|
||||
}
|
||||
self.stop_reason = Some(reason);
|
||||
self.response()
|
||||
.write_str(debug_stop_reason_string(self.stop_reason))
|
||||
}
|
||||
};
|
||||
Some(res)
|
||||
}
|
||||
|
||||
fn handle_request(&mut self, mut req: Request<'_>) -> Result<Option<Response>> {
|
||||
println!("received {:02x?}", req);
|
||||
|
||||
if req.kind == RequestKind::Signal {
|
||||
self.client.send_command(EmulatorCommand::Pause);
|
||||
let res = self
|
||||
.response()
|
||||
.write_str("T05;thread:p1.t1;threads:p1.t1;reason:trap;");
|
||||
self.send(res).await?;
|
||||
continue;
|
||||
self.client
|
||||
.send_command(EmulatorCommand::DebugInterrupt(self.sim_id));
|
||||
return Ok(None); // we'll send a message when the emulator reports it has stopped
|
||||
}
|
||||
|
||||
if req.match_str("QStartNoAckMode") {
|
||||
let res = if req.match_str("QStartNoAckMode") {
|
||||
let res = self.response().write_str("OK");
|
||||
self.send(res).await?;
|
||||
self.ack_messages = false;
|
||||
res
|
||||
} else if req.match_str("qSupported:") {
|
||||
let res = self
|
||||
.response()
|
||||
.write_str("multiprocess+;swbreak+;vContSupported+;PacketSize=10000");
|
||||
self.send(res).await?;
|
||||
} else if req.match_str("QThreadSuffixSupported")
|
||||
|| req.match_str("QListThreadsInStopReply")
|
||||
|| req.match_str("QEnableErrorStrings")
|
||||
self.response()
|
||||
.write_str("multiprocess+;swbreak+;vContSupported+;PacketSize=10000")
|
||||
} else if req
|
||||
.match_some_str([
|
||||
"QThreadSuffixSupported",
|
||||
"QListThreadsInStopReply",
|
||||
"QEnableErrorStrings",
|
||||
])
|
||||
.is_some()
|
||||
{
|
||||
let res = self.response().write_str("OK");
|
||||
self.send(res).await?;
|
||||
self.response().write_str("OK")
|
||||
} else if req.match_str("qHostInfo") {
|
||||
let res = self.response().write_str(&format!(
|
||||
self.response().write_str(&format!(
|
||||
"triple:{};endian:little;ptrsize:4;",
|
||||
hex::encode("v810-unknown-vb")
|
||||
));
|
||||
self.send(res).await?;
|
||||
))
|
||||
} else if req.match_str("qProcessInfo") {
|
||||
let res = self.response().write_str(&format!(
|
||||
self.response().write_str(&format!(
|
||||
"pid:1;triple:{};endian:little;ptrsize:4;",
|
||||
hex::encode("v810-unknown-vb")
|
||||
));
|
||||
self.send(res).await?;
|
||||
))
|
||||
} else if req.match_str("qRegisterInfo") {
|
||||
let mut get_reg_info = || {
|
||||
let register = req.match_hex::<usize>()?;
|
||||
REGISTERS.get(register)
|
||||
};
|
||||
let Some(reg_info) = get_reg_info() else {
|
||||
self.send_empty().await?;
|
||||
continue;
|
||||
};
|
||||
let res = self.response().write_str(®_info.to_description());
|
||||
self.send(res).await?;
|
||||
if let Some(reg_info) = get_reg_info() {
|
||||
self.response().write_str(®_info.to_description())
|
||||
} else {
|
||||
self.response()
|
||||
}
|
||||
} else if req.match_str("vCont?") {
|
||||
let res = self.response().write_str("vCont;c;");
|
||||
self.send(res).await?;
|
||||
self.response().write_str("vCont;c;")
|
||||
} else if req.match_str("qC") {
|
||||
// The v810 has no threads, so report that the "current thread" is 1.
|
||||
let res = self.response().write_str("QCp1.t1");
|
||||
self.send(res).await?;
|
||||
self.response().write_str("QCp1.t1")
|
||||
} else if req.match_str("qfThreadInfo") {
|
||||
let res = self.response().write_str("mp1.t1");
|
||||
self.send(res).await?;
|
||||
self.response().write_str("mp1.t1")
|
||||
} else if req.match_str("qsThreadInfo") {
|
||||
let res = self.response().write_str("l");
|
||||
self.send(res).await?;
|
||||
self.response().write_str("l")
|
||||
} else if req.match_str("k") {
|
||||
bail!("debug process was killed");
|
||||
} else if req.match_str("?") {
|
||||
let res = self.response().write_str("T00;thread:p1.t1;threads:p1.t1;");
|
||||
self.send(res).await?;
|
||||
} else if req.match_str("c") || req.match_str("vCont;c:") {
|
||||
self.client.send_command(EmulatorCommand::Resume);
|
||||
self.response()
|
||||
.write_str(debug_stop_reason_string(self.stop_reason))
|
||||
} else if req.match_some_str(["c", "vCont;c:"]).is_some() {
|
||||
self.client
|
||||
.send_command(EmulatorCommand::DebugContinue(self.sim_id));
|
||||
self.stop_reason = None;
|
||||
// Don't send a response until we hit a breakpoint or get interrupted
|
||||
return Ok(None);
|
||||
} else if req.match_str("p") {
|
||||
let mut read_register = || {
|
||||
let register_index = req.match_hex::<usize>()?;
|
||||
let register = REGISTERS.get(register_index)?.to_vb_register();
|
||||
let (tx, rx) = ::oneshot::channel();
|
||||
self.client.send_command(EmulatorCommand::ReadRegister(
|
||||
self.sim_id,
|
||||
register,
|
||||
tx,
|
||||
));
|
||||
self.client
|
||||
.send_command(EmulatorCommand::ReadRegister(self.sim_id, register, tx));
|
||||
rx.recv().ok()
|
||||
};
|
||||
let Some(value) = read_register() else {
|
||||
self.send_empty().await?;
|
||||
continue;
|
||||
};
|
||||
let res = self.response().write_hex(value);
|
||||
self.send(res).await?;
|
||||
} else if req.match_str("m") {
|
||||
if let Some(value) = read_register() {
|
||||
self.response().write_hex(value)
|
||||
} else {
|
||||
self.response()
|
||||
}
|
||||
} else if let Some(op) = req.match_some_str(["m", "x"]) {
|
||||
let mut read_memory = || {
|
||||
let start = req.match_hex::<u32>()?;
|
||||
if !req.match_str(",") {
|
||||
|
@ -263,58 +289,29 @@ impl GdbConnection {
|
|||
));
|
||||
rx.recv().ok()
|
||||
};
|
||||
let Some(memory) = read_memory() else {
|
||||
self.send_empty().await?;
|
||||
continue;
|
||||
};
|
||||
if let Some(memory) = read_memory() {
|
||||
let mut res = self.response();
|
||||
if op == "m" {
|
||||
// send the hex-encoded byte stream
|
||||
for byte in &memory {
|
||||
res = res.write_hex(*byte);
|
||||
}
|
||||
self.memory_buf = Some(memory);
|
||||
self.send(res).await?;
|
||||
} else if req.match_str("x") {
|
||||
let mut read_memory = || {
|
||||
let start = req.match_hex::<u32>()?;
|
||||
if !req.match_str(",") {
|
||||
return None;
|
||||
};
|
||||
let size = req.match_hex::<u32>()?;
|
||||
let mut buf = self.memory_buf.take().unwrap_or_default();
|
||||
buf.clear();
|
||||
let (tx, rx) = ::oneshot::channel();
|
||||
self.client.send_command(EmulatorCommand::ReadMemory(
|
||||
self.sim_id,
|
||||
start..(start + size),
|
||||
buf,
|
||||
tx,
|
||||
));
|
||||
rx.recv().ok()
|
||||
};
|
||||
let Some(memory) = read_memory() else {
|
||||
self.send_empty().await?;
|
||||
continue;
|
||||
};
|
||||
let mut res = self.response();
|
||||
if memory.is_empty() {
|
||||
res = res.write_str("OK");
|
||||
} else {
|
||||
// send the raw byte stream
|
||||
for byte in &memory {
|
||||
res = res.write_byte(*byte);
|
||||
}
|
||||
}
|
||||
self.memory_buf = Some(memory);
|
||||
self.send(res).await?;
|
||||
res
|
||||
} else {
|
||||
self.response()
|
||||
}
|
||||
} else {
|
||||
// unrecognized command
|
||||
self.send_empty().await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_empty(&mut self) -> std::io::Result<()> {
|
||||
let res = self.response();
|
||||
self.send(res).await
|
||||
self.response()
|
||||
};
|
||||
Ok(Some(res))
|
||||
}
|
||||
|
||||
fn response(&mut self) -> Response {
|
||||
|
@ -323,15 +320,18 @@ impl GdbConnection {
|
|||
self.ack_messages,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async fn send(&mut self, res: Response) -> std::io::Result<()> {
|
||||
let buffer = res.finish();
|
||||
match std::str::from_utf8(&buffer) {
|
||||
Ok(text) => println!("response: {text}"),
|
||||
Err(_) => println!("response: {buffer:02x?}"),
|
||||
}
|
||||
self.stream_out.write_all(&buffer).await?;
|
||||
self.response_buf = Some(buffer);
|
||||
self.stream_out.flush().await
|
||||
impl Drop for GdbConnection {
|
||||
fn drop(&mut self) {
|
||||
self.client
|
||||
.send_command(EmulatorCommand::StopDebugging(self.sim_id));
|
||||
}
|
||||
}
|
||||
|
||||
fn debug_stop_reason_string(reason: Option<DebugStopReason>) -> &'static str {
|
||||
match reason {
|
||||
Some(DebugStopReason::Trapped) => "T05;thread:p1.t1;threads:p1.t1;reason:trap;",
|
||||
None => "T00;thread:p1.t1;threads:p1.t1;",
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,66 +35,7 @@ impl std::fmt::Debug for Request<'_> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a> Request<'a> {
|
||||
pub async fn read<R: AsyncRead + Unpin>(
|
||||
reader: &mut R,
|
||||
buffer: &'a mut Vec<u8>,
|
||||
) -> Result<Self> {
|
||||
buffer.clear();
|
||||
|
||||
let mut char = reader.read_u8().await?;
|
||||
while char == b'+' {
|
||||
// just ignore positive acks
|
||||
char = reader.read_u8().await?;
|
||||
}
|
||||
if char == b'-' {
|
||||
bail!("no support for negative acks");
|
||||
}
|
||||
if char == 0x03 {
|
||||
// This is how the client "cancels an in-flight request"
|
||||
buffer.push(char);
|
||||
return Ok(Self {
|
||||
kind: RequestKind::Signal,
|
||||
buffer,
|
||||
});
|
||||
}
|
||||
if char != b'$' {
|
||||
// Messages are supposed to start with a dollar sign
|
||||
bail!("malformed message");
|
||||
}
|
||||
|
||||
// now read the body
|
||||
let mut checksum = 0u8;
|
||||
char = reader.read_u8().await?;
|
||||
while char != b'#' {
|
||||
if char == b'}' {
|
||||
// escape character
|
||||
checksum = checksum.wrapping_add(char);
|
||||
char = reader.read_u8().await?;
|
||||
checksum = checksum.wrapping_add(char);
|
||||
buffer.push(char ^ 0x20);
|
||||
} else {
|
||||
checksum = checksum.wrapping_add(char);
|
||||
buffer.push(char);
|
||||
}
|
||||
char = reader.read_u8().await?;
|
||||
}
|
||||
|
||||
let mut checksum_bytes = [b'0'; 2];
|
||||
reader.read_exact(&mut checksum_bytes).await?;
|
||||
let (real_checksum, 2) = u8::from_radix_16(&checksum_bytes) else {
|
||||
bail!("invalid checksum");
|
||||
};
|
||||
if checksum != real_checksum {
|
||||
bail!("mismatched checksum");
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
kind: RequestKind::Command,
|
||||
buffer,
|
||||
})
|
||||
}
|
||||
|
||||
impl Request<'_> {
|
||||
pub fn match_str(&mut self, prefix: &str) -> bool {
|
||||
if let Some(new_buffer) = self.buffer.strip_prefix(prefix.as_bytes()) {
|
||||
self.buffer = new_buffer;
|
||||
|
@ -103,6 +44,13 @@ impl<'a> Request<'a> {
|
|||
false
|
||||
}
|
||||
|
||||
pub fn match_some_str<'a, I: IntoIterator<Item = &'a str>>(
|
||||
&mut self,
|
||||
prefixes: I,
|
||||
) -> Option<&'a str> {
|
||||
prefixes.into_iter().find(|&prefix| self.match_str(prefix))
|
||||
}
|
||||
|
||||
pub fn match_hex<I: FromRadix16>(&mut self) -> Option<I> {
|
||||
match I::from_radix_16(self.buffer) {
|
||||
(_, 0) => None,
|
||||
|
@ -113,3 +61,119 @@ impl<'a> Request<'a> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RequestSource<R> {
|
||||
reader: R,
|
||||
buffer: Vec<u8>,
|
||||
state: RequestReadState,
|
||||
}
|
||||
|
||||
impl<R: AsyncRead + Unpin> RequestSource<R> {
|
||||
pub fn new(reader: R) -> Self {
|
||||
Self {
|
||||
reader,
|
||||
buffer: vec![],
|
||||
state: RequestReadState::Header,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn recv(&mut self) -> Result<Request<'_>> {
|
||||
let mut char = self.reader.read_u8().await?;
|
||||
if matches!(self.state, RequestReadState::Start) {
|
||||
self.buffer.clear();
|
||||
self.state = RequestReadState::Header;
|
||||
}
|
||||
if matches!(self.state, RequestReadState::Header) {
|
||||
// Just ignore positive acks
|
||||
while char == b'+' {
|
||||
char = self.reader.read_u8().await?;
|
||||
}
|
||||
if char == b'-' {
|
||||
bail!("no support for negative acks");
|
||||
}
|
||||
if char == 0x03 {
|
||||
// This is how the client "cancels an in-flight request"
|
||||
self.buffer.push(char);
|
||||
self.state = RequestReadState::Start;
|
||||
return Ok(Request {
|
||||
kind: RequestKind::Signal,
|
||||
buffer: &self.buffer,
|
||||
});
|
||||
}
|
||||
if char != b'$' {
|
||||
// Messages are supposed to start with a dollar sign
|
||||
bail!("malformed message");
|
||||
}
|
||||
self.state = RequestReadState::Body {
|
||||
checksum: 0,
|
||||
escaping: false,
|
||||
};
|
||||
char = self.reader.read_u8().await?;
|
||||
}
|
||||
while let RequestReadState::Body { checksum, escaping } = &mut self.state {
|
||||
if char == b'#' && !*escaping {
|
||||
self.state = RequestReadState::Checksum {
|
||||
expected: *checksum,
|
||||
actual: 0,
|
||||
digits: 0,
|
||||
};
|
||||
char = self.reader.read_u8().await?;
|
||||
break;
|
||||
}
|
||||
*checksum = checksum.wrapping_add(char);
|
||||
|
||||
if *escaping {
|
||||
// escaped character
|
||||
self.buffer.push(char ^ 0x20);
|
||||
*escaping = false;
|
||||
} else if char == b'}' {
|
||||
// next character will be escaped
|
||||
*escaping = true;
|
||||
} else {
|
||||
self.buffer.push(char);
|
||||
}
|
||||
char = self.reader.read_u8().await?;
|
||||
}
|
||||
while let RequestReadState::Checksum {
|
||||
expected,
|
||||
actual,
|
||||
digits,
|
||||
} = &mut self.state
|
||||
{
|
||||
let digit = match char {
|
||||
b'0'..=b'9' => char - b'0',
|
||||
b'a'..=b'f' => char - b'a' + 10,
|
||||
b'A'..=b'F' => char - b'A' + 10,
|
||||
_ => bail!("invalid checksum"),
|
||||
};
|
||||
*actual = (*actual << 4) + digit;
|
||||
*digits += 1;
|
||||
if *digits == 2 {
|
||||
if *expected != *actual {
|
||||
bail!("mismatched checksum");
|
||||
}
|
||||
self.state = RequestReadState::Start;
|
||||
return Ok(Request {
|
||||
kind: RequestKind::Command,
|
||||
buffer: &self.buffer,
|
||||
});
|
||||
}
|
||||
char = self.reader.read_u8().await?;
|
||||
}
|
||||
unreachable!();
|
||||
}
|
||||
}
|
||||
|
||||
enum RequestReadState {
|
||||
Start,
|
||||
Header,
|
||||
Body {
|
||||
checksum: u8,
|
||||
escaping: bool,
|
||||
},
|
||||
Checksum {
|
||||
expected: u8,
|
||||
actual: u8,
|
||||
digits: u8,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -83,20 +83,18 @@ impl GameWindow {
|
|||
});
|
||||
ui.menu_button("Emulation", |ui| {
|
||||
let state = self.client.emulator_state();
|
||||
let can_interact = self.client.sim_state(self.sim_id) == SimState::Ready;
|
||||
let is_ready = self.client.sim_state(self.sim_id) == SimState::Ready;
|
||||
let can_pause = is_ready && state == EmulatorState::Running;
|
||||
if state == EmulatorState::Running {
|
||||
if ui.add_enabled(can_interact, Button::new("Pause")).clicked() {
|
||||
if ui.add_enabled(is_ready, Button::new("Pause")).clicked() {
|
||||
self.client.send_command(EmulatorCommand::Pause);
|
||||
ui.close_menu();
|
||||
}
|
||||
} else if ui
|
||||
.add_enabled(can_interact, Button::new("Resume"))
|
||||
.clicked()
|
||||
{
|
||||
} else if ui.add_enabled(can_pause, Button::new("Resume")).clicked() {
|
||||
self.client.send_command(EmulatorCommand::Resume);
|
||||
ui.close_menu();
|
||||
}
|
||||
if ui.add_enabled(can_interact, Button::new("Reset")).clicked() {
|
||||
if ui.add_enabled(is_ready, Button::new("Reset")).clicked() {
|
||||
self.client
|
||||
.send_command(EmulatorCommand::Reset(self.sim_id));
|
||||
ui.close_menu();
|
||||
|
|
Loading…
Reference in New Issue