diff --git a/backend/src/bin/embassy-init.rs b/backend/src/bin/embassy-init.rs index 07f2832de..6e666ff96 100644 --- a/backend/src/bin/embassy-init.rs +++ b/backend/src/bin/embassy-init.rs @@ -4,7 +4,9 @@ use std::time::Duration; use embassy::context::rpc::RpcContextConfig; use embassy::context::{DiagnosticContext, SetupContext}; +use embassy::disk::fsck::RepairStrategy; use embassy::disk::main::DEFAULT_PASSWORD; +use embassy::disk::REPAIR_DISK_PATH; use embassy::hostname::get_product_key; use embassy::middleware::cors::cors; use embassy::middleware::diagnostic::diagnostic; @@ -79,9 +81,17 @@ async fn setup_or_init(cfg_path: Option<&str>) -> Result<(), Error> { .await? .trim(), cfg.datadir(), + if tokio::fs::metadata(REPAIR_DISK_PATH).await.is_ok() { + RepairStrategy::Aggressive + } else { + RepairStrategy::Preen + }, DEFAULT_PASSWORD, ) .await?; + tokio::fs::remove_file(REPAIR_DISK_PATH) + .await + .with_ctx(|_| (embassy::ErrorKind::Filesystem, REPAIR_DISK_PATH))?; tracing::info!("Loaded Disk"); embassy::init::init(&cfg, &get_product_key().await?).await?; } diff --git a/backend/src/diagnostic.rs b/backend/src/diagnostic.rs index 0564fd7ac..50f4e697a 100644 --- a/backend/src/diagnostic.rs +++ b/backend/src/diagnostic.rs @@ -5,6 +5,7 @@ use rpc_toolkit::command; use rpc_toolkit::yajrc::RpcError; use crate::context::DiagnosticContext; +use crate::disk::repair; use crate::logs::{display_logs, fetch_logs, LogResponse, LogSource}; use crate::shutdown::Shutdown; use crate::util::display_none; @@ -12,7 +13,7 @@ use crate::Error; pub const SYSTEMD_UNIT: &'static str = "embassy-init"; -#[command(subcommands(error, logs, exit, restart, forget_disk))] +#[command(subcommands(error, logs, exit, restart, forget_disk, disk))] pub fn diagnostic() -> Result<(), Error> { Ok(()) } @@ -56,7 +57,12 @@ pub fn restart(#[context] ctx: DiagnosticContext) -> Result<(), Error> { Ok(()) } -#[command(rename = "forget-disk", display(display_none))] +#[command(subcommands(forget_disk, repair))] +pub fn disk() -> Result<(), Error> { + Ok(()) +} + +#[command(rename = "forget", display(display_none))] pub async fn forget_disk() -> Result<(), Error> { let disk_guid = Path::new("/embassy-os/disk.guid"); if tokio::fs::metadata(disk_guid).await.is_ok() { diff --git a/backend/src/disk/fsck.rs b/backend/src/disk/fsck.rs new file mode 100644 index 000000000..024adcb3a --- /dev/null +++ b/backend/src/disk/fsck.rs @@ -0,0 +1,100 @@ +use std::ffi::OsStr; +use std::path::Path; + +use color_eyre::eyre::eyre; +use tokio::process::Command; +use tracing::instrument; + +use crate::{Error, ResultExt}; + +#[derive(Debug, Clone, Copy)] +pub struct RequiresReboot(pub bool); +impl std::ops::BitOrAssign for RequiresReboot { + fn bitor_assign(&mut self, rhs: Self) { + self.0 |= rhs.0 + } +} + +#[derive(Debug, Clone, Copy)] +pub enum RepairStrategy { + Preen, + Aggressive, +} +impl RepairStrategy { + pub async fn e2fsck( + &self, + logicalname: impl AsRef + std::fmt::Debug, + ) -> Result { + match self { + RepairStrategy::Preen => e2fsck_preen(logicalname).await, + RepairStrategy::Aggressive => e2fsck_aggressive(logicalname).await, + } + } +} + +#[instrument] +pub async fn e2fsck_preen( + logicalname: impl AsRef + std::fmt::Debug, +) -> Result { + e2fsck_runner(Command::new("e2fsck").arg("-p"), logicalname).await +} + +#[instrument] +pub async fn e2fsck_aggressive( + logicalname: impl AsRef + std::fmt::Debug, +) -> Result { + e2fsck_runner( + Command::new("e2fsck").arg("-y").arg("-z").arg( + Path::new("/embassy-os") + .join( + logicalname + .as_ref() + .file_name() + .unwrap_or(OsStr::new("unknown")), + ) + .with_extension("e2undo"), + ), + logicalname, + ) + .await +} + +async fn e2fsck_runner( + e2fsck_cmd: &mut Command, + logicalname: impl AsRef + std::fmt::Debug, +) -> Result { + let e2fsck_out = e2fsck_cmd.arg(logicalname.as_ref()).output().await?; + let e2fsck_stderr = String::from_utf8(e2fsck_out.stderr)?; + let code = e2fsck_out.status.code().ok_or_else(|| { + Error::new( + eyre!("e2fsck: process terminated by signal"), + crate::ErrorKind::DiskManagement, + ) + })?; + if code & 4 != 0 { + tracing::error!( + "some filesystem errors NOT corrected on {}:\n{}", + logicalname.as_ref().display(), + e2fsck_stderr, + ); + } else if code & 1 != 0 { + tracing::warn!( + "filesystem errors corrected on {}:\n{}", + logicalname.as_ref().display(), + e2fsck_stderr, + ); + } + if code < 8 { + if code & 2 != 0 { + tracing::warn!("reboot required"); + Ok(RequiresReboot(true)) + } else { + Ok(RequiresReboot(false)) + } + } else { + Err(Error::new( + eyre!("e2fsck: {}", e2fsck_stderr), + crate::ErrorKind::DiskManagement, + )) + } +} diff --git a/backend/src/disk/main.rs b/backend/src/disk/main.rs index 3f05e4258..65fe18e02 100644 --- a/backend/src/disk/main.rs +++ b/backend/src/disk/main.rs @@ -5,6 +5,7 @@ use color_eyre::eyre::eyre; use tokio::process::Command; use tracing::instrument; +use super::fsck::{RepairStrategy, RequiresReboot}; use super::util::pvscan; use crate::disk::mount::filesystem::block_dev::mount; use crate::disk::mount::filesystem::ReadWrite; @@ -196,7 +197,12 @@ pub async fn export>(guid: &str, datadir: P) -> Result<(), Error> } #[instrument(skip(datadir, password))] -pub async fn import>(guid: &str, datadir: P, password: &str) -> Result<(), Error> { +pub async fn import>( + guid: &str, + datadir: P, + repair: RepairStrategy, + password: &str, +) -> Result<(), Error> { let scan = pvscan().await?; if scan .values() @@ -244,7 +250,7 @@ pub async fn import>(guid: &str, datadir: P, password: &str) -> R .arg(guid) .invoke(crate::ErrorKind::DiskManagement) .await?; - mount_all_fs(guid, datadir, password).await?; + mount_all_fs(guid, datadir, repair, password).await?; Ok(()) } @@ -253,8 +259,9 @@ pub async fn mount_fs>( guid: &str, datadir: P, name: &str, + repair: RepairStrategy, password: &str, -) -> Result<(), Error> { +) -> Result { tokio::fs::write(PASSWORD_PATH, password) .await .with_ctx(|_| (crate::ErrorKind::Filesystem, PASSWORD_PATH))?; @@ -267,27 +274,26 @@ pub async fn mount_fs>( .arg(format!("{}_{}", guid, name)) .invoke(crate::ErrorKind::DiskManagement) .await?; - mount( - Path::new("/dev/mapper").join(format!("{}_{}", guid, name)), - datadir.as_ref().join(name), - ReadWrite, - ) - .await?; + let mapper_path = Path::new("/dev/mapper").join(format!("{}_{}", guid, name)); + let reboot = repair.e2fsck(&mapper_path).await?; + mount(&mapper_path, datadir.as_ref().join(name), ReadWrite).await?; tokio::fs::remove_file(PASSWORD_PATH) .await .with_ctx(|_| (crate::ErrorKind::Filesystem, PASSWORD_PATH))?; - Ok(()) + Ok(reboot) } #[instrument(skip(datadir, password))] pub async fn mount_all_fs>( guid: &str, datadir: P, + repair: RepairStrategy, password: &str, -) -> Result<(), Error> { - mount_fs(guid, &datadir, "main", password).await?; - mount_fs(guid, &datadir, "package-data", password).await?; - Ok(()) +) -> Result { + let mut reboot = RequiresReboot(false); + reboot |= mount_fs(guid, &datadir, "main", repair, password).await?; + reboot |= mount_fs(guid, &datadir, "package-data", repair, password).await?; + Ok(reboot) } diff --git a/backend/src/disk/mod.rs b/backend/src/disk/mod.rs index 174a058e0..2dab5c467 100644 --- a/backend/src/disk/mod.rs +++ b/backend/src/disk/mod.rs @@ -2,17 +2,20 @@ use clap::ArgMatches; use rpc_toolkit::command; use self::util::DiskListResponse; +use crate::util::display_none; use crate::util::serde::{display_serializable, IoFormat}; use crate::Error; +pub mod fsck; pub mod main; pub mod mount; pub mod quirks; pub mod util; -pub const BOOT_RW_PATH: &'static str = "/media/boot-rw"; +pub const BOOT_RW_PATH: &str = "/media/boot-rw"; +pub const REPAIR_DISK_PATH: &str = "/embassy-os/repair-disk"; -#[command(subcommands(list))] +#[command(subcommands(list, repair))] pub fn disk() -> Result<(), Error> { Ok(()) } @@ -79,3 +82,9 @@ pub async fn list( ) -> Result { crate::disk::util::list().await } + +#[command(display(display_none))] +pub async fn repair() -> Result<(), Error> { + tokio::fs::write(REPAIR_DISK_PATH, b"").await?; + Ok(()) +} diff --git a/backend/src/setup.rs b/backend/src/setup.rs index f330664dc..1d5402c99 100644 --- a/backend/src/setup.rs +++ b/backend/src/setup.rs @@ -28,6 +28,7 @@ use crate::context::rpc::RpcContextConfig; use crate::context::setup::SetupResult; use crate::context::SetupContext; use crate::db::model::RecoveredPackageInfo; +use crate::disk::fsck::RepairStrategy; use crate::disk::main::DEFAULT_PASSWORD; use crate::disk::mount::filesystem::block_dev::BlockDev; use crate::disk::mount::filesystem::cifs::Cifs; @@ -96,7 +97,13 @@ pub async fn attach( #[context] ctx: SetupContext, #[arg] guid: Arc, ) -> Result { - crate::disk::main::import(&*guid, &ctx.datadir, DEFAULT_PASSWORD).await?; + crate::disk::main::import( + &*guid, + &ctx.datadir, + RepairStrategy::Preen, + DEFAULT_PASSWORD, + ) + .await?; let product_key_path = Path::new("/embassy-data/main/product_key.txt"); if tokio::fs::metadata(product_key_path).await.is_ok() { let pkey = tokio::fs::read_to_string(product_key_path).await?; @@ -301,7 +308,13 @@ pub async fn execute_inner( ) .await?, ); - crate::disk::main::import(&*guid, &ctx.datadir, DEFAULT_PASSWORD).await?; + crate::disk::main::import( + &*guid, + &ctx.datadir, + RepairStrategy::Preen, + DEFAULT_PASSWORD, + ) + .await?; let res = if let Some(recovery_source) = recovery_source { let (tor_addr, root_ca, recover_fut) = recover(