Skip to content

Commit cbfc534

Browse files
committed
fix(tail): dispatch tail_mode to follow(), scan entire file backwards
for last N lines
1 parent f6b38ae commit cbfc534

File tree

2 files changed

+26
-39
lines changed

2 files changed

+26
-39
lines changed

src/reader/reader.zig

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
const std = @import("std");
66
const flags = @import("../flags/flags.zig");
77
const simd = @import("simd.zig");
8+
const tail_reader = @import("tail.zig");
89
const gzip = @import("gzip.zig");
910

1011
/// Cached analysis of a single log line.
@@ -26,13 +27,6 @@ const LineInfo = struct {
2627
starts_with_bracket: bool,
2728
};
2829

29-
/// Reads logs from the given files, applying the provided arguments for filtering and formatting.
30-
pub fn readLogs(allocator: std.mem.Allocator, args: flags.Args) !void {
31-
for (args.files) |path| {
32-
try readStreaming(allocator, path, args);
33-
}
34-
}
35-
3630
/// Analyzes a line and returns a fully populated `LineInfo`.
3731
/// All subsequent operations (filtering, printing) use this result directly,
3832
/// so the line is parsed only once per call path.
@@ -284,18 +278,27 @@ fn getOptimalBufferSize(file: std.fs.File) usize {
284278

285279
/// Entry point for reading a log file with filtering and colored output.
286280
/// Dispatches to pagination or continuous streaming based on `args.num_lines`.
287-
/// If the file is a gzip (.gz) file, pagination is not supported and the file is streamed continuously.
281+
/// Public entry point called by main.zig.
282+
/// Dispatches to tail follow mode, gzip, pagination, or continuous streaming.
283+
pub fn readLogs(allocator: std.mem.Allocator, args: flags.Args) !void {
284+
if (args.tail_mode) {
285+
try tail_reader.follow(allocator, args);
286+
return;
287+
}
288+
for (args.files) |path| {
289+
try readStreaming(allocator, path, args);
290+
}
291+
}
292+
288293
pub fn readStreaming(
289294
allocator: std.mem.Allocator,
290295
path: []const u8,
291296
args: flags.Args,
292297
) !void {
293-
// Gzip files не поддерживают пагинацию — seek в сжатый поток невозможен.
294298
if (gzip.isGzip(path)) {
295299
const filter_state = FilterState.init(args);
296300
return gzip.readGzip(allocator, path, filter_state);
297301
}
298-
299302
if (args.num_lines > 0) {
300303
try readWithPagination(allocator, path, args);
301304
} else {

src/reader/tail.zig

Lines changed: 13 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,7 @@ pub fn follow(
3535
// FilterState is built once and reused across all lines and files.
3636
const filter_state = formats.FilterState.init(args);
3737

38-
// Allocate a single shared read buffer for the follow loop and for
39-
// readLastNLines — avoids a second READ_BUF_SIZE allocation per file.
38+
// Allocate a single shared read buffer reused by readLastNLines and the follow loop.
4039
const read_buf = try allocator.alloc(u8, READ_BUF_SIZE);
4140
defer allocator.free(read_buf);
4241

@@ -110,13 +109,9 @@ pub fn follow(
110109
/// prints matching ones via `filter_state`, and returns the file position
111110
/// after the last consumed byte.
112111
///
113-
/// Scans backwards through the file in SCAN_CHUNK steps until N newlines
114-
/// are found or the beginning of the file is reached. This handles files of
115-
/// any size correctly — the old single-chunk approach missed lines when the
116-
/// last N lines spanned more than 8 KB.
117-
///
118-
/// `read_buf` is a caller-owned scratch buffer of at least READ_BUF_SIZE bytes,
119-
/// allowing the caller to reuse one allocation across multiple calls.
112+
/// Scans backwards in READ_BUF_SIZE windows until N newlines are found or
113+
/// the beginning of the file is reached. This handles files of any size —
114+
/// the old single 8 KB chunk missed lines when the last N lines exceeded that size.
120115
pub fn readLastNLines(
121116
allocator: std.mem.Allocator,
122117
file: *std.fs.File,
@@ -127,47 +122,41 @@ pub fn readLastNLines(
127122
) !u64 {
128123
const n: usize = if (args.num_lines == 0) 10 else args.num_lines;
129124

130-
// Scan chunk size for the backwards pass — 64 KB matches READ_BUF_SIZE
131-
// so one chunk is almost always enough for typical log lines.
132-
const SCAN_CHUNK: u64 = 64 * 1024;
133-
134-
const scan_buf = try allocator.alloc(u8, SCAN_CHUNK);
125+
const scan_buf = try allocator.alloc(u8, READ_BUF_SIZE);
135126
defer allocator.free(scan_buf);
136127

137-
// Scan backwards in SCAN_CHUNK windows until we have found N newlines.
128+
// Scan backwards in READ_BUF_SIZE windows until N newlines are found.
138129
var newlines_found: usize = 0;
139-
var read_from: u64 = 0; // absolute file offset to start forward reading from
130+
var read_from: u64 = 0;
140131
var scan_end: u64 = file_size;
141132

142133
outer: while (scan_end > 0) {
143-
const chunk_size: u64 = @min(scan_end, SCAN_CHUNK);
134+
const chunk_size: u64 = @min(scan_end, READ_BUF_SIZE);
144135
const chunk_start: u64 = scan_end - chunk_size;
145136

146137
try file.seekTo(chunk_start);
147138
const bytes_read = try file.read(scan_buf[0..chunk_size]);
148139

149-
// Walk backwards through this chunk counting newlines.
150140
var idx: usize = bytes_read;
151141
while (idx > 0) {
152142
idx -= 1;
153143
if (scan_buf[idx] == '\n') {
154144
newlines_found += 1;
155145
if (newlines_found == n) {
156-
// The line after this newline is where we start reading.
157146
read_from = chunk_start + idx + 1;
158147
break :outer;
159148
}
160149
}
161150
}
162151

163-
if (chunk_start == 0) break; // reached the beginning
152+
if (chunk_start == 0) break;
164153
scan_end = chunk_start;
165154
}
166155

167156
// read_from stays 0 if fewer than N newlines exist in the entire file.
168157
try file.seekTo(read_from);
169158

170-
var carry = try std.ArrayList(u8).initCapacity(allocator, 4096);
159+
var carry = std.ArrayList(u8){};
171160
defer carry.deinit(allocator);
172161
var pos: u64 = read_from;
173162

@@ -194,9 +183,7 @@ fn readAvailable(
194183
/// `filter_state.printIfMatch`. Partial trailing lines are saved in `carry`
195184
/// and prepended on the next call. `position` is advanced by bytes read.
196185
///
197-
/// NOTE: empty lines (consecutive newlines) are silently skipped. This is
198-
/// intentional for tail output but means blank separators between log entries
199-
/// are not forwarded to the filter. Revisit if blank-line-aware formats are added.
186+
/// NOTE: empty lines (consecutive newlines) are silently skipped.
200187
pub fn readToEOF(
201188
allocator: std.mem.Allocator,
202189
file: *std.fs.File,
@@ -212,10 +199,8 @@ pub fn readToEOF(
212199
position.* += n;
213200
var slice = buf[0..n];
214201

215-
// BUG FIX: the old code did `defer allocator.free(combined)` inside the
216-
// `if` block, which freed `combined` before the line-scanning loop below
217-
// could use `slice` — a use-after-free. The fix hoists the defer outside
218-
// the `if` so `combined` lives for the full iteration of the outer loop.
202+
// Hoist combined lifetime outside the if-block to prevent use-after-free:
203+
// `defer allocator.free` inside the if would free before the scan loop runs.
219204
var combined: ?[]u8 = null;
220205
defer if (combined) |c| allocator.free(c);
221206

@@ -233,7 +218,6 @@ pub fn readToEOF(
233218
start = nl + 1;
234219
}
235220

236-
// Save the trailing partial line for the next iteration.
237221
if (start < slice.len) {
238222
try carry.appendSlice(allocator, slice[start..]);
239223
}

0 commit comments

Comments
 (0)