Repository: qtile/qtile

Issue:
Widget crashed: Systray
### Issue description

When i enable both `widget.Bluetooth` and `widget.Systray` then widget bar is crashing

### Version

0.25.1.dev0+g005da458.d20240413

### Backend

X11 (default)

### Config

```python
from libqtile import bar, qtile, widget
from libqtile.config import Group, Key, Screen
from libqtile.lazy import lazy

from spotify import Spotify

# General
mod = "mod4"
terminal = "alacritty"
mvol = "pulsemixer --toggle-mute"
vup = "pactl set-sink-volume 0 +5%"
vdown = "pactl set-sink-volume 0 -5%"
bup = "brightnessctl s 10%+"
bdown = "brightnessctl s 10%-"
filemanager = "alacritty -e lf"
browser = "firefox"
emacs = "emacs"
dmenu = "dmenu_run"
powerprompt = "power"


keys = [
    # Navigation
    Key([mod], "h", lazy.layout.left(), desc="Move focus to left"),
    Key([mod], "l", lazy.layout.right(), desc="Move focus to right"),
    Key([mod], "j", lazy.layout.down(), desc="Move focus down"),
    Key([mod], "k", lazy.layout.up(), desc="Move focus up"),
    Key([mod], "Tab", lazy.screen.toggle_group(), desc="Switch last view group"),
    # Shuffle window
    Key([mod, "control"], "h", lazy.layout.shuffle_left(), desc="Suffle to left"),
    Key([mod, "control"], "l", lazy.layout.shuffle_right(), desc="Shuffle to right"),
    Key([mod, "control"], "j", lazy.layout.shuffle_down(), desc="Shuffle down"),
    Key([mod, "control"], "k", lazy.layout.shuffle_up(), desc="Shuffle up"),
    # Adjust window
    Key([mod, "shift"], "h", lazy.layout.grow_left(), desc="Grow to left"),
    Key([mod, "shift"], "l", lazy.layout.grow_right(), desc="Grow to right"),
    Key([mod, "shift"], "j", lazy.layout.grow_down(), desc="Grow down"),
    Key([mod, "shift"], "k", lazy.layout.grow_up(), desc="Grow up"),
    Key([mod, "control"], "space", lazy.layout.normalize(), desc="Reset all window"),
    Key([mod], "f", lazy.window.toggle_fullscreen(), desc="Fullscreen toggle"),
    # Media keys
    Key([], "XF86AudioMute", lazy.spawn(mvol), desc="Mute volume"),
    Key([], "XF86AudioRaiseVolume", lazy.spawn(vup), desc="Volume Up"),
    Key([], "XF86AudioLowerVolume", lazy.spawn(vdown), desc="volume down"),
    Key([], "XF86MonBrightnessUp", lazy.spawn(bup), desc="Brightness up"),
    Key([], "XF86MonBrightnessDown", lazy.spawn(bdown), desc="Brightness up"),
    Key([], "XF86TouchpadToggle", lazy.spawn("padtoggle"), desc="Toggle touchpad"),
    # System
    Key([mod], "q", lazy.window.kill(), desc="Kill focus window"),
    Key([mod, "control"], "r", lazy.reload_config(), desc="Reload configuration"),
    Key([mod], "escape", lazy.spawn(powerprompt), desc="Power menu"),
    Key([mod], "i", lazy.next_layout(), desc="Toggle layouts"),
    Key([mod, "control"], "x", lazy.shutdown(), desc="Shutdown qtile"),
    # Programs
    Key([mod], "Return", lazy.spawn(terminal), desc="Launch terminal"),
    Key([mod], "d", lazy.spawn(dmenu), desc="Dmenu"),
    Key([mod], "o", lazy.spawn(filemanager), desc="File manager lf"),
    Key([mod], "b", lazy.spawn(browser), desc="Web browser"),
    Key([mod], "e", lazy.spawn(emacs), desc="Emacs"),
    Key([mod], "w", lazy.spawn("searchweb"), desc="Web search via dmenu"),
]

# Groups
groups = [Group(f"{i+1}", label="●") for i in range(8)]

for i in groups:
    keys.extend(
        [
            Key(
                [mod],
                i.name,
                lazy.group[i.name].toscreen(),
                desc="Switch to group {}".format(i.name),
            ),
            Key(
                [mod, "shift"],
                i.name,
                lazy.window.togroup(i.name, switch_group=True),
            ),
            Key(
                [mod, "control"],
                i.name,
                lazy.window.togroup(i.name),
                desc="move focused window to group {}".format(i.name),
            ),
        ]
    )

# Gruvbox colors
color = [
    ["#1d2021", "#1d2021"],  # 0
    ["#cc241d", "#cc241d"],  # 1
    ["#98971a", "#98971a"],  # 2
    ["#d79921", "#d79921"],  # 3
    ["#458588", "#458588"],  # 4
    ["#b16286", "#b16286"],  # 5
    ["#689d6a", "#689d6a"],  # 6
    ["#a89984", "#a89984"],  # 7
    ["#928374", "#928374"],  # 8
    ["#fb4934", "#fb4934"],  # 9
    ["#b8bb26", "#b8bb26"],  # 10
    ["#fabd2f", "#fabd2f"],  # 11
    ["#83a598", "#83a598"],  # 12
    ["#d3869b", "#d3869b"],  # 13
    ["#8ec07c", "#8ec07c"],  # 14
    ["#ebdbb2", "#ebdbb2"],  # 15
    ["#3c3836", "#3c3836"],  # 16
]

# Layout theme
layout_theme = {
    "border_width": 2,
    "border_focus": color[1],
    "border_normal": color[0],
}
floating_theme = layout_theme.copy()

# Widget theme
widget_defaults = dict(
    font="monospace bold",
    fontsize=16,
    background=color[0],
    padding=5,
)
extension_defaults = widget_defaults.copy()


def power():
    qtile.cmd_spawn("power")


# Bar
screens = [
    Screen(
        top=bar.Bar(
            [
                widget.GroupBox(
                    fontsize=22,
                    background=color[0],
                    active=color[1],
                    inactive=color[16],
                    this_current_screen_border=color[0],
                    block_highlight_text_color=color[2],
                    margin_y=3,
                    margin_x=0,
                    padding_x=3,
                ),
                widget.CurrentLayout(
                    fmt="{}",
                    foreground=color[3],
                ),
                widget.WindowName(
                    format="{name}",
                    max_char=51,
                    foreground=color[4],
                ),
                widget.Sep(
                    size_percent=60,
                ),
                Spotify(
                    format="{artist} {icon}  {track}",
                    foreground=color[5],
                ),
                widget.Sep(
                     size_percent=60,
                 ),
                widget.Bluetooth(),
                widget.Sep(
                    size_percent=60,
                ),
                widget.Memory(
                    format="Mem:{MemUsed: .0f}{mm}/{MemTotal: .0f}{mm}",
                    foreground=color[14],
                ),
                widget.Sep(
                    size_percent=60,
                ),
                widget.Volume(
                    fmt="  {}",
                    foreground=color[12],
                ),
                widget.Sep(
                    size_percent=60,
                ),
                widget.Clock(
                    format=" %Y-%m-%d %a %I:%M %p",
                    foreground=color[3],
                ),
                widget.Battery(
                    format="{char} {percent:2.0%} {hour:d}:{min:02d}",
                    charge_char="  ",
                    full_char=" ",
                    discharge_char=" ",
                    update_interval=5,
                    mouse_callbacks={"Button1": power},
                    foreground=color[2],
                ),
                widget.Sep(
                    size_percent=60,
                ),
                widget.Systray(
                    icon_size=18,
                ),
            ],
            24,
        )
    )
]
```


