use std::path::Path;
use std::process::ExitCode;
use std::sync::Arc;
use std::time::{Duration, Instant};
use rsemu::core::clock::GlobalTime;
use rsemu::host::chardev::{CharDevice, CharPort, ports};
use rsemu::host::terminal::Terminal;
use rsemu::machine::{Machine, catalog};
const USAGE: &str = "\
rsemu — a multiplatform emulator built bottom-up on a generic framework
USAGE:
rsemu <COMMAND> [OPTIONS]
COMMANDS:
run <machine> Run a machine description
machines List machines this build can emulate
devices List registered device classes
describe <class> Show a device class: properties, defaults, buses
convert <machine> Convert a machine file between its text and JSON forms
RUN OPTIONS:
<machine> A path to a .machine file, or a name from `rsemu machines`
--cart <file> Bind the `cart` media slot (a NES cartridge)
--rom <file> Bind the `rom` media slot
--monitor <name> Bind the `rom` slot to one of rsemu's own monitor
images instead of a file: `rsmon` (the default, ours,
MIT) or `wozmon` (the 1976 Woz Monitor, public domain)
--disk <file> Bind the `disk` media slot
--media <n>=<file> Bind any media slot by name
-p <name>=<value> Override a `param` declared in the machine file
--for <duration> How much virtual time to run, as `1s`, `500ms`, `2m`
(default 1s, or forever with a console attached)
--console <name> Attach this terminal to a named character port. A
machine that opens exactly one is picked up on its own,
so `rsemu run apple1` is interactive already
--headless Do not attach a terminal, whatever the machine opened
-q, --quiet Only print the summary
OPTIONS:
-h, --help Print this help
-V, --version Print version and build configuration
";
fn main() -> ExitCode {
let args: Vec<String> = std::env::args().skip(1).collect();
let Some(first) = args.first().map(String::as_str) else {
print!("{USAGE}");
return ExitCode::from(2);
};
match first {
"-h" | "--help" | "help" => {
print!("{USAGE}");
ExitCode::SUCCESS
}
"-V" | "--version" | "version" => {
println!("{}", rsemu::build_info());
ExitCode::SUCCESS
}
"machines" => machines(),
"devices" => devices(),
"describe" => describe(args.get(1).map(String::as_str)),
"run" => run(&args[1..]),
"convert" => {
eprintln!(
"rsemu: {}",
rsemu::Error::Unimplemented("the JSON projection (ROADMAP.md §5)")
);
ExitCode::from(2)
}
other => {
eprintln!("rsemu: unknown command `{other}`\n");
eprint!("{USAGE}");
ExitCode::from(2)
}
}
}
fn machines() -> ExitCode {
let machines = catalog::machines();
if machines.is_empty() {
println!("no machines in this build; rebuild with a `machine-*` feature");
return ExitCode::SUCCESS;
}
for entry in machines {
println!("{:<12} {}", entry.name, entry.summary);
if !entry.media.is_empty() {
let slots: Vec<String> = entry
.media
.iter()
.map(|s| format!("--{s} <file>"))
.collect();
println!("{:<12} media {}", "", slots.join(", "));
}
}
ExitCode::SUCCESS
}
fn devices() -> ExitCode {
let registry = match catalog::registry() {
Ok(r) => r,
Err(e) => return fail(&e),
};
if registry.is_empty() {
println!("no device classes in this build");
return ExitCode::SUCCESS;
}
for class in registry.classes() {
println!("{:<16} {}", class.name, class.summary);
}
ExitCode::SUCCESS
}
fn describe(class: Option<&str>) -> ExitCode {
let Some(name) = class else {
eprintln!("rsemu: describe needs a class name; `rsemu devices` lists them");
return ExitCode::from(2);
};
let registry = match catalog::registry() {
Ok(r) => r,
Err(e) => return fail(&e),
};
let Some(class) = registry.get(name) else {
let e = registry
.create(name, &rsemu::core::props::Props::new())
.expect_err("a class that is not there cannot construct");
return fail(&e);
};
println!("{} (v{})", class.name, class.version);
println!(" {}", class.summary);
if class.properties.is_empty() {
println!(" no properties");
return ExitCode::SUCCESS;
}
println!(" properties:");
for p in class.properties {
println!(
" {:<10} {:<10} {:<9} {}",
p.name,
p.kind.as_str(),
if p.required { "required" } else { "optional" },
p.summary
);
}
ExitCode::SUCCESS
}
struct RunArgs {
machine: String,
media: Vec<(String, String)>,
monitor: Option<String>,
params: Vec<(String, String)>,
span: GlobalTime,
span_given: bool,
console: Option<String>,
headless: bool,
quiet: bool,
}
fn run(args: &[String]) -> ExitCode {
let parsed = match parse_run(args) {
Ok(a) => a,
Err(e) => {
eprintln!("rsemu: {e}");
return ExitCode::from(2);
}
};
let mut images: Vec<(String, Vec<u8>)> = Vec::new();
for (slot, path) in &parsed.media {
match std::fs::read(path) {
Ok(bytes) => images.push((slot.clone(), bytes)),
Err(e) => {
eprintln!("rsemu: cannot read {path}: {e}");
return ExitCode::FAILURE;
}
}
}
if !images.iter().any(|(slot, _)| slot == "rom") {
match builtin_rom(parsed.monitor.as_deref(), &parsed.machine) {
Ok(Some(image)) => images.push((String::from("rom"), image)),
Ok(None) => {}
Err(e) => {
eprintln!("rsemu: {e}");
return ExitCode::from(2);
}
}
}
let (name, source) = match load_description(&parsed.machine) {
Ok(pair) => pair,
Err(e) => {
eprintln!("rsemu: {e}");
return ExitCode::from(2);
}
};
let mut options = match catalog::build_options() {
Ok(o) => o,
Err(e) => return fail(&e),
};
for (slot, bytes) in &images {
options
.realize
.media
.insert(slot.as_str(), bytes.as_slice());
}
for (key, value) in &parsed.params {
options = options.with_param(key.clone(), value.clone());
}
let registry = match catalog::registry() {
Ok(r) => r,
Err(e) => return fail(&e),
};
let mut machine = match rsemu::machine::build(&name, &source, ®istry, &options) {
Ok(m) => m,
Err(e) => return fail(&e),
};
if !parsed.quiet {
describe_machine(&machine);
}
match console_port(&parsed) {
Err(e) => {
eprintln!("rsemu: {e}");
return ExitCode::from(2);
}
Ok(Some(port)) => return interact(&mut machine, &port, &parsed),
Ok(None) => {}
}
if let Err(e) = machine.run_for(parsed.span) {
eprintln!("rsemu: {e}");
summarise(&machine);
return ExitCode::FAILURE;
}
summarise(&machine);
ExitCode::SUCCESS
}
fn console_port(args: &RunArgs) -> Result<Option<Arc<CharPort>>, String> {
if args.headless {
return Ok(None);
}
if let Some(name) = &args.console {
return ports::get(name).map(Some).ok_or_else(|| {
format!(
"no character port named `{name}`; this machine opened {}",
list(&ports::names())
)
});
}
let names = ports::names();
match names.len() {
0 => Ok(None),
1 => Ok(ports::get(&names[0])),
_ => Err(format!(
"this machine has {} character ports ({}); pick one with --console, or --headless",
names.len(),
list(&names)
)),
}
}
fn list(names: &[String]) -> String {
if names.is_empty() {
return String::from("none");
}
names
.iter()
.map(|n| format!("`{n}`"))
.collect::<Vec<String>>()
.join(", ")
}
const SLICE: GlobalTime = GlobalTime::from_nanos(10_000_000);
const IDLE_SLICES: u32 = 200;
fn interact(machine: &mut Machine, port: &CharPort, args: &RunArgs) -> ExitCode {
let term = Terminal::open();
if !args.quiet {
if term.is_raw() {
eprintln!(" console attached — Ctrl-C to stop\n");
} else {
eprintln!(
" console attached, cooked mode — stdin could not be put in raw mode.\n \
On a terminal that means input arrives a line at a time and is\n \
echoed twice, once by the host and once by the guest.\n"
);
}
}
let deadline = args
.span_given
.then(|| machine.now().saturating_add(args.span));
let started = Instant::now();
let mut elapsed = GlobalTime::ZERO;
let mut idle = 0u32;
let status = loop {
if term.interrupted() {
break ExitCode::SUCCESS;
}
if deadline.is_some_and(|d| machine.now() >= d) {
break ExitCode::SUCCESS;
}
let mut moved = term.pump(port);
if let Err(e) = machine.run_until(machine.now().saturating_add(SLICE)) {
eprintln!("\r\nrsemu: {e}");
break ExitCode::FAILURE;
}
moved += term.pump(port);
if term.at_eof() && moved == 0 {
idle += 1;
if idle >= IDLE_SLICES {
break ExitCode::SUCCESS;
}
} else {
idle = 0;
}
elapsed = elapsed.saturating_add(SLICE);
let target = Duration::from_nanos(elapsed.as_nanos());
if let Some(wait) = target.checked_sub(started.elapsed()) {
std::thread::sleep(wait);
}
};
term.flush();
drop(term);
if !args.quiet {
println!();
summarise(machine);
}
status
}
fn load_description(what: &str) -> Result<(String, String), String> {
let path = Path::new(what);
if path.is_file() {
let text = std::fs::read_to_string(path).map_err(|e| format!("cannot read {what}: {e}"))?;
return Ok((what.to_string(), text));
}
if let Some(entry) = catalog::machine(what) {
return Ok((entry.name.to_string(), entry.source.to_string()));
}
if what.contains('/') || what.ends_with(".machine") {
return Err(format!("no such machine file: {what}"));
}
Err(format!(
"no machine named `{what}`; `rsemu machines` lists this build's catalog"
))
}
fn builtin_rom(monitor: Option<&str>, machine: &str) -> Result<Option<Vec<u8>>, String> {
#[cfg(feature = "dev-wdc")]
if monitor == Some("wozmon") {
return Ok(Some(rsemu::dev::wdc::WOZMON_IMAGE.to_vec()));
}
if let Some(name) = monitor
&& name != "rsmon"
{
return Err(format!(
"--monitor {name}: this build has `rsmon`{}",
if cfg!(feature = "dev-wdc") {
" and `wozmon`"
} else {
""
}
));
}
let stem = machine
.rsplit('/')
.next()
.unwrap_or(machine)
.strip_suffix(".machine")
.unwrap_or_else(|| machine.rsplit('/').next().unwrap_or(machine));
match stem {
#[cfg(feature = "dev-wdc")]
"beneater-6502" => Ok(Some(rsemu::dev::wdc::RSMON_IMAGE.to_vec())),
#[cfg(feature = "dev-apple1")]
"apple1" => Ok(Some(rsemu::dev::apple1::RSMON.to_vec())),
_ => Ok(None),
}
}
fn parse_run(args: &[String]) -> Result<RunArgs, String> {
let mut out = RunArgs {
machine: String::new(),
media: Vec::new(),
monitor: None,
params: Vec::new(),
span: GlobalTime::from_nanos(1_000_000_000),
span_given: false,
console: None,
headless: false,
quiet: false,
};
let mut i = 0;
while i < args.len() {
let arg = args[i].as_str();
let mut value = |name: &str| -> Result<String, String> {
i += 1;
args.get(i)
.cloned()
.ok_or_else(|| format!("{name} needs a value"))
};
match arg {
"--cart" | "--rom" | "--disk" => {
let slot = arg.trim_start_matches('-').to_string();
let path = value(arg)?;
out.media.push((slot, path));
}
"--media" => {
let spec = value(arg)?;
let (slot, path) = spec
.split_once('=')
.ok_or_else(|| format!("--media wants <name>=<file>, got `{spec}`"))?;
out.media.push((slot.to_string(), path.to_string()));
}
"-p" | "--param" => {
let spec = value(arg)?;
let (key, val) = spec
.split_once('=')
.ok_or_else(|| format!("-p wants <name>=<value>, got `{spec}`"))?;
out.params.push((key.to_string(), val.to_string()));
}
"--for" => {
let text = value(arg)?;
let d = rsemu::core::props::parse_duration(&text)
.map_err(|e| format!("--for {text}: {e}"))?;
out.span = GlobalTime::from_nanos(d.as_picos() / 1_000);
out.span_given = true;
}
"--monitor" => out.monitor = Some(value(arg)?),
"--console" => out.console = Some(value(arg)?),
"--headless" => out.headless = true,
"-q" | "--quiet" => out.quiet = true,
other if other.starts_with('-') => {
return Err(format!("unknown option `{other}`"));
}
other => {
if !out.machine.is_empty() {
return Err(format!("`{other}`: only one machine at a time"));
}
out.machine = other.to_string();
}
}
i += 1;
}
if out.machine.is_empty() {
return Err(String::from(
"run needs a machine; `rsemu machines` lists this build's catalog",
));
}
Ok(out)
}
fn describe_machine(machine: &Machine) {
println!("machine \"{}\"", machine.name());
for space in machine.spaces() {
println!(" space {:<8} {} bits", space.name(), space.space().bits());
}
for device in machine.devices() {
let clock = match device.domain() {
Some(_) => "clocked",
None => "",
};
println!(
" object {:<8} {:<16} {clock}",
device.path(),
device.class().name
);
}
}
fn summarise(machine: &Machine) {
println!("ran to {} ns of virtual time", machine.now().as_nanos());
for device in machine.devices() {
let Some(domain) = device.domain() else {
continue;
};
if let Ok(ticks) = machine.clocks().ticks(domain) {
println!(" {:<8} {ticks} ticks", device.path());
}
}
match machine.state_hash() {
Ok(hash) => println!("state hash {hash:#018x}"),
Err(e) => eprintln!("rsemu: cannot hash state: {e}"),
}
}
fn fail(e: &rsemu::Error) -> ExitCode {
eprintln!("rsemu: {e}");
ExitCode::FAILURE
}