2 Commits
Author SHA256 Message Date
kira 212fa7ed44 Most stuff should be complete. 2026-06-24 17:44:17 +02:00
kira 76a04b7d3e Adding function to perform bcachfs unlock. 2026-06-23 18:34:46 +02:00
4 changed files with 205 additions and 63 deletions
Generated
+14
View File
@@ -290,6 +290,7 @@ name = "nyanit"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"ratatui", "ratatui",
"ratatui-textarea",
"regex", "regex",
"termion", "termion",
] ]
@@ -394,6 +395,19 @@ dependencies = [
"termion", "termion",
] ]
[[package]]
name = "ratatui-textarea"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c78d5ba0f26f97baed69a4c479f268a31c7b5b89d68ab939842152e383d6e73"
dependencies = [
"ratatui-core",
"ratatui-termion",
"ratatui-widgets",
"unicode-segmentation",
"unicode-width",
]
[[package]] [[package]]
name = "ratatui-widgets" name = "ratatui-widgets"
version = "0.3.1" version = "0.3.1"
+2 -1
View File
@@ -5,6 +5,7 @@ edition = "2024"
[dependencies] [dependencies]
ratatui = { version = "0.30", default-features = false, features = ["termion"] } ratatui = { version = "0.30", default-features = false, features = ["termion"] }
ratatui-textarea = { version = "0.9", default-features = false, features = ["termion"] }
regex = "1.12.4" regex = "1.12.4"
termion = "4" termion = "4"
@@ -14,5 +15,5 @@ lto = true
debug = false debug = false
incremental = false incremental = false
codegen-units = 1 codegen-units = 1
opt-level = 3 opt-level = 2
strip = true strip = true
+118 -37
View File
@@ -14,25 +14,27 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
use ratatui::widgets::Borders; use ratatui::{Frame, Terminal, backend::TermionBackend};
use ratatui::{Frame, Terminal, backend::TermionBackend, widgets::Paragraph};
use ratatui::{ use ratatui::{
layout::{Constraint, Layout}, layout::{Constraint, Layout},
style::{Style, Stylize}, style::{Color, Style, Stylize},
text::{Line}, text::Line,
widgets::{Block, List, ListState}, widgets::{Block, Borders, List, ListState, Paragraph},
}; };
use ratatui_textarea::TextArea;
use std::io::{self, Read, stdout}; use std::io::{self, Read, stdout};
use std::process::ExitCode; use std::process::ExitCode;
use std::thread; use std::thread;
use std::time::Duration; use std::time::Duration;
use termion::event::{Event, Key}; use termion::event::{self, Event};
use termion::raw::IntoRawMode; use termion::raw::IntoRawMode;
mod nyan_system; mod nyan_system;
use nyan_system::{ExecCommandError}; use nyan_system::ExecCommandError;
const BEE_CAT: &str = include_str!("bee_cat.txt"); const BEE_CAT: &str = include_str!("bee_cat.txt");
@@ -71,17 +73,8 @@ enum NyanitState {
HandOff(String), HandOff(String),
} }
fn init_stage() -> Result<NyanitState, ExecCommandError> { /// Mounting bee-root and chiking if there any snapshots
// mount sysfss fn mount_and_check_snaps() -> Result<NyanitState, ExecCommandError> {
nyan_system::prepare_env()?;
// check if device is encrypted
if nyan_system::bzzpss_dev_encrypted(ROOT_DEV)? {
// if encrypted return EnterPassword so used get promted for pass
return Ok(NyanitState::EnterPassword);
}
// if unencrypted
// mount bee-root // mount bee-root
nyan_system::mount_bee_root(TMP_ROOT, ROOT_DEV)?; nyan_system::mount_bee_root(TMP_ROOT, ROOT_DEV)?;
@@ -93,24 +86,40 @@ fn init_stage() -> Result<NyanitState, ExecCommandError> {
return Ok(NyanitState::WaitForF2(snaps_list)); return Ok(NyanitState::WaitForF2(snaps_list));
} }
// tell to finish boot // else tell to finish boot
Ok(NyanitState::HandOff(TMP_ROOT.into())) Ok(NyanitState::HandOff(TMP_ROOT.into()))
} }
pub struct NyanitTUI { fn init_stage() -> Result<NyanitState, ExecCommandError> {
// mount sysfss
nyan_system::prepare_env()?;
// check if device is encrypted
if nyan_system::bzzpss_dev_encrypted(ROOT_DEV)? {
// if encrypted return EnterPassword so used get promted for pass
return Ok(NyanitState::EnterPassword);
}
// if unencrypted
mount_and_check_snaps()
}
struct NyanitTUI<'a> {
exit: bool, exit: bool,
state: NyanitState, state: NyanitState,
list_state: ListState, list_state: ListState,
time_counter: u32, time_counter: u32,
textarea: Option<TextArea<'a>>,
} }
impl NyanitTUI { impl NyanitTUI<'_> {
pub fn new() -> io::Result<Self> { pub fn new() -> io::Result<Self> {
Ok(Self { Ok(Self {
exit: false, exit: false,
state: NyanitState::Init, state: NyanitState::Init,
list_state: ListState::default().with_selected(Some(0)), list_state: ListState::default().with_selected(Some(0)),
time_counter: 0, time_counter: 0,
textarea: None,
}) })
} }
@@ -127,19 +136,34 @@ impl NyanitTUI {
}; };
// if drive unencrypted and there is no root snapshots finish boot // if drive unencrypted and there is no root snapshots finish boot
if let NyanitState::HandOff(root_mnt) = &self.state { 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) { if let Err(ex) = nyan_system::hand_off_control(root_mnt, SYS_INIT) {
self.state = NyanitState::Error(ex.to_string()) self.state = NyanitState::Error(ex.to_string())
} }
} }
// init password input
NyanitState::EnterPassword => {
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_style(Style::default().fg(Color::LightYellow));
textarea.set_block(Block::default().borders(Borders::ALL).title("Password"));
self.textarea = Some(textarea);
}
_ => (),
}
// Init terminal
// 1. Enable raw mode // 1. Enable raw mode
let stdout = stdout().into_raw_mode()?; let stdout = stdout().into_raw_mode()?;
// 2. Setup Terminal // 2. Setup Terminal
let backend = TermionBackend::new(stdout); let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?; let mut terminal = Terminal::new(backend)?;
// init tty input and start actually drawing things now! // 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().bytes();
self.time_counter = 0; self.time_counter = 0;
@@ -207,37 +231,94 @@ impl NyanitTUI {
&mut self.list_state, &mut self.list_state,
); );
} }
NyanitState::EnterPassword => {
frame.render_widget(self.textarea.as_ref().unwrap(), vertical_chunks[1]);
}
NyanitState::HandOff(_) => {
let ok_text = Paragraph::new("Booting\nKira Linux\n \u{1F41D} \u{1F41D} :3")
.block(Block::bordered().light_green())
.light_green();
frame.render_widget(ok_text, vertical_chunks[1]);
// just some time to look at message
thread::sleep(Duration::from_millis(1000))
}
_ => (), _ => (),
}; };
} }
fn handle_events(&mut self, tty_input: &mut io::Bytes<termion::AsyncReader>) -> io::Result<()> { fn handle_events(&mut self, tty_input: &mut io::Bytes<termion::AsyncReader>) -> io::Result<()> {
// cheking if there is any input to read // cheking if there is any input to read
if let Some(item) = tty_input.next() { if let Some(item) = tty_input.next() {
let read_byte = item?; let read_byte = item?;
let e = termion::event::parse_event(read_byte, tty_input)?; let e = event::parse_event(read_byte, tty_input)?;
match &self.state { match &self.state {
// finishing boot
NyanitState::HandOff(root_mnt) => {
if let Err(ex) = nyan_system::hand_off_control(root_mnt, SYS_INIT) {
self.state = NyanitState::Error(ex.to_string())
}
}
NyanitState::WaitForF2(vol_list) => { NyanitState::WaitForF2(vol_list) => {
if e == Event::Key(Key::F(2)) { if e == Event::Key(event::Key::F(2)) {
self.state = NyanitState::ChooseSnapshot(vol_list.clone()); self.state = NyanitState::ChooseSnapshot(vol_list.clone());
} }
}, }
NyanitState::ChooseSnapshot(vol_list) => { NyanitState::ChooseSnapshot(vol_list) => match e {
match e { Event::Key(event::Key::Down) => self.list_state.select_next(),
Event::Key(Key::Down) => self.list_state.select_next(), Event::Key(event::Key::Up) => self.list_state.select_previous(),
Event::Key(Key::Up) => self.list_state.select_previous(), Event::Key(event::Key::Char('\n')) => {
Event::Key(Key::Char('\n')) => if let Some(idx) = self.list_state.selected() { if let Some(idx) = self.list_state.selected() {
self.state = NyanitState::HandOff(vol_list[idx].clone()); self.state = NyanitState::HandOff(vol_list[idx].clone());
}
}
_ => (),
}, },
NyanitState::EnterPassword => {
match e {
// try to unlock if enter pressed
Event::Key(event::Key::Char('\n')) => {
let password = self.textarea.as_ref().unwrap().lines()[0].clone();
match nyan_system::bcachefs_unlock(password, ROOT_DEV) {
Ok(unlocked) => {
if unlocked {
// need to check for subvolumes and stuff
self.state = match mount_and_check_snaps() {
Ok(st) => st,
Err(ex) => NyanitState::Error(ex.to_string()),
}
}
// ask to enter password again
else {
self.textarea
.as_mut()
.unwrap()
.set_placeholder_text("Wrong password, try again");
self.textarea
.as_mut()
.unwrap()
.set_style(Style::default().fg(Color::LightRed));
self.textarea.as_mut().unwrap().clear();
}
}
Err(ex) => {
self.state = NyanitState::Error(ex.to_string());
}
}
}
_ => {
self.textarea
.as_mut()
.unwrap()
.set_style(Style::default().fg(Color::LightYellow));
self.textarea.as_mut().unwrap().input(e);
}
}
}
_ => (), _ => (),
} }
}, } else {
_ => ()
}
}
else {
// Tick time // Tick time
thread::sleep(Duration::from_millis(500)); thread::sleep(Duration::from_millis(500));
self.time_counter = self.time_counter.wrapping_add(1u32); self.time_counter = self.time_counter.wrapping_add(1u32);
+64 -18
View File
@@ -111,33 +111,79 @@ pub fn exec_command(cmd: &str, args: &Vec<&str>) -> Result<(), ExecCommandError>
/// exec given command with arguments, returning stdout parsed to UTF-8 string /// exec given command with arguments, returning stdout parsed to UTF-8 string
/// capturing stderr in case or failure /// capturing stderr in case or failure
pub fn exec_command_out(cmd: &str, args: &Vec<&str>) -> Result<String, ExecCommandError> { // pub fn exec_command_out(cmd: &str, args: &Vec<&str>) -> Result<String, ExecCommandError> {
let out = Command::new(cmd).args(args).output()?; // let out = Command::new(cmd).args(args).output()?;
if out.status.success() { // if out.status.success() {
Ok(String::from_utf8(out.stdout)?) // Ok(String::from_utf8(out.stdout)?)
} else { // } else {
Err(CommandFailed::new(cmd, args, out.stderr).into()) // Err(CommandFailed::new(cmd, args, out.stderr).into())
} // }
} // }
// ls /dev/disk/by-partlabel/ // ls /dev/disk/by-partlabel/
// here we can find gpt part labels // here we can find gpt part labels
pub fn list_gpt_labeled_devs() -> Result<Vec<String>, ExecCommandError> { // pub fn list_gpt_labeled_devs() -> Result<Vec<String>, ExecCommandError> {
use std::os::unix::fs::FileTypeExt; // use std::os::unix::fs::FileTypeExt;
let mut res: Vec<String> = Vec::new(); // let mut res: Vec<String> = Vec::new();
for maybe_entry in fs::read_dir("/dev/disk/by-partlabel/")? { // for maybe_entry in fs::read_dir("/dev/disk/by-partlabel/")? {
let entry = maybe_entry?; // let entry = maybe_entry?;
if entry.file_type()?.is_block_device() { // if entry.file_type()?.is_block_device() {
res.push(entry.path().to_str().unwrap().to_string()); // res.push(entry.path().to_str().unwrap().to_string());
} // }
// }
// Ok(res)
// }
/// bcachefs unlock --file=/tmp/bzzpsspass.txt /dev/loop0somedev
/// Ok so. It is stupid, because bcacheutils far from ideal.
/// But if we provide wrong password from file or stdin this stuff plays stupid and asks for right password.
/// Sooo.. In order to know if unlock was sucessfull we need to check if program exited with Ok status,
/// or it shits to STDERR still waiting for stdin
/// Returns true if unlock successful, and false otherwise
pub fn bcachefs_unlock(pass: String, dev: &str) -> Result<bool, ExecCommandError> {
use std::io::Read;
use std::io::Write;
use std::process::Stdio;
let mut child = Command::new("bcachefs")
.arg("unlock")
.arg(dev)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let mut c_stderr = child.stderr.take().ok_or_else(||{"Unable to get child process STDERR".to_string()})?;
// Write password to STDIN in a limited scope
{
let mut c_stdin = child.stdin.take().ok_or_else(||{"Unable to get child process STDIN".to_string()})?;
// neccessery to use thread or we may be deadlocking
c_stdin.write_all(pass.as_bytes())?;
let bytes_written = c_stdin.write(b"\n")?;
println!("bytes_written: {}", bytes_written);
// stdin is dropped here automatically when the block ends
} }
Ok(res) let mut buffer = [0u8; 10];
// This blocks.
// if unlok fails it reads 10 bytes (piece of err message)
// if sucess it reads 0 bytes and exit
let read_b = c_stderr.read(&mut buffer)?;
if read_b > 0 {
println!("STDERR error not empty, password incorrect! Try to kill a child.");
child.kill()?;
}
// Safe to wait now; child will see EOF
let exit_status = child.wait()?;
// if exit status is SUCCESS - unloking is successful
Ok(exit_status.success())
} }
/// Cheking if device is encripted /// Cheking if device is encripted
pub fn bzzpss_dev_encrypted(dev_name: &str) -> Result<bool, ExecCommandError> { pub fn bzzpss_dev_encrypted(dev_name: &str) -> Result<bool, ExecCommandError> {
match exec_command("bcachefs", &vec!["unlock", "--check"]) { match exec_command("bcachefs", &vec!["unlock", "--check", dev_name]) {
Ok(_) => Ok(true), // if status OK - dev encrypted Ok(_) => Ok(true), // if status OK - dev encrypted
Err(ExecCommandError::CommandFailed(_)) => Ok(false), // if bcachefs returned error - unencrypted Err(ExecCommandError::CommandFailed(_)) => Ok(false), // if bcachefs returned error - unencrypted
Err(ex) => Err(ex), // othervice its error Err(ex) => Err(ex), // othervice its error