Repository: godbus/dbus
Pull request #123 (2018-10-24 20:15:35)

always invoke (Add|Remove)MatchSignal on bus object, add additional match options
I have added AddMatchSignal and RemoveMatchSignal to BusObject interface, so users don't have to do a type assert to real type. I also changed logic a bit - *MatchSignal methods now always operate on bus Object, so users can call this method on any BusObject (doc was not clear enough, because there is type BusObject and also method BusObject that returns BusObject for DBus special object).

I also added possilbility to specify additional filter option for *MatchSignal methods.

jsouthworth: Looks good, thanks!

Patch:
diff --git a/object.go b/object.go
index 5720dea..f27ffe1 100644
--- a/object.go
+++ b/object.go
@@ -13,6 +13,8 @@ type BusObject interface {
 	CallWithContext(ctx context.Context, method string, flags Flags, args ...interface{}) *Call
 	Go(method string, flags Flags, ch chan *Call, args ...interface{}) *Call
 	GoWithContext(ctx context.Context, method string, flags Flags, ch chan *Call, args ...interface{}) *Call
+	AddMatchSignal(iface, member string, options ...MatchOption) *Call
+	RemoveMatchSignal(iface, member string, options ...MatchOption) *Call
 	GetProperty(p string) (Variant, error)
 	Destination() string
 	Path() ObjectPath
@@ -35,23 +37,65 @@ func (o *Object) CallWithContext(ctx context.Context, method string, flags Flags
 	return <-o.createCall(ctx, method, flags, make(chan *Call, 1), args...).Done
 }
 
-// AddMatchSignal subscribes BusObject to signals from specified interface and
-// method (member).
-func (o *Object) AddMatchSignal(iface, member string) *Call {
-	return o.Call(
+// MatchOption specifies option for dbus routing match rule. Options can be constructed with WithMatch* helpers.
+// For full list of available options consult
+// https://dbus.freedesktop.org/doc/dbus-specification.html#message-bus-routing-match-rules
+type MatchOption struct {
+	key   string
+	value string
+}
+
+// WithMatchOption creates match option with given key and value
+func WithMatchOption(key, value string) MatchOption {
+	return MatchOption{key, value}
+}
+
+// WithMatchObjectPath creates match option that filters events based on given path
+func WithMatchObjectPath(path ObjectPath) MatchOption {
+	return MatchOption{"path", string(path)}
+}
+
+func formatMatchOptions(options []MatchOption) string {
+	items := make([]string, 0, len(options))
+	for _, option := range options {
+		items = append(items, option.key+"='"+option.value+"'")
+	}
+
+	return strings.Join(items, ",")
+}
+
+// AddMatchSignal subscribes BusObject to signals from specified interface,
+// method (member). Additional filter rules can be added via WithMatch* option constructors.
+// Note: To filter events by object path you have to specify this path via an option.
+func (o *Object) AddMatchSignal(iface, member string, options ...MatchOption) *Call {
+	base := []MatchOption{
+		{"type", "signal"},
+		{"interface", iface},
+		{"member", member},
+	}
+
+	options = append(base, options...)
+	return o.conn.BusObject().Call(
 		"org.freedesktop.DBus.AddMatch",
 		0,
-		"type='signal',interface='"+iface+"',member='"+member+"'",
+		formatMatchOptions(options),
 	)
 }
 
-// RemoveMatchSignal unsubscribes BusObject to signals from specified interface and
-// method (member).
-func (o *Object) RemoveMatchSignal(iface, member string) *Call {
-	return o.Call(
+// RemoveMatchSignal unsubscribes BusObject from signals from specified interface,
+// method (member). Additional filter rules can be added via WithMatch* option constructors
+func (o *Object) RemoveMatchSignal(iface, member string, options ...MatchOption) *Call {
+	base := []MatchOption{
+		{"type", "signal"},
+		{"interface", iface},
+		{"member", member},
+	}
+
+	options = append(base, options...)
+	return o.conn.BusObject().Call(
 		"org.freedesktop.DBus.RemoveMatch",
 		0,
-		"type='signal',interface='"+iface+"',member='"+member+"'",
+		formatMatchOptions(options),
 	)
 }
 


Test patch:
diff --git a/object_test.go b/object_test.go
index aad6945..f04e367 100644
--- a/object_test.go
+++ b/object_test.go
@@ -57,3 +57,94 @@ func TestObjectGoWithContext(t *testing.T) {
 		t.Fatal("Expected call to respond in 1 Millisecond")
 	}
 }
+
+type nopServer struct{}
+
+func (_ nopServer) Nop() *Error {
+	return nil
+}
+
+func fetchSignal(t *testing.T, ch chan *Signal, timeout time.Duration) *Signal {
+	select {
+	case sig := <-ch:
+		return sig
+	case <-time.After(timeout):
+		t.Fatalf("Failed to fetch signal in specified timeout %s", timeout)
+	}
+	return nil
+}
+
+func TestObjectSignalHandling(t *testing.T) {
+	bus, err := SessionBus()
+	if err != nil {
+		t.Fatalf("Unexpected error connecting to session bus: %s", err)
+	}
+
+	name := bus.Names()[0]
+	path := ObjectPath("/org/godbus/DBus/TestSignals")
+	otherPath := ObjectPath("/org/other-godbus/DBus/TestSignals")
+	iface := "org.godbus.DBus.TestSignals"
+	otherIface := "org.godbus.DBus.OtherTestSignals"
+	err = bus.Export(nopServer{}, path, iface)
+	if err != nil {
+		t.Fatalf("Unexpected error registering nop server: %v", err)
+	}
+
+	obj := bus.Object(name, path)
+	obj.AddMatchSignal(iface, "Heartbeat", WithMatchObjectPath(obj.Path()))
+
+	ch := make(chan *Signal, 5)
+	bus.Signal(ch)
+
+	go func() {
+		defer func() {
+			if err := recover(); err != nil {
+				t.Errorf("Catched panic in emitter goroutine: %v", err)
+			}
+		}()
+
+		// desired signals
+		bus.Emit(path, iface+".Heartbeat", uint32(1))
+		bus.Emit(path, iface+".Heartbeat", uint32(2))
+		// undesired signals
+		bus.Emit(otherPath, iface+".Heartbeat", uint32(3))
+		bus.Emit(otherPath, otherIface+".Heartbeat", uint32(4))
+		bus.Emit(path, iface+".Updated", false)
+		// sentinel
+		bus.Emit(path, iface+".Heartbeat", uint32(5))
+
+		time.Sleep(100 * time.Millisecond)
+		bus.Emit(path, iface+".Heartbeat", uint32(6))
+	}()
+
+	checkSignal := func(sig *Signal, value uint32) {
+		if sig.Path != path {
+			t.Errorf("signal.Path mismatch: %s != %s", path, sig.Path)
+		}
+
+		name := iface + ".Heartbeat"
+		if sig.Name != name {
+			t.Errorf("signal.Name mismatch: %s != %s", name, sig.Name)
+		}
+
+		if len(sig.Body) != 1 {
+			t.Errorf("Invalid signal body length: %d", len(sig.Body))
+			return
+		}
+
+		if sig.Body[0] != interface{}(value) {
+			t.Errorf("signal value mismatch: %d != %d", value, sig.Body[0])
+		}
+	}
+
+	checkSignal(fetchSignal(t, ch, 50*time.Millisecond), 1)
+	checkSignal(fetchSignal(t, ch, 50*time.Millisecond), 2)
+	checkSignal(fetchSignal(t, ch, 50*time.Millisecond), 5)
+
+	obj.RemoveMatchSignal(iface, "Heartbeat", WithMatchObjectPath(obj.Path()))
+	select {
+	case sig := <-ch:
+		t.Errorf("Got signal after removing subscription: %v", sig)
+	case <-time.After(200 * time.Millisecond):
+	}
+}