diff --git a/object.go b/object.go index 5720deaf..f27ffe14 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), ) } diff --git a/object_test.go b/object_test.go index aad6945b..f04e367c 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): + } +}