From ad4e60e8be25fafa67e4ebfbd0637aa03d1dc61e Mon Sep 17 00:00:00 2001 From: Paul Holzinger Date: Wed, 26 Jun 2024 16:02:24 +0200 Subject: [PATCH] StopAtEOF: keep sending lines until EOF When a StopAtEOF() is called the code should continue to send all lines to the Lines channel. The issue here is if the caller is not ready to receive a new line the code blocks as it is using a unbuffered channel. However <-tail.Dying() would return in this case so the line was skipped. This means that the caller did not get all lines until EOF. Now we still want to skip in case any other reason for kill was given therefore add special logic to only not read the Dying channel on the EOF case. The one downside is that StopAtEOF() could block forever if the caller never reads new Lines but this seems logical to me. If the caller wants to wait for EOF but never reads remaining Lines this would be a bug on their end. Fixes #37 Signed-off-by: Paul Holzinger --- tail.go | 25 +++++++++++++++++++------ tail_test.go | 32 ++++++++++++++++++++++++++++++-- 2 files changed, 49 insertions(+), 8 deletions(-) diff --git a/tail.go b/tail.go index c962599..af5029b 100644 --- a/tail.go +++ b/tail.go @@ -2,11 +2,11 @@ // Copyright (c) 2015 HPE Software Inc. All rights reserved. // Copyright (c) 2013 ActiveState Software Inc. All rights reserved. -//nxadm/tail provides a Go library that emulates the features of the BSD `tail` -//program. The library comes with full support for truncation/move detection as -//it is designed to work with log rotation tools. The library works on all -//operating systems supported by Go, including POSIX systems like Linux and -//*BSD, and MS Windows. Go 1.9 is the oldest compiler release supported. +// nxadm/tail provides a Go library that emulates the features of the BSD `tail` +// program. The library comes with full support for truncation/move detection as +// it is designed to work with log rotation tools. The library works on all +// operating systems supported by Go, including POSIX systems like Linux and +// *BSD, and MS Windows. Go 1.9 is the oldest compiler release supported. package tail import ( @@ -450,12 +450,25 @@ func (tail *Tail) sendLine(line string) bool { lines = util.PartitionString(line, tail.MaxLineSize) } + // This is a bit weird here, when a users requests stopAtEof we + // must keep sending all lines however <-tail.Dying() will return + // immediately at this point so the select below may not have + // chance to send the line if the reader side has is not yet ready. + // But if StopAtEOF was not set and it is a "normal" Kill then we + // should exit right away still thus the special logic here. + earlyExitChan := tail.Dying() + if tail.Err() == errStopAtEOF { + // Note that receive from a nil channel blocks forever so + // below we know it can only take the tail.Lines case. + earlyExitChan = nil + } + for _, line := range lines { tail.lineNum++ offset, _ := tail.Tell() select { case tail.Lines <- &Line{line, tail.lineNum, SeekInfo{Offset: offset}, now, nil}: - case <-tail.Dying(): + case <-earlyExitChan: return true } } diff --git a/tail_test.go b/tail_test.go index 7b9319e..cd2a0a2 100644 --- a/tail_test.go +++ b/tail_test.go @@ -622,6 +622,26 @@ func TestIncompleteLinesWithoutFollow(t *testing.T) { tail.Cleanup() } +func TestFollowUntilEof(t *testing.T) { + tailTest, cleanup := NewTailTest("incomplete-lines-no-follow", t) + defer cleanup() + filename := "test.txt" + config := Config{ + Follow: false, + } + tailTest.CreateFile(filename, "hello\nworld\n") + tail := tailTest.StartTail(filename, config) + + // StopAtEOF blocks until the read is done and in order to do so + // we have to drain the lines channel first which ReadLinesWithError does. + go tail.StopAtEOF() + tailTest.ReadLinesWithError(tail, []string{"hello", "world"}, false, errStopAtEOF) + + tailTest.RemoveFile(filename) + tail.Stop() + tail.Cleanup() +} + func reSeek(t *testing.T, poll bool) { var name string if poll { @@ -765,6 +785,14 @@ func (t TailTest) VerifyTailOutputUsingCursor(tail *Tail, lines []string, expect } func (t TailTest) ReadLines(tail *Tail, lines []string, useCursor bool) { + t.readLines(tail, lines, useCursor, nil) +} + +func (t TailTest) ReadLinesWithError(tail *Tail, lines []string, useCursor bool, err error) { + t.readLines(tail, lines, useCursor, err) +} + +func (t TailTest) readLines(tail *Tail, lines []string, useCursor bool, expectErr error) { cursor := 1 for _, line := range lines { @@ -773,8 +801,8 @@ func (t TailTest) ReadLines(tail *Tail, lines []string, useCursor bool) { if !ok { // tail.Lines is closed and empty. err := tail.Err() - if err != nil { - t.Fatalf("tail ended with error: %v", err) + if err != expectErr { + t.Fatalf("tail ended with unexpected error: %v", err) } t.Fatalf("tail ended early; expecting more: %v", lines[cursor:]) }