diff --git a/cmake/compile_definitions/macos.cmake b/cmake/compile_definitions/macos.cmake index fb33d3bf2..448ad65ee 100644 --- a/cmake/compile_definitions/macos.cmake +++ b/cmake/compile_definitions/macos.cmake @@ -28,9 +28,6 @@ list(APPEND SUNSHINE_EXTERNAL_LIBRARIES set(APPLE_PLIST_FILE "${SUNSHINE_SOURCE_ASSETS_DIR}/macos/assets/Info.plist") -# todo - tray is not working on macos -set(SUNSHINE_TRAY 0) - set(PLATFORM_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/platform/macos/av_audio.h" "${CMAKE_SOURCE_DIR}/src/platform/macos/av_audio.m" diff --git a/cmake/prep/options.cmake b/cmake/prep/options.cmake index cc2e1e1f5..b1b916ac8 100644 --- a/cmake/prep/options.cmake +++ b/cmake/prep/options.cmake @@ -16,7 +16,7 @@ option(BUILD_WERROR "Enable -Werror flag." OFF) # if this option is set, the build will exit after configuring special package configuration files option(SUNSHINE_CONFIGURE_ONLY "Configure special files only, then exit." OFF) -option(SUNSHINE_ENABLE_TRAY "Enable system tray icon. This option will be ignored on macOS." ON) +option(SUNSHINE_ENABLE_TRAY "Enable system tray icon." ON) option(SUNSHINE_SYSTEM_WAYLAND_PROTOCOLS "Use system installation of wayland-protocols rather than the submodule." OFF) diff --git a/packaging/sunshine.rb b/packaging/sunshine.rb index 33eab1e7e..1c299a6ae 100644 --- a/packaging/sunshine.rb +++ b/packaging/sunshine.rb @@ -120,7 +120,6 @@ class @PROJECT_NAME@ < Formula end args << "-DCUDA_FAIL_ON_MISSING=OFF" if OS.linux? - args << "-DSUNSHINE_ENABLE_TRAY=OFF" if OS.mac? system "cmake", "-S", ".", "-B", "build", "-G", "Unix Makefiles", *std_cmake_args, diff --git a/src/main.cpp b/src/main.cpp index d7dc1d1cb..050b29904 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -88,6 +88,45 @@ WINAPI BOOL ConsoleCtrlHandler(DWORD type) { } #endif +#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 +constexpr bool tray_is_enabled = true; +#else +constexpr bool tray_is_enabled = false; +#endif + +void mainThreadLoop(const std::shared_ptr> &shutdown_event) { + bool run_loop = false; + + // Conditions that would require the main thread event loop +#ifndef _WIN32 + run_loop = tray_is_enabled; // On Windows, tray runs in separate thread, so no main loop needed for tray +#endif + + if (!run_loop) { + BOOST_LOG(info) << "No main thread features enabled, skipping event loop"sv; + return; + } + + // Main thread event loop + BOOST_LOG(info) << "Starting main loop"sv; + while (true) { + if (shutdown_event->peek()) { + BOOST_LOG(info) << "Shutdown event detected, breaking main loop"sv; + if (tray_is_enabled) { + system_tray::end_tray(); + } + break; + } + + if (tray_is_enabled) { + system_tray::process_tray_events(); + } + + // Sleep to avoid busy waiting + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } +} + int main(int argc, char *argv[]) { lifetime::argv = argv; @@ -157,7 +196,7 @@ int main(int argc, char *argv[]) { BOOST_LOG(error) << "Display device session failed to initialize"sv; } -#ifdef WIN32 +#ifdef _WIN32 // Modify relevant NVIDIA control panel settings if the system has corresponding gpu if (nvprefs_instance.load()) { // Restore global settings to the undo file left by improper termination of sunshine.exe @@ -246,11 +285,6 @@ int main(int argc, char *argv[]) { task_pool.start(1); -#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 - // create tray thread and detach it - system_tray::run_tray(); -#endif - // Create signal handler after logging has been initialized auto shutdown_event = mail::man->event(mail::shutdown); on_signal(SIGINT, [&force_shutdown, &display_device_deinit_guard, shutdown_event]() { @@ -350,7 +384,23 @@ int main(int argc, char *argv[]) { } #endif - // Wait for shutdown + if (tray_is_enabled) { + BOOST_LOG(info) << "Starting system tray"sv; +#ifdef _WIN32 + // TODO: Windows has a weird bug where when running as a service and on the first Windows boot, + // he tray icon would not appear even though Sunshine is running correctly otherwise. + // Restarting the service would allow the icon to appear normally. + // For now we will keep the Windows tray icon on a separate thread. + // Ideally, we would run the system tray on the main thread for all platforms. + system_tray::init_tray_threaded(); +#else + system_tray::init_tray(); +#endif + } + + mainThreadLoop(shutdown_event); + + // Wait for shutdown, this is not necessary when we're using the main event loop shutdown_event->view(); httpThread.join(); @@ -360,17 +410,17 @@ int main(int argc, char *argv[]) { task_pool.stop(); task_pool.join(); - // stop system tray -#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 - system_tray::end_tray(); -#endif - -#ifdef WIN32 +#ifdef _WIN32 // Restore global NVIDIA control panel settings if (nvprefs_instance.owning_undo_file() && nvprefs_instance.load()) { nvprefs_instance.restore_global_profile(); nvprefs_instance.unload(); } + + // Stop the threaded tray if it was started + if (tray_is_enabled) { + system_tray::end_tray_threaded(); + } #endif return lifetime::desired_exit_code; diff --git a/src/system_tray.cpp b/src/system_tray.cpp index 068c5266a..812483560 100644 --- a/src/system_tray.cpp +++ b/src/system_tray.cpp @@ -27,8 +27,12 @@ #endif // standard includes + #include + #include #include + #include #include + #include // lib includes #include @@ -47,38 +51,43 @@ using namespace std::literals; // system_tray namespace namespace system_tray { - static std::atomic tray_initialized = false; + static std::atomic tray_initialized = false; - void tray_open_ui_cb(struct tray_menu *item) { + // Threading variables for all platforms + static std::thread tray_thread; + static std::atomic tray_thread_running = false; + static std::atomic tray_thread_should_exit = false; + + void tray_open_ui_cb([[maybe_unused]] struct tray_menu *item) { BOOST_LOG(info) << "Opening UI from system tray"sv; launch_ui(); } - void tray_donate_github_cb(struct tray_menu *item) { + void tray_donate_github_cb([[maybe_unused]] struct tray_menu *item) { platf::open_url("https://github.com/sponsors/LizardByte"); } - void tray_donate_patreon_cb(struct tray_menu *item) { + void tray_donate_patreon_cb([[maybe_unused]] struct tray_menu *item) { platf::open_url("https://www.patreon.com/LizardByte"); } - void tray_donate_paypal_cb(struct tray_menu *item) { + void tray_donate_paypal_cb([[maybe_unused]] struct tray_menu *item) { platf::open_url("https://www.paypal.com/paypalme/ReenigneArcher"); } - void tray_reset_display_device_config_cb(struct tray_menu *item) { + void tray_reset_display_device_config_cb([[maybe_unused]] struct tray_menu *item) { BOOST_LOG(info) << "Resetting display device config from system tray"sv; std::ignore = display_device::reset_persistence(); } - void tray_restart_cb(struct tray_menu *item) { + void tray_restart_cb([[maybe_unused]] struct tray_menu *item) { BOOST_LOG(info) << "Restarting from system tray"sv; platf::restart(); } - void tray_quit_cb(struct tray_menu *item) { + void tray_quit_cb([[maybe_unused]] struct tray_menu *item) { BOOST_LOG(info) << "Quitting from system tray"sv; #ifdef _WIN32 @@ -123,7 +132,7 @@ namespace system_tray { .allIconPaths = {TRAY_ICON, TRAY_ICON_LOCKED, TRAY_ICON_PLAYING, TRAY_ICON_PAUSING}, }; - int system_tray() { + int init_tray() { #ifdef _WIN32 // If we're running as SYSTEM, Explorer.exe will not have permission to open our thread handle // to monitor for thread termination. If Explorer fails to open our thread, our tray icon @@ -189,39 +198,32 @@ namespace system_tray { if (tray_init(&tray) < 0) { BOOST_LOG(warning) << "Failed to create system tray"sv; return 1; - } else { - BOOST_LOG(info) << "System tray created"sv; } + BOOST_LOG(info) << "System tray created"sv; tray_initialized = true; - while (tray_loop(1) == 0) { - BOOST_LOG(debug) << "System tray loop"sv; + return 0; + } + + int process_tray_events() { + if (!tray_initialized) { + return 1; + } + + // Process one iteration of the tray loop with non-blocking mode (0) + if (const int result = tray_loop(0); result != 0) { + BOOST_LOG(warning) << "System tray loop failed"sv; + return result; } return 0; } - void run_tray() { - // create the system tray - #if defined(__APPLE__) || defined(__MACH__) - // macOS requires that UI elements be created on the main thread - // creating tray using dispatch queue does not work, although the code doesn't actually throw any (visible) errors - - // dispatch_async(dispatch_get_main_queue(), ^{ - // system_tray(); - // }); - - BOOST_LOG(info) << "system_tray() is not yet implemented for this platform."sv; - #else // Windows, Linux - // create tray in separate thread - std::thread tray_thread(system_tray); - tray_thread.detach(); - #endif - } - int end_tray() { - tray_initialized = false; - tray_exit(); + if (tray_initialized) { + tray_initialized = false; + tray_exit(); + } return 0; } @@ -238,10 +240,10 @@ namespace system_tray { tray_update(&tray); tray.icon = TRAY_ICON_PLAYING; tray.notification_title = "Stream Started"; - char msg[256]; - snprintf(msg, std::size(msg), "Streaming started for %s", app_name.c_str()); - tray.notification_text = msg; - tray.tooltip = msg; + + static std::string msg = std::format("Streaming started for {}", app_name); + tray.notification_text = msg.c_str(); + tray.tooltip = msg.c_str(); tray.notification_icon = TRAY_ICON_PLAYING; tray_update(&tray); } @@ -257,12 +259,12 @@ namespace system_tray { tray.notification_icon = nullptr; tray.icon = TRAY_ICON_PAUSING; tray_update(&tray); - char msg[256]; - snprintf(msg, std::size(msg), "Streaming paused for %s", app_name.c_str()); + + static std::string msg = std::format("Streaming paused for {}", app_name); tray.icon = TRAY_ICON_PAUSING; tray.notification_title = "Stream Paused"; - tray.notification_text = msg; - tray.tooltip = msg; + tray.notification_text = msg.c_str(); + tray.tooltip = msg.c_str(); tray.notification_icon = TRAY_ICON_PAUSING; tray_update(&tray); } @@ -278,12 +280,12 @@ namespace system_tray { tray.notification_icon = nullptr; tray.icon = TRAY_ICON; tray_update(&tray); - char msg[256]; - snprintf(msg, std::size(msg), "Application %s successfully stopped", app_name.c_str()); + + static std::string msg = std::format("Application {} successfully stopped", app_name); tray.icon = TRAY_ICON; tray.notification_icon = TRAY_ICON; tray.notification_title = "Application Stopped"; - tray.notification_text = msg; + tray.notification_text = msg.c_str(); tray.tooltip = PROJECT_NAME; tray_update(&tray); } @@ -310,5 +312,94 @@ namespace system_tray { tray_update(&tray); } + // Threading functions available on all platforms + static void tray_thread_worker() { + BOOST_LOG(info) << "System tray thread started"sv; + + // Initialize the tray in this thread + if (init_tray() != 0) { + BOOST_LOG(error) << "Failed to initialize tray in thread"sv; + tray_thread_running = false; + return; + } + + tray_thread_running = true; + + // Main tray event loop + while (!tray_thread_should_exit) { + if (process_tray_events() != 0) { + BOOST_LOG(warning) << "Tray event processing failed in thread"sv; + break; + } + + // Sleep to avoid busy waiting + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + + // Clean up the tray + end_tray(); + tray_thread_running = false; + BOOST_LOG(info) << "System tray thread ended"sv; + } + + int init_tray_threaded() { + if (tray_thread_running) { + BOOST_LOG(warning) << "Tray thread is already running"sv; + return 1; + } + + tray_thread_should_exit = false; + + try { + tray_thread = std::thread(tray_thread_worker); + + // Wait for the thread to start and initialize + const auto start_time = std::chrono::steady_clock::now(); + while (!tray_thread_running && !tray_thread_should_exit) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + // Timeout after 10 seconds + if (std::chrono::steady_clock::now() - start_time > std::chrono::seconds(10)) { + BOOST_LOG(error) << "Tray thread initialization timeout"sv; + tray_thread_should_exit = true; + if (tray_thread.joinable()) { + tray_thread.join(); + } + return 1; + } + } + + if (!tray_thread_running) { + BOOST_LOG(error) << "Tray thread failed to start"sv; + if (tray_thread.joinable()) { + tray_thread.join(); + } + return 1; + } + + BOOST_LOG(info) << "System tray thread initialized successfully"sv; + return 0; + } catch (const std::exception &e) { + BOOST_LOG(error) << "Failed to create tray thread: " << e.what(); + return 1; + } + } + + int end_tray_threaded() { + if (!tray_thread_running) { + return 0; + } + + BOOST_LOG(info) << "Stopping system tray thread"sv; + tray_thread_should_exit = true; + + if (tray_thread.joinable()) { + tray_thread.join(); + } + + BOOST_LOG(info) << "System tray thread stopped"sv; + return 0; + } + } // namespace system_tray #endif diff --git a/src/system_tray.h b/src/system_tray.h index 00a17f92c..b96997e95 100644 --- a/src/system_tray.h +++ b/src/system_tray.h @@ -12,56 +12,55 @@ namespace system_tray { * @brief Callback for opening the UI from the system tray. * @param item The tray menu item. */ - void tray_open_ui_cb(struct tray_menu *item); + void tray_open_ui_cb([[maybe_unused]] struct tray_menu *item); /** * @brief Callback for opening GitHub Sponsors from the system tray. * @param item The tray menu item. */ - void tray_donate_github_cb(struct tray_menu *item); + void tray_donate_github_cb([[maybe_unused]] struct tray_menu *item); /** * @brief Callback for opening Patreon from the system tray. * @param item The tray menu item. */ - void tray_donate_patreon_cb(struct tray_menu *item); + void tray_donate_patreon_cb([[maybe_unused]] struct tray_menu *item); /** * @brief Callback for opening PayPal donation from the system tray. * @param item The tray menu item. */ - void tray_donate_paypal_cb(struct tray_menu *item); + void tray_donate_paypal_cb([[maybe_unused]] struct tray_menu *item); /** * @brief Callback for resetting display device configuration. * @param item The tray menu item. */ - void tray_reset_display_device_config_cb(struct tray_menu *item); + void tray_reset_display_device_config_cb([[maybe_unused]] struct tray_menu *item); /** * @brief Callback for restarting Sunshine from the system tray. * @param item The tray menu item. */ - void tray_restart_cb(struct tray_menu *item); + void tray_restart_cb([[maybe_unused]] struct tray_menu *item); /** * @brief Callback for exiting Sunshine from the system tray. * @param item The tray menu item. */ - void tray_quit_cb(struct tray_menu *item); + void tray_quit_cb([[maybe_unused]] struct tray_menu *item); /** - * @brief Create the system tray. - * @details This function has an endless loop, so it should be run in a separate thread. - * @return 1 if the system tray failed to create, otherwise 0 once the tray has been terminated. + * @brief Initializes the system tray without starting a loop. + * @return 0 if initialization was successful, non-zero otherwise. */ - int system_tray(); + int init_tray(); /** - * @brief Run the system tray with platform specific options. - * @todo macOS requires that UI elements be created on the main thread, so the system tray is not currently implemented for macOS. + * @brief Processes a single tray event iteration. + * @return 0 if processing was successful, non-zero otherwise. */ - int run_tray(); + int process_tray_events(); /** * @brief Exit the system tray. @@ -91,4 +90,16 @@ namespace system_tray { * @brief Spawns a notification for PIN Pairing. Clicking it opens the PIN Web UI Page */ void update_tray_require_pin(); + + /** + * @brief Initializes and runs the system tray in a separate thread. + * @return 0 if initialization was successful, non-zero otherwise. + */ + int init_tray_threaded(); + + /** + * @brief Stops the threaded system tray and waits for the thread to finish. + * @return 0 after stopping the threaded tray. + */ + int end_tray_threaded(); } // namespace system_tray