rigless 0.2.0

Virtual hardware-in-the-loop runtime: run unmodified firmware on an emulated MCU with every peripheral, chip model and unit of virtual time in Rust, and a CPU core of its own that translates the firmware's instructions to host code
Documentation
// Copyright (c) 2026 Rigless Labs.
// SPDX-License-Identifier: MIT OR Apache-2.0
//! Non-stop mode, "Debugging with GDB" for GDB 18.1, sections 5.5.2, E.10 and E.11: GDB doesn't
//! wait on the core, so it can read memory while the firmware runs. A resume answers `OK` at once,
//! each stop comes as a `%Stop` notification GDB takes with `vStopped`, and between stretches the
//! server answers what doesn't need the core held. Reads while the core runs are peeks, so a live
//! view leaves the run the same run. The section numbers here are those of the manual for GDB 18.1.

use std::io;

use super::super::Core;
use super::history::Halted;
use super::packet::Connection;
use super::threads::{Reason, stop_reply};
use super::{Asked, Debugger, Held};
use crate::emulator::HaltReason;

/// What needs the core held, as the start of each packet, and so is refused while it runs in
/// non-stop mode: its registers, writes to memory, which would change the run, the monitor, and
/// going back.
/// A resume of the running core is ignored, section E.3's `vCont`, and everything else is answered.
const REFUSED_RUNNING: &[&str] = &["g", "G", "p", "P", "M", "X", "vFlash", "qRcmd,", "bs", "bc"];

impl Debugger<'_> {
    /// Whether the connected debugger asked for non-stop mode.
    pub(super) fn non_stop(&self) -> bool {
        self.negotiated().non_stop
    }

    /// Whether `packet` needs the core held while it runs in non-stop mode. A resume from an
    /// address would write the program counter of a core that runs.
    pub(super) fn refused_running(&self, packet: &str) -> bool {
        let from_an_address = match packet.as_bytes().first() {
            Some(b'c' | b's') => packet.len() > 1,
            Some(b'C' | b'S') => packet.contains(';'),
            _ => false,
        };
        self.non_stop()
            && self.core != Held::Halted
            && (from_an_address
                || REFUSED_RUNNING
                    .iter()
                    .any(|start| packet.starts_with(start)))
    }

    /// `QNonStop:1` or `QNonStop:0`, section E.5. All-stop mode holds a core that runs, section
    /// E.11, and GDB asks why with `?`.
    pub(super) fn set_non_stop(&mut self, core: &mut Core, on: bool) -> Asked {
        self.threads.only_core(on);
        if !on && self.core != Held::Halted {
            self.hold(core, Reason::Halt, Halted::By(HaltReason::Requested));
        }
        if let Some(connection) = self.connection.as_mut() {
            connection.negotiated.non_stop = on;
            connection.telling = false;
        }
        Asked::Reply("OK".to_owned())
    }

    /// Tells GDB of `stop`, a stop reply, in non-stop mode, as a notification. The core is one
    /// thread, which GDB takes as running until it's taken the stop with `vStopped`, and which a
    /// resume leaves held until then, so there's never a second stop to tell before that.
    pub(super) fn tell(&mut self, stop: &str) -> io::Result<()> {
        let connection = self.connection()?;
        connection.telling = true;
        connection.notify(&format!("Stop:{stop}"))
    }

    /// Whether GDB has still to take the stop it was told with `vStopped`.
    pub(super) fn telling(&self) -> bool {
        self.connection
            .as_ref()
            .is_some_and(|connection| connection.telling)
    }

    /// At the run's end, in non-stop mode: answers what GDB sends once it's told the run exited,
    /// up to the `vStopped` that takes the exit, which GDB reports only once that's answered. A
    /// debugger that says nothing for the read timeout `connecting` set is left, and so is one
    /// that sends four packets with no `vStopped` among them.
    pub(super) fn exit_taken(&mut self) {
        for _ in 0..4 {
            let Ok(packet) = self.connection().and_then(Connection::next_packet) else {
                return;
            };
            let answer = if packet == "vStopped" { "OK" } else { "E01" };
            if self
                .connection()
                .and_then(|connection| connection.send(answer))
                .is_err()
                || packet == "vStopped"
            {
                return;
            }
        }
    }

    /// `vStopped`, GDB taking the stop it was told: there's no other, so `OK`.
    pub(super) fn told(&mut self) -> Asked {
        if let Some(connection) = self.connection.as_mut() {
            connection.telling = false;
        }
        Asked::Reply("OK".to_owned())
    }

    /// `?` in non-stop mode, section E.11, which starts the telling over: the core's stop, which
    /// `vStopped` then follows with `OK`, or `OK` while it runs.
    pub(super) fn stops_again(&mut self) -> Asked {
        let held = self.core == Held::Halted;
        let stop = held.then(|| stop_reply(self.stopped, self.negotiated()));
        if let Some(connection) = self.connection.as_mut() {
            connection.telling = held;
        }
        Asked::Reply(stop.unwrap_or_else(|| "OK".to_owned()))
    }

    /// `vCont`'s `t` or `vCtrlC`: `OK`, and a core that runs stops for `reason` and GDB is told.
    pub(super) fn stop(&mut self, core: &mut Core, reason: Reason) -> io::Result<()> {
        self.connection()?.send("OK")?;
        if self.core == Held::Halted {
            return Ok(());
        }
        self.halt_core(core, reason)
    }
}