3333 #include < chrono>
3434 #include < csignal>
3535 #include < format>
36+ #include < functional>
37+ #include < mutex>
3638 #include < string>
3739 #include < thread>
40+ #include < utility>
3841
3942 // lib includes
4043 #include < boost/filesystem.hpp>
@@ -55,6 +58,88 @@ using namespace std::literals;
5558namespace system_tray {
5659 static std::atomic tray_initialized = false ;
5760
61+ namespace detail {
62+ // Holds the shared state used by the async tray update workers. Packaged
63+ // in a Meyers-singleton accessor below so the mutex and the persistent
64+ // string buffers are function-local statics rather than file-scope
65+ // globals.
66+ struct tray_async_state_t {
67+ // Serializes mutation of the `tray` struct across the detached workers
68+ // spawned by update_tray_*(). tray_update() itself is internally
69+ // serialized on Linux/macOS via the tray library's own main-loop
70+ // dispatch, but we can still race on the fields of `tray` without this.
71+ std::mutex mutex;
72+
73+ // Persistent string storage for notification text. tray_update() on
74+ // Linux stores pointers into these strings and needs them to stay
75+ // valid until the next update, so they cannot be local to the worker.
76+ // Accessed only while holding `mutex`.
77+ std::string playing_msg;
78+ std::string pausing_msg;
79+ std::string stopped_msg;
80+ };
81+
82+ static tray_async_state_t &tray_async_state () {
83+ static tray_async_state_t state;
84+ return state;
85+ }
86+
87+ // Runs `fn` under the tray-async lock with exception containment. Pulled
88+ // out of the worker lambda so the lambda body is a single try/catch.
89+ static void run_tray_update_locked (const std::function<void ()> &fn) {
90+ auto &state = tray_async_state ();
91+ std::lock_guard lk (state.mutex );
92+ if (!tray_initialized) {
93+ return ;
94+ }
95+ fn ();
96+ }
97+ } // namespace detail
98+
99+ // Spawn a detached worker that performs a tray update.
100+ //
101+ // Rationale: on Linux, tray_update() synchronously waits for the GTK
102+ // main loop (i.e. the tray thread) to run the update callback — which
103+ // in turn calls libnotify and libayatana-appindicator. If the active
104+ // notification daemon is unresponsive (a common failure mode on
105+ // Wayland compositors during desktop transitions or when the daemon
106+ // crashes), the callback blocks indefinitely and the caller blocks
107+ // with it.
108+ //
109+ // The caller is frequently stream::session::join(), which arms a
110+ // 10-second NVENC-deadlock watchdog that triggers debug_trap() on
111+ // timeout. A hung notification daemon therefore ends up terminating
112+ // the entire sunshine process with SIGTRAP. See #4199.
113+ //
114+ // Running the update on a detached thread decouples the caller from
115+ // the tray subsystem: session teardown completes promptly, while
116+ // the worker stays blocked on tray_update() until the notification
117+ // daemon eventually responds (or the process exits).
118+ static void run_tray_async (std::function<void ()> fn) {
119+ if (!tray_initialized) {
120+ return ;
121+ }
122+ try {
123+ // std::jthread is used for its RAII guarantees; the thread is
124+ // immediately detached because tray_update() is allowed to block
125+ // indefinitely on a hung notification daemon and must not delay
126+ // process shutdown or the caller's critical path.
127+ std::jthread worker ([fn = std::move (fn)]() mutable {
128+ try {
129+ detail::run_tray_update_locked (fn);
130+ } catch (const std::exception &e) {
131+ BOOST_LOG (warning) << " Tray update threw: " sv << e.what ();
132+ }
133+ });
134+ worker.detach ();
135+ } catch (const std::system_error &e) {
136+ // std::jthread construction can fail with std::system_error if the
137+ // OS is out of resources; in that case we drop the update rather
138+ // than surface the failure to the caller's critical path.
139+ BOOST_LOG (warning) << " Failed to spawn tray update thread: " sv << e.what ();
140+ }
141+ }
142+
58143 void tray_open_ui_cb ([[maybe_unused]] struct tray_menu *item) {
59144 BOOST_LOG (info) << " Opening UI from system tray" sv;
60145 launch_ui ();
@@ -283,88 +368,84 @@ namespace system_tray {
283368 }
284369
285370 void update_tray_playing (std::string app_name) {
286- if (!tray_initialized) {
287- return ;
288- }
289-
290- tray.notification_title = nullptr ;
291- tray.notification_text = nullptr ;
292- tray.notification_cb = nullptr ;
293- tray.notification_icon = nullptr ;
294- tray.icon = TRAY_ICON_PLAYING;
295- tray_update (&tray);
296- tray.icon = TRAY_ICON_PLAYING;
297- tray.notification_title = " Stream Started" ;
298-
299- static std::string msg = std::format (" Streaming started for {}" , app_name);
300- tray.notification_text = msg.c_str ();
301- tray.tooltip = msg.c_str ();
302- tray.notification_icon = TRAY_ICON_PLAYING;
303- tray_update (&tray);
371+ run_tray_async ([name = std::move (app_name)]() {
372+ auto &state = detail::tray_async_state ();
373+ tray.notification_title = nullptr ;
374+ tray.notification_text = nullptr ;
375+ tray.notification_cb = nullptr ;
376+ tray.notification_icon = nullptr ;
377+ tray.icon = TRAY_ICON_PLAYING;
378+ tray_update (&tray);
379+
380+ state.playing_msg = std::format (" Streaming started for {}" , name);
381+ tray.icon = TRAY_ICON_PLAYING;
382+ tray.notification_title = " Stream Started" ;
383+ tray.notification_text = state.playing_msg .c_str ();
384+ tray.tooltip = state.playing_msg .c_str ();
385+ tray.notification_icon = TRAY_ICON_PLAYING;
386+ tray_update (&tray);
387+ });
304388 }
305389
306390 void update_tray_pausing (std::string app_name) {
307- if (!tray_initialized) {
308- return ;
309- }
310-
311- tray.notification_title = nullptr ;
312- tray.notification_text = nullptr ;
313- tray.notification_cb = nullptr ;
314- tray.notification_icon = nullptr ;
315- tray.icon = TRAY_ICON_PAUSING;
316- tray_update (&tray);
317-
318- static std::string msg = std::format (" Streaming paused for {}" , app_name);
319- tray.icon = TRAY_ICON_PAUSING;
320- tray.notification_title = " Stream Paused" ;
321- tray.notification_text = msg.c_str ();
322- tray.tooltip = msg.c_str ();
323- tray.notification_icon = TRAY_ICON_PAUSING;
324- tray_update (&tray);
391+ run_tray_async ([name = std::move (app_name)]() {
392+ auto &state = detail::tray_async_state ();
393+ tray.notification_title = nullptr ;
394+ tray.notification_text = nullptr ;
395+ tray.notification_cb = nullptr ;
396+ tray.notification_icon = nullptr ;
397+ tray.icon = TRAY_ICON_PAUSING;
398+ tray_update (&tray);
399+
400+ state.pausing_msg = std::format (" Streaming paused for {}" , name);
401+ tray.icon = TRAY_ICON_PAUSING;
402+ tray.notification_title = " Stream Paused" ;
403+ tray.notification_text = state.pausing_msg .c_str ();
404+ tray.tooltip = state.pausing_msg .c_str ();
405+ tray.notification_icon = TRAY_ICON_PAUSING;
406+ tray_update (&tray);
407+ });
325408 }
326409
327410 void update_tray_stopped (std::string app_name) {
328- if (!tray_initialized) {
329- return ;
330- }
331-
332- tray.notification_title = nullptr ;
333- tray.notification_text = nullptr ;
334- tray.notification_cb = nullptr ;
335- tray.notification_icon = nullptr ;
336- tray.icon = TRAY_ICON;
337- tray_update (&tray);
338-
339- static std::string msg = std::format (" Application {} successfully stopped" , app_name);
340- tray.icon = TRAY_ICON;
341- tray.notification_icon = TRAY_ICON;
342- tray.notification_title = " Application Stopped" ;
343- tray.notification_text = msg.c_str ();
344- tray.tooltip = PROJECT_NAME;
345- tray_update (&tray);
411+ run_tray_async ([name = std::move (app_name)]() {
412+ auto &state = detail::tray_async_state ();
413+ tray.notification_title = nullptr ;
414+ tray.notification_text = nullptr ;
415+ tray.notification_cb = nullptr ;
416+ tray.notification_icon = nullptr ;
417+ tray.icon = TRAY_ICON;
418+ tray_update (&tray);
419+
420+ state.stopped_msg = std::format (" Application {} successfully stopped" , name);
421+ tray.icon = TRAY_ICON;
422+ tray.notification_icon = TRAY_ICON;
423+ tray.notification_title = " Application Stopped" ;
424+ tray.notification_text = state.stopped_msg .c_str ();
425+ tray.tooltip = PROJECT_NAME;
426+ tray_update (&tray);
427+ });
346428 }
347429
348430 void update_tray_require_pin () {
349- if (!tray_initialized) {
350- return ;
351- }
352-
353- tray.notification_title = nullptr ;
354- tray.notification_text = nullptr ;
355- tray.notification_cb = nullptr ;
356- tray.notification_icon = nullptr ;
357- tray.icon = TRAY_ICON;
358- tray_update (&tray);
359- tray.icon = TRAY_ICON;
360- tray.notification_title = " Incoming Pairing Request" ;
361- tray.notification_text = " Click here to complete the pairing process" ;
362- tray.notification_icon = TRAY_ICON_LOCKED;
363- tray.tooltip = PROJECT_NAME;
364- tray.notification_cb = []() {
365- launch_ui (" /pin" );
366- };
367- tray_update (&tray);
431+ run_tray_async ([]() {
432+ tray.notification_title = nullptr ;
433+ tray.notification_text = nullptr ;
434+ tray.notification_cb = nullptr ;
435+ tray.notification_icon = nullptr ;
436+ tray.icon = TRAY_ICON;
437+ tray_update (&tray);
438+
439+ tray.icon = TRAY_ICON;
440+ tray.notification_title = " Incoming Pairing Request" ;
441+ tray.notification_text = " Click here to complete the pairing process" ;
442+ tray.notification_icon = TRAY_ICON_LOCKED;
443+ tray.tooltip = PROJECT_NAME;
444+ tray.notification_cb = []() {
445+ launch_ui (" /pin" );
446+ };
447+ tray_update (&tray);
448+ });
368449 }
369450
370451 // Threading functions available on all platforms
0 commit comments