use std::collections::BTreeMap;
use std::io::{self, Write};
use std::net::{Ipv4Addr, SocketAddr, TcpListener};
use std::path::PathBuf;
use std::process::ExitCode;
#[cfg(unix)]
use std::sync::atomic::{AtomicBool, Ordering};
use anyhow::{Context, bail};
use clap::{Parser, Subcommand};
use crate::chip::uart::Host;
use crate::emulator::{KernelSymbol, KernelThread, NO_DEADLINE, WalkThreads};
use crate::machine::{Board, Constructor};
use crate::soc::{Endian, Width};
use crate::time::{VirtualNanos, parse_virtual_time};
#[derive(Parser)]
#[command(version = crate::version::RUNTIME, about)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Svd {
#[command(subcommand)]
command: Svd,
},
Run(Box<Run>),
}
#[derive(Subcommand)]
enum Svd {
Generate {
soc_directory: PathBuf,
},
}
#[derive(clap::Args)]
#[expect(
clippy::struct_excessive_bools,
reason = "each is a switch of `rigless run`, which clap reads into a bool"
)]
struct Run {
#[arg(long)]
machine: String,
#[arg(long, value_name = "MCU")]
target: Option<String>,
#[arg(long, value_parser = parse_virtual_time)]
until: Option<VirtualNanos>,
#[arg(long, value_name = "[HOST:]PORT", value_parser = parse_debugger_address)]
gdb: Option<SocketAddr>,
#[arg(long, requires = "gdb")]
wait_for_debugger: bool,
#[arg(long)]
semihosting: bool,
#[arg(long, value_name = "[HOST:]PORT", value_parser = parse_debugger_address)]
trace: Option<SocketAddr>,
#[arg(long, value_name = "[HOST:]PORT", value_parser = parse_debugger_address)]
rtt: Option<SocketAddr>,
#[arg(long, value_name = "PATH")]
coverage: Option<PathBuf>,
#[arg(long, value_parser = parse_period, value_name = "PERIOD")]
outputs: Option<VirtualNanos>,
#[arg(long)]
quiet: bool,
#[arg(
long,
conflicts_with_all = [
"until",
"outputs",
"quiet",
"coverage",
"host",
"semihosting",
"trace",
"rtt"
]
)]
link: bool,
#[arg(long, requires = "link", value_name = "SEED", default_value_t = 0)]
seed: u64,
#[arg(long, requires = "link", conflicts_with = "seed", value_name = "PATH")]
restore: Option<PathBuf>,
#[arg(long, value_name = "WIRE=PORT")]
host: Vec<String>,
#[arg(long = "image", value_name = "PATH[@ADDRESS]")]
images: Vec<Placement>,
#[arg(long, value_name = "ADDRESS", value_parser = parse_address)]
boot: Option<u64>,
#[arg(value_name = "FIRMWARE[@ADDRESS]")]
firmware: Placement,
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct Placement {
path: PathBuf,
at: Option<u64>,
}
impl Run {
fn debugger(&self) -> anyhow::Result<Option<TcpListener>> {
let Some(address) = self.gdb else {
return Ok(None);
};
let listener = TcpListener::bind(address)
.with_context(|| format!("listening for GDB on {address}"))?;
let state = if self.wait_for_debugger {
"waiting for a debugger"
} else {
"serving GDB"
};
writeln!(io::stderr(), "{state} on {}", listener.local_addr()?)?;
Ok(Some(listener))
}
fn port(name: &str, address: Option<SocketAddr>) -> anyhow::Result<Option<TcpListener>> {
let Some(address) = address else {
return Ok(None);
};
let listener = TcpListener::bind(address)
.with_context(|| format!("listening for the {name} on {address}"))?;
writeln!(io::stderr(), "{name} on {}", listener.local_addr()?)?;
Ok(Some(listener))
}
fn images(&self) -> anyhow::Result<Vec<crate::image::Image>> {
std::iter::once(&self.firmware)
.chain(&self.images)
.map(|placement| {
crate::image::Image::read(&placement.path, placement.at).map_err(Into::into)
})
.collect()
}
}
#[must_use]
pub fn main(machines: &[(&'static str, Constructor)]) -> ExitCode {
let done = match Cli::parse().command {
Command::Svd {
command: Svd::Generate { soc_directory },
} => crate::svd::write_module(&soc_directory)
.map(|()| None)
.with_context(|| format!("generating {}", soc_directory.display())),
Command::Run(run) => run_firmware(&run, machines),
};
match done {
Ok(Some(code)) => ExitCode::from(code.to_le_bytes()[0]),
Ok(None) => ExitCode::SUCCESS,
Err(error) => {
writeln!(io::stderr(), "Error: {error:?}").ok();
ExitCode::FAILURE
}
}
}
fn attach_hosts(machine: &mut dyn Board, name: &str, ports: &[String]) -> anyhow::Result<()> {
let mut chosen: Vec<(&str, u16)> = Vec::with_capacity(ports.len());
for override_ in ports {
let (wire, port) = override_
.split_once('=')
.with_context(|| format!("{override_} is not WIRE=PORT"))?;
let port = port
.parse()
.with_context(|| format!("{port} is not a port"))?;
chosen.push((wire, port));
}
for wire in machine.hosts() {
let port = chosen
.iter()
.find(|(named, _)| *named == wire.name)
.map_or(wire.port, |(_, port)| *port);
let host = Host::listen(port)
.with_context(|| format!("listening for {} on :{port}", wire.name))?;
let port = host.port()?;
if !machine.attach_host(wire.name, Box::new(host)) {
bail!("{name} names no wire called {}", wire.name);
}
writeln!(io::stderr(), "{} on :{port}", wire.name)?;
}
Ok(())
}
fn target_index(machine: &dyn Board, named: Option<&str>) -> anyhow::Result<usize> {
let names = machine.target_names();
match (named, &names[..]) {
(Some(name), _) => machine.target(name).with_context(|| {
format!(
"{} has no MCU named {name}, only {}",
machine.name(),
names.join(", ")
)
}),
(None, [_only]) => Ok(0),
(None, _) => bail!(
"{} has the MCUs {}, so name the one to run with --target",
machine.name(),
names.join(", ")
),
}
}
fn parse_period(text: &str) -> Result<VirtualNanos, String> {
let period = parse_virtual_time(text)?;
if period.0 == 0 {
return Err(format!(
"{text} would read the outputs forever at one instant. Give a period above zero"
));
}
Ok(period)
}
fn parse_address(text: &str) -> Result<u64, String> {
match text.strip_prefix("0x") {
Some(digits) => u64::from_str_radix(digits, 16),
None => text.parse(),
}
.map_err(|error| format!("{text} isn't an address like 0x8000: {error}"))
}
impl std::str::FromStr for Placement {
type Err = std::convert::Infallible;
fn from_str(text: &str) -> Result<Self, Self::Err> {
let (path, at) = text
.rsplit_once('@')
.filter(|(path, _)| !path.is_empty())
.and_then(|(path, address)| Some((path, Some(parse_address(address).ok()?))))
.unwrap_or((text, None));
Ok(Self {
path: PathBuf::from(path),
at,
})
}
}
fn parse_debugger_address(text: &str) -> Result<SocketAddr, String> {
if let Ok(port) = text.parse::<u16>() {
return Ok(SocketAddr::from((Ipv4Addr::LOCALHOST, port)));
}
text.parse().map_err(|error| {
format!("{text} is neither a port nor an address and port like 0.0.0.0:3333 or [::1]:3333: {error}")
})
}
fn build(run: &Run, machines: &[(&'static str, Constructor)]) -> anyhow::Result<Box<dyn Board>> {
let Some((name, build)) = machines.iter().find(|(name, _)| *name == run.machine) else {
let names: Vec<&str> = machines.iter().map(|(name, _)| *name).collect();
bail!(
"no machine named {}: this build has {}",
run.machine,
names.join(", ")
);
};
let board = build().with_context(|| format!("building {name}"))?;
if board.name() != *name {
bail!(
"the machine built for {name} calls itself {}, and a run names a machine by what it calls itself",
board.name()
);
}
Ok(board)
}
fn run_firmware(
run: &Run,
machines: &[(&'static str, Constructor)],
) -> anyhow::Result<Option<u32>> {
if run.link {
let restored = run
.restore
.as_deref()
.map(crate::snapshot::Snapshot::read)
.transpose()?;
let images = run.images()?;
let board = build(run, machines)?;
let target = target_index(&*board, run.target.as_deref())?;
let debugger = run.debugger()?;
let kernels = kernels();
let mut launch = crate::link::Launch::new(&images);
launch.boot = run.boot;
launch.debugger = debugger.as_ref();
launch.wait_for_debugger = run.wait_for_debugger;
launch.seed = run.seed;
launch.restore = restored.as_ref();
launch.kernels = &kernels;
crate::link::serve(board, target, &launch, crate::link::Served::stdio())?;
return Ok(None);
}
let images = run.images()?;
let mut board = build(run, machines)?;
let target = target_index(&*board, run.target.as_deref())?;
let name = board.name();
#[cfg(unix)]
catch_interrupt().context("catching Ctrl-C")?;
attach_hosts(&mut *board, name, &run.host)?;
let trace = Run::port("trace", run.trace)?;
let rtt = Run::port("rtt", run.rtt)?;
let debugger = run.debugger()?;
let kernels = kernels();
let mut start = crate::emulator::Start::new(&images);
start.trace = trace.as_ref();
start.rtt = rtt.as_ref();
start.boot = run.boot;
start.debugger = debugger.as_ref();
start.wait_for_debugger = run.wait_for_debugger;
start.semihosting = run.semihosting;
start.kernels = &kernels;
#[cfg(unix)]
{
start.interrupt = Some(&INTERRUPT);
}
let session = crate::session::Session::new(board, target, run.until.unwrap_or(NO_DEADLINE))?;
let accesses = if run.quiet {
crate::emulator::Accesses::Counted
} else {
crate::emulator::Accesses::Printed
};
let printed =
crate::emulator::run_printed(session, &start, run.outputs, accesses, io::stdout())?;
if let Some(path) = &run.coverage {
std::fs::write(path, printed.coverage.to_string())
.with_context(|| format!("writing {}", path.display()))?;
}
Ok(printed.exit_status)
}
#[cfg(unix)]
static INTERRUPT: AtomicBool = AtomicBool::new(false);
#[cfg(unix)]
#[expect(
unsafe_code,
reason = "a signal's handler is set through the C library"
)]
fn catch_interrupt() -> io::Result<()> {
extern "C" fn interrupted(_signal: libc::c_int) {
INTERRUPT.store(true, Ordering::Relaxed);
}
let handler: extern "C" fn(libc::c_int) = interrupted;
let mut action: libc::sigaction = unsafe { std::mem::zeroed() };
action.sa_sigaction = handler as libc::sighandler_t;
action.sa_flags = libc::SA_RESETHAND | libc::SA_RESTART;
unsafe { libc::sigemptyset(&raw mut action.sa_mask) };
if unsafe { libc::sigaction(libc::SIGINT, &raw const action, std::ptr::null_mut()) } == 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
}
pub(crate) fn kernels() -> Vec<Box<dyn WalkThreads>> {
rigless_rtos::Kernel::ALL
.iter()
.map(|&kernel| -> Box<dyn WalkThreads> {
let symbols = kernel
.symbols()
.iter()
.map(|symbol| KernelSymbol {
name: symbol.name,
required: symbol.required,
})
.collect();
Box::new(Kernel { kernel, symbols })
})
.collect()
}
#[derive(Debug)]
struct Kernel {
kernel: rigless_rtos::Kernel,
symbols: Vec<KernelSymbol>,
}
impl WalkThreads for Kernel {
fn symbols(&self) -> &[KernelSymbol] {
&self.symbols
}
fn walk(
&self,
found: &[Option<u64>],
registers: &BTreeMap<&str, Vec<u8>>,
endian: Endian,
read: &mut dyn FnMut(u64, Width) -> Option<u64>,
) -> Option<Vec<KernelThread>> {
let endian = match endian {
Endian::Little => rigless_rtos::Endian::Little,
Endian::Big => rigless_rtos::Endian::Big,
};
let threads = self
.kernel
.threads(found, registers, endian, &mut |address, bytes| {
let width = [Width::W8, Width::W16, Width::W32, Width::W64]
.into_iter()
.find(|width| width.bytes() == u32::from(bytes))?;
read(address, width)
})?;
let threads = threads.into_iter().map(|thread| KernelThread {
id: thread.id,
name: (!thread.name.is_empty()).then_some(thread.name),
state: thread.state,
saved: thread.saved,
});
Some(threads.collect())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_debugger_port_alone_is_on_the_loopback_and_an_address_names_its_interface() {
assert_eq!(
parse_debugger_address("3333"),
Ok(SocketAddr::from((Ipv4Addr::LOCALHOST, 3333)))
);
assert_eq!(
parse_debugger_address("0.0.0.0:3333"),
Ok(SocketAddr::from((Ipv4Addr::UNSPECIFIED, 3333)))
);
assert_eq!(
parse_debugger_address("[::1]:3333"),
Ok(SocketAddr::from((std::net::Ipv6Addr::LOCALHOST, 3333)))
);
assert!(
parse_debugger_address("localhost:3333").is_err(),
"a name would resolve to whichever family the host prefers"
);
assert!(parse_debugger_address("70000").is_err());
}
#[test]
fn a_seed_is_taken_only_with_link() {
let run = |arguments: &[&str]| {
let command = ["rigless", "run", "--machine", "a-board"];
Cli::try_parse_from(command.iter().chain(arguments).chain(&["firmware.elf"])).map(
|cli| match cli.command {
Command::Run(run) => run.seed,
Command::Svd { .. } => unreachable!("asked to run"),
},
)
};
assert_eq!(run(&["--link", "--seed", "7"]).unwrap(), 7);
assert_eq!(run(&["--link"]).unwrap(), 0, "0 when it's left out");
assert!(
run(&["--seed", "7"]).is_err(),
"a seed keys the link's draws"
);
assert!(run(&["--link", "--seed", "-1"]).is_err());
}
#[test]
fn a_file_takes_the_address_after_its_last_at_sign_when_that_is_one() {
let placed = |text: &str| text.parse::<Placement>().unwrap();
let at = |path: &str, at| Placement {
path: PathBuf::from(path),
at,
};
assert_eq!(placed("app.bin@0x8000"), at("app.bin", Some(0x8000)));
assert_eq!(placed("app.bin@32768"), at("app.bin", Some(0x8000)));
assert_eq!(placed("app.elf"), at("app.elf", None));
assert_eq!(placed("builds@2/app.elf"), at("builds@2/app.elf", None));
assert_eq!(placed("me@host.bin@0x10"), at("me@host.bin", Some(0x10)));
assert_eq!(placed("@0x10"), at("@0x10", None));
assert!(parse_address("0x").is_err());
assert!(parse_address("0x1_0000_0000_0000_0000").is_err());
}
#[test]
fn a_period_of_zero_is_refused_since_the_run_would_never_move_on() {
assert!(parse_period("0ns").is_err());
assert!(parse_period("0s").is_err());
assert_eq!(parse_period("1ns"), Ok(VirtualNanos(1)));
}
}