Files
fds-os/rust/fds-cli/src/main.rs
T
2026-09-22 13:23:34 +08:00

278 lines
10 KiB
Rust

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