mirror of
https://github.com/edera-dev/sprout.git
synced 2026-11-04 19:02:20 +00:00
feat(bls): boot counting (#107)
* feat(bls): parse and render boot counters * feat(bls): sort entries with no boot tries left last * feat(boot): strip boot counters from bls entry ids * feat(boot): consume bls boot counter tries before booting * fix(boot): never default to a bad boot counter entry while a good one exists
This commit is contained in:
5 files changed
+398
-7
No files matched your search
@@ -0,0 +1,54 @@
|
||||
use alloc::{format, string::String};
|
||||
use anyhow::{Context, Result};
|
||||
use edera_sprout_bls::BootCounter;
|
||||
use uefi::{
|
||||
CString16, Handle,
|
||||
fs::{FileSystem, PathBuf},
|
||||
proto::media::fs::SimpleFileSystem,
|
||||
};
|
||||
|
||||
/// Where the boot counter of an entry is stored, so that a try can be consumed when the
|
||||
/// entry is booted.
|
||||
#[derive(Clone)]
|
||||
pub struct BootCounterTarget {
|
||||
/// The counter as it is currently encoded in the file name.
|
||||
pub counter: BootCounter,
|
||||
/// The handle of the filesystem that holds the entry file.
|
||||
pub filesystem: Handle,
|
||||
/// The directory that holds the entry file.
|
||||
pub directory: PathBuf,
|
||||
/// The entry id, which is the file name without the counter and extension.
|
||||
pub id: String,
|
||||
/// The current file name, including the counter and the extension.
|
||||
pub file_name: String,
|
||||
}
|
||||
|
||||
impl BootCounterTarget {
|
||||
/// Consumes one try by renaming the entry file, such as `foo+3.conf` to `foo+2-1.conf`.
|
||||
/// Returns the path of the renamed file.
|
||||
pub fn consume(&self) -> Result<PathBuf> {
|
||||
// The extension is taken from the current file name, as it may be in any case.
|
||||
let extension = &self.file_name[self.file_name.len() - ".conf".len()..];
|
||||
let new_name = format!(
|
||||
"{}{}",
|
||||
self.counter.decremented().render(&self.id),
|
||||
extension
|
||||
);
|
||||
|
||||
let mut from = self.directory.clone();
|
||||
from.push(PathBuf::from(
|
||||
CString16::try_from(self.file_name.as_str()).context("invalid entry file name")?,
|
||||
));
|
||||
let mut to = self.directory.clone();
|
||||
to.push(PathBuf::from(
|
||||
CString16::try_from(new_name.as_str()).context("invalid new entry file name")?,
|
||||
));
|
||||
|
||||
let fs = uefi::boot::open_protocol_exclusive::<SimpleFileSystem>(self.filesystem)
|
||||
.context("unable to open the entry filesystem")?;
|
||||
let mut fs = FileSystem::new(fs);
|
||||
fs.rename(&from, &to)
|
||||
.context("unable to rename the entry file")?;
|
||||
Ok(to)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::boot_counter::BootCounterTarget;
|
||||
use crate::context::SproutContext;
|
||||
use alloc::rc::Rc;
|
||||
use alloc::string::{String, ToString};
|
||||
@@ -14,6 +15,7 @@ pub struct BootableEntry {
|
||||
default: bool,
|
||||
pin_name: bool,
|
||||
sort_key: Option<String>,
|
||||
boot_counter: Option<BootCounterTarget>,
|
||||
}
|
||||
|
||||
impl BootableEntry {
|
||||
@@ -32,6 +34,7 @@ impl BootableEntry {
|
||||
default: false,
|
||||
pin_name: false,
|
||||
sort_key: None,
|
||||
boot_counter: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +103,23 @@ impl BootableEntry {
|
||||
self.pin_name = true;
|
||||
}
|
||||
|
||||
/// Record where the boot counter of this entry is stored.
|
||||
pub fn set_boot_counter(&mut self, target: BootCounterTarget) {
|
||||
self.boot_counter = Some(target);
|
||||
}
|
||||
|
||||
/// Fetch where the boot counter of this entry is stored, if it has one.
|
||||
pub fn boot_counter(&self) -> Option<&BootCounterTarget> {
|
||||
self.boot_counter.as_ref()
|
||||
}
|
||||
|
||||
/// Fetch whether the entry has a boot counter with no tries left.
|
||||
pub fn is_bad(&self) -> bool {
|
||||
self.boot_counter
|
||||
.as_ref()
|
||||
.is_some_and(|target| target.counter.is_bad())
|
||||
}
|
||||
|
||||
/// Prepend the name of the entry with `prefix`.
|
||||
pub fn prepend_name_prefix(&mut self, prefix: &str) {
|
||||
self.name.insert_str(0, prefix);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::boot_counter::BootCounterTarget;
|
||||
use crate::context::SproutContext;
|
||||
use crate::entries::BootableEntry;
|
||||
use alloc::{
|
||||
@@ -8,7 +9,7 @@ use alloc::{
|
||||
};
|
||||
use anyhow::{Context, Result};
|
||||
use core::{cmp::Ordering, str::FromStr};
|
||||
use edera_sprout_bls::{BlsEntry, sort_bls};
|
||||
use edera_sprout_bls::{BlsEntry, BootCounter, sort_bls};
|
||||
use edera_sprout_config::generators::bls::BlsConfiguration;
|
||||
use log::warn;
|
||||
use uefi::{
|
||||
@@ -108,6 +109,15 @@ pub fn generate(context: Rc<SproutContext>, bls: &BlsConfiguration) -> Result<Ve
|
||||
continue;
|
||||
}
|
||||
|
||||
// Split the boot counter off the file name. The entry id never includes the counter, so
|
||||
// that it stays the same as tries are consumed.
|
||||
let file_name = entry.file_name().to_string();
|
||||
let (id, boot_counter) = {
|
||||
let (id, counter) = BootCounter::parse(&name);
|
||||
(id.to_string(), counter)
|
||||
};
|
||||
let name = id;
|
||||
|
||||
// Create a mutable path so we can append the file name to produce the full path.
|
||||
let mut full_entry_path = entries_path.to_path_buf();
|
||||
full_entry_path.push(entry.file_name());
|
||||
@@ -133,13 +143,14 @@ pub fn generate(context: Rc<SproutContext>, bls: &BlsConfiguration) -> Result<Ve
|
||||
};
|
||||
|
||||
// Parse the entry file as a BLS entry.
|
||||
let entry = match BlsEntry::from_str(&content) {
|
||||
let mut entry = match BlsEntry::from_str(&content) {
|
||||
Ok(entry) => entry,
|
||||
Err(error) => {
|
||||
warn!("unable to parse bls entry {}: {}", name, error);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
entry.boot_counter = boot_counter;
|
||||
|
||||
// Ignore entries that are not valid for Sprout.
|
||||
if !entry.is_valid() {
|
||||
@@ -195,6 +206,9 @@ pub fn generate(context: Rc<SproutContext>, bls: &BlsConfiguration) -> Result<Ve
|
||||
context.set("version", version);
|
||||
context.set("machine-id", machine_id);
|
||||
|
||||
// Keep the id for the boot counter, as the name is moved into the bootable entry.
|
||||
let boot_id = name.clone();
|
||||
|
||||
// Produce a new bootable entry.
|
||||
let mut boot = BootableEntry::new(
|
||||
name,
|
||||
@@ -210,6 +224,17 @@ pub fn generate(context: Rc<SproutContext>, bls: &BlsConfiguration) -> Result<Ve
|
||||
boot.mark_pin_name();
|
||||
}
|
||||
|
||||
// Record where the boot counter lives so a try can be consumed when this entry boots.
|
||||
if let Some(counter) = boot_counter {
|
||||
boot.set_boot_counter(BootCounterTarget {
|
||||
counter,
|
||||
filesystem: bls_resolved.filesystem_handle,
|
||||
directory: entries_path.clone(),
|
||||
id: boot_id,
|
||||
file_name,
|
||||
});
|
||||
}
|
||||
|
||||
// Add the BLS entry to the list, along with the bootable entry.
|
||||
entries.push((entry, boot));
|
||||
}
|
||||
|
||||
+32
-5
@@ -36,6 +36,9 @@ pub mod actions;
|
||||
/// autoconfigure: Autoconfigure Sprout based on the detected environment.
|
||||
pub mod autoconfigure;
|
||||
|
||||
/// boot_counter: Consume the boot counter tries of BLS entries.
|
||||
pub mod boot_counter;
|
||||
|
||||
/// config: Sprout configuration mechanism.
|
||||
pub mod config;
|
||||
|
||||
@@ -407,11 +410,20 @@ fn run() -> Result<()> {
|
||||
force_boot_entry = Some(bootloader_interface_oneshot_entry.clone());
|
||||
}
|
||||
|
||||
// If no entries were the default, pick the first entry as the default entry.
|
||||
if entries.iter().all(|entry| !entry.is_default())
|
||||
&& let Some(entry) = entries.first_mut()
|
||||
{
|
||||
entry.mark_default();
|
||||
// Entries that have no boot counter tries left are never picked automatically.
|
||||
// If no entries are the default, pick the first usable entry as the default entry.
|
||||
let default_flags = edera_sprout_bls::resolve_default_flags(
|
||||
&entries
|
||||
.iter()
|
||||
.map(|entry| (entry.is_default(), entry.is_bad()))
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
for (entry, default) in entries.iter_mut().zip(default_flags) {
|
||||
if default {
|
||||
entry.mark_default();
|
||||
} else {
|
||||
entry.unmark_default();
|
||||
}
|
||||
}
|
||||
|
||||
// Convert the menu timeout to a duration.
|
||||
@@ -465,6 +477,21 @@ fn run() -> Result<()> {
|
||||
// Execute the late phase, now that the entry is chosen but before its actions are executed.
|
||||
phase(context.clone(), &config.phases.late).context("unable to execute late phase")?;
|
||||
|
||||
// Consume a try from the boot counter of the selected entry.
|
||||
// Failing to do so must not prevent the entry from booting.
|
||||
if let Some(target) = entry.boot_counter()
|
||||
&& !target.counter.is_bad()
|
||||
{
|
||||
match target.consume() {
|
||||
Ok(path) => info!("updated boot counter of entry {}: {}", entry.name(), path),
|
||||
Err(error) => warn!(
|
||||
"unable to update boot counter of entry {}: {:#}",
|
||||
entry.name(),
|
||||
error
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// Execute all the actions for the selected entry.
|
||||
for action in &entry.declaration().actions {
|
||||
let action = entry.context().stamp(action);
|
||||
|
||||
Reference in new issue
Block a user