diff --git a/appmgr/embassy-init.service b/appmgr/embassy-init.service index 7c8825116..ac90e5776 100644 --- a/appmgr/embassy-init.service +++ b/appmgr/embassy-init.service @@ -11,4 +11,4 @@ ExecStart=/usr/local/bin/embassy-init RemainAfterExit=true [Install] -WantedBy=embassyd.service \ No newline at end of file +WantedBy=embassyd.service diff --git a/appmgr/src/disk/mod.rs b/appmgr/src/disk/mod.rs index 00e32b5c1..3483360e7 100644 --- a/appmgr/src/disk/mod.rs +++ b/appmgr/src/disk/mod.rs @@ -14,8 +14,11 @@ use crate::util::{display_serializable, IoFormat, Version}; use crate::Error; pub mod main; +pub mod quirks; pub mod util; +pub const BOOT_RW_PATH: &'static str = "/media/boot-rw"; + #[command(subcommands(list, backup_info))] pub fn disk() -> Result<(), Error> { Ok(()) diff --git a/appmgr/src/disk/quirks.rs b/appmgr/src/disk/quirks.rs new file mode 100644 index 000000000..eed27c79f --- /dev/null +++ b/appmgr/src/disk/quirks.rs @@ -0,0 +1,155 @@ +use std::num::ParseIntError; +use std::path::Path; +use std::time::Duration; + +use color_eyre::eyre::eyre; +use tokio::io::AsyncWriteExt; +use tracing::instrument; + +use super::BOOT_RW_PATH; +use crate::util::AtomicFile; +use crate::Error; + +pub const QUIRK_PATH: &'static str = "/sys/module/usb_storage/parameters/quirks"; + +pub const WHITELIST: [(VendorId, ProductId); 4] = [ + (VendorId(0x1d6b), ProductId(0x0002)), // root hub usb2 + (VendorId(0x1d6b), ProductId(0x0003)), // root hub usb3 + (VendorId(0x2109), ProductId(0x3431)), + (VendorId(0x1058), ProductId(0x262f)), // western digital black HDD +]; + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct VendorId(u16); +impl std::str::FromStr for VendorId { + type Err = ParseIntError; + fn from_str(s: &str) -> Result { + u16::from_str_radix(s.trim(), 16).map(VendorId) + } +} +impl std::fmt::Display for VendorId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:04x}", self.0) + } +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct ProductId(u16); +impl std::str::FromStr for ProductId { + type Err = ParseIntError; + fn from_str(s: &str) -> Result { + u16::from_str_radix(s.trim(), 16).map(ProductId) + } +} +impl std::fmt::Display for ProductId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:04x}", self.0) + } +} + +#[derive(Clone, Debug)] +pub struct Quirks(Vec<(VendorId, ProductId)>); +impl Quirks { + pub fn add(&mut self, vendor: VendorId, product: ProductId) { + self.0.push((vendor, product)); + } + pub fn contains(&self, vendor: VendorId, product: ProductId) -> bool { + self.0.contains(&(vendor, product)) + } +} +impl std::fmt::Display for Quirks { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut comma = false; + for (vendor, product) in &self.0 { + if comma { + write!(f, ",")?; + } else { + comma = true; + } + write!(f, "{}:{}:u", vendor, product)?; + } + Ok(()) + } +} +impl std::str::FromStr for Quirks { + type Err = Error; + fn from_str(s: &str) -> Result { + let s = s.trim(); + let mut quirks = Vec::new(); + for item in s.split(",") { + if let [vendor, product, "u"] = item.splitn(3, ":").collect::>().as_slice() { + quirks.push((vendor.parse()?, product.parse()?)); + } else { + return Err(Error::new( + eyre!("Invalid quirk: `{}`", item), + crate::ErrorKind::DiskManagement, + )); + } + } + Ok(Quirks(quirks)) + } +} + +#[instrument] +pub async fn update_quirks(quirks: &mut Quirks) -> Result<(), Error> { + let mut usb_devices = tokio::fs::read_dir("/sys/bus/usb/devices/").await?; + while let Some(usb_device) = usb_devices.next_entry().await? { + if tokio::fs::metadata(usb_device.path().join("idVendor")) + .await + .is_err() + { + continue; + } + let vendor = tokio::fs::read_to_string(usb_device.path().join("idVendor")) + .await? + .parse()?; + let product = tokio::fs::read_to_string(usb_device.path().join("idProduct")) + .await? + .parse()?; + if WHITELIST.contains(&(vendor, product)) || quirks.contains(vendor, product) { + continue; + } + quirks.add(vendor, product); + tokio::fs::write(QUIRK_PATH, quirks.to_string()).await?; + + reconnect_usb(usb_device.path()).await?; + } + Ok(()) +} + +#[instrument(skip(usb_device_path))] +pub async fn reconnect_usb(usb_device_path: impl AsRef) -> Result<(), Error> { + let authorized_path = usb_device_path.as_ref().join("authorized"); + let mut authorized_file = tokio::fs::File::create(&authorized_path).await?; + authorized_file.write_all(b"0").await?; + authorized_file.sync_all().await?; + drop(authorized_file); + tokio::time::sleep(Duration::from_secs(1)).await; + let mut authorized_file = tokio::fs::File::create(&authorized_path).await?; + authorized_file.write_all(b"1").await?; + authorized_file.sync_all().await?; + tokio::time::sleep(Duration::from_secs(1)).await; + Ok(()) +} + +#[instrument] +pub async fn fetch_quirks() -> Result { + Ok(tokio::fs::read_to_string(QUIRK_PATH).await?.parse()?) +} + +#[instrument] +pub async fn save_quirks(quirks: &Quirks) -> Result<(), Error> { + let orig_path = Path::new(BOOT_RW_PATH).join("cmdline.txt.orig"); + let target_path = Path::new(BOOT_RW_PATH).join("cmdline.txt"); + if tokio::fs::metadata(&orig_path).await.is_err() { + tokio::fs::copy(&target_path, &orig_path).await?; + } + let cmdline = tokio::fs::read_to_string(&orig_path).await?; + let mut target = AtomicFile::new(&target_path).await?; + target + .write_all(format!("usb-storage.quirks={} {}", quirks, cmdline).as_bytes()) + .await?; + target.save().await?; + + Ok(()) +} diff --git a/appmgr/src/disk/util.rs b/appmgr/src/disk/util.rs index ffc4ce878..affa1a255 100644 --- a/appmgr/src/disk/util.rs +++ b/appmgr/src/disk/util.rs @@ -22,6 +22,7 @@ use tokio::process::Command; use tokio::sync::Mutex; use tracing::instrument; +use super::quirks::{fetch_quirks, save_quirks, update_quirks}; use super::BackupInfo; use crate::auth::check_password; use crate::middleware::encrypt::{decrypt_slice, encrypt_slice}; @@ -206,6 +207,9 @@ pub async fn pvscan() -> Result>, Error> { #[instrument] pub async fn list() -> Result, Error> { + let mut quirks = fetch_quirks().await?; + update_quirks(&mut quirks).await?; + save_quirks(&mut quirks).await?; let disk_guids = pvscan().await?; let disks = tokio_stream::wrappers::ReadDirStream::new( tokio::fs::read_dir(DISK_PATH) diff --git a/build/fstab b/build/fstab new file mode 100644 index 000000000..53d09249f --- /dev/null +++ b/build/fstab @@ -0,0 +1,3 @@ +LABEL=green / ext4 discard,errors=remount-ro 0 1 +LABEL=system-boot /media/boot-rw vfat defaults 0 1 +/media/boot-rw /boot/firmware none defaults,bind,ro 0 0 diff --git a/build/initialization.sh b/build/initialization.sh index b284590c6..ef4afbda0 100755 --- a/build/initialization.sh +++ b/build/initialization.sh @@ -47,7 +47,7 @@ ControlPort 9051 CookieAuthentication 1 EOF -echo 'overlayroot="tmpfs"' > /etc/overlayroot.local.conf +echo 'overlayroot="tmpfs":swap=1,recurse=0' > /etc/overlayroot.local.conf systemctl disable initialization.service sync reboot diff --git a/build/write-image.sh b/build/write-image.sh index a96078d61..06f847c7f 100755 --- a/build/write-image.sh +++ b/build/write-image.sh @@ -21,8 +21,11 @@ sudo e2label ${OUTPUT_DEVICE}p4 blue mkdir -p /tmp/eos-mnt sudo mount ${OUTPUT_DEVICE}p1 /tmp/eos-mnt -sudo sed -i 's/^/usb-storage.quirks=152d:0562:u /g' /tmp/eos-mnt/cmdline.txt sudo sed -i 's/LABEL=writable/LABEL=green/g' /tmp/eos-mnt/cmdline.txt +# create a copy of the cmdline *without* the quirk string, so that it can be easily amended +sudo cp /tmp/eos-mnt/cmdline.txt /tmp/eos-mnt/cmdline.txt.orig +sudo sed -i 's/^/usb-storage.quirks=152d:0562:u /g' /tmp/eos-mnt/cmdline.txt + cat /tmp/eos-mnt/config.txt | grep -v "dtoverlay=" | sudo tee /tmp/eos-mnt/config.txt.tmp echo "dtoverlay=pwm-2chan" | sudo tee -a /tmp/eos-mnt/config.txt.tmp sudo mv /tmp/eos-mnt/config.txt.tmp /tmp/eos-mnt/config.txt @@ -35,8 +38,8 @@ sudo umount /tmp/eos-mnt sudo mount ${OUTPUT_DEVICE}p3 /tmp/eos-mnt -sudo sed -i 's/LABEL=writable/LABEL=green/g' /tmp/eos-mnt/etc/fstab -sudo sed -i 's/LABEL=system-boot\(\s\+\S\+\s\+\S\+\s\+\)defaults/LABEL=system-boot\1defaults,ro/g' /tmp/eos-mnt/etc/fstab +sudo mkdir /tmp/eos-mnt/media/boot-rw +sudo cp build/fstab /tmp/eos-mnt/etc/fstab # Enter the appmgr directory, copy over the built EmbassyOS binaries and systemd services, edit the nginx config, then create the .ssh directory cd appmgr/