From 04e77f741b37268bcc19970312d6f4a788543db6 Mon Sep 17 00:00:00 2001 From: WGH Date: Wed, 19 Oct 2022 20:59:34 +0300 Subject: [PATCH 1/4] journal: add StderrIsJournalStream function This function can be used for automatic protocol upgrade described in [1]. Both unit tests and runnable example are included. Only the latter requires systemd, as unit tests are self-sufficient, and only test that JOURNAL_STREAM environment variable is checked properly. [1] https://systemd.io/JOURNAL_NATIVE_PROTOCOL/#automatic-protocol-upgrading --- examples/journal/main.go | 37 +++++++++ examples/journal/run.sh | 13 ++++ journal/journal_unix.go | 34 +++++++++ journal/journal_unix_test.go | 142 +++++++++++++++++++++++++++++++++++ journal/journal_windows.go | 4 + scripts/ci-runner.sh | 2 + 6 files changed, 232 insertions(+) create mode 100644 examples/journal/main.go create mode 100755 examples/journal/run.sh create mode 100644 journal/journal_unix_test.go diff --git a/examples/journal/main.go b/examples/journal/main.go new file mode 100644 index 00000000..86dadc96 --- /dev/null +++ b/examples/journal/main.go @@ -0,0 +1,37 @@ +// Copyright 2022 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License + +package main + +import ( + "fmt" + "os" + + "github.com/coreos/go-systemd/v22/journal" +) + +func main() { + ok, err := journal.StderrIsJournalStream() + if err != nil { + panic(err) + } + + if ok { + // use journal native protocol + journal.Send("this is a message logged through the native protocol", journal.PriInfo, nil) + } else { + // use stderr + fmt.Fprintln(os.Stderr, "this is a message logged through stderr") + } +} diff --git a/examples/journal/run.sh b/examples/journal/run.sh new file mode 100755 index 00000000..2909b8f0 --- /dev/null +++ b/examples/journal/run.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +set -e + +go build + +echo "Running directly" +./journal + +echo "Running through systemd" +unit_name="run-$(systemd-id128 new)" +systemd-run -u "$unit_name" --user --wait --quiet ./journal +journalctl --user -u "$unit_name" diff --git a/journal/journal_unix.go b/journal/journal_unix.go index 439ad287..a4f43ad1 100644 --- a/journal/journal_unix.go +++ b/journal/journal_unix.go @@ -69,6 +69,40 @@ func Enabled() bool { return true } +// StderrIsJournalStream returns whether the process stderr is connected +// to the Journal's stream transport. +// +// This can be used for automatic protocol upgrading described in [Journal Native Protocol]. +// +// Returns true if JOURNAL_STREAM environment variable is present, +// and stderr's device and inode numbers match it. +// +// Error is returned if unexpected error occurs: e.g. if JOURNAL_STREAM environment variable +// is present, but malformed, fstat syscall fails, etc. +// +// [Journal Native Protocol]: https://systemd.io/JOURNAL_NATIVE_PROTOCOL/#automatic-protocol-upgrading +func StderrIsJournalStream() (bool, error) { + journalStream := os.Getenv("JOURNAL_STREAM") + if journalStream == "" { + return false, nil + } + + var expectedStat syscall.Stat_t + _, err := fmt.Sscanf(journalStream, "%d:%d", &expectedStat.Dev, &expectedStat.Ino) + if err != nil { + return false, fmt.Errorf("failed to parse JOURNAL_STREAM=%q: %v", journalStream, err) + } + + var stat syscall.Stat_t + err = syscall.Fstat(syscall.Stderr, &stat) + if err != nil { + return false, err + } + + match := stat.Dev == expectedStat.Dev && stat.Ino == expectedStat.Ino + return match, nil +} + // Send a message to the local systemd journal. vars is a map of journald // fields to values. Fields must be composed of uppercase letters, numbers, // and underscores, but must not start with an underscore. Within these diff --git a/journal/journal_unix_test.go b/journal/journal_unix_test.go new file mode 100644 index 00000000..7a512723 --- /dev/null +++ b/journal/journal_unix_test.go @@ -0,0 +1,142 @@ +// Copyright 2022 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !windows +// +build !windows + +package journal_test + +import ( + "fmt" + "os" + "syscall" + "testing" + + "github.com/coreos/go-systemd/v22/journal" +) + +func TestStderrIsJournalStream(t *testing.T) { + if _, ok := os.LookupEnv("JOURNAL_STREAM"); ok { + t.Fatal("unset JOURNAL_STREAM before running this test") + } + + t.Run("Missing", func(t *testing.T) { + ok, err := journal.StderrIsJournalStream() + if err != nil { + t.Fatal(err) + } + if ok { + t.Error("stderr shouldn't be connected to journal stream") + } + }) + t.Run("Present", func(t *testing.T) { + f, stat := getUnixStreamSocket(t) + defer f.Close() + os.Setenv("JOURNAL_STREAM", fmt.Sprintf("%d:%d", stat.Dev, stat.Ino)) + defer os.Unsetenv("JOURNAL_STREAM") + replaceStderr(int(f.Fd()), func() { + ok, err := journal.StderrIsJournalStream() + if err != nil { + t.Fatal(err) + } + if !ok { + t.Error("stderr should've been connected to journal stream") + } + }) + }) + t.Run("NotMatching", func(t *testing.T) { + f, stat := getUnixStreamSocket(t) + defer f.Close() + os.Setenv("JOURNAL_STREAM", fmt.Sprintf("%d:%d", stat.Dev+1, stat.Ino)) + defer os.Unsetenv("JOURNAL_STREAM") + replaceStderr(int(f.Fd()), func() { + ok, err := journal.StderrIsJournalStream() + if err != nil { + t.Fatal(err) + } + if ok { + t.Error("stderr shouldn't be connected to journal stream") + } + }) + }) + t.Run("Malformed", func(t *testing.T) { + f, stat := getUnixStreamSocket(t) + defer f.Close() + os.Setenv("JOURNAL_STREAM", fmt.Sprintf("%d-%d", stat.Dev, stat.Ino)) + defer os.Unsetenv("JOURNAL_STREAM") + replaceStderr(int(f.Fd()), func() { + _, err := journal.StderrIsJournalStream() + if err == nil { + t.Fatal("JOURNAL_STREAM is malformed, but no error returned") + } + }) + }) +} + +func ExampleStderrIsJournalStream() { + // NOTE: this is just an example. Production code + // will likely use this to setup a logging library + // to write messages to either journal or stderr. + ok, err := journal.StderrIsJournalStream() + if err != nil { + panic(err) + } + + if ok { + // use journal native protocol + journal.Send("this is a message logged through the native protocol", journal.PriInfo, nil) + } else { + // use stderr + fmt.Fprintln(os.Stderr, "this is a message logged through stderr") + } +} + +func replaceStderr(fd int, cb func()) { + savedStderr, err := syscall.Dup(syscall.Stderr) + if err != nil { + panic(err) + } + defer syscall.Close(savedStderr) + err = syscall.Dup2(fd, syscall.Stderr) + if err != nil { + panic(err) + } + defer func() { + err := syscall.Dup2(savedStderr, syscall.Stderr) + if err != nil { + panic(err) + } + }() + cb() +} + +// getUnixStreamSocket returns a unix stream socket obtained with +// socketpair(2), and its fstat result. Only one end of the socket pair +// is returned, and the other end is closed immediately: we don't need +// it for our purposes. +func getUnixStreamSocket(t *testing.T) (*os.File, *syscall.Stat_t) { + fds, err := syscall.Socketpair(syscall.AF_UNIX, syscall.SOCK_STREAM, 0) + if err != nil { + t.Fatal(os.NewSyscallError("socketpair", err)) + } + // we don't need the remote end for our tests + syscall.Close(fds[1]) + + file := os.NewFile(uintptr(fds[0]), "unix-stream") + stat, err := file.Stat() + if err != nil { + t.Fatal(err) + } + return file, stat.Sys().(*syscall.Stat_t) +} diff --git a/journal/journal_windows.go b/journal/journal_windows.go index 677aca68..99fae64c 100644 --- a/journal/journal_windows.go +++ b/journal/journal_windows.go @@ -33,3 +33,7 @@ func Enabled() bool { func Send(message string, priority Priority, vars map[string]string) error { return errors.New("could not initialize socket to journald") } + +func StderrIsJournalStream() (bool, error) { + return false, nil +} diff --git a/scripts/ci-runner.sh b/scripts/ci-runner.sh index 51752044..abce9713 100755 --- a/scripts/ci-runner.sh +++ b/scripts/ci-runner.sh @@ -23,6 +23,8 @@ function build_tests { echo " - examples/${ex}" go build -o ./test_bins/${ex}.example ./examples/activation/${ex}.go done + # just to make sure it's buildable + go build -o ./test_bins/journal ./examples/journal/ } function run_tests { From 09b205da494df1584a23400843222df04505715a Mon Sep 17 00:00:00 2001 From: WGH Date: Fri, 4 Nov 2022 16:06:31 +0300 Subject: [PATCH 2/4] journal: add StdoutIsJournalStream function See discussion in https://github.com/coreos/go-systemd/pull/410 --- journal/journal_unix.go | 20 +++++++++++++++++++- journal/journal_windows.go | 4 ++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/journal/journal_unix.go b/journal/journal_unix.go index a4f43ad1..c5b23a81 100644 --- a/journal/journal_unix.go +++ b/journal/journal_unix.go @@ -82,6 +82,24 @@ func Enabled() bool { // // [Journal Native Protocol]: https://systemd.io/JOURNAL_NATIVE_PROTOCOL/#automatic-protocol-upgrading func StderrIsJournalStream() (bool, error) { + return fdIsJournalStream(syscall.Stderr) +} + +// StdoutIsJournalStream returns whether the process stdout is connected +// to the Journal's stream transport. +// +// Returns true if JOURNAL_STREAM environment variable is present, +// and stdout's device and inode numbers match it. +// +// Error is returned if unexpected error occurs: e.g. if JOURNAL_STREAM environment variable +// is present, but malformed, fstat syscall fails, etc. +// +// Most users should probably use [StderrIsJournalStream]. +func StdoutIsJournalStream() (bool, error) { + return fdIsJournalStream(syscall.Stdout) +} + +func fdIsJournalStream(fd int) (bool, error) { journalStream := os.Getenv("JOURNAL_STREAM") if journalStream == "" { return false, nil @@ -94,7 +112,7 @@ func StderrIsJournalStream() (bool, error) { } var stat syscall.Stat_t - err = syscall.Fstat(syscall.Stderr, &stat) + err = syscall.Fstat(fd, &stat) if err != nil { return false, err } diff --git a/journal/journal_windows.go b/journal/journal_windows.go index 99fae64c..322e41e7 100644 --- a/journal/journal_windows.go +++ b/journal/journal_windows.go @@ -37,3 +37,7 @@ func Send(message string, priority Priority, vars map[string]string) error { func StderrIsJournalStream() (bool, error) { return false, nil } + +func StdoutIsJournalStream() (bool, error) { + return false, nil +} From fe8fac503b9ea1ed64085bd54f53c767be451c54 Mon Sep 17 00:00:00 2001 From: WGH Date: Sat, 5 Nov 2022 13:23:29 +0300 Subject: [PATCH 3/4] ci: bump Ubuntu versions for container workflows A test that will be added in a latter commit requires systemd-run --wait, which is not available on Ubuntu 16.04. DEBIAN_FRONTEND=noninteractive prevents hang on "Setting up tzdata" that became a problem on Ubuntu 20.04 (though something similar could happen on any Debian/Ubuntu version). --- .github/workflows/containers.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/containers.yml b/.github/workflows/containers.yml index 3087aee7..acb2a8c9 100644 --- a/.github/workflows/containers.yml +++ b/.github/workflows/containers.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - baseimage: ['debian:stretch', 'ubuntu:16.04', 'ubuntu:18.04'] + baseimage: ['debian:stretch', 'ubuntu:18.04', 'ubuntu:20.04'] steps: - run: sudo apt-get -qq update - name: Install libsystemd-dev @@ -38,7 +38,7 @@ jobs: - name: Pull base image - ${{ matrix.baseimage }} run: docker pull ${{ matrix.baseimage }} - name: Install packages for ${{ matrix.baseimage }} - run: docker run --privileged -e GOPATH=${GOPATH} --cidfile=/tmp/cidfile ${{ matrix.baseimage }} /bin/bash -c "apt-get update && apt-get install -y sudo build-essential git golang dbus libsystemd-dev libpam-systemd systemd-container" + run: docker run --privileged -e GOPATH=${GOPATH} --cidfile=/tmp/cidfile ${{ matrix.baseimage }} /bin/bash -c "export DEBIAN_FRONTEND=noninteractive; apt-get update && apt-get install -y sudo build-essential git golang dbus libsystemd-dev libpam-systemd systemd-container" - name: Persist base container run: docker commit `cat /tmp/cidfile` go-systemd/container-tests - run: rm -f /tmp/cidfile From 4ca62222b9b97c2f13a136b55ee6e8ed9c173913 Mon Sep 17 00:00:00 2001 From: WGH Date: Fri, 4 Nov 2022 19:56:53 +0300 Subject: [PATCH 4/4] journal: add proper StderrIsJournalStream test To ensure that StderrIsJournalStream properly works in real conditions, this test re-executes itself with systemd-run, and observes exit code and logged entries. --- journal/journal_unix_test.go | 48 +++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/journal/journal_unix_test.go b/journal/journal_unix_test.go index 7a512723..3483d337 100644 --- a/journal/journal_unix_test.go +++ b/journal/journal_unix_test.go @@ -20,13 +20,14 @@ package journal_test import ( "fmt" "os" + "os/exec" "syscall" "testing" "github.com/coreos/go-systemd/v22/journal" ) -func TestStderrIsJournalStream(t *testing.T) { +func TestJournalStreamParsing(t *testing.T) { if _, ok := os.LookupEnv("JOURNAL_STREAM"); ok { t.Fatal("unset JOURNAL_STREAM before running this test") } @@ -84,6 +85,51 @@ func TestStderrIsJournalStream(t *testing.T) { }) } +func TestStderrIsJournalStream(t *testing.T) { + const ( + message = "TEST_MESSAGE" + ) + + userOrSystem := "--user" + if os.Getuid() == 0 { + userOrSystem = "--system" + } + + if _, ok := os.LookupEnv("JOURNAL_STREAM"); !ok { + // Re-execute this test under systemd (see the else branch), + // and observe its exit code. + args := []string{ + "systemd-run", + userOrSystem, + "--wait", + "--quiet", + "--", + os.Args[0], + "-test.run=TestStderrIsJournalStream", + "-test.count=1", // inhibit caching + } + + cmd := exec.Command(args[0], args[1:]...) + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + t.Fatal(err) + } + } else { + ok, err := journal.StderrIsJournalStream() + if err != nil { + t.Fatal(err) + } + if !ok { + t.Fatal("StderrIsJournalStream should've returned true") + } + + err = journal.Send(message, journal.PriInfo, nil) + if err != nil { + t.Fatal(err) + } + } +} + func ExampleStderrIsJournalStream() { // NOTE: this is just an example. Production code // will likely use this to setup a logging library