diff --git a/Cargo.lock b/Cargo.lock index 7ef7da1..efee12c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -25,9 +25,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "bitflags" -version = "2.13.1" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" [[package]] name = "by_address" @@ -46,9 +46,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" [[package]] name = "cfg_aliases" @@ -72,9 +72,9 @@ dependencies = [ [[package]] name = "darling" -version = "0.24.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88490bf1b990d87eaaa7ac8aa887f629a08e7359765b4911faf63c3763347d23" +checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec" dependencies = [ "darling_core", "darling_macro", @@ -82,26 +82,26 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.24.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "084e274f91c482280130e1e34e0b8d6e66776a060d7b6de7b84289ca778868c4" +checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff" dependencies = [ "ident_case", "proc-macro2", "quote", "strsim", - "syn 3.0.3", + "syn 3.0.5", ] [[package]] name = "darling_macro" -version = "0.24.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f5792fa0d41cd2325ce0ffa64f0a340eaebd4971a3a0c5e1ffd2cc488a355e" +checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" dependencies = [ "darling_core", "quote", - "syn 3.0.3", + "syn 3.0.5", ] [[package]] @@ -112,9 +112,9 @@ checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" [[package]] name = "either" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" [[package]] name = "equivalent" @@ -181,7 +181,7 @@ dependencies = [ "indoc", "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.5", ] [[package]] @@ -233,13 +233,19 @@ dependencies = [ [[package]] name = "lru" -version = "0.18.2" +version = "0.18.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d2f2f9b4ba7e6b24d95e7e899329d35be83bcded72c8540cdd5368932d1d90a" +checksum = "ff9840bcc50b71349309900da0ce7279aa336ae71d73250b07998932c7d97c25" dependencies = [ "hashbrown 0.17.1", ] +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + [[package]] name = "nix" version = "0.31.3" @@ -290,6 +296,8 @@ dependencies = [ "ratatui", "ratatui-textarea", "regex-lite", + "serde", + "serde_json", "termion", ] @@ -479,7 +487,20 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.5", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", ] [[package]] @@ -528,9 +549,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.3" +version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" dependencies = [ "proc-macro2", "quote", @@ -564,7 +585,7 @@ checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.5", ] [[package]] @@ -590,9 +611,9 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "unicode-ident" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +checksum = "ab72a15cf68d77cb0987d3684aa8a45c5ef827e8cb49ee2f30bfd7ba2feb519f" [[package]] name = "unicode-segmentation" @@ -616,3 +637,9 @@ name = "unicode-width" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml index 99217d9..f772d27 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,8 @@ version = "0.2.1" edition = "2024" [dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1.0" nix = { version = "0.31", features = ["mount"] } ratatui = { version = "0.30", default-features = false, features = ["termion"] } ratatui-textarea = { version = "0.9", default-features = false, features = ["termion"] } @@ -17,3 +19,6 @@ incremental = false codegen-units = 1 opt-level = "s" strip = true + +[registry] +global-min-publish-age = "14 days" \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index bc6785b..ca62f76 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,18 +18,19 @@ use ratatui::{Frame, Terminal, backend::TermionBackend}; use ratatui::{ layout::{Constraint, Layout}, style::{Color, Style, Stylize}, - text::Line, - widgets::{Block, Borders, List, ListState, Paragraph}, + text::{Line, Span}, + widgets::{Block, Borders, Gauge, List, ListItem, ListState, Padding, Paragraph, Wrap}, }; use ratatui_textarea::TextArea; -use std::io::{self, Read, stdout}; +use std::io::{self, stdout}; use std::process::ExitCode; use std::thread; use std::time::Duration; -use termion::event::{self, Event}; +use termion::event::Key; +use termion::input::TermRead; use termion::raw::IntoRawMode; mod nyan_system; @@ -37,15 +38,23 @@ mod nyan_system; use nyan_system::ExecCommandError; const BEE_CAT: &str = include_str!("bee_cat.txt"); +const SNAP_CHOOSE_LIST_CAPTION: &str = + "Select snapshot to rollback or \"bzz current\" to boot current system state"; + +const CONFIRM_TEXT1: &str = "You going to roll back system statate to "; +const CONFIRM_TEXT2: &str = "This action cannot be undone!"; + +const TICK_TIME_MILLS: u64 = 27u64; +const WAIT_FOR_F2_SEC: u64 = 3u64; //const ROOT_PARTLABEL: &str = "bee-root"; const ROOT_PARTLABEL: &str = "bee-root"; -const TMP_REAL_ROOT: &str = "/mnt/real-bee"; -const TMP_REAL_ROOT_SNAPS: &str = "/mnt/real-bee/bzz-snaps"; -const TMP_ROOT: &str = "/mnt/real-bee/bee-root"; -const TMP_ROOT_SNAPS: &str = "/mnt/real-bee/bee-root/bzz-snaps"; +const TMP_ROOT: &str = "/mnt/bee-root"; +const TMP_ROOT_SNAPS: &str = "/mnt/bee-root/bzz-snaps"; const SYS_INIT: &str = "/sbin/init"; +const CURRENT_BOOT_LIST_TEXT: &str = "bzz current"; + // fn bee_root_is_present() -> bool { // nyan_system::list_gpt_labeled_devs().iter().filter(|s| s.ends_with(ROOT_PARTLABEL)).count() > 0 // } @@ -65,57 +74,57 @@ const SYS_INIT: &str = "/sbin/init"; // ExitCode::SUCCESS // } +#[derive(Debug, PartialEq)] +struct RollbackState { + progress: f64, + progress_text: String, + snapshot_date_time: String, +} +impl RollbackState { + // Constructor function + pub fn new(snap_date_time: String) -> Self { + RollbackState { + progress: 0.0, + progress_text: "".into(), + snapshot_date_time: snap_date_time, + } + } +} + #[derive(Debug, PartialEq)] enum NyanitState { Init, - WaitForF2(Vec), - ChooseSnapshot(Vec), + RollBack(RollbackState), + ConfirmRollback(String), + WaitForF2, + /// list of snapshoots + ChooseSnapshot, EnterPassword, Error(String), - HandOff(String), + HandOff, } -/// Mounting bee-root and chiking if there any snapshots -fn mount_and_check_snaps(bee_root_dev: &str) -> Result { - // mount bee-root - nyan_system::mount_bee_root(bee_root_dev, TMP_REAL_ROOT)?; +fn gen_conf_par<'a>(snap_dt: String) -> Paragraph<'a> { + let first_line = Line::from(vec![Span::raw(CONFIRM_TEXT1), Span::raw(snap_dt).bold()]); + let confirm_line = Line::from(vec![ + Span::raw("Press ("), + Span::raw("Y").bold(), + Span::raw(") to confirm or ("), + Span::raw("N").bold(), + Span::raw(") to decline."), + ]); - // let root_list = nyan_system::list_dir(TMP_ROOT)?; - // return Err(ExecCommandError::Other(root_list)); - - // list snaps if any - let mut snaps_list = nyan_system::list_bzzpss_snaps(TMP_REAL_ROOT_SNAPS)?; - // if any snaps detected whait for user to press F2 - if snaps_list.len() > 0 { - snaps_list.insert(0, TMP_ROOT.to_string()); - return Ok(NyanitState::WaitForF2(snaps_list)); - } - - // else tell to finish boot - Ok(NyanitState::HandOff(TMP_ROOT.into())) -} - -fn init_stage() -> Result<(String, NyanitState), ExecCommandError> { - // mount sysfss - nyan_system::prepare_env().map_err(|ex| format!("prepare_env error: {}", ex))?; - - // get bee-root device name aka /dev/sda1 - // trying for max 3 sec - let bee_root_dev = nyan_system::try_find_partlabel(ROOT_PARTLABEL, 3) - .ok_or_else(|| "Timed out trying to find dev by partlabel.".to_string())?; - - // works until this point!!!! - // check if device is encrypted - if nyan_system::bzzpss_dev_encrypted(&bee_root_dev) - .map_err(|ex| format!("Error checking if device is encrypted: {}", ex))? - { - // if encrypted return EnterPassword so used get promted for pass - return Ok((bee_root_dev, NyanitState::EnterPassword)); - } - - // if unencrypted - let check_res = mount_and_check_snaps(&bee_root_dev)?; - Ok((bee_root_dev, check_res)) + let conf_lines: Vec = vec![ + first_line, + Line::default(), + Line::from(CONFIRM_TEXT2).bold(), + Line::default(), + confirm_line, + ]; + Paragraph::new(conf_lines) + .centered() + .wrap(Wrap::default()) + .light_red() } struct NyanitTUI<'a> { @@ -123,8 +132,12 @@ struct NyanitTUI<'a> { state: NyanitState, bee_root_dev: String, list_state: ListState, - time_counter: u32, + /// time counter manly for F2 waiting in mills + time_counter: u64, + /// time counter manly for F2 waiting in seconds + time_counter_sec: u64, textarea: Option>, + snaps_list: Vec, } impl NyanitTUI<'_> { @@ -135,17 +148,70 @@ impl NyanitTUI<'_> { bee_root_dev: String::new(), list_state: ListState::default().with_selected(Some(0)), time_counter: 0, + time_counter_sec: 0, textarea: None, + snaps_list: Vec::new(), }) } + /// Mounting bee-root and chiking if there any snapshots + fn mount_and_check_snaps(&mut self) -> Result { + // mount bee-root + nyan_system::mount_bee_root(&self.bee_root_dev, TMP_ROOT)?; + + // let root_list = nyan_system::list_dir(TMP_ROOT)?; + // return Err(ExecCommandError::Other(root_list)); + + // list snaps if any + self.snaps_list = nyan_system::list_bzzpss_snaps(TMP_ROOT_SNAPS)?; + + // if any snaps detected check hive DB update_status + if self.snaps_list.len() > 0 { + // if pac_lock accessible and exist - rolling back + if let Ok(Some(snap_date_time)) = nyan_system::check_pac_lock() + && self.snaps_list.contains(&snap_date_time) + { + Ok(NyanitState::ConfirmRollback(snap_date_time)) + } + // else user choose what to do + else { + Ok(NyanitState::WaitForF2) + } + } + // else tell to finish boot + else { + Ok(NyanitState::HandOff) + } + } + + fn init_stage(&mut self) -> Result { + // mount sysfss + nyan_system::prepare_env().map_err(|ex| format!("prepare_env error: {}", ex))?; + + // get bee-root device name aka /dev/sda1 + // trying for max 3 sec + + self.bee_root_dev = nyan_system::try_find_partlabel(ROOT_PARTLABEL, 3) + .ok_or_else(|| "Timed out trying to find dev by partlabel.".to_string())?; + + // works until this point!!!! + // check if device is encrypted + if nyan_system::bzzpss_dev_encrypted(&self.bee_root_dev) + .map_err(|ex| format!("Error checking if device is encrypted: {}", ex))? + { + // if encrypted return EnterPassword so user get promted for pass + return Ok(NyanitState::EnterPassword); + } + + // if unencrypted + let check_res = self.mount_and_check_snaps()?; + Ok(check_res) + } + /// runs the application's main loop until the user quits pub fn run(&mut self) -> io::Result<()> { - self.state = match init_stage() { - Ok(res) => { - self.bee_root_dev = res.0; - res.1 - } + self.state = match self.init_stage() { + Ok(res) => res, Err(ex) => { NyanitState::Error(format!("Error while trying to bzzz-pss things:\n{}", ex)) } @@ -154,8 +220,8 @@ impl NyanitTUI<'_> { // if drive unencrypted and there is no root snapshots finish boot match &self.state { // if drive unencrypted and there is no root snapshots finish boot - NyanitState::HandOff(root_mnt) => { - if let Err(ex) = nyan_system::hand_off_control(root_mnt, SYS_INIT) { + NyanitState::HandOff => { + if let Err(ex) = nyan_system::hand_off_control(TMP_ROOT, SYS_INIT) { self.state = NyanitState::Error(ex.to_string()) } } @@ -164,9 +230,13 @@ impl NyanitTUI<'_> { let mut textarea: TextArea<'_> = TextArea::default(); textarea.set_cursor_line_style(Style::default()); textarea.set_mask_char('\u{1F41D}'); //U+2022 BULLET (•) - textarea.set_placeholder_text("Please enter your password"); + textarea.set_placeholder_text("Please enter your disk unlock password"); textarea.set_style(Style::default().fg(Color::LightYellow)); - textarea.set_block(Block::default().borders(Borders::ALL).title("Password")); + textarea.set_block( + Block::default() + .borders(Borders::ALL) + .title(" System disk unlock password "), + ); self.textarea = Some(textarea); } _ => (), @@ -180,7 +250,7 @@ impl NyanitTUI<'_> { let mut terminal = Terminal::new(backend)?; // init tty async input and start actually drawing things now! - let mut tty_input_async = termion::async_stdin().bytes(); + let mut tty_input_async = termion::async_stdin().keys(); self.time_counter = 0; while !self.exit { @@ -225,67 +295,150 @@ impl NyanitTUI<'_> { match &self.state { NyanitState::Error(ex) => { // render big red wolf in the center with err messaghe - let err_paragraph = Paragraph::new(ex.as_str()).centered().light_red().bold(); + let err_paragraph = Paragraph::new(ex.as_str()) + .centered() + .wrap(Wrap::default()) + .light_red() + .bold(); frame.render_widget(err_paragraph, vertical_chunks[1]); } - NyanitState::WaitForF2(_) => { + NyanitState::WaitForF2 => { // show press F2 for options text - let f2_paragraph = Paragraph::new("Press F2 for options") - .centered() - .magenta() - .bold(); + let f2_lines: Vec = vec![ + Line::from("Press F2 for options"), + Line::default(), + Line::from((WAIT_FOR_F2_SEC.saturating_sub(self.time_counter)).to_string()), + ]; + + let f2_paragraph = Paragraph::new(f2_lines).centered().magenta().bold(); + frame.render_widget(f2_paragraph, vertical_chunks[1]); } - NyanitState::ChooseSnapshot(snaps_list) => { - // render menu for snap choosing + NyanitState::ChooseSnapshot => { + // 1. Split main chank to make space for description + let snap_list_chunks = Layout::vertical([ + Constraint::Length(2), // Description test + Constraint::Fill(1), // to shrink unused space + ]) + .split(vertical_chunks[1]); + + let caption_paragraph = Paragraph::new(SNAP_CHOOSE_LIST_CAPTION) + .centered() + .wrap(Wrap::default()) + .light_red() + .bold(); + frame.render_widget(caption_paragraph, snap_list_chunks[0]); + + // construct items list to get proper text size + let mut list_items: Vec = vec![ListItem::new(CURRENT_BOOT_LIST_TEXT)]; + self.snaps_list + .iter() + .for_each(|s| list_items.push(ListItem::new(s.as_str()))); + + let max_width = 2 + list_items.iter().map(|i| i.width()).max().unwrap_or(0) as u16; + + let render_list = List::new(list_items) + .highlight_symbol(">>") + .highlight_style(Style::new().bold().on_light_red()); + frame.render_stateful_widget( - List::new(snaps_list.iter().map(String::as_str)) - .block(Block::default().borders(Borders::ALL)) - .highlight_symbol(">>") - .highlight_style(Style::new().bold()), - vertical_chunks[1], + render_list, + snap_list_chunks[1].centered_horizontally(Constraint::Max(max_width)), &mut self.list_state, ); } - NyanitState::EnterPassword => { - frame.render_widget(self.textarea.as_ref().unwrap(), vertical_chunks[1]); + NyanitState::ConfirmRollback(snap_dt) => { + let par1 = gen_conf_par(snap_dt.clone()); + frame.render_widget(par1, vertical_chunks[1]); } - NyanitState::HandOff(_) => { - let ok_text = Paragraph::new("Booting\nKira Linux\n \u{1F41D} \u{1F41D} :3") - .block(Block::bordered().light_green()) + NyanitState::RollBack(roll_state) => { + let centered = vertical_chunks[1].centered_vertically(Constraint::Length(9)); + // 1. Create the outer block + let block = Block::default() + .padding(Padding::proportional(1)) + .borders(Borders::ALL) + .light_red() + .title(" Rolling back system state "); + + // 2. Get the inner area of the block + let block_inner_area = block.inner(centered); + + frame.render_widget(block, centered); + + // 3. Split the inner area into two parts: top for text, bottom for gauge + let chunks = Layout::vertical([ + Constraint::Length(3), // gauge + Constraint::Length(1), // spacing + Constraint::Length(1), // Height of the text paragraph + ]) + .split(block_inner_area); + let gauge = Gauge::default() + .gauge_style(Style::default().fg(Color::LightRed)) + .ratio(roll_state.progress); + frame.render_widget(gauge, chunks[0]); + + let text = Paragraph::new(roll_state.progress_text.clone()).centered(); + frame.render_widget(text, chunks[2]); + } + NyanitState::EnterPassword => { + frame.render_widget( + self.textarea.as_ref().unwrap(), + vertical_chunks[1] + .centered_vertically(Constraint::Max(3)) + .centered_horizontally(Constraint::Percentage(75)), + ); + } + NyanitState::HandOff => { + let ok_text = Paragraph::new("Booting\nKira Linux\n\u{1F41D} \u{1F41D} :3") + .centered() .light_green(); - frame.render_widget(ok_text, vertical_chunks[1]); - // just some time to look at message + frame.render_widget( + ok_text, + vertical_chunks[1].centered_vertically(Constraint::Length(3)), + ); + // just some time to look at beez thread::sleep(Duration::from_millis(1000)) } _ => (), }; } - fn handle_events(&mut self, tty_input: &mut io::Bytes) -> io::Result<()> { + fn handle_events( + &mut self, + tty_input: &mut termion::input::Keys, + ) -> io::Result<()> { // cheking if there is any input to read if let Some(item) = tty_input.next() { - let read_byte = item?; + let e = item?; - let e = event::parse_event(read_byte, tty_input)?; match &self.state { // finishing boot - NyanitState::HandOff(root_mnt) => { - if let Err(ex) = nyan_system::hand_off_control(root_mnt, SYS_INIT) { + // we allways booting to mnt rootnow + NyanitState::HandOff => { + if let Err(ex) = nyan_system::hand_off_control(TMP_ROOT, SYS_INIT) { self.state = NyanitState::Error(ex.to_string()) } } - NyanitState::WaitForF2(vol_list) => { - if e == Event::Key(event::Key::F(2)) { - self.state = NyanitState::ChooseSnapshot(vol_list.clone()); + NyanitState::WaitForF2 => { + if e == Key::F(2) { + self.state = NyanitState::ChooseSnapshot; } } - NyanitState::ChooseSnapshot(vol_list) => match e { - Event::Key(event::Key::Down) => self.list_state.select_next(), - Event::Key(event::Key::Up) => self.list_state.select_previous(), - Event::Key(event::Key::Char('\n')) => { + NyanitState::ChooseSnapshot => match e { + Key::Down => self.list_state.select_next(), + Key::Up => self.list_state.select_previous(), + Key::Char('\n') => { if let Some(idx) = self.list_state.selected() { - self.state = NyanitState::HandOff(vol_list[idx].clone()); + // if try to boot into current state + if idx == 0 { + self.state = NyanitState::HandOff; + } + // else we going to restore selected snapshoot as current state + else { + // idx-1 because self.snaps_list do not contains current system, only snapshots date time + self.state = + NyanitState::ConfirmRollback(self.snaps_list[idx - 1].clone()); + } } } _ => (), @@ -293,14 +446,13 @@ impl NyanitTUI<'_> { NyanitState::EnterPassword => { match e { // try to unlock if enter pressed - Event::Key(event::Key::Char('\n')) => { + Key::Char('\n') => { let password = self.textarea.as_ref().unwrap().lines()[0].clone(); match nyan_system::bcachefs_unlock(password, &self.bee_root_dev) { Ok(unlocked) => { if unlocked { // need to check for subvolumes and stuff - self.state = match mount_and_check_snaps(&self.bee_root_dev) - { + self.state = match self.mount_and_check_snaps() { Ok(st) => { self.time_counter = 0; st @@ -335,18 +487,27 @@ impl NyanitTUI<'_> { } } } + NyanitState::ConfirmRollback(snap_dt) => match e { + Key::Char('Y') | Key::Char('y') => { + let roll_state = RollbackState::new(snap_dt.clone()); + self.state = NyanitState::RollBack(roll_state); + } + Key::Char(_) => self.state = NyanitState::ChooseSnapshot, + _ => (), + }, _ => (), } } else { // Tick time - thread::sleep(Duration::from_millis(500)); + thread::sleep(Duration::from_millis(TICK_TIME_MILLS)); - if let NyanitState::WaitForF2(_) = self.state { - self.time_counter = self.time_counter.wrapping_add(1u32); - // 2.5 sec wait - if self.time_counter > 5 { + if let NyanitState::WaitForF2 = self.state { + self.time_counter = self.time_counter.wrapping_add(TICK_TIME_MILLS); + self.time_counter_sec = self.time_counter / 1000; + // wait for F2 + if self.time_counter_sec > WAIT_FOR_F2_SEC { // boot from root - self.state = NyanitState::HandOff(TMP_ROOT.into()); + self.state = NyanitState::HandOff; } } } diff --git a/src/nyan_system.rs b/src/nyan_system.rs index 500dc23..7773fb4 100644 --- a/src/nyan_system.rs +++ b/src/nyan_system.rs @@ -22,9 +22,26 @@ use nix; use std::fs; use std::process::Command; -// timeout for block deices population +/// timeout for block deices population const WAIT_FOR_BLK_DEVS: u64 = 5u64; +/// list of subvolumes to move from current system to snapshoot when being restored +const SBUVOL_TO_MOVE: [(&str, &str); 1] = [("var", "/tmp")]; + +const TMP_ROOT: &str = "/mnt/bee-root"; +const TMP_ROOT_SNAPS: &str = "/mnt/bee-root/bzz-snaps"; +const TMP_ROOT_USR: &str = "/mnt/bee-root/usr"; +const TMP_ROOT_VAR: &str = "/mnt/bee-root/var"; + +/// Path where /usr subvolume got moved on snapshot restore +const TMP_ROOT_SNAPS_USR: &str = "/mnt/bee-root/bzz-snaps/tmp_usr"; +/// Path where /var subvolume got moved on snapshot restore +const TMP_ROOT_SNAPS_VAR: &str = "/mnt/bee-root/bzz-snaps/tmp_var"; + +const TMP_ROOT_SNAPS_PREFIX: &str = "/mnt/bee-root/bzz-snaps/tmp_"; + +const PAC_LOCK_FILE: &str = "/var/lib/hive-up/pac-lock"; + #[derive(Debug)] pub struct CommandFailed { pub stderr: Vec, @@ -249,13 +266,17 @@ pub fn bzzpss_dev_encrypted(dev_name: &str) -> Result { } } -//const TMP_ROOT: &str = "/mnt/bee-root"; +/// +/// Looking for snapshots in snaps_dir for usr and var +/// And return list of date-time part of filename if both snapshots exists +/// pub fn list_bzzpss_snaps(snaps_dir: &str) -> Result, ExecCommandError> { use regex_lite::Regex; use std::path::Path; - // root snapshoot in a form of bzz-yyyy-mm-ddTHH-MM-SS - let re = Regex::new(r"^bzz-[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}-[0-9]{2}-[0-9]{2}$").unwrap(); + // usr or var snapshoot in a form of bzz-usr|var-yyyy-mm-ddTHH-MM-SS + let re = Regex::new(r"^bzz-(usr|var)-([0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}-[0-9]{2}-[0-9]{2})$") + .unwrap(); let mut res: Vec = Vec::new(); // if snaps dir path do not exist return empty list @@ -263,19 +284,231 @@ pub fn list_bzzpss_snaps(snaps_dir: &str) -> Result, ExecCommandErro return Ok(res); } + // contains only date-time part + let mut usr_snaps: Vec = Vec::new(); + let mut var_snaps: Vec = Vec::new(); + + // collect snapshots date-time for maybe_entry in fs::read_dir(snaps_dir)? { let entry = maybe_entry?; if entry.file_type()?.is_dir() { - if let Some(dir_name) = entry.file_name().to_str() - && re.is_match(dir_name) - { - res.push(dir_name.to_string()); + if let Some(dir_name) = entry.file_name().to_str() { + if let Some(captured) = re.captures(&dir_name) { + let subvol_name = &captured[1]; + let date_time = captured[2].to_string(); + if subvol_name == "usr" { + usr_snaps.push(date_time); + } else { + var_snaps.push(date_time); + } + } } } } + // now we need to make sure snapshots of /usr and /var both exist + // just fools safe, may reconsider in future + usr_snaps.sort(); + for snap_time in usr_snaps { + if var_snaps.contains(&snap_time) { + res.push(snap_time); + } + } Ok(res) } +/// +/// creates rw snapshot of "subvolume" and places it into "target" +/// bcachefs subvolume snapshot --rw /subvolume /target +/// +pub fn snapshot_rw(subvolume: &str, target: &str) -> Result<(), ExecCommandError> { + exec_command( + "bcachefs", + &vec!["subvolume", "snapshot", "--rw", subvolume, target], + )?; + + Ok(()) +} + +/// +/// bcachefs subvolume delete shortcut +/// +pub fn subvolume_delete(subvolume: &str) -> Result<(), ExecCommandError> { + exec_command("bcachefs", &vec!["subvolume", "delete", subvolume])?; + + Ok(()) +} + +/// +/// Helper function to get snapshot names in form of "bzz-(usr|var)-date-time" +/// vol: "usr" or "var" +/// dt: datetime +/// +fn snap_name_from_dt(vol: &str, dt: &str) -> String { + format!("bzz-{}-{}", vol, dt) +} + +/// +/// +/// // Stable Rust example using from_fn +fn first_n(n: u64) -> impl Iterator { + let mut count = 0; + std::iter::from_fn(move || { + if count < n { + let val = count; + count += 1; + Some(val) + } else { + None + } + }) +} + +enum RollbackStates { + Start, + RenameUsrVarAct, + ChangeToRw, + MoveSubvolums, + RemoveOldVols, + ClearSnaps, + Finish, + Err, +} + +pub fn roll_back_snap( + snaps_list: &Vec, + target_snap: &str, +) -> impl Iterator> { + let usr_ro_snap_path = format!("{}/bzz-{}-{}", TMP_ROOT_SNAPS, "usr", target_snap); + let var_ro_snap_path = format!("{}/bzz-{}-{}", TMP_ROOT_SNAPS, "usr", target_snap); + + let mut roll_state = RollbackStates::Start; + + std::iter::from_fn(move || match roll_state { + RollbackStates::Start => { + roll_state = RollbackStates::RenameUsrVarAct; + Some(Ok("Move usr and var to temporary location".into())) + } + RollbackStates::RenameUsrVarAct => { + let move_res = fs::rename(TMP_ROOT_USR, TMP_ROOT_SNAPS_USR) + .and_then(|_| fs::rename(TMP_ROOT_VAR, TMP_ROOT_SNAPS_VAR)); + if let Err(ex) = move_res { + roll_state = RollbackStates::Err; + Some(Err(ex.into())) + } else { + roll_state = RollbackStates::ChangeToRw; + Some(Ok("Setting snapshot as current system ...".into())) + } + } + RollbackStates::ChangeToRw => { + let rw_res = snapshot_rw(&usr_ro_snap_path, TMP_ROOT_USR) + .and_then(|_| snapshot_rw(&var_ro_snap_path, TMP_ROOT_VAR)); + if let Err(ex) = rw_res { + roll_state = RollbackStates::Err; + Some(Err(ex)) + } else { + roll_state = RollbackStates::MoveSubvolums; + Some(Ok( + "Moving subvolumes from old /usr and /var to new one".into() + )) + } + } + RollbackStates::MoveSubvolums => { + let mut mv_res = Some(Ok("Removing old /usr and /var".into())); + for (vol, svol_path) in SBUVOL_TO_MOVE { + let v_from = format!("{}{}{}", TMP_ROOT_SNAPS_PREFIX, vol, svol_path); + let v_dest = format!("{}/{}{}", TMP_ROOT, vol, svol_path); + if let Err(ex) = fs::rename(v_from, v_dest) { + roll_state = RollbackStates::Err; + mv_res = Some(Err(ex.into())); + break; + } + } + roll_state = RollbackStates::RemoveOldVols; + mv_res + } + RollbackStates::RemoveOldVols => { + let rem_old_res = subvolume_delete(TMP_ROOT_SNAPS_USR) + .and_then(|_| subvolume_delete(TMP_ROOT_SNAPS_VAR)); + if let Err(ex) = rem_old_res { + roll_state = RollbackStates::Err; + Some(Err(ex)) + } else { + roll_state = RollbackStates::ClearSnaps; + Some(Ok("Clearing snapshots...".into())) + } + } + RollbackStates::ClearSnaps => { + let mut clr_snap_res = Some(Ok("All done!".into())); + for snap_date_time in snaps_list.iter().rev() { + let usr_snap = format!("{}/bzz-{}-{}", TMP_ROOT_SNAPS, "usr", snap_date_time); + let var_snap = format!("{}/bzz-{}-{}", TMP_ROOT_SNAPS, "usr", snap_date_time); + if let Err(ex) = + subvolume_delete(&usr_snap).and_then(|_| subvolume_delete(&var_snap)) + { + clr_snap_res = Some(Err(ex)); + roll_state = RollbackStates::Err; + break; + } + if snap_date_time == target_snap { + break; + } + } + clr_snap_res + } + RollbackStates::Finish => None, + RollbackStates::Err => None, + }) + + // 1. Rename usr and var to TMP_ROOT_SNAPS/tmp_var|usr + // fs::rename(TMP_ROOT_USR, TMP_ROOT_SNAPS_USR)?; + // fs::rename(TMP_ROOT_VAR, TMP_ROOT_SNAPS_VAR)?; + + // 2. snapshot desired snaps as RW to /usr and /var + // snapshot_rw(&usr_ro_snap_path, TMP_ROOT_USR)?; + // snapshot_rw(&var_ro_snap_path, TMP_ROOT_VAR)?; + + // 3. move subvolumes from old /usr and /var + // for (vol, svol_path) in SBUVOL_TO_MOVE { + // let v_from = format!("{}{}{}", TMP_ROOT_SNAPS_PREFIX, vol, svol_path); + // let v_dest = format!("{}/{}{}",TMP_ROOT,vol,svol_path); + // fs::rename(v_from, v_dest)?; + // } + + // 4. removing tmp /usr /var + // subvolume_delete(TMP_ROOT_SNAPS_USR)?; + // subvolume_delete(TMP_ROOT_SNAPS_VAR)?; + + // 5. Removing all snapshoots made after one we restored, including inself + // Iterate in reverse untill we get to target_snap in snaps list + // for snap_date_time in snaps_list.iter().rev() { + // let usr_snap = format!("{}/bzz-{}-{}", TMP_ROOT_SNAPS, "usr", snap_date_time); + // let var_snap = format!("{}/bzz-{}-{}", TMP_ROOT_SNAPS, "usr", snap_date_time); + // subvolume_delete(&usr_snap)?; + // subvolume_delete(&var_snap)?; + // if snap_date_time == target_snap { + // break; + // } + // } + + // Ok(()) +} + +/// +/// Checks if pac_lock file exist and returns date-time from it if it does, +/// Otherwise return None +/// +pub fn check_pac_lock() -> Result, ExecCommandError> { + use std::path::Path; + let pac_lock_path = Path::new(PAC_LOCK_FILE); + if pac_lock_path.exists() { + let snap_dt = fs::read_to_string(pac_lock_path)?; + Ok(Some(snap_dt.trim().into())) + } + else { + Ok(None) + } +} + // pulls /sys/class/block fn wait_for_block_devices(timeout_secs: u64) -> bool { use std::path::Path; @@ -300,7 +533,6 @@ fn wait_for_block_devices(timeout_secs: u64) -> bool { false } - // mounting process //# Mount essential virtual filesystems // mount -t proc none /proc @@ -351,7 +583,8 @@ pub fn prepare_env() -> Result<(), ExecCommandError> { .map_err(|ex| format!("Error mounting /dev: {}", ex.to_string()))?; //mount -t tmpfs -o nosuid,nodev,noexec,mode=0755 none /run - let run_mount_flags = MsFlags::MS_NOSUID | MsFlags::MS_NODEV | MsFlags::MS_NOEXEC | MsFlags::MS_RELATIME; + let run_mount_flags = + MsFlags::MS_NOSUID | MsFlags::MS_NODEV | MsFlags::MS_NOEXEC | MsFlags::MS_RELATIME; mount( Some("run"), "/run", @@ -367,7 +600,6 @@ pub fn prepare_env() -> Result<(), ExecCommandError> { // Creating dir for udev so this little shit wont hang // fs::create_dir("/run/udev")?; - Ok(()) } @@ -412,7 +644,14 @@ pub fn mount_bee_root(dev_name: &str, mount_point: &str) -> Result<(), ExecComma root_mount_flags, None::<&str>, ) - .map_err(|ex| format!("Error mounting root {} to {} :\n{}",dev_name, mount_point, ex.to_string()))?; + .map_err(|ex| { + format!( + "Error mounting root {} to {} :\n{}", + dev_name, + mount_point, + ex.to_string() + ) + })?; // exec_command("/usr/bin/mount", &vec!["-o", "ro", dev_name, mount_point])?; Ok(()) }