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, }, /// 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, /// 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, }, /// 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 { 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()); } }