136 lines
5.0 KiB
Rust
136 lines
5.0 KiB
Rust
use clap::{Parser, Subcommand};
|
|
use fds_common::{Bay, Result};
|
|
use fds_workstation::emulator::Session;
|
|
use std::{path::PathBuf, process::ExitCode};
|
|
#[derive(Parser)]
|
|
#[command(
|
|
version,
|
|
about = "Boot FDS/OS and hotplug virtual USB cartridges on a Linux workstation",
|
|
after_help = "QEMU virt tests FDS software, not Raspberry Pi firmware or physical hardware. DATA uses retained temporary overlays; source images remain unchanged."
|
|
)]
|
|
struct Cli {
|
|
/// Private session directory. Choose a new directory for each boot.
|
|
#[arg(long, global = true, default_value = "out/emulator")]
|
|
session: PathBuf,
|
|
#[command(subcommand)]
|
|
command: Action,
|
|
}
|
|
#[derive(Subcommand)]
|
|
enum Action {
|
|
/// Check workstation QEMU tools without creating or starting a VM.
|
|
Doctor {
|
|
#[arg(long)]
|
|
qemu_runner: Option<PathBuf>,
|
|
},
|
|
/// Boot the supplied FDS kernel, initramfs and SYSTEM; wait for its user console.
|
|
Start {
|
|
#[arg(long, default_value = "out/kernel/boot/kernel_2712.img")]
|
|
kernel: PathBuf,
|
|
#[arg(long, default_value = "out/fds-initramfs.img")]
|
|
initramfs: PathBuf,
|
|
#[arg(long, default_value = "out/fds-system-cli.img")]
|
|
system: PathBuf,
|
|
/// Optional wrapper accepting qemu-system-aarch64 or qemu-img plus arguments.
|
|
#[arg(long)]
|
|
qemu_runner: Option<PathBuf>,
|
|
/// Guest RAM in MiB; emulation uses two CPU cores with TCG.
|
|
#[arg(long, default_value_t=1024, value_parser=clap::value_parser!(u32).range(512..=32768))]
|
|
memory_mib: u32,
|
|
#[arg(long, default_value_t=180, value_parser=clap::value_parser!(u64).range(1..=1800))]
|
|
timeout: u64,
|
|
},
|
|
/// Report actual QEMU state, devices, and retained image/overlay locations.
|
|
Status,
|
|
/// Attach the ordinary FDS console; Ctrl-] detaches without stopping QEMU.
|
|
Console,
|
|
/// Execute one ordinary-user guest command; arguments are passed literally.
|
|
Guest {
|
|
#[arg(long, default_value_t=120, value_parser=clap::value_parser!(u64).range(1..=1800))]
|
|
timeout: u64,
|
|
#[arg(required = true, trailing_var_arg = true, allow_hyphen_values = true)]
|
|
arguments: Vec<String>,
|
|
},
|
|
/// Insert a regular cartridge disk image into bay 01 through 12.
|
|
Insert { bay: Bay, image: PathBuf },
|
|
/// Ask FDS to stop users and declare SAFE, then remove the virtual USB device.
|
|
Eject { bay: Bay },
|
|
/// Simulate pulling a cartridge without guest eject; writable data may be lost.
|
|
Unplug { bay: Bay },
|
|
/// Request native FDS shutdown; --force instead cuts virtual power immediately.
|
|
Stop {
|
|
#[arg(long)]
|
|
force: bool,
|
|
},
|
|
}
|
|
fn run() -> Result<u8> {
|
|
let cli = Cli::parse();
|
|
let value = match cli.command {
|
|
Action::Doctor { qemu_runner } => {
|
|
fds_workstation::doctor::emulator(qemu_runner.as_deref())?
|
|
}
|
|
Action::Start {
|
|
kernel,
|
|
initramfs,
|
|
system,
|
|
qemu_runner,
|
|
memory_mib,
|
|
timeout,
|
|
} => Session::start(
|
|
&cli.session,
|
|
&kernel,
|
|
&initramfs,
|
|
&system,
|
|
qemu_runner.as_deref(),
|
|
memory_mib,
|
|
timeout,
|
|
)?,
|
|
action => {
|
|
let mut session = Session::load(&cli.session)?;
|
|
match action {
|
|
Action::Status => session.status()?,
|
|
Action::Console => {
|
|
session.console()?;
|
|
return Ok(0);
|
|
}
|
|
Action::Guest { timeout, arguments } => {
|
|
let result = session.guest(&arguments, timeout)?;
|
|
print!("{}", result.output);
|
|
return Ok(result.status);
|
|
}
|
|
Action::Insert { bay, image } => session.insert(bay, &image)?,
|
|
Action::Eject { bay } => session.remove(bay, false)?,
|
|
Action::Unplug { bay } => session.remove(bay, true)?,
|
|
Action::Stop { force } => session.stop(force)?,
|
|
Action::Start { .. } | Action::Doctor { .. } => unreachable!(),
|
|
}
|
|
}
|
|
};
|
|
println!("{}", serde_json::to_string_pretty(&value).unwrap());
|
|
Ok(0)
|
|
}
|
|
fn main() -> ExitCode {
|
|
match run() {
|
|
Ok(code) => ExitCode::from(code),
|
|
Err(error) => {
|
|
eprintln!("fds-emulator: {error}");
|
|
ExitCode::from(2)
|
|
}
|
|
}
|
|
}
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use clap::CommandFactory;
|
|
#[test]
|
|
fn clap_preserves_guest_options_and_bounds_hardware() {
|
|
Cli::command().debug_assert();
|
|
let value =
|
|
Cli::try_parse_from(["fds-emulator", "guest", "--", "fds", "--json", "bays"]).unwrap();
|
|
assert!(
|
|
matches!(value.command,Action::Guest { arguments,.. } if arguments == ["fds","--json","bays"])
|
|
);
|
|
assert!(Cli::try_parse_from(["fds-emulator", "insert", "13", "file.img"]).is_err());
|
|
assert!(Cli::try_parse_from(["fds-emulator", "start", "--memory-mib", "0"]).is_err());
|
|
}
|
|
}
|