261026.37 · 21085a84dd5a7256f0a1f09b4f390e35ab0d43c6
Under systemd a line is logged once
--restart and failures log a line and print it on stderr. A unit's stderr is the journal, so each line showed twice there. Where JOURNAL_STREAM names stderr, www now only logs it.
reading declarations
@@ -164,6 +164,18 @@ func openLogFile(path string) (*os.File, error) { return os.OpenFile(path, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o600) } +// stderrIsJournal reports whether stderr is a journal stream that systemd+// connected, as it does for a unit's process, by JOURNAL_STREAM, its+// device and inode. A line logged then needs no copy on stderr, which+// would show it twice in the journal.+func stderrIsJournal() bool {+ var st syscall.Stat_t+ if os.Getenv("JOURNAL_STREAM") == "" || syscall.Fstat(2, &st) != nil {+ return false+ }+ return os.Getenv("JOURNAL_STREAM") == fmt.Sprintf("%d:%d", st.Dev, st.Ino)+}+ // logSink is where a detached server's stderr goes: a journal stream, or // the fallback file. A panic writes there, past every logger. It returns // nil when neither opens, and stderr then goes nowhere, as before.
@@ -3,10 +3,12 @@ package main import ( "bytes" "encoding/binary"+ "fmt" "net" "os" "path/filepath" "strings"+ "syscall" "testing" "time" )@@ -175,3 +177,24 @@ func TestRequestLogShared(t *testing.T) { t.Errorf("shared log lacks the visitor:\n%s", b) } }++// Under systemd, stderr is the journal, so what is logged is not printed+// as well.+func TestStderrIsJournal(t *testing.T) {+ var st syscall.Stat_t+ if err := syscall.Fstat(2, &st); err != nil {+ t.Skip(err)+ }+ t.Setenv("JOURNAL_STREAM", fmt.Sprintf("%d:%d", st.Dev, st.Ino))+ if !stderrIsJournal() {+ t.Error("stderr's own device and inode are not the journal")+ }+ t.Setenv("JOURNAL_STREAM", fmt.Sprintf("%d:%d", st.Dev, st.Ino+1))+ if stderrIsJournal() {+ t.Error("another inode is the journal")+ }+ t.Setenv("JOURNAL_STREAM", "")+ if stderrIsJournal() {+ t.Error("no JOURNAL_STREAM is the journal")+ }+}
@@ -761,7 +761,9 @@ func detach(c config) int { func failNow(code syscall.Errno, msg string) int { logErr("E%d: %s", code, msg)- fmt.Fprintf(os.Stderr, "E%d: %s\n", code, msg)+ if !stderrIsJournal() {+ fmt.Fprintf(os.Stderr, "E%d: %s\n", code, msg)+ } return int(code) }
@@ -90,7 +90,9 @@ func reloading(n int) string { // say tells the user and the log what a restart did. func say(format string, args ...any) { logInfo(format, args...)- fmt.Fprintf(os.Stderr, format+"\n", args...)+ if !stderrIsJournal() {+ fmt.Fprintf(os.Stderr, format+"\n", args...)+ } } // startDir serves c.dir on c.ep once the servers are replaced. A tree the