ldo / dbussy

Python binding for D-Bus using asyncio

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Unable to duplicate bus filter

kent-williams opened this issue · comments

Hello,

First off thank you so much for sharing this library! I'm working on an asyncio bluetooth low energy (bluez) library utilizing your library.

I have a class that sets up a callback filter using add_filter & bus_add_match. My issue is that when multiple instances of this class are created, only the first instance to register the filter is receiving all the message signals. I have been sure to include the path, since that is the only differentiator between the bus_add_match calls. My question therefore is, should I be treating the bus_add_match as a global rule, or should they be able to register match rules individually?

Hopefully this is clear enough, happy to expand if not.
Thanks!
-Kent

Let me try and understand what you are doing.

Are you opening multiple connections to the same D-Bus daemon? Or are you registering these callbacks on the same connection? I imagine that separate Connection objects would each receive their own copies of appropriately-addressed incoming messages.

If you are registering multiple filters on the same Connection, are you returning DBUS.HANDLER_RESULT_NOT_YET_HANDLED from the handlers? Because DBUS.HANDLER_RESULT_HANDLED will stop the search for further filters.

For each instance of the class, I am opening a new connection to the same D-Bus daemon (DBUS.BUS_SYSTEM). For each one I am calling bus_add_match with the same rules, but differing only in the path. All signals that were registered with different path rules are being sent to the first registered filter and match only, and no other instances after that.

I recently observed something new. When I switched the signal call back return from HANDLER_RESULT_HANDLED to HANDLER_RESULT_NOT_YET_HANDLED, each instance of the class received the message. This means that the path key is not being properly filtered I'd imagine, and therefore the message is sent to both, even though it should only be sent to the callback that registered the appropriate keys including the sender path. The bus_add_match looks like the following:

self._dbus.bus_add_match({"type": "signal", "interface": "org.freedesktop.DBus.Properties", "member": "PropertiesChanged",
            "arg0": "org.bluez.GattCharacteristic1", "path": char_path})

Where the path looks something like '/org/bluez/hci0/dev_D4_D2_34_E3_BC_C5/service001b/char001f'

Does anything stand out that might indicate why that sender path isn't being filtered correctly?

Thanks!

Just a note that add_filter lets you filter all messages. As for bus_add_match, note that it should do prefix matching if either the message or match path ends with /. The latter doesn’t in your case, but what about the former?

I very much appreciate the help! I hadn't discovered bus_add_match_action until now, and it works as expected. I must still be misunderstanding the use of add_filter and bus_add_match.

I am however experiencing a SIGSEGV when attempting to call bus_remove_match_action. I was experiencing the same behavior when trying to use remove_filter. I'm passing it the same rule and func. Thoughts?

Just a note, are you specifying private = True on at least the second Connection.bus_get call? Otherwise libdbus will return a common shared connection object.

I was not, I had it set to False, but I have attempted using True with the same result unfortunately. This is occurring with only a single instance as well, regardless of private state.

Here is an example script I put together:

import sys
import os
import asyncio
import dbussy as dbus
from dbussy import \
    DBUS

def get_bool_env(name, default) :
    val = os.environ.get(name, "").lower()
    if default :
        result = not any(val.startswith(c) for c in "0nf")
    else :
        result = any(val.startswith(c) for c in "1yt")
    #end if
    return \
        result
#end get_bool_env

#+
# Mainline
#-

separate_conns = get_bool_env("SEPARATE_CONNS", True)
private_conn = get_bool_env("PRIVATE_CONN", True)
sys.stderr.write("Separate connections: %s; private conn: %s\n" % (separate_conns, private_conn))

loop = asyncio.get_event_loop()

