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:
@@ -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]
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user