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 const CHUNK: u64 = 65536;
222 let mut chunks: Vec<Vec<u8>> = Vec::new();
223 let mut pos = file_size;
224 let mut count = 0u64;
225 let mut found_start = false;
226
227 let mut reader = file;
228
229 while pos > 0 {
230 let read_start = if pos > CHUNK { pos - CHUNK } else { 0 };
231 let read_len = (pos - read_start) as usize;
232
233 reader.seek(io::SeekFrom::Start(read_start))?;
234 let mut buf = vec![0u8; read_len];
235 reader.read_exact(&mut buf)?;
236
237 let search_end = if pos == file_size && !buf.is_empty() && buf[buf.len() - 1] == delimiter {
239 buf.len() - 1
240 } else {
241 buf.len()
242 };
243
244 for i in (0..search_end).rev() {
245 if buf[i] == delimiter {
246 count += 1;
247 if count == n {
248 out.write_all(&buf[i + 1..])?;
250 for chunk in chunks.iter().rev() {
251 out.write_all(chunk)?;
252 }
253 found_start = true;
254 break;
255 }
256 }
257 }
258
259 if found_start {
260 break;
261 }
262
263 chunks.push(buf);
264 pos = read_start;
265 }
266
267 if !found_start {
268 for chunk in chunks.iter().rev() {
270 out.write_all(chunk)?;
271 }
272 }
273
274 Ok(true)
275}
276
277fn tail_lines_from_streaming_file(
279 path: &Path,
280 n: u64,
281 delimiter: u8,
282 out: &mut impl Write,
283) -> io::Result<bool> {
284 #[cfg(target_os = "linux")]
285 let file = {
286 use std::os::unix::fs::OpenOptionsExt;
287 std::fs::OpenOptions::new()
288 .read(true)
289 .custom_flags(libc::O_NOATIME)
290 .open(path)
291 .or_else(|_| std::fs::File::open(path))?
292 };
293 #[cfg(not(target_os = "linux"))]
294 let file = std::fs::File::open(path)?;
295
296 if n <= 1 {
297 #[cfg(target_os = "linux")]
299 {
300 use std::os::unix::io::AsRawFd;
301 let in_fd = file.as_raw_fd();
302 let stdout = io::stdout();
303 let out_fd = stdout.as_raw_fd();
304 let file_size = file.metadata()?.len() as usize;
305 return sendfile_to_stdout_raw(in_fd, file_size, out_fd);
306 }
307 #[cfg(not(target_os = "linux"))]
308 {
309 let mut reader = io::BufReader::with_capacity(65536, file);
310 let mut buf = [0u8; 65536];
311 loop {
312 let n = match reader.read(&mut buf) {
313 Ok(0) => break,
314 Ok(n) => n,
315 Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
316 Err(e) => return Err(e),
317 };
318 out.write_all(&buf[..n])?;
319 }
320 return Ok(true);
321 }
322 }
323
324 let skip = n - 1;
325 let mut reader = io::BufReader::with_capacity(65536, file);
326 let mut buf = [0u8; 65536];
327 let mut count = 0u64;
328 let mut skipping = true;
329
330 loop {
331 let bytes_read = match reader.read(&mut buf) {
332 Ok(0) => break,
333 Ok(n) => n,
334 Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
335 Err(e) => return Err(e),
336 };
337
338 let chunk = &buf[..bytes_read];
339
340 if skipping {
341 for pos in memchr_iter(delimiter, chunk) {
342 count += 1;
343 if count == skip {
344 let start = pos + 1;
346 if start < chunk.len() {
347 out.write_all(&chunk[start..])?;
348 }
349 skipping = false;
350 break;
351 }
352 }
353 } else {
354 out.write_all(chunk)?;
355 }
356 }
357
358 Ok(true)
359}
360
361#[cfg(target_os = "linux")]
363fn sendfile_to_stdout_raw(in_fd: i32, file_size: usize, out_fd: i32) -> io::Result<bool> {
364 let mut offset: libc::off_t = 0;
365 let mut remaining = file_size;
366 while remaining > 0 {
367 let chunk = remaining.min(0x7ffff000);
368 let ret = unsafe { libc::sendfile(out_fd, in_fd, &mut offset, chunk) };
369 if ret > 0 {
370 remaining -= ret as usize;
371 } else if ret == 0 {
372 break;
373 } else {
374 let err = io::Error::last_os_error();
375 if err.kind() == io::ErrorKind::Interrupted {
376 continue;
377 }
378 return Err(err);
379 }
380 }
381 Ok(true)
382}
383
384pub fn tail_file(
386 filename: &str,
387 config: &TailConfig,
388 out: &mut impl Write,
389 tool_name: &str,
390) -> io::Result<bool> {
391 let delimiter = if config.zero_terminated { b'\0' } else { b'\n' };
392
393 if filename != "-" {
394 let path = Path::new(filename);
395
396 match &config.mode {
397 TailMode::Lines(n) => {
398 match tail_lines_streaming_file(path, *n, delimiter, out) {
400 Ok(true) => return Ok(true),
401 Err(e) => {
402 eprintln!(
403 "{}: cannot open '{}' for reading: {}",
404 tool_name,
405 filename,
406 crate::common::io_error_msg(&e)
407 );
408 return Ok(false);
409 }
410 _ => {}
411 }
412 }
413 TailMode::LinesFrom(n) => {
414 match tail_lines_from_streaming_file(path, *n, delimiter, out) {
416 Ok(true) => return Ok(true),
417 Err(e) => {
418 eprintln!(
419 "{}: cannot open '{}' for reading: {}",
420 tool_name,
421 filename,
422 crate::common::io_error_msg(&e)
423 );
424 return Ok(false);
425 }
426 _ => {}
427 }
428 }
429 TailMode::Bytes(_n) => {
430 #[cfg(target_os = "linux")]
431 {
432 use std::os::unix::io::AsRawFd;
433 let stdout = io::stdout();
434 let out_fd = stdout.as_raw_fd();
435 if let Ok(true) = sendfile_tail_bytes(path, *_n, out_fd) {
436 return Ok(true);
437 }
438 }
439 }
440 TailMode::BytesFrom(_n) => {
441 #[cfg(target_os = "linux")]
442 {
443 use std::os::unix::io::AsRawFd;
444 let stdout = io::stdout();
445 let out_fd = stdout.as_raw_fd();
446 if let Ok(true) = sendfile_tail_bytes_from(path, *_n, out_fd) {
447 return Ok(true);
448 }
449 }
450 }
451 }
452 }
453
454 let data: FileData = if filename == "-" {
456 match read_stdin() {
457 Ok(d) => FileData::Owned(d),
458 Err(e) => {
459 eprintln!(
460 "{}: standard input: {}",
461 tool_name,
462 crate::common::io_error_msg(&e)
463 );
464 return Ok(false);
465 }
466 }
467 } else {
468 match read_file(Path::new(filename)) {
469 Ok(d) => d,
470 Err(e) => {
471 eprintln!(
472 "{}: cannot open '{}' for reading: {}",
473 tool_name,
474 filename,
475 crate::common::io_error_msg(&e)
476 );
477 return Ok(false);
478 }
479 }
480 };
481
482 match &config.mode {
483 TailMode::Lines(n) => tail_lines(&data, *n, delimiter, out)?,
484 TailMode::LinesFrom(n) => tail_lines_from(&data, *n, delimiter, out)?,
485 TailMode::Bytes(n) => tail_bytes(&data, *n, out)?,
486 TailMode::BytesFrom(n) => tail_bytes_from(&data, *n, out)?,
487 }
488
489 Ok(true)
490}
491
492#[cfg(target_os = "linux")]
494fn sendfile_tail_bytes_from(path: &Path, n: u64, out_fd: i32) -> io::Result<bool> {
495 use std::os::unix::fs::OpenOptionsExt;
496
497 let file = std::fs::OpenOptions::new()
498 .read(true)
499 .custom_flags(libc::O_NOATIME)
500 .open(path)
501 .or_else(|_| std::fs::File::open(path))?;
502
503 let metadata = file.metadata()?;
504 let file_size = metadata.len();
505
506 if file_size == 0 {
507 return Ok(true);
508 }
509
510 let start = if n <= 1 { 0 } else { (n - 1).min(file_size) };
511
512 if start >= file_size {
513 return Ok(true);
514 }
515
516 use std::os::unix::io::AsRawFd;
517 let in_fd = file.as_raw_fd();
518 let mut offset: libc::off_t = start as libc::off_t;
519 let mut remaining = (file_size - start) as usize;
520
521 while remaining > 0 {
522 let chunk = remaining.min(0x7ffff000);
523 let ret = unsafe { libc::sendfile(out_fd, in_fd, &mut offset, chunk) };
524 if ret > 0 {
525 remaining -= ret as usize;
526 } else if ret == 0 {
527 break;
528 } else {
529 let err = io::Error::last_os_error();
530 if err.kind() == io::ErrorKind::Interrupted {
531 continue;
532 }
533 return Err(err);
534 }
535 }
536
537 Ok(true)
538}
539
540#[cfg(target_os = "linux")]
542pub fn follow_file(filename: &str, config: &TailConfig, out: &mut impl Write) -> io::Result<()> {
543 use std::thread;
544 use std::time::Duration;
545
546 let sleep_duration = Duration::from_secs_f64(config.sleep_interval);
547 let path = Path::new(filename);
548
549 let mut last_size = match std::fs::metadata(path) {
550 Ok(m) => m.len(),
551 Err(_) => 0,
552 };
553
554 loop {
555 if let Some(pid) = config.pid {
557 if unsafe { libc::kill(pid as i32, 0) } != 0 {
558 break;
559 }
560 }
561
562 thread::sleep(sleep_duration);
563
564 let current_size = match std::fs::metadata(path) {
565 Ok(m) => m.len(),
566 Err(_) => {
567 if config.retry {
568 continue;
569 }
570 break;
571 }
572 };
573
574 if current_size > last_size {
575 let file = std::fs::File::open(path)?;
577 use std::os::unix::io::AsRawFd;
578 let in_fd = file.as_raw_fd();
579 let stdout = io::stdout();
580 let out_fd = stdout.as_raw_fd();
581 let mut offset = last_size as libc::off_t;
582 let mut remaining = (current_size - last_size) as usize;
583
584 while remaining > 0 {
585 let chunk = remaining.min(0x7ffff000);
586 let ret = unsafe { libc::sendfile(out_fd, in_fd, &mut offset, chunk) };
587 if ret > 0 {
588 remaining -= ret as usize;
589 } else if ret == 0 {
590 break;
591 } else {
592 let err = io::Error::last_os_error();
593 if err.kind() == io::ErrorKind::Interrupted {
594 continue;
595 }
596 return Err(err);
597 }
598 }
599 let _ = out.flush();
600 last_size = current_size;
601 } else if current_size < last_size {
602 last_size = current_size;
604 }
605 }
606
607 Ok(())
608}
609
610#[cfg(not(target_os = "linux"))]
611pub fn follow_file(filename: &str, config: &TailConfig, out: &mut impl Write) -> io::Result<()> {
612 use std::io::{Read, Seek};
613 use std::thread;
614 use std::time::Duration;
615
616 let sleep_duration = Duration::from_secs_f64(config.sleep_interval);
617 let path = Path::new(filename);
618
619 let mut last_size = match std::fs::metadata(path) {
620 Ok(m) => m.len(),
621 Err(_) => 0,
622 };
623
624 loop {
625 thread::sleep(sleep_duration);
626
627 let current_size = match std::fs::metadata(path) {
628 Ok(m) => m.len(),
629 Err(_) => {
630 if config.retry {
631 continue;
632 }
633 break;
634 }
635 };
636
637 if current_size > last_size {
638 let mut file = std::fs::File::open(path)?;
639 file.seek(io::SeekFrom::Start(last_size))?;
640 let mut buf = vec![0u8; (current_size - last_size) as usize];
641 file.read_exact(&mut buf)?;
642 out.write_all(&buf)?;
643 out.flush()?;
644 last_size = current_size;
645 } else if current_size < last_size {
646 last_size = current_size;
647 }
648 }
649
650 Ok(())
651}