You've already forked archr-flasher
mirror of
https://github.com/archr-linux/archr-flasher.git
synced 2026-07-12 18:19:47 -07:00
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.
This commit is contained in:
@@ -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<File>,
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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<File>,
|
||||
}
|
||||
|
||||
#[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")]
|
||||
|
||||
@@ -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<OwnedHandle> {
|
||||
fn lock_and_dismount_volumes(disk: u32, diag: &mut DiagLog) -> Vec<OwnedHandle> {
|
||||
let mut held = Vec::new();
|
||||
// SAFETY: no arguments.
|
||||
let mask = unsafe { GetLogicalDrives() };
|
||||
@@ -270,6 +270,7 @@ fn lock_and_dismount_volumes(disk: u32) -> Vec<OwnedHandle> {
|
||||
}
|
||||
|
||||
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<OwnedHandle> {
|
||||
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<Str
|
||||
#[allow(unused_imports)]
|
||||
use crate::flash::{emit_progress, validate_device, check_temp_space, poll_flash_progress, decompress_xz, decompress_gz};
|
||||
#[allow(unused_imports)]
|
||||
use crate::diaglog::DiagLog;
|
||||
#[allow(unused_imports)]
|
||||
use std::fs;
|
||||
#[allow(unused_imports)]
|
||||
use std::path::PathBuf;
|
||||
@@ -571,7 +602,36 @@ pub fn flash_image_privileged(
|
||||
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
|
||||
let mut diag = DiagLog::new();
|
||||
let result = flash_core_windows(
|
||||
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 = "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()))
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user