Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

daemon: Adds SdNotifyBarrier #343

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions daemon/sdnotifybarrier.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// 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 (
"context"
"errors"
"io"
"net"
"os"
"syscall"
"time"
)

// 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.
//
// If `unsetEnvironment` is true, the environment variable `NOTIFY_SOCKET`
// will be unconditionally unset.
//
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spurious blank line.

This feature has been added quite recently in systemd v246, so it would be good to mention the minimum requirement here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a note about the systemd version.
The extra empty line at the end is a convention used by the go source code itself when a comment has multiple paragraphs. Since gofmt has no opinion on this it falls on you I guess, so I removed it :-)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I guess I'm up to some learning today. Do you have examples/references of this? Does some godoc renderer rely on this?

// This feature was added in systemd v246
func SdNotifyBarrier(ctx context.Context, unsetEnvironment bool) 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 ErrNoNotificationSocket
}

// 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
ctxch := make(chan struct{})
go func() {
select {
case <-ctx.Done():
pipe_r.SetReadDeadline(time.Now())
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: pipe_r.Close() would work here too but setting an immediate read deadline has the upside of returning a descriptive error message: i/o timeout

case <-ctxch:
}
}()
var b [1]byte
_, err = pipe_r.Read(b[:])
close(ctxch)
if err == io.EOF {
err = nil
}

return err
}
177 changes: 177 additions & 0 deletions daemon/sdnotifybarrier_test.go
Original file line number Diff line number Diff line change
@@ -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"
"context"
"io/ioutil"
"net"
"os"
"strings"
"syscall"
"testing"
"time"
)

func TestSdNotifyBarrier(t *testing.T) {

testDir, e := ioutil.TempDir("/tmp/", "test-")
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: I used this since it's what I saw used in TestSdNotify and assumed there was a good reason (like a CI limitation.) If that's not the case, the testing package provides a nice TempDir() function that might be better for these tests (and more portable.) https://golang.org/pkg/testing/#T.TempDir

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() {
ctx, _ := context.WithTimeout(context.Background(), 500*time.Millisecond)
resultCh <- SdNotifyBarrier(ctx, tt.unsetEnv)
}()

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)
}

}
}