Notifications can't be replaced
The org.freedesktop.Notifications.Notify
method accepts a uint32 parameter named replaces_id
:
The optional notification ID that this notification replaces. The server must atomically (ie with no flicker or other visual cues) replace the given notification with this one. This allows clients to effectively modify the notification while it's active. A value of value of 0 means that this notification won't replace any existing notifications.
Unfortunately, this parameter is not respected. The notification daemon always behaves as though replaces_id
is 0. Below is an example where two slightly different messages were sent to the notification-daemon, where each had an identical non-zero replaces_id
. Note, however, that the messages don't actually need to be distinct, as shown below. The bug also appears when the messages are otherwise 100% identical.
I've reproduced this behavior on two physically separate hosts with drastically different hardware, across a time span of 6-12 months. Both hosts are running Arch Linux and GNOME 3.34.0.
I've reproduced this behavior with both Python and Rust applications. I've not reproduced it with dbus-send
, mainly because I don't know how to send empty arguments with dbus-send. Here's the shell (ha!) of a dbus-send
command which will reproduce the behavior:
dbus-send \
--session \
--type=method_call \
--dest=org.freedesktop.Notifications \
/org/freedesktop.Notifications \
org.freedesktop.Notifications.Notify \
string:'Trivial Notification Generator' \
uint32:8896011 \
string: \
string:'TNG message summary' \
string:'TNG message body' \
array: \
dict: \
int32:-1
Here's a relevant snippet from the Python application:
def send(summary: str, body: str) -> None:
"""Send a message to the notification service.
:param summary: The summary text briefly describing the notification.
:param body: The detailed body text.
"""
parameters = GLib.Variant('(susssasa{sv}i)', (
'Notification Generator',
0, # CHANGE THIS to reproduce the problem
'',
summary,
body,
[],
{},
-1,
))
proxy = Gio.DBusProxy.new_for_bus_sync(
bus_type=Gio.BusType.SESSION,
flags=Gio.DBusProxyFlags.NONE,
info=None,
name='org.freedesktop.Notifications',
object_path='/org/freedesktop/Notifications',
interface_name='org.freedesktop.Notifications',
cancellable=None,
)
proxy.call_sync(
method_name='Notify',
parameters=parameters,
flags=Gio.DBusCallFlags.NONE,
timeout_msec=-1, # use default timeout
cancellable=None,
)
...and here's a snippet from the Rust application:
pub fn notify(notification: &Notification) {
let conn = dbus::Connection::get_private(dbus::BusType::Session)
.expect("Failed to establish connection to session bus.");
let destination = "org.freedesktop.Notifications";
let path = "/org/freedesktop/Notifications";
let iface = "org.freedesktop.Notifications";
let name = "Notify";
let app_name = "Notification Generator";
let replaces_id: u32 = 0; // CHANGE THIS to reproduce the problem
let app_icon = "";
let summary = ¬ification.summary;
let body = ¬ification.body;
let actions: Vec<String> = vec![];
let hints: HashMap<String, Variant<Box<dyn RefArg>>> = HashMap::new();
let expire_timeout: i32 = -1;
let req = dbus::Message::new_method_call(destination, path, iface, name)
.expect("Failed to compile message due to invalid headers.")
.append1(app_name)
.append1(replaces_id)
.append1(app_icon)
.append1(summary)
.append1(body)
.append1(actions)
.append1(hints)
.append1(expire_timeout);
conn.send_with_reply_and_block(req, 5000)
.expect("Method call failed.");
}