FDS/OS 1.0

This commit is contained in:
2026-09-21 22:29:23 +08:00
commit 99bc3d15c5
430 changed files with 34876 additions and 0 deletions
+232
View File
@@ -0,0 +1,232 @@
use clap::{CommandFactory, Parser, Subcommand};
use fds_common::{
Error, Result, read_text,
trace::{self, Point, Report},
};
use std::{
fs,
os::unix::fs::PermissionsExt,
path::{Path, PathBuf},
process::ExitCode,
};
fn root() -> Result<()> {
if unsafe { libc::geteuid() } != 0 {
return Err(Error("This boot event requires root".into()));
}
Ok(())
}
fn clock_floor() -> Result<()> {
root()?;
let epoch: i64 = read_text(Path::new("/usr/share/fds/build-epoch"), 32)?
.trim()
.parse()
.map_err(|_| Error("Invalid image clock floor".into()))?;
if !(1..=4_102_444_800).contains(&epoch) {
return Err(Error(
"Image clock floor is outside the supported range".into(),
));
}
let mut before: libc::timespec = unsafe { std::mem::zeroed() };
if unsafe { libc::clock_gettime(libc::CLOCK_REALTIME, &mut before) } < 0 {
return Err(std::io::Error::last_os_error().into());
}
let advance = before.tv_sec < epoch;
if advance {
let floor = libc::timespec {
tv_sec: epoch,
tv_nsec: 0,
};
if unsafe { libc::clock_settime(libc::CLOCK_REALTIME, &floor) } < 0 {
return Err(std::io::Error::last_os_error().into());
}
}
fs::create_dir_all("/run/fds")?;
let status = serde_json::json!({"format":1, "source":if advance {"image_floor"} else {"retained_kernel_clock"},
"image_epoch":epoch, "previous_unix_seconds":before.tv_sec, "minimum_unix_seconds":before.tv_sec.max(epoch)});
fs::write(
"/run/fds/clock.json",
serde_json::to_vec_pretty(&status).map_err(|e| Error(e.to_string()))?,
)?;
Ok(())
}
fn adopt() -> Result<()> {
root()?;
let instant = trace::now()?;
let directory = Path::new(trace::RUNTIME);
fs::create_dir_all(directory.join("console"))?;
fs::set_permissions(directory, fs::Permissions::from_mode(0o755))?;
fs::set_permissions(directory.join("console"), fs::Permissions::from_mode(0o755))?;
let path = std::ffi::CString::new(directory.join("console").to_str().unwrap()).unwrap();
if unsafe { libc::chown(path.as_ptr(), 1000, 1000) } < 0 {
return Err(std::io::Error::last_os_error().into());
}
for point in Point::ALL {
let source = Path::new(trace::EARLY).join(format!("{}.json", point.name()));
if source.exists() {
fs::rename(&source, directory.join(source.file_name().unwrap())).or_else(|error| {
if error.raw_os_error() != Some(libc::EXDEV) {
return Err(error);
}
fs::copy(&source, directory.join(source.file_name().unwrap()))?;
fs::remove_file(source)
})?;
}
}
trace::save(directory, Point::S6Start, instant)?;
// Preserve a valid kernel/RTC clock. A machine without an RTC must still
// create files newer than immutable image inputs; no time server is awaited.
clock_floor()
}
#[derive(Parser)]
#[command(
version,
about = "Record and compare measured boot events",
after_help = "Measurements use Linux CLOCK_BOOTTIME, excluding firmware and power-on."
)]
struct Cli {
#[command(subcommand)]
command: Option<Action>,
}
#[derive(Subcommand)]
enum Action {
/// Import stage0 events and record native s6 startup (root only).
Adopt,
/// Advance an older clock to the image timestamp (root only).
ClockFloor,
/// Record a named boot event.
Mark {
#[arg(value_parser = Point::parse)]
event: Point,
},
/// Show this boot's events and durations.
Report {
#[arg(long)]
json: bool,
},
/// Compare reports; a regression over 100 ms requires an explanation.
Compare {
baseline: PathBuf,
current: PathBuf,
#[arg(long, value_parser = explanation)]
explain: Option<String>,
},
}
fn explanation(value: &str) -> std::result::Result<String, String> {
if value.trim().is_empty() {
Err("An explanation must not be blank".into())
} else {
Ok(value.into())
}
}
fn run() -> Result<()> {
match Cli::parse().command {
None => {
Cli::command().print_help()?;
println!();
}
Some(Action::Adopt) => adopt()?,
Some(Action::ClockFloor) => clock_floor()?,
Some(Action::Mark { event: point }) => {
let directory = if point == Point::ConsoleReady {
let uid = unsafe { libc::geteuid() };
if uid != 0 && uid != 1000 {
return Err(Error("Console readiness requires the FDS user".into()));
}
Path::new(trace::RUNTIME).join("console")
} else {
root()?;
Path::new(trace::RUNTIME).to_owned()
};
trace::save(&directory, point, trace::now()?)?;
}
Some(Action::Report { json }) => trace::load(Path::new(trace::RUNTIME))?.print(json)?,
Some(Action::Compare {
baseline,
current,
explain,
}) => compare(&baseline, &current, explain.as_deref())?,
}
Ok(())
}
fn compare(baseline: &Path, current: &Path, explanation: Option<&str>) -> Result<()> {
let load = |path| -> Result<Report> {
serde_json::from_str(&read_text(path, 65536)?).map_err(|e| Error(e.to_string()))
};
let (before, after) = (load(baseline)?, load(current)?);
if before.format != 1
|| after.format != 1
|| before.clock != after.clock
|| before.platform != after.platform
{
return Err(Error(
"Compare reports from the same platform and clock".into(),
));
}
let elapsed = |report: &Report| {
report
.durations_ns
.get("kernel-to-console")
.copied()
.ok_or_else(|| Error("Console readiness was not recorded".into()))
};
let delta = i128::from(elapsed(&after)?) - i128::from(elapsed(&before)?);
println!(
"KERNEL TO CONSOLE CHANGE: {:+.3} ms",
delta as f64 / 1_000_000.0
);
if let Some(reason) = explanation {
println!("EXPLANATION: {reason}");
}
if delta > 100_000_000 && explanation.is_none() {
return Err(Error(
"Regression exceeds 100 ms; fix it or record --explain REASON".into(),
));
}
Ok(())
}
fn main() -> ExitCode {
match run() {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("fds-boottrace: {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-boottrace", "mark", "console-ready"]).is_ok());
assert!(Cli::try_parse_from(["fds-boottrace", "mark", "unknown"]).is_err());
assert!(Cli::try_parse_from(["fds-boottrace", "report", "--json"]).is_ok());
assert!(
Cli::try_parse_from([
"fds-boottrace",
"compare",
"before",
"after",
"--explain",
"measured change"
])
.is_ok()
);
assert!(
Cli::try_parse_from([
"fds-boottrace",
"compare",
"before",
"after",
"--explain",
" "
])
.is_err()
);
assert!(Cli::try_parse_from(["fds-boottrace", "clock-floor", "--json"]).is_err());
}
}