FDS/OS 1.0
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "fds-stage0"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
description = "Static FDS early boot discovery and root handoff"
|
||||
|
||||
[dependencies]
|
||||
clap.workspace = true
|
||||
fds-common = { path = "../fds-common" }
|
||||
serde_json = "1"
|
||||
libc = "0.2"
|
||||
@@ -0,0 +1,533 @@
|
||||
//! Linux PID-1 operations. No shell, external mount program or udev is used.
|
||||
use fds_common::{
|
||||
Error, Result,
|
||||
boot::BootOptions,
|
||||
read_text,
|
||||
sysfs::{self, BlockPartition, Selection},
|
||||
trace::{self, Point},
|
||||
};
|
||||
use std::{
|
||||
ffi::CString,
|
||||
fs,
|
||||
io::{self, Read, Write},
|
||||
os::{
|
||||
fd::{AsRawFd, FromRawFd, OwnedFd},
|
||||
unix::{
|
||||
fs::{FileTypeExt, MetadataExt, PermissionsExt},
|
||||
process::CommandExt,
|
||||
},
|
||||
},
|
||||
path::Path,
|
||||
process::{Child, Command, Stdio},
|
||||
};
|
||||
|
||||
fn c(value: &str) -> Result<CString> {
|
||||
CString::new(value).map_err(|_| Error("NUL in syscall argument".into()))
|
||||
}
|
||||
fn checked(value: libc::c_int, operation: &str) -> Result<()> {
|
||||
if value < 0 {
|
||||
Err(Error(format!(
|
||||
"{operation}: {}",
|
||||
io::Error::last_os_error()
|
||||
)))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
fn mount(source: &str, target: &str, kind: &str, flags: libc::c_ulong) -> Result<()> {
|
||||
let (source, target, kind) = (c(source)?, c(target)?, c(kind)?);
|
||||
// All strings remain alive for the syscall; no data/options pointer is passed.
|
||||
checked(
|
||||
unsafe {
|
||||
libc::mount(
|
||||
source.as_ptr(),
|
||||
target.as_ptr(),
|
||||
kind.as_ptr(),
|
||||
flags,
|
||||
std::ptr::null(),
|
||||
)
|
||||
},
|
||||
"mount",
|
||||
)
|
||||
}
|
||||
fn move_mount(source: &str, target: &str) -> Result<()> {
|
||||
let (source, target) = (c(source)?, c(target)?);
|
||||
checked(
|
||||
unsafe {
|
||||
libc::mount(
|
||||
source.as_ptr(),
|
||||
target.as_ptr(),
|
||||
std::ptr::null(),
|
||||
libc::MS_MOVE,
|
||||
std::ptr::null(),
|
||||
)
|
||||
},
|
||||
"move mount",
|
||||
)
|
||||
}
|
||||
fn unmount(target: &str) -> Result<()> {
|
||||
checked(unsafe { libc::umount(c(target)?.as_ptr()) }, "unmount")
|
||||
}
|
||||
fn prepare() -> Result<()> {
|
||||
if unsafe { libc::getpid() } != 1 || unsafe { libc::geteuid() } != 0 {
|
||||
return Err(Error(
|
||||
"Normal stage0 boot requires root PID 1 in an initramfs".into(),
|
||||
));
|
||||
}
|
||||
let mut stat: libc::statfs = unsafe { std::mem::zeroed() };
|
||||
checked(
|
||||
unsafe { libc::statfs(c("/")?.as_ptr(), &mut stat) },
|
||||
"inspect initial root",
|
||||
)?;
|
||||
// Linux UAPI linux/magic.h; libc exposes differing signed types by ABI.
|
||||
if ![0x8584_58f6_u64, 0x0102_1994_u64].contains(&(stat.f_type as u64)) {
|
||||
return Err(Error(
|
||||
"Refusing root handoff: initial root is not ramfs/tmpfs".into(),
|
||||
));
|
||||
}
|
||||
for path in ["/proc", "/sys", "/dev", "/newroot"] {
|
||||
fs::create_dir_all(path)?;
|
||||
}
|
||||
mount(
|
||||
"proc",
|
||||
"/proc",
|
||||
"proc",
|
||||
libc::MS_NOSUID | libc::MS_NODEV | libc::MS_NOEXEC,
|
||||
)?;
|
||||
mount(
|
||||
"sysfs",
|
||||
"/sys",
|
||||
"sysfs",
|
||||
libc::MS_NOSUID | libc::MS_NODEV | libc::MS_NOEXEC,
|
||||
)?;
|
||||
mount("devtmpfs", "/dev", "devtmpfs", libc::MS_NOSUID)?;
|
||||
fs::create_dir_all("/dev/fds-early")?;
|
||||
fs::set_permissions("/dev/fds-early", fs::Permissions::from_mode(0o700))?;
|
||||
Ok(())
|
||||
}
|
||||
fn netlink() -> Result<OwnedFd> {
|
||||
let fd = unsafe {
|
||||
libc::socket(
|
||||
libc::AF_NETLINK,
|
||||
libc::SOCK_DGRAM | libc::SOCK_CLOEXEC,
|
||||
libc::NETLINK_KOBJECT_UEVENT,
|
||||
)
|
||||
};
|
||||
checked(fd, "open kernel uevent socket")?;
|
||||
let fd = unsafe { OwnedFd::from_raw_fd(fd) };
|
||||
let mut address: libc::sockaddr_nl = unsafe { std::mem::zeroed() };
|
||||
address.nl_family = libc::AF_NETLINK as u16;
|
||||
address.nl_groups = 1;
|
||||
checked(
|
||||
unsafe {
|
||||
libc::bind(
|
||||
fd.as_raw_fd(),
|
||||
(&address as *const libc::sockaddr_nl).cast(),
|
||||
std::mem::size_of_val(&address) as _,
|
||||
)
|
||||
},
|
||||
"bind kernel uevent socket",
|
||||
)?;
|
||||
Ok(fd)
|
||||
}
|
||||
fn child_events() -> Result<OwnedFd> {
|
||||
let mut signals: libc::sigset_t = unsafe { std::mem::zeroed() };
|
||||
unsafe {
|
||||
libc::sigemptyset(&mut signals);
|
||||
libc::sigaddset(&mut signals, libc::SIGCHLD);
|
||||
}
|
||||
checked(
|
||||
unsafe { libc::sigprocmask(libc::SIG_BLOCK, &signals, std::ptr::null_mut()) },
|
||||
"block SIGCHLD",
|
||||
)?;
|
||||
let fd = unsafe { libc::signalfd(-1, &signals, libc::SFD_CLOEXEC | libc::SFD_NONBLOCK) };
|
||||
checked(fd, "open child event descriptor")?;
|
||||
Ok(unsafe { OwnedFd::from_raw_fd(fd) })
|
||||
}
|
||||
fn start_monitor(debug: bool) -> Result<Child> {
|
||||
let mut command = Command::new("/sbin/dasungd");
|
||||
command
|
||||
.args(["--config", "/etc/dasungd-early.toml", "daemon"])
|
||||
.env_clear();
|
||||
if !debug {
|
||||
command.stdout(Stdio::null()).stderr(Stdio::null());
|
||||
}
|
||||
command
|
||||
.spawn()
|
||||
.map_err(|e| Error(format!("Start early Dasung controller: {e}")))
|
||||
}
|
||||
fn stop_monitor(child: &mut Child) -> Result<()> {
|
||||
if child.try_wait()?.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
let pid = child.id() as libc::pid_t;
|
||||
let fd = unsafe { libc::syscall(libc::SYS_pidfd_open, pid, 0) } as libc::c_int;
|
||||
checked(fd, "open early-controller process handle")?;
|
||||
let fd = unsafe { OwnedFd::from_raw_fd(fd) };
|
||||
checked(
|
||||
unsafe { libc::kill(pid, libc::SIGTERM) },
|
||||
"stop early monitor controller",
|
||||
)?;
|
||||
let mut poll = libc::pollfd {
|
||||
fd: fd.as_raw_fd(),
|
||||
events: libc::POLLIN,
|
||||
revents: 0,
|
||||
};
|
||||
loop {
|
||||
// This is a process-exit deadline, not a delay before proceeding.
|
||||
let ready = unsafe { libc::poll(&mut poll, 1, 2000) };
|
||||
if ready < 0 && io::Error::last_os_error().kind() == io::ErrorKind::Interrupted {
|
||||
continue;
|
||||
}
|
||||
checked(ready, "wait for early controller exit")?;
|
||||
if ready == 0 {
|
||||
child.kill()?;
|
||||
}
|
||||
child.wait()?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
fn try_root(device: &BlockPartition) -> Result<()> {
|
||||
let node = fs::File::open(&device.device)?;
|
||||
let stat = node.metadata()?;
|
||||
if !stat.file_type().is_block_device()
|
||||
|| libc::major(stat.rdev()) != device.major
|
||||
|| libc::minor(stat.rdev()) != device.minor
|
||||
{
|
||||
return Err(Error("Block device changed during discovery".into()));
|
||||
}
|
||||
mount(
|
||||
&format!("/proc/self/fd/{}", node.as_raw_fd()),
|
||||
"/newroot",
|
||||
"erofs",
|
||||
libc::MS_RDONLY,
|
||||
)?;
|
||||
let verify = || -> Result<()> {
|
||||
let identity = read_text(Path::new("/newroot/usr/lib/os-release"), 16384)?;
|
||||
if !identity.lines().any(|line| line == "ID=fds") {
|
||||
return Err(Error("Selected root is not an FDS image".into()));
|
||||
}
|
||||
for path in [
|
||||
"/newroot/sbin/init",
|
||||
"/newroot/usr/bin/execlineb",
|
||||
"/newroot/usr/bin/s6-linux-init",
|
||||
] {
|
||||
let meta = fs::metadata(path)?;
|
||||
if !meta.is_file() || meta.mode() & 0o111 == 0 {
|
||||
return Err(Error(format!("Missing executable: {path}")));
|
||||
}
|
||||
}
|
||||
for path in ["/newroot/proc", "/newroot/sys", "/newroot/dev"] {
|
||||
let meta = fs::symlink_metadata(path)?;
|
||||
if !meta.is_dir() {
|
||||
return Err(Error(format!("Invalid early mountpoint: {path}")));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
if let Err(error) = verify() {
|
||||
unmount("/newroot")?;
|
||||
return Err(error);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn remove_initial_tree(path: &Path, device: u64) -> Result<()> {
|
||||
for entry in fs::read_dir(path)? {
|
||||
let entry = entry?;
|
||||
let metadata = fs::symlink_metadata(entry.path())?;
|
||||
if metadata.dev() != device {
|
||||
continue;
|
||||
}
|
||||
if metadata.is_dir() {
|
||||
remove_initial_tree(&entry.path(), device)?;
|
||||
fs::remove_dir(entry.path())?;
|
||||
} else {
|
||||
fs::remove_file(entry.path())?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn handoff(mut monitor: Child, debug: bool) -> Result<()> {
|
||||
stop_monitor(&mut monitor)?;
|
||||
if let Ok(instant) = trace::now() {
|
||||
trace::mark_early(Point::RootSwitch, instant);
|
||||
}
|
||||
for source in ["/proc", "/sys", "/dev"] {
|
||||
move_mount(source, &format!("/newroot{source}"))?;
|
||||
}
|
||||
let old_device = fs::metadata("/")?.dev();
|
||||
if fs::metadata("/newroot")?.dev() == old_device {
|
||||
return Err(Error("New root is not a separate filesystem".into()));
|
||||
}
|
||||
std::env::set_current_dir("/newroot")?;
|
||||
// Only the initial ramfs/tmpfs is removed. Moved mounts and EROFS are skipped.
|
||||
remove_initial_tree(Path::new("/"), old_device)?;
|
||||
move_mount("/newroot", "/")?;
|
||||
checked(unsafe { libc::chroot(c(".")?.as_ptr()) }, "enter new root")?;
|
||||
std::env::set_current_dir("/")?;
|
||||
let mut empty: libc::sigset_t = unsafe { std::mem::zeroed() };
|
||||
unsafe {
|
||||
libc::sigemptyset(&mut empty);
|
||||
}
|
||||
checked(
|
||||
unsafe { libc::sigprocmask(libc::SIG_SETMASK, &empty, std::ptr::null_mut()) },
|
||||
"restore init signal mask",
|
||||
)?;
|
||||
if debug {
|
||||
println!("FDS_STAGE0_HANDOFF: native s6 on read-only SYSTEM");
|
||||
}
|
||||
let error = Command::new("/sbin/init")
|
||||
.env_clear()
|
||||
.env("PATH", "/usr/bin:/bin")
|
||||
.env("LANG", "en_US.UTF-8")
|
||||
.exec();
|
||||
Err(Error(format!("Execute native init: {error}")))
|
||||
}
|
||||
fn power(command: &str) -> Result<()> {
|
||||
let action = match command {
|
||||
"reboot" => libc::LINUX_REBOOT_CMD_RESTART,
|
||||
"poweroff" => libc::LINUX_REBOOT_CMD_POWER_OFF,
|
||||
_ => return Err(Error("Invalid power command".into())),
|
||||
};
|
||||
checked(
|
||||
unsafe { libc::reboot(action) },
|
||||
"early-userspace power request",
|
||||
)
|
||||
}
|
||||
#[derive(Default)]
|
||||
struct ConsoleInput {
|
||||
pending: Vec<u8>,
|
||||
discard: bool,
|
||||
}
|
||||
impl ConsoleInput {
|
||||
fn accept(&mut self, bytes: &[u8]) -> Vec<String> {
|
||||
let mut commands = Vec::new();
|
||||
for &byte in bytes {
|
||||
if byte == b'\n' || byte == b'\r' {
|
||||
if !self.discard {
|
||||
commands.push(String::from_utf8_lossy(&self.pending).trim().to_owned());
|
||||
}
|
||||
self.pending.clear();
|
||||
self.discard = false;
|
||||
} else if !self.discard && self.pending.len() < 256 {
|
||||
self.pending.push(byte);
|
||||
} else {
|
||||
// Discard the complete overlong line, including any command suffix.
|
||||
self.pending.clear();
|
||||
self.discard = true;
|
||||
}
|
||||
}
|
||||
commands
|
||||
}
|
||||
}
|
||||
fn read_console(input: &mut ConsoleInput) -> Result<Option<Vec<String>>> {
|
||||
let mut bytes = [0; 256];
|
||||
let count = io::stdin().read(&mut bytes)?;
|
||||
if count == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(input.accept(&bytes[..count])))
|
||||
}
|
||||
fn block_event(fd: &OwnedFd) -> Result<bool> {
|
||||
let mut bytes = [0; 65536];
|
||||
let mut from: libc::sockaddr_nl = unsafe { std::mem::zeroed() };
|
||||
let mut length = std::mem::size_of_val(&from) as libc::socklen_t;
|
||||
let count = unsafe {
|
||||
libc::recvfrom(
|
||||
fd.as_raw_fd(),
|
||||
bytes.as_mut_ptr().cast(),
|
||||
bytes.len(),
|
||||
0,
|
||||
(&mut from as *mut libc::sockaddr_nl).cast(),
|
||||
&mut length,
|
||||
)
|
||||
};
|
||||
if count < 0 {
|
||||
// Lost notifications require a fresh scan, never an arbitrary wait.
|
||||
if io::Error::last_os_error().raw_os_error() == Some(libc::ENOBUFS) {
|
||||
return Ok(true);
|
||||
}
|
||||
return Err(io::Error::last_os_error().into());
|
||||
}
|
||||
Ok(from.nl_pid == 0
|
||||
&& bytes[..count as usize]
|
||||
.split(|b| *b == 0)
|
||||
.any(|field| field == b"SUBSYSTEM=block"))
|
||||
}
|
||||
pub fn boot() -> Result<()> {
|
||||
let entered = trace::now().ok();
|
||||
prepare()?;
|
||||
if let Some(instant) = entered {
|
||||
trace::mark_early(Point::Stage0Start, instant);
|
||||
}
|
||||
let mut options = BootOptions::parse(&read_text(Path::new("/proc/cmdline"), 65536)?)?;
|
||||
let events = netlink()?; // Bind before scanning so insertion cannot fall into a gap.
|
||||
let signals = child_events()?;
|
||||
let mut monitor = start_monitor(options.debug)?;
|
||||
println!("FELIS DATA SYSTEMS\nPORTABLE COMPUTER FP-85\n\nFDS BOOT ROM 0.1");
|
||||
println!("Commands while waiting: list, rescan, recovery, reboot, poweroff");
|
||||
let mut previous = String::new();
|
||||
let mut pending = ConsoleInput::default();
|
||||
let mut console_open = true;
|
||||
let mut restart_count = 0;
|
||||
loop {
|
||||
let candidates = sysfs::partitions(Path::new("/sys"))?;
|
||||
let selection = sysfs::select(&candidates, options.root_label());
|
||||
let state = match &selection {
|
||||
Selection::Missing => format!(
|
||||
"{} MEDIA NOT PRESENT\nINSERT {} CARTRIDGE",
|
||||
options.root_label(),
|
||||
options.root_label()
|
||||
),
|
||||
Selection::Ambiguous(devices) => format!(
|
||||
"MULTIPLE {} CARTRIDGES ({})\nREMOVE EXTRA MEDIA OR ENTER recovery",
|
||||
options.root_label(),
|
||||
devices.len()
|
||||
),
|
||||
Selection::Unique(device) => {
|
||||
let found = trace::now().ok();
|
||||
match try_root(device) {
|
||||
Ok(()) => {
|
||||
if let Some(instant) = found {
|
||||
trace::mark_early(Point::SystemFound, instant);
|
||||
}
|
||||
if let Ok(instant) = trace::now() {
|
||||
trace::mark_early(Point::RootMounted, instant);
|
||||
}
|
||||
if options.debug {
|
||||
println!("FDS_STAGE0_ROOT: {}", options.root_label());
|
||||
}
|
||||
println!("MEMORY ........ READY\nSYSTEM ........ FDS/OS 0.1");
|
||||
return handoff(monitor, options.debug);
|
||||
}
|
||||
Err(error) => {
|
||||
format!("SYSTEM CANNOT BE USED: {error}\nREPLACE MEDIA OR ENTER recovery")
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
if state != previous {
|
||||
println!("{state}");
|
||||
if options.debug {
|
||||
println!("FDS_STAGE0_WAIT");
|
||||
}
|
||||
io::stdout().flush()?;
|
||||
previous = state;
|
||||
}
|
||||
loop {
|
||||
let mut fds = [
|
||||
libc::pollfd {
|
||||
fd: events.as_raw_fd(),
|
||||
events: libc::POLLIN,
|
||||
revents: 0,
|
||||
},
|
||||
libc::pollfd {
|
||||
fd: if console_open { 0 } else { -1 },
|
||||
events: libc::POLLIN,
|
||||
revents: 0,
|
||||
},
|
||||
libc::pollfd {
|
||||
fd: signals.as_raw_fd(),
|
||||
events: libc::POLLIN,
|
||||
revents: 0,
|
||||
},
|
||||
];
|
||||
let ready = unsafe { libc::poll(fds.as_mut_ptr(), fds.len() as _, -1) };
|
||||
if ready < 0 && io::Error::last_os_error().kind() == io::ErrorKind::Interrupted {
|
||||
continue;
|
||||
}
|
||||
checked(ready, "wait for boot events")?;
|
||||
if fds[2].revents & libc::POLLIN != 0 {
|
||||
let mut info: libc::signalfd_siginfo = unsafe { std::mem::zeroed() };
|
||||
unsafe {
|
||||
libc::read(
|
||||
signals.as_raw_fd(),
|
||||
(&mut info as *mut libc::signalfd_siginfo).cast(),
|
||||
std::mem::size_of_val(&info),
|
||||
);
|
||||
}
|
||||
if let Some(status) = monitor.try_wait()? {
|
||||
if restart_count >= 3 {
|
||||
return Err(Error(format!(
|
||||
"Early monitor controller repeatedly exited: {status}"
|
||||
)));
|
||||
}
|
||||
restart_count += 1;
|
||||
monitor = start_monitor(options.debug)?;
|
||||
}
|
||||
}
|
||||
let mut rescan = fds[0].revents & libc::POLLIN != 0 && block_event(&events)?;
|
||||
if fds[1].revents & libc::POLLIN != 0 {
|
||||
let commands = read_console(&mut pending)?;
|
||||
console_open = commands.is_some();
|
||||
for command in commands.into_iter().flatten() {
|
||||
match command.as_str() {
|
||||
"recovery" => {
|
||||
options.mode = fds_common::boot::BootMode::Recovery;
|
||||
rescan = true;
|
||||
}
|
||||
"rescan" => rescan = true,
|
||||
"list" => println!(
|
||||
"{}",
|
||||
serde_json::to_string(&candidates).map_err(|e| Error(e.to_string()))?
|
||||
),
|
||||
"reboot" | "poweroff" => {
|
||||
stop_monitor(&mut monitor)?;
|
||||
power(&command)?;
|
||||
}
|
||||
"" => (),
|
||||
_ => println!("Commands: list, rescan, recovery, reboot, poweroff"),
|
||||
}
|
||||
}
|
||||
}
|
||||
if fds[1].revents & (libc::POLLHUP | libc::POLLERR | libc::POLLNVAL) != 0 {
|
||||
console_open = false;
|
||||
}
|
||||
if rescan {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn emergency(error: &Error) -> ! {
|
||||
eprintln!("FDS_STAGE0_ERROR: {error}\nBoot stopped. Enter reboot or poweroff.");
|
||||
let mut pending = ConsoleInput::default();
|
||||
let mut console_open = true;
|
||||
loop {
|
||||
let mut input = libc::pollfd {
|
||||
fd: if console_open { 0 } else { -1 },
|
||||
events: libc::POLLIN,
|
||||
revents: 0,
|
||||
};
|
||||
let ready = unsafe { libc::poll(&mut input, 1, -1) };
|
||||
if ready > 0 && input.revents & libc::POLLIN != 0 {
|
||||
if let Ok(commands) = read_console(&mut pending) {
|
||||
console_open = commands.is_some();
|
||||
for command in commands.into_iter().flatten() {
|
||||
if matches!(command.as_str(), "reboot" | "poweroff") {
|
||||
let _ = power(&command);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if ready > 0 {
|
||||
// With no console, wait for an external signal instead of busy looping.
|
||||
unsafe {
|
||||
libc::pause();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::ConsoleInput;
|
||||
#[test]
|
||||
fn console_handles_split_input_and_discards_overlong_command_suffixes() {
|
||||
let mut input = ConsoleInput::default();
|
||||
assert!(input.accept(b"reco").is_empty());
|
||||
assert_eq!(input.accept(b"very\n"), ["recovery"]);
|
||||
assert!(input.accept(&[b'x'; 256]).is_empty());
|
||||
assert!(input.accept(b"xpoweroff\n").is_empty());
|
||||
assert_eq!(input.accept(b"list\n"), ["list"]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
mod linux;
|
||||
use clap::{Parser, ValueEnum};
|
||||
use fds_common::{Error, MAX_CONFIG_BYTES, Result, boot::BootOptions, read_text, sysfs};
|
||||
use std::{path::PathBuf, process::ExitCode};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
version,
|
||||
about = "Early boot discovery and native s6 handoff",
|
||||
after_help = "With no arguments, root PID 1 boots FDS. Diagnostic options only read their supplied inputs."
|
||||
)]
|
||||
struct Cli {
|
||||
/// Validate a kernel command-line file without booting.
|
||||
#[arg(long, value_name = "FILE", conflicts_with = "probe")]
|
||||
check_cmdline: Option<PathBuf>,
|
||||
/// Inspect SYSTEM or RECOVERY discovery in the supplied sysfs tree.
|
||||
#[arg(long, value_name = "SYSFS_ROOT")]
|
||||
probe: Option<PathBuf>,
|
||||
#[arg(value_enum, requires = "probe", conflicts_with = "check_cmdline")]
|
||||
label: Option<Label>,
|
||||
}
|
||||
#[derive(Clone, Copy, ValueEnum)]
|
||||
#[value(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
enum Label {
|
||||
FdsSystem,
|
||||
FdsRecovery,
|
||||
}
|
||||
|
||||
fn run(cli: Cli) -> Result<()> {
|
||||
if let Some(path) = cli.check_cmdline {
|
||||
let options = BootOptions::parse(&read_text(&path, MAX_CONFIG_BYTES)?)?;
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string(&options).map_err(|e| Error(e.to_string()))?
|
||||
);
|
||||
} else if let Some(path) = cli.probe {
|
||||
let label = match cli.label.unwrap_or(Label::FdsSystem) {
|
||||
Label::FdsSystem => "FDS_SYSTEM",
|
||||
Label::FdsRecovery => "FDS_RECOVERY",
|
||||
};
|
||||
let selection = sysfs::select(&sysfs::partitions(&path)?, label);
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string(&selection).map_err(|e| Error(e.to_string()))?
|
||||
);
|
||||
} else {
|
||||
return linux::boot();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn main() -> ExitCode {
|
||||
// PID 1 must enter the emergency path instead of exiting on malformed options.
|
||||
let cli = match Cli::try_parse() {
|
||||
Ok(cli) => cli,
|
||||
Err(error) => {
|
||||
if unsafe { libc::getpid() } == 1 {
|
||||
linux::emergency(&Error(error.to_string()));
|
||||
}
|
||||
error.exit();
|
||||
}
|
||||
};
|
||||
match run(cli) {
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
Err(error) => {
|
||||
if unsafe { libc::getpid() } == 1 {
|
||||
linux::emergency(&error);
|
||||
}
|
||||
eprintln!("fds-stage0: {error}");
|
||||
ExitCode::from(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod cli_tests {
|
||||
use super::*;
|
||||
use clap::{CommandFactory, Parser};
|
||||
#[test]
|
||||
fn typed_command_contract() {
|
||||
Cli::command().debug_assert();
|
||||
assert!(Cli::try_parse_from(["fds-stage0"]).unwrap().probe.is_none());
|
||||
for args in [
|
||||
vec!["fds-stage0", "--probe", "/sys"],
|
||||
vec!["fds-stage0", "--probe", "/sys", "FDS_RECOVERY"],
|
||||
vec!["fds-stage0", "--check-cmdline", "cmdline"],
|
||||
] {
|
||||
assert!(Cli::try_parse_from(args).is_ok());
|
||||
}
|
||||
for args in [
|
||||
vec!["fds-stage0", "FDS_SYSTEM"],
|
||||
vec!["fds-stage0", "--probe", "/sys", "other"],
|
||||
vec![
|
||||
"fds-stage0",
|
||||
"--probe",
|
||||
"/sys",
|
||||
"--check-cmdline",
|
||||
"cmdline",
|
||||
],
|
||||
vec!["fds-stage0", "--check-cmdline", "cmdline", "FDS_RECOVERY"],
|
||||
] {
|
||||
assert!(Cli::try_parse_from(&args).is_err(), "{args:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user