use std::{
path::PathBuf,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
};
use tokio::sync::mpsc;
use crate::tui::app::FlashEvent;
use flashkraft_core::flash_helper::{run_pipeline, FlashEvent as CoreFlashEvent};
use flashkraft_core::FlashUpdate;
pub async fn run_flash(
image_path: PathBuf,
device_path: PathBuf,
cancel_token: Arc<AtomicBool>,
tx: mpsc::UnboundedSender<FlashEvent>,
) {
match run_flash_inner(image_path, device_path, cancel_token, tx.clone()).await {
Ok(()) => {
}
Err(e) => {
let _ = tx.send(FlashEvent::Failed(e));
}
}
}
async fn run_flash_inner(
image_path: PathBuf,
device_path: PathBuf,
cancel_token: Arc<AtomicBool>,
tx: mpsc::UnboundedSender<FlashEvent>,
) -> Result<(), String> {
let image_size = image_path
.metadata()
.map_err(|e| format!("Cannot read image file: {e}"))?
.len();
if image_size == 0 {
return Err("Image file is empty.".to_string());
}
let img_str = image_path
.to_str()
.ok_or("Image path contains invalid UTF-8")?
.to_owned();
let dev_str = device_path
.to_str()
.ok_or("Device path contains invalid UTF-8")?
.to_owned();
let (core_tx, core_rx) = std::sync::mpsc::channel::<CoreFlashEvent>();
let cancel_thread = cancel_token.clone();
std::thread::spawn(move || {
run_pipeline(&img_str, &dev_str, core_tx, cancel_thread);
});
loop {
if cancel_token.load(Ordering::SeqCst) {
return Err("Flash operation cancelled by user.".to_string());
}
loop {
match core_rx.try_recv() {
Ok(CoreFlashEvent::Done) => {
let _ = tx.send(FlashUpdate::Completed);
return Ok(());
}
Ok(CoreFlashEvent::Error(e)) => {
return Err(e);
}
Ok(core_event) => {
let _ = tx.send(FlashUpdate::from(core_event));
}
Err(std::sync::mpsc::TryRecvError::Empty) => {
break;
}
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
if cancel_token.load(Ordering::SeqCst) {
return Err("Flash operation cancelled by user.".to_string());
}
return Err("Flash thread terminated unexpectedly.".to_string());
}
}
}
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::sync::mpsc;
fn make_channel() -> (
mpsc::UnboundedSender<FlashEvent>,
mpsc::UnboundedReceiver<FlashEvent>,
) {
mpsc::unbounded_channel()
}
fn drain(rx: &mut mpsc::UnboundedReceiver<FlashEvent>) -> Vec<FlashEvent> {
let mut out = Vec::new();
while let Ok(e) = rx.try_recv() {
out.push(e);
}
out
}
#[tokio::test]
async fn run_flash_missing_image_sends_failed() {
let (tx, mut rx) = make_channel();
let cancel = Arc::new(AtomicBool::new(false));
run_flash(
PathBuf::from("/nonexistent/image.img"),
PathBuf::from("/dev/null"),
cancel,
tx,
)
.await;
let events = drain(&mut rx);
assert!(
events.iter().any(|e| matches!(e, FlashEvent::Failed(_))),
"expected a Failed event for missing image, got: {events:?}"
);
}
#[tokio::test]
async fn run_flash_empty_image_sends_failed() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let (tx, mut rx) = make_channel();
let cancel = Arc::new(AtomicBool::new(false));
run_flash(
tmp.path().to_path_buf(),
PathBuf::from("/dev/null"),
cancel,
tx,
)
.await;
let events = drain(&mut rx);
assert!(
events.iter().any(|e| matches!(e, FlashEvent::Failed(_))),
"expected a Failed event for empty image, got: {events:?}"
);
}
#[test]
fn core_event_stage_maps_to_message() {
use flashkraft_core::flash_helper::{FlashEvent as CoreFlashEvent, FlashStage};
let label = FlashStage::Writing.to_string();
let update = FlashUpdate::from(CoreFlashEvent::Stage(FlashStage::Writing));
assert!(matches!(update, FlashUpdate::Message(s) if s == label));
}
#[test]
fn core_event_progress_maps_correctly() {
use flashkraft_core::flash_helper::FlashEvent as CoreFlashEvent;
let update = FlashUpdate::from(CoreFlashEvent::Progress {
bytes_written: 512,
total_bytes: 1024,
speed_mb_s: 42.0,
});
match update {
FlashUpdate::Progress {
progress,
bytes_written,
speed_mb_s,
} => {
assert!((progress - 0.5).abs() < 1e-6);
assert_eq!(bytes_written, 512);
assert!((speed_mb_s - 42.0).abs() < 1e-6);
}
_ => panic!("unexpected variant"),
}
}
#[test]
fn core_event_log_maps_to_message() {
use flashkraft_core::flash_helper::FlashEvent as CoreFlashEvent;
let msg = "hello from pipeline".to_string();
let update = FlashUpdate::from(CoreFlashEvent::Log(msg.clone()));
assert!(matches!(update, FlashUpdate::Message(m) if m == msg));
}
#[test]
fn core_event_done_maps_to_completed() {
use flashkraft_core::flash_helper::FlashEvent as CoreFlashEvent;
let update = FlashUpdate::from(CoreFlashEvent::Done);
assert!(matches!(update, FlashUpdate::Completed));
}
#[test]
fn core_event_error_maps_to_failed() {
use flashkraft_core::flash_helper::FlashEvent as CoreFlashEvent;
let msg = "something broke".to_string();
let update = FlashUpdate::from(CoreFlashEvent::Error(msg.clone()));
assert!(matches!(update, FlashUpdate::Failed(m) if m == msg));
}
#[tokio::test]
async fn run_flash_cancelled_before_start_sends_failed() {
let tmp = tempfile::NamedTempFile::new().unwrap();
std::fs::write(tmp.path(), b"dummy image data").unwrap();
let cancel = Arc::new(AtomicBool::new(true)); let (tx, mut rx) = make_channel();
run_flash(
tmp.path().to_path_buf(),
PathBuf::from("/dev/null"),
cancel,
tx,
)
.await;
let events = drain(&mut rx);
let _ = events;
}
}