Create tcp server

This commit is contained in:
Simon Gellis 2024-12-30 21:55:30 -05:00
parent 1a54c2e6fc
commit 47a05968fb
9 changed files with 355 additions and 1 deletions

94
Cargo.lock generated
View File

@ -28,6 +28,15 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "addr2line"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
dependencies = [
"gimli",
]
[[package]] [[package]]
name = "adler2" name = "adler2"
version = "2.0.0" version = "2.0.0"
@ -400,6 +409,21 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "backtrace"
version = "0.3.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
dependencies = [
"addr2line",
"cfg-if",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "bindgen" name = "bindgen"
version = "0.70.1" version = "0.70.1"
@ -1306,6 +1330,12 @@ dependencies = [
"windows 0.58.0", "windows 0.58.0",
] ]
[[package]]
name = "gimli"
version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]] [[package]]
name = "gl_generator" name = "gl_generator"
version = "0.14.0" version = "0.14.0"
@ -1736,6 +1766,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"thread-priority", "thread-priority",
"tokio",
"wgpu", "wgpu",
"windows 0.58.0", "windows 0.58.0",
"winit", "winit",
@ -1902,6 +1933,17 @@ dependencies = [
"simd-adler32", "simd-adler32",
] ]
[[package]]
name = "mio"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
dependencies = [
"libc",
"wasi",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "naga" name = "naga"
version = "23.1.0" version = "23.1.0"
@ -2276,6 +2318,15 @@ dependencies = [
"objc2-foundation", "objc2-foundation",
] ]
[[package]]
name = "object"
version = "0.36.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "oboe" name = "oboe"
version = "0.6.1" version = "0.6.1"
@ -2696,6 +2747,12 @@ dependencies = [
"realfft", "realfft",
] ]
[[package]]
name = "rustc-demangle"
version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]] [[package]]
name = "rustc-hash" name = "rustc-hash"
version = "1.1.0" version = "1.1.0"
@ -2918,6 +2975,16 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "socket2"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8"
dependencies = [
"libc",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "spirv" name = "spirv"
version = "0.3.0+sdk-1.3.268.0" version = "0.3.0+sdk-1.3.268.0"
@ -3070,6 +3137,33 @@ dependencies = [
"zerovec", "zerovec",
] ]
[[package]]
name = "tokio"
version = "1.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551"
dependencies = [
"backtrace",
"bytes",
"libc",
"mio",
"pin-project-lite",
"socket2",
"tokio-macros",
"windows-sys 0.52.0",
]
[[package]]
name = "tokio-macros"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.8.19" version = "0.8.19"

View File

@ -32,6 +32,7 @@ rubato = "0.16"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
thread-priority = "1" thread-priority = "1"
tokio = { version = "1", features = ["io-util", "macros", "net", "rt", "sync"] }
wgpu = "23" wgpu = "23"
winit = { version = "0.30", features = ["serde"] } winit = { version = "0.30", features = ["serde"] }

View File

