278 lines
10 KiB
Rust
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)
|
|
}
|
|
}
|
|
}
|