diff --git a/gui/core.h b/gui/core.h index f90f4ab7..4dab831d 100644 --- a/gui/core.h +++ b/gui/core.h @@ -265,6 +265,8 @@ struct RustError *vmm_draw(struct Vmm *vmm); struct DebugResult vmm_dispatch_debug(struct Vmm *vmm, struct KernelStop *stop); +ptrdiff_t vmm_debug_socket(struct Vmm *vmm); + void vmm_shutdown(struct Vmm *vmm); bool vmm_shutting_down(struct Vmm *vmm); diff --git a/gui/main_window.cpp b/gui/main_window.cpp index ee7a4fae..a4bc501a 100644 --- a/gui/main_window.cpp +++ b/gui/main_window.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -35,6 +36,9 @@ #include #include +#ifndef _WIN32 +#include +#endif #include namespace Args { @@ -55,7 +59,7 @@ MainWindow::MainWindow( m_games(nullptr), m_launch(nullptr), m_screen(nullptr), - m_debug(nullptr) + m_debugNoti(nullptr) { setWindowTitle("Obliteration"); @@ -393,7 +397,7 @@ void MainWindow::acceptDebuggerFailed(const QString &msg) this, "Error", QString("Failed to accept a debugger connection: %1.").arg(msg)); - m_debug.free(); + m_debugServer.free(); } void MainWindow::vmmError(const QString &msg) @@ -440,7 +444,48 @@ void MainWindow::log(VmmLog type, const QString &msg) } } -void MainWindow::breakpoint(KernelStop *stop) +void MainWindow::setupDebugger() +{ + // Setup GDB session. + dispatchDebug(nullptr); + + if (vmm_shutting_down(m_vmm)) { + return; + } + + // Enable non-blocking on debug socket. On Windows QSocketNotifier will do this for us. + auto sock = vmm_debug_socket(m_vmm); + +#ifndef _WIN32 + auto flags = fcntl(sock, F_GETFL); + + if (flags < 0) { + QMessageBox::critical( + this, + "Error", + "Couldn't get file flags from debug socket."); + stopDebug(); + return; + } else if (fcntl(sock, F_SETFL, flags | O_NONBLOCK) < 0) { + QMessageBox::critical( + this, + "Error", + "Couldn't enable non-blocking mode on debug socket."); + stopDebug(); + return; + } +#endif + + // Watch for incoming data. + m_debugNoti = new QSocketNotifier(QSocketNotifier::Read, this); + m_debugNoti->setSocket(sock); + + connect(m_debugNoti, &QSocketNotifier::activated, [this] { dispatchDebug(nullptr); }); + + m_debugNoti->setEnabled(true); +} + +void MainWindow::dispatchDebug(KernelStop *stop) { // Do nothing if the previous thread already trigger the shutdown. if (vmm_shutting_down(m_vmm)) { @@ -471,23 +516,8 @@ void MainWindow::breakpoint(KernelStop *stop) break; } - if (!vmm_shutting_down(m_vmm)) { - return; - } - - // We can't free the VMM here because the thread that trigger this method are waiting - // for us to return. - if (m_args.isSet(Args::debug)) { - QMetaObject::invokeMethod( - this, - &MainWindow::close, - Qt::QueuedConnection); - } else { - QMetaObject::invokeMethod( - this, - &MainWindow::waitKernelExit, - Qt::QueuedConnection, - true); + if (vmm_shutting_down(m_vmm)) { + stopDebug(); } } @@ -556,7 +586,7 @@ void MainWindow::startDebug(const QString &addr) // Start debug server. Rust error; - m_debug = debug_server_start( + m_debugServer = debug_server_start( this, addr.toStdString().c_str(), &error, @@ -583,7 +613,7 @@ void MainWindow::startDebug(const QString &addr) } }); - if (!m_debug) { + if (!m_debugServer) { auto msg = QString("Failed to start a debug server on %1: %2") .arg(addr) .arg(error_message(error)); @@ -600,7 +630,7 @@ void MainWindow::startDebug(const QString &addr) QMessageBox::information( this, "Debug", - QString("Waiting for a debugger at %1.").arg(debug_server_addr(m_debug))); + QString("Waiting for a debugger at %1.").arg(debug_server_addr(m_debugServer))); } } @@ -610,7 +640,7 @@ void MainWindow::startVmm(Debugger *d) Rust debug(d); std::string kernel; - m_debug.free(); + m_debugServer.free(); if (QFile::exists(".obliteration-development")) { auto b = std::filesystem::current_path(); @@ -717,9 +747,30 @@ bool MainWindow::requireVmmStopped() return true; } +void MainWindow::stopDebug() +{ + // We can't free the VMM here because the thread that trigger this method are waiting + // for us to return. + if (m_args.isSet(Args::debug)) { + QMetaObject::invokeMethod( + this, + &MainWindow::close, + Qt::QueuedConnection); + } else { + QMetaObject::invokeMethod( + this, + &MainWindow::waitKernelExit, + Qt::QueuedConnection, + true); + } +} + void MainWindow::killVmm() { m_vmm.free(); + + delete m_debugNoti; + m_debugNoti = nullptr; } void MainWindow::vmmHandler(const VmmEvent *ev, void *cx) @@ -751,11 +802,18 @@ void MainWindow::vmmHandler(const VmmEvent *ev, void *cx) QString::fromUtf8(ev->log.data, ev->log.len)); break; case VmmEvent_Breakpoint: - QMetaObject::invokeMethod( - w, - &MainWindow::breakpoint, - Qt::BlockingQueuedConnection, - ev->breakpoint.stop); + if (auto stop = ev->breakpoint.stop; stop) { + QMetaObject::invokeMethod( + w, + &MainWindow::dispatchDebug, + Qt::BlockingQueuedConnection, + stop); + } else { + QMetaObject::invokeMethod( + w, + &MainWindow::setupDebugger, + Qt::BlockingQueuedConnection); + } break; } } diff --git a/gui/main_window.hpp b/gui/main_window.hpp index ef6e1436..b5bfa463 100644 --- a/gui/main_window.hpp +++ b/gui/main_window.hpp @@ -15,6 +15,7 @@ class LogsViewer; class ProfileList; class QCommandLineOption; class QCommandLineParser; +class QSocketNotifier; class QStackedWidget; class Screen; @@ -50,9 +51,11 @@ private: void vmmError(const QString &msg); void waitKernelExit(bool success); void log(VmmLog type, const QString &msg); - void breakpoint(KernelStop *stop); + void setupDebugger(); + void dispatchDebug(KernelStop *stop); bool loadGame(const QString &gameId); bool requireVmmStopped(); + void stopDebug(); void killVmm(); static void vmmHandler(const VmmEvent *ev, void *cx); @@ -64,7 +67,8 @@ private: LaunchSettings *m_launch; Screen *m_screen; QPointer m_logs; - Rust m_debug; + Rust m_debugServer; + QSocketNotifier *m_debugNoti; Rust m_vmm; // Destroy first. }; diff --git a/gui/src/debug/mod.rs b/gui/src/debug/mod.rs index 8366d3e5..8b201bda 100644 --- a/gui/src/debug/mod.rs +++ b/gui/src/debug/mod.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 use crate::error::RustError; use std::ffi::{c_char, c_void, CStr, CString}; -use std::io::{Read, Write}; +use std::io::{ErrorKind, Read, Write}; use std::net::{TcpListener, TcpStream}; use std::ptr::null_mut; use std::sync::atomic::{AtomicBool, Ordering}; @@ -65,7 +65,7 @@ pub unsafe extern "C" fn debug_server_start( next: 0, })), }, - Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + Err(e) if e.kind() == ErrorKind::WouldBlock => { sleep(Duration::from_millis(500)); continue; } @@ -138,11 +138,19 @@ pub struct Debugger { } impl Debugger { + #[cfg(unix)] + pub fn socket(&self) -> std::os::fd::RawFd { + std::os::fd::AsRawFd::as_raw_fd(&self.sock) + } + + #[cfg(windows)] + pub fn socket(&self) -> std::os::windows::io::RawSocket { + std::os::windows::io::AsRawSocket::as_raw_socket(&self.sock) + } + pub fn read(&mut self) -> Result { // Fill the buffer if needed. while self.next == self.buf.len() { - use std::io::ErrorKind; - // Clear previous data. self.buf.clear(); self.next = 0; @@ -175,15 +183,40 @@ impl gdbstub::conn::Connection for Debugger { type Error = std::io::Error; fn write(&mut self, byte: u8) -> Result<(), Self::Error> { - Write::write_all(&mut self.sock, std::slice::from_ref(&byte)) + self.write_all(std::slice::from_ref(&byte)) } - fn write_all(&mut self, buf: &[u8]) -> Result<(), Self::Error> { - Write::write_all(&mut self.sock, buf) + fn write_all(&mut self, mut buf: &[u8]) -> Result<(), Self::Error> { + while !buf.is_empty() { + let written = match Write::write(&mut self.sock, buf) { + Ok(v) => v, + Err(e) => { + if matches!(e.kind(), ErrorKind::Interrupted | ErrorKind::WouldBlock) { + continue; + } else { + return Err(e); + } + } + }; + + if written == 0 { + return Err(std::io::Error::from(ErrorKind::WriteZero)); + } + + buf = &buf[written..]; + } + + Ok(()) } fn flush(&mut self) -> Result<(), Self::Error> { - Write::flush(&mut self.sock) + while let Err(e) = Write::flush(&mut self.sock) { + if !matches!(e.kind(), ErrorKind::Interrupted | ErrorKind::WouldBlock) { + return Err(e); + } + } + + Ok(()) } fn on_session_start(&mut self) -> Result<(), Self::Error> { diff --git a/gui/src/vmm/debug/mod.rs b/gui/src/vmm/debug/mod.rs index f0bb99e5..2980cd6c 100644 --- a/gui/src/vmm/debug/mod.rs +++ b/gui/src/vmm/debug/mod.rs @@ -1,6 +1,63 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pub use self::arch::*; +use crate::debug::Debugger; +use crate::error::RustError; +use gdbstub::stub::state_machine::state::{Idle, Running}; +use gdbstub::stub::state_machine::{GdbStubStateMachine, GdbStubStateMachineInner}; +use gdbstub::stub::MultiThreadStopReason; + #[cfg_attr(target_arch = "aarch64", path = "aarch64.rs")] #[cfg_attr(target_arch = "x86_64", path = "x86_64.rs")] mod arch; + +pub fn dispatch_idle( + target: &mut Target, + mut state: GdbStubStateMachineInner<'static, Idle, Target, Debugger>, +) -> Result, RustError> { + let b = state + .borrow_conn() + .read() + .map_err(|e| RustError::with_source("couldn't read data from the debugger", e))?; + + state + .incoming_data(target, b) + .map_err(|e| RustError::with_source("couldn't process data from the debugger", e)) +} + +pub fn dispatch_running( + target: &mut Target, + mut state: GdbStubStateMachineInner<'static, Running, Target, Debugger>, + stop: Option>, +) -> Result< + Result< + GdbStubStateMachine<'static, Target, Debugger>, + GdbStubStateMachineInner<'static, Running, Target, Debugger>, + >, + RustError, +> { + // Check If we are here because of a breakpoint. + if let Some(r) = stop { + return state + .report_stop(target, r) + .map(Ok) + .map_err(|e| RustError::with_source("couldn't report stop reason to the debugger", e)); + } + + // Check for pending command. + let b = match state.borrow_conn().read() { + Ok(v) => v, + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => return Ok(Err(state)), + Err(e) => { + return Err(RustError::with_source( + "couldn't read data from the debugger", + e, + )); + } + }; + + state + .incoming_data(target, b) + .map(Ok) + .map_err(|e| RustError::with_source("couldn't process data from the debugger", e)) +} diff --git a/gui/src/vmm/mod.rs b/gui/src/vmm/mod.rs index a202a3a3..663c8363 100644 --- a/gui/src/vmm/mod.rs +++ b/gui/src/vmm/mod.rs @@ -447,16 +447,12 @@ pub unsafe extern "C" fn vmm_start( // Setup GDB stub. let gdb = match debugger - .map(move |client| -> Result { + .map(move |client| { let mut target = self::debug::Target::new(); - let state = gdbstub::stub::GdbStub::new(*client) - .run_state_machine(&mut target) - .map_err(|e| RustError::with_source("couldn't setup a GDB stub", e))?; - Ok(GdbStub { - target, - state: Some(state), - }) + gdbstub::stub::GdbStub::new(*client) + .run_state_machine(&mut target) + .map_err(|e| RustError::with_source("couldn't setup a GDB stub", e)) }) .transpose() { @@ -491,7 +487,7 @@ pub unsafe extern "C" fn vmm_start( // Create VMM. let vmm = Vmm { - cpus: vec![main], + cpus: vec![CpuController { thread: main }], screen, gdb, shutdown, @@ -515,24 +511,34 @@ pub unsafe extern "C" fn vmm_draw(vmm: *mut Vmm) -> *mut RustError { #[no_mangle] pub unsafe extern "C" fn vmm_dispatch_debug(vmm: *mut Vmm, stop: *mut KernelStop) -> DebugResult { - let vmm = &mut *vmm; - let gdb = vmm.gdb.as_mut().unwrap(); - let stop = match stop.is_null() { + // Consume stop reason now to prevent memory leak. + let mut stop = match stop.is_null() { true => None, - false => Some(Box::from_raw(stop)), + false => Some(Box::from_raw(stop).0), }; + // Setup target object. + let vmm = &mut *vmm; + let mut target = self::debug::Target::new(); + loop { // Check current state. - let mut s = match gdb.state.take().unwrap() { - GdbStubStateMachine::Idle(s) => s, + let r = match vmm.gdb.take().unwrap() { + GdbStubStateMachine::Idle(s) => self::debug::dispatch_idle(&mut target, s), GdbStubStateMachine::Running(s) => { - gdb.state = Some(s.into()); - return DebugResult::Ok; + match self::debug::dispatch_running(&mut target, s, stop.take()) { + Ok(Ok(v)) => Ok(v), + Ok(Err(v)) => { + // No pending data from the debugger. + vmm.gdb = Some(v.into()); + return DebugResult::Ok; + } + Err(e) => Err(e), + } } GdbStubStateMachine::CtrlCInterrupt(s) => { - match s.interrupt_handled(&mut gdb.target, None::>) { - Ok(v) => gdb.state = Some(v), + match s.interrupt_handled(&mut target, None::>) { + Ok(v) => vmm.gdb = Some(v), Err(e) => { let r = RustError::with_source("couldn't handle CTRL+C from a debugger", e); return DebugResult::Error { reason: r.into_c() }; @@ -543,25 +549,28 @@ pub unsafe extern "C" fn vmm_dispatch_debug(vmm: *mut Vmm, stop: *mut KernelStop GdbStubStateMachine::Disconnected(_) => return DebugResult::Disconnected, }; - // Read data from the client. - let b = match s.borrow_conn().read() { - Ok(v) => v, - Err(e) => { - let r = RustError::with_source("couldn't read data from a debugger", e); - return DebugResult::Error { reason: r.into_c() }; - } - }; - - match s.incoming_data(&mut gdb.target, b) { - Ok(v) => gdb.state = Some(v), - Err(e) => { - let r = RustError::with_source("couldn't process data from a debugger", e); - return DebugResult::Error { reason: r.into_c() }; - } + match r { + Ok(v) => vmm.gdb = Some(v), + Err(e) => return DebugResult::Error { reason: e.into_c() }, } } } +#[no_mangle] +pub unsafe extern "C" fn vmm_debug_socket(vmm: *mut Vmm) -> isize { + let s = match &mut (*vmm).gdb { + Some(v) => v, + None => return -1, + }; + + match s { + GdbStubStateMachine::Idle(s) => s.borrow_conn().socket() as _, + GdbStubStateMachine::Running(s) => s.borrow_conn().socket() as _, + GdbStubStateMachine::CtrlCInterrupt(s) => s.borrow_conn().socket() as _, + GdbStubStateMachine::Disconnected(s) => s.borrow_conn().socket() as _, + } +} + #[no_mangle] pub unsafe extern "C" fn vmm_shutdown(vmm: *mut Vmm) { (*vmm).shutdown.store(true, Ordering::Relaxed); @@ -676,9 +685,9 @@ fn get_page_size() -> Result, std::io::Error> { /// Manage a virtual machine that run the kernel. pub struct Vmm { - cpus: Vec>, + cpus: Vec, screen: self::screen::Default, - gdb: Option, + gdb: Option>, shutdown: Arc, } @@ -688,7 +697,7 @@ impl Drop for Vmm { self.shutdown.store(true, Ordering::Relaxed); for cpu in self.cpus.drain(..) { - cpu.join().unwrap(); + cpu.thread.join().unwrap(); } } } @@ -788,10 +797,9 @@ struct CpuArgs { shutdown: Arc, } -/// States for GDB stub. -struct GdbStub { - target: self::debug::Target, - state: Option>, +/// Contains objects to control a CPU from a GUI. +struct CpuController { + thread: JoinHandle<()>, } /// Represents an error when [`vmm_new()`] fails.