mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 12:11:56 +00:00
The BIOS_BOOT_TYPE_GUID constant had the wrong value, so find_bios_boot_partition never matched the actual BIOS boot partition created by the gpt crate. This caused it to appear as an available backup target.
259 lines
8.0 KiB
Rust
259 lines
8.0 KiB
Rust
use std::collections::BTreeMap;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use itertools::Itertools;
|
|
use lazy_format::lazy_format;
|
|
use rpc_toolkit::{CallRemoteHandler, Context, Empty, HandlerExt, ParentHandler, from_fn_async};
|
|
use serde::{Deserialize, Serialize};
|
|
use tokio::process::Command;
|
|
|
|
use crate::{Error, ErrorKind};
|
|
use crate::context::{CliContext, RpcContext};
|
|
use crate::disk::util::DiskInfo;
|
|
use crate::prelude::*;
|
|
use crate::util::Invoke;
|
|
use crate::util::serde::{HandlerExtSerde, WithIoFormat, display_serializable};
|
|
|
|
pub mod fsck;
|
|
pub mod main;
|
|
pub mod mount;
|
|
pub mod util;
|
|
|
|
pub const BOOT_RW_PATH: &str = "/media/boot-rw";
|
|
pub const REPAIR_DISK_PATH: &str = "/media/startos/config/repair-disk";
|
|
|
|
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct OsPartitionInfo {
|
|
pub bios: Option<PathBuf>,
|
|
pub boot: PathBuf,
|
|
pub root: PathBuf,
|
|
#[serde(default)]
|
|
pub extra_boot: BTreeMap<String, PathBuf>,
|
|
#[serde(skip)]
|
|
pub data: Option<PathBuf>,
|
|
}
|
|
impl OsPartitionInfo {
|
|
pub fn contains(&self, logicalname: impl AsRef<Path>) -> bool {
|
|
let p = logicalname.as_ref();
|
|
self.bios.as_deref() == Some(p)
|
|
|| p == &*self.boot
|
|
|| p == &*self.root
|
|
|| self.extra_boot.values().any(|v| v == p)
|
|
}
|
|
|
|
/// Build partition info by parsing /etc/fstab and resolving device specs,
|
|
/// then discovering the BIOS boot partition (which is never mounted).
|
|
pub async fn from_fstab() -> Result<Self, Error> {
|
|
let fstab = tokio::fs::read_to_string("/etc/fstab")
|
|
.await
|
|
.with_ctx(|_| (ErrorKind::Filesystem, "/etc/fstab"))?;
|
|
|
|
let mut boot = None;
|
|
let mut root = None;
|
|
let mut extra_boot = BTreeMap::new();
|
|
|
|
for line in fstab.lines() {
|
|
let line = line.trim();
|
|
if line.is_empty() || line.starts_with('#') {
|
|
continue;
|
|
}
|
|
let mut fields = line.split_whitespace();
|
|
let Some(source) = fields.next() else {
|
|
continue;
|
|
};
|
|
let Some(target) = fields.next() else {
|
|
continue;
|
|
};
|
|
|
|
let dev = match resolve_fstab_source(source).await {
|
|
Ok(d) => d,
|
|
Err(e) => {
|
|
tracing::warn!("Failed to resolve fstab source {source}: {e}");
|
|
continue;
|
|
}
|
|
};
|
|
|
|
match target {
|
|
"/" => root = Some(dev),
|
|
"/boot" => boot = Some(dev),
|
|
t if t.starts_with("/boot/") => {
|
|
if let Some(name) = t.strip_prefix("/boot/") {
|
|
extra_boot.insert(name.to_string(), dev);
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
let boot = boot.unwrap_or_default();
|
|
let bios = if !boot.as_os_str().is_empty() {
|
|
find_bios_boot_partition(&boot).await.ok().flatten()
|
|
} else {
|
|
None
|
|
};
|
|
|
|
Ok(Self {
|
|
bios,
|
|
boot,
|
|
root: root.unwrap_or_default(),
|
|
extra_boot,
|
|
data: None,
|
|
})
|
|
}
|
|
}
|
|
|
|
const BIOS_BOOT_TYPE_GUID: &str = "21686148-6449-6E6F-744E-656564454649";
|
|
|
|
/// Find the BIOS boot partition on the same disk as `known_part`.
|
|
async fn find_bios_boot_partition(known_part: &Path) -> Result<Option<PathBuf>, Error> {
|
|
let output = Command::new("lsblk")
|
|
.args(["-n", "-l", "-o", "NAME,PKNAME,PARTTYPE"])
|
|
.arg(known_part)
|
|
.invoke(ErrorKind::DiskManagement)
|
|
.await?;
|
|
let text = String::from_utf8(output)?;
|
|
|
|
let parent_disk = text.lines().find_map(|line| {
|
|
let mut fields = line.split_whitespace();
|
|
let _name = fields.next()?;
|
|
let pkname = fields.next()?;
|
|
(!pkname.is_empty()).then(|| pkname.to_string())
|
|
});
|
|
|
|
let Some(parent_disk) = parent_disk else {
|
|
return Ok(None);
|
|
};
|
|
|
|
let output = Command::new("lsblk")
|
|
.args(["-n", "-l", "-o", "NAME,PARTTYPE"])
|
|
.arg(format!("/dev/{parent_disk}"))
|
|
.invoke(ErrorKind::DiskManagement)
|
|
.await?;
|
|
let text = String::from_utf8(output)?;
|
|
|
|
for line in text.lines() {
|
|
let mut fields = line.split_whitespace();
|
|
let Some(name) = fields.next() else { continue };
|
|
let Some(parttype) = fields.next() else {
|
|
continue;
|
|
};
|
|
if parttype.eq_ignore_ascii_case(BIOS_BOOT_TYPE_GUID) {
|
|
return Ok(Some(PathBuf::from(format!("/dev/{name}"))));
|
|
}
|
|
}
|
|
|
|
Ok(None)
|
|
}
|
|
|
|
/// Resolve an fstab device spec (e.g. /dev/sda1, PARTUUID=..., UUID=...) to a
|
|
/// canonical device path.
|
|
async fn resolve_fstab_source(source: &str) -> Result<PathBuf, Error> {
|
|
if source.starts_with('/') {
|
|
return Ok(
|
|
tokio::fs::canonicalize(source)
|
|
.await
|
|
.unwrap_or_else(|_| PathBuf::from(source)),
|
|
);
|
|
}
|
|
// PARTUUID=, UUID=, LABEL= — resolve via blkid
|
|
let output = Command::new("blkid")
|
|
.args(["-o", "device", "-t", source])
|
|
.invoke(ErrorKind::DiskManagement)
|
|
.await?;
|
|
Ok(PathBuf::from(String::from_utf8(output)?.trim()))
|
|
}
|
|
|
|
pub fn disk<C: Context>() -> ParentHandler<C> {
|
|
ParentHandler::new()
|
|
.subcommand(
|
|
"list",
|
|
from_fn_async(list)
|
|
.with_display_serializable()
|
|
.with_custom_display_fn(|handle, result| display_disk_info(handle.params, result))
|
|
.with_about("about.list-disk-info")
|
|
.with_call_remote::<CliContext>(),
|
|
)
|
|
.subcommand("repair", from_fn_async(|_: C| repair()).no_cli())
|
|
.subcommand(
|
|
"repair",
|
|
CallRemoteHandler::<CliContext, _, _>::new(
|
|
from_fn_async(|_: RpcContext| repair())
|
|
.no_display()
|
|
.with_about("about.repair-disk-corruption"),
|
|
),
|
|
)
|
|
}
|
|
|
|
fn display_disk_info(params: WithIoFormat<Empty>, args: Vec<DiskInfo>) -> Result<(), Error> {
|
|
use prettytable::*;
|
|
|
|
if let Some(format) = params.format {
|
|
return display_serializable(format, args);
|
|
}
|
|
|
|
let mut table = Table::new();
|
|
table.add_row(row![bc =>
|
|
"LOGICALNAME",
|
|
"LABEL",
|
|
"CAPACITY",
|
|
"USED",
|
|
"STARTOS VERSION"
|
|
]);
|
|
for disk in args {
|
|
let row = row![
|
|
disk.logicalname.display(),
|
|
"N/A",
|
|
&format!("{:.2} GiB", disk.capacity as f64 / 1024.0 / 1024.0 / 1024.0),
|
|
"N/A",
|
|
"N/A",
|
|
];
|
|
table.add_row(row);
|
|
for part in disk.partitions {
|
|
let row = row![
|
|
part.logicalname.display(),
|
|
if let Some(label) = part.label.as_ref() {
|
|
label
|
|
} else {
|
|
"N/A"
|
|
},
|
|
part.capacity,
|
|
&if let Some(used) = part
|
|
.used
|
|
.map(|u| format!("{:.2} GiB", u as f64 / 1024.0 / 1024.0 / 1024.0))
|
|
{
|
|
used
|
|
} else {
|
|
"N/A".to_owned()
|
|
},
|
|
&if part.start_os.is_empty() {
|
|
"N/A".to_owned()
|
|
} else if part.start_os.len() == 1 {
|
|
part.start_os
|
|
.first_key_value()
|
|
.map(|(_, info)| info.version.to_string())
|
|
.unwrap()
|
|
} else {
|
|
part.start_os
|
|
.iter()
|
|
.map(|(id, info)| lazy_format!("{} ({})", info.version, id))
|
|
.join(", ")
|
|
},
|
|
];
|
|
table.add_row(row);
|
|
}
|
|
}
|
|
table.print_tty(false)?;
|
|
Ok(())
|
|
}
|
|
|
|
// #[command(display(display_disk_info))]
|
|
pub async fn list(ctx: RpcContext, _: Empty) -> Result<Vec<DiskInfo>, Error> {
|
|
crate::disk::util::list(&ctx.os_partitions).await
|
|
}
|
|
|
|
pub async fn repair() -> Result<(), Error> {
|
|
tokio::fs::write(REPAIR_DISK_PATH, b"").await?;
|
|
Ok(())
|
|
}
|