@ -18,7 +18,7 @@ use crate::{
emulator::{EmulatorClient, EmulatorCommand, SimId}, emulator::{EmulatorClient, EmulatorCommand, SimId},
input::MappingProvider, input::MappingProvider,
persistence::Persistence, persistence::Persistence,
window::{AboutWindow, AppWindow, GameWindow, InputWindow}, window::{AboutWindow, AppWindow, GameWindow, GdbServerWindow, InputWindow},
}; };
fn load_icon() -> anyhow::Result<IconData> { fn load_icon() -> anyhow::Result<IconData> {
@ -183,6 +183,10 @@ impl ApplicationHandler<UserEvent> for Application {
let about = AboutWindow; let about = AboutWindow;
self.open(event_loop, Box::new(about)); self.open(event_loop, Box::new(about));
} }
UserEvent::OpenDebugger(sim_id) => {
let debugger = GdbServerWindow::new(sim_id, self.client.clone());
self.open(event_loop, Box::new(debugger));
}
UserEvent::OpenInput => { UserEvent::OpenInput => {
let input = InputWindow::new(self.mappings.clone()); let input = InputWindow::new(self.mappings.clone());
self.open(event_loop, Box::new(input)); self.open(event_loop, Box::new(input));
@ -374,6 +378,7 @@ impl Drop for Viewport {
pub enum UserEvent { pub enum UserEvent {
GamepadEvent(gilrs::Event), GamepadEvent(gilrs::Event),
OpenAbout, OpenAbout,
OpenDebugger(SimId),
OpenInput, OpenInput,
OpenPlayer2, OpenPlayer2,
} }

View File

@ -1,5 +1,6 @@
use std::{ use std::{
collections::HashMap, collections::HashMap,
fmt::Display,
fs::{self, File}, fs::{self, File},
io::{Read, Seek, SeekFrom, Write}, io::{Read, Seek, SeekFrom, Write},
path::{Path, PathBuf}, path::{Path, PathBuf},
@ -45,6 +46,14 @@ impl SimId {
} }
} }
} }
impl Display for SimId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Player1 => "Player 1",
Self::Player2 => "Player 2",
})
}
}
struct Cart { struct Cart {
rom_path: PathBuf, rom_path: PathBuf,

154
src/gdbserver.rs Normal file
View File

@ -0,0 +1,154 @@
use std::{
error::Error,
sync::{Arc, Mutex},
thread,
};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt as _, BufReader, BufWriter},
net::{
tcp::{OwnedReadHalf, OwnedWriteHalf},
TcpListener, TcpStream,
},
select,
sync::oneshot,
};
use crate::emulator::{EmulatorClient, EmulatorCommand, SimId};
pub struct GdbServer {
sim_id: SimId,
client: EmulatorClient,
status: Arc<Mutex<GdbServerStatus>>,
killer: Option<oneshot::Sender<()>>,
}
impl GdbServer {
pub fn new(sim_id: SimId, client: EmulatorClient) -> Self {
Self {
sim_id,
client,
status: Arc::new(Mutex::new(GdbServerStatus::Stopped)),
killer: None,
}
}
pub fn status(&self) -> GdbServerStatus {
self.status.lock().unwrap().clone()
}
pub fn start(&mut self, port: u16) {
let sim_id = self.sim_id;
let client = self.client.clone();
let status = self.status.clone();
let (tx, rx) = oneshot::channel();
self.killer = Some(tx);
thread::spawn(move || {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(async move {
select! {
_ = run_server(sim_id, client, port, &status) => {}
_ = rx => {
*status.lock().unwrap() = GdbServerStatus::Stopped;
}
}
})
});
}
pub fn stop(&mut self) {
if let Some(killer) = self.killer.take() {
let _ = killer.send(());
}
}
}
async fn run_server(
sim_id: SimId,
client: EmulatorClient,
port: u16,
status: &Mutex<GdbServerStatus>,
) {
client.send_command(EmulatorCommand::Pause);
let Some(stream) = try_connect(port, status).await else {
return;
};
let connection = GdbConnection::new(sim_id, client, stream);
match connection.run().await {
Ok(()) => {
*status.lock().unwrap() = GdbServerStatus::Stopped;
}
Err(error) => {
*status.lock().unwrap() = GdbServerStatus::Error(error.to_string());
}
}
}
async fn try_connect(port: u16, status: &Mutex<GdbServerStatus>) -> Option<TcpStream> {
*status.lock().unwrap() = GdbServerStatus::Connecting;
let listener = match TcpListener::bind(("127.0.0.1", port)).await {
Ok(l) => l,
Err(err) => {
*status.lock().unwrap() = GdbServerStatus::Error(err.to_string());
return None;
}
};
match listener.accept().await {
Ok((stream, _)) => {
*status.lock().unwrap() = GdbServerStatus::Running;
Some(stream)
}
Err(err) => {
*status.lock().unwrap() = GdbServerStatus::Error(err.to_string());
None
}
}
}
#[derive(Clone)]
pub enum GdbServerStatus {
Stopped,
Connecting,
Running,
Error(String),
}
impl GdbServerStatus {
pub fn running(&self) -> bool {
matches!(self, Self::Connecting | Self::Running)
}
}
struct GdbConnection {
sim_id: SimId,
client: EmulatorClient,
stream_in: BufReader<OwnedReadHalf>,
stream_out: BufWriter<OwnedWriteHalf>,
}
impl GdbConnection {
fn new(sim_id: SimId, client: EmulatorClient, stream: TcpStream) -> Self {
let (rx, tx) = stream.into_split();
Self {
sim_id,
client,
stream_in: BufReader::new(rx),
stream_out: BufWriter::new(tx),
}
}
async fn run(mut self) -> Result<(), Box<dyn Error>> {
println!("Connected for {}", self.sim_id);
self.client.send_command(EmulatorCommand::Resume);
loop {
let byte = self.read_byte().await?;
println!("{byte}");
self.stream_out.write_u8(byte).await?;
}
}
async fn read_byte(&mut self) -> std::io::Result<u8> {
self.stream_in.read_u8().await
}
}

