diff --git a/src/cmdline.rs b/src/cmdline.rs index ec3e955..e5f7172 100644 --- a/src/cmdline.rs +++ b/src/cmdline.rs @@ -1,12 +1,17 @@ // SPDX-FileCopyrightText: 2024 The rsinit Authors // SPDX-License-Identifier: GPL-2.0-only +use std::fmt::Debug; + use nix::mount::MsFlags; -use crate::util::{read_file, Result}; +use crate::{ + init::CmdlineCallback, + util::{read_file, Result}, +}; #[derive(Debug, PartialEq)] -pub struct CmdlineOptions { +pub struct CmdlineOptions<'a> { pub root: Option, pub rootfstype: Option, pub rootflags: Option, @@ -14,12 +19,30 @@ pub struct CmdlineOptions { pub nfsroot: Option, pub init: String, pub cleanup: bool, + callbacks: CmdlineOptionsCallbacks<'a>, +} + +#[derive(Default)] +struct CmdlineOptionsCallbacks<'a>(Vec>>); + +impl PartialEq for CmdlineOptionsCallbacks<'_> { + fn eq(&self, _other: &Self) -> bool { + true + } +} + +impl Debug for CmdlineOptionsCallbacks<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CmdlineOptionsCallbacks") + .field("callbacks_count", &self.0.len()) + .finish() + } } const SBIN_INIT: &str = "/sbin/init"; -impl Default for CmdlineOptions { - fn default() -> CmdlineOptions { +impl<'a> Default for CmdlineOptions<'a> { + fn default() -> CmdlineOptions<'a> { CmdlineOptions { root: None, rootfstype: None, @@ -28,15 +51,21 @@ impl Default for CmdlineOptions { nfsroot: None, init: SBIN_INIT.into(), cleanup: true, + callbacks: CmdlineOptionsCallbacks::default(), } } } -fn ensure_value<'a>(key: &str, value: Option<&'a str>) -> Result<&'a str> { +pub fn ensure_value<'a>(key: &str, value: Option<&'a str>) -> Result<&'a str> { value.ok_or(format!("Cmdline option '{key}' must have an argument!").into()) } -fn parse_option(key: &str, value: Option<&str>, options: &mut CmdlineOptions) -> Result<()> { +fn parse_option<'a>( + key: &str, + value: Option<&str>, + options: &mut CmdlineOptions, + callbacks: &mut [Box>], +) -> Result<()> { match key { "root" => options.root = Some(ensure_value(key, value)?.to_string()), "rootfstype" => options.rootfstype = Some(ensure_value(key, value)?.to_string()), @@ -45,7 +74,11 @@ fn parse_option(key: &str, value: Option<&str>, options: &mut CmdlineOptions) -> "rw" => options.rootfsflags.remove(MsFlags::MS_RDONLY), "nfsroot" => options.nfsroot = Some(ensure_value(key, value)?.to_string()), "init" => options.init = ensure_value(key, value)?.into(), - _ => (), + _ => { + for cb in callbacks { + cb(key, value)? + } + } } Ok(()) } @@ -91,8 +124,24 @@ fn parse_nfsroot(options: &mut CmdlineOptions) -> Result<()> { Ok(()) } -impl CmdlineOptions { - pub fn from_string(cmdline: &str) -> Result { +impl<'a> CmdlineOptions<'a> { + pub fn new() -> Self { + Self::default() + } + + pub fn new_with_callbacks(callbacks: Vec>>) -> Self { + Self { + callbacks: CmdlineOptionsCallbacks(callbacks), + ..Default::default() + } + } + + pub fn from_file(&mut self, path: &str) -> Result { + let cmdline = read_file(path)?; + self.from_string(&cmdline) + } + + pub fn from_string(&mut self, cmdline: &str) -> Result { let mut options = Self::default(); let mut have_value = false; let mut quoted = false; @@ -128,6 +177,7 @@ impl CmdlineOptions { None }, &mut options, + &mut self.callbacks.0, )?; } key = &cmdline[0..0]; @@ -150,15 +200,12 @@ impl CmdlineOptions { Ok(options) } - - pub fn from_file(filename: &str) -> Result { - let cmdline = read_file(filename)?; - Self::from_string(&cmdline) - } } #[cfg(test)] mod tests { + use std::cell::RefCell; + use super::*; #[test] @@ -171,7 +218,7 @@ mod tests { ..Default::default() }; - let options = CmdlineOptions::from_string(cmdline).expect("failed"); + let options = CmdlineOptions::new().from_string(cmdline).expect("failed"); assert_eq!(options, expected); } @@ -189,7 +236,7 @@ mod tests { ..Default::default() }; - let options = CmdlineOptions::from_string(cmdline).expect("failed"); + let options = CmdlineOptions::new().from_string(cmdline).expect("failed"); assert_eq!(options, expected); } @@ -206,7 +253,7 @@ mod tests { ..Default::default() }; - let options = CmdlineOptions::from_string(cmdline).expect("failed"); + let options = CmdlineOptions::new().from_string(cmdline).expect("failed"); assert_eq!(options, expected); } @@ -226,7 +273,7 @@ mod tests { ..Default::default() }; - let options = CmdlineOptions::from_string(cmdline).expect("failed"); + let options = CmdlineOptions::new().from_string(cmdline).expect("failed"); assert_eq!(options, expected); } @@ -241,8 +288,36 @@ mod tests { ..Default::default() }; - let options = CmdlineOptions::from_string(cmdline).expect("failed"); + let options = CmdlineOptions::new().from_string(cmdline).expect("failed"); + + assert_eq!(options, expected); + } + + #[test] + fn test_callbacks() { + let cmdline = "root=/dev/mmcblk0p1 rsinit.custom=xyz\n"; + let custom_value = RefCell::new(String::new()); + + let expected = CmdlineOptions { + root: Some("/dev/mmcblk0p1".into()), + ..Default::default() + }; + + let cb = Box::new(|key: &str, value: Option<&str>| { + match key { + "rsinit.custom" => { + *custom_value.borrow_mut() = ensure_value(key, value)?.to_owned(); + } + _ => {} + } + Result::Ok(()) + }); + + let options = CmdlineOptions::new_with_callbacks(vec![cb]) + .from_string(cmdline) + .expect("failed"); assert_eq!(options, expected); + assert_eq!(&*custom_value.borrow(), "xyz"); } } diff --git a/src/init.rs b/src/init.rs index 3c5955a..5a718c7 100644 --- a/src/init.rs +++ b/src/init.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: GPL-2.0-only use std::borrow::Borrow; -use std::env; use std::env::current_exe; use std::ffi::CString; use std::fmt::Write as _; @@ -12,6 +11,7 @@ use std::io::Write as _; use std::os::fd::AsFd; use std::os::unix::ffi::OsStrExt; use std::panic::set_hook; +use std::{env, mem}; use log::{debug, Level, LevelFilter, Metadata, Record}; #[cfg(feature = "reboot-on-failure")] @@ -86,12 +86,23 @@ fn finalize() { let _ = reboot(RebootMode::RB_AUTOBOOT); } -pub struct InitContext { - pub options: CmdlineOptions, +pub struct InitContext<'a> { + pub options: CmdlineOptions<'a>, + callbacks: InitContextCallbacks<'a>, } -impl InitContext { - pub fn new() -> Result { +pub type CmdlineCallback<'a> = dyn FnMut(&str, Option<&str>) -> Result<()> + 'a; +pub type InitCallback<'a> = dyn FnMut(&mut CmdlineOptions) -> Result<()> + 'a; + +#[derive(Default)] +pub struct InitContextCallbacks<'a> { + pub cmdline_cb: Vec>>, + pub post_setup_cb: Vec>>, + pub post_root_mount_cb: Vec>>, +} + +impl<'a> InitContext<'a> { + pub fn new(callbacks: Option>) -> Result { setup_console()?; set_hook(Box::new(|panic_info| { @@ -101,21 +112,39 @@ impl InitContext { Ok(Self { options: CmdlineOptions::default(), + callbacks: callbacks.unwrap_or_default(), }) } - pub fn setup(self: &mut InitContext) -> Result<()> { + pub fn add_cmdline_cb(self: &mut InitContext<'a>, cmdline_cb: Box>) { + self.callbacks.cmdline_cb.push(cmdline_cb); + } + + pub fn add_post_setup_cb(self: &mut InitContext<'a>, post_setup_cb: Box>) { + self.callbacks.post_setup_cb.push(post_setup_cb); + } + + pub fn add_post_root_mount_cb( + self: &mut InitContext<'a>, + post_root_mount_cb: Box>, + ) { + self.callbacks.post_root_mount_cb.push(post_root_mount_cb); + } + + pub fn setup(self: &mut InitContext<'a>) -> Result<()> { mount_special()?; setup_log()?; - self.options = CmdlineOptions::from_file("/proc/cmdline")?; + let callbacks = mem::take(&mut self.callbacks.cmdline_cb); + + self.options = CmdlineOptions::new_with_callbacks(callbacks).from_file("/proc/cmdline")?; Ok(()) } #[cfg(any(feature = "dmverity", feature = "usb9pfs"))] - pub fn prepare_aux(self: &mut InitContext) -> Result<()> { + pub fn prepare_aux(self: &mut InitContext<'a>) -> Result<()> { #[cfg(feature = "dmverity")] if prepare_dmverity(&mut self.options)? { return Ok(()); @@ -127,7 +156,7 @@ impl InitContext { Ok(()) } - pub fn switch_root(self: &mut InitContext) -> Result<()> { + pub fn switch_root(self: &mut InitContext<'a>) -> Result<()> { #[cfg(feature = "systemd")] mount_systemd(&mut self.options)?; @@ -144,7 +173,7 @@ impl InitContext { Ok(()) } - pub fn mount_root(self: &InitContext) -> Result<()> { + pub fn mount_root(self: &InitContext<'a>) -> Result<()> { mount_root( self.options.root.as_deref(), self.options.rootfstype.as_deref(), @@ -154,7 +183,7 @@ impl InitContext { Ok(()) } - pub fn start_init(self: &InitContext) -> Result<()> { + pub fn start_init(self: &InitContext<'a>) -> Result<()> { let mut args = Vec::new(); args.push(CString::new(self.options.init.as_str())?); @@ -174,28 +203,36 @@ impl InitContext { Ok(()) } - pub fn finish(self: &mut InitContext) -> Result<()> { + pub fn finish(self: &mut InitContext<'a>) -> Result<()> { self.switch_root()?; self.start_init()?; Ok(()) } - pub fn run(self: &mut InitContext) -> Result<()> { + pub fn run(self: &mut InitContext<'a>) -> Result<()> { self.setup()?; + for cb in &mut self.callbacks.post_setup_cb { + cb(&mut self.options)?; + } + #[cfg(any(feature = "dmverity", feature = "usb9pfs"))] self.prepare_aux()?; self.mount_root()?; + for cb in &mut self.callbacks.post_root_mount_cb { + cb(&mut self.options)?; + } + self.finish()?; Ok(()) } } -impl Drop for InitContext { +impl Drop for InitContext<'_> { fn drop(&mut self) { finalize(); } diff --git a/src/main.rs b/src/main.rs index 5dca74d..be76ef3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,17 +1,37 @@ // SPDX-FileCopyrightText: 2024 The rsinit Authors // SPDX-License-Identifier: GPL-2.0-only -use std::env; +use std::net::IpAddr; +use std::{cell::RefCell, env}; extern crate rsinit; -use rsinit::init::InitContext; +use log::info; +use nix::mount::MsFlags; +use rsinit::cmdline::CmdlineOptions; +use rsinit::mount::do_mount; #[cfg(feature = "systemd")] use rsinit::systemd::shutdown; use rsinit::util::Result; +use rsinit::{ + cmdline::ensure_value, + init::{InitContext, InitContextCallbacks}, +}; fn main() -> Result<()> { - let mut init = InitContext::new()?; + // This object needs to be alive as long as the InitContext is alive! The RefCell allows us to + // handout multiple mutable references in the callbacks. + let mount_args = RefCell::new(MountArgs::default()); + + let callbacks = InitContextCallbacks { + cmdline_cb: vec![Box::new(|key, value| { + mount_args.borrow_mut().parse_cmdline(key, value) + })], + post_setup_cb: vec![], + post_root_mount_cb: vec![Box::new(|ctx| mount_args.borrow().do_mounts(ctx))], + }; + + let mut ctx = InitContext::new(Some(callbacks))?; let cmd = env::args().next().ok_or("No cmd to run as found")?; println!("Running {cmd}..."); @@ -19,9 +39,146 @@ fn main() -> Result<()> { if let Err(e) = match cmd.as_str() { #[cfg(feature = "systemd")] "/shutdown" => shutdown(), - _ => init.run(), + _ => ctx.run(), } { println!("{e}"); } + Ok(()) } + +#[derive(Debug, PartialEq)] + +struct MountOption { + source: String, + destination: String, + options: String, +} + +#[derive(Default, Debug, PartialEq)] +struct MountArgs { + bind: Vec, + nfs: Vec, +} + +impl MountArgs { + fn parse_cmdline(&mut self, key: &str, value: Option<&str>) -> Result<()> { + match key { + "rsinit.bind" => { + let val = ensure_value(key, value)?; + + let (src, dst) = val.split_once(',').ok_or(format!( + "Bind mount option must be in the format ',', got: {val}" + ))?; + + self.bind.push(MountOption { + source: src.to_string(), + destination: dst.to_string(), + options: String::new(), + }); + } + "rsinit.nfs" => { + let val = ensure_value(key, value)?; + + let (src, dst) = val.split_once(',').ok_or(format!( + "NFS mount option must be in the format ':,', got: {val}" + ))?; + + let (host, _) = src + .split_once(':') + .ok_or("NFS source must be in the format ':'")?; + + host.parse::().map_err(|_| { + "NFS host must be a valid IP address as DNS lookup is not supported (yet)" + })?; + + self.nfs.push(MountOption { + source: src.to_string(), + destination: dst.to_string(), + options: format!("addr={host},vers=3,proto=tcp,nolock"), + }); + } + _ => {} + } + Ok(()) + } + + fn do_mounts(&self, _: &mut CmdlineOptions) -> Result<()> { + for MountOption { + source, + destination, + options, + } in &self.nfs + { + info!("NFS mounting {source} to {destination} with options {options}"); + + do_mount( + Some(source), + &destination, + Some("nfs"), + MsFlags::empty(), + Some(options), + ).inspect_err(|_|{ + info!("Failed to NFS mount {source} to {destination}"); + info!("In case of ENETUNREACH or ENETDOWN ensure that an IP address is assigned to the network interface."); + info!("Via DHCP this can be done by adding 'ip=::::::dhcp' e.g. 'ip=:::::eth0:dhcp' to the kernel command-line."); + info!("Good luck next time!"); + })?; + } + + for MountOption { + source, + destination, + options: _, + } in &self.bind + { + info!("Bind mounting {source} to {destination}"); + + do_mount(Some(source), &destination, None, MsFlags::MS_BIND, None)?; + } + + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_bind_args() { + let mut args = MountArgs::default(); + + args.parse_cmdline("rsinit.bind", Some("/lib/modules,/root/lib/modules")) + .unwrap(); + + assert_eq!( + args.bind, + &[MountOption { + source: "/lib/modules".to_string(), + destination: "/root/lib/modules".to_string(), + options: String::new(), + }] + ); + } + + #[test] + fn test_nfs_args() { + let mut args = MountArgs::default(); + + args.parse_cmdline( + "rsinit.nfs", + Some("192.168.0.1:/path/lib/modules,/lib/modules"), + ) + .unwrap(); + + assert_eq!( + args.nfs[0], + MountOption { + source: "192.168.0.1:/path/lib/modules".to_string(), + destination: "/lib/modules".to_string(), + options: "addr=192.168.0.1,vers=3,proto=tcp,nolock".to_string(), + } + ); + } +}