1use std::io::{self, Read, Seek, Write};
2use std::path::Path;
3
4use memchr::{memchr_iter, memrchr_iter};
5
6use crate::common::io::{FileData, read_file, read_stdin};
7
8#[derive(Clone, Debug)]
10pub enum TailMode {
11 Lines(u64),
13 LinesFrom(u64),
15 Bytes(u64),
17 BytesFrom(u64),
19}
20
21#[derive(Clone, Debug, PartialEq)]
23pub enum FollowMode {
24 None,
25 Descriptor,
26 Name,
27}
28
29#[derive(Clone, Debug)]
31pub struct TailConfig {
32 pub mode: TailMode,
33 pub follow: FollowMode,
34 pub retry: bool,
35 pub pid: Option<u32>,
36 pub sleep_interval: f64,
37 pub max_unchanged_stats: u64,
38 pub zero_terminated: bool,
39}
40
41impl Default for TailConfig {
42 fn default() -> Self {
43 Self {
44 mode: TailMode::Lines(10),
45 follow: FollowMode::None,
46 retry: false,
47 pid: None,
48 sleep_interval: 1.0,
49 max_unchanged_stats: 5,
50 zero_terminated: false,
51 }
52 }
53}
54
55pub fn parse_size(s: &str) -> Result<u64, String> {
57 crate::head::parse_size(s)
58}
59
60pub fn tail_lines(data: &[u8], n: u64, delimiter: u8, out: &mut impl Write) -> io::Result<()> {
62 if n == 0 || data.is_empty() {
63 return Ok(());
64 }
65
66 let mut count = 0u64;
68
69 let search_end = if !data.is_empty() && data[data.len() - 1] == delimiter {
71 data.len() - 1
72 } else {
73 data.len()
74 };
75
76 for pos in memrchr_iter(delimiter, &data[..search_end]) {
77 count += 1;
78 if count == n {
79 return out.write_all(&data[pos + 1..]);
80 }
81 }
82
83 out.write_all(data)
85}
86
87pub fn tail_lines_from(data: &[u8], n: u64, delimiter: u8, out: &mut impl Write) -> io::Result<()> {
89 if data.is_empty() {
90 return Ok(());
91 }
92
93 if n <= 1 {
94 return out.write_all(data);
95 }
96
97 let skip = n - 1;
99 let mut count = 0u64;
100
101 for pos in memchr_iter(delimiter, data) {
102 count += 1;
103 if count == skip {
104 let start = pos + 1;
105 if start < data.len() {
106 return out.write_all(&data[start..]);
107 }
108 return Ok(());
109 }
110 }
111
112 Ok(())
114}
115
116pub fn tail_bytes(data: &[u8], n: u64, out: &mut impl Write) -> io::Result<()> {
118 if n == 0 || data.is_empty() {
119 return Ok(());
120 }
121
122 let n = n.min(data.len() as u64) as usize;
123 out.write_all(&data[data.len() - n..])
124}
125
126pub fn tail_bytes_from(data: &[u8], n: u64, out: &mut impl Write) -> io::Result<()> {
128 if data.is_empty() {
129 return Ok(());
130 }
131
132 if n <= 1 {
133 return out.write_all(data);
134 }
135
136 let start = ((n - 1) as usize).min(data.len());
137 if start < data.len() {
138 out.write_all(&data[start..])
139 } else {
140 Ok(())
141 }
142}
143
144#[cfg(target_os = "linux")]
146pub fn sendfile_tail_bytes(path: &Path, n: u64, out_fd: i32) -> io::Result<bool> {
147 use std::os::unix::fs::OpenOptionsExt;
148
149 let file = std::fs::OpenOptions::new()
150 .read(true)
151 .custom_flags(libc::O_NOATIME)
152 .open(path)
153 .or_else(|_| std::fs::File::open(path))?;
154
155 let metadata = file.metadata()?;
156 let file_size = metadata.len();
157
158 if file_size == 0 {
159 return Ok(true);
160 }
161
162 let n = n.min(file_size);
163 let start = file_size - n;
164
165 use std::os::unix::io::AsRawFd;
166 let in_fd = file.as_raw_fd();
167 let mut offset: libc::off_t = start as libc::off_t;
168 let mut remaining = n as usize;
169
170 while remaining > 0 {
171 let chunk = remaining.min(0x7ffff000);
172 let ret = unsafe { libc::sendfile(out_fd, in_fd, &mut offset, chunk) };
173 if ret > 0 {
174 remaining -= ret as usize;
175 } else if ret == 0 {
176 break;
177 } else {
178 let err = io::Error::last_os_error();
179 if err.kind() == io::ErrorKind::Interrupted {
180 continue;
181 }
182 return Err(err);
183 }
184 }
185
186 Ok(true)
187}
188
189fn tail_lines_streaming_file(
192 path: &Path,
193 n: u64,
194 delimiter: u8,
195 out: &mut impl Write,
196) -> io::Result<bool> {
197 if n == 0 {
198 return Ok(true);
199 }
200
201 #[cfg(target_os = "linux")]
202 let file = {
203 use std::os::unix::fs::OpenOptionsExt;
204 std::fs::OpenOptions::new()
205 .read(true)
206 .custom_flags(libc::O_NOATIME)
207 .open(path)
208 .or_else(|_| std::fs::File::open(path))?
209 };
210 #[cfg(not(target_os = "linux"))]
211 let file = std::fs::File::open(path)?;
212
213 let metadata = file.metadata()?;
214 let file_size = metadata.len();
215
216 if file_size == 0 {
217 return Ok(true);
218 }
219
220 #[cfg(target_os = "linux")]
223 {
224 use std::os::unix::io::AsRawFd;
225 let fd = file.as_raw_fd();
226 let ptr = unsafe {
227 libc::mmap(
228 std::ptr::null_mut(),
229 file_size as libc::size_t,
230 libc::PROT_READ,
231 libc::MAP_PRIVATE | libc::MAP_NORESERVE,
232 fd,
233 0,
234 )
235 };
236 if ptr != libc::MAP_FAILED {
237 let _ = unsafe { libc::madvise(ptr, file_size as libc::size_t, libc::MADV_SEQUENTIAL) };
239 let data = unsafe { std::slice::from_raw_parts(ptr as *const u8, file_size as usize) };
240 let result = tail_lines(data, n, delimiter, out);
241 unsafe {
242 libc::munmap(ptr, file_size as libc::size_t);
243 }
244 return result.map(|_| true);
245 }
246 }
247
248 const CHUNK: u64 = 262144;
250 let mut chunks: Vec<Vec<u8>> = Vec::new();
251 let mut pos = file_size;
252 let mut count = 0u64;
253 let mut found_start = false;
254
255 let mut reader = file;
256
257 while pos > 0 {
258 let read_start = if pos > CHUNK { pos - CHUNK } else { 0 };
259 let read_len = (pos - read_start) as usize;
260
261 reader.seek(io::SeekFrom::Start(read_start))?;
262 let mut buf = vec![0u8; read_len];
263 reader.read_exact(&mut buf)?;
264
265 let search_end = if pos == file_size && !buf.is_empty() && buf[buf.len() - 1] == delimiter {
267 buf.len() - 1
268 } else {
269 buf.len()
270 };
271
272 for rpos in memrchr_iter(delimiter, &buf[..search_end]) {
273 count += 1;
274 if count == n {
275 out.write_all(&buf[rpos + 1..])?;
276 for chunk in chunks.iter().rev() {
277 out.write_all(chunk)?;
278 }
279 found_start = true;
280 break;
281 }
282 }
283
284 if found_start {
285 break;
286 }
287
288 chunks.push(buf);
289 pos = read_start;
290 }
291
292 if !found_start {
293 for chunk in chunks.iter().rev() {
295 out.write_all(chunk)?;
296 }
297 }
298
299 Ok(true)
300}
301
302fn tail_lines_from_streaming_file(
304 path: &Path,
305 n: u64,
306 delimiter: u8,
307 out: &mut impl Write,
308) -> io::Result<bool> {
309 #[cfg(target_os = "linux")]
310 let file = {
311 use std::os::unix::fs::OpenOptionsExt;
312 std::fs::OpenOptions::new()
313 .read(true)
314 .custom_flags(libc::O_NOATIME)
315 .open(path)
316 .or_else(|_| std::fs::File::open(path))?
317 };
318 #[cfg(not(target_os = "linux"))]
319 let file = std::fs::File::open(path)?;
320
321 if n <= 1 {
322 #[cfg(target_os = "linux")]
324 {
325 use std::os::unix::io::AsRawFd;
326 let in_fd = file.as_raw_fd();
327 let stdout = io::stdout();
328 let out_fd = stdout.as_raw_fd();
329 let file_size = file.metadata()?.len() as usize;
330 return sendfile_to_stdout_raw(in_fd, file_size, out_fd);
331 }
332 #[cfg(not(target_os = "linux"))]
333 {
334 let mut reader = io::BufReader::with_capacity(1024 * 1024, file);
335 let mut buf = [0u8; 262144];
336 loop {
337 let n = match reader.read(&mut buf) {
338 Ok(0) => break,
339 Ok(n) => n,
340 Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
341 Err(e) => return Err(e),
342 };
343 out.write_all(&buf[..n])?;
344 }
345 return Ok(true);
346 }
347 }
348
349 let skip = n - 1;
350 let mut reader = io::BufReader::with_capacity(1024 * 1024, file);
351 let mut buf = [0u8; 262144];
352 let mut count = 0u64;
353 let mut skipping = true;
354
355 loop {
356 let bytes_read = match reader.read(&mut buf) {
357 Ok(0) => break,
358 Ok(n) => n,
359 Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
360 Err(e) => return Err(e),
361 };
362
363 let chunk = &buf[..bytes_read];
364
365 if skipping {
366 for pos in memchr_iter(delimiter, chunk) {
367 count += 1;
368 if count == skip {
369 let start = pos + 1;
371 if start < chunk.len() {
372 out.write_all(&chunk[start..])?;
373 }
374 skipping = false;
375 break;
376 }
377 }
378 } else {
379 out.write_all(chunk)?;
380 }
381 }
382
383 Ok(true)
384}
385
386#[cfg(target_os = "linux")]
388fn sendfile_to_stdout_raw(in_fd: i32, file_size: usize, out_fd: i32) -> io::Result<bool> {
389 let mut offset: libc::off_t = 0;
390 let mut remaining = file_size;
391 while remaining > 0 {
392 let chunk = remaining.min(0x7ffff000);
393 let ret = unsafe { libc::sendfile(out_fd, in_fd, &mut offset, chunk) };
394 if ret > 0 {
395 remaining -= ret as usize;
396 } else if ret == 0 {
397 break;
398 } else {
399 let err = io::Error::last_os_error();
400 if err.kind() == io::ErrorKind::Interrupted {
401 continue;
402 }
403 return Err(err);
404 }
405 }
406 Ok(true)
407}
408
409pub fn tail_file(
411 filename: &str,
412 config: &TailConfig,
413 out: &mut impl Write,
414 tool_name: &str,
415) -> io::Result<bool> {
416 let delimiter = if config.zero_terminated { b'\0' } else { b'\n' };
417
418 if filename != "-" {
419 let path = Path::new(filename);
420
421 match &config.mode {
422 TailMode::Lines(n) => {
423 match tail_lines_streaming_file(path, *n, delimiter, out) {
425 Ok(true) => return Ok(true),
426 Err(e) => {
427 eprintln!(
428 "{}: cannot open '{}' for reading: {}",
429 tool_name,
430 filename,
431 crate::common::io_error_msg(&e)
432 );
433 return Ok(false);
434 }
435 _ => {}
436 }
437 }
438 TailMode::LinesFrom(n) => {
439 match tail_lines_from_streaming_file(path, *n, delimiter, out) {
441 Ok(true) => return Ok(true),
442 Err(e) => {
443 eprintln!(
444 "{}: cannot open '{}' for reading: {}",
445 tool_name,
446 filename,
447 crate::common::io_error_msg(&e)
448 );
449 return Ok(false);
450 }
451 _ => {}
452 }
453 }
454 TailMode::Bytes(_n) => {
455 #[cfg(target_os = "linux")]
456 {
457 use std::os::unix::io::AsRawFd;
458 let stdout = io::stdout();
459 let out_fd = stdout.as_raw_fd();
460 if let Ok(true) = sendfile_tail_bytes(path, *_n, out_fd) {
461 return Ok(true);
462 }
463 }
464 }
465 TailMode::BytesFrom(_n) => {
466 #[cfg(target_os = "linux")]
467 {
468 use std::os::unix::io::AsRawFd;
469 let stdout = io::stdout();
470 let out_fd = stdout.as_raw_fd();
471 if let Ok(true) = sendfile_tail_bytes_from(path, *_n, out_fd) {
472 return Ok(true);
473 }
474 }
475 }
476 }
477 }
478
479 let data: FileData = if filename == "-" {
481 match read_stdin() {
482 Ok(d) => FileData::Owned(d),
483 Err(e) => {
484 eprintln!(
485 "{}: standard input: {}",
486 tool_name,
487 crate::common::io_error_msg(&e)
488 );
489 return Ok(false);
490 }
491 }
492 } else {
493 match read_file(Path::new(filename)) {
494 Ok(d) => d,
495 Err(e) => {
496 eprintln!(
497 "{}: cannot open '{}' for reading: {}",
498 tool_name,
499 filename,
500 crate::common::io_error_msg(&e)
501 );
502 return Ok(false);
503 }
504 }
505 };
506
507 match &config.mode {
508 TailMode::Lines(n) => tail_lines(&data, *n, delimiter, out)?,
509 TailMode::LinesFrom(n) => tail_lines_from(&data, *n, delimiter, out)?,
510 TailMode::Bytes(n) => tail_bytes(&data, *n, out)?,
511 TailMode::BytesFrom(n) => tail_bytes_from(&data, *n, out)?,
512 }
513
514 Ok(true)
515}
516
517#[cfg(target_os = "linux")]
519fn sendfile_tail_bytes_from(path: &Path, n: u64, out_fd: i32) -> io::Result<bool> {
520 use std::os::unix::fs::OpenOptionsExt;
521
522 let file = std::fs::OpenOptions::new()
523 .read(true)
524 .custom_flags(libc::O_NOATIME)
525 .open(path)
526 .or_else(|_| std::fs::File::open(path))?;
527
528 let metadata = file.metadata()?;
529 let file_size = metadata.len();
530
531 if file_size == 0 {
532 return Ok(true);
533 }
534
535 let start = if n <= 1 { 0 } else { (n - 1).min(file_size) };
536
537 if start >= file_size {
538 return Ok(true);
539 }
540
541 use std::os::unix::io::AsRawFd;
542 let in_fd = file.as_raw_fd();
543 let mut offset: libc::off_t = start as libc::off_t;
544 let mut remaining = (file_size - start) as usize;
545
546 while remaining > 0 {
547 let chunk = remaining.min(0x7ffff000);
548 let ret = unsafe { libc::sendfile(out_fd, in_fd, &mut offset, chunk) };
549 if ret > 0 {
550 remaining -= ret as usize;
551 } else if ret == 0 {
552 break;
553 } else {
554 let err = io::Error::last_os_error();
555 if err.kind() == io::ErrorKind::Interrupted {
556 continue;
557 }
558 return Err(err);
559 }
560 }
561
562 Ok(true)
563}
564
565#[cfg(target_os = "linux")]
567pub fn follow_file(filename: &str, config: &TailConfig, out: &mut impl Write) -> io::Result<()> {
568 use std::thread;
569 use std::time::Duration;
570
571 let sleep_duration = Duration::from_secs_f64(config.sleep_interval);
572 let path = Path::new(filename);
573
574 let mut last_size = match std::fs::metadata(path) {
575 Ok(m) => m.len(),
576 Err(_) => 0,
577 };
578
579 loop {
580 if let Some(pid) = config.pid {
582 if unsafe { libc::kill(pid as i32, 0) } != 0 {
583 break;
584 }
585 }
586
587 thread::sleep(sleep_duration);
588
589 let current_size = match std::fs::metadata(path) {
590 Ok(m) => m.len(),
591 Err(_) => {
592 if config.retry {
593 continue;
594 }
595 break;
596 }
597 };
598
599 if current_size > last_size {
600 let file = std::fs::File::open(path)?;
602 use std::os::unix::io::AsRawFd;
603 let in_fd = file.as_raw_fd();
604 let stdout = io::stdout();
605 let out_fd = stdout.as_raw_fd();
606 let mut offset = last_size as libc::off_t;
607 let mut remaining = (current_size - last_size) as usize;
608
609 while remaining > 0 {
610 let chunk = remaining.min(0x7ffff000);
611 let ret = unsafe { libc::sendfile(out_fd, in_fd, &mut offset, chunk) };
612 if ret > 0 {
613 remaining -= ret as usize;
614 } else if ret == 0 {
615 break;
616 } else {
617 let err = io::Error::last_os_error();
618 if err.kind() == io::ErrorKind::Interrupted {
619 continue;
620 }
621 return Err(err);
622 }
623 }
624 let _ = out.flush();
625 last_size = current_size;
626 } else if current_size < last_size {
627 last_size = current_size;
629 }
630 }
631
632 Ok(())
633}
634
635#[cfg(not(target_os = "linux"))]
636pub fn follow_file(filename: &str, config: &TailConfig, out: &mut impl Write) -> io::Result<()> {
637 use std::io::{Read, Seek};
638 use std::thread;
639 use std::time::Duration;
640
641 let sleep_duration = Duration::from_secs_f64(config.sleep_interval);
642 let path = Path::new(filename);
643
644 let mut last_size = match std::fs::metadata(path) {
645 Ok(m) => m.len(),
646 Err(_) => 0,
647 };
648
649 loop {
650 thread::sleep(sleep_duration);
651
652 let current_size = match std::fs::metadata(path) {
653 Ok(m) => m.len(),
654 Err(_) => {
655 if config.retry {
656 continue;
657 }
658 break;
659 }
660 };
661
662 if current_size > last_size {
663 let mut file = std::fs::File::open(path)?;
664 file.seek(io::SeekFrom::Start(last_size))?;
665 let mut buf = vec![0u8; (current_size - last_size) as usize];
666 file.read_exact(&mut buf)?;
667 out.write_all(&buf)?;
668 out.flush()?;
669 last_size = current_size;
670 } else if current_size < last_size {
671 last_size = current_size;
672 }
673 }
674
675 Ok(())
676}