View File

@ -14,6 +14,7 @@ mod app;
mod audio; mod audio;
mod controller; mod controller;
mod emulator; mod emulator;
mod gdbserver;
mod graphics; mod graphics;
mod input; mod input;
mod persistence; mod persistence;

View File

@ -1,12 +1,14 @@
pub use about::AboutWindow; pub use about::AboutWindow;
use egui::{Context, ViewportBuilder, ViewportId}; use egui::{Context, ViewportBuilder, ViewportId};
pub use game::GameWindow; pub use game::GameWindow;
pub use gdb::GdbServerWindow;
pub use input::InputWindow; pub use input::InputWindow;
use winit::event::KeyEvent; use winit::event::KeyEvent;
mod about; mod about;
mod game; mod game;
mod game_screen; mod game_screen;
mod gdb;
mod input; mod input;
pub trait AppWindow { pub trait AppWindow {

View File

@ -121,6 +121,14 @@ impl GameWindow {
} }
} }
}); });
ui.menu_button("Tools", |ui| {
if ui.button("GDB Server").clicked() {
self.proxy
.send_event(UserEvent::OpenDebugger(self.sim_id))
.unwrap();
ui.close_menu();
}
});
ui.menu_button("About", |ui| { ui.menu_button("About", |ui| {
self.proxy.send_event(UserEvent::OpenAbout).unwrap(); self.proxy.send_event(UserEvent::OpenAbout).unwrap();
ui.close_menu(); ui.close_menu();

80
src/window/gdb.rs Normal file
View File

@ -0,0 +1,80 @@
use egui::{Button, CentralPanel, TextEdit, ViewportBuilder, ViewportId};
use crate::{
emulator::{EmulatorClient, SimId},
gdbserver::{GdbServer, GdbServerStatus},
};
use super::AppWindow;
pub struct GdbServerWindow {
sim_id: SimId,
port_str: String,
server: GdbServer,
}
impl GdbServerWindow {
pub fn new(sim_id: SimId, client: EmulatorClient) -> Self {
Self {
sim_id,
port_str: (8080 + sim_id.to_index()).to_string(),
server: GdbServer::new(sim_id, client),
}
}
}
impl AppWindow for GdbServerWindow {
fn viewport_id(&self) -> ViewportId {
ViewportId::from_hash_of(format!("Debugger-{}", self.sim_id))
}
fn initial_viewport(&self) -> ViewportBuilder {
ViewportBuilder::default()
.with_title(format!("GDB Server ({})", self.sim_id))
.with_inner_size((300.0, 200.0))
}
fn show(&mut self, ctx: &egui::Context) {
let port_num: Option<u16> = self.port_str.parse().ok();
let status = self.server.status();
CentralPanel::default().show(ctx, |ui| {
ui.horizontal(|ui| {
if port_num.is_none() {
let style = ui.style_mut();
let error = style.visuals.error_fg_color;
style.visuals.widgets.active.bg_stroke.color = error;
style.visuals.widgets.hovered.bg_stroke.color = error;
}
ui.label("Port");
let port_editor = TextEdit::singleline(&mut self.port_str).desired_width(100.0);
ui.add_enabled(!status.running(), port_editor);
});
if !status.running() {
let start_button = Button::new("Start");
if ui.add_enabled(port_num.is_some(), start_button).clicked() {
let port = port_num.unwrap();
self.server.start(port);
}
} else {
let stop_button = Button::new("Stop");
if ui.add(stop_button).clicked() {
self.server.stop();
}
}
match &status {
GdbServerStatus::Stopped => {}
GdbServerStatus::Connecting => {
ui.label("Connecting...");
}
GdbServerStatus::Running => {
ui.label("Running");
}
GdbServerStatus::Error(message) => {
ui.label(message);
}
}
});
}
}