From 1e3faab431785cb8345a8914e991f1541dfc9090 Mon Sep 17 00:00:00 2001 From: Rasmus Andersson Date: Fri, 2 Oct 2020 16:23:25 -0700 Subject: [PATCH 1/5] daemon: Adds SdNotifyBarrier --- daemon/sdnotifybarrier.go | 89 +++++++++++++++++ daemon/sdnotifybarrier_test.go | 177 +++++++++++++++++++++++++++++++++ 2 files changed, 266 insertions(+) create mode 100644 daemon/sdnotifybarrier.go create mode 100644 daemon/sdnotifybarrier_test.go diff --git a/daemon/sdnotifybarrier.go b/daemon/sdnotifybarrier.go new file mode 100644 index 00000000..c3aec9a9 --- /dev/null +++ b/daemon/sdnotifybarrier.go @@ -0,0 +1,89 @@ +// Copyright 2020 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 daemon + +import ( + "errors" + "io" + "net" + "os" + "syscall" + "time" +) + +var ErrEnvironment = errors.New("unsupported environment") + +// SdNotifyBarrier allows the caller to synchronize against reception of +// previously sent notification messages and uses the "BARRIER=1" command. +// +// If `unsetEnvironment` is true, the environment variable `NOTIFY_SOCKET` +// will be unconditionally unset. +// +func SdNotifyBarrier(unsetEnvironment bool, timeout time.Duration) error { + // modelled after libsystemd's sd_notify_barrier + + // construct unix socket address from systemd environment variable + socketAddr := &net.UnixAddr{ + Name: os.Getenv("NOTIFY_SOCKET"), + Net: "unixgram", + } + if socketAddr.Name == "" { + return ErrEnvironment + } + + // create a pipe for communicating with systemd daemon + pipe_r, pipe_w, err := os.Pipe() // (r *File, w *File, error) + if err != nil { + return err + } + + if unsetEnvironment { + if err := os.Unsetenv("NOTIFY_SOCKET"); err != nil { + return err + } + } + + // connect to unix socket at socketAddr + conn, err := net.DialUnix(socketAddr.Net, nil, socketAddr) + if err != nil { + return err + } + defer conn.Close() + + // get the FD for the unix socket file + connf, err := conn.File() + if err != nil { + return err + } + + // send over write end of the pipe to the systemd daemon + rights := syscall.UnixRights(int(pipe_w.Fd())) + err = syscall.Sendmsg(int(connf.Fd()), []byte("BARRIER=1"), rights, nil, 0) + if err != nil { + return err + } + pipe_w.Close() + + // wait for systemd to close the pipe + var b [1]byte + pipe_r.SetReadDeadline(time.Now().Add(timeout)) + _, err = pipe_r.Read(b[:]) + if err == io.EOF { + err = nil + } + + return err +} diff --git a/daemon/sdnotifybarrier_test.go b/daemon/sdnotifybarrier_test.go new file mode 100644 index 00000000..d110766a --- /dev/null +++ b/daemon/sdnotifybarrier_test.go @@ -0,0 +1,177 @@ +// Copyright 2020 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 daemon + +import ( + "bytes" + "io/ioutil" + "net" + "os" + "strings" + "syscall" + "testing" + "time" +) + +const intSize uint = 32 << (^uint(0) >> 63) // bits of int on target platform + +func TestSdNotifyBarrier(t *testing.T) { + + testDir, e := ioutil.TempDir("/tmp/", "test-") + if e != nil { + panic(e) + } + defer os.RemoveAll(testDir) + + notifySocket := testDir + "/notify-socket.sock" + laddr := net.UnixAddr{ + Name: notifySocket, + Net: "unixgram", + } + sock, err := net.ListenUnixgram("unixgram", &laddr) + if err != nil { + panic(err) + } + + messageExpected := []byte("BARRIER=1") + + tests := []struct { + unsetEnv bool + envSocket string + expectErr string + expectReadN int // num in-band bytes to recv on socket + expectReadOobN int // num out-of-band bytes to recv on socket + }{ + // should succeed + { + unsetEnv: false, + envSocket: notifySocket, + expectErr: "", + expectReadN: len(messageExpected), + expectReadOobN: syscall.CmsgSpace(4 /*1xFD*/), + }, + // failure to open systemd socket should result in an error + { + unsetEnv: false, + envSocket: testDir + "/missing.sock", + expectErr: "no such file", + expectReadN: 0, + expectReadOobN: 0, + }, + // notification not supported + { + unsetEnv: false, + envSocket: "", + expectErr: ErrEnvironment.Error(), + expectReadN: 0, + expectReadOobN: 0, + }, + } + + resultCh := make(chan error) + + // allocate message and out-of-band buffers + var msgBuf [128]byte + oobBuf := make([]byte, syscall.CmsgSpace(4 /*1xFD/1xint32*/)) + + for i, tt := range tests { + must(os.Unsetenv("NOTIFY_SOCKET")) + if tt.envSocket != "" { + must(os.Setenv("NOTIFY_SOCKET", tt.envSocket)) + } + + go func() { + resultCh <- SdNotifyBarrier(tt.unsetEnv, 100*time.Millisecond) + }() + + if tt.envSocket == notifySocket { + // pretend to be systemd and read the message that SdNotifyBarrier wrote to sock + // returns (n, oobn, flags int, addr *UnixAddr, err error) + n, oobn, _, _, err := sock.ReadMsgUnix(msgBuf[:], oobBuf[:]) + // fmt.Printf("ReadMsgUnix -> %v, %v, %v, %v, %v\n", n, oobn, flags, from, err) + if err != nil { + t.Errorf("#%d: failed to read socket: %v", i, err) + continue + } + + // check bytes read + if tt.expectReadN != n { + t.Errorf("#%d: want expectReadN %v, got %v", i, tt.expectReadN, n) + continue + } + if tt.expectReadOobN != oobn { + t.Errorf("#%d: want expectReadOobN %v, got %v", i, tt.expectReadOobN, n) + continue + } + + // check message + if n > 0 { + if !bytes.Equal(msgBuf[:n], messageExpected) { + t.Errorf("#%d: want message %q, got %q", i, messageExpected, msgBuf[:n]) + continue + } + } + + // parse OOB message + if oobn > 0 { + mv, err := syscall.ParseSocketControlMessage(oobBuf) + if err != nil { + t.Errorf("#%d: ParseSocketControlMessage failed: %v", i, err) + continue + } + + if len(mv) != 1 { + // should be just control message in the oob data + t.Errorf("#%d: want len(mv)=1, got %v", i, len(mv)) + continue + } + + // parse socket fd from message 0 + fds, err := syscall.ParseUnixRights(&mv[0]) + if err != nil { + t.Errorf("#%d: ParseUnixRights failed: %v", i, err) + continue + } + if len(fds) != 1 { + // should be just one socket file descriptor in the control message + t.Errorf("#%d: want len(fds)=1, got %v", i, len(fds)) + continue + } + + // finally close the socket to signal back to SdNotifyBarrier + syscall.Close(fds[0]) + } + } // if tt.envSocket == notifySocket + + err = <-resultCh + + // check error + if len(tt.expectErr) > 0 { + if err == nil { + t.Errorf("#%d: want non-nil err, got nil", i) + } else if !strings.Contains(err.Error(), tt.expectErr) { + t.Errorf("#%d: want err with substr %q, got %q", i, tt.expectErr, err.Error()) + } + } else if len(tt.expectErr) == 0 && err != nil { + t.Errorf("#%d: want nil err, got %v", i, err) + } + + // if unsetEnvironment was requested, verify NOTIFY_SOCKET is not set + if tt.unsetEnv && tt.envSocket != "" && os.Getenv("NOTIFY_SOCKET") != "" { + t.Errorf("#%d: environment variable not cleaned up", i) + } + + } +} From fb10d8ac544f6027e65e88bde7e55aa1ec8c52ef Mon Sep 17 00:00:00 2001 From: Rasmus Andersson Date: Fri, 2 Oct 2020 16:32:44 -0700 Subject: [PATCH 2/5] test: removes unused intSize constant and increases timeout --- daemon/sdnotifybarrier_test.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/daemon/sdnotifybarrier_test.go b/daemon/sdnotifybarrier_test.go index d110766a..7bb463af 100644 --- a/daemon/sdnotifybarrier_test.go +++ b/daemon/sdnotifybarrier_test.go @@ -25,8 +25,6 @@ import ( "time" ) -const intSize uint = 32 << (^uint(0) >> 63) // bits of int on target platform - func TestSdNotifyBarrier(t *testing.T) { testDir, e := ioutil.TempDir("/tmp/", "test-") @@ -93,7 +91,7 @@ func TestSdNotifyBarrier(t *testing.T) { } go func() { - resultCh <- SdNotifyBarrier(tt.unsetEnv, 100*time.Millisecond) + resultCh <- SdNotifyBarrier(tt.unsetEnv, 500*time.Millisecond) }() if tt.envSocket == notifySocket { From 1d21980c03d135ba51ae41d9cba21f0d107525fd Mon Sep 17 00:00:00 2001 From: Rasmus Andersson Date: Sun, 18 Oct 2020 11:42:37 -0700 Subject: [PATCH 3/5] SdNotifyBarrier: use context instead of timeout --- daemon/sdnotifybarrier.go | 13 +++++++++++-- daemon/sdnotifybarrier_test.go | 4 +++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/daemon/sdnotifybarrier.go b/daemon/sdnotifybarrier.go index c3aec9a9..d76f3b41 100644 --- a/daemon/sdnotifybarrier.go +++ b/daemon/sdnotifybarrier.go @@ -16,6 +16,7 @@ package daemon import ( + "context" "errors" "io" "net" @@ -32,7 +33,7 @@ var ErrEnvironment = errors.New("unsupported environment") // If `unsetEnvironment` is true, the environment variable `NOTIFY_SOCKET` // will be unconditionally unset. // -func SdNotifyBarrier(unsetEnvironment bool, timeout time.Duration) error { +func SdNotifyBarrier(ctx context.Context, unsetEnvironment bool) error { // modelled after libsystemd's sd_notify_barrier // construct unix socket address from systemd environment variable @@ -78,9 +79,17 @@ func SdNotifyBarrier(unsetEnvironment bool, timeout time.Duration) error { pipe_w.Close() // wait for systemd to close the pipe + ctxch := make(chan struct{}) + go func() { + select { + case <-ctx.Done(): + pipe_r.SetReadDeadline(time.Now()) + case <-ctxch: + } + }() var b [1]byte - pipe_r.SetReadDeadline(time.Now().Add(timeout)) _, err = pipe_r.Read(b[:]) + close(ctxch) if err == io.EOF { err = nil } diff --git a/daemon/sdnotifybarrier_test.go b/daemon/sdnotifybarrier_test.go index 7bb463af..2b9e6395 100644 --- a/daemon/sdnotifybarrier_test.go +++ b/daemon/sdnotifybarrier_test.go @@ -16,6 +16,7 @@ package daemon import ( "bytes" + "context" "io/ioutil" "net" "os" @@ -91,7 +92,8 @@ func TestSdNotifyBarrier(t *testing.T) { } go func() { - resultCh <- SdNotifyBarrier(tt.unsetEnv, 500*time.Millisecond) + ctx, _ := context.WithTimeout(context.Background(), 500*time.Millisecond) + resultCh <- SdNotifyBarrier(ctx, tt.unsetEnv) }() if tt.envSocket == notifySocket { From 9f000296a7f81573987d8d15eb989b959b017ea8 Mon Sep 17 00:00:00 2001 From: Rasmus Andersson Date: Sun, 18 Oct 2020 11:44:08 -0700 Subject: [PATCH 4/5] SdNotifyBarrier: add comment about when then feature was added to systemd --- daemon/sdnotifybarrier.go | 1 + 1 file changed, 1 insertion(+) diff --git a/daemon/sdnotifybarrier.go b/daemon/sdnotifybarrier.go index d76f3b41..9b0c1414 100644 --- a/daemon/sdnotifybarrier.go +++ b/daemon/sdnotifybarrier.go @@ -33,6 +33,7 @@ var ErrEnvironment = errors.New("unsupported environment") // If `unsetEnvironment` is true, the environment variable `NOTIFY_SOCKET` // will be unconditionally unset. // +// This feature was added in systemd v246 func SdNotifyBarrier(ctx context.Context, unsetEnvironment bool) error { // modelled after libsystemd's sd_notify_barrier From 97a6d36ab80dc22954628b309e8c057d3b509716 Mon Sep 17 00:00:00 2001 From: Rasmus Andersson Date: Sun, 18 Oct 2020 11:47:24 -0700 Subject: [PATCH 5/5] add comment and use more specific name for error when NOTIFY_SOCKET is not present in env --- daemon/sdnotifybarrier.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/daemon/sdnotifybarrier.go b/daemon/sdnotifybarrier.go index 9b0c1414..c88ed9f7 100644 --- a/daemon/sdnotifybarrier.go +++ b/daemon/sdnotifybarrier.go @@ -25,7 +25,8 @@ import ( "time" ) -var ErrEnvironment = errors.New("unsupported environment") +// ErrNoNotificationSocket is returned when NOTIFY_SOCKET is not set in the environment +var ErrNoNotificationSocket = errors.New("notification socket not available") // SdNotifyBarrier allows the caller to synchronize against reception of // previously sent notification messages and uses the "BARRIER=1" command. @@ -43,7 +44,7 @@ func SdNotifyBarrier(ctx context.Context, unsetEnvironment bool) error { Net: "unixgram", } if socketAddr.Name == "" { - return ErrEnvironment + return ErrNoNotificationSocket } // create a pipe for communicating with systemd daemon