From 971d91b3f270c0b0abfa51e8978ac1c734e6151b Mon Sep 17 00:00:00 2001 From: Douglas Teles Date: Wed, 8 Jul 2026 13:14:54 -0300 Subject: [PATCH] flasher: universal streaming flash log on every platform The diagnostic log born on the macOS path is now a shared module used by all three flash paths: one well-known location (archr-flasher-flash.log in the temp dir), reset at the start of each flash, flushed line by line so even a hard crash leaves the full trace, with version, OS and start time in the header. Windows logs the volume lock/dismount results, write and verify hashes and the post-write config script output; Linux captures the helper's full stderr; macOS keeps its existing narration. Error messages on every platform now end with the log location, without disturbing the stable err: tokens the frontend matches on. --- src-tauri/src/diaglog.rs | 56 +++++++++++++++++++++++++ src-tauri/src/flash_linux.rs | 35 ++++++++++++++++ src-tauri/src/flash_macos.rs | 42 ++----------------- src-tauri/src/flash_windows.rs | 77 +++++++++++++++++++++++++++++++--- src-tauri/src/main.rs | 1 + 5 files changed, 167 insertions(+), 44 deletions(-) create mode 100644 src-tauri/src/diaglog.rs diff --git a/src-tauri/src/diaglog.rs b/src-tauri/src/diaglog.rs new file mode 100644 index 0000000..4f9301d --- /dev/null +++ b/src-tauri/src/diaglog.rs @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2026 ArchR +// +// Streaming diagnostic log for the flash pipeline, shared by every +// platform. Reset at the start of each flash and flushed line by line, +// so crashes and kills still leave a complete trace the user can attach +// to a bug report. + +use std::fs::File; +use std::io::Write; +use std::path::PathBuf; +use std::time::{SystemTime, UNIX_EPOCH}; + +/// Single well-known location, independent of the platform flash path. +pub fn log_path() -> PathBuf { + std::env::temp_dir().join("archr-flasher-flash.log") +} + +pub struct DiagLog { + file: Option, +} + +impl DiagLog { + /// Truncate the previous run's log and write the header. + pub fn new() -> Self { + let file = File::create(log_path()).ok(); + let mut log = DiagLog { file }; + let epoch = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0); + log.push_str(&format!( + "archr-flasher {} flash log (os: {}, started: {} unix)\n", + env!("CARGO_PKG_VERSION"), + std::env::consts::OS, + epoch + )); + log + } + + pub fn push_str(&mut self, line: &str) { + if let Some(f) = self.file.as_mut() { + let _ = f.write_all(line.as_bytes()); + let _ = f.flush(); + } + } +} + +/// Append the log location to an error message without disturbing the +/// stable err:* tokens the frontend matches at the start of the string. +pub fn with_log_hint(err: String) -> String { + if err == "cancelled" { + return err; + } + format!("{} (log: {})", err, log_path().display()) +} diff --git a/src-tauri/src/flash_linux.rs b/src-tauri/src/flash_linux.rs index 4e36795..d1c41a2 100644 --- a/src-tauri/src/flash_linux.rs +++ b/src-tauri/src/flash_linux.rs @@ -16,6 +16,7 @@ use std::time::Duration; use tauri::{AppHandle, Emitter}; use serde::Serialize; use crate::flash::{emit_progress, validate_device, check_temp_space, poll_flash_progress, decompress_xz, decompress_gz}; +use crate::diaglog::DiagLog; // --------------------------------------------------------------------------- // Linux: pkexec + helper script @@ -28,11 +29,35 @@ pub fn flash_image_privileged( custom_dtbo_path: &str, variant: &str, verify: bool, +) -> Result<(), String> { + let mut diag = DiagLog::new(); + let result = flash_core_linux( + app, image_path, device, custom_dtbo_path, variant, verify, &mut diag, + ); + match &result { + Ok(()) => diag.push_str("result: success\n"), + Err(e) => diag.push_str(&format!("result: {}\n", e)), + } + result.map_err(crate::diaglog::with_log_hint) +} + +#[cfg(target_os = "linux")] +fn flash_core_linux( + app: &AppHandle, + image_path: &str, + device: &str, + custom_dtbo_path: &str, + variant: &str, + verify: bool, + diag: &mut DiagLog, ) -> Result<(), String> { use std::process::{Command, Stdio}; use std::os::unix::fs::PermissionsExt; validate_device(device)?; + diag.push_str(&format!( + "device: {}\nimage: {}\nverify: {}\n", device, image_path, verify + )); let is_xz = image_path.ends_with(".xz"); let is_gz = image_path.ends_with(".gz") && !is_xz; @@ -89,6 +114,7 @@ pub fn flash_image_privileged( .arg("systemd-inhibit").output() .map(|o| o.status.success()) .unwrap_or(false); + diag.push_str(&format!("systemd-inhibit available: {}\n", has_inhibit)); let mut cmd = if has_inhibit { let mut c = Command::new("systemd-inhibit"); @@ -150,6 +176,15 @@ pub fn flash_image_privileged( let output = child.wait_with_output() .map_err(|e| format!("Failed to wait for pkexec: {}", e))?; + // The helper script narrates every stage to stderr; keep the whole + // capture in the diagnostic log regardless of the outcome (no env + // or secrets ever go through this channel). + diag.push_str(&format!( + "helper exit: {:?}\n--- helper stderr ---\n{}\n---\n", + output.status, + String::from_utf8_lossy(&output.stderr).trim() + )); + // Stop polling thread stop.store(true, Ordering::Relaxed); let _ = poll_handle.join(); diff --git a/src-tauri/src/flash_macos.rs b/src-tauri/src/flash_macos.rs index dcf1a7e..ab5db43 100644 --- a/src-tauri/src/flash_macos.rs +++ b/src-tauri/src/flash_macos.rs @@ -21,34 +21,7 @@ use tauri::{AppHandle, Emitter}; use serde::Serialize; use crate::flash::{emit_progress, validate_device, check_temp_space, poll_flash_progress, decompress_xz, decompress_gz}; use crate::rawwrite_unix::{receive_fd_from_helper, write_image_to_raw_fd, configure_boot_volume, FdHelperError}; - -/// Streaming diagnostic log: every line lands on disk the moment it is -/// pushed, so crashes and kills still leave a complete trace. Reset at -/// the start of each flash. -#[cfg(target_os = "macos")] -struct DiagLog { - file: Option, -} - -#[cfg(target_os = "macos")] -impl DiagLog { - fn new(path: &Path) -> Self { - let file = File::create(path).ok(); - let mut log = DiagLog { file }; - log.push_str(&format!( - "archr-flasher {} flash log\n", - env!("CARGO_PKG_VERSION") - )); - log - } - - fn push_str(&mut self, line: &str) { - if let Some(f) = self.file.as_mut() { - let _ = f.write_all(line.as_bytes()); - let _ = f.flush(); - } - } -} +use crate::diaglog::DiagLog; #[cfg(target_os = "macos")] pub fn flash_image_privileged( @@ -59,22 +32,13 @@ pub fn flash_image_privileged( variant: &str, verify: bool, ) -> Result<(), String> { - let log_path = std::env::temp_dir().join("archr-flasher-macos.log"); - // Reset per run and stream every stage as it happens, so even a hard - // crash leaves the full story on disk for bug reports. - let mut diag = DiagLog::new(&log_path); + let mut diag = DiagLog::new(); let result = flash_core(app, image_path, device, custom_dtbo_path, variant, verify, &mut diag); match &result { Ok(()) => diag.push_str("result: success\n"), Err(e) => diag.push_str(&format!("result: {}\n", e)), } - result.map_err(|e| { - if e == "cancelled" { - e - } else { - format!("{} (log: {})", e, log_path.display()) - } - }) + result.map_err(crate::diaglog::with_log_hint) } #[cfg(target_os = "macos")] diff --git a/src-tauri/src/flash_windows.rs b/src-tauri/src/flash_windows.rs index fade0ec..5915efc 100644 --- a/src-tauri/src/flash_windows.rs +++ b/src-tauri/src/flash_windows.rs @@ -232,7 +232,7 @@ fn ioctl(h: Handle, code: u32) -> bool { /// WriteFile came back ERROR_ACCESS_DENIED). Dropping the returned handles /// releases the locks. Best-effort: a volume that can't be locked is still /// dismounted and its handle kept. -fn lock_and_dismount_volumes(disk: u32) -> Vec { +fn lock_and_dismount_volumes(disk: u32, diag: &mut DiagLog) -> Vec { let mut held = Vec::new(); // SAFETY: no arguments. let mask = unsafe { GetLogicalDrives() }; @@ -270,6 +270,7 @@ fn lock_and_dismount_volumes(disk: u32) -> Vec { } eprintln!("[flash-win] locking + dismounting volume {}:", letter); + diag.push_str(&format!("volume {}: lock+dismount\n", letter)); let vh = match open_handle(&vol_path, GENERIC_READ | GENERIC_WRITE, 0) { Ok(h) => h, Err(_) => continue, @@ -293,6 +294,9 @@ fn lock_and_dismount_volumes(disk: u32) -> Vec { ioctl(vh.0, FSCTL_DISMOUNT_VOLUME); if !locked { eprintln!("[flash-win] warning: could not lock {}: — write may still be denied", letter); + diag.push_str(&format!("volume {}: LOCK FAILED, write may be denied\n", letter)); + } else { + diag.push_str(&format!("volume {}: locked\n", letter)); } // Remove the drive letter so Explorer stops probing it (mountpoint @@ -344,6 +348,7 @@ pub fn write_image( image_path: &str, progress_path: &str, verify: bool, + diag: &mut DiagLog, ) -> Result<(), String> { let disk = extract_disk_number(device)?; let image_size = std::fs::metadata(image_path) @@ -354,6 +359,10 @@ pub fn write_image( eprintln!(" device = {} (disk {})", device, disk); eprintln!(" image = {} ({} bytes)", image_path, image_size); eprintln!(" verify = {}", verify); + diag.push_str(&format!( + "native write: device {} (disk {}), image {} ({} bytes), verify {}\n", + device, disk, image_path, image_size, verify + )); // Lock + dismount the disk's volumes and HOLD the locks for the whole // write+verify. Two things matter here (learned from Rufus, format.c): @@ -364,25 +373,45 @@ pub fn write_image( // reassign the volume, which invalidates the lock we are holding and // re-arms the write protection. The image write lays down the new // partition table itself, so no pre-clean is needed. - let volume_locks = lock_and_dismount_volumes(disk); + let volume_locks = lock_and_dismount_volumes(disk, diag); write_progress(progress_path, "STAGE:writing"); - let write_hash = write_pass(device, image_path, image_size, progress_path)?; + let write_hash = match write_pass(device, image_path, image_size, progress_path) { + Ok(h) => { + diag.push_str(&format!("write pass done, sha256 {}\n", h)); + h + } + Err(e) => { + diag.push_str(&format!("write pass failed: {}\n", e)); + return Err(e); + } + }; if verify { write_progress(progress_path, "STAGE:verifying:0"); - let read_hash = verify_pass(device, image_size, progress_path)?; + let read_hash = match verify_pass(device, image_size, progress_path) { + Ok(h) => h, + Err(e) => { + diag.push_str(&format!("verify read failed: {}\n", e)); + return Err(e); + } + }; if read_hash != write_hash { eprintln!(" verify mismatch: expected {} got {}", write_hash, read_hash); + diag.push_str(&format!( + "verify mismatch: wrote {}, read back {}\n", write_hash, read_hash + )); return Err("verify_failed".to_string()); } eprintln!(" verify ok: {}", read_hash); + diag.push_str("verify ok, hashes match\n"); } // Release the volume locks so the freshly-written partitions can mount // for the config step, then ask Windows to re-read the partition table. drop(volume_locks); rescan_disk(device); + diag.push_str("volume locks released, disk rescanned\n"); write_progress(progress_path, "STAGE:done"); Ok(()) } @@ -544,6 +573,8 @@ fn verify_pass(device: &str, image_size: u64, progress_path: &str) -> Result diag.push_str("result: success\n"), + Err(e) => diag.push_str(&format!("result: {}\n", e)), + } + result.map_err(crate::diaglog::with_log_hint) +} + +#[cfg(target_os = "windows")] +fn flash_core_windows( + app: &AppHandle, + image_path: &str, + device: &str, + custom_dtbo_path: &str, + variant: &str, + verify: bool, + diag: &mut DiagLog, +) -> Result<(), String> { + use std::os::windows::process::CommandExt; + use std::process::{Command, Stdio}; + + const CREATE_NO_WINDOW: u32 = 0x08000000; + validate_device(device)?; + diag.push_str(&format!( + "device: {}\nimage: {}\nverify: {}\n", device, image_path, verify + )); let is_xz = image_path.ends_with(".xz"); let is_gz = image_path.ends_with(".gz") && !is_xz; @@ -624,6 +684,7 @@ pub fn flash_image_privileged( &img_to_flash.to_string_lossy(), &progress_file.to_string_lossy(), verify, + diag, ); stop.store(true, Ordering::Relaxed); @@ -664,12 +725,18 @@ pub fn flash_image_privileged( let _ = fs::remove_file(&script_path); let _ = fs::remove_file(&progress_file); + let cfg_stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + diag.push_str(&format!( + "config script: {:?}\n--- stdout ---\n{}\n--- stderr ---\n{}\n", + output.status, cfg_stdout.trim(), stderr.trim() + )); + if output.status.success() { emit_progress(app, 100.0, "done"); return Ok(()); } - let stderr = String::from_utf8_lossy(&output.stderr); Err(format!("Flash failed: {}", stderr.trim())) } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index aafb7ca..a0670cd 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,6 +1,7 @@ // Prevents additional console window on Windows in release #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] +mod diaglog; mod disk; mod flash; mod github;