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
This commit is contained in:
dailz
2026-06-03 14:47:23 +08:00
parent b3256b2917
commit b6e655bff6
3 changed files with 78 additions and 44 deletions

View File

@@ -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<memmap2::Mmap>,
@@ -108,13 +116,17 @@ impl FileReader {
Ok(())
}
pub fn update_for_append(&mut self) -> Result<u64> {
pub fn update_for_append(&mut self) -> Result<AppendStatus> {
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]

View File

@@ -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<u64> {
pub fn update_for_append(&mut self) -> Result<AppendStatus> {
match &mut self.state {
ReaderState::Ready { reader, .. } => reader.update_for_append(),
_ => Ok(0),
_ => Ok(AppendStatus::Unchanged),
}
}

View File

@@ -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();
}
_ => {}
}