### Logs

```text
2024-05-01 18:50:24,670 ERROR libqtile loop.py:_handle_exception():L62 Exception in event loop:
Traceback (most recent call last):
  File "/usr/lib/python3.12/site-packages/libqtile/widget/bluetooth.py", line 393, in _config_async
    await self._connect()
  File "/usr/lib/python3.12/site-packages/libqtile/widget/bluetooth.py", line 397, in _connect
    self.bus = await MessageBus(bus_type=BusType.SYSTEM).connect()
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/dbus_next/aio/message_bus.py", line 149, in connect
    await self._authenticate()
  File "/usr/lib/python3.12/site-packages/dbus_next/aio/message_bus.py", line 390, in _authenticate
    response = self._auth._receive_line(await self._auth_readline())
                                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/dbus_next/aio/message_bus.py", line 376, in _auth_readline
    buf += await self._loop.sock_recv(self._sock, 2)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/asyncio/selector_events.py", line 391, in sock_recv
    handle = self._add_reader(fd, self._sock_recv, fut, sock, n)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/asyncio/selector_events.py", line 288, in _add_reader
    self._selector.modify(fd, mask | selectors.EVENT_READ,
  File "/usr/lib/python3.12/selectors.py", line 389, in modify
    self._selector.modify(key.fd, selector_events)
FileNotFoundError: [Errno 2] No such file or directory
2024-05-01 18:50:24,906 ERROR libqtile manager.py:_finalize_configurables():L321 exception during finalize
Traceback (most recent call last):
  File "/usr/lib/python3.12/site-packages/libqtile/core/manager.py", line 309, in _finalize_configurables
    widget.finalize()
  File "/usr/lib/python3.12/site-packages/libqtile/widget/bluetooth.py", line 689, in finalize
    self.object_manager.off_interfaces_added(self._interface_added)
    ^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/libqtile/command/base.py", line 281, in __getattr__
    raise AttributeError(f"{self.__class__} has no attribute {name}")
AttributeError: <class 'libqtile.widget.bluetooth.Bluetooth'> has no attribute object_manager
2024-05-01 18:50:24,920 ERROR libqtile bar.py:_configure_widget():L388 Systray widget crashed during _configure with error:
Traceback (most recent call last):
  File "/usr/lib/python3.12/site-packages/libqtile/bar.py", line 379, in _configure_widget
    widget._configure(self.qtile, self)
  File "/usr/lib/python3.12/site-packages/libqtile/widget/systray.py", line 154, in _configure
    raise ConfigError("Only one Systray can be used.")
libqtile.confreader.ConfigError: Only one Systray can be used.
```


