From b6e655bff656873948e135b4a1bfb2da26e7cd82 Mon Sep 17 00:00:00 2001 From: dailz Date: Wed, 3 Jun 2026 14:47:23 +0800 Subject: [PATCH] fix(io): handle file shrink in update_for_append to prevent SIGBUS File truncation/rotation during mmap lifetime caused SIGBUS crash because update_for_append() ignored new_size < old_size, leaving a stale mapping that would fault on access. Introduce AppendStatus enum (Unchanged/Appended/Reloaded) so the caller can distinguish shrink events from normal appends. On shrink, reload() rebuilds the mmap and line index. The TUI layer clamps cursor and invalidates viewport cache on Reloaded, matching the existing handle_file_truncated() behavior. Fixes #1 --- crates/core/src/io/file_reader.rs | 37 ++++++++--- crates/core/src/io/progressive_reader.rs | 6 +- crates/tui/src/app.rs | 79 ++++++++++++++---------- 3 files changed, 78 insertions(+), 44 deletions(-) diff --git a/crates/core/src/io/file_reader.rs b/crates/core/src/io/file_reader.rs index 79f7a1d..fa1725b 100644 --- a/crates/core/src/io/file_reader.rs +++ b/crates/core/src/io/file_reader.rs @@ -9,6 +9,14 @@ use crate::io::index_cache::IndexCache; use crate::io::line_index::LineIndex; use std::path::{Path, PathBuf}; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AppendStatus { + Unchanged, + Appended(u64), + /// File shrank; mmap and index rebuilt via reload(). + Reloaded, +} + pub struct FileReader { path: PathBuf, mmap: Option, @@ -108,13 +116,17 @@ impl FileReader { Ok(()) } - pub fn update_for_append(&mut self) -> Result { + pub fn update_for_append(&mut self) -> Result { let file = std::fs::File::open(&self.path)?; let new_size = file.metadata()?.len(); let old_size = self.mmap.as_ref().map_or(0u64, |m| m.len() as u64); - if new_size <= old_size { - return Ok(0); + if new_size < old_size { + self.reload()?; + return Ok(AppendStatus::Reloaded); + } + if new_size == old_size { + return Ok(AppendStatus::Unchanged); } let old_lines = self.line_index.line_count() as u64; @@ -127,7 +139,9 @@ impl FileReader { .extend_from_bytes(&mmap[old_size as usize..], old_size); self.mmap = Some(mmap); - Ok(self.line_index.line_count() as u64 - old_lines) + Ok(AppendStatus::Appended( + self.line_index.line_count() as u64 - old_lines, + )) } } @@ -305,8 +319,8 @@ mod tests { file.write_all(b"ccc\nddd\n").unwrap(); } - let new_lines = reader.update_for_append().unwrap(); - assert_eq!(new_lines, 2); + let status = reader.update_for_append().unwrap(); + assert_eq!(status, AppendStatus::Appended(2)); assert_eq!(reader.line_count(), 4); assert_eq!(reader.get_line(0), Some("aaa")); assert_eq!(reader.get_line(1), Some("bbb")); @@ -320,8 +334,8 @@ mod tests { let mut reader = FileReader::open(f.path()).unwrap(); assert_eq!(reader.line_count(), 1); - let new_lines = reader.update_for_append().unwrap(); - assert_eq!(new_lines, 0); + let status = reader.update_for_append().unwrap(); + assert_eq!(status, AppendStatus::Unchanged); assert_eq!(reader.line_count(), 1); } @@ -341,8 +355,11 @@ mod tests { file.write_all(b"x\n").unwrap(); } - let new_lines = reader.update_for_append().unwrap(); - assert_eq!(new_lines, 0); + let status = reader.update_for_append().unwrap(); + assert_eq!(status, AppendStatus::Reloaded); + assert_eq!(reader.line_count(), 1); + assert_eq!(reader.file_size(), 2); + assert_eq!(reader.get_line(0), Some("x")); } #[test] diff --git a/crates/core/src/io/progressive_reader.rs b/crates/core/src/io/progressive_reader.rs index 62e8ee9..bbb9dcf 100644 --- a/crates/core/src/io/progressive_reader.rs +++ b/crates/core/src/io/progressive_reader.rs @@ -3,7 +3,7 @@ use std::fmt; use std::path::{Path, PathBuf}; use crate::error::{CoreError, Result}; -use crate::io::file_reader::FileReader; +use crate::io::file_reader::{AppendStatus, FileReader}; use crate::io::index_cache::IndexCache; use crate::io::line_index::LineIndex; use crate::io::line_sampler::sample_line_count; @@ -656,10 +656,10 @@ impl ProgressiveFileReader { } } - pub fn update_for_append(&mut self) -> Result { + pub fn update_for_append(&mut self) -> Result { match &mut self.state { ReaderState::Ready { reader, .. } => reader.update_for_append(), - _ => Ok(0), + _ => Ok(AppendStatus::Unchanged), } } diff --git a/crates/tui/src/app.rs b/crates/tui/src/app.rs index 9abca76..e953556 100644 --- a/crates/tui/src/app.rs +++ b/crates/tui/src/app.rs @@ -859,43 +859,58 @@ impl App { let width = self.get_content_width(); match &mut self.loading_state { AppLoadingState::Ready { reader } => { - if let Ok(_new_lines) = reader.update_for_append() { - let _ = reader.save_cache(); + let status = reader.update_for_append(); + match status { + Ok( + log_viewer_core::io::file_reader::AppendStatus::Appended(_new_lines), + ) => { + let _ = reader.save_cache(); - let (old_line_count, can_extend) = { - match &reader.state { - log_viewer_core::io::progressive_reader::ReaderState::Ready { - visual_height_index: Some(idx), - .. - } => (idx.line_count(), idx.is_valid_for(self.json_format, width)), - _ => (0, false), - } - }; - let new_line_count = reader.line_count(); - - if can_extend && new_line_count > old_line_count { - if let log_viewer_core::io::progressive_reader::ReaderState::Ready { - visual_height_index: Some(index), - reader: fr, - } = &mut reader.state - { - let mut new_heights = Vec::with_capacity(new_line_count - old_line_count); - for i in old_line_count..new_line_count { - let line_text = fr.get_line(i).unwrap_or(""); - new_heights.push(compute_line_visual_height( - line_text, - width, - self.json_format, - )); + let (old_line_count, can_extend) = { + match &reader.state { + log_viewer_core::io::progressive_reader::ReaderState::Ready { + visual_height_index: Some(idx), + .. + } => (idx.line_count(), idx.is_valid_for(self.json_format, width)), + _ => (0, false), } - index.extend_from_heights(&new_heights); + }; + let new_line_count = reader.line_count(); + + if can_extend && new_line_count > old_line_count { + if let log_viewer_core::io::progressive_reader::ReaderState::Ready { + visual_height_index: Some(index), + reader: fr, + } = &mut reader.state + { + let mut new_heights = Vec::with_capacity(new_line_count - old_line_count); + for i in old_line_count..new_line_count { + let line_text = fr.get_line(i).unwrap_or(""); + new_heights.push(compute_line_visual_height( + line_text, + width, + self.json_format, + )); + } + index.extend_from_heights(&new_heights); + } + } else { + reader.invalidate_visual_height_index(); + reader.start_visual_height_rebuild(width, self.json_format); } - } else { + + self.viewport_cache.invalidate(); + } + Ok(log_viewer_core::io::file_reader::AppendStatus::Reloaded) => { + let _ = reader.save_cache(); reader.invalidate_visual_height_index(); reader.start_visual_height_rebuild(width, self.json_format); + self.cursor_line = self.cursor_line.min(self.total_lines().saturating_sub(1)); + self.v_sub_offset = 0; + self.viewport_cache.invalidate(); + self.clamp_v_offset(); } - - self.viewport_cache.invalidate(); + Ok(log_viewer_core::io::file_reader::AppendStatus::Unchanged) | Err(_) => {} } } _ => {} @@ -911,7 +926,9 @@ impl App { reader.invalidate_visual_height_index(); reader.start_visual_height_rebuild(width, self.json_format); self.cursor_line = self.cursor_line.min(self.total_lines().saturating_sub(1)); + self.v_sub_offset = 0; self.viewport_cache.invalidate(); + self.clamp_v_offset(); } _ => {} }