mod cli; mod machine; mod power; mod recovery; use clap::Parser; use cli::{ Action, DataCommand, InspectCommand, PowerCommand, ProfileCommand, RecoveryCommand, Switch, }; use fds_common::{ Error, Result, VERSION, control::{self, Request}, manifest::Manifest, read_text, trace, }; use std::{path::Path, process::ExitCode}; fn run() -> Result<()> { let (command, json) = cli::Cli::parse().into_command(); let Some(command) = command else { return Ok(cli::help(None)?); }; match command { Action::Machine { command: Some(command), } => machine::run(command, json)?, Action::Machine { command: None } => cli::help(Some("machine"))?, Action::Recovery { command: None } => cli::help(Some("recovery"))?, Action::Recovery { command: Some(RecoveryCommand::Check(args)), } => recovery::run(args.bay, false, None, json)?, Action::Recovery { command: Some(RecoveryCommand::Repair { bay, confirm }), } => recovery::run(bay, true, confirm, json)?, Action::Poweroff => power::client(Request::Poweroff, json)?, Action::Reboot => power::client(Request::Reboot, json)?, Action::Power(args) => match args.command.unwrap_or(PowerCommand::Status) { PowerCommand::Poweroff => power::client(Request::Poweroff, json)?, PowerCommand::Reboot => power::client(Request::Reboot, json)?, PowerCommand::Status => power::client(Request::PowerStatus, json)?, PowerCommand::Resume => power::client(Request::PowerResume, json)?, PowerCommand::ShutdownHook => power::shutdown_hook()?, PowerCommand::HoldShutdown => power::hold_shutdown()?, PowerCommand::RecordFinal { outcome } => power::record_final(outcome.is_some())?, }, Action::Burn { command } => fds_burn::client::burn(command, json)?, Action::Format { command } => fds_burn::client::format(command, json)?, Action::Inspect(args) => { if let Some(InspectCommand::Image { image: path }) = args.command { let file = std::fs::File::open(path)?; if !file.metadata()?.is_file() { return Err(Error("Image inspection requires a regular file".into())); } let mut info = fds_burn::image::inspect(&file, file.metadata()?.len())?; info.sha256 = Some(fds_burn::image::digest(&file, info.bytes, |_| Ok(()))?); println!( "{}", serde_json::to_string_pretty(&info).map_err(|e| Error(e.to_string()))? ); } else { let path = args .target .expect("Clap requires a target or image subcommand"); if let Some(bay) = path .to_str() .and_then(|value| fds_burn::client::bay(value).ok()) { fds_burn::client::inspect_bay(bay, json)?; } else { inspect_manifest(&path, json)?; } } } Action::Profiles => cartridge(Request::Profiles, json)?, Action::Profile { command } => cartridge( Request::Profile { profile: match command { ProfileCommand::Activate { name } => name, ProfileCommand::Deactivate => "cli".into(), }, }, json, )?, Action::Network { state } => cartridge( Request::Network { enabled: matches!(state, Switch::On), }, json, )?, Action::Bays => cartridge(Request::Bays, json)?, Action::Bay(args) => cartridge(Request::Bay { bay: args.bay }, json)?, Action::Eject(args) => cartridge(Request::Eject { bay: args.bay }, json)?, Action::Rescan => cartridge(Request::Rescan, json)?, Action::Data { command: DataCommand::Use(args), } => cartridge(Request::DataUse { bay: args.bay }, json)?, Action::Run { bay, arguments } => cartridge(Request::Run { bay, arguments }, json)?, Action::Topology => cartridge(Request::Topology, json)?, Action::Version => println!("fds {VERSION}"), Action::BootProfile => trace::load(Path::new(trace::RUNTIME))?.print(json)?, Action::Info => info(json)?, } Ok(()) } fn info(json: bool) -> Result<()> { let kernel = read_text(Path::new("/proc/sys/kernel/osrelease"), 4096)? .trim() .to_owned(); let pid1 = std::fs::read_link("/proc/1/exe") .map(|p| p.display().to_string()) .unwrap_or_else(|_| { read_text(Path::new("/proc/1/comm"), 4096) .map(|s| s.trim().to_owned()) .unwrap_or_else(|_| "unavailable".into()) }); let target = if cfg!(all( target_arch = "aarch64", target_env = "musl", target_feature = "crt-static" )) { "aarch64 static-musl" } else { "host test build" }; if json { println!( "{}", serde_json::json!({"fds_version": VERSION, "target": target, "kernel": kernel, "pid1": pid1}) ); } else { println!("FDS/OS {VERSION}\nTOOLS {target}\nKERNEL {kernel}\nINIT {pid1}"); } Ok(()) } fn inspect_manifest(path: &Path, json: bool) -> Result<()> { let manifest = Manifest::load(path)?; if json { println!( "{}", serde_json::to_string_pretty(&manifest).map_err(|e| Error(e.to_string()))? ); } else { println!( "{}\n{} {}\nID {}\nMEDIA {}", manifest.cartridge.class.label(), manifest.cartridge.name, manifest.cartridge.version, manifest.cartridge.id, if manifest.media.writable { "WRITABLE" } else { "READ-ONLY" } ); if let Some(activation) = manifest.activation { println!("PROFILE {}", activation.profile); } } Ok(()) } fn cartridge(request: Request, json: bool) -> Result<()> { let debug = matches!(request, Request::Topology); let details = matches!(request, Request::Bay { .. }); let response = control::request(&request)?; if json { println!( "{}", serde_json::to_string_pretty(&response).map_err(|e| Error(e.to_string()))? ); } else { if let Some(pid) = response.started_pid { println!("STARTED {pid} Output: /run/log/cartridged/current"); } if let Some(p) = response.profiles { println!( "PROFILES cli, windowmaker\nDESKTOP {} {}\nNETWORK {}", p.desktop, if p.desktop == "windowmaker" { if p.ready_ns.is_some() { "READY" } else { "STARTING" } } else { "" }, if p.network.is_empty() { "OFF".into() } else { p.network.join(", ") } ); if let Some(bay) = p.environment_bay { println!("ENVIRONMENT BAY {bay}"); } if let (Some(start), Some(end)) = (p.activation_ns, p.ready_ns) { println!( "ACTIVATION TO READY {:.3} ms", end.saturating_sub(start) as f64 / 1_000_000.0 ); } if let Some(error) = p.error { println!("ERROR {error}"); } return Ok(()); } for bay in response.bays { println!( "BAY {} {}{}", bay.bay, bay.state.to_uppercase().replace('_', " "), bay.name .as_ref() .map(|n| format!(" {n}")) .unwrap_or_default() ); if let Some(detail) = bay.detail { println!(" {detail}"); } if let Some(manifest) = bay.manifest { println!( " {} {} {}", manifest.cartridge.class.label(), manifest.cartridge.id, manifest.cartridge.version ); } if let Some(catalogue) = bay.software { for software in catalogue.software { println!( " SOFTWARE {} {} (partition {})", software.id, software.version, software.partition ); for command in software.commands.keys() { println!(" fds run {} -- {}:{}", bay.bay, software.id, command); } } } if let Some(mount) = bay.mount { println!(" MOUNT {mount}"); } if details { for command in bay.commands { println!(" FOREGROUND {}", command.alias); } } if bay.consumers > 0 { println!(" MANAGED PROCESSES {}", bay.consumers); } if debug { for device in bay.devices { println!( " {} USB {}:{}", device.topology, device.vendor, device.product ); } } } if debug { for device in response.unmapped { println!( "UNMAPPED {} USB {}:{}", device.topology, device.vendor, device.product ); } } } Ok(()) } fn main() -> ExitCode { match run() { Ok(()) => ExitCode::SUCCESS, Err(error) => { eprintln!("fds: {error}"); ExitCode::from(2) } } }