### Required

- [X] I have searched past issues to see if this bug has already been reported, and it hasn't been.
- [X] I understand that people give their precious time for free, and thus I've done my very best to make this problem as easy as possible to investigate.


Discussion:
That is very odd indeed! There's no reason those two should impact eachother.

I'll take a look when I get a chance.

In the meantime, can you confirm that the same issue arises if you add the `Bluetooth` widget to the default config?


> That is very odd indeed! There's no reason those two should impact eachother.
> 
> I'll take a look when I get a chance.
> 
> In the meantime, can you confirm that the same issue arises if you add the `Bluetooth` widget to the default config?

yes sir, same result with the default config. btw it happens when i reload the configuration.
That was my next question! The systray error is about having multiple instances of the widget but it should be fine reloading the config normally. So, for some reason, the bluetooth widget must be stopping the widget from being unloaded correctly.

It's a weird one so thanks for reporting it!
> That was my next question! The systray error is about having multiple instances of the widget but it should be fine reloading the config normally. So, for some reason, the bluetooth widget must be stopping the widget from being unloaded correctly.
> 
> It's a weird one so thanks for reporting it!

okay thanks sir, so the problem is with `widget.Bluetooth`? if i disabled `widget.Systray` and enable only the `widget.Bluetooth` it doesn't crash. same with if only `widget.Systray` is enabled. it just crashing when i enable them both at the same time.
My guess is the Bluetooth widget is crashing when you reload the config. This is then stopping the systray from being unloaded correctly. 

Can you try putting the Bluetooth widget after the systray and see if the systray still crashes?
> My guess is the Bluetooth widget is crashing when you reload the config. This is then stopping the systray from being unloaded correctly.
> 
> Can you try putting the Bluetooth widget after the systray and see if the systray still crashes?

