diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c90565e..7b8842cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,10 @@ Versioning](https://semver.org/spec/v2.0.0.html). aside from the audio thread pool extension. Support for the extension will be added in a future yabridge release as Windows-only plugins that rely on the feature get released. +- Notifications are now sent by directly talking to D-Bus instead of using the + `notify-send` command line tool. This ensures that you'll always see + yabridge's notifications, even when using more niche distros where you may not + have `notify-send` installed by default. - The new `editor_disable_host_scaling` compatibility prevents hosts from setting an explicit DPI scaling factor for the editor. In some cases this can help with inconsistent scaling when using HiDPI scaling. This option affects @@ -61,6 +65,10 @@ Versioning](https://semver.org/spec/v2.0.0.html). (, ). Building against older versions will result in memory errors. +- The Meson build now requires the `libdbus-1` package to be installed. + Yabridge's binaries will not link against the shared library, but it does use + the definitions from the headers to dynamically link against D-Bus at runtime + when it needs to send a desktop notification. ## [4.0.2] - 2022-06-27 diff --git a/ROADMAP.md b/ROADMAP.md index 84c38cc7..d602e7ae 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -16,12 +16,6 @@ page lists some of those. - An easier [updater](https://github.com/robbert-vdh/yabridge/issues/51) through a new `yabridgectl update` command for distros that don't package yabridge. -# For a major release - -- Replace the use of `notify-send` for notifications with using `libdbus` - directly. Most systems will have both available by default, but some less - common distros split `notify-send` from the rest of the `libnotify` package. - # Somewhere in the future, possibly - CLAP audio thread pool support. Implementing this efficiently is less than diff --git a/meson.build b/meson.build index cd09e821..a072b25c 100644 --- a/meson.build +++ b/meson.build @@ -241,7 +241,7 @@ else bitsery_dep = dependency('bitsery', version : '>=5.2.0') endif -# The DBus headers are also only accessed through the include path. We don't +# The D-Bus headers are also only accessed through the include path. We don't # link to libdbus-1 to make soname changes don't completely break yabridge. dbus_dep = dependency('dbus-1').partial_dependency(compile_args : true, includes : true) function2_dep = dependency('function2', version : '>=4.0.0') diff --git a/src/common/notifications.cpp b/src/common/notifications.cpp index 2f19bb71..f4cf2399 100644 --- a/src/common/notifications.cpp +++ b/src/common/notifications.cpp @@ -34,21 +34,34 @@ std::atomic libdbus_handle = nullptr; std::mutex libdbus_mutex; // We'll fetch all of these functions at runtime when send a first notification -decltype(dbus_connection_unref)* libdbus_connection_unref = nullptr; -decltype(dbus_bus_get)* libdbus_bus_get = nullptr; -decltype(dbus_error_free)* libdbus_error_free = nullptr; -decltype(dbus_error_init)* libdbus_error_init = nullptr; -decltype(dbus_error_is_set)* libdbus_error_is_set = nullptr; -decltype(dbus_connection_set_exit_on_disconnect)* - libdbus_connection_set_exit_on_disconnect = nullptr; +#define LIBDBUS_FUNCTIONS \ + X(dbus_bus_get) \ + X(dbus_connection_flush) \ + X(dbus_connection_send) \ + X(dbus_connection_set_exit_on_disconnect) \ + X(dbus_connection_unref) \ + X(dbus_error_free) \ + X(dbus_error_init) \ + X(dbus_error_is_set) \ + X(dbus_message_get_serial) \ + X(dbus_message_iter_append_basic) \ + X(dbus_message_iter_close_container) \ + X(dbus_message_iter_init_append) \ + X(dbus_message_iter_open_container) \ + X(dbus_message_new_method_call) \ + X(dbus_message_unref) + +#define X(name) decltype(name)* lib##name = nullptr; +LIBDBUS_FUNCTIONS +#undef X std::unique_ptr libdbus_connection( nullptr, libdbus_connection_unref); /** - * Try to set up DBus. Returns `false` if a function could not be resolved or if - * we could not connect to the DBus session. + * Try to set up D-Bus. Returns `false` if a function could not be resolved or + * if we could not connect to the D-Bus session. */ bool setup_libdbus() { // If this function is called from two threads at the same time, then we can @@ -68,7 +81,7 @@ bool setup_libdbus() { return false; } -#define LOAD_FUNCTION(name) \ +#define X(name) \ do { \ lib##name = \ reinterpret_cast(dlsym(handle, #name)); \ @@ -78,18 +91,13 @@ bool setup_libdbus() { "', not sending desktop notifications"); \ return false; \ } \ - } while (false) + } while (false); - LOAD_FUNCTION(dbus_connection_unref); - LOAD_FUNCTION(dbus_bus_get); - LOAD_FUNCTION(dbus_error_free); - LOAD_FUNCTION(dbus_error_init); - LOAD_FUNCTION(dbus_error_is_set); - LOAD_FUNCTION(dbus_connection_set_exit_on_disconnect); + LIBDBUS_FUNCTIONS -#undef LOAD_FUNCTION +#undef X - // With every function ready, we can try connecting to the DBus interface + // With every function ready, we can try connecting to the D-Bus interface DBusError error; libdbus_error_init(&error); @@ -97,7 +105,7 @@ bool setup_libdbus() { libdbus_bus_get(DBusBusType::DBUS_BUS_SESSION, &error)); if (libdbus_error_is_set(&error)) { assert(error.message); - logger.log("Could not connect to DBus session bus: " + + logger.log("Could not connect to D-Bus session bus: " + std::string(error.message)); libdbus_error_free(&error); @@ -119,7 +127,7 @@ bool setup_libdbus() { bool send_notification(const std::string& title, const std::string body, std::optional origin) { - // The first time this function is called we'll need to set up the DBus + // The first time this function is called we'll need to set up the D-Bus // interface. Previously yabridge relied on notify-send, but some distros // don't install that by default. if (!libdbus_handle && !setup_libdbus()) { @@ -151,20 +159,65 @@ bool send_notification(const std::string& title, } } - Process process("notify-send"); - process.arg("--urgency=normal"); - process.arg("--app-name=yabridge"); - process.arg(title); - process.arg(formatted_body.str()); + // Actually sending the notification is done directly using the D-Bus API. + std::unique_ptr message( + libdbus_message_new_method_call( + "org.freedesktop.Notifications", "/org/freedesktop/Notifications", + "org.freedesktop.Notifications", "Notify"), + libdbus_message_unref); + assert(message); - // We will have printed the message to the terminal anyways, so if the user - // doesn't have libnotify installed we'll just fail silently - const auto result = process.spawn_get_status(); - return std::visit( - overload{ - [](int status) -> bool { return status == EXIT_SUCCESS; }, - [](const Process::CommandNotFound&) -> bool { return false; }, - [](const std::error_code&) -> bool { return false; }, - }, - result); + // Can't use `dbus_message_append_args` because that doesn't support + // dictionaries + DBusMessageIter iter; + libdbus_message_iter_init_append(message.get(), &iter); + + const char* app_name = "yabridge"; + assert( + libdbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &app_name)); + + // TODO: Add the replacing thing, see if that helps prevent notifications + // from stacking up + const dbus_uint32_t replaces_id = 0; + assert(libdbus_message_iter_append_basic(&iter, DBUS_TYPE_UINT32, + &replaces_id)); + + const char* app_icon = ""; + assert( + libdbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &app_icon)); + + const char* title_cstr = title.c_str(); + assert(libdbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, + &title_cstr)); + + const std::string formatted_body_str = formatted_body.str(); + const char* formatted_body_cstr = formatted_body_str.c_str(); + assert(libdbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, + &formatted_body_cstr)); + + // Our actions array is empty + DBusMessageIter array_iter; + assert(libdbus_message_iter_open_container( + &iter, DBUS_TYPE_ARRAY, DBUS_TYPE_STRING_AS_STRING, &array_iter)); + assert(libdbus_message_iter_close_container(&iter, &array_iter)); + + // We also don't have any hints, but we can't use the simple + // `libdbus_message_append_args` API because we can't use it to add an empty + // hints dictionary + assert(libdbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}", + &array_iter)); + assert(libdbus_message_iter_close_container(&iter, &array_iter)); + + // -1 is an implementation specific default duration + const dbus_int32_t expiry_timeout = -1; + assert(libdbus_message_iter_append_basic(&iter, DBUS_TYPE_INT32, + &expiry_timeout)); + + // And after all of that we can finally send the actual notification + dbus_uint32_t message_serial = libdbus_message_get_serial(message.get()); + const bool result = libdbus_connection_send(libdbus_connection.get(), + message.get(), &message_serial); + libdbus_connection_flush(libdbus_connection.get()); + + return result; } diff --git a/src/common/notifications.h b/src/common/notifications.h index 32a60d83..486dfb9c 100644 --- a/src/common/notifications.h +++ b/src/common/notifications.h @@ -22,14 +22,11 @@ #include #include -// TODO: At some point, provide an alternative to notify-send by dlopen()-ing -// libdbus instead. Some more obscure distros won't have notify-send -// available. - /** - * Send a desktop notification using `notify-send`. Used for diagnostics when a - * plugin fails to load since the user may not be checking the output in a - * terminal. + * Send a desktop notification using the D-Bus notifications protocol + * (). + * Used for diagnostics when a plugin fails to load since the user may not be + * checking the output in a terminal. * * @param title The title (or technically, summary) of the notification. * @param body The message to display. This can contain line feeds, and it any @@ -40,7 +37,7 @@ * user can more easily navigate to the plugin's path. * * @return Whether the notification was sent. This will be false if - * `notify-send` is not available. + * `libdbus-1.so` is not available. */ bool send_notification(const std::string& title, const std::string body,