async def mainline() :
    conn1 = await dbus.Connection.bus_get_async(type = DBUS.BUS_SESSION, private = False)
    conn1.enable_receive_message({DBUS.MESSAGE_TYPE_SIGNAL})
    if separate_conns :
        conn2 = await dbus.Connection.bus_get_async(type = DBUS.BUS_SESSION, private = private_conn)
        conn2.enable_receive_message({DBUS.MESSAGE_TYPE_SIGNAL})
        use_conn2 = conn2
    else :
        conn2 = None
        use_conn2 = conn1
    #end if

    await conn1.bus_add_match_async \
      (
        {
            "type" : "signal",
            "interface": "org.freedesktop.DBus.Properties",
            "path" : "/org/bluez/hci0/dev_D4_D2_34_E3_BC_C5/service001b/char001f",
            "arg0": "org.bluez.GattCharacteristic1",
        }
      )
    await use_conn2.bus_add_match_async \
      (
        {
            "type" : "signal",
            "interface": "org.freedesktop.DBus.Properties",
            "path" : "/org/bluez/hci1/dev_D4_D2_34_E3_BC_C5/service001b/char001f",
            "arg0": "org.bluez.GattCharacteristic2",
        }
      )
    to_check = []
    while True :
        if len(to_check) == 0 :
            to_check = [["conn1", conn1]] + ([], [["conn2", conn2]])[conn2 != None]
        #end if
        conn_name, conn = to_check.pop()
        message = await conn.receive_message_async(timeout = 0.25)
        if message != None :
            sys.stderr.write("%s got a message, type %d, serial = %d\n" % (conn_name, message.type, message.serial))
            if message.type == DBUS.MESSAGE_TYPE_METHOD_CALL :
                sys.stderr.write(" destination = %s\n" % repr(message.destination))
            #end if
            if message.type in (DBUS.MESSAGE_TYPE_METHOD_CALL, DBUS.MESSAGE_TYPE_SIGNAL) :
                sys.stderr.write(" path = %s, interface = %s, name = %s\n" % (repr(message.path), repr(message.interface), repr(message.member)))
            #end if
            sys.stderr.write(" contents = %s\n" % repr(list(message.objects)))
            sys.stdout.flush()
        #end if
    #end while
#end mainline

loop.run_until_complete(mainline())

Does that look like it reproduces your situation? By default it opens two bus connections, sets a different filter on each, then sits in a loop polling them for incoming messages. When I run that (without setting any environment variables), and send it signals with commands like

dbus-send --session --type=signal /org/bluez/hci0/dev_D4_D2_34_E3_BC_C5/service001b/char001f org.freedesktop.DBus.Properties.PropertiesChanged string:org.bluez.GattCharacteristic1

and

dbus-send --session --type=signal /org/bluez/hci1/dev_D4_D2_34_E3_BC_C5/service001b/char001f org.freedesktop.DBus.Properties.PropertiesChanged string:org.bluez.GattCharacteristic2

I always get output which indicates that each message was received on exactly the right connection.

Feel free to point out what I have missed.

Thanks so much for putting that together, I will report back tomorrow!

This certainly works as expected, I now realize that I was incorrectly using add_filter and bus_add_match. I am now accomplishing the same objective with bus_add_match_action, and it works as expected, so all good there.

My only remaining issue now is that I receive a SIGSEGV when attempting to call bus_remove_match_action. It's happening at this line. Please let me know if examining some of the values in that func would help.

This example does bus_add/remove_match_async, and seems to work OK for me without crashes. Is that helpful to you?

I've done some more testing and continue to get the SIGSEGV when bus_remove_match_action_async is called when no more match actions are remaining and self.remove_filter(self._rule_action_match, None) is called here. You can see where I am adding and removing the match actions here add#1, add#2 and remove#1, remove#2. Apologies, the code is a bit rough still. I can't see any significant difference in my use and yours.

@ldo I am no longer experiencing the seg fault after your latest changes! Thanks so much for continuing to help me. I'll go ahead and close this.

Glad to hear it. All I did was realize that an explicit PendingCall.cancel() call could cause an exception inside the pending_done callback. It should also cause an exception in the await_reply() call, but that one will be passed back to the caller.

Oops, sorry, that comment applied to a different bug. I have no idea why my recent changes would have fixed your problem. :)

Oh no, the mystery remains!

@ldo This may help narrow down which change solved it.. I am not experiencing the seg fault with the current master, but I am with the current wheel on pypi.

@kent-williams Unfortunately that upload is quite a few months old now. The best way to track the difference down is with git-bisect.