yes sir it's still crashing. ill post the `qtile.log`
```
2024-05-01 20:41:11,310 ERROR libqtile loop.py:_handle_exception():L62 Exception in event loop:
Traceback (most recent call last):
  File "/usr/lib/python3.12/site-packages/libqtile/widget/bluetooth.py", line 393, in _config_async
    await self._connect()
  File "/usr/lib/python3.12/site-packages/libqtile/widget/bluetooth.py", line 397, in _connect
    self.bus = await MessageBus(bus_type=BusType.SYSTEM).connect()
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/dbus_next/aio/message_bus.py", line 149, in connect
    await self._authenticate()
  File "/usr/lib/python3.12/site-packages/dbus_next/aio/message_bus.py", line 390, in _authenticate
    response = self._auth._receive_line(await self._auth_readline())
                                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/dbus_next/aio/message_bus.py", line 376, in _auth_readline
    buf += await self._loop.sock_recv(self._sock, 2)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/asyncio/selector_events.py", line 391, in sock_recv
    handle = self._add_reader(fd, self._sock_recv, fut, sock, n)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/asyncio/selector_events.py", line 288, in _add_reader
    self._selector.modify(fd, mask | selectors.EVENT_READ,
  File "/usr/lib/python3.12/selectors.py", line 389, in modify
    self._selector.modify(key.fd, selector_events)
FileNotFoundError: [Errno 2] No such file or directory
2024-05-01 20:41:12,475 ERROR libqtile manager.py:_finalize_configurables():L321 exception during finalize
Traceback (most recent call last):
  File "/usr/lib/python3.12/site-packages/libqtile/core/manager.py", line 309, in _finalize_configurables
    widget.finalize()
  File "/usr/lib/python3.12/site-packages/libqtile/widget/bluetooth.py", line 689, in finalize
    self.object_manager.off_interfaces_added(self._interface_added)
    ^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/libqtile/command/base.py", line 281, in __getattr__
    raise AttributeError(f"{self.__class__} has no attribute {name}")
AttributeError: <class 'libqtile.widget.bluetooth.Bluetooth'> has no attribute object_manager
2024-05-01 20:41:13,063 ERROR libqtile manager.py:_finalize_configurables():L321 exception during finalize
Traceback (most recent call last):
  File "/usr/lib/python3.12/site-packages/libqtile/hook.py", line 150, in _subscribe
    lst.remove(func)
ValueError: list.remove(x): x not in list

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/lib/python3.12/site-packages/libqtile/core/manager.py", line 309, in _finalize_configurables
    widget.finalize()
  File "/usr/lib/python3.12/site-packages/libqtile/widget/groupbox.py", line 145, in finalize
    self.remove_hooks()
  File "/usr/lib/python3.12/site-packages/libqtile/widget/groupbox.py", line 85, in remove_hooks
    hook.unsubscribe.client_managed(self._hook_response)
  File "/usr/lib/python3.12/site-packages/libqtile/hook.py", line 123, in _hook_func
    return self._subscribe(hook.name, func)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/libqtile/hook.py", line 152, in _subscribe
    raise utils.QtileError(
libqtile.utils.QtileError: Tried to unsubscribe a hook that was not currently subscribed
2024-05-01 20:41:13,077 ERROR libqtile bar.py:_configure_widget():L388 Systray widget crashed during _configure with error:
Traceback (most recent call last):
  File "/usr/lib/python3.12/site-packages/libqtile/bar.py", line 379, in _configure_widget
    widget._configure(self.qtile, self)
  File "/usr/lib/python3.12/site-packages/libqtile/widget/systray.py", line 154, in _configure
    raise ConfigError("Only one Systray can be used.")
libqtile.confreader.ConfigError: Only one Systray can be used.
```
Ok. I need a new theory then!
okay thanks sir, ill disable `widget.Bluetooth` for the meantime.
Did you restart qtile after moving the Bluetooth widget position, or just reload the config?
Just tried it here. No issue on my machine. If you disable systray do you still get the Bluetooth error? I suspect that's the source of the problem here.
> Did you restart qtile after moving the Bluetooth widget position, or just reload the config?

yes i did restart qtile.
> Just tried it here. No issue on my machine. If you disable systray do you still get the Bluetooth error? I suspect that's the source of the problem here.

if i disable `widget.Systray` i dont have the error. 
try spamming the reload keybind `[mod, "control"] "r"` and check if you'll get `Widget crash` while both `widget.Bluetooth` and `widget.Systray` enabled.

Resolving patch:
diff --git a/libqtile/widget/bluetooth.py b/libqtile/widget/bluetooth.py
--- a/libqtile/widget/bluetooth.py
+++ b/libqtile/widget/bluetooth.py
@@ -379,6 +379,7 @@ def __init__(self, **config):
             {"Button1": self.click, "Button4": self.scroll_up, "Button5": self.scroll_down}
         )
         self.timer = None
+        self.object_manager = None
 
     def _configure(self, qtile, bar):
         base._TextBox._configure(self, qtile, bar)
@@ -674,15 +675,20 @@ def hide(self):
         self.refresh()
 
     def finalize(self):
-        """Remove dbus signal handlers before finalising."""
+        # if we failed to connect, there is nothing to finalize.
+        if self.bus is None:
+            return
+
+        # Remove dbus signal handlers before finalising.
         # Clearing dicts will call the __del__ method on the stored objects
         # which has been defined to remove signal handlers
         self.devices.clear()
         self.adapters.clear()
 
         # Remove object manager's handlers
-        self.object_manager.off_interfaces_added(self._interface_added)
-        self.object_manager.off_interfaces_removed(self._interface_removed)
+        if self.object_manager is not None:
+            self.object_manager.off_interfaces_added(self._interface_added)
+            self.object_manager.off_interfaces_removed(self._interface_removed)
 
         # Disconnect the bus connection
         self.bus.disconnect()


Test patch: