Repository: qtile/qtile

Issue:
Widget `Notify` throws error in logs every time it's used (even if it works fine).
### The issue:

Every time I feed my Notify widget with something (using, for example, `notify-send`), it throws the following into the log file:

````
Traceback (most recent call last):    
  File "/usr/lib/python3.11/asyncio/events.py", line 80, in _run    
    self._context.run(self._callback, *self._args)    
  File "/usr/lib/python3.11/site-packages/libqtile/core/manager.py", line 909, in f    
    func(*args)    
  File "/usr/lib/python3.11/site-packages/libqtile/widget/notify.py", line 132, in real_update    
    self.set_notif_text(notif)    
  File "/usr/lib/python3.11/site-packages/libqtile/widget/notify.py", line 104, in set_notif_text    
    self.text = pangocffi.markup_escape_text(notif.summary)    
    ^^^^^^^^^    
  File "/usr/lib/python3.11/site-packages/libqtile/widget/base.py", line 490, in text    
    self.layout.text = self.formatted_text    
    ^^^^^^^^^^^^^^^^    
  File "/usr/lib/python3.11/site-packages/libqtile/drawer.py", line 73, in text    
    self.layout.set_attributes(attrlist)    
  File "/usr/lib/python3.11/site-packages/libqtile/pangocffi.py", line 118, in set_attributes    
    pango.pango_layout_set_attributes(self._pointer, attrs)    
TypeError: initializer for ctype 'PangoLayout *' must be a cdata pointer, not NoneType
````

This error is repeated 16 times (always).

I've tried it on a default settings for the widget, so I don't think my configuration is relevant here.

````
~ ❯ qtile --version                                                                                                                                                                                                                                                                                                                                                26s
0.22.2.dev127+gffbf518e
````

### Required:

- [X] I have searched past issues to see if this bug has already been reported.


Discussion:
There is an open PR which solves that error message. However, it targets a different scenario which I'm not sure should apply here so I'll need to take a closer look at this one.

Thanks for the report.
FYI: I've just noticed the problem occurs only on Wayland. When I've switched to X11 session, log file is clean.
Interesting. Will check it out later.
Can't replicate this. Notification displays ok in wayland with no error message.
````
~ ❯ echo > .local/share/qtile/qtile.log
~ ❯ grep -c -i typeerror .local/share/qtile/qtile.log 
~ ❯ notify-send "test"
~ ❯ grep -c -i typeerror .local/share/qtile/qtile.log
9
~ ❯ head -n 20 .local/share/qtile/qtile.log          

2023-02-11 21:47:26,268 ERROR libqtile loop.py:_handle_exception():L62 Exception in event loop:
Traceback (most recent call last):
  File "/usr/lib/python3.11/asyncio/events.py", line 80, in _run
    self._context.run(self._callback, *self._args)
  File "/usr/lib/python3.11/site-packages/libqtile/core/manager.py", line 909, in f
    func(*args)
  File "/usr/lib/python3.11/site-packages/libqtile/widget/notify.py", line 132, in real_update
    self.set_notif_text(notif)
  File "/usr/lib/python3.11/site-packages/libqtile/widget/notify.py", line 104, in set_notif_text
    self.text = pangocffi.markup_escape_text(notif.summary)
    ^^^^^^^^^
  File "/usr/lib/python3.11/site-packages/libqtile/widget/base.py", line 490, in text
    self.layout.text = self.formatted_text
    ^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/site-packages/libqtile/drawer.py", line 73, in text
    self.layout.set_attributes(attrlist)
  File "/usr/lib/python3.11/site-packages/libqtile/pangocffi.py", line 118, in set_attributes
    pango.pango_layout_set_attributes(self._pointer, attrs)
TypeError: initializer for ctype 'PangoLayout *' must be a cdata pointer, not NoneType
~ ❯ 
````

My config is [here](https://git.insomniac.pl/ftpd/dotfiles/src/branch/wayland/_desktop/qtile/config.py) if it matters.
Btw. what's stopping you from merging https://github.com/qtile/qtile/pull/4113? Maybe it will 'automagically' help with this issue too.
> Btw. what's stopping you from merging #4113? Maybe it will 'automagically' help with this issue too.

It slows down our tests significantly.
I got a similar error with my X11 system.

I don't know if this is related to this issue, but in my case it also results in:
`TypeError: initializer for ctype 'PangoLayout *' must be a cdata pointer, not NoneType`.

When the error occurs, my system freezes completely and I'm not able to do anything anymore. No reaction from qtile. I had to switch to a tty to kill and restart qtile.

After every reboot I get the last two Errors after ~2-15 min of uptime. The first Error 
`ConfigError("Only one Systray can be used.")`
only appears from time to time.

[Here](https://gitlab.com/ManuelHaake/dotfiles/-/blob/master/.config/qtile/config.py) is my config if it helps. If I should create a separate issue for my problem, please let me know.


```
2023-02-19 18:46:13,162 ERROR libqtile bar.py:_configure_widget():L333 Systray widget crashed during _configure with error:
Traceback (most recent call last):
  File "/usr/lib/python3.10/site-packages/libqtile/bar.py", line 324, in _configure_widget
    widget._configure(self.qtile, self)
  File "/usr/lib/python3.10/site-packages/libqtile/widget/systray.py", line 152, in _configure
    raise ConfigError("Only one Systray can be used.")
libqtile.confreader.ConfigError: Only one Systray can be used.

2023-02-19 18:46:13,618 ERROR libqtile loop.py:_handle_exception():L63 GroupBox has no attribute: length
Traceback (most recent call last):
  File "/usr/lib/python3.10/asyncio/events.py", line 80, in _run
    self._context.run(self._callback, *self._args)
  File "/usr/lib/python3.10/site-packages/libqtile/core/manager.py", line 862, in f
    func(*args)
  File "/usr/lib/python3.10/site-packages/libqtile/bar.py", line 563, in _actual_draw
    self._resize(self.length, self.widgets)
  File "/usr/lib/python3.10/site-packages/libqtile/bar.py", line 410, in _resize
    stretchspace = length - sum([i.length for i in widgets if i.length_type != STRETCH])
  File "/usr/lib/python3.10/site-packages/libqtile/bar.py", line 410, in <listcomp>
    stretchspace = length - sum([i.length for i in widgets if i.length_type != STRETCH])
  File "/usr/lib/python3.10/site-packages/libqtile/configurable.py", line 48, in __getattr__
    raise AttributeError("%s has no attribute: %s" % (cname, name))
AttributeError: GroupBox has no attribute: length

2023-02-19 18:46:13,619 ERROR libqtile base.py:on_done():L802 Failed to reschedule.
Traceback (most recent call last):
  File "/usr/lib/python3.10/site-packages/libqtile/widget/base.py", line 794, in on_done
    self.update(result)
  File "/usr/lib/python3.10/site-packages/libqtile/widget/base.py", line 697, in update
    old_width = self.layout.width
  File "/usr/lib/python3.10/site-packages/libqtile/drawer.py", line 81, in width
    return self.layout.get_pixel_size()[0]
  File "/usr/lib/python3.10/site-packages/libqtile/pangocffi.py", line 138, in get_pixel_size
    pango.pango_layout_get_pixel_size(self._pointer, width, height)
TypeError: initializer for ctype 'PangoLayout *' must be a cdata pointer, not NoneType

```

After updating to 0.22.2.dev150+ge293f26c the problem is gone for me.
> I got a similar error with my X11 system.
> 
> I don't know if this is related to this issue, but in my case it also results in: `TypeError: initializer for ctype 'PangoLayout *' must be a cdata pointer, not NoneType`.
> 
> When the error occurs, my system freezes completely and I'm not able to do anything anymore. No reaction from qtile. I had to switch to a tty to kill and restart qtile.
> 
> After every reboot I get the last two Errors after ~2-15 min of uptime. The first Error `ConfigError("Only one Systray can be used.")` only appears from time to time.
> 
> [Here](https://gitlab.com/ManuelHaake/dotfiles/-/blob/master/.config/qtile/config.py) is my config if it helps. If I should create a separate issue for my problem, please let me know.
> 
> ```
> 2023-02-19 18:46:13,162 ERROR libqtile bar.py:_configure_widget():L333 Systray widget crashed during _configure with error:
> Traceback (most recent call last):
>   File "/usr/lib/python3.10/site-packages/libqtile/bar.py", line 324, in _configure_widget
>     widget._configure(self.qtile, self)
>   File "/usr/lib/python3.10/site-packages/libqtile/widget/systray.py", line 152, in _configure
>     raise ConfigError("Only one Systray can be used.")
> libqtile.confreader.ConfigError: Only one Systray can be used.
> 
> 2023-02-19 18:46:13,618 ERROR libqtile loop.py:_handle_exception():L63 GroupBox has no attribute: length
> Traceback (most recent call last):
>   File "/usr/lib/python3.10/asyncio/events.py", line 80, in _run
>     self._context.run(self._callback, *self._args)
>   File "/usr/lib/python3.10/site-packages/libqtile/core/manager.py", line 862, in f
>     func(*args)
>   File "/usr/lib/python3.10/site-packages/libqtile/bar.py", line 563, in _actual_draw
>     self._resize(self.length, self.widgets)
>   File "/usr/lib/python3.10/site-packages/libqtile/bar.py", line 410, in _resize
>     stretchspace = length - sum([i.length for i in widgets if i.length_type != STRETCH])
>   File "/usr/lib/python3.10/site-packages/libqtile/bar.py", line 410, in <listcomp>
>     stretchspace = length - sum([i.length for i in widgets if i.length_type != STRETCH])
>   File "/usr/lib/python3.10/site-packages/libqtile/configurable.py", line 48, in __getattr__
>     raise AttributeError("%s has no attribute: %s" % (cname, name))
> AttributeError: GroupBox has no attribute: length
> 
> 2023-02-19 18:46:13,619 ERROR libqtile base.py:on_done():L802 Failed to reschedule.
> Traceback (most recent call last):
>   File "/usr/lib/python3.10/site-packages/libqtile/widget/base.py", line 794, in on_done
>     self.update(result)
>   File "/usr/lib/python3.10/site-packages/libqtile/widget/base.py", line 697, in update
>     old_width = self.layout.width
>   File "/usr/lib/python3.10/site-packages/libqtile/drawer.py", line 81, in width
>     return self.layout.get_pixel_size()[0]
>   File "/usr/lib/python3.10/site-packages/libqtile/pangocffi.py", line 138, in get_pixel_size
>     pango.pango_layout_get_pixel_size(self._pointer, width, height)
> TypeError: initializer for ctype 'PangoLayout *' must be a cdata pointer, not NoneType
> ```

I think your issue is different to this one as this thread is specific to the `Notify` widget which you're not using.

There's a separate PR to fix the error message that you're seeing (#4113) but it hasn't been merged yet. I suspect this will fix your issue.

The `Systray` error message is different again but is exactly what it says. You can't have more than one tray but you're using it multiple times in your config.
I'm going to close this as the original issue is solved.

@lock-3 if the answers above don't help then you can start a new thread.
Sorry, but it seems my previous comment was a bit premature.

I don't have errors when I start qtile, until I reload it by `lazy.reload_config()`. After reload, each new notification generates one (always just one) error in the logfile.

@elParaguayo can you please reopen?
Are you able to see if adding the fix in #4113 helps as that is linked to reloading the config?
It can be a little troublesome, as now I've switched to qtile managed by my package manager. It is still a git version, but I can't specify other repositories/branches. And this is the only way I got it working in Wayland.

I will try to setup your branch in a virtualenv, but no promises. I will update you soon.
Understood.

The trouble for me is I can't replicate this issue with the Notify widget so I don't know if it's unrelated to that other one or not.
Yeah, I still can't get manually installed version working with Wayland.

But: I've modified `base.py` from your branch manually to check. Unfortunately, it doesn't solve the problem. I've noticed one more thing: the amount of occurrences of this error is strictly correlated to the numbers of restarts - so after I start qtile after reboot I don't have them, first reload makes one, second reload two and so on and so on. 
Thanks for testing that. Sorry it didn't work.

It sounds like this is something to do with callbacks from the notification manager not being cleared on a reload and so notifications are still being sent to finalised version of the widget. I'll take a look later.
> It sounds like this is something to do with callbacks from the notification manager not being cleared on a reload and so notifications are still being sent to finalised version of the widget. I'll take a look later.

Yes - pretty sure this is the issue. I can recreate after reloading the config. Now to work on the fix...


Resolving patch:
diff --git a/libqtile/notify.py b/libqtile/notify.py
--- a/libqtile/notify.py
+++ b/libqtile/notify.py
@@ -25,8 +25,8 @@
 from typing import Any
 
 try:
-    from dbus_next import ReleaseNameReply
     from dbus_next.aio import MessageBus
+    from dbus_next.constants import NameFlag, RequestNameReply
     from dbus_next.service import ServiceInterface, method, signal
 
     has_dbus = True
@@ -34,7 +34,6 @@
     has_dbus = False
 
 from libqtile.log_utils import logger
-from libqtile.utils import create_task
 
 BUS_NAME = "org.freedesktop.Notifications"
 SERVICE_PATH = "/org/freedesktop/Notifications"
@@ -130,17 +129,42 @@ def __init__(self):
             self.callbacks = []
             self.close_callbacks = []
             self._service = None
+            self.bus = None
 
         async def service(self):
-            if self._service is None:
-                try:
-                    self.bus = await MessageBus().connect()
+            if not self.callbacks:
+                if self.bus is None:
+                    try:
+                        self.bus = await MessageBus().connect()
+                    except Exception:
+                        logger.exception("Dbus connection failed")
+                        self._service = None
+                        return self._service
+
                     self._service = NotificationService(self)
                     self.bus.export(SERVICE_PATH, self._service)
-                    await self.bus.request_name(BUS_NAME)
-                except Exception:
-                    logger.exception("Dbus connection failed")
+
+                reply = await self.bus.request_name(
+                    BUS_NAME,
+                    flags=NameFlag.ALLOW_REPLACEMENT
+                    | NameFlag.REPLACE_EXISTING
+                    | NameFlag.DO_NOT_QUEUE,
+                )
+
+                # Check the reply to see if another server is already running.
+                # Other replies could be RequestNameReply.PRIMARY_OWNER or
+                # RequestNameReply.ALREADY_ONWER. Both would indicate qtile server
+                # is running successfully so we don't need to check for them.
+                # RequestNameReply.IN_QUEUE should not be possible as we pass
+                # NameFlag.DO_NOT_QUEUE when requesting the name.
+                if reply == RequestNameReply.EXISTS:
+                    logger.warning(
+                        "Cannot start notification server as another server is already running."
+                    )
                     self._service = None
+                    self.bus.disconnect()
+                    self.bus = None
+
             return self._service
 
         async def register(self, callback, capabilities=None, on_close=None):
@@ -168,23 +192,6 @@ def unregister(self, callback, on_close=None):
                 except ValueError:
                     logger.error("Unable to remove notify on_close callback. Unknown callback.")
 
-            if not self.callbacks:
-                return create_task(self._release())
-
-        async def _release(self):
-            """
-            If the manager has no more callbacks then we need to release the service name
-            from dbus and reset _service to None (to force subsequent calls to `register` to
-            re-register the name on dbus.)
-            """
-            reply = await self.bus.release_name(BUS_NAME)
-
-            if reply != ReleaseNameReply.RELEASED:
-                logger.error("Could not release %s", BUS_NAME)
-                return
-
-            self._service = None
-
         def add(self, notif):
             self.notifications.append(notif)
             notif.id = len(self.notifications)
diff --git a/libqtile/widget/notify.py b/libqtile/widget/notify.py
--- a/libqtile/widget/notify.py
+++ b/libqtile/widget/notify.py
@@ -207,23 +207,5 @@ def invoke(self):
             self._invoke()
 
     def finalize(self):
-        # We may need some async calls as part of the finalize call
-        # We run this with `call_soon_threadsafe` as this waits for
-        # the job to finish before continuing. This is important as,
-        # if the config is just reloading, we need to finish deregistering
-        # the notification server before the new Notify widget instance
-        # registers and creates a new server.
-        self.qtile.call_soon_threadsafe(self._finalize)
-        base._TextBox.finalize(self)
-
-    async def _finalize(self):
-        if notifier is not None:
-            task = notifier.unregister(self.update)
-
-            # If the notifier has no more callbacks then it needs to be stopped.
-            # The returned task will handle the release of the service name from
-            # dbus. We await it here to make sure it's finished before we
-            # complete the finalisation of this widget.
-            if task:
-                await task
+        notifier.unregister(self.update)
         base._TextBox.finalize(self)


Test patch:
diff --git a/test/widgets/test_notify.py b/test/widgets/test_notify.py
--- a/test/widgets/test_notify.py
+++ b/test/widgets/test_notify.py
@@ -21,7 +21,6 @@
 # NOTE: This test only tests the functionality of the widget and parts of the manager
 # The notification service (in libqtile/notify.py) is tested separately
 # TO DO: notification service test ;)
-import asyncio
 import shutil
 import subprocess
 import textwrap
@@ -30,7 +29,6 @@
 
 import libqtile.config
 from libqtile.bar import Bar
-from libqtile.notify import notifier
 from libqtile.widget import notify
 
 
@@ -288,7 +286,6 @@ def on_notification_closed(self, msg):
     manager_nospawn.c.eval(handler)
 
     _, result = manager_nospawn.c.eval("self.signal_listener")
-    print(result)
 
     # Send first notification and check time and display time
     notif_1 = [NS]
@@ -348,27 +345,20 @@ def test_parser(text):
 
 
 @pytest.mark.usefixtures("dbus")
-def test_unregister():
+def test_unregister(manager_nospawn, minimal_conf_noscreen):
     """Short test to check if notifier deregisters correctly."""
 
-    async def deregister():
-        def no_op(*args, **kwargs):
-            pass
+    def notifier_has_callbacks():
+        _, out = manager_nospawn.c.widget["notify"].eval("notifier.callbacks")
+        return out != "[]"
 
-        do_nothing = no_op
-
-        await notifier.register(no_op)
-        await notifier.register(do_nothing)
+    widget = notify.Notify()
+    config = minimal_conf_noscreen
+    config.screens = [libqtile.config.Screen(top=Bar([widget], 10))]
+    manager_nospawn.start(config)
 
-        # Remove one callback: service will not be stopped
-        task = notifier.unregister(no_op)
-        assert task is None
-        assert notifier._service
+    assert notifier_has_callbacks()
 
-        # Remove last callback: service will now be stopped
-        task = notifier.unregister(do_nothing)
-        assert task
-        await task
-        assert notifier._service is None
+    _ = manager_nospawn.c.widget["notify"].eval("self.finalize()")
 
-    asyncio.run(deregister())
+    assert not notifier_has_callbacks()