diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index c5564835..de144f76 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -11,7 +11,7 @@ permissions: env: # Minimum supported Go toolchain - ACTION_MINIMUM_TOOLCHAIN: "1.12" + ACTION_MINIMUM_TOOLCHAIN: "1.13" jobs: build: diff --git a/README.md b/README.md index 9f9f6de0..11c79d26 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # go-systemd [![godoc](https://img.shields.io/badge/godoc-reference-5272B4)](https://pkg.go.dev/mod/github.com/coreos/go-systemd/v22/?tab=packages) -![minimum golang 1.12](https://img.shields.io/badge/golang-1.12%2B-orange.svg) +![minimum golang 1.13](https://img.shields.io/badge/golang-1.13%2B-orange.svg) Go bindings to systemd. The project has several packages: diff --git a/go.mod b/go.mod index d285cf36..1c935b0f 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,5 @@ module github.com/coreos/go-systemd/v22 -go 1.12 +go 1.13 require github.com/godbus/dbus/v5 v5.0.4 diff --git a/login1/dbus.go b/login1/dbus.go index ca71308c..0ff7cccc 100644 --- a/login1/dbus.go +++ b/login1/dbus.go @@ -16,6 +16,7 @@ package login1 import ( + "context" "fmt" "os" "strconv" @@ -32,8 +33,34 @@ const ( // Conn is a connection to systemds dbus endpoint. type Conn struct { - conn *dbus.Conn - object dbus.BusObject + conn Connection + connManager connectionManager + object Caller +} + +// Objector describes functionality required from a given D-Bus connection. +type Connection interface { + Object(string, dbus.ObjectPath) dbus.BusObject + Signal(ch chan<- *dbus.Signal) + // TODO: This should be replaced with AddMatchSignal. + // See https://github.com/coreos/go-systemd/issues/388 for details. + BusObject() dbus.BusObject +} + +// ConnectionManager explicitly wraps dependencies on established D-Bus connection. +type connectionManager interface { + Hello() error + Auth(authMethods []dbus.Auth) error + Close() error + + Connection +} + +// Caller describes required functionality from D-Bus object. +type Caller interface { + // TODO: This method should eventually be removed, as it provides no context support. + Call(method string, flags dbus.Flags, args ...interface{}) *dbus.Call + CallWithContext(ctx context.Context, method string, flags dbus.Flags, args ...interface{}) *dbus.Call } // New establishes a connection to the system bus and authenticates. @@ -47,20 +74,32 @@ func New() (*Conn, error) { return c, nil } +// NewWithConnection creates new login1 client using given D-Bus connection. +func NewWithConnection(connection Connection) (*Conn, error) { + if connection == nil { + return nil, fmt.Errorf("no connection given") + } + + return &Conn{ + conn: connection, + object: connection.Object(dbusDest, dbusPath), + }, nil +} + // Close closes the dbus connection func (c *Conn) Close() { if c == nil { return } - if c.conn != nil { - c.conn.Close() + if c.conn != nil && c.connManager != nil { + c.connManager.Close() } } func (c *Conn) initConnection() error { var err error - c.conn, err = dbus.SystemBusPrivate() + c.connManager, err = dbus.SystemBusPrivate() if err != nil { return err } @@ -70,18 +109,19 @@ func (c *Conn) initConnection() error { // libc) methods := []dbus.Auth{dbus.AuthExternal(strconv.Itoa(os.Getuid()))} - err = c.conn.Auth(methods) + err = c.connManager.Auth(methods) if err != nil { - c.conn.Close() + c.connManager.Close() return err } - err = c.conn.Hello() + err = c.connManager.Hello() if err != nil { - c.conn.Close() + c.connManager.Close() return err } + c.conn = c.connManager c.object = c.conn.Object("org.freedesktop.login1", dbus.ObjectPath(dbusPath)) return nil @@ -309,6 +349,15 @@ func (c *Conn) Reboot(askForAuth bool) { c.object.Call(dbusInterface+".Reboot", 0, askForAuth) } +// Reboot asks logind for a reboot using given context, optionally asking for auth. +func (c *Conn) RebootWithContext(ctx context.Context, askForAuth bool) error { + if call := c.object.CallWithContext(ctx, dbusInterface+".Reboot", 0, askForAuth); call.Err != nil { + return fmt.Errorf("calling reboot: %w", call.Err) + } + + return nil +} + // Inhibit takes inhibition lock in logind. func (c *Conn) Inhibit(what, who, why, mode string) (*os.File, error) { var fd dbus.UnixFD diff --git a/login1/dbus_test.go b/login1/dbus_test.go index b570c921..1fd64e09 100644 --- a/login1/dbus_test.go +++ b/login1/dbus_test.go @@ -12,26 +12,31 @@ // See the License for the specific language governing permissions and // limitations under the License. -package login1 +package login1_test import ( + "context" + "errors" "fmt" "os/user" "regexp" "testing" + + "github.com/godbus/dbus/v5" + + "github.com/coreos/go-systemd/v22/login1" ) // TestNew ensures that New() works without errors. func TestNew(t *testing.T) { - _, err := New() - + _, err := login1.New() if err != nil { t.Fatal(err) } } func TestListSessions(t *testing.T) { - c, err := New() + c, err := login1.New() if err != nil { t.Fatal(err) } @@ -60,7 +65,7 @@ func TestListSessions(t *testing.T) { } func TestListUsers(t *testing.T) { - c, err := New() + c, err := login1.New() if err != nil { t.Fatal(err) } @@ -87,3 +92,320 @@ func TestListUsers(t *testing.T) { } } } + +func Test_Creating_new_connection_with_custom_connection(t *testing.T) { + t.Parallel() + + t.Run("connects_to_global_login1_path_and_interface", func(t *testing.T) { + t.Parallel() + + objectConstructorCalled := false + + connectionWithContextCheck := &mockConnection{ + ObjectF: func(dest string, path dbus.ObjectPath) dbus.BusObject { + objectConstructorCalled = true + + expectedDest := "org.freedesktop.login1" + + if dest != expectedDest { + t.Fatalf("Expected D-Bus destination %q, got %q", expectedDest, dest) + } + + expectedPath := dbus.ObjectPath("/org/freedesktop/login1") + + if path != expectedPath { + t.Fatalf("Expected D-Bus path %q, got %q", expectedPath, path) + } + + return nil + }, + } + + if _, err := login1.NewWithConnection(connectionWithContextCheck); err != nil { + t.Fatalf("Unexpected error creating connection: %v", err) + } + + if !objectConstructorCalled { + t.Fatalf("Expected object constructor to be called") + } + }) + + t.Run("returns_error_when_no_custom_connection_is_given", func(t *testing.T) { + t.Parallel() + + testConn, err := login1.NewWithConnection(nil) + if err == nil { + t.Fatalf("Expected error creating connection with no connector") + } + + if testConn != nil { + t.Fatalf("Expected connection to be nil when New returns error") + } + }) +} + +//nolint:funlen // Many subtests. +func Test_Rebooting_with_context(t *testing.T) { + t.Parallel() + + t.Run("calls_login1_reboot_method_on_manager_interface", func(t *testing.T) { + t.Parallel() + + rebootCalled := false + + askForReboot := false + + connectionWithContextCheck := &mockConnection{ + ObjectF: func(string, dbus.ObjectPath) dbus.BusObject { + return &mockObject{ + CallWithContextF: func(ctx context.Context, method string, flags dbus.Flags, args ...interface{}) *dbus.Call { + rebootCalled = true + + expectedMethodName := "org.freedesktop.login1.Manager.Reboot" + + if method != expectedMethodName { + t.Fatalf("Expected method %q being called, got %q", expectedMethodName, method) + } + + if len(args) != 1 { + t.Fatalf("Expected one argument to call, got %q", args) + } + + askedForReboot, ok := args[0].(bool) + if !ok { + t.Fatalf("Expected first argument to be of type %T, got %T", askForReboot, args[0]) + } + + if askForReboot != askedForReboot { + t.Fatalf("Expected argument to be %t, got %t", askForReboot, askedForReboot) + } + + return &dbus.Call{} + }, + } + }, + } + + testConn, err := login1.NewWithConnection(connectionWithContextCheck) + if err != nil { + t.Fatalf("Unexpected error creating connection: %v", err) + } + + if err := testConn.RebootWithContext(context.Background(), askForReboot); err != nil { + t.Fatalf("Unexpected error rebooting: %v", err) + } + + if !rebootCalled { + t.Fatalf("Expected reboot method call on given D-Bus connection") + } + }) + + t.Run("asks_for_auth_when_requested", func(t *testing.T) { + t.Parallel() + + rebootCalled := false + + askForReboot := true + + connectionWithContextCheck := &mockConnection{ + ObjectF: func(string, dbus.ObjectPath) dbus.BusObject { + return &mockObject{ + CallWithContextF: func(ctx context.Context, method string, flags dbus.Flags, args ...interface{}) *dbus.Call { + rebootCalled = true + + if len(args) != 1 { + t.Fatalf("Expected one argument to call, got %q", args) + } + + askedForReboot, ok := args[0].(bool) + if !ok { + t.Fatalf("Expected first argument to be of type %T, got %T", askForReboot, args[0]) + } + + if askForReboot != askedForReboot { + t.Fatalf("Expected argument to be %t, got %t", askForReboot, askedForReboot) + } + + return &dbus.Call{} + }, + } + }, + } + + testConn, err := login1.NewWithConnection(connectionWithContextCheck) + if err != nil { + t.Fatalf("Unexpected error creating connection: %v", err) + } + + if err := testConn.RebootWithContext(context.Background(), askForReboot); err != nil { + t.Fatalf("Unexpected error rebooting: %v", err) + } + + if !rebootCalled { + t.Fatalf("Expected reboot method call on given D-Bus connection") + } + }) + + t.Run("use_given_context_for_D-Bus_call", func(t *testing.T) { + t.Parallel() + + testKey := struct{}{} + expectedValue := "bar" + + ctx := context.WithValue(context.Background(), testKey, expectedValue) + + connectionWithContextCheck := &mockConnection{ + ObjectF: func(string, dbus.ObjectPath) dbus.BusObject { + return &mockObject{ + CallWithContextF: func(ctx context.Context, method string, flags dbus.Flags, args ...interface{}) *dbus.Call { + if val := ctx.Value(testKey); val != expectedValue { + t.Fatalf("Got unexpected context on call") + } + + return &dbus.Call{} + }, + } + }, + } + + testConn, err := login1.NewWithConnection(connectionWithContextCheck) + if err != nil { + t.Fatalf("Unexpected error creating connection: %v", err) + } + + if err := testConn.RebootWithContext(ctx, false); err != nil { + t.Fatalf("Unexpected error rebooting: %v", err) + } + }) + + t.Run("returns_error_when_D-Bus_call_fails", func(t *testing.T) { + t.Parallel() + + expectedError := fmt.Errorf("reboot error") + + connectionWithFailingObjectCall := &mockConnection{ + ObjectF: func(string, dbus.ObjectPath) dbus.BusObject { + return &mockObject{ + CallWithContextF: func(ctx context.Context, method string, flags dbus.Flags, args ...interface{}) *dbus.Call { + return &dbus.Call{ + Err: expectedError, + } + }, + } + }, + } + + testConn, err := login1.NewWithConnection(connectionWithFailingObjectCall) + if err != nil { + t.Fatalf("Unexpected error creating connection: %v", err) + } + + if err := testConn.RebootWithContext(context.Background(), false); !errors.Is(err, expectedError) { + t.Fatalf("Unexpected error rebooting: %v", err) + } + }) +} + +// mockConnection is a test helper for mocking dbus.Conn. +type mockConnection struct { + ObjectF func(string, dbus.ObjectPath) dbus.BusObject +} + +// Auth ... +func (m *mockConnection) Auth(authMethods []dbus.Auth) error { + return nil +} + +// Hello ... +func (m *mockConnection) Hello() error { + return nil +} + +// Signal ... +func (m *mockConnection) Signal(ch chan<- *dbus.Signal) {} + +// Object ... +func (m *mockConnection) Object(dest string, path dbus.ObjectPath) dbus.BusObject { + if m.ObjectF == nil { + return nil + } + + return m.ObjectF(dest, path) +} + +// Close ... +func (m *mockConnection) Close() error { + return nil +} + +// BusObject ... +func (m *mockConnection) BusObject() dbus.BusObject { + return nil +} + +// mockObject is a mock of dbus.BusObject. +type mockObject struct { + CallWithContextF func(context.Context, string, dbus.Flags, ...interface{}) *dbus.Call + CallF func(string, dbus.Flags, ...interface{}) *dbus.Call +} + +// mockObject must implement dbus.BusObject to be usable for other packages in tests, though not +// all methods must actually be mockable. See https://github.com/dbus/dbus/issues/252 for details. +var _ dbus.BusObject = &mockObject{} + +// CallWithContext ... +// +//nolint:lll // Upstream signature, can't do much with that. +func (m *mockObject) CallWithContext(ctx context.Context, method string, flags dbus.Flags, args ...interface{}) *dbus.Call { + if m.CallWithContextF == nil { + return &dbus.Call{} + } + + return m.CallWithContextF(ctx, method, flags, args...) +} + +// Call ... +func (m *mockObject) Call(method string, flags dbus.Flags, args ...interface{}) *dbus.Call { + if m.CallF == nil { + return &dbus.Call{} + } + + return m.CallF(method, flags, args...) +} + +// Go ... +func (m *mockObject) Go(method string, flags dbus.Flags, ch chan *dbus.Call, args ...interface{}) *dbus.Call { + return &dbus.Call{} +} + +// GoWithContext ... +// +//nolint:lll // Upstream signature, can't do much with that. +func (m *mockObject) GoWithContext(ctx context.Context, method string, flags dbus.Flags, ch chan *dbus.Call, args ...interface{}) *dbus.Call { + return &dbus.Call{} +} + +// AddMatchSignal ... +func (m *mockObject) AddMatchSignal(iface, member string, options ...dbus.MatchOption) *dbus.Call { + return &dbus.Call{} +} + +// RemoveMatchSignal ... +func (m *mockObject) RemoveMatchSignal(iface, member string, options ...dbus.MatchOption) *dbus.Call { + return &dbus.Call{} +} + +// GetProperty ... +func (m *mockObject) GetProperty(p string) (dbus.Variant, error) { return dbus.Variant{}, nil } + +// StoreProperty ... +func (m *mockObject) StoreProperty(p string, value interface{}) error { return nil } + +// SetProperty ... +func (m *mockObject) SetProperty(p string, v interface{}) error { return nil } + +// Destination ... +func (m *mockObject) Destination() string { return "" } + +// Path ... +func (m *mockObject) Path() dbus.ObjectPath { return "" }