diff --git a/tail.go b/tail.go index c962599..29c8292 100644 --- a/tail.go +++ b/tail.go @@ -1,12 +1,11 @@ -// Copyright (c) 2019 FOSS contributors of https://github.com/nxadm/tail // 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 ( @@ -67,19 +66,29 @@ type logger interface { Println(v ...interface{}) } +type ReOpenBackoff struct { + Enable bool // Whether to enable the reopen backoff + MaxBackoff time.Duration // The maximum backoff amount. Default is 1s. Set to a negative value to uncap back off time. + MaxAttempts int // The maximum number of back off attempts. Default is 10. Set to a negative value for unlimited backoffs. + InitialBackoff time.Duration // The starting backoff duration. Default is 100ms. +} + // Config is used to specify how a file must be tailed. type Config struct { // File-specifc - Location *SeekInfo // Tail from this location. If nil, start at the beginning of the file - ReOpen bool // Reopen recreated files (tail -F) - MustExist bool // Fail early if the file does not exist - Poll bool // Poll for file changes instead of using the default inotify - Pipe bool // The file is a named pipe (mkfifo) + Location *SeekInfo // Tail from this location. If nil, start at the beginning of the file + ReOpen bool // Reopen recreated files (tail -F) + MustExist bool // Fail early if the file does not exist + Poll bool // Poll for file changes instead of using the default inotify + Pipe bool // The file is a named pipe (mkfifo) + ReOpenBackoff ReOpenBackoff // Generic IO - Follow bool // Continue looking for new lines (tail -f) - MaxLineSize int // If non-zero, split longer lines into multiple lines - CompleteLines bool // Only return complete lines (that end with "\n" or EOF when Follow is false) + Follow bool // Continue looking for new lines (tail -f) + MaxLineSize int // If non-zero, split longer lines into multiple lines + CompleteLines bool // Only return complete lines (that end with "\n" or EOF when Follow is false) + KeepBufferOnReOpen bool // Keep line buffer accross reopening a file. + CloseWhileWaiting bool // Whether or not to close the file while waiting for newlines. // Optionally, use a ratelimiter (e.g. created by the ratelimiter/NewLeakyBucket function) RateLimiter *ratelimiter.LeakyBucket @@ -98,6 +107,11 @@ type Tail struct { reader *bufio.Reader lineNum int + // Values to handle closing during wait + lastOffset int64 + lastLineNum int + keepBuffer bool + lineBuf *strings.Builder watcher watch.FileWatcher @@ -115,6 +129,26 @@ var ( DiscardingLogger = log.New(ioutil.Discard, "", 0) ) +func validateBackoffConfig(bc ReOpenBackoff) ReOpenBackoff { + if !bc.Enable { + return bc + } + + if bc.MaxAttempts == 0 { + bc.MaxAttempts = 10 + } + + if bc.InitialBackoff <= 0 { + bc.InitialBackoff = 100 * time.Millisecond + } + + if bc.MaxBackoff == 0 { + bc.MaxBackoff = 10 * time.Second + } + + return bc +} + // TailFile begins tailing the file. And returns a pointer to a Tail struct // and an error. An output stream is made available via the Tail.Lines // channel (e.g. to be looped and printed). To handle errors during tailing, @@ -125,6 +159,8 @@ func TailFile(filename string, config Config) (*Tail, error) { util.Fatal("cannot set ReOpen without Follow.") } + config.ReOpenBackoff = validateBackoffConfig(config.ReOpenBackoff) + t := &Tail{ Filename: filename, Lines: make(chan *Line), @@ -208,15 +244,61 @@ func (tail *Tail) closeFile() { } } +func (tail *Tail) openFileWithBackoff() (*os.File, error) { + f, err := OpenFile(tail.Filename) + if err != nil && os.IsNotExist(err) { + return nil, err + } + if err == nil { + return f, nil + } + + if !tail.ReOpenBackoff.Enable || tail.ReOpenBackoff.MaxAttempts == 0 { + return nil, err + } + + curBackoff := tail.ReOpenBackoff.InitialBackoff + currAttempt := 0 + + var lastErr error = err + + for { + if tail.ReOpenBackoff.MaxAttempts > 0 && + tail.ReOpenBackoff.MaxAttempts < currAttempt { + return nil, lastErr + } + select { + case <-tail.Dying(): + return nil, ErrStop + case <-time.After(curBackoff): + } + + currAttempt++ + f, err := OpenFile(tail.Filename) + if err != nil && os.IsNotExist(err) { + return nil, err + } + if err == nil { + return f, nil + } + lastErr = err + curBackoff = curBackoff * 2 + if tail.ReOpenBackoff.MaxBackoff <= 0 && + curBackoff > tail.ReOpenBackoff.MaxBackoff { + curBackoff = tail.ReOpenBackoff.MaxBackoff + } + } +} + func (tail *Tail) reopen() error { - if tail.lineBuf != nil { + if !tail.keepBuffer && !tail.KeepBufferOnReOpen && tail.lineBuf != nil { tail.lineBuf.Reset() } tail.closeFile() tail.lineNum = 0 for { var err error - tail.file, err = OpenFile(tail.Filename) + tail.file, err = tail.openFileWithBackoff() if err != nil { if os.IsNotExist(err) { tail.Logger.Printf("Waiting for %s to appear...", tail.Filename) @@ -267,6 +349,36 @@ func (tail *Tail) readLine() (string, error) { } } +func (tail *Tail) waitCloseFile() { + tail.lastLineNum = tail.lineNum + loc, err := tail.Tell() + tail.closeFile() + + if err != nil { + return + } + + tail.lastOffset = loc +} + +func (tail *Tail) waitReopenFile() { + err := tail.reopen() + tail.lineNum = tail.lastLineNum + if err != nil { + if err != tomb.ErrDying { + tail.Kill(err) + } + return + } + + _, err = tail.file.Seek(tail.lastOffset, io.SeekStart) + if err != nil { + tail.Killf("Seek error on %s: %s", tail.Filename, err) + return + } + tail.openReader() +} + func (tail *Tail) tailFileSync() { defer tail.Done() defer tail.close() @@ -314,6 +426,7 @@ func (tail *Tail) tailFileSync() { // file when rate limit is reached. msg := ("Too much log activity; waiting a second before resuming tailing") offset, _ := tail.Tell() + tail.lastOffset = offset tail.Lines <- &Line{msg, tail.lineNum, SeekInfo{Offset: offset}, time.Now(), errors.New(msg)} select { case <-time.After(time.Second): @@ -377,16 +490,33 @@ func (tail *Tail) waitForChanges() error { if err != nil { return err } + tail.lastOffset = pos tail.changes, err = tail.watcher.ChangeEvents(&tail.Tomb, pos) if err != nil { return err } } + if tail.CloseWhileWaiting { + // Since we are just waiting for the file to update, we don't want + // to reset our line buffer + tail.keepBuffer = true + tail.waitCloseFile() + } + select { case <-tail.changes.Modified: + if tail.CloseWhileWaiting { + tail.waitReopenFile() + // Now that the file is re-opened, we want to keep the normal + // line buffer reset logic + tail.keepBuffer = false + } return nil case <-tail.changes.Deleted: + // If the event is not an updated modification, we want to use the + // normal line buffer reset logic + tail.keepBuffer = false tail.changes = nil if tail.ReOpen { // XXX: we must not log from a library. @@ -401,6 +531,9 @@ func (tail *Tail) waitForChanges() error { tail.Logger.Printf("Stopping tail as file no longer exists: %s", tail.Filename) return ErrStop case <-tail.changes.Truncated: + // If the event is not an updated modification, we want to use the + // normal line buffer reset logic + tail.keepBuffer = false // Always reopen truncated files (Follow is true) tail.Logger.Printf("Re-opening truncated file %s ...", tail.Filename) if err := tail.reopen(); err != nil { @@ -410,6 +543,9 @@ func (tail *Tail) waitForChanges() error { tail.openReader() return nil case <-tail.Dying(): + // If the event is not an updated modification, we want to use the + // normal line buffer reset logic + tail.keepBuffer = false return ErrStop } } @@ -453,6 +589,7 @@ func (tail *Tail) sendLine(line string) bool { for _, line := range lines { tail.lineNum++ offset, _ := tail.Tell() + tail.lastOffset = offset select { case tail.Lines <- &Line{line, tail.lineNum, SeekInfo{Offset: offset}, now, nil}: case <-tail.Dying(): diff --git a/tail_test.go b/tail_test.go index 7b9319e..8747886 100644 --- a/tail_test.go +++ b/tail_test.go @@ -123,6 +123,27 @@ func TestStopNonEmptyFile(t *testing.T) { // success here is if it doesn't panic. } +func TestStopAtEOFWithCloseDuringWait(t *testing.T) { + tailTest, cleanup := NewTailTest("maxlinesize", t) + defer cleanup() + tailTest.CreateFile("test.txt", "hello\nthere\nworld\n") + tail := tailTest.StartTail("test.txt", Config{Follow: true, Location: nil, CloseWhileWaiting: true}) + + // read "hello" + line := <-tail.Lines + if line.Text != "hello" { + t.Errorf("Expected to get 'hello', got '%s' instead", line.Text) + } + + if line.Num != 1 { + t.Errorf("Expected to get 1, got %d instead", line.Num) + } + + tailTest.VerifyTailOutput(tail, []string{"there", "world"}, false) + tail.StopAtEOF() + tailTest.Cleanup(tail, true) +} + func TestStopAtEOF(t *testing.T) { tailTest, cleanup := NewTailTest("maxlinesize", t) defer cleanup() @@ -153,6 +174,21 @@ func TestMaxLineSizeNoFollow(t *testing.T) { maxLineSize(t, false, "hello\nworld\nfin\nhe", []string{"hel", "lo", "wor", "ld", "fin", "he"}) } +func TestOver4096ByteLineWithCloseDuringWait(t *testing.T) { + tailTest, cleanup := NewTailTest("Over4096ByteLine", t) + defer cleanup() + testString := strings.Repeat("a", 4097) + tailTest.CreateFile("test.txt", "test\n"+testString+"\nhello\nworld\n") + tail := tailTest.StartTail("test.txt", Config{Follow: true, Location: nil, CloseWhileWaiting: true}) + go tailTest.VerifyTailOutput(tail, []string{"test", testString, "hello", "world"}, false) + + // Delete after a reasonable delay, to give tail sufficient time + // to read all lines. + <-time.After(100 * time.Millisecond) + tailTest.RemoveFile("test.txt") + tailTest.Cleanup(tail, true) +} + func TestOver4096ByteLine(t *testing.T) { tailTest, cleanup := NewTailTest("Over4096ByteLine", t) defer cleanup() @@ -183,6 +219,21 @@ func TestOver4096ByteLineWithSetMaxLineSize(t *testing.T) { tailTest.Cleanup(tail, true) } +func TestOver4096ByteLineWithSetMaxLineSizeWithCloseDuringWait(t *testing.T) { + tailTest, cleanup := NewTailTest("Over4096ByteLineMaxLineSize", t) + defer cleanup() + testString := strings.Repeat("a", 4097) + tailTest.CreateFile("test.txt", "test\n"+testString+"\nhello\nworld\n") + tail := tailTest.StartTail("test.txt", Config{Follow: true, Location: nil, MaxLineSize: 4097, CloseWhileWaiting: true}) + go tailTest.VerifyTailOutput(tail, []string{"test", testString, "hello", "world"}, false) + + // Delete after a reasonable delay, to give tail sufficient time + // to read all lines. + <-time.After(100 * time.Millisecond) + tailTest.RemoveFile("test.txt") + tailTest.Cleanup(tail, true) +} + func TestReOpenWithCursor(t *testing.T) { delay := 300 * time.Millisecond // account for POLL_DURATION tailTest, cleanup := NewTailTest("reopen-cursor", t) @@ -218,6 +269,41 @@ func TestReOpenWithCursor(t *testing.T) { tailTest.Cleanup(tail, false) } +func TestReOpenWithCursorWithCloseDuringWait(t *testing.T) { + delay := 300 * time.Millisecond // account for POLL_DURATION + tailTest, cleanup := NewTailTest("reopen-cursor", t) + defer cleanup() + tailTest.CreateFile("test.txt", "hello\nworld\n") + tail := tailTest.StartTail( + "test.txt", + Config{Follow: true, ReOpen: true, Poll: true, CloseWhileWaiting: true}) + content := []string{"hello", "world", "more", "data", "endofworld"} + go tailTest.VerifyTailOutputUsingCursor(tail, content, false) + + // deletion must trigger reopen + <-time.After(delay) + tailTest.RemoveFile("test.txt") + <-time.After(delay) + tailTest.CreateFile("test.txt", "hello\nworld\nmore\ndata\n") + + // rename must trigger reopen + <-time.After(delay) + tailTest.RenameFile("test.txt", "test.txt.rotated") + <-time.After(delay) + tailTest.CreateFile("test.txt", "hello\nworld\nmore\ndata\nendofworld\n") + + // Delete after a reasonable delay, to give tail sufficient time + // to read all lines. + <-time.After(delay) + tailTest.RemoveFile("test.txt") + <-time.After(delay) + + // Do not bother with stopping as it could kill the tomb during + // the reading of data written above. Timings can vary based on + // test environment. + tailTest.Cleanup(tail, false) +} + func TestLocationFull(t *testing.T) { tailTest, cleanup := NewTailTest("location-full", t) defer cleanup() @@ -232,6 +318,20 @@ func TestLocationFull(t *testing.T) { tailTest.Cleanup(tail, true) } +func TestLocationFullWithCloseDuringWait(t *testing.T) { + tailTest, cleanup := NewTailTest("location-full", t) + defer cleanup() + tailTest.CreateFile("test.txt", "hello\nworld\n") + tail := tailTest.StartTail("test.txt", Config{Follow: true, Location: nil, CloseWhileWaiting: true}) + go tailTest.VerifyTailOutput(tail, []string{"hello", "world"}, false) + + // Delete after a reasonable delay, to give tail sufficient time + // to read all lines. + <-time.After(100 * time.Millisecond) + tailTest.RemoveFile("test.txt") + tailTest.Cleanup(tail, true) +} + func TestLocationFullDontFollow(t *testing.T) { tailTest, cleanup := NewTailTest("location-full-dontfollow", t) defer cleanup() @@ -247,6 +347,21 @@ func TestLocationFullDontFollow(t *testing.T) { tailTest.Cleanup(tail, true) } +func TestLocationFullDontFollowWithCloseDuringWait(t *testing.T) { + tailTest, cleanup := NewTailTest("location-full-dontfollow", t) + defer cleanup() + tailTest.CreateFile("test.txt", "hello\nworld\n") + tail := tailTest.StartTail("test.txt", Config{Follow: false, Location: nil, CloseWhileWaiting: true}) + go tailTest.VerifyTailOutput(tail, []string{"hello", "world"}, false) + + // Add more data only after reasonable delay. + <-time.After(100 * time.Millisecond) + tailTest.AppendFile("test.txt", "more\ndata\n") + <-time.After(100 * time.Millisecond) + + tailTest.Cleanup(tail, true) +} + func TestLocationEnd(t *testing.T) { tailTest, cleanup := NewTailTest("location-end", t) defer cleanup() @@ -264,6 +379,23 @@ func TestLocationEnd(t *testing.T) { tailTest.Cleanup(tail, true) } +func TestLocationEndWithCloseDuringWait(t *testing.T) { + tailTest, cleanup := NewTailTest("location-end", t) + defer cleanup() + tailTest.CreateFile("test.txt", "hello\nworld\n") + tail := tailTest.StartTail("test.txt", Config{Follow: true, Location: &SeekInfo{0, io.SeekEnd}, CloseWhileWaiting: true}) + go tailTest.VerifyTailOutput(tail, []string{"more", "data"}, false) + + <-time.After(100 * time.Millisecond) + tailTest.AppendFile("test.txt", "more\ndata\n") + + // Delete after a reasonable delay, to give tail sufficient time + // to read all lines. + <-time.After(100 * time.Millisecond) + tailTest.RemoveFile("test.txt") + tailTest.Cleanup(tail, true) +} + func TestLocationMiddle(t *testing.T) { // Test reading from middle. tailTest, cleanup := NewTailTest("location-middle", t) @@ -282,26 +414,60 @@ func TestLocationMiddle(t *testing.T) { tailTest.Cleanup(tail, true) } +func TestLocationMiddleWithCloseDuringWait(t *testing.T) { + // Test reading from middle. + tailTest, cleanup := NewTailTest("location-middle", t) + defer cleanup() + tailTest.CreateFile("test.txt", "hello\nworld\n") + tail := tailTest.StartTail("test.txt", Config{Follow: true, Location: &SeekInfo{-6, io.SeekEnd}, CloseWhileWaiting: true}) + go tailTest.VerifyTailOutput(tail, []string{"world", "more", "data"}, false) + + <-time.After(100 * time.Millisecond) + tailTest.AppendFile("test.txt", "more\ndata\n") + + // Delete after a reasonable delay, to give tail sufficient time + // to read all lines. + <-time.After(100 * time.Millisecond) + tailTest.RemoveFile("test.txt") + tailTest.Cleanup(tail, true) +} + // The use of polling file watcher could affect file rotation // (detected via renames), so test these explicitly. func TestReOpenInotify(t *testing.T) { - reOpen(t, false) + reOpen(t, false, false) +} + +func TestReOpenInotifyWithCloseWhenWait(t *testing.T) { + reOpen(t, false, true) } func TestReOpenPolling(t *testing.T) { - reOpen(t, true) + reOpen(t, true, false) +} + +func TestReOpenPollingWithCloseWhenWait(t *testing.T) { + reOpen(t, true, true) } // The use of polling file watcher could affect file rotation // (detected via renames), so test these explicitly. func TestReSeekInotify(t *testing.T) { - reSeek(t, false) + reSeek(t, false, false) +} + +func TestReSeekInotifyWithCloseWhenWait(t *testing.T) { + reSeek(t, false, true) } func TestReSeekPolling(t *testing.T) { - reSeek(t, true) + reSeek(t, true, false) +} + +func TestReSeekPollingWithCloseWhenWait(t *testing.T) { + reSeek(t, true, true) } func TestReSeekWithCursor(t *testing.T) { @@ -330,6 +496,32 @@ func TestReSeekWithCursor(t *testing.T) { tailTest.Cleanup(tail, false) } +func TestReSeekWithCursorWithCloseDuringWait(t *testing.T) { + tailTest, cleanup := NewTailTest("reseek-cursor", t) + defer cleanup() + tailTest.CreateFile("test.txt", "a really long string goes here\nhello\nworld\n") + tail := tailTest.StartTail( + "test.txt", + Config{Follow: true, ReOpen: false, Poll: false, CloseWhileWaiting: true}) + + go tailTest.VerifyTailOutputUsingCursor(tail, []string{ + "a really long string goes here", "hello", "world", "but", "not", "me"}, false) + + // truncate now + <-time.After(100 * time.Millisecond) + tailTest.TruncateFile("test.txt", "skip\nme\nplease\nbut\nnot\nme\n") + + // Delete after a reasonable delay, to give tail sufficient time + // to read all lines. + <-time.After(100 * time.Millisecond) + tailTest.RemoveFile("test.txt") + + // Do not bother with stopping as it could kill the tomb during + // the reading of data written above. Timings can vary based on + // test environment. + tailTest.Cleanup(tail, false) +} + func TestRateLimiting(t *testing.T) { tailTest, cleanup := NewTailTest("rate-limiting", t) defer cleanup() @@ -399,6 +591,46 @@ func TestTell(t *testing.T) { tail.Cleanup() } +func TestTellWithCloseDuringWait(t *testing.T) { + tailTest, cleanup := NewTailTest("tell-position", t) + defer cleanup() + tailTest.CreateFile("test.txt", "hello\nworld\nagain\nmore\n") + config := Config{ + Follow: false, + Location: &SeekInfo{0, io.SeekStart}, CloseWhileWaiting: true} + tail := tailTest.StartTail("test.txt", config) + // read one line + line := <-tail.Lines + if line.Num != 1 { + tailTest.Errorf("expected line to have number 1 but got %d", line.Num) + } + offset, err := tail.Tell() + if err != nil { + tailTest.Errorf("Tell return error: %s", err.Error()) + } + tail.Stop() + + config = Config{ + Follow: false, + Location: &SeekInfo{offset, io.SeekStart}} + tail = tailTest.StartTail("test.txt", config) + for l := range tail.Lines { + // it may readed one line in the chan(tail.Lines), + // so it may lost one line. + if l.Text != "world" && l.Text != "again" { + tailTest.Fatalf("mismatch; expected world or again, but got %s", + l.Text) + if l.Num < 1 || l.Num > 2 { + tailTest.Errorf("expected line number to be between 1 and 2 but got %d", l.Num) + } + } + break + } + tailTest.RemoveFile("test.txt") + tail.Stop() + tail.Cleanup() +} + func TestBlockUntilExists(t *testing.T) { tailTest, cleanup := NewTailTest("block-until-file-exists", t) defer cleanup() @@ -422,6 +654,30 @@ func TestBlockUntilExists(t *testing.T) { tail.Cleanup() } +func TestBlockUntilExistsWithCloseDuringWait(t *testing.T) { + tailTest, cleanup := NewTailTest("block-until-file-exists", t) + defer cleanup() + config := Config{ + Follow: true, + CloseWhileWaiting: true, + } + tail := tailTest.StartTail("test.txt", config) + go func() { + time.Sleep(100 * time.Millisecond) + tailTest.CreateFile("test.txt", "hello world\n") + }() + for l := range tail.Lines { + if l.Text != "hello world" { + tailTest.Fatalf("mismatch; expected hello world, but got %s", + l.Text) + } + break + } + tailTest.RemoveFile("test.txt") + tail.Stop() + tail.Cleanup() +} + func maxLineSize(t *testing.T, follow bool, fileContent string, expected []string) { tailTest, cleanup := NewTailTest("maxlinesize", t) defer cleanup() @@ -436,7 +692,7 @@ func maxLineSize(t *testing.T, follow bool, fileContent string, expected []strin tailTest.Cleanup(tail, true) } -func reOpen(t *testing.T, poll bool) { +func reOpen(t *testing.T, poll bool, closeWhenWait bool) { var name string var delay time.Duration if poll { @@ -451,7 +707,7 @@ func reOpen(t *testing.T, poll bool) { tailTest.CreateFile("test.txt", "hello\nworld\n") tail := tailTest.StartTail( "test.txt", - Config{Follow: true, ReOpen: true, Poll: poll}) + Config{Follow: true, ReOpen: true, Poll: poll, CloseWhileWaiting: closeWhenWait}) content := []string{"hello", "world", "more", "data", "endofworld"} go tailTest.VerifyTailOutput(tail, content, false) @@ -487,6 +743,34 @@ func reOpen(t *testing.T, poll bool) { tailTest.Cleanup(tail, false) } +func TestInotify_WaitForCreateThenMoveWithCloseDuringWait(t *testing.T) { + tailTest, cleanup := NewTailTest("wait-for-create-then-reopen", t) + defer cleanup() + os.Remove(tailTest.path + "/test.txt") // Make sure the file does NOT exist. + + tail := tailTest.StartTail( + "test.txt", + Config{Follow: true, ReOpen: true, Poll: false, CloseWhileWaiting: true}) + + content := []string{"hello", "world", "endofworld"} + go tailTest.VerifyTailOutput(tail, content, false) + + time.Sleep(50 * time.Millisecond) + tailTest.CreateFile("test.txt", "hello\nworld\n") + time.Sleep(50 * time.Millisecond) + tailTest.RenameFile("test.txt", "test.txt.rotated") + time.Sleep(50 * time.Millisecond) + tailTest.CreateFile("test.txt", "endofworld\n") + time.Sleep(50 * time.Millisecond) + tailTest.RemoveFile("test.txt.rotated") + tailTest.RemoveFile("test.txt") + + // Do not bother with stopping as it could kill the tomb during + // the reading of data written above. Timings can vary based on + // test environment. + tailTest.Cleanup(tail, false) +} + func TestInotify_WaitForCreateThenMove(t *testing.T) { tailTest, cleanup := NewTailTest("wait-for-create-then-reopen", t) defer cleanup() @@ -515,6 +799,36 @@ func TestInotify_WaitForCreateThenMove(t *testing.T) { tailTest.Cleanup(tail, false) } +func TestIncompleteLinesWithCloseDuringWait(t *testing.T) { + tailTest, cleanup := NewTailTest("incomplete-lines", t) + defer cleanup() + filename := "test.txt" + config := Config{ + Follow: true, + CompleteLines: true, + CloseWhileWaiting: true, + } + tail := tailTest.StartTail(filename, config) + go func() { + time.Sleep(100 * time.Millisecond) + tailTest.CreateFile(filename, "hello world\n") + time.Sleep(100 * time.Millisecond) + // here we intentially write a partial line to see if `Tail` contains + // information that it's incomplete + tailTest.AppendFile(filename, "hello") + time.Sleep(100 * time.Millisecond) + tailTest.AppendFile(filename, " again\n") + }() + + lines := []string{"hello world", "hello again"} + + tailTest.ReadLines(tail, lines, false) + + tailTest.RemoveFile(filename) + tail.Stop() + tail.Cleanup() +} + func TestIncompleteLines(t *testing.T) { tailTest, cleanup := NewTailTest("incomplete-lines", t) defer cleanup() @@ -572,6 +886,35 @@ func TestIncompleteLongLines(t *testing.T) { tail.Cleanup() } +func TestIncompleteLongLinesWithCloseDuringWait(t *testing.T) { + tailTest, cleanup := NewTailTest("incomplete-lines-long", t) + defer cleanup() + filename := "test.txt" + config := Config{ + Follow: true, + MaxLineSize: 3, + CompleteLines: true, + CloseWhileWaiting: true, + } + tail := tailTest.StartTail(filename, config) + go func() { + time.Sleep(100 * time.Millisecond) + tailTest.CreateFile(filename, "hello world\n") + time.Sleep(100 * time.Millisecond) + tailTest.AppendFile(filename, "hello") + time.Sleep(100 * time.Millisecond) + tailTest.AppendFile(filename, "again\n") + }() + + lines := []string{"hel", "lo ", "wor", "ld", "hel", "loa", "gai", "n"} + + tailTest.ReadLines(tail, lines, false) + + tailTest.RemoveFile(filename) + tail.Stop() + tail.Cleanup() +} + func TestIncompleteLinesWithReopens(t *testing.T) { tailTest, cleanup := NewTailTest("incomplete-lines-reopens", t) defer cleanup() @@ -598,6 +941,33 @@ func TestIncompleteLinesWithReopens(t *testing.T) { tail.Cleanup() } +func TestIncompleteLinesWithReopensWithCloseDuringWait(t *testing.T) { + tailTest, cleanup := NewTailTest("incomplete-lines-reopens", t) + defer cleanup() + filename := "test.txt" + config := Config{ + Follow: true, + CompleteLines: true, + CloseWhileWaiting: true, + } + tail := tailTest.StartTail(filename, config) + go func() { + time.Sleep(100 * time.Millisecond) + tailTest.CreateFile(filename, "hello world\nhi") + time.Sleep(100 * time.Millisecond) + tailTest.TruncateFile(filename, "rewriting\n") + }() + + // not that the "hi" gets lost, because it was never a complete line + lines := []string{"hello world", "rewriting"} + + tailTest.ReadLines(tail, lines, false) + + tailTest.RemoveFile(filename) + tail.Stop() + tail.Cleanup() +} + func TestIncompleteLinesWithoutFollow(t *testing.T) { tailTest, cleanup := NewTailTest("incomplete-lines-no-follow", t) defer cleanup() @@ -622,7 +992,7 @@ func TestIncompleteLinesWithoutFollow(t *testing.T) { tail.Cleanup() } -func reSeek(t *testing.T, poll bool) { +func reSeek(t *testing.T, poll bool, closeWhenWait bool) { var name string if poll { name = "reseek-polling" @@ -634,7 +1004,7 @@ func reSeek(t *testing.T, poll bool) { tailTest.CreateFile("test.txt", "a really long string goes here\nhello\nworld\n") tail := tailTest.StartTail( "test.txt", - Config{Follow: true, ReOpen: false, Poll: poll}) + Config{Follow: true, ReOpen: false, Poll: poll, CloseWhileWaiting: closeWhenWait}) go tailTest.VerifyTailOutput(tail, []string{ "a really long string goes here", "hello", "world", "h311o", "w0r1d", "endofworld"}, false)