Skip to content

Commit afc9ced

Browse files
committed
fix: run libnotify actions asynchronously
1 parent 84e46f1 commit afc9ced

1 file changed

Lines changed: 111 additions & 40 deletions

File tree

src/tray_linux.cpp

Lines changed: 111 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include <memory>
1111
#include <mutex>
1212
#include <string>
13+
#include <thread>
1314
#include <utility>
1415

1516
// lib includes
@@ -21,17 +22,36 @@
2122

2223
namespace tray_linux {
2324
/**
24-
* Currently shown notification object
25+
* Notification element struct
2526
*/
26-
NotifyNotification *notification_current = nullptr; // NOSONAR(cpp:S5421) - mutable state, not const
27+
struct notification_data {
28+
/**
29+
* @brief Notification object
30+
*/
31+
NotifyNotification *obj = nullptr;
32+
/**
33+
* @brief Notification callback
34+
*/
35+
void (*cb)() = nullptr;
36+
/**
37+
* @brief Notification shown indicator
38+
*/
39+
bool shown = false;
40+
/**
41+
* @brief Notification mutex for async thread synchronization
42+
*/
43+
std::mutex mutex;
44+
};
45+
2746
/**
28-
* Currently shown notification callback
47+
* Currently shown notifications
2948
*/
30-
void (*notification_current_callback)() = nullptr; // NOSONAR(cpp:S5421) - mutable state, not const
49+
std::vector<std::shared_ptr<notification_data>> notifications; // NOSONAR(cpp:S5421) - mutable state, not const
3150
/**
32-
* Lock for currently shown notification/callback
51+
* Lock for currently shown notifications vector
3352
*/
34-
std::mutex notification_mutex; // NOSONAR(cpp:S5421) - mutable state, not const
53+
std::mutex notifications_mutex; // NOSONAR(cpp:S5421) - mutable state, not const
54+
3555
/**
3656
* QtTrayMenu instance
3757
*/
@@ -42,39 +62,77 @@ namespace tray_linux {
4262
void (*log_callback)(int, const char *) = nullptr; // NOSONAR(cpp:S5421) - mutable state, not const
4363

4464
/**
45-
* @brief Initialize notifications
46-
* @param app_name application name for notifications
47-
* @return true if successful
65+
* @brief Acknowledge notification asynchronously with timeout to avoid Dbus lockups
66+
* @param notification - Tray notification to close
67+
* @param timeout - optional timeout for async run in ms (default: 1000)
4868
*/
49-
bool init_notify(const char *app_name) {
50-
if (!notify_is_initted()) {
51-
std::scoped_lock lock(notification_mutex);
52-
return notify_init(app_name);
69+
void async_tray_notification_acknowledge_(const std::shared_ptr<notification_data> &notification, int timeout = 1000) {
70+
std::thread t([notification]() { // NOSONAR(cpp:S6168) - jthread is only available on C++20 onwards
71+
std::scoped_lock lock(notification->mutex);
72+
if (notification->shown && notification->obj != nullptr && NOTIFY_IS_NOTIFICATION(notification->obj) && notify_notification_close(notification->obj, nullptr)) {
73+
notification->shown = false;
74+
g_object_unref(G_OBJECT(notification->obj));
75+
notification->obj = nullptr;
76+
notification->cb = nullptr;
77+
}
78+
});
79+
while (notification->obj != nullptr && timeout > 0) {
80+
std::this_thread::sleep_for(std::chrono::milliseconds(10));
81+
timeout -= 10;
82+
}
83+
if (timeout > 0) {
84+
// Finished. Join thread.
85+
t.join();
86+
} else {
87+
// Timed out. Detach thread and continue.
88+
t.detach(); // NOSONAR(cpp:S5962) - Keep this running until it times out by itself (usually after 25 seconds due to DBus)
5389
}
54-
return true; // Already initialized, so init was successful
5590
}
5691

5792
/**
58-
* @brief Acknowledge/click current notification
93+
* @brief Acknowledge/click current notifications
5994
* @param run_callback - Run notification callback when acknowledging
6095
*/
61-
void acknowledge_notification(const bool run_callback = false) {
96+
void acknowledge_notifications(bool run_callback = false) {
6297
if (notify_is_initted()) {
63-
std::scoped_lock lock(notification_mutex);
64-
if (notification_current != nullptr && NOTIFY_IS_NOTIFICATION(notification_current)) {
65-
if (run_callback && notification_current_callback != nullptr) {
66-
notification_current_callback();
98+
std::scoped_lock lock(notifications_mutex);
99+
for (auto notification : notifications) {
100+
if (run_callback && notification->cb != nullptr) {
101+
notification->cb();
67102
}
68-
notify_notification_close(notification_current, nullptr);
69-
g_object_unref(G_OBJECT(notification_current));
70-
notification_current = nullptr;
71-
notification_current_callback = nullptr;
103+
async_tray_notification_acknowledge_(notification);
72104
}
105+
notifications.clear();
73106
} else if (qt_tray_menu != nullptr && QtTrayMenu::supportsMessages()) {
74107
qt_tray_menu->clickMessage();
75108
}
76109
}
77110

111+
/**
112+
* @brief Show notification asynchronously with timeout to avoid Dbus lockups
113+
* @param notification - Tray notification to show
114+
* @param timeout - optional timeout for async run in ms (default: 1000)
115+
*/
116+
void async_tray_notification_show_(const std::shared_ptr<notification_data> &notification, int timeout = 1000) {
117+
std::thread t([notification]() { // NOSONAR(cpp:S6168) - jthread is only available on C++20 onwards
118+
std::scoped_lock lock(notification->mutex);
119+
if (notification->obj != nullptr && NOTIFY_IS_NOTIFICATION(notification->obj) && notify_notification_show(notification->obj, nullptr)) {
120+
notification->shown = true;
121+
}
122+
});
123+
while (!notification->shown && timeout > 0) {
124+
std::this_thread::sleep_for(std::chrono::milliseconds(10));
125+
timeout -= 10;
126+
}
127+
if (timeout > 0) {
128+
// Finished. Join thread.
129+
t.join();
130+
} else {
131+
// Timed out. Detach thread and continue.
132+
t.detach(); // NOSONAR(cpp:S5962) - Keep this running until it times out by itself (usually after 25 seconds due to DBus)
133+
}
134+
}
135+
78136
/**
79137
* @brief Show tray notification via desktop-independent interface
80138
* @param tray Tray structure containing notification information
@@ -85,39 +143,52 @@ namespace tray_linux {
85143
}
86144
// Try to notify using libnotify
87145
if (notify_is_initted()) {
88-
if (notification_current != nullptr) {
89-
acknowledge_notification();
146+
if (!notifications.empty()) {
147+
acknowledge_notifications();
90148
}
91-
std::scoped_lock lock(notification_mutex);
149+
std::scoped_lock lock(notifications_mutex);
92150
std::filesystem::path notification_icon = tray->notification_icon != nullptr ? tray->notification_icon : tray->icon;
93151
if (std::filesystem::exists(notification_icon)) {
94152
// Use absolute path for filesystem icon files, not a relative one
95153
notification_icon = std::filesystem::absolute(notification_icon);
96154
}
97-
notification_current = notify_notification_new(tray->notification_title, tray->notification_text, notification_icon.c_str());
98-
if (notification_current != nullptr && NOTIFY_IS_NOTIFICATION(notification_current)) {
155+
auto notification = std::make_shared<struct notification_data>();
156+
notification->obj = notify_notification_new(tray->notification_title, tray->notification_text, notification_icon.c_str());
157+
if (notification->obj != nullptr && NOTIFY_IS_NOTIFICATION(notification->obj)) {
99158
if (tray->notification_cb != nullptr) {
100-
notification_current_callback = tray->notification_cb;
101-
notify_notification_add_action(notification_current, "default", "Default", NOTIFY_ACTION_CALLBACK(tray->notification_cb), nullptr, nullptr);
102-
}
103-
if (notify_notification_show(notification_current, nullptr)) {
104-
return;
159+
notification->cb = tray->notification_cb;
160+
notify_notification_add_action(notification->obj, "default", "Default", NOTIFY_ACTION_CALLBACK(tray->notification_cb), nullptr, nullptr);
105161
}
162+
notifications.emplace_back(notification);
163+
async_tray_notification_show_(notification);
106164
}
107-
}
108-
// Fallback to QtTrayMenu notification
109-
if (qt_tray_menu != nullptr && QtTrayMenu::supportsMessages()) {
165+
} else if (qt_tray_menu != nullptr && QtTrayMenu::supportsMessages()) {
166+
// Fallback to QtTrayMenu notification
110167
qt_tray_menu->showMessage(tray->notification_title, tray->notification_text, tray->notification_icon, tray->notification_cb);
111168
}
112169
}
113170

171+
/**
172+
* @brief Initialize notifications
173+
* @param app_name application name for notifications
174+
* @return true if successful
175+
*/
176+
bool init_notify(const char *app_name) {
177+
if (!notify_is_initted()) {
178+
if (!notifications.empty()) {
179+
acknowledge_notifications();
180+
}
181+
return notify_init(app_name);
182+
}
183+
return true; // Already initialized, so init was successful
184+
}
185+
114186
/**
115187
* @brief Uninitialize notifications
116188
*/
117189
void uninit_notify() {
118190
if (notify_is_initted()) {
119-
acknowledge_notification();
120-
std::scoped_lock lock(notification_mutex);
191+
acknowledge_notifications();
121192
notify_uninit();
122193
}
123194
}
@@ -251,6 +322,6 @@ extern "C" {
251322
}
252323

253324
void tray_simulate_notification_click(void) {
254-
tray_linux::acknowledge_notification(true);
325+
tray_linux::acknowledge_notifications(true);
255326
}
256327
} // extern "C"

0 commit comments

Comments
 (0)