diff --git a/internal/entryhuman/entry.go b/internal/entryhuman/entry.go index cd63ccb..8af4d82 100644 --- a/internal/entryhuman/entry.go +++ b/internal/entryhuman/entry.go @@ -12,6 +12,7 @@ import ( "reflect" "strconv" "strings" + "syscall" "time" "unicode" @@ -224,9 +225,25 @@ func isTTY(w io.Writer) bool { if w == forceColorWriter { return true } - f, ok := w.(interface { - Fd() uintptr - }) + // SyscallConn is safe during file close. + if sc, ok := w.(interface { + SyscallConn() (syscall.RawConn, error) + }); ok { + conn, err := sc.SyscallConn() + if err != nil { + return false + } + var isTerm bool + err = conn.Control(func(fd uintptr) { + isTerm = term.IsTerminal(int(fd)) + }) + if err != nil { + return false + } + return isTerm + } + // Fallback to unsafe Fd. + f, ok := w.(interface{ Fd() uintptr }) return ok && term.IsTerminal(int(f.Fd())) } diff --git a/internal/entryhuman/entry_test.go b/internal/entryhuman/entry_test.go index 06ba312..45a885a 100644 --- a/internal/entryhuman/entry_test.go +++ b/internal/entryhuman/entry_test.go @@ -178,6 +178,34 @@ func TestEntry(t *testing.T) { assert.Equal(t, "entry matches", string(wantByt), gotBuf.String()) }) } + + t.Run("isTTY during file close", func(t *testing.T) { + t.Parallel() + + tmpdir := t.TempDir() + f, err := os.CreateTemp(tmpdir, "slog") + if err != nil { + t.Fatal(err) + } + defer f.Close() + + done := make(chan struct{}, 2) + go func() { + entryhuman.Fmt(new(bytes.Buffer), f, slog.SinkEntry{ + Level: slog.LevelCritical, + Fields: slog.M( + slog.F("hey", "hi"), + ), + }) + done <- struct{}{} + }() + go func() { + _ = f.Close() + done <- struct{}{} + }() + <-done + <-done + }) } func BenchmarkFmt(b *testing.B) {