Files
fds-os/rust/fds-workstation/src/emulator-main.rs
T
2026-09-21 22:29:23 +08:00

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