/** * @file src/system_tray.cpp * @brief Definitions for the system tray icon and notification system. */ // macros #if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 #if defined(_WIN32) #define WIN32_LEAN_AND_MEAN #include #include #define TRAY_ICON WEB_DIR "images/sunshine.ico" #define TRAY_ICON_PLAYING WEB_DIR "images/sunshine-playing.ico" #define TRAY_ICON_PAUSING WEB_DIR "images/sunshine-pausing.ico" #define TRAY_ICON_LOCKED WEB_DIR "images/sunshine-locked.ico" #elif defined(__linux__) || defined(linux) || defined(__linux) #define TRAY_ICON SUNSHINE_TRAY_PREFIX "-tray" #define TRAY_ICON_PLAYING SUNSHINE_TRAY_PREFIX "-playing" #define TRAY_ICON_PAUSING SUNSHINE_TRAY_PREFIX "-pausing" #define TRAY_ICON_LOCKED SUNSHINE_TRAY_PREFIX "-locked" #elif defined(__APPLE__) || defined(__MACH__) #define TRAY_ICON WEB_DIR "images/logo-sunshine-16.png" #define TRAY_ICON_PLAYING WEB_DIR "images/sunshine-playing-16.png" #define TRAY_ICON_PAUSING WEB_DIR "images/sunshine-pausing-16.png" #define TRAY_ICON_LOCKED WEB_DIR "images/sunshine-locked-16.png" #include #endif // standard includes #include #include #include #include #include #include // lib includes #include #include #include // local includes #include "confighttp.h" #include "display_device.h" #include "logging.h" #include "platform/common.h" #include "process.h" #include "src/entry_handler.h" using namespace std::literals; // system_tray namespace namespace system_tray { static std::atomic tray_initialized = false; // 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([[maybe_unused]] struct tray_menu *item) { platf::open_url("https://github.com/sponsors/LizardByte"); } void tray_donate_patreon_cb([[maybe_unused]] struct tray_menu *item) { platf::open_url("https://www.patreon.com/LizardByte"); } 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([[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([[maybe_unused]] struct tray_menu *item) { BOOST_LOG(info) << "Restarting from system tray"sv; platf::restart(); } void tray_quit_cb([[maybe_unused]] struct tray_menu *item) { BOOST_LOG(info) << "Quitting from system tray"sv; #ifdef _WIN32 // If we're running in a service, return a special status to // tell it to terminate too, otherwise it will just respawn us. if (GetConsoleWindow() == nullptr) { lifetime::exit_sunshine(ERROR_SHUTDOWN_IN_PROGRESS, true); return; } #endif lifetime::exit_sunshine(0, true); } // Tray menu static struct tray tray = { .icon = TRAY_ICON, .tooltip = PROJECT_NAME, .menu = (struct tray_menu[]) { // todo - use boost/locale to translate menu strings {.text = "Open Sunshine", .cb = tray_open_ui_cb}, {.text = "-"}, {.text = "Donate", .submenu = (struct tray_menu[]) { {.text = "GitHub Sponsors", .cb = tray_donate_github_cb}, {.text = "Patreon", .cb = tray_donate_patreon_cb}, {.text = "PayPal", .cb = tray_donate_paypal_cb}, {.text = nullptr} }}, {.text = "-"}, // Currently display device settings are only supported on Windows #ifdef _WIN32 {.text = "Reset Display Device Config", .cb = tray_reset_display_device_config_cb}, #endif {.text = "Restart", .cb = tray_restart_cb}, {.text = "Quit", .cb = tray_quit_cb}, {.text = nullptr} }, .iconPathCount = 4, .allIconPaths = {TRAY_ICON, TRAY_ICON_LOCKED, TRAY_ICON_PLAYING, TRAY_ICON_PAUSING}, }; 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 // will persist forever if we terminate unexpectedly. To avoid this, we will modify our thread // DACL to add an ACE that allows SYNCHRONIZE access to Everyone. { PACL old_dacl; PSECURITY_DESCRIPTOR sd; auto error = GetSecurityInfo(GetCurrentThread(), SE_KERNEL_OBJECT, DACL_SECURITY_INFORMATION, nullptr, nullptr, &old_dacl, nullptr, &sd); if (error != ERROR_SUCCESS) { BOOST_LOG(warning) << "GetSecurityInfo() failed: "sv << error; return 1; } auto free_sd = util::fail_guard([sd]() { LocalFree(sd); }); SID_IDENTIFIER_AUTHORITY sid_authority = SECURITY_WORLD_SID_AUTHORITY; PSID world_sid; if (!AllocateAndInitializeSid(&sid_authority, 1, SECURITY_WORLD_RID, 0, 0, 0, 0, 0, 0, 0, &world_sid)) { error = GetLastError(); BOOST_LOG(warning) << "AllocateAndInitializeSid() failed: "sv << error; return 1; } auto free_sid = util::fail_guard([world_sid]() { FreeSid(world_sid); }); EXPLICIT_ACCESS ea {}; ea.grfAccessPermissions = SYNCHRONIZE; ea.grfAccessMode = GRANT_ACCESS; ea.grfInheritance = NO_INHERITANCE; ea.Trustee.TrusteeForm = TRUSTEE_IS_SID; ea.Trustee.ptstrName = (LPSTR) world_sid; PACL new_dacl; error = SetEntriesInAcl(1, &ea, old_dacl, &new_dacl); if (error != ERROR_SUCCESS) { BOOST_LOG(warning) << "SetEntriesInAcl() failed: "sv << error; return 1; } auto free_new_dacl = util::fail_guard([new_dacl]() { LocalFree(new_dacl); }); error = SetSecurityInfo(GetCurrentThread(), SE_KERNEL_OBJECT, DACL_SECURITY_INFORMATION, nullptr, nullptr, new_dacl, nullptr); if (error != ERROR_SUCCESS) { BOOST_LOG(warning) << "SetSecurityInfo() failed: "sv << error; return 1; } } // Wait for the shell to be initialized before registering the tray icon. // This ensures the tray icon works reliably after a logoff/logon cycle. while (GetShellWindow() == nullptr) { Sleep(1000); } #endif if (tray_init(&tray) < 0) { BOOST_LOG(warning) << "Failed to create system tray"sv; return 1; } BOOST_LOG(info) << "System tray created"sv; tray_initialized = true; 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; } int end_tray() { if (tray_initialized) { tray_initialized = false; tray_exit(); } return 0; } void update_tray_playing(std::string app_name) { if (!tray_initialized) { return; } tray.notification_title = nullptr; tray.notification_text = nullptr; tray.notification_cb = nullptr; tray.notification_icon = nullptr; tray.icon = TRAY_ICON_PLAYING; tray_update(&tray); tray.icon = TRAY_ICON_PLAYING; tray.notification_title = "Stream Started"; 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); } void update_tray_pausing(std::string app_name) { if (!tray_initialized) { return; } tray.notification_title = nullptr; tray.notification_text = nullptr; tray.notification_cb = nullptr; tray.notification_icon = nullptr; tray.icon = TRAY_ICON_PAUSING; tray_update(&tray); 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.c_str(); tray.tooltip = msg.c_str(); tray.notification_icon = TRAY_ICON_PAUSING; tray_update(&tray); } void update_tray_stopped(std::string app_name) { if (!tray_initialized) { return; } tray.notification_title = nullptr; tray.notification_text = nullptr; tray.notification_cb = nullptr; tray.notification_icon = nullptr; tray.icon = TRAY_ICON; tray_update(&tray); 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.c_str(); tray.tooltip = PROJECT_NAME; tray_update(&tray); } void update_tray_require_pin() { if (!tray_initialized) { return; } tray.notification_title = nullptr; tray.notification_text = nullptr; tray.notification_cb = nullptr; tray.notification_icon = nullptr; tray.icon = TRAY_ICON; tray_update(&tray); tray.icon = TRAY_ICON; tray.notification_title = "Incoming Pairing Request"; tray.notification_text = "Click here to complete the pairing process"; tray.notification_icon = TRAY_ICON_LOCKED; tray.tooltip = PROJECT_NAME; tray.notification_cb = []() { launch_ui("/pin